diff --git a/repos/effect/.agents/skills/grill-me/SKILL.md b/repos/effect/.agents/skills/grill-me/SKILL.md new file mode 100644 index 0000000..d65f67b --- /dev/null +++ b/repos/effect/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,43 @@ +--- +name: grill-me +description: Interview the user about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me about every aspect of this plan until we reach a shared understanding and a defensible design. + +Ask exactly one question at a time, then wait for my answer before asking the next question. + +Use each answer to choose the next highest-leverage unresolved question. Maintain an implicit decision tree of resolved decisions, open questions, assumptions, dependencies, risks, and rejected alternatives. + +For each question, include: + +- clear answer options when appropriate +- your recommended answer, marked as recommended +- a brief reason for the recommendation + +Use open-ended questions when fixed options would prematurely constrain the design space. + +Challenge vague, inconsistent, risky, or unsupported assumptions. If an answer creates a contradiction or unresolved dependency, ask a follow-up before moving on. + +Cover, as relevant: + +- goals and non-goals +- users and stakeholders +- constraints +- alternatives +- APIs and interfaces +- data model +- error handling +- security +- observability +- testing +- migration and rollout +- failure modes +- operational ownership +- success criteria + +If repository facts are needed, inspect the codebase instead of asking the user. Do not ask me to provide information that can be determined locally. + +When an available user-input tool such as `request_user_input` fits the question, use it to ask one short question with a small set of mutually exclusive options. Otherwise, ask in plain text and present clear possible answers as a numbered list when that helps me answer quickly. Include your recommended option and mark it as recommended. + +Stop when the major branches of the design tree have been resolved. Then summarize the agreed design, remaining risks, assumptions, rejected alternatives, and next steps. diff --git a/repos/effect/.agents/skills/jsdocs/SKILL.md b/repos/effect/.agents/skills/jsdocs/SKILL.md new file mode 100644 index 0000000..10b368b --- /dev/null +++ b/repos/effect/.agents/skills/jsdocs/SKILL.md @@ -0,0 +1,203 @@ +--- +name: jsdocs +description: Write, insert, or update Effect public API JSDoc so it satisfies the jsdocs oxlint rule. Use when adding or fixing JSDoc comments, resolving jsdocs diagnostics, preparing docs for JSON extraction, or reviewing public API documentation. +--- + +Use this skill to write well-formed JSDoc for Effect public APIs. + +## Workflow + +When updating public API JSDoc: + +1. Inspect the declaration, implementation, nearby tests, and nearby JSDoc before editing. +2. Decide whether the task is a single API fix or a module refinement pass. +3. Rewrite comments into the required documentation shape while preserving correct facts and examples. +4. For module refinements, complex APIs, or APIs with related alternatives, run the `@see` and `**Gotchas**` audits. +5. Run the narrowest relevant validation. + +## Required documentation shape + +Use a normal multiline JSDoc comment in TypeScript source: + +```ts +/** + * Short description as one paragraph. + * + * **When to use** + * + * Optional practical usage guidance. + * + * **Details** + * + * Optional details for complex APIs, options, overloads, or behavior. + * + * **Gotchas** + * + * Optional edge cases, footguns, or surprising behavior. + * + * **Example** (Short title) + * + * Optional prose explaining the example. + * + * ```ts + * const result = example() + * ``` + * + * @category constructors + * @since 1.0.0 + */ +``` + +## Prose Rules + +- Use sober, practical prose. +- Write all public JSDoc prose in English. +- Do not use jargon when a plain word works. +- Do not be clever. +- Do not add filler sections. +- The short description is required and must be exactly one paragraph. +- Make the short description stand on its own. Do not rely on `**When to use**` + to make the API understandable. +- For functions and methods, prefer present-tense, action-first prose such as + `Creates`, `Returns`, `Checks`, `Provides`, `Represents`, `Converts`, + `Decodes`, or `Formats`. +- For technical value exports, use consistent noun forms such as `Schema for`, + `Layer that`, `Service that`, `Context reference that`, or + `Constructors and matchers for`. +- Avoid leading `A` or `An` for canonical technical nouns when the surrounding + module uses a standard noun family, for example prefer `Schema for ...` over + `A schema for ...`. +- Do not describe implementation mechanics when a public concept is clearer. + For example, prefer `Constructors and matchers for ...` over wording that + only says an API uses `Data.taggedEnum`. +- Avoid generic purity or non-mutation remarks unless they document a real + surprise, caveat, or meaningful contrast with a mutating-looking API. +- Optional sections must appear in this order: + 1. `**When to use**` + 2. `**Details**` + 3. `**Gotchas**` +- Include an optional section only when it has useful, non-empty content. +- Prefer prose over bullet lists for single-item `**Details**`, `**When to use**`, or `**Gotchas**` sections. Use bullets only when there are two or more parallel facts, options, cases, or caveats. +- `**When to use**` describes the positive use case for the documented API. Do not use it as a routing section for sibling APIs. If neighboring APIs need to be mentioned, put that boundary in `@see` tag text instead. +- `**When to use**` is important when the API has close alternatives, trade-offs, or `@see` tags. If `@see` tags are present, inspect the referenced APIs and add `**When to use**` when it clarifies the documented API's own use case. +- `**When to use**` must start with one of these practical guidance forms: `Use to`, `Use when`, `Use as`, or `Use with`. Avoid bullet lists and vague openers such as `Use this...` or `Useful for...`. +- Prefer reader-centered `**When to use**` wording, especially `Use when you ...`, + when the sentence describes a user's goal. Avoid third-person noun-phrase + subjects such as `the input is ...`, `a service needs ...`, or + `values should ...` when they would become awkward in generated prompts. +- A good `**When to use**` sentence should still read naturally if reused as + a user intent prompt, for example after `I need ...` or `I have ...`. +- Keep `short` and `**When to use**` distinct: the short description says what + the API is or does; `**When to use**` says when to choose it. +- Add internal `@see` tags only for semantically useful related public APIs. +- Write `@see` tag text as normal prose after the link; no special separator is required. Prefer forms like `@see {@link otherApi} for ...` when a short explanation helps. +- Use exactly one blank line between the short description, sections, examples, and tags. +- Do not use Markdown headings such as `# Heading` or ad hoc bold headings such as `**Notes**`; only the standard headings are allowed. +- Examples must use `**Example** (Title)`, optional prose, and exactly one non-empty `ts` code fence. +- Example titles must be unique after trimming and lowercasing. +- Example titles should be short use-case phrases, not generic labels. +- Prefer gerund or action-noun titles that read naturally after `for`, for + example `Parsing JSON`, `Creating a scoped runtime`, or `Comparing structs`. +- Avoid imperative titles such as `Parse JSON`, vague labels such as `Syntax` + or `Basic usage`, and title-cased fragments such as `String Ordering`. +- Preserve canonical technical capitalization inside the phrase, such as + `Option`, `Effect`, `Schema`, `DateTime`, `HashMap`, `Base64`, and `JSON`. +- For multiple examples on the same API, make each title describe the distinct + use case shown by that example. +- Prefer examples with stable, deterministic output. Avoid assertions or + `console.log` comments that depend on stack traces, object inspection, + `Error` formatting, concurrency order, timing, randomness, or + environment-specific formatting. Examples may assume Node.js console + formatting. Direct `Set` / `Map` output is acceptable when insertion order is + deterministic and the expected output uses Node's format; otherwise + demonstrate a stable property instead. +- Do not use `@example`. +- Do not put TypeScript code fences outside `**Example** (Title)` sections. +- Inline `{@link Symbol}` targets must resolve to TypeScript symbols; do not link to URLs with `{@link}`. +- Avoid overlinking in prose. Use `{@link Symbol}` only when navigation to + that symbol helps the reader choose or understand the API. For the API being + documented, the module's central type, nearby obvious names, or repeated + mentions, prefer plain code formatting such as `Cause`, `Effect`, or + `Context`. +- Do not document module-level comments; module JSDoc is ignored by this rule. +- `@internal` means the item is ignored; do not rewrite it as public docs. +- Default exports are ignored by this rule and do not need JSDoc. +- Do not add unsupported constructs such as enums or empty exports in checked files. +- For low-level public values, prefer accurate categories such as `symbols`, + `type IDs`, or `prototypes` over compensating with verbose descriptions. + +## Tag rules + +When multiple tags are present, keep them in this order: + +1. `@deprecated` +2. `@default` +3. `@see` +4. `@category` +5. `@since` + +Tag requirements by declaration kind: + +- Root declarations require `@category` and stable-semver `@since`, and must + not use `@default`. +- Namespaces and declarations inside namespaces require stable-semver `@since`, + may use `@category`, and must not use `@default`. +- Member JSDoc is optional. When present, it follows the same prose and layout + rules, may use optional stable-semver `@since`, may use non-empty `@default`, + and must not use `@category`. +- Any declaration may use `@deprecated` with a non-empty message and repeated + non-empty `@see` tags for semantically useful related public APIs. + +## Updating existing JSDoc + +When fixing or updating existing docs: + +1. Preserve correct facts and examples. +2. Rewrite the layout into the standard template. +3. Move usage guidance into `**When to use**`, behavior details into `**Details**`, and real caveats into `**Gotchas**`. +4. Convert `@example` tags and loose `ts` fences into `**Example** (Title)` sections. +5. Preserve valid `@see`, `@deprecated`, `@default`, `@category`, and `@since` tags. +6. Remove `@see` tags that do not point to semantically useful related public APIs. +7. Replace redundant inline `{@link ...}` tags with plain code formatting when + the link target is already obvious from the current declaration or module. +8. Remove sections that would be empty. + +## Module refinement + +When asked to refine an existing module: + +1. First scan the module for local documentation patterns, repeated API families, and category conventions. +2. Keep the change focused on documentation quality unless the user also asked for rule or source changes. +3. Prefer improving existing comments over rewriting every comment into a new voice. +4. Preserve examples unless they are wrong, stale, nondeterministic, or fail + the required documentation shape. +5. Apply the `@see` and `**Gotchas**` audits across the module before finishing. + +## See audit + +When refining an existing public API module, always do a dedicated `@see` pass: + +1. Inspect existing `@see` tags and referenced APIs before keeping, changing, or removing them. +2. Look for close alternatives in the same module or API family when the documented API is one of several ways to do similar work. +3. Keep or add `@see` only when the linked API is semantically useful to understand the documented API. +4. Good `@see` targets include sibling APIs, alternatives, inverse operations, lower-level or higher-level variants, complementary operations, and closely returned, consumed, or configured types/values. +5. Do not use `@see` for implementation dependencies, broad concepts, external background links, APIs that merely share a word or name, helper APIs used only inside examples, undocumented/private members, or APIs that are only generally compatible. +6. When `@see` tags are kept or added, include `**When to use**` guidance if the documented API's own use case is not obvious from the short description. Keep comparisons with sibling APIs in the `@see` tag text. + +## Gotchas audit + +When refining an existing public API module, always do a dedicated `**Gotchas**` pass: + +1. Scan existing prose for caveat language: warnings, exceptions, limitations, preconditions, special cases, or behavior that is easy to misuse. +2. Inspect the implementation and nearby tests for behavior that is not obvious from the type signature or short description. +3. Move real caveats from `**Details**` into `**Gotchas**` when they describe edge cases, footguns, preconditions, surprising behavior, or important failure modes. +4. Add `**Gotchas**` only when the caveat is concrete and useful to a reader choosing or using the API. +5. If no gotchas are added during a refinement pass, state that a gotchas audit was performed and why no caveats were worth documenting. + +## Validation + +Run the narrowest validation that matches the change: + +- For JSDoc or example changes in a package with generated docs, run `pnpm docgen` from that package directory. +- Run `pnpm lint` because the linter includes the custom rule that checks public API JSDoc. +- Do not run broad validation for prose-only skill edits. diff --git a/repos/effect/.agents/skills/scratchpad/SKILL.md b/repos/effect/.agents/skills/scratchpad/SKILL.md new file mode 100644 index 0000000..b42bdc8 --- /dev/null +++ b/repos/effect/.agents/skills/scratchpad/SKILL.md @@ -0,0 +1,44 @@ +--- +name: scratchpad +description: Extract the JSDoc example nearest the active source selection or cursor into ./scratchpad as a TypeScript file. Use when the user asks to dump, copy, open, or try a source example in scratchpad. +--- + +Use this skill to create a scratchpad TypeScript file from the JSDoc `**Example**` +nearest the user's active source cursor or selection. + +## Workflow + +1. Determine the source path and line: + - Use the IDE active file and selection/cursor line when present. + - Use an explicit file and line when the user provides them. + - If no line or selection is available, ask for it. +2. Run: + + ```sh + node .agents/skills/scratchpad/scripts/extract-example.mjs + ``` + +3. If the script exits with code 2 because there is no obvious runner, ask the + user whether to preserve the example exactly, name an Effect value to run, or + cancel. + - To preserve exactly, rerun with `--mode preserve`. + - To run a specific Effect value, rerun with `--runner `. +4. Report the created path as a clickable file link. This is the deterministic + way to open it in the code pane. +5. Do not run the scratchpad file unless the user explicitly asks. + +## Behavior + +- The script chooses the example whose `**Example**` section contains the line; + otherwise it chooses the first following example; otherwise the nearest + previous example. +- Filenames are derived from the source file and example title, for example + `scratchpad/Schedule-retrying-and-repeating-effects.ts`. +- Existing files are not overwritten. The script appends a numeric suffix. +- In auto mode, if a top-level `program` binding exists, the script appends: + + ```ts + Effect.runPromise(program).then(console.log, console.error) + ``` + +- If the example already contains an Effect runner, the script preserves it. diff --git a/repos/effect/.agents/skills/scratchpad/agents/openai.yaml b/repos/effect/.agents/skills/scratchpad/agents/openai.yaml new file mode 100644 index 0000000..f83a2a4 --- /dev/null +++ b/repos/effect/.agents/skills/scratchpad/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Scratchpad" + short_description: "Extract examples into scratchpad" + default_prompt: "Use $scratchpad to extract the active JSDoc example into scratchpad." + +policy: + allow_implicit_invocation: true diff --git a/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs b/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs new file mode 100644 index 0000000..e668122 --- /dev/null +++ b/repos/effect/.agents/skills/scratchpad/scripts/extract-example.mjs @@ -0,0 +1,260 @@ +#!/usr/bin/env node + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { basename, extname, isAbsolute, join, relative, resolve } from "node:path" + +const usage = `Usage: + node .agents/skills/scratchpad/scripts/extract-example.mjs [--mode auto|preserve] [--runner ] [--out-dir ] + +Examples: + node .agents/skills/scratchpad/scripts/extract-example.mjs packages/effect/src/Schedule.ts 9 + node .agents/skills/scratchpad/scripts/extract-example.mjs packages/effect/src/Schedule.ts 9 --mode preserve + node .agents/skills/scratchpad/scripts/extract-example.mjs packages/effect/src/Schedule.ts 9 --runner myProgram +` + +const args = process.argv.slice(2) +const sourcePath = args[0] +const lineInput = args[1] +let mode = "auto" +let runner = undefined +let outDir = "scratchpad" + +for (let index = 2; index < args.length; index++) { + const arg = args[index] + if (arg === "--mode") { + mode = args[++index] + } else if (arg === "--runner") { + runner = args[++index] + } else if (arg === "--out-dir") { + outDir = args[++index] + } else { + fail(`Unknown option: ${arg}`) + } +} + +if (!sourcePath || !lineInput) { + fail(usage) +} + +if (mode !== "auto" && mode !== "preserve") { + fail(`Invalid --mode: ${mode}`) +} + +if (runner !== undefined && !/^[A-Za-z_$][\w$]*$/.test(runner)) { + fail(`Invalid --runner identifier: ${runner}`) +} + +const line = Number.parseInt(lineInput, 10) + +if (!Number.isSafeInteger(line) || line < 1) { + fail(`Invalid line number: ${lineInput}`) +} + +const resolvedSourcePath = resolve(sourcePath) +const source = readFileSync(resolvedSourcePath, "utf8") +const sourceLines = source.split(/\r?\n/) +const examples = findExamples(sourceLines) + +if (examples.length === 0) { + fail(`No JSDoc examples found in ${sourcePath}`) +} + +const example = chooseExample(examples, line) +const hasRunner = /\bEffect\.run[A-Za-z]*\s*\(/.test(example.code) +const programRunner = /^\s*(?:export\s+)?(?:const|let|var)\s+program\s*=/m.test(example.code) + +let code = example.code.trimEnd() +let runnerStatus = "none" + +if (runner !== undefined) { + code = appendRunner(code, runner) + runnerStatus = `appended:${runner}` +} else if (mode === "auto") { + if (hasRunner) { + runnerStatus = "already-present" + } else if (programRunner) { + code = appendRunner(code, "program") + runnerStatus = "appended:program" + } else { + const payload = { + status: "needs-runner", + title: example.title, + sourcePath: displayPath(resolvedSourcePath), + titleLine: example.titleLine, + codeStartLine: example.codeStartLine, + codeEndLine: example.codeEndLine + } + process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`) + process.exit(2) + } +} + +mkdirSync(outDir, { recursive: true }) + +const outputPath = uniqueOutputPath(outDir, resolvedSourcePath, example.title) +writeFileSync(outputPath, `${code}\n`, "utf8") + +process.stdout.write( + `${JSON.stringify( + { + outputPath: displayPath(resolve(outputPath)), + title: example.title, + sourcePath: displayPath(resolvedSourcePath), + titleLine: example.titleLine, + codeStartLine: example.codeStartLine, + codeEndLine: example.codeEndLine, + runner: runnerStatus + }, + null, + 2 + )}\n` +) + +function findExamples(lines) { + const examples = [] + let blockStart = -1 + let block = [] + + for (let index = 0; index < lines.length; index++) { + const line = lines[index] + + if (blockStart === -1 && line.includes("/**")) { + blockStart = index + block = [line] + if (line.includes("*/")) { + collectExamples(examples, block, blockStart) + blockStart = -1 + } + continue + } + + if (blockStart !== -1) { + block.push(line) + if (line.includes("*/")) { + collectExamples(examples, block, blockStart) + blockStart = -1 + } + } + } + + return examples +} + +function collectExamples(examples, block, blockStart) { + const cleaned = block.map(cleanJSDocLine) + + for (let index = 0; index < cleaned.length; index++) { + const line = cleaned[index] + const titleMatch = line.match(/\*\*Example\*\*(?:\s*\(([^)]+)\))?/) + + if (titleMatch === null) { + continue + } + + const title = titleMatch[1]?.trim() || `example-${blockStart + index + 1}` + const fenceStart = findFenceStart(cleaned, index + 1) + + if (fenceStart === -1) { + continue + } + + const fenceEnd = findFenceEnd(cleaned, fenceStart + 1) + + if (fenceEnd === -1) { + continue + } + + examples.push({ + title, + titleLine: blockStart + index + 1, + codeStartLine: blockStart + fenceStart + 2, + codeEndLine: blockStart + fenceEnd, + code: cleaned.slice(fenceStart + 1, fenceEnd).join("\n") + }) + + index = fenceEnd + } +} + +function findFenceStart(lines, startIndex) { + for (let index = startIndex; index < lines.length; index++) { + const trimmed = lines[index].trim() + + if (trimmed.startsWith("**Example**")) { + return -1 + } + + if (/^```(?:ts|typescript)?\s*$/.test(trimmed)) { + return index + } + } + + return -1 +} + +function findFenceEnd(lines, startIndex) { + for (let index = startIndex; index < lines.length; index++) { + if (lines[index].trim() === "```") { + return index + } + } + + return -1 +} + +function cleanJSDocLine(line) { + return line.replace(/^\s*\/\*\*\s?/, "").replace(/^\s*\*\/\s?$/, "").replace(/^\s*\* ?/, "") +} + +function chooseExample(examples, line) { + const containing = examples.find((example) => example.titleLine <= line && line <= example.codeEndLine) + + if (containing !== undefined) { + return containing + } + + const following = examples.find((example) => line < example.titleLine) + + if (following !== undefined) { + return following + } + + return examples[examples.length - 1] +} + +function appendRunner(code, identifier) { + return `${code.trimEnd()}\n\nEffect.runPromise(${identifier}).then(console.log, console.error)` +} + +function uniqueOutputPath(directory, source, title) { + const sourceName = basename(source, extname(source)) + const titleSlug = slug(title) || "example" + const base = `${sourceName}-${titleSlug}` + let candidate = join(directory, `${base}.ts`) + let suffix = 2 + + while (existsSync(candidate)) { + candidate = join(directory, `${base}-${suffix}.ts`) + suffix++ + } + + return candidate +} + +function slug(value) { + return value + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") +} + +function displayPath(path) { + return isAbsolute(path) ? relative(process.cwd(), path) || "." : path +} + +function fail(message) { + process.stderr.write(`${message}\n`) + process.exit(1) +} diff --git a/repos/effect/.changeset/add-adaptive-rate-limiter-store.md b/repos/effect/.changeset/add-adaptive-rate-limiter-store.md new file mode 100644 index 0000000..c191add --- /dev/null +++ b/repos/effect/.changeset/add-adaptive-rate-limiter-store.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add adaptive consume and feedback operations to the unstable persistent RateLimiterStore API, including in-memory and Redis-backed bounded cooldown, learning, learned pacing, and expiry behavior for 429 Retry-After feedback. diff --git a/repos/effect/.changeset/add-bigdecimal-sumall-multiplyall.md b/repos/effect/.changeset/add-bigdecimal-sumall-multiplyall.md new file mode 100644 index 0000000..3d3a695 --- /dev/null +++ b/repos/effect/.changeset/add-bigdecimal-sumall-multiplyall.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Added `BigDecimal.sumAll` and `BigDecimal.multiplyAll` for feature parity with `Number` and `BigInt`, closes #1880. diff --git a/repos/effect/.changeset/add-chunk-schema.md b/repos/effect/.changeset/add-chunk-schema.md new file mode 100644 index 0000000..890e0fc --- /dev/null +++ b/repos/effect/.changeset/add-chunk-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `Chunk` schema, closes #1585. diff --git a/repos/effect/.changeset/add-command-hidden.md b/repos/effect/.changeset/add-command-hidden.md new file mode 100644 index 0000000..34754c5 --- /dev/null +++ b/repos/effect/.changeset/add-command-hidden.md @@ -0,0 +1,19 @@ +--- +"effect": patch +--- + +Add `Command.withHidden` to hide subcommands from `--help` output, shell completions, and "did you mean?" suggestions, while keeping them fully invocable by exact name. + +Useful for experimental or internal subcommands that should be accepted but not advertised on the public CLI surface. + +```ts +import { Command } from "effect/unstable/cli" + +const experimental = Command.make("experimental").pipe( + Command.withHidden +) + +const root = Command.make("mycli").pipe( + Command.withSubcommands([experimental]) +) +``` diff --git a/repos/effect/.changeset/add-config-nested.md b/repos/effect/.changeset/add-config-nested.md new file mode 100644 index 0000000..22f53da --- /dev/null +++ b/repos/effect/.changeset/add-config-nested.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Config.nested` combinator to scope a config under a named prefix, closes #1437. diff --git a/repos/effect/.changeset/add-flag-hidden.md b/repos/effect/.changeset/add-flag-hidden.md new file mode 100644 index 0000000..1c31e6c --- /dev/null +++ b/repos/effect/.changeset/add-flag-hidden.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Add `Flag.withHidden` (and `Param.withHidden`) to hide flags from `--help` output and shell completions while keeping them fully parseable on the command line. + +Useful for experimental, internal, or deprecated flags that should be accepted but not advertised, e.g. `--experimental-foo`, debug toggles, or escape hatches that are not yet committed to the public CLI surface. + +```ts +import { Flag } from "effect/unstable/cli" + +const experimental = Flag.boolean("experimental-foo").pipe( + Flag.withHidden +) +``` diff --git a/repos/effect/.changeset/add-from-string-schemas.md b/repos/effect/.changeset/add-from-string-schemas.md new file mode 100644 index 0000000..5ee9f82 --- /dev/null +++ b/repos/effect/.changeset/add-from-string-schemas.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `DateFromString`, `BigIntFromString`, `BigDecimalFromString`, `TimeZoneNamedFromString`, `TimeZoneFromString`, and `DateTimeZonedFromString` schemas, closes #1941. diff --git a/repos/effect/.changeset/add-headers-remove-many.md b/repos/effect/.changeset/add-headers-remove-many.md new file mode 100644 index 0000000..8d09bb5 --- /dev/null +++ b/repos/effect/.changeset/add-headers-remove-many.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/http Headers: add `removeMany` combinator for removing multiple headers at once diff --git a/repos/effect/.changeset/add-indexeddb-kvs-layer.md b/repos/effect/.changeset/add-indexeddb-kvs-layer.md new file mode 100644 index 0000000..bb6671b --- /dev/null +++ b/repos/effect/.changeset/add-indexeddb-kvs-layer.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Adds an IndexedDB backed implementation of `KeyValueStore` as `BrowserKeyValueStore.layerIndexedDb`. This backend allows for non-blocking `KeyValueStore` operations, unlike the existing `Storage` api backed implementations. diff --git a/repos/effect/.changeset/add-make-msgpack.md b/repos/effect/.changeset/add-make-msgpack.md new file mode 100644 index 0000000..9abec1e --- /dev/null +++ b/repos/effect/.changeset/add-make-msgpack.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `RpcSerialization.makeMsgPack` for creating MessagePack serialization with custom msgpackr options. On Cloudflare Workers with `allow_eval_during_startup` (default for `compatibility_date >= 2025-06-01`), pass `{ useRecords: false }` to prevent msgpackr's JIT code generation via `new Function()`, which is blocked during request handling. Also fixes silent error swallowing in the `msgPack` decode path — non-incomplete errors are now rethrown instead of returning `[]`. diff --git a/repos/effect/.changeset/add-make-option.md b/repos/effect/.changeset/add-make-option.md new file mode 100644 index 0000000..4984b63 --- /dev/null +++ b/repos/effect/.changeset/add-make-option.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `SchemaParser.makeOption` and `Schema.makeOption` for constructing schema values as `Option`. diff --git a/repos/effect/.changeset/add-missing-tx-modules.md b/repos/effect/.changeset/add-missing-tx-modules.md new file mode 100644 index 0000000..aa7cc30 --- /dev/null +++ b/repos/effect/.changeset/add-missing-tx-modules.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Add transactional STM modules: TxDeferred, TxPriorityQueue, TxPubSub, TxReentrantLock, TxSubscriptionRef. + +Refactor transaction model: remove `Effect.atomic`/`Effect.atomicWith`. All Tx operations now return `Effect` requiring explicit `Effect.tx(...)` at boundaries. + +Expose `TxPubSub.acquireSubscriber`/`releaseSubscriber` for composable transaction boundaries. Fix `TxSubscriptionRef.changes` race condition ensuring current value is delivered first. + +Remove `TxRandom` module. diff --git a/repos/effect/.changeset/add-newtype-module.md b/repos/effect/.changeset/add-newtype-module.md new file mode 100644 index 0000000..c407aeb --- /dev/null +++ b/repos/effect/.changeset/add-newtype-module.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Newtype` module. diff --git a/repos/effect/.changeset/add-scalar-show-operation-id.md b/repos/effect/.changeset/add-scalar-show-operation-id.md new file mode 100644 index 0000000..04d131e --- /dev/null +++ b/repos/effect/.changeset/add-scalar-show-operation-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `showOperationId` to `HttpApiScalar.ScalarConfig`. diff --git a/repos/effect/.changeset/add-schedule-tap.md b/repos/effect/.changeset/add-schedule-tap.md new file mode 100644 index 0000000..0f4ccc3 --- /dev/null +++ b/repos/effect/.changeset/add-schedule-tap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Added `Schedule.tap`, which allows observing full schedule metadata without altering schedule inputs or outputs. diff --git a/repos/effect/.changeset/add-schema-annotate-encoded.md b/repos/effect/.changeset/add-schema-annotate-encoded.md new file mode 100644 index 0000000..0e5b7e1 --- /dev/null +++ b/repos/effect/.changeset/add-schema-annotate-encoded.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `annotateEncoded` function for annotating the encoded side of a schema. diff --git a/repos/effect/.changeset/add-schema-array-ensure.md b/repos/effect/.changeset/add-schema-array-ensure.md new file mode 100644 index 0000000..908baea --- /dev/null +++ b/repos/effect/.changeset/add-schema-array-ensure.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.ArrayEnsure`. diff --git a/repos/effect/.changeset/add-schema-bigdecimal.md b/repos/effect/.changeset/add-schema-bigdecimal.md new file mode 100644 index 0000000..76d278f --- /dev/null +++ b/repos/effect/.changeset/add-schema-bigdecimal.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `BigDecimal` schema with comparison checks (`isGreaterThanBigDecimal`, `isGreaterThanOrEqualToBigDecimal`, `isLessThanBigDecimal`, `isLessThanOrEqualToBigDecimal`, `isBetweenBigDecimal`). diff --git a/repos/effect/.changeset/add-schema-datetime.md b/repos/effect/.changeset/add-schema-datetime.md new file mode 100644 index 0000000..c72f460 --- /dev/null +++ b/repos/effect/.changeset/add-schema-datetime.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `DateTimeZoned`, `TimeZoneOffset`, `TimeZoneNamed`, and `TimeZone` schemas. diff --git a/repos/effect/.changeset/add-schema-error-module.md b/repos/effect/.changeset/add-schema-error-module.md new file mode 100644 index 0000000..d28c2c2 --- /dev/null +++ b/repos/effect/.changeset/add-schema-error-module.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Expose `SchemaError` as a public module and re-export `Schema.isSchemaError`. + +This gives consumers a stable import path and guard for schema failures without +depending on the internal schema implementation, while preserving the existing +`Schema.SchemaError` surface. diff --git a/repos/effect/.changeset/add-schema-option-from-optional-nullor.md b/repos/effect/.changeset/add-schema-option-from-optional-nullor.md new file mode 100644 index 0000000..d634eea --- /dev/null +++ b/repos/effect/.changeset/add-schema-option-from-optional-nullor.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `OptionFromOptionalNullOr` schema, closes #1707. diff --git a/repos/effect/.changeset/add-schema-option-from-undefined-nullish.md b/repos/effect/.changeset/add-schema-option-from-undefined-nullish.md new file mode 100644 index 0000000..c2d5693 --- /dev/null +++ b/repos/effect/.changeset/add-schema-option-from-undefined-nullish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `OptionFromUndefinedOr` and `OptionFromNullishOr` schemas. diff --git a/repos/effect/.changeset/add-schema-string-encoding.md b/repos/effect/.changeset/add-schema-string-encoding.md new file mode 100644 index 0000000..7910ae2 --- /dev/null +++ b/repos/effect/.changeset/add-schema-string-encoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `StringFromBase64`, `StringFromBase64Url`, `StringFromHex`, and `StringFromUriComponent` schemas for decoding encoded strings into UTF-8 strings, closes #1995. diff --git a/repos/effect/.changeset/add-sql-pglite.md b/repos/effect/.changeset/add-sql-pglite.md new file mode 100644 index 0000000..e0eb64e --- /dev/null +++ b/repos/effect/.changeset/add-sql-pglite.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pglite": minor +--- + +Add `@effect/sql-pglite` package, wrapping `@electric-sql/pglite` with the Effect SQL client (Postgres dialect, Effect-managed transactions via savepoints, listen/notify, dumpDataDir/refreshArrayTypes, and a Migrator). diff --git a/repos/effect/.changeset/add-standard-jsdoc-rule.md b/repos/effect/.changeset/add-standard-jsdoc-rule.md new file mode 100644 index 0000000..317fe35 --- /dev/null +++ b/repos/effect/.changeset/add-standard-jsdoc-rule.md @@ -0,0 +1,5 @@ +--- +"@effect/oxc": patch +--- + +Add an `effect/standard-jsdoc` oxlint rule for staged public API JSDoc standardization. diff --git a/repos/effect/.changeset/add-stream-broadcastn.md b/repos/effect/.changeset/add-stream-broadcastn.md new file mode 100644 index 0000000..ca1fa61 --- /dev/null +++ b/repos/effect/.changeset/add-stream-broadcastn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Stream.broadcastN for fixed-size stream broadcasts. diff --git a/repos/effect/.changeset/add-unstable-encoding-export.md b/repos/effect/.changeset/add-unstable-encoding-export.md new file mode 100644 index 0000000..99b1072 --- /dev/null +++ b/repos/effect/.changeset/add-unstable-encoding-export.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `unstable/encoding` subpath export. diff --git a/repos/effect/.changeset/add-values-unprepared.md b/repos/effect/.changeset/add-values-unprepared.md new file mode 100644 index 0000000..d0ac10c --- /dev/null +++ b/repos/effect/.changeset/add-values-unprepared.md @@ -0,0 +1,17 @@ +--- +"effect": patch +"@effect/sql-clickhouse": patch +"@effect/sql-d1": patch +"@effect/sql-libsql": patch +"@effect/sql-mssql": patch +"@effect/sql-mysql2": patch +"@effect/sql-pg": patch +"@effect/sql-pglite": patch +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-do": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +--- + +Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. diff --git a/repos/effect/.changeset/afraid-cobras-like.md b/repos/effect/.changeset/afraid-cobras-like.md new file mode 100644 index 0000000..c471ffa --- /dev/null +++ b/repos/effect/.changeset/afraid-cobras-like.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +default to endOnDone: false in NodeStdio diff --git a/repos/effect/.changeset/ai-openai-config-field-leak.md b/repos/effect/.changeset/ai-openai-config-field-leak.md new file mode 100644 index 0000000..58df093 --- /dev/null +++ b/repos/effect/.changeset/ai-openai-config-field-leak.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Fix `OpenAiLanguageModel` leaking library-only config fields (`fileIdPrefixes`, `strictJsonSchema`) into request body, causing OpenAI 400 errors. diff --git a/repos/effect/.changeset/ai-openai-file-nullable-fields.md b/repos/effect/.changeset/ai-openai-file-nullable-fields.md new file mode 100644 index 0000000..b59718e --- /dev/null +++ b/repos/effect/.changeset/ai-openai-file-nullable-fields.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix `OpenAIFile` schema decode failure on responses where `expires_at` and `status_details` are returned as literal `null`. The OpenAI files endpoint returns `null` (not omitted) for these fields when no expiration / status detail applies (e.g. uploads with `purpose: "user_data"`), but the upstream OpenAPI spec marks them only as optional. Codegen patches widen both fields to allow `null`, which now decodes cleanly via `OpenAiClient.createFile`, `retrieveFile`, `listFiles`, and any other endpoint returning the `OpenAIFile` shape. diff --git a/repos/effect/.changeset/anthropic-4-6-structured-output.md b/repos/effect/.changeset/anthropic-4-6-structured-output.md new file mode 100644 index 0000000..3900a93 --- /dev/null +++ b/repos/effect/.changeset/anthropic-4-6-structured-output.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Mark the Claude 4-6 generation as supporting native structured output in `getModelCapabilities`. `claude-opus-4-6` and `claude-sonnet-4-6` support Anthropic's constrained-decoding structured output (verified against the live API), but were classified as `supportsStructuredOutput: false`, so `generateObject` fell back to a forced JSON tool instead of requesting `output_config.format` (`json_schema`). `claude-opus-4-7` / `claude-opus-4-8` are classified the same way for when the generated `Model` enum picks them up. diff --git a/repos/effect/.changeset/anthropic-open-model-enum.md b/repos/effect/.changeset/anthropic-open-model-enum.md new file mode 100644 index 0000000..b5ab039 --- /dev/null +++ b/repos/effect/.changeset/anthropic-open-model-enum.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-anthropic": patch +--- + +Widen the Anthropic `Model` schema to accept both known model identifiers as well +as any string to allow for newer models diff --git a/repos/effect/.changeset/apply-httpapi-endpoint-client-transform.md b/repos/effect/.changeset/apply-httpapi-endpoint-client-transform.md new file mode 100644 index 0000000..ce9d74c --- /dev/null +++ b/repos/effect/.changeset/apply-httpapi-endpoint-client-transform.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply `transformClient` when building an individual HttpApi endpoint client, preserving the supplied client's error and service channels. diff --git a/repos/effect/.changeset/asyncresult-exhaustive.md b/repos/effect/.changeset/asyncresult-exhaustive.md new file mode 100644 index 0000000..d448fd6 --- /dev/null +++ b/repos/effect/.changeset/asyncresult-exhaustive.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add an exhaustive finalizer to the AsyncResult builder. diff --git a/repos/effect/.changeset/atom-stream-error-type.md b/repos/effect/.changeset/atom-stream-error-type.md new file mode 100644 index 0000000..a0809ca --- /dev/null +++ b/repos/effect/.changeset/atom-stream-error-type.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose `NoSuchElementError` in the error type of stream-based `Atom.make` overloads. diff --git a/repos/effect/.changeset/beige-paths-sort.md b/repos/effect/.changeset/beige-paths-sort.md new file mode 100644 index 0000000..0cfad27 --- /dev/null +++ b/repos/effect/.changeset/beige-paths-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +move ChildProcess apis into spawner service diff --git a/repos/effect/.changeset/better-apples-nail.md b/repos/effect/.changeset/better-apples-nail.md new file mode 100644 index 0000000..338b25b --- /dev/null +++ b/repos/effect/.changeset/better-apples-nail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Effect.repeat now uses effect return value when using options diff --git a/repos/effect/.changeset/better-rocks-arrive.md b/repos/effect/.changeset/better-rocks-arrive.md new file mode 100644 index 0000000..c703f17 --- /dev/null +++ b/repos/effect/.changeset/better-rocks-arrive.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +add back openai reasoning types diff --git a/repos/effect/.changeset/better-shrimps-follow.md b/repos/effect/.changeset/better-shrimps-follow.md new file mode 100644 index 0000000..95cb596 --- /dev/null +++ b/repos/effect/.changeset/better-shrimps-follow.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +prefix opentelemetry modules to avoid collisions diff --git a/repos/effect/.changeset/big-pans-look.md b/repos/effect/.changeset/big-pans-look.md new file mode 100644 index 0000000..0e4fd59 --- /dev/null +++ b/repos/effect/.changeset/big-pans-look.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Rpc.custom diff --git a/repos/effect/.changeset/blue-dingos-greet.md b/repos/effect/.changeset/blue-dingos-greet.md new file mode 100644 index 0000000..01e918b --- /dev/null +++ b/repos/effect/.changeset/blue-dingos-greet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.filter` support for synchronous `Filter.Filter` overloads and correctly handle non-effect `Result` return values at runtime. diff --git a/repos/effect/.changeset/blue-ligers-cheat.md b/repos/effect/.changeset/blue-ligers-cheat.md new file mode 100644 index 0000000..37a66d7 --- /dev/null +++ b/repos/effect/.changeset/blue-ligers-cheat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update unstable schema variant helpers to use array-based arguments for `FieldOnly`, `FieldExcept`, and `Union`, aligning `VariantSchema` and `Model` with other v4 API shapes. diff --git a/repos/effect/.changeset/blue-onions-smile.md b/repos/effect/.changeset/blue-onions-smile.md new file mode 100644 index 0000000..2ef4e62 --- /dev/null +++ b/repos/effect/.changeset/blue-onions-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Preserve streamed OpenAI compat tool call ids and names across fragmented chat completion chunks. diff --git a/repos/effect/.changeset/blue-ravens-type.md b/repos/effect/.changeset/blue-ravens-type.md new file mode 100644 index 0000000..0919ee4 --- /dev/null +++ b/repos/effect/.changeset/blue-ravens-type.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure streamed tool results are emitted before the finish part so chat history includes tool outputs before stream termination. diff --git a/repos/effect/.changeset/blue-trams-kiss.md b/repos/effect/.changeset/blue-trams-kiss.md new file mode 100644 index 0000000..27fa6f3 --- /dev/null +++ b/repos/effect/.changeset/blue-trams-kiss.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix consolePretty ignoring explicit colors option in non-TTY environments. + +When colors is explicitly set to true, prettyLoggerTty was still gating it with processStdoutIsTTY check, making it impossible to enable colors in non-TTY environments like Vite dev server. diff --git a/repos/effect/.changeset/bold-chairs-yawn.md b/repos/effect/.changeset/bold-chairs-yawn.md new file mode 100644 index 0000000..1b14a10 --- /dev/null +++ b/repos/effect/.changeset/bold-chairs-yawn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add HttpApiTest module diff --git a/repos/effect/.changeset/bold-planets-shout.md b/repos/effect/.changeset/bold-planets-shout.md new file mode 100644 index 0000000..001dbd0 --- /dev/null +++ b/repos/effect/.changeset/bold-planets-shout.md @@ -0,0 +1,30 @@ +--- +"effect": patch +--- + +add support for merging external events into `Prompt.custom` render loops via an optional `events` dequeue and `receive` handler. + +The prompt races user input against events from the dequeue, allowing background events to trigger re-renders without waiting for a keypress: + +```ts +const eventQueue = yield * Queue.make() + +const prompt = Prompt.custom( + { count: 0 }, + Queue.asDequeue(eventQueue), // <-- provide the event queue as a dequeue to the prompt + { + render: (state) => Effect.succeed(`Count: ${state.count}`), + process: (input, state) => + Effect.succeed( + Match.value(input).pipe( + // handle user input + Match.tag("Input", () => Action.Submit({ value: state.count })), + // handle external events from the queue + Match.tag("Event", (input) => Action.NextFrame({ state: { count: state.count + input.value } })), + Match.exhaustive + ) + ), + clear: () => Effect.succeed("") + } +) +``` diff --git a/repos/effect/.changeset/breezy-meals-see.md b/repos/effect/.changeset/breezy-meals-see.md new file mode 100644 index 0000000..c4a8359 --- /dev/null +++ b/repos/effect/.changeset/breezy-meals-see.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +support tag unions in Effect.catchTag/Reason diff --git a/repos/effect/.changeset/bright-bugs-bow.md b/repos/effect/.changeset/bright-bugs-bow.md new file mode 100644 index 0000000..ca6cb86 --- /dev/null +++ b/repos/effect/.changeset/bright-bugs-bow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Run request resolver batch fibers with request services by using `Effect.runForkWith`, so resolver delay effects and `runAll` execution see the request service map. diff --git a/repos/effect/.changeset/bright-canyons-clean.md b/repos/effect/.changeset/bright-canyons-clean.md new file mode 100644 index 0000000..3bddee3 --- /dev/null +++ b/repos/effect/.changeset/bright-canyons-clean.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add support for deferred responses in rpc diff --git a/repos/effect/.changeset/bright-dogs-fail.md b/repos/effect/.changeset/bright-dogs-fail.md new file mode 100644 index 0000000..f29db94 --- /dev/null +++ b/repos/effect/.changeset/bright-dogs-fail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Model.BooleanSqlite`, a model field schema that uses `0 | 1` encoding for database variants and plain `boolean` encoding for JSON variants. diff --git a/repos/effect/.changeset/bright-flags-stand.md b/repos/effect/.changeset/bright-flags-stand.md new file mode 100644 index 0000000..131bcf4 --- /dev/null +++ b/repos/effect/.changeset/bright-flags-stand.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix CLI parsing so command-local flags can override globals without breaking global flags before subcommands. diff --git a/repos/effect/.changeset/bright-laws-teach.md b/repos/effect/.changeset/bright-laws-teach.md new file mode 100644 index 0000000..12df791 --- /dev/null +++ b/repos/effect/.changeset/bright-laws-teach.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Config.Success` type utility, closes #1783. diff --git a/repos/effect/.changeset/bright-lemons-dance.md b/repos/effect/.changeset/bright-lemons-dance.md new file mode 100644 index 0000000..0bae933 --- /dev/null +++ b/repos/effect/.changeset/bright-lemons-dance.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `Effect.acquireRelease` release finalizers to depend on the surrounding environment. diff --git a/repos/effect/.changeset/bright-planes-smash.md b/repos/effect/.changeset/bright-planes-smash.md new file mode 100644 index 0000000..edf82b8 --- /dev/null +++ b/repos/effect/.changeset/bright-planes-smash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `responseText` to `AiError.StructuredOutputError` and populate it from `LanguageModel.generateObject` so failed structured output decodes include the full LLM text. diff --git a/repos/effect/.changeset/bright-rats-attend.md b/repos/effect/.changeset/bright-rats-attend.md new file mode 100644 index 0000000..071f378 --- /dev/null +++ b/repos/effect/.changeset/bright-rats-attend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make fiber keepAlive setInterval evaluation lazy diff --git a/repos/effect/.changeset/bright-toes-rush.md b/repos/effect/.changeset/bright-toes-rush.md new file mode 100644 index 0000000..34bda7c --- /dev/null +++ b/repos/effect/.changeset/bright-toes-rush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Persist MCP HTTP session and protocol headers after initialize so follow-up JSON-RPC requests include `MCP-Protocol-Version`. diff --git a/repos/effect/.changeset/bumpy-boxes-teach.md b/repos/effect/.changeset/bumpy-boxes-teach.md new file mode 100644 index 0000000..3120542 --- /dev/null +++ b/repos/effect/.changeset/bumpy-boxes-teach.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename Atom's `Context` type to `AtomContext` diff --git a/repos/effect/.changeset/busy-lions-sneeze.md b/repos/effect/.changeset/busy-lions-sneeze.md new file mode 100644 index 0000000..de2167c --- /dev/null +++ b/repos/effect/.changeset/busy-lions-sneeze.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Fix HTTP Referer header name in the `OpenRouterClient` diff --git a/repos/effect/.changeset/busy-maps-attend.md b/repos/effect/.changeset/busy-maps-attend.md new file mode 100644 index 0000000..3956987 --- /dev/null +++ b/repos/effect/.changeset/busy-maps-attend.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-bun": patch +"@effect/platform-node": patch +--- + +add rows to Terminal diff --git a/repos/effect/.changeset/calm-buckets-own.md b/repos/effect/.changeset/calm-buckets-own.md new file mode 100644 index 0000000..0610fbd --- /dev/null +++ b/repos/effect/.changeset/calm-buckets-own.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +improve openai socket errors diff --git a/repos/effect/.changeset/calm-carrots-march.md b/repos/effect/.changeset/calm-carrots-march.md new file mode 100644 index 0000000..7570350 --- /dev/null +++ b/repos/effect/.changeset/calm-carrots-march.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Allow unstable HttpApi middleware to declare multiple error schemas with arrays. + +Middleware errors now follow endpoint error behavior for response status resolution, client decoding, and generated API schemas. diff --git a/repos/effect/.changeset/calm-cars-rest.md b/repos/effect/.changeset/calm-cars-rest.md new file mode 100644 index 0000000..a61b82e --- /dev/null +++ b/repos/effect/.changeset/calm-cars-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add grouped subcommand support to `Command.withSubcommands`, including help output sections for named groups while keeping ungrouped commands under `SUBCOMMANDS`. diff --git a/repos/effect/.changeset/calm-masks-count.md b/repos/effect/.changeset/calm-masks-count.md new file mode 100644 index 0000000..b70fc9a --- /dev/null +++ b/repos/effect/.changeset/calm-masks-count.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.DateFromMillis` and `SchemaTransformation.dateFromMillis` for decoding millisecond timestamps into `Date` values. diff --git a/repos/effect/.changeset/calm-panthers-nail.md b/repos/effect/.changeset/calm-panthers-nail.md new file mode 100644 index 0000000..1f855e5 --- /dev/null +++ b/repos/effect/.changeset/calm-panthers-nail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `disableFatalDefects` to `RpcServer.layerHttp`, `RpcServer.toHttpEffect`, and `RpcServer.toHttpEffectWebsocket` option types to match existing runtime support. diff --git a/repos/effect/.changeset/calm-ravens-reflect.md b/repos/effect/.changeset/calm-ravens-reflect.md new file mode 100644 index 0000000..ba31e47 --- /dev/null +++ b/repos/effect/.changeset/calm-ravens-reflect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `__proto__` group and endpoint identifiers in HTTP APIs, generated clients, and URL builders. diff --git a/repos/effect/.changeset/calm-seas-smile.md b/repos/effect/.changeset/calm-seas-smile.md new file mode 100644 index 0000000..bde18ad --- /dev/null +++ b/repos/effect/.changeset/calm-seas-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Align workflow tags with RPCs by changing `Workflow.make` to accept the tag as its first argument, exposing workflow tags as `_tag`, and supporting `class MyWorkflow extends Workflow.make(...) {}`. diff --git a/repos/effect/.changeset/calm-squids-hug.md b/repos/effect/.changeset/calm-squids-hug.md new file mode 100644 index 0000000..3ac3c61 --- /dev/null +++ b/repos/effect/.changeset/calm-squids-hug.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Context.Key type, used a base for Context.Service and Context.Reference diff --git a/repos/effect/.changeset/calm-tracers-sample.md b/repos/effect/.changeset/calm-tracers-sample.md new file mode 100644 index 0000000..3bbb0ad --- /dev/null +++ b/repos/effect/.changeset/calm-tracers-sample.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Preserve sampling decisions when adapting generic Effect external spans to OpenTelemetry. diff --git a/repos/effect/.changeset/chatty-poets-type.md b/repos/effect/.changeset/chatty-poets-type.md new file mode 100644 index 0000000..623273c --- /dev/null +++ b/repos/effect/.changeset/chatty-poets-type.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove Effect.Yieldable diff --git a/repos/effect/.changeset/chilled-mice-wash.md b/repos/effect/.changeset/chilled-mice-wash.md new file mode 100644 index 0000000..08e529a --- /dev/null +++ b/repos/effect/.changeset/chilled-mice-wash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpServerResponse.fromClientResponse` for directly converting client responses into server responses. diff --git a/repos/effect/.changeset/chilly-pumas-rule.md b/repos/effect/.changeset/chilly-pumas-rule.md new file mode 100644 index 0000000..b53d351 --- /dev/null +++ b/repos/effect/.changeset/chilly-pumas-rule.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `Data.Class`, `Data.TaggedClass`, and `Cause.YieldableError` pipeable. diff --git a/repos/effect/.changeset/chubby-buckets-feel.md b/repos/effect/.changeset/chubby-buckets-feel.md new file mode 100644 index 0000000..bf1cc12 --- /dev/null +++ b/repos/effect/.changeset/chubby-buckets-feel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix issues with metro bundler diff --git a/repos/effect/.changeset/chubby-parents-flow.md b/repos/effect/.changeset/chubby-parents-flow.md new file mode 100644 index 0000000..8c87cad --- /dev/null +++ b/repos/effect/.changeset/chubby-parents-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow undefined for VariantSchema.Overridable input diff --git a/repos/effect/.changeset/chubby-planets-fall.md b/repos/effect/.changeset/chubby-planets-fall.md new file mode 100644 index 0000000..15c60d5 --- /dev/null +++ b/repos/effect/.changeset/chubby-planets-fall.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Remove duplicate `ToolApprovalResponsePartOptions` from Anthropic package diff --git a/repos/effect/.changeset/clean-balloons-tan.md b/repos/effect/.changeset/clean-balloons-tan.md new file mode 100644 index 0000000..1c4ff47 --- /dev/null +++ b/repos/effect/.changeset/clean-balloons-tan.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Proxy function arity from `Effect.fn` APIs so wrapped functions preserve the original `length` value. diff --git a/repos/effect/.changeset/clean-bulldogs-care.md b/repos/effect/.changeset/clean-bulldogs-care.md new file mode 100644 index 0000000..48f430f --- /dev/null +++ b/repos/effect/.changeset/clean-bulldogs-care.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Use `pg_notify` in `PgClient.notify` so channel and payload are sent through parameters instead of a `NOTIFY` statement string. diff --git a/repos/effect/.changeset/clean-dryers-sneeze.md b/repos/effect/.changeset/clean-dryers-sneeze.md new file mode 100644 index 0000000..96316e6 --- /dev/null +++ b/repos/effect/.changeset/clean-dryers-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Random.nextBoolean` for generating random boolean values. diff --git a/repos/effect/.changeset/clean-geese-work.md b/repos/effect/.changeset/clean-geese-work.md new file mode 100644 index 0000000..e91346b --- /dev/null +++ b/repos/effect/.changeset/clean-geese-work.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Inspectable.stringifyCircular` and fix `Formatter.formatJson` so shared object references are preserved while only circular references are omitted. diff --git a/repos/effect/.changeset/clean-goats-wave.md b/repos/effect/.changeset/clean-goats-wave.md new file mode 100644 index 0000000..944fd84 --- /dev/null +++ b/repos/effect/.changeset/clean-goats-wave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Ctrl-U` line clearing support to editable CLI prompts. diff --git a/repos/effect/.changeset/clean-needles-shake.md b/repos/effect/.changeset/clean-needles-shake.md new file mode 100644 index 0000000..17abeea --- /dev/null +++ b/repos/effect/.changeset/clean-needles-shake.md @@ -0,0 +1,23 @@ +--- +"effect": patch +--- + +Fix module-level side effects that defeated bundler tree-shaking. + +Bare top-level statements cannot be `#__PURE__`-annotated by the build, so +bundlers must retain them and everything they reference, even in bundles that +never use the code: + +- `Option`: the standalone `Object.defineProperty(SomeProto, "valueOrUndefined", ...)` + statement anchored the whole `Option` proto chain into every bundle. It is + now folded into the `SomeProto` initializer. +- `Headers`: same pattern with `Object.defineProperties(Proto, ...)`, folded + into the initializer. +- `Logger`: module-level `process.stdout.isTTY` property reads (potential + getters, never droppable) moved inside `consolePretty`. +- `Utils`: when `internalCall` was unused, its dropped binding left behind a + retained initializer tail (`standard`/`forced` probe with computed property + reads). The selection is now wrapped in a single pure-annotated call. + +A minimal `Effect.succeed(123).pipe(Effect.runFork)` bundle shrinks by ~1.3% +gzipped; bundles that don't use `Option` or `Headers` no longer pay for them. diff --git a/repos/effect/.changeset/clean-tires-guess.md b/repos/effect/.changeset/clean-tires-guess.md new file mode 100644 index 0000000..29463e9 --- /dev/null +++ b/repos/effect/.changeset/clean-tires-guess.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +clean up sql-pg constructors and layers diff --git a/repos/effect/.changeset/clear-hairs-pump.md b/repos/effect/.changeset/clear-hairs-pump.md new file mode 100644 index 0000000..ac42927 --- /dev/null +++ b/repos/effect/.changeset/clear-hairs-pump.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix activity retry policy diff --git a/repos/effect/.changeset/clear-spies-boil.md b/repos/effect/.changeset/clear-spies-boil.md new file mode 100644 index 0000000..44318f6 --- /dev/null +++ b/repos/effect/.changeset/clear-spies-boil.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +improve idb support for compound indexes diff --git a/repos/effect/.changeset/cli-config-built-ins.md b/repos/effect/.changeset/cli-config-built-ins.md new file mode 100644 index 0000000..6e0dbcd --- /dev/null +++ b/repos/effect/.changeset/cli-config-built-ins.md @@ -0,0 +1,24 @@ +--- +"effect": patch +--- + +Add a scoped `CliConfig` service for customizing the built-in global flags used by CLI command runners. + +For example, provide an explicit list that omits `GlobalFlag.LogLevel` to remove the built-in `--log-level` flag: + +```ts +import { Effect } from "effect" +import { CliConfig, Command, GlobalFlag } from "effect/unstable/cli" + +const program = Command.run(command, { version: "1.0.0" }).pipe( + Effect.provide( + CliConfig.layer({ + builtIns: [ + GlobalFlag.Help, + GlobalFlag.Version, + GlobalFlag.Completions + ] + }) + ) +) +``` diff --git a/repos/effect/.changeset/cli-help-choices.md b/repos/effect/.changeset/cli-help-choices.md new file mode 100644 index 0000000..379f751 --- /dev/null +++ b/repos/effect/.changeset/cli-help-choices.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Append concrete choice values to CLI flag help descriptions so generated help shows valid command-line inputs. diff --git a/repos/effect/.changeset/cli-wizard-mode.md b/repos/effect/.changeset/cli-wizard-mode.md new file mode 100644 index 0000000..0b7c9f2 --- /dev/null +++ b/repos/effect/.changeset/cli-wizard-mode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reintroduce interactive CLI wizard mode through the `--wizard` flag and `Command.wizard`. diff --git a/repos/effect/.changeset/cold-knives-lie.md b/repos/effect/.changeset/cold-knives-lie.md new file mode 100644 index 0000000..5e7051f --- /dev/null +++ b/repos/effect/.changeset/cold-knives-lie.md @@ -0,0 +1,35 @@ +--- +"effect": patch +--- + +Port `Pipeable.Class` from v3. + +```ts +class MyClass extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } +} +console.log(new MyClass(2).pipe((x) => x.methodA())) // 2 +``` + +```ts +class A { + constructor(public a: number) {} + methodA() { + return this.a + } +} +class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } +} +console.log(new B("pipe").pipe((x) => x.methodB())) // ['pipe', 4] +``` diff --git a/repos/effect/.changeset/cold-rooms-show.md b/repos/effect/.changeset/cold-rooms-show.md new file mode 100644 index 0000000..ab2258a --- /dev/null +++ b/repos/effect/.changeset/cold-rooms-show.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +use Cause.NoSuchElementError for idb .first queries diff --git a/repos/effect/.changeset/cold-sloths-wave.md b/repos/effect/.changeset/cold-sloths-wave.md new file mode 100644 index 0000000..6e6a1f7 --- /dev/null +++ b/repos/effect/.changeset/cold-sloths-wave.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Add support for OpenAI `keepalive` response stream events. diff --git a/repos/effect/.changeset/common-mammals-tickle.md b/repos/effect/.changeset/common-mammals-tickle.md new file mode 100644 index 0000000..5108982 --- /dev/null +++ b/repos/effect/.changeset/common-mammals-tickle.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix generation order for recursive schemas referenced by earlier recursive definitions, closes #6357. diff --git a/repos/effect/.changeset/compact-json-schema-enum.md b/repos/effect/.changeset/compact-json-schema-enum.md new file mode 100644 index 0000000..f6b4e76 --- /dev/null +++ b/repos/effect/.changeset/compact-json-schema-enum.md @@ -0,0 +1,25 @@ +--- +"effect": patch +--- + +Schema: collapse same-type literal branches in JSON Schema output into a single `enum` array, closes #1868. + +Before: + +```json +{ + "anyOf": [ + { "type": "string", "enum": ["A"] }, + { "type": "string", "enum": ["B"] } + ] +} +``` + +After: + +```json +{ + "type": "string", + "enum": ["A", "B"] +} +``` diff --git a/repos/effect/.changeset/config-withdefault-eager.md b/repos/effect/.changeset/config-withdefault-eager.md new file mode 100644 index 0000000..107cd6f --- /dev/null +++ b/repos/effect/.changeset/config-withdefault-eager.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Revert `Config.withDefault` to v3 behavior, closes #1530. + +Make `Config.withDefault` accept an eager value instead of `LazyArg`, aligning with CLI module conventions. diff --git a/repos/effect/.changeset/config.json b/repos/effect/.changeset/config.json new file mode 100644 index 0000000..4511dd3 --- /dev/null +++ b/repos/effect/.changeset/config.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "Effect-TS/effect" }], + "commit": false, + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [ + "scratchpad", + "scripts" + ], + "privatePackages": false, + "fixed": [ + [ + "effect", + "@effect/*" + ] + ], + "snapshot": { + "useCalculatedVersion": false, + "prereleaseTemplate": "{tag}-{commit}" + } +} diff --git a/repos/effect/.changeset/consolidate-encoding.md b/repos/effect/.changeset/consolidate-encoding.md new file mode 100644 index 0000000..8e0cf7c --- /dev/null +++ b/repos/effect/.changeset/consolidate-encoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Encoding: consolidate `effect/encoding` sub-modules (Base64, Base64Url, Hex, EncodingError) into a top-level `Encoding` module. Functions are now prefixed: `encodeBase64`, `decodeBase64`, `encodeHex`, `decodeHex`, etc. The `effect/encoding` sub-path export is removed. diff --git a/repos/effect/.changeset/consolidate-sql-error.md b/repos/effect/.changeset/consolidate-sql-error.md new file mode 100644 index 0000000..fdefa5f --- /dev/null +++ b/repos/effect/.changeset/consolidate-sql-error.md @@ -0,0 +1,16 @@ +--- +"effect": patch +"@effect/sql-clickhouse": patch +"@effect/sql-d1": patch +"@effect/sql-libsql": patch +"@effect/sql-mssql": patch +"@effect/sql-mysql2": patch +"@effect/sql-pg": patch +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-do": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +--- + +Consolidate the SqlError changes to the new reason-based shape across effect and the SQL drivers, classifying native failures into structured reasons with Unknown fallback where native codes are unavailable. diff --git a/repos/effect/.changeset/crisp-seas-warn.md b/repos/effect/.changeset/crisp-seas-warn.md new file mode 100644 index 0000000..31fac07 --- /dev/null +++ b/repos/effect/.changeset/crisp-seas-warn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure transformed Atom's don't extend idle ttl diff --git a/repos/effect/.changeset/cron-locale-independent-aliases.md b/repos/effect/.changeset/cron-locale-independent-aliases.md new file mode 100644 index 0000000..9b84a1c --- /dev/null +++ b/repos/effect/.changeset/cron-locale-independent-aliases.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize cron month and weekday aliases independently of the host locale. diff --git a/repos/effect/.changeset/cron-single-value-step.md b/repos/effect/.changeset/cron-single-value-step.md new file mode 100644 index 0000000..02a9fff --- /dev/null +++ b/repos/effect/.changeset/cron-single-value-step.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow cron fields like `5/15` to expand from the starting value through the field maximum. diff --git a/repos/effect/.changeset/cron-testclock-infinity.md b/repos/effect/.changeset/cron-testclock-infinity.md new file mode 100644 index 0000000..3aea00e --- /dev/null +++ b/repos/effect/.changeset/cron-testclock-infinity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.cron` when the test clock is adjusted to infinity. diff --git a/repos/effect/.changeset/cuddly-rooms-bet.md b/repos/effect/.changeset/cuddly-rooms-bet.md new file mode 100644 index 0000000..8d09b0a --- /dev/null +++ b/repos/effect/.changeset/cuddly-rooms-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Stream.timeoutOrElse diff --git a/repos/effect/.changeset/curly-poems-talk.md b/repos/effect/.changeset/curly-poems-talk.md new file mode 100644 index 0000000..97fa70b --- /dev/null +++ b/repos/effect/.changeset/curly-poems-talk.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `Schema.HashSet` for decoding and encoding `HashSet` values. diff --git a/repos/effect/.changeset/curly-spies-relax.md b/repos/effect/.changeset/curly-spies-relax.md new file mode 100644 index 0000000..a495ef8 --- /dev/null +++ b/repos/effect/.changeset/curly-spies-relax.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +narrow types for Effect.retry/repeat while option diff --git a/repos/effect/.changeset/curvy-apples-float.md b/repos/effect/.changeset/curvy-apples-float.md new file mode 100644 index 0000000..fcdfac6 --- /dev/null +++ b/repos/effect/.changeset/curvy-apples-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.validate` for validating collections while accumulating all failures, equivalent to the v3 `Effect.validateAll` behavior. diff --git a/repos/effect/.changeset/curvy-birds-float.md b/repos/effect/.changeset/curvy-birds-float.md new file mode 100644 index 0000000..e0a68dd --- /dev/null +++ b/repos/effect/.changeset/curvy-birds-float.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Update `Schema.Void` to model ignored `void` return values. + +Runtime parsing now accepts any present value and discards it as `undefined`. +This matches TypeScript `void` return values, where callers do not observe the +returned value. Use `Schema.Undefined` when the input must be exactly +`undefined`. diff --git a/repos/effect/.changeset/custom-http-security-openapi-generator.md b/repos/effect/.changeset/custom-http-security-openapi-generator.md new file mode 100644 index 0000000..c338435 --- /dev/null +++ b/repos/effect/.changeset/custom-http-security-openapi-generator.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Add HttpApi generation support for custom OpenAPI HTTP security schemes. diff --git a/repos/effect/.changeset/cute-heads-thank.md b/repos/effect/.changeset/cute-heads-thank.md new file mode 100644 index 0000000..25a2f17 --- /dev/null +++ b/repos/effect/.changeset/cute-heads-thank.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +catch defects when building Entity handlers diff --git a/repos/effect/.changeset/cyan-loops-grow.md b/repos/effect/.changeset/cyan-loops-grow.md new file mode 100644 index 0000000..a807ca0 --- /dev/null +++ b/repos/effect/.changeset/cyan-loops-grow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +improve idb transaction api diff --git a/repos/effect/.changeset/cyan-radios-switch.md b/repos/effect/.changeset/cyan-radios-switch.md new file mode 100644 index 0000000..6763e29 --- /dev/null +++ b/repos/effect/.changeset/cyan-radios-switch.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Broaden the `vitest` peer dependency range to support both v3 and v4. diff --git a/repos/effect/.changeset/cyan-shirts-grin.md b/repos/effect/.changeset/cyan-shirts-grin.md new file mode 100644 index 0000000..96d0ce0 --- /dev/null +++ b/repos/effect/.changeset/cyan-shirts-grin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fork memo map on nested builds diff --git a/repos/effect/.changeset/deep-rivers-spend.md b/repos/effect/.changeset/deep-rivers-spend.md new file mode 100644 index 0000000..2c89a07 --- /dev/null +++ b/repos/effect/.changeset/deep-rivers-spend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Stream.withSpan options diff --git a/repos/effect/.changeset/dirty-lamps-trade.md b/repos/effect/.changeset/dirty-lamps-trade.md new file mode 100644 index 0000000..26c515d --- /dev/null +++ b/repos/effect/.changeset/dirty-lamps-trade.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Result.failVoid diff --git a/repos/effect/.changeset/dirty-laws-wear.md b/repos/effect/.changeset/dirty-laws-wear.md new file mode 100644 index 0000000..68ad0d5 --- /dev/null +++ b/repos/effect/.changeset/dirty-laws-wear.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +handle missing output array in openai responses diff --git a/repos/effect/.changeset/duration-temporal-object-input.md b/repos/effect/.changeset/duration-temporal-object-input.md new file mode 100644 index 0000000..faa02f6 --- /dev/null +++ b/repos/effect/.changeset/duration-temporal-object-input.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Add `DurationObject` to `Duration.Input` to support Temporal-style object input. + +Durations can now be created from objects with named unit properties like `{ hours: 1, minutes: 30 }`, similar to `Temporal.Duration.from()`. Supported fields: `weeks`, `days`, `hours`, `minutes`, `seconds`, `millis`, `micros`, `nanos`. diff --git a/repos/effect/.changeset/eager-coats-cheat.md b/repos/effect/.changeset/eager-coats-cheat.md new file mode 100644 index 0000000..d75b97a --- /dev/null +++ b/repos/effect/.changeset/eager-coats-cheat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename SqlSchema.findOne\* apis diff --git a/repos/effect/.changeset/early-birds-dream.md b/repos/effect/.changeset/early-birds-dream.md new file mode 100644 index 0000000..a6de037 --- /dev/null +++ b/repos/effect/.changeset/early-birds-dream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add trait for customizing exit codes diff --git a/repos/effect/.changeset/early-donuts-argue.md b/repos/effect/.changeset/early-donuts-argue.md new file mode 100644 index 0000000..fe1b701 --- /dev/null +++ b/repos/effect/.changeset/early-donuts-argue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix cache constructor inference by moving the lookup option diff --git a/repos/effect/.changeset/early-peaches-check.md b/repos/effect/.changeset/early-peaches-check.md new file mode 100644 index 0000000..0782936 --- /dev/null +++ b/repos/effect/.changeset/early-peaches-check.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +generate binary arrays from streams with less copying diff --git a/repos/effect/.changeset/eff-691-default-logger-ordering.md b/repos/effect/.changeset/eff-691-default-logger-ordering.md new file mode 100644 index 0000000..91fe054 --- /dev/null +++ b/repos/effect/.changeset/eff-691-default-logger-ordering.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve message item ordering in the default logger when logging a `Cause` with message values. diff --git a/repos/effect/.changeset/eff-693-rpcgroup-handler-deps.md b/repos/effect/.changeset/eff-693-rpcgroup-handler-deps.md new file mode 100644 index 0000000..e812b19 --- /dev/null +++ b/repos/effect/.changeset/eff-693-rpcgroup-handler-deps.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RpcGroup.toLayer` and `RpcGroup.toLayerHandler` service requirement inference so handler dependencies are preserved for non-stream RPC handlers. diff --git a/repos/effect/.changeset/eff-694-cli-completions-module.md b/repos/effect/.changeset/eff-694-cli-completions-module.md new file mode 100644 index 0000000..fcce292 --- /dev/null +++ b/repos/effect/.changeset/eff-694-cli-completions-module.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose CLI completions as a public unstable module at `effect/unstable/cli/Completions`. diff --git a/repos/effect/.changeset/eff-695-layer-mock-dual-api.md b/repos/effect/.changeset/eff-695-layer-mock-dual-api.md new file mode 100644 index 0000000..b4766bb --- /dev/null +++ b/repos/effect/.changeset/eff-695-layer-mock-dual-api.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `Layer.mock` a dual API so it supports both `Layer.mock(Service)(impl)` and `Layer.mock(Service, impl)`. diff --git a/repos/effect/.changeset/eff-697-rpcserialization-json-array-decode.md b/repos/effect/.changeset/eff-697-rpcserialization-json-array-decode.md new file mode 100644 index 0000000..062fcf8 --- /dev/null +++ b/repos/effect/.changeset/eff-697-rpcserialization-json-array-decode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RpcSerialization.json` decode so JSON array payloads are not wrapped in an extra outer array. diff --git a/repos/effect/.changeset/eff-698-rpcserialization-unreachable-branch.md b/repos/effect/.changeset/eff-698-rpcserialization-unreachable-branch.md new file mode 100644 index 0000000..cb1b1de --- /dev/null +++ b/repos/effect/.changeset/eff-698-rpcserialization-unreachable-branch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove an unreachable array branch in `decodeJsonRpcRaw` to simplify JSON-RPC decode logic without changing behavior. diff --git a/repos/effect/.changeset/eff-700-httpapi-middleware-errors.md b/repos/effect/.changeset/eff-700-httpapi-middleware-errors.md new file mode 100644 index 0000000..e95a360 --- /dev/null +++ b/repos/effect/.changeset/eff-700-httpapi-middleware-errors.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Improve unstable HttpApi runtime failures for missing server middleware and missing group implementations. + +- HttpApiBuilder.applyMiddleware now resolves middleware services via Context.getUnsafe, so missing middleware fails with a clear "Service not found: " error instead of an opaque is not a function TypeError. +- HttpApiBuilder.layer now reports missing groups with actionable context (group identifier, service key, suggested HttpApiBuilder.group(...) call, and available group keys). +- Added regression tests in packages/platform-node/test/HttpApi.test.ts covering: + - addHttpApi + API-level middleware applied across merged groups + - missing middleware service diagnostics + - missing addHttpApi group layer diagnostics diff --git a/repos/effect/.changeset/eff-701-httpapierror-respondable.md b/repos/effect/.changeset/eff-701-httpapierror-respondable.md new file mode 100644 index 0000000..9b78e50 --- /dev/null +++ b/repos/effect/.changeset/eff-701-httpapierror-respondable.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make all built-in `HttpApiError` classes implement `HttpServerRespondable`, so they can be returned directly from plain HTTP server handlers outside of `HttpApi`. diff --git a/repos/effect/.changeset/eff-704-stream-merge-predicate.md b/repos/effect/.changeset/eff-704-stream-merge-predicate.md new file mode 100644 index 0000000..a312260 --- /dev/null +++ b/repos/effect/.changeset/eff-704-stream-merge-predicate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use predicate-based `dual` dispatch for `Stream.merge` so data-last calls with optional `options` are handled correctly. diff --git a/repos/effect/.changeset/eff-705-layer-tap-apis.md b/repos/effect/.changeset/eff-705-layer-tap-apis.md new file mode 100644 index 0000000..c6b96bc --- /dev/null +++ b/repos/effect/.changeset/eff-705-layer-tap-apis.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Layer.tap`, `Layer.tapError`, and `Layer.tapCause` APIs for effectful observation of layer success and failure without changing layer outputs. diff --git a/repos/effect/.changeset/eff-706-servicemap-mutate.md b/repos/effect/.changeset/eff-706-servicemap-mutate.md new file mode 100644 index 0000000..6ce63c4 --- /dev/null +++ b/repos/effect/.changeset/eff-706-servicemap-mutate.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/opentelemetry": patch +--- + +Refactor call sites with multiple `Context` mutations to use `Context.mutate` for batched updates. diff --git a/repos/effect/.changeset/eff-716-response-id-tracker-map.md b/repos/effect/.changeset/eff-716-response-id-tracker-map.md new file mode 100644 index 0000000..8605f1b --- /dev/null +++ b/repos/effect/.changeset/eff-716-response-id-tracker-map.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use a normal Map in ResponseIdTracker and clear it on divergence / reset instead of reallocating a WeakMap. diff --git a/repos/effect/.changeset/eff-717-openai-socket-cancel.md b/repos/effect/.changeset/eff-717-openai-socket-cancel.md new file mode 100644 index 0000000..d4adf58 --- /dev/null +++ b/repos/effect/.changeset/eff-717-openai-socket-cancel.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Ensure OpenAiSocket sends a `{"type":"response.cancel"}` websocket event when a response stream is interrupted. diff --git a/repos/effect/.changeset/eff-718-embedding-model-surface.md b/repos/effect/.changeset/eff-718-embedding-model-surface.md new file mode 100644 index 0000000..e8754bd --- /dev/null +++ b/repos/effect/.changeset/eff-718-embedding-model-surface.md @@ -0,0 +1,13 @@ +--- +"effect": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Add unstable EmbeddingModel support across core and OpenAI providers. + +- Add the unstable EmbeddingModel module API surface in `effect`, including service, request, response, and provider types. +- Implement the unstable EmbeddingModel runtime constructor in `effect`, with `RequestResolver` batching, `embed` / `embedMany` spans, provider error propagation, deterministic ordering, and empty-input `embedMany` fast-path behavior. +- Add and align EmbeddingModel behavior tests in `effect` for embedding usage, batching, ordering, and error handling. +- Add `OpenAiEmbeddingModel` in `@effect/ai-openai`, including model / make / layer constructors, config overrides, and provider output index validation with deterministic reordering. +- Add OpenAI-compatible EmbeddingModel provider support in `@effect/ai-openai-compat`, including config overrides, layer constructors, and output index validation. diff --git a/repos/effect/.changeset/eff-725-fix-catch-jsdoc.md b/repos/effect/.changeset/eff-725-fix-catch-jsdoc.md new file mode 100644 index 0000000..a5ca6b5 --- /dev/null +++ b/repos/effect/.changeset/eff-725-fix-catch-jsdoc.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix JSDoc wording for `Effect.catch` to consistently reference the current API name. diff --git a/repos/effect/.changeset/eff-726-model-dimensions.md b/repos/effect/.changeset/eff-726-model-dimensions.md new file mode 100644 index 0000000..1a6022b --- /dev/null +++ b/repos/effect/.changeset/eff-726-model-dimensions.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Add `EmbeddingModel.ModelDimensions` and require dimensions in embedding provider `model` constructors. diff --git a/repos/effect/.changeset/eff-727-cli-help-alignment.md b/repos/effect/.changeset/eff-727-cli-help-alignment.md new file mode 100644 index 0000000..32e9ada --- /dev/null +++ b/repos/effect/.changeset/eff-727-cli-help-alignment.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Align CLI help flag and global flag descriptions to a single column even when some flag names are very long. diff --git a/repos/effect/.changeset/eff-730-language-model-incremental-fallback.md b/repos/effect/.changeset/eff-730-language-model-incremental-fallback.md new file mode 100644 index 0000000..55fa6a0 --- /dev/null +++ b/repos/effect/.changeset/eff-730-language-model-incremental-fallback.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `LanguageModel` incremental prompt fallback to reliably retry with the full prompt when an incremental request fails with `InvalidRequestError`. diff --git a/repos/effect/.changeset/eff-736-cached-with-ttl.md b/repos/effect/.changeset/eff-736-cached-with-ttl.md new file mode 100644 index 0000000..8ef0529 --- /dev/null +++ b/repos/effect/.changeset/eff-736-cached-with-ttl.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.cachedWithTTL` and `Effect.cachedInvalidateWithTTL` to start TTL expiration when the cached value is produced instead of when computation starts. diff --git a/repos/effect/.changeset/eff-738-cron-prev.md b/repos/effect/.changeset/eff-738-cron-prev.md new file mode 100644 index 0000000..b9109d4 --- /dev/null +++ b/repos/effect/.changeset/eff-738-cron-prev.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Backport `Cron.prev` with reverse lookup tables and cron stepping logic, including DST-aware reverse traversal. diff --git a/repos/effect/.changeset/eff-739-openai-function-call-done.md b/repos/effect/.changeset/eff-739-openai-function-call-done.md new file mode 100644 index 0000000..9069313 --- /dev/null +++ b/repos/effect/.changeset/eff-739-openai-function-call-done.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Handle streamed OpenAI function calls from `response.function_call_arguments.done` so tool calls are emitted even when `response.output_item.done` is missing. diff --git a/repos/effect/.changeset/eff-740-missing-summary-parts.md b/repos/effect/.changeset/eff-740-missing-summary-parts.md new file mode 100644 index 0000000..c481d83 --- /dev/null +++ b/repos/effect/.changeset/eff-740-missing-summary-parts.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix OpenAI reasoning stream state handling so out-of-order reasoning summary events do not crash when prior reasoning item state is missing. diff --git a/repos/effect/.changeset/eff-742-http-client-request-web.md b/repos/effect/.changeset/eff-742-http-client-request-web.md new file mode 100644 index 0000000..47c2016 --- /dev/null +++ b/repos/effect/.changeset/eff-742-http-client-request-web.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/http HttpClientRequest: add toWeb and fromWeb conversions for web Request objects diff --git a/repos/effect/.changeset/eff-744-sqlite-migrator-lock.md b/repos/effect/.changeset/eff-744-sqlite-migrator-lock.md new file mode 100644 index 0000000..badee2a --- /dev/null +++ b/repos/effect/.changeset/eff-744-sqlite-migrator-lock.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix sql migrator lock handling to only treat duplicate migration-row inserts as a concurrent migration lock. diff --git a/repos/effect/.changeset/eff-746-fixed-iteration-catchup.md b/repos/effect/.changeset/eff-746-fixed-iteration-catchup.md new file mode 100644 index 0000000..db267d5 --- /dev/null +++ b/repos/effect/.changeset/eff-746-fixed-iteration-catchup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.fixed` to run the next iteration immediately when the previous action takes longer than the configured interval. diff --git a/repos/effect/.changeset/eff-747-unify-effect.md b/repos/effect/.changeset/eff-747-unify-effect.md new file mode 100644 index 0000000..251737c --- /dev/null +++ b/repos/effect/.changeset/eff-747-unify-effect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Unify.unify` so unions of `Effect` values collapse to a single unified `Effect` type again. diff --git a/repos/effect/.changeset/eff-754-url-builder-any.md b/repos/effect/.changeset/eff-754-url-builder-any.md new file mode 100644 index 0000000..9dbd7bb --- /dev/null +++ b/repos/effect/.changeset/eff-754-url-builder-any.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Relax `HttpApiClient.urlBuilder` to accept `HttpApi.Any` instead of requiring `HttpApi.AnyWithProps`. +This allows use in helpers generic over `HttpApi.Any` while preserving inferred URL builder types. diff --git a/repos/effect/.changeset/eff-755-references-core.md b/repos/effect/.changeset/eff-755-references-core.md new file mode 100644 index 0000000..76715f9 --- /dev/null +++ b/repos/effect/.changeset/eff-755-references-core.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Re-export additional core runtime references from `effect/References`, including logger and error reporter references. diff --git a/repos/effect/.changeset/eff-769-select-text-highlight.md b/repos/effect/.changeset/eff-769-select-text-highlight.md new file mode 100644 index 0000000..f69988f --- /dev/null +++ b/repos/effect/.changeset/eff-769-select-text-highlight.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Highlight active option labels in `Prompt.select` and `Prompt.multiSelect` using cyan text so selection state is visible beyond the pointer / checkbox icon. diff --git a/repos/effect/.changeset/eff-774-mutable-list-append-all-empty-array.md b/repos/effect/.changeset/eff-774-mutable-list-append-all-empty-array.md new file mode 100644 index 0000000..7a3ee8c --- /dev/null +++ b/repos/effect/.changeset/eff-774-mutable-list-append-all-empty-array.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `MutableList.appendAll` / `appendAllUnsafe` so empty arrays are treated as a no-op instead of leaving behind an empty internal bucket. diff --git a/repos/effect/.changeset/eff-777-schema-make-effect.md b/repos/effect/.changeset/eff-777-schema-make-effect.md new file mode 100644 index 0000000..d80d615 --- /dev/null +++ b/repos/effect/.changeset/eff-777-schema-make-effect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `schema.makeEffect(input, options?)` to `Schema.Bottom` and schema-backed classes, matching the existing constructor behavior exposed by `makeUnsafe` / `makeOption` while returning an `Effect` failure with `Schema.SchemaError`. diff --git a/repos/effect/.changeset/eff-778-http-middleware-path-logger.md b/repos/effect/.changeset/eff-778-http-middleware-path-logger.md new file mode 100644 index 0000000..716bc47 --- /dev/null +++ b/repos/effect/.changeset/eff-778-http-middleware-path-logger.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update the unstable HTTP middleware logger to annotate only the request path in `http.url` instead of including the full URL (query / fragment), and add a regression test. diff --git a/repos/effect/.changeset/eff-779-keyvaluestore-layer-sql.md b/repos/effect/.changeset/eff-779-keyvaluestore-layer-sql.md new file mode 100644 index 0000000..ce80278 --- /dev/null +++ b/repos/effect/.changeset/eff-779-keyvaluestore-layer-sql.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `KeyValueStore.layerSql` to back key-value storage with a SQL database via `SqlClient`. diff --git a/repos/effect/.changeset/eff-780-layer-unify.md b/repos/effect/.changeset/eff-780-layer-unify.md new file mode 100644 index 0000000..59bc0c2 --- /dev/null +++ b/repos/effect/.changeset/eff-780-layer-unify.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Unify.unify` so Layer unions merge correctly, and add type tests covering Layer unification. diff --git a/repos/effect/.changeset/eff-781-fix-stream-toqueue-types.md b/repos/effect/.changeset/eff-781-fix-stream-toqueue-types.md new file mode 100644 index 0000000..3f3ff71 --- /dev/null +++ b/repos/effect/.changeset/eff-781-fix-stream-toqueue-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.toQueue` types and implementation to return a `Queue.Dequeue` in both overloads and delegate to `Channel.toQueueArray`. diff --git a/repos/effect/.changeset/eff-782-httpapi-status-literals.md b/repos/effect/.changeset/eff-782-httpapi-status-literals.md new file mode 100644 index 0000000..6eb4c1e --- /dev/null +++ b/repos/effect/.changeset/eff-782-httpapi-status-literals.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add support for common HTTP status string literals in `HttpApiSchema.status` (for example, `HttpApiSchema.status("Created")` resolves to status code `201`). diff --git a/repos/effect/.changeset/eff-783-atom-http-api-errors.md b/repos/effect/.changeset/eff-783-atom-http-api-errors.md new file mode 100644 index 0000000..f62f2ae --- /dev/null +++ b/repos/effect/.changeset/eff-783-atom-http-api-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `AtomHttpApi` query and mutation error inference to include endpoint middleware and client middleware errors, matching `HttpApiClient` behavior (including response-only mutation mode). diff --git a/repos/effect/.changeset/eff-819-cluster-workflow-shard-groups.md b/repos/effect/.changeset/eff-819-cluster-workflow-shard-groups.md new file mode 100644 index 0000000..852c583 --- /dev/null +++ b/repos/effect/.changeset/eff-819-cluster-workflow-shard-groups.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure ClusterWorkflowEngine routes durable clock wakeups and registered workflow deferred completions through the owning workflow's shard group. diff --git a/repos/effect/.changeset/eff-849-transpose-option.md b/repos/effect/.changeset/eff-849-transpose-option.md new file mode 100644 index 0000000..ccdbd42 --- /dev/null +++ b/repos/effect/.changeset/eff-849-transpose-option.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.transposeOption` for converting an `Option>` into an `Effect, E, R>`. diff --git a/repos/effect/.changeset/eight-turkeys-own.md b/repos/effect/.changeset/eight-turkeys-own.md new file mode 100644 index 0000000..1665cd0 --- /dev/null +++ b/repos/effect/.changeset/eight-turkeys-own.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-node": patch +"@effect/platform-bun": patch +"effect": patch +--- + +use cause annotations for detecting client aborts diff --git a/repos/effect/.changeset/eighty-lies-deny.md b/repos/effect/.changeset/eighty-lies-deny.md new file mode 100644 index 0000000..3b6e333 --- /dev/null +++ b/repos/effect/.changeset/eighty-lies-deny.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +change default ErrorReporter severity to Info diff --git a/repos/effect/.changeset/eighty-poets-draw.md b/repos/effect/.changeset/eighty-poets-draw.md new file mode 100644 index 0000000..27a23ed --- /dev/null +++ b/repos/effect/.changeset/eighty-poets-draw.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +--- + +Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. diff --git a/repos/effect/.changeset/eighty-swans-scream.md b/repos/effect/.changeset/eighty-swans-scream.md new file mode 100644 index 0000000..eff5092 --- /dev/null +++ b/repos/effect/.changeset/eighty-swans-scream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add HttpApiSchemaError for determining where a schema error originates from diff --git a/repos/effect/.changeset/eighty-teeth-sniff.md b/repos/effect/.changeset/eighty-teeth-sniff.md new file mode 100644 index 0000000..4ce98bc --- /dev/null +++ b/repos/effect/.changeset/eighty-teeth-sniff.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify Filter by removing Args type parameter diff --git a/repos/effect/.changeset/eleven-apes-share.md b/repos/effect/.changeset/eleven-apes-share.md new file mode 100644 index 0000000..4e63ebc --- /dev/null +++ b/repos/effect/.changeset/eleven-apes-share.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Add a new `OpenAiClientGenerated` module that exposes the generated OpenAI client as a dedicated context service with `make`, `layer`, and `layerConfig` constructors. This provides a compatibility path for direct generated-client access while preserving existing auth, base URL, header, and `OpenAiConfig.transformClient` wiring. diff --git a/repos/effect/.changeset/eleven-numbers-bake.md b/repos/effect/.changeset/eleven-numbers-bake.md new file mode 100644 index 0000000..7966ef8 --- /dev/null +++ b/repos/effect/.changeset/eleven-numbers-bake.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +pass workflow parent on discard diff --git a/repos/effect/.changeset/empty-env-values-missing.md b/repos/effect/.changeset/empty-env-values-missing.md new file mode 100644 index 0000000..cd894f9 --- /dev/null +++ b/repos/effect/.changeset/empty-env-values-missing.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Treat empty strings as missing values in built-in `ConfigProvider`s by default. + +`ConfigProvider.fromEnv`, `ConfigProvider.fromDotEnvContents`, `ConfigProvider.fromDotEnv`, `ConfigProvider.fromUnknown`, and `ConfigProvider.fromDir` now treat literal empty strings as absent values when loaded as values, allowing `Config.withDefault` and `Config.option` to recover. Container discovery still reflects the source structure. Pass `preserveEmptyStrings: true` to restore the previous behavior. + +`ConfigProvider.fromDotEnv({ expandVariables: true })` now expands variables consistently with `ConfigProvider.fromDotEnvContents`. diff --git a/repos/effect/.changeset/empty-gifts-beg.md b/repos/effect/.changeset/empty-gifts-beg.md new file mode 100644 index 0000000..c7d3982 --- /dev/null +++ b/repos/effect/.changeset/empty-gifts-beg.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +process schema properties / elements concurrently diff --git a/repos/effect/.changeset/empty-http-rpc-client.md b/repos/effect/.changeset/empty-http-rpc-client.md new file mode 100644 index 0000000..36685f7 --- /dev/null +++ b/repos/effect/.changeset/empty-http-rpc-client.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fail RpcClient HTTP requests when the server response contains no RPC messages instead of leaving requests pending. diff --git a/repos/effect/.changeset/eventlog-unencrypted.md b/repos/effect/.changeset/eventlog-unencrypted.md new file mode 100644 index 0000000..2a3e8a8 --- /dev/null +++ b/repos/effect/.changeset/eventlog-unencrypted.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add EventLogServerUnencrypted module diff --git a/repos/effect/.changeset/every-olives-burn.md b/repos/effect/.changeset/every-olives-burn.md new file mode 100644 index 0000000..87e34c2 --- /dev/null +++ b/repos/effect/.changeset/every-olives-burn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +change default operation in redis from LPUSH TO RPUSH diff --git a/repos/effect/.changeset/expand-schema-filter-output.md b/repos/effect/.changeset/expand-schema-filter-output.md new file mode 100644 index 0000000..c269804 --- /dev/null +++ b/repos/effect/.changeset/expand-schema-filter-output.md @@ -0,0 +1,24 @@ +--- +"effect": patch +--- + +Schema: expand `FilterOutput` and add `FilterIssue` for richer filter failures. + +The return type of a `Schema.makeFilter` predicate now supports two additional shapes: + +- `{ path, issue }` where `issue` is `string | SchemaIssue.Issue` (previously only `{ path, message: string }` was accepted). The `issue` arm lets you attach a fully-formed `Issue` at a nested path without manually constructing a `Pointer`. +- `ReadonlyArray` to report several failures at once. An empty array is success, a single-element array is equivalent to returning that element, and multi-entry arrays are grouped into an `Issue.Composite`. This removes the need to import `SchemaIssue` and hand-build a `Composite` for multi-field validators. + +The single-failure shapes (`undefined`, `true`, `false`, `string`, `SchemaIssue.Issue`) are unchanged. + +**Breaking**: the object shape renamed from `{ path, message }` to `{ path, issue }`. Call sites that used the old shape must rename the field; the migration is mechanical. + +```ts +// before +Schema.makeFilter((o) => ({ path: ["a"], message: "bad" })) + +// after +Schema.makeFilter((o) => ({ path: ["a"], issue: "bad" })) +``` + +Also renamed `{ path, message }` to `{ path, issue }` in the accepted return type of `SchemaGetter.checkEffect`. diff --git a/repos/effect/.changeset/export-schema-encode-keys-interface.md b/repos/effect/.changeset/export-schema-encode-keys-interface.md new file mode 100644 index 0000000..2290cd6 --- /dev/null +++ b/repos/effect/.changeset/export-schema-encode-keys-interface.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Export the `Schema.encodeKeys` interface, closes #2070. + +Previously the interface was internal, so exporting a value whose inferred type referenced it triggered TypeScript error `TS4023: Exported variable has or is using name 'encodeKeys' from external module ... but cannot be named`, e.g.: diff --git a/repos/effect/.changeset/extract-semaphore-latch.md b/repos/effect/.changeset/extract-semaphore-latch.md new file mode 100644 index 0000000..755512b --- /dev/null +++ b/repos/effect/.changeset/extract-semaphore-latch.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Extract `Semaphore` and `Latch` into their own modules. + +`Semaphore.make` / `Semaphore.makeUnsafe` replace `Effect.makeSemaphore` / `Effect.makeSemaphoreUnsafe`. +`Latch.make` / `Latch.makeUnsafe` replace `Effect.makeLatch` / `Effect.makeLatchUnsafe`. + +Merge `PartitionedSemaphore` into `Semaphore` as `Semaphore.Partitioned`, `Semaphore.makePartitioned`, `Semaphore.makePartitionedUnsafe`. diff --git a/repos/effect/.changeset/fair-bees-relax.md b/repos/effect/.changeset/fair-bees-relax.md new file mode 100644 index 0000000..4e711ae --- /dev/null +++ b/repos/effect/.changeset/fair-bees-relax.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix missing `yield*` in `OpenAiLanguageModel.prepareResponseFormat` diff --git a/repos/effect/.changeset/fair-birds-limit.md b/repos/effect/.changeset/fair-birds-limit.md new file mode 100644 index 0000000..c729574 --- /dev/null +++ b/repos/effect/.changeset/fair-birds-limit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Schedule.upTo options for limiting schedules by duration and/or recurrence count. diff --git a/repos/effect/.changeset/fair-buttons-share.md b/repos/effect/.changeset/fair-buttons-share.md new file mode 100644 index 0000000..397f129 --- /dev/null +++ b/repos/effect/.changeset/fair-buttons-share.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Make `AtomRpc.query` and `AtomHttpApi.query` return serializable atoms by default when query results are schema-backed. + +The atom serialization key now uses each API's built-in request schemas so dehydrated state can be keyed consistently across server and client. diff --git a/repos/effect/.changeset/fair-cooks-stop.md b/repos/effect/.changeset/fair-cooks-stop.md new file mode 100644 index 0000000..9aabf7f --- /dev/null +++ b/repos/effect/.changeset/fair-cooks-stop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schedule.jittered` to randomize schedule delays between 80% and 120% of the original delay. diff --git a/repos/effect/.changeset/fair-cups-train.md b/repos/effect/.changeset/fair-cups-train.md new file mode 100644 index 0000000..d72aa2f --- /dev/null +++ b/repos/effect/.changeset/fair-cups-train.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TxPubSub.publish` and `TxPubSub.publishAll` overloads to require `Effect.Transaction` in their return environment. diff --git a/repos/effect/.changeset/fair-dryers-speak.md b/repos/effect/.changeset/fair-dryers-speak.md new file mode 100644 index 0000000..cef623e --- /dev/null +++ b/repos/effect/.changeset/fair-dryers-speak.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpClientRequest.bodyFormDataRecord` and `HttpBody.makeFormDataRecord` helpers for creating multipart form bodies from plain records. diff --git a/repos/effect/.changeset/fair-forks-shake.md b/repos/effect/.changeset/fair-forks-shake.md new file mode 100644 index 0000000..395d1ec --- /dev/null +++ b/repos/effect/.changeset/fair-forks-shake.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix unstable CLI boolean flags so `Flag.optional(Flag.boolean(...))` returns `Option.none()` when omitted, and support canonical `--no-` negation for boolean flags. diff --git a/repos/effect/.changeset/fair-jobs-like.md b/repos/effect/.changeset/fair-jobs-like.md new file mode 100644 index 0000000..e985873 --- /dev/null +++ b/repos/effect/.changeset/fair-jobs-like.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add a `radius` option to `Graph` search configuration, allowing `dfs`, `bfs`, and `dfsPostOrder` traversals to limit returned nodes by edge distance from the configured start nodes. Traversals can also use `direction: "undirected"` to follow edges in either direction. diff --git a/repos/effect/.changeset/fair-pandas-prove.md b/repos/effect/.changeset/fair-pandas-prove.md new file mode 100644 index 0000000..d88f5fb --- /dev/null +++ b/repos/effect/.changeset/fair-pandas-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Changed socket close handling so all close codes are treated as errors by default unless `closeCodeIsError` is overridden. diff --git a/repos/effect/.changeset/fair-pants-float.md b/repos/effect/.changeset/fair-pants-float.md new file mode 100644 index 0000000..020c76a --- /dev/null +++ b/repos/effect/.changeset/fair-pants-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a race in `Semaphore.take` where interruption could leak permits after a waiter was resumed. diff --git a/repos/effect/.changeset/fair-poems-visit.md b/repos/effect/.changeset/fair-poems-visit.md new file mode 100644 index 0000000..3aa7a73 --- /dev/null +++ b/repos/effect/.changeset/fair-poems-visit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpServerRequest.toClientRequest` for direct server-to-client request conversion. diff --git a/repos/effect/.changeset/famous-wolves-lead.md b/repos/effect/.changeset/famous-wolves-lead.md new file mode 100644 index 0000000..19f2bba --- /dev/null +++ b/repos/effect/.changeset/famous-wolves-lead.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +don't omit reasoning from openai config diff --git a/repos/effect/.changeset/fancy-glasses-grow.md b/repos/effect/.changeset/fancy-glasses-grow.md new file mode 100644 index 0000000..1f55db9 --- /dev/null +++ b/repos/effect/.changeset/fancy-glasses-grow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add IndexedDb modules diff --git a/repos/effect/.changeset/fast-graph-path-queues.md b/repos/effect/.changeset/fast-graph-path-queues.md new file mode 100644 index 0000000..66d1881 --- /dev/null +++ b/repos/effect/.changeset/fast-graph-path-queues.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve `Graph.dijkstra` and `Graph.astar` priority queue performance. diff --git a/repos/effect/.changeset/fast-times-camp.md b/repos/effect/.changeset/fast-times-camp.md new file mode 100644 index 0000000..f44c6cb --- /dev/null +++ b/repos/effect/.changeset/fast-times-camp.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allocate less effects when reading a file diff --git a/repos/effect/.changeset/few-birds-matter.md b/repos/effect/.changeset/few-birds-matter.md new file mode 100644 index 0000000..66fdca8 --- /dev/null +++ b/repos/effect/.changeset/few-birds-matter.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Allow custom request properties in openai-compat model config and chat request types, and forward model-level custom properties to chat-completions payloads. diff --git a/repos/effect/.changeset/few-cougars-dig.md b/repos/effect/.changeset/few-cougars-dig.md new file mode 100644 index 0000000..86055e6 --- /dev/null +++ b/repos/effect/.changeset/few-cougars-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effect.abortSignal diff --git a/repos/effect/.changeset/few-foxes-grin.md b/repos/effect/.changeset/few-foxes-grin.md new file mode 100644 index 0000000..8d7631f --- /dev/null +++ b/repos/effect/.changeset/few-foxes-grin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Command.withExamples` to attach concrete usage examples to CLI commands, expose them through `HelpDoc.examples`, and render them in the default help formatter. diff --git a/repos/effect/.changeset/few-mirrors-pull.md b/repos/effect/.changeset/few-mirrors-pull.md new file mode 100644 index 0000000..dd1bee2 --- /dev/null +++ b/repos/effect/.changeset/few-mirrors-pull.md @@ -0,0 +1,14 @@ +--- +"effect": patch +--- + +Make CLI global settings directly yieldable and simplify built-in names. + +`GlobalFlag.setting` now takes `{ flag, defaultValue }` and returns a setting that is a `Context.Reference`, so handlers and `Command.provide*` effects can `yield*` global setting values directly. + +Built-in settings keep internal behavior in `runWith` (for example, `--log-level` still configures `References.MinimumLogLevel`) while also being readable as values. + +Also renamed built-in globals: + +- `GlobalFlag.CompletionsFlag` -> `GlobalFlag.Completions` +- `GlobalFlag.LogLevelFlag` -> `GlobalFlag.LogLevel` diff --git a/repos/effect/.changeset/few-socks-poke.md b/repos/effect/.changeset/few-socks-poke.md new file mode 100644 index 0000000..cdc249e --- /dev/null +++ b/repos/effect/.changeset/few-socks-poke.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Accept both `in-memory` and `in_memory` for OpenAI `prompt_cache_retention` schema fields. diff --git a/repos/effect/.changeset/fiber-runtime-start-metrics.md b/repos/effect/.changeset/fiber-runtime-start-metrics.md new file mode 100644 index 0000000..212fc64 --- /dev/null +++ b/repos/effect/.changeset/fiber-runtime-start-metrics.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Record fiber runtime start metrics when fibers are constructed so yielded fibers are only counted once. diff --git a/repos/effect/.changeset/fiery-jokes-care.md b/repos/effect/.changeset/fiery-jokes-care.md new file mode 100644 index 0000000..0093f23 --- /dev/null +++ b/repos/effect/.changeset/fiery-jokes-care.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Entity.keepAlive diff --git a/repos/effect/.changeset/fiery-mammals-call.md b/repos/effect/.changeset/fiery-mammals-call.md new file mode 100644 index 0000000..2fce3b1 --- /dev/null +++ b/repos/effect/.changeset/fiery-mammals-call.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure tagged enum \_tag is correctly set diff --git a/repos/effect/.changeset/fine-walls-decide.md b/repos/effect/.changeset/fine-walls-decide.md new file mode 100644 index 0000000..7f6e67d --- /dev/null +++ b/repos/effect/.changeset/fine-walls-decide.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +don't remove SIGINT listener until fiber exit diff --git a/repos/effect/.changeset/first-success-of.md b/repos/effect/.changeset/first-success-of.md new file mode 100644 index 0000000..e31e679 --- /dev/null +++ b/repos/effect/.changeset/first-success-of.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `Effect.firstSuccessOf` from Effect v3. diff --git a/repos/effect/.changeset/five-parents-relax.md b/repos/effect/.changeset/five-parents-relax.md new file mode 100644 index 0000000..15192e8 --- /dev/null +++ b/repos/effect/.changeset/five-parents-relax.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +allow customizing idb durability diff --git a/repos/effect/.changeset/five-worms-rhyme.md b/repos/effect/.changeset/five-worms-rhyme.md new file mode 100644 index 0000000..01707d1 --- /dev/null +++ b/repos/effect/.changeset/five-worms-rhyme.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix eventlog skipping entries diff --git a/repos/effect/.changeset/fix-1332.md b/repos/effect/.changeset/fix-1332.md new file mode 100644 index 0000000..4ab4fa1 --- /dev/null +++ b/repos/effect/.changeset/fix-1332.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: avoid eager resolution for type-level helpers, closes #1332 diff --git a/repos/effect/.changeset/fix-1917.md b/repos/effect/.changeset/fix-1917.md new file mode 100644 index 0000000..f7cf342 --- /dev/null +++ b/repos/effect/.changeset/fix-1917.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Simplify and align the default-value APIs. + +`Schema.withConstructorDefault` now accepts an `Effect` instead of `(o: Option) => Option | Effect>`. + +`Schema.withDecodingDefault` / `Schema.withDecodingDefaultKey` now accept an `Effect` instead of `() => T`, enabling effectful defaults. + +`SchemaGetter.withDefault` follows the same change, accepting `Effect` instead of `() => T`. diff --git a/repos/effect/.changeset/fix-1927.md b/repos/effect/.changeset/fix-1927.md new file mode 100644 index 0000000..7882a21 --- /dev/null +++ b/repos/effect/.changeset/fix-1927.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reuse existing references when duplicate identifiers have the same representation, closes #1927. diff --git a/repos/effect/.changeset/fix-1940.md b/repos/effect/.changeset/fix-1940.md new file mode 100644 index 0000000..be9ef1a --- /dev/null +++ b/repos/effect/.changeset/fix-1940.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `ErrorClass` and `TaggedErrorClass` `toString` to match native `Error` output format (e.g. `E: my message` instead of `E({"message":"my message"})`), closes #1940. + +Also fix prototype properties (e.g. `name`) being lost after `.extend()`. diff --git a/repos/effect/.changeset/fix-1947.md b/repos/effect/.changeset/fix-1947.md new file mode 100644 index 0000000..f5cd72f --- /dev/null +++ b/repos/effect/.changeset/fix-1947.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Add `Schema.resolveAnnotationsKey` API to retrieve the context (key-level) annotations from a schema, closes #1947. + +Also rename `Schema.resolveInto` to `Schema.resolveAnnotations`. diff --git a/repos/effect/.changeset/fix-2002.md b/repos/effect/.changeset/fix-2002.md new file mode 100644 index 0000000..a2de203 --- /dev/null +++ b/repos/effect/.changeset/fix-2002.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.splitLines` to correctly handle standalone `\r` as a line terminator and flush the final unterminated line when the stream ends, closes #2002. diff --git a/repos/effect/.changeset/fix-2012.md b/repos/effect/.changeset/fix-2012.md new file mode 100644 index 0000000..6b7ae93 --- /dev/null +++ b/repos/effect/.changeset/fix-2012.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add withDecodingDefaultTypeKey / withDecodingDefaultType, closes #2012 diff --git a/repos/effect/.changeset/fix-2015.md b/repos/effect/.changeset/fix-2015.md new file mode 100644 index 0000000..0675306 --- /dev/null +++ b/repos/effect/.changeset/fix-2015.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: allow `Class` constructors to accept `void` when all fields are optional, closes #2015. diff --git a/repos/effect/.changeset/fix-2260.md b/repos/effect/.changeset/fix-2260.md new file mode 100644 index 0000000..dddb2c1 --- /dev/null +++ b/repos/effect/.changeset/fix-2260.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add JSON Schema custom annotation passthrough option, closes #2260 diff --git a/repos/effect/.changeset/fix-2268.md b/repos/effect/.changeset/fix-2268.md new file mode 100644 index 0000000..b0977d4 --- /dev/null +++ b/repos/effect/.changeset/fix-2268.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: reintroduce `.value` on `Schema.Array` and `Schema.NonEmptyArray` for consistency with other collection wrappers (`Chunk`, `HashSet`, etc.), closes #2268. diff --git a/repos/effect/.changeset/fix-2271.md b/repos/effect/.changeset/fix-2271.md new file mode 100644 index 0000000..1be94c3 --- /dev/null +++ b/repos/effect/.changeset/fix-2271.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Clarify that `Data.$is(tag)` only checks the `_tag` field, not the full structure, closes #2271. diff --git a/repos/effect/.changeset/fix-2384.md b/repos/effect/.changeset/fix-2384.md new file mode 100644 index 0000000..6494902 --- /dev/null +++ b/repos/effect/.changeset/fix-2384.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +`Config.withDefault` now only recovers from missing data for literal/union +schemas. Invalid present values now propagate validation errors instead of +using the default, closes #2384. diff --git a/repos/effect/.changeset/fix-2414.md b/repos/effect/.changeset/fix-2414.md new file mode 100644 index 0000000..984fba9 --- /dev/null +++ b/repos/effect/.changeset/fix-2414.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Derive template literal arbitraries from encoded parts, closes #2414. diff --git a/repos/effect/.changeset/fix-2419.md b/repos/effect/.changeset/fix-2419.md new file mode 100644 index 0000000..ec9b13b --- /dev/null +++ b/repos/effect/.changeset/fix-2419.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow schema class `.extend` to accept a `Struct` and preserve checks from the extension schema, closes #2419. diff --git a/repos/effect/.changeset/fix-2497.md b/repos/effect/.changeset/fix-2497.md new file mode 100644 index 0000000..4425db7 --- /dev/null +++ b/repos/effect/.changeset/fix-2497.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.andThenResult` to emit `self` outputs as `Failure` and `other` outputs as `Success`, closes #2497. diff --git a/repos/effect/.changeset/fix-2499.md b/repos/effect/.changeset/fix-2499.md new file mode 100644 index 0000000..9e26f84 --- /dev/null +++ b/repos/effect/.changeset/fix-2499.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix excess property handling in schema-backed class constructors, closes #2499. diff --git a/repos/effect/.changeset/fix-6464.md b/repos/effect/.changeset/fix-6464.md new file mode 100644 index 0000000..13d7b52 --- /dev/null +++ b/repos/effect/.changeset/fix-6464.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Distribute `HttpApiBuilder` handler requirements per service so request middleware layers can provide them, closes #6464. diff --git a/repos/effect/.changeset/fix-ai-empty-params-structured-output.md b/repos/effect/.changeset/fix-ai-empty-params-structured-output.md new file mode 100644 index 0000000..aa38068 --- /dev/null +++ b/repos/effect/.changeset/fix-ai-empty-params-structured-output.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve unstable AI structured output handling for empty tool params and add `Tool.EmptyParams`, closes #1749. diff --git a/repos/effect/.changeset/fix-ai-text-toolkit-typing.md b/repos/effect/.changeset/fix-ai-text-toolkit-typing.md new file mode 100644 index 0000000..da37eea --- /dev/null +++ b/repos/effect/.changeset/fix-ai-text-toolkit-typing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix AI text method toolkit typing to support generic handler toolkits, preserve toolkit union inference, and keep response part narrowing by tool name. diff --git a/repos/effect/.changeset/fix-anthropic-caller-toolid.md b/repos/effect/.changeset/fix-anthropic-caller-toolid.md new file mode 100644 index 0000000..866bc4d --- /dev/null +++ b/repos/effect/.changeset/fix-anthropic-caller-toolid.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix non-streaming Anthropic responses throwing when a tool call carries `caller` metadata. The mapper emitted `caller.toolId: undefined`, but `ProviderMetadata` is `Record(String, NullOr(Json))` and `undefined` is not a valid Json value, so decoding the model's own response threw `Expected JSON value`. Emit `null` instead, matching the streaming mappers. diff --git a/repos/effect/.changeset/fix-anthropic-memory-tool-requires-handler.md b/repos/effect/.changeset/fix-anthropic-memory-tool-requires-handler.md new file mode 100644 index 0000000..f9bd984 --- /dev/null +++ b/repos/effect/.changeset/fix-anthropic-memory-tool-requires-handler.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix `Memory_20250818` provider-defined tool missing `requiresHandler: true`. Like the other client-executed tools (`TextEditor_20250728`, `Bash_2025*`, `ComputerUse_2025*`), the memory tool requires the application to implement its execution (view/create/str_replace/insert/delete/rename over `/memories/*`). Without this flag, `Tool.HandlersFor` excluded it from the required handlers, making it impossible to type-check a handler for `Memory_20250818` in `Toolkit.toLayer`. diff --git a/repos/effect/.changeset/fix-anthropic-memory-tool.md b/repos/effect/.changeset/fix-anthropic-memory-tool.md new file mode 100644 index 0000000..3a9a9cd --- /dev/null +++ b/repos/effect/.changeset/fix-anthropic-memory-tool.md @@ -0,0 +1,11 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix client-executed provider tools (Memory, Text Editor, Computer Use, Bash) which were unusable on the wire. + +- `makeResponse` (and the streaming equivalents) now map a provider `tool_use` wire name (e.g. `"memory"`) back to the tool's custom name (e.g. `"AnthropicMemory"`) that the toolkit is keyed by, instead of raising `ToolNotFoundError`. +- `AnthropicTool.MemoryCreateCommand` now includes the required `file_text` field, so a `create` command no longer drops the file body. +- Optional parameters on client-executed provider tools now use `Schema.optionalKey` instead of `Schema.optional`, which the Anthropic codec rejected with "Unsupported AST Undefined": `Memory`/`TextEditor` `view_range`, `ComputerUse` `coordinate`, and `Bash` `restart`. + +Closes #2615. diff --git a/repos/effect/.changeset/fix-atom-kvs-async-write.md b/repos/effect/.changeset/fix-atom-kvs-async-write.md new file mode 100644 index 0000000..2227e7a --- /dev/null +++ b/repos/effect/.changeset/fix-atom-kvs-async-write.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Atom.kvs` async mode to retain its `AsyncResult` value shape after writes. diff --git a/repos/effect/.changeset/fix-catch-orelse-error-erasure.md b/repos/effect/.changeset/fix-catch-orelse-error-erasure.md new file mode 100644 index 0000000..0f4846b --- /dev/null +++ b/repos/effect/.changeset/fix-catch-orelse-error-erasure.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fixed the `catch*` combinators silently dropping unhandled error types diff --git a/repos/effect/.changeset/fix-class-constructor-defaults.md b/repos/effect/.changeset/fix-class-constructor-defaults.md new file mode 100644 index 0000000..8e9e824 --- /dev/null +++ b/repos/effect/.changeset/fix-class-constructor-defaults.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: rename `MakeOptions.disableValidation` to `disableChecks`. Apply constructor defaults when `disableChecks` is true, closes #1841. diff --git a/repos/effect/.changeset/fix-cli-missing-flag-values.md b/repos/effect/.changeset/fix-cli-missing-flag-values.md new file mode 100644 index 0000000..0dfb373 --- /dev/null +++ b/repos/effect/.changeset/fix-cli-missing-flag-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report an error when a CLI flag, including `--completions`, is provided without its required value. diff --git a/repos/effect/.changeset/fix-cli-mixed-global-flag-context.md b/repos/effect/.changeset/fix-cli-mixed-global-flag-context.md new file mode 100644 index 0000000..73ab0d1 --- /dev/null +++ b/repos/effect/.changeset/fix-cli-mixed-global-flag-context.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Command.withGlobalFlags` type inference when mixing `GlobalFlag.action` and `GlobalFlag.setting`. + +`Setting` service identifiers are now correctly removed from command requirements in mixed global flag arrays. diff --git a/repos/effect/.changeset/fix-cli-subcommands-requirements.md b/repos/effect/.changeset/fix-cli-subcommands-requirements.md new file mode 100644 index 0000000..5635267 --- /dev/null +++ b/repos/effect/.changeset/fix-cli-subcommands-requirements.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +- Fix `Command.withSubcommands` collapsing the inferred requirements type to `never` when given more than one subcommand +- Export a `Command.Services` utility type to extract the required services from a `Command` diff --git a/repos/effect/.changeset/fix-cluster-entity-context-bleed.md b/repos/effect/.changeset/fix-cluster-entity-context-bleed.md new file mode 100644 index 0000000..c9037d3 --- /dev/null +++ b/repos/effect/.changeset/fix-cluster-entity-context-bleed.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use registration context for cluster entities diff --git a/repos/effect/.changeset/fix-config-array-default.md b/repos/effect/.changeset/fix-config-array-default.md new file mode 100644 index 0000000..129e3d1 --- /dev/null +++ b/repos/effect/.changeset/fix-config-array-default.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Config.schema` so missing array values are treated as missing data, allowing `Config.withDefault` to apply. diff --git a/repos/effect/.changeset/fix-config-withDefault.md b/repos/effect/.changeset/fix-config-withDefault.md new file mode 100644 index 0000000..a0ce253 --- /dev/null +++ b/repos/effect/.changeset/fix-config-withDefault.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Config.withDefault` type inference, closes #1530. diff --git a/repos/effect/.changeset/fix-config-withdefault-filter.md b/repos/effect/.changeset/fix-config-withdefault-filter.md new file mode 100644 index 0000000..ddddc94 --- /dev/null +++ b/repos/effect/.changeset/fix-config-withdefault-filter.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +`Config.withDefault` no longer recovers from schema filter failures. A filter +failure means a present value reached refinement checks, so using the default +could hide invalid configuration values. diff --git a/repos/effect/.changeset/fix-cron-and-representations.md b/repos/effect/.changeset/fix-cron-and-representations.md new file mode 100644 index 0000000..064eeb3 --- /dev/null +++ b/repos/effect/.changeset/fix-cron-and-representations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Cron` day and weekday intersection semantics in inspection representations. diff --git a/repos/effect/.changeset/fix-cron-make-validation.md b/repos/effect/.changeset/fix-cron-make-validation.md new file mode 100644 index 0000000..5f9e350 --- /dev/null +++ b/repos/effect/.changeset/fix-cron-make-validation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Validate `Cron.make` field constraints and treat weekday `7` as Sunday consistently with cron parsing. diff --git a/repos/effect/.changeset/fix-cron-next-missing-day-overflow.md b/repos/effect/.changeset/fix-cron-next-missing-day-overflow.md new file mode 100644 index 0000000..6b392d1 --- /dev/null +++ b/repos/effect/.changeset/fix-cron-next-missing-day-overflow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.next` skipping earlier matching days when the upcoming day-of-month does not exist in the current month. diff --git a/repos/effect/.changeset/fix-cron-parser-semantics.md b/repos/effect/.changeset/fix-cron-parser-semantics.md new file mode 100644 index 0000000..840d6f0 --- /dev/null +++ b/repos/effect/.changeset/fix-cron-parser-semantics.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix cron parsing and scheduling edge cases for whitespace, Sunday `7`, strict numeric tokens, explicit full day ranges, and month-constrained day-of-month / weekday matching. diff --git a/repos/effect/.changeset/fix-cron-prev-month-rollover.md b/repos/effect/.changeset/fix-cron-prev-month-rollover.md new file mode 100644 index 0000000..8085ddc --- /dev/null +++ b/repos/effect/.changeset/fix-cron-prev-month-rollover.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.prev` day-of-month rollover across shorter months and non-leap years. diff --git a/repos/effect/.changeset/fix-cron-prev-weekday-wrap.md b/repos/effect/.changeset/fix-cron-prev-weekday-wrap.md new file mode 100644 index 0000000..e4b8307 --- /dev/null +++ b/repos/effect/.changeset/fix-cron-prev-weekday-wrap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.prev` weekday wrapping to always return a matching instant before the input. diff --git a/repos/effect/.changeset/fix-cron-timezone-hash.md b/repos/effect/.changeset/fix-cron-timezone-hash.md new file mode 100644 index 0000000..6ee572d --- /dev/null +++ b/repos/effect/.changeset/fix-cron-timezone-hash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make Cron equality and hashing include the optional timezone consistently. diff --git a/repos/effect/.changeset/fix-datetime-gmt.md b/repos/effect/.changeset/fix-datetime-gmt.md new file mode 100644 index 0000000..02baaa8 --- /dev/null +++ b/repos/effect/.changeset/fix-datetime-gmt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `DateTime.makeUnsafe` incorrectly appending "Z" to date strings containing "GMT" diff --git a/repos/effect/.changeset/fix-devtools-flush-on-teardown.md b/repos/effect/.changeset/fix-devtools-flush-on-teardown.md new file mode 100644 index 0000000..d5c46f9 --- /dev/null +++ b/repos/effect/.changeset/fix-devtools-flush-on-teardown.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Fix DevToolsClient not flushing final span events on teardown. + +The stream consumer was `forkScoped`, causing it to be interrupted before +it could drain remaining queue items. Replaced with `forkChild` and +`Fiber.await` in the finalizer so the stream drains naturally after the +queue is failed. diff --git a/repos/effect/.changeset/fix-durable-race-replay.md b/repos/effect/.changeset/fix-durable-race-replay.md new file mode 100644 index 0000000..2b0e3b2 --- /dev/null +++ b/repos/effect/.changeset/fix-durable-race-replay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix replay of persisted `DurableDeferred.raceAll` results. diff --git a/repos/effect/.changeset/fix-duration-symmetric-rounding.md b/repos/effect/.changeset/fix-duration-symmetric-rounding.md new file mode 100644 index 0000000..2fdb282 --- /dev/null +++ b/repos/effect/.changeset/fix-duration-symmetric-rounding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Round fractional durations symmetrically when normalizing to nanoseconds. diff --git a/repos/effect/.changeset/fix-entity-manager-defect-replay.md b/repos/effect/.changeset/fix-entity-manager-defect-replay.md new file mode 100644 index 0000000..5428fc6 --- /dev/null +++ b/repos/effect/.changeset/fix-entity-manager-defect-replay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix EntityManager defect restarts so in-flight requests are replayed instead of being dropped when the old entity scope is interrupted. diff --git a/repos/effect/.changeset/fix-entity-proxy-rpc-handler-context.md b/repos/effect/.changeset/fix-entity-proxy-rpc-handler-context.md new file mode 100644 index 0000000..9b4e1c3 --- /dev/null +++ b/repos/effect/.changeset/fix-entity-proxy-rpc-handler-context.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix entity proxy RPC handlers to provide the context expected by RpcServer. diff --git a/repos/effect/.changeset/fix-entity-proxy-server-path-params.md b/repos/effect/.changeset/fix-entity-proxy-server-path-params.md new file mode 100644 index 0000000..2cbf13f --- /dev/null +++ b/repos/effect/.changeset/fix-entity-proxy-server-path-params.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `EntityProxyServer.layerHttpApi` using `path.entityId` instead of `params.entityId` diff --git a/repos/effect/.changeset/fix-from-json-string-identifier.md b/repos/effect/.changeset/fix-from-json-string-identifier.md new file mode 100644 index 0000000..b2e478f --- /dev/null +++ b/repos/effect/.changeset/fix-from-json-string-identifier.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve content schema identifiers when emitting JSON Schema for `Schema.fromJsonString`. + +This keeps user-defined identifiers attached to the decoded JSON payload while giving the generated JSON string wrapper its own derived name, avoiding client codegen outputs where the payload type is renamed behind the transport wrapper. diff --git a/repos/effect/.changeset/fix-from-readable-stream-cancel-defect.md b/repos/effect/.changeset/fix-from-readable-stream-cancel-defect.md new file mode 100644 index 0000000..4c70506 --- /dev/null +++ b/repos/effect/.changeset/fix-from-readable-stream-cancel-defect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Stream.fromReadableStream: swallow the `reader.cancel()` rejection in the finalizer. Cancelling the reader of an already-errored ReadableStream rejects with the stored error, which turned the typed `onError` failure into a defect. diff --git a/repos/effect/.changeset/fix-graph-allocator-equality.md b/repos/effect/.changeset/fix-graph-allocator-equality.md new file mode 100644 index 0000000..d796cad --- /dev/null +++ b/repos/effect/.changeset/fix-graph-allocator-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix immutable Graph equality and hashing to include future node and edge identifier allocation. diff --git a/repos/effect/.changeset/fix-graph-bellman-ford-self-cycle.md b/repos/effect/.changeset/fix-graph-bellman-ford-self-cycle.md new file mode 100644 index 0000000..07dcc32 --- /dev/null +++ b/repos/effect/.changeset/fix-graph-bellman-ford-self-cycle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.bellmanFord` to detect reachable negative cycles when the source and target are the same node. diff --git a/repos/effect/.changeset/fix-graph-curried-getters.md b/repos/effect/.changeset/fix-graph-curried-getters.md new file mode 100644 index 0000000..4d79479 --- /dev/null +++ b/repos/effect/.changeset/fix-graph-curried-getters.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix standalone data-last `Graph.getNode` and `Graph.getEdge` inference. diff --git a/repos/effect/.changeset/fix-graph-edge-transforms.md b/repos/effect/.changeset/fix-graph-edge-transforms.md new file mode 100644 index 0000000..e0c410e --- /dev/null +++ b/repos/effect/.changeset/fix-graph-edge-transforms.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.mapEdges` and `Graph.filterMapEdges` to preserve `Graph.Edge` instances when transforming edge data. diff --git a/repos/effect/.changeset/fix-graph-finite-edge-weights.md b/repos/effect/.changeset/fix-graph-finite-edge-weights.md new file mode 100644 index 0000000..171304d --- /dev/null +++ b/repos/effect/.changeset/fix-graph-finite-edge-weights.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject `NaN` and `-Infinity` edge weights in Graph shortest-path algorithms. diff --git a/repos/effect/.changeset/fix-graph-mutable-hash.md b/repos/effect/.changeset/fix-graph-mutable-hash.md new file mode 100644 index 0000000..6246309 --- /dev/null +++ b/repos/effect/.changeset/fix-graph-mutable-hash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix mutable Graph equality and hashing to use reference identity while preserving structural semantics for immutable graphs. diff --git a/repos/effect/.changeset/fix-graph-mutable-topo.md b/repos/effect/.changeset/fix-graph-mutable-topo.md new file mode 100644 index 0000000..8a929bd --- /dev/null +++ b/repos/effect/.changeset/fix-graph-mutable-topo.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix topological walkers silently completing with an incomplete order when a mutable graph becomes cyclic after walker creation. diff --git a/repos/effect/.changeset/fix-graph-topo-types.md b/repos/effect/.changeset/fix-graph-topo-types.md new file mode 100644 index 0000000..adab4eb --- /dev/null +++ b/repos/effect/.changeset/fix-graph-topo-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Restrict `Graph.topo` to directed graphs at the type level while retaining runtime validation for unsafe undirected inputs. diff --git a/repos/effect/.changeset/fix-graph-undirected-traversal.md b/repos/effect/.changeset/fix-graph-undirected-traversal.md new file mode 100644 index 0000000..e80e046 --- /dev/null +++ b/repos/effect/.changeset/fix-graph-undirected-traversal.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Graph traversal and shortest-path algorithms to traverse undirected edges independently of their stored source/target orientation. diff --git a/repos/effect/.changeset/fix-graph-walker-repeatability.md b/repos/effect/.changeset/fix-graph-walker-repeatability.md new file mode 100644 index 0000000..25c1ccd --- /dev/null +++ b/repos/effect/.changeset/fix-graph-walker-repeatability.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.Walker` to create a fresh iterable for each direct iteration. diff --git a/repos/effect/.changeset/fix-graphviz-dot-escaping.md b/repos/effect/.changeset/fix-graphviz-dot-escaping.md new file mode 100644 index 0000000..35bd254 --- /dev/null +++ b/repos/effect/.changeset/fix-graphviz-dot-escaping.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.toGraphViz` to quote DOT graph names and escape labels as literal text. diff --git a/repos/effect/.changeset/fix-has-interrupts-only-empty.md b/repos/effect/.changeset/fix-has-interrupts-only-empty.md new file mode 100644 index 0000000..5434d7a --- /dev/null +++ b/repos/effect/.changeset/fix-has-interrupts-only-empty.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cause.hasInterruptsOnly` to return `false` for empty causes. diff --git a/repos/effect/.changeset/fix-hashmap-bit31-ordering.md b/repos/effect/.changeset/fix-hashmap-bit31-ordering.md new file mode 100644 index 0000000..b56bac4 --- /dev/null +++ b/repos/effect/.changeset/fix-hashmap-bit31-ordering.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +HashMap: compare HAMT bit positions as unsigned to preserve entry lookup when bit 31 is set diff --git a/repos/effect/.changeset/fix-headers-proto-enumerability.md b/repos/effect/.changeset/fix-headers-proto-enumerability.md new file mode 100644 index 0000000..686c038 --- /dev/null +++ b/repos/effect/.changeset/fix-headers-proto-enumerability.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/http Headers: hide inspectable prototype methods from for..in iteration to avoid invalid header names in runtime fetch polyfills diff --git a/repos/effect/.changeset/fix-http-incoming-message-parse-options.md b/repos/effect/.changeset/fix-http-incoming-message-parse-options.md new file mode 100644 index 0000000..1f71f78 --- /dev/null +++ b/repos/effect/.changeset/fix-http-incoming-message-parse-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpIncomingMessage.schemaBodyJson` to forward parse options via the `parseOptions` annotation key. diff --git a/repos/effect/.changeset/fix-http-tracer-response-cause.md b/repos/effect/.changeset/fix-http-tracer-response-cause.md new file mode 100644 index 0000000..30a5589 --- /dev/null +++ b/repos/effect/.changeset/fix-http-tracer-response-cause.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Exclude response metadata from HTTP server span failures after response headers have been sent. diff --git a/repos/effect/.changeset/fix-httpapi-authorization-decoding.md b/repos/effect/.changeset/fix-httpapi-authorization-decoding.md new file mode 100644 index 0000000..b41cb06 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-authorization-decoding.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix HttpApi authorization decoding. + +Previously, `HttpApiBuilder.securityDecode` removed the expected scheme length and one following character from the `Authorization` header without verifying either value. A Bearer decoder could therefore pass credentials from a different scheme such as `Basic`, accept a malformed header without a separating space, or retain leading spaces when more than one separator was present. + +The decoder now validates the declared scheme before returning credentials, matches it case-insensitively as required by [RFC 9110 section 11.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-11.1), and consumes one or more separating spaces. Missing, malformed, or mismatched headers produce the existing empty credential value so security middleware can reject them consistently. + +Basic authentication previously split the decoded `user-pass` value at every colon, causing otherwise valid passwords containing `:` to be discarded. It now uses only the first colon as the separator and preserves the rest of the password, following [RFC 7617 section 2](https://www.rfc-editor.org/rfc/rfc7617.html#section-2). diff --git a/repos/effect/.changeset/fix-httpapi-client-error-content-type.md b/repos/effect/.changeset/fix-httpapi-client-error-content-type.md new file mode 100644 index 0000000..2de77be --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-client-error-content-type.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix HttpApi client error decoding. + +Generated clients previously combined every error schema for a status into one union decoder. When schemas used different encodings, their declaration order could determine the decoded error instead of the response `Content-Type`; for example, a text decoder could accept a JSON response before the JSON decoder was tried. + +Error responses are now grouped and selected by normalized content type, matching buffered success responses. Normalization happens before grouping, so declarations that differ only by casing or parameters such as `charset` share one union decoder instead of making later schemas unreachable. + +No-content schemas are represented by a headerless alternative, allowing empty error responses without a `Content-Type` header to decode correctly. Unsupported content types preserve the existing combination of `StatusCodeError` and the response decoding failure. diff --git a/repos/effect/.changeset/fix-httpapi-endpoint-error-inference.md b/repos/effect/.changeset/fix-httpapi-endpoint-error-inference.md new file mode 100644 index 0000000..c476f25 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-endpoint-error-inference.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix HttpApiEndpoint endpoint error inference when success schemas include streams. diff --git a/repos/effect/.changeset/fix-httpapi-malformed-json-400.md b/repos/effect/.changeset/fix-httpapi-malformed-json-400.md new file mode 100644 index 0000000..2f704e0 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-malformed-json-400.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Map HttpApi json defects to SchemaError diff --git a/repos/effect/.changeset/fix-httpapi-runtime-shape.md b/repos/effect/.changeset/fix-httpapi-runtime-shape.md new file mode 100644 index 0000000..a8c9bd8 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-runtime-shape.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApi.make` so it stores the API identifier and starts with an empty `groups` object instead of a `Map`. This makes empty APIs match the shape they have after groups are added. diff --git a/repos/effect/.changeset/fix-httpapi-schema-types.md b/repos/effect/.changeset/fix-httpapi-schema-types.md new file mode 100644 index 0000000..d5dd339 --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-schema-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix published HttpApi declaration files by exporting schema metadata types referenced by public declarations. diff --git a/repos/effect/.changeset/fix-httpapi-security-middleware-cache.md b/repos/effect/.changeset/fix-httpapi-security-middleware-cache.md new file mode 100644 index 0000000..cf9e68f --- /dev/null +++ b/repos/effect/.changeset/fix-httpapi-security-middleware-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiBuilder` security middleware caching so separate handler builds do not reuse the first provided middleware implementation. diff --git a/repos/effect/.changeset/fix-is-json-dag.md b/repos/effect/.changeset/fix-is-json-dag.md new file mode 100644 index 0000000..68809e0 --- /dev/null +++ b/repos/effect/.changeset/fix-is-json-dag.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `SchemaAST.isJson` rejecting DAGs as cycles, closes #2021. + +The previous implementation marked every visited object in a single `seen` set and never removed it, so any value that referenced the same object through two different paths (a DAG, e.g. `{ x: shared, y: shared }`) was treated as a cycle and returned `false`. Cycle detection now tracks only the current recursion path (popping on exit) and memoizes fully validated subtrees, so DAGs are accepted while true cycles are still rejected. diff --git a/repos/effect/.changeset/fix-json-schema-anyof-oneof-siblings.md b/repos/effect/.changeset/fix-json-schema-anyof-oneof-siblings.md new file mode 100644 index 0000000..9f0840d --- /dev/null +++ b/repos/effect/.changeset/fix-json-schema-anyof-oneof-siblings.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +SchemaRepresentation: support `anyOf`/`oneOf` with sibling keywords in `fromJsonSchemaMultiDocument` diff --git a/repos/effect/.changeset/fix-json-schema-import-json.md b/repos/effect/.changeset/fix-json-schema-import-json.md new file mode 100644 index 0000000..048a0d7 --- /dev/null +++ b/repos/effect/.changeset/fix-json-schema-import-json.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Import unconstrained JSON Schema nodes as `Schema.Json` instead of `Schema.Unknown`. diff --git a/repos/effect/.changeset/fix-keepalive-blocked-timers.md b/repos/effect/.changeset/fix-keepalive-blocked-timers.md new file mode 100644 index 0000000..8cdb6fa --- /dev/null +++ b/repos/effect/.changeset/fix-keepalive-blocked-timers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +runtime: guard keepAlive setInterval / clearInterval so Effect.runPromise works in runtimes that block timer APIs diff --git a/repos/effect/.changeset/fix-mcp-param-name-resolution.md b/repos/effect/.changeset/fix-mcp-param-name-resolution.md new file mode 100644 index 0000000..2ec5b38 --- /dev/null +++ b/repos/effect/.changeset/fix-mcp-param-name-resolution.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix MCP resource template parameter names resolving as `param0`, `param1` instead of actual names by checking `isParam` on the original schema before `toCodecStringTree` transformation. diff --git a/repos/effect/.changeset/fix-mermaid-escape-special-chars.md b/repos/effect/.changeset/fix-mermaid-escape-special-chars.md new file mode 100644 index 0000000..873f71b --- /dev/null +++ b/repos/effect/.changeset/fix-mermaid-escape-special-chars.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.toMermaid` to escape special characters using HTML entity codes per the Mermaid specification. diff --git a/repos/effect/.changeset/fix-mutable-list-filter-length.md b/repos/effect/.changeset/fix-mutable-list-filter-length.md new file mode 100644 index 0000000..c6a6a84 --- /dev/null +++ b/repos/effect/.changeset/fix-mutable-list-filter-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `MutableList.filter` and `MutableList.remove` length updates. diff --git a/repos/effect/.changeset/fix-number-remainder-scientific-notation.md b/repos/effect/.changeset/fix-number-remainder-scientific-notation.md new file mode 100644 index 0000000..9b43310 --- /dev/null +++ b/repos/effect/.changeset/fix-number-remainder-scientific-notation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Number.remainder` for very small and large values formatted in scientific notation. diff --git a/repos/effect/.changeset/fix-object-keyword-json-schema.md b/repos/effect/.changeset/fix-object-keyword-json-schema.md new file mode 100644 index 0000000..411cc65 --- /dev/null +++ b/repos/effect/.changeset/fix-object-keyword-json-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Emit `Schema.ObjectKeyword` as an object-or-array JSON Schema union. diff --git a/repos/effect/.changeset/fix-one-shot-iterables.md b/repos/effect/.changeset/fix-one-shot-iterables.md new file mode 100644 index 0000000..9908168 --- /dev/null +++ b/repos/effect/.changeset/fix-one-shot-iterables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix one-shot iterable handling in Array.rotate, Iterable.cartesian, and in-memory RunnerStorage acquisition diff --git a/repos/effect/.changeset/fix-openai-mcp-tool-names.md b/repos/effect/.changeset/fix-openai-mcp-tool-names.md new file mode 100644 index 0000000..c6009ba --- /dev/null +++ b/repos/effect/.changeset/fix-openai-mcp-tool-names.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix OpenAI MCP tool call handling to keep the canonical `OpenAiMcp` tool name across response and stream paths, including approval flows. diff --git a/repos/effect/.changeset/fix-openapi-generator-form-urlencoded.md b/repos/effect/.changeset/fix-openapi-generator-form-urlencoded.md new file mode 100644 index 0000000..0b37c34 --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-generator-form-urlencoded.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Support `application/x-www-form-urlencoded` request bodies in `httpclient` output format. Previously, form-urlencoded request bodies were silently dropped, producing operations with no payload parameter. The generator now emits `HttpClientRequest.bodyUrlParams` for these endpoints, matching the existing pattern for `multipart/form-data` (`bodyFormData`) and `application/json` (`bodyJsonUnsafe`). The `httpapi` format was already handling this content type correctly. diff --git a/repos/effect/.changeset/fix-openapi-generator-swagger2openapi.md b/repos/effect/.changeset/fix-openapi-generator-swagger2openapi.md new file mode 100644 index 0000000..38ac06d --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-generator-swagger2openapi.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Declare `swagger2openapi` as a runtime dependency so published `openapigen` installs can resolve the converter imported by `OpenApiGenerator`. diff --git a/repos/effect/.changeset/fix-openapi-preserve-multiple-response-content-types.md b/repos/effect/.changeset/fix-openapi-preserve-multiple-response-content-types.md new file mode 100644 index 0000000..7074326 --- /dev/null +++ b/repos/effect/.changeset/fix-openapi-preserve-multiple-response-content-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `OpenApi.fromApi` preserving multiple response content types for one status code, closes #1485. diff --git a/repos/effect/.changeset/fix-openrouter-sparse-array.md b/repos/effect/.changeset/fix-openrouter-sparse-array.md new file mode 100644 index 0000000..74f87f9 --- /dev/null +++ b/repos/effect/.changeset/fix-openrouter-sparse-array.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Fix sparse array crash in `streamText` tool call handling. diff --git a/repos/effect/.changeset/fix-otel-logger-clock-skew.md b/repos/effect/.changeset/fix-otel-logger-clock-skew.md new file mode 100644 index 0000000..8e6ae29 --- /dev/null +++ b/repos/effect/.changeset/fix-otel-logger-clock-skew.md @@ -0,0 +1,7 @@ +--- +"@effect/opentelemetry": patch +--- + +Use monotonic clock for log timestamps to match span timestamps. + +The Logger used `Date.now()` (wall clock) for log `timestamp` while the Tracer used `clock.currentTimeNanosUnsafe()` (monotonic clock) for span `startTime`. This caused logs to appear before their parent span due to clock drift between the two sources. Both now use the same monotonic clock via `nanosToHrTime(clock.currentTimeNanosUnsafe())`. diff --git a/repos/effect/.changeset/fix-otel-logger-severity-number.md b/repos/effect/.changeset/fix-otel-logger-severity-number.md new file mode 100644 index 0000000..b411ad4 --- /dev/null +++ b/repos/effect/.changeset/fix-otel-logger-severity-number.md @@ -0,0 +1,20 @@ +--- +"@effect/opentelemetry": patch +--- + +Logger: emit OTel-spec `SeverityNumber` (1-24) instead of Effect's internal log-level ordinal. + +`OtelLogger.make` previously passed `LogLevel.getOrdinal(level)` (e.g. Info=20000, Error=40000) as `severityNumber`, which falls outside the OpenTelemetry logs data model spec range (1-24). Backends that validate the field (Honeycomb, Datadog, etc.) bucket such values as `UNSPECIFIED`. + +The mapping now follows the spec: + +| Effect LogLevel | OTel SeverityNumber | +| --------------- | ------------------- | +| Trace | TRACE (1) | +| Debug | DEBUG (5) | +| Info | INFO (9) | +| Warn | WARN (13) | +| Error | ERROR (17) | +| Fatal | FATAL (21) | + +Also exports the helper `logLevelToSeverityNumber` for downstream use. diff --git a/repos/effect/.changeset/fix-persisted-queue-attempt-accounting.md b/repos/effect/.changeset/fix-persisted-queue-attempt-accounting.md new file mode 100644 index 0000000..8737be0 --- /dev/null +++ b/repos/effect/.changeset/fix-persisted-queue-attempt-accounting.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `PersistedQueue` to count schema decoding and malformed SQL payload failures as processing attempts. diff --git a/repos/effect/.changeset/fix-queue-collect-duplication.md b/repos/effect/.changeset/fix-queue-collect-duplication.md new file mode 100644 index 0000000..bc29d2e --- /dev/null +++ b/repos/effect/.changeset/fix-queue-collect-duplication.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Queue.collect: stop duplicating drained messages by appending each batch once diff --git a/repos/effect/.changeset/fix-random-string-seeds.md b/repos/effect/.changeset/fix-random-string-seeds.md new file mode 100644 index 0000000..201923e --- /dev/null +++ b/repos/effect/.changeset/fix-random-string-seeds.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix string seed encoding in Random.withSeed so short, trailing, and astral UTF-8 bytes affect deterministic streams. diff --git a/repos/effect/.changeset/fix-ratelimiter-tokenbucket-redis-ttl.md b/repos/effect/.changeset/fix-ratelimiter-tokenbucket-redis-ttl.md new file mode 100644 index 0000000..874cd2a --- /dev/null +++ b/repos/effect/.changeset/fix-ratelimiter-tokenbucket-redis-ttl.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the Redis `RateLimiterStore` token-bucket failing with opaque errors under memory pressure: it now writes its keys with a TTL and guards against a missing refill timestamp. diff --git a/repos/effect/.changeset/fix-redis-persisted-queue.md b/repos/effect/.changeset/fix-redis-persisted-queue.md new file mode 100644 index 0000000..8ec5249 --- /dev/null +++ b/repos/effect/.changeset/fix-redis-persisted-queue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Redis-backed `PersistedQueue` reset and failed-item handling. diff --git a/repos/effect/.changeset/fix-remainder-scientific-notation.md b/repos/effect/.changeset/fix-remainder-scientific-notation.md new file mode 100644 index 0000000..1a0b109 --- /dev/null +++ b/repos/effect/.changeset/fix-remainder-scientific-notation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +`Number.remainder`: fix incorrect results for small floats in scientific notation (e.g. `1e-7`). diff --git a/repos/effect/.changeset/fix-request-resolver-pending-batches-leak.md b/repos/effect/.changeset/fix-request-resolver-pending-batches-leak.md new file mode 100644 index 0000000..9721cd1 --- /dev/null +++ b/repos/effect/.changeset/fix-request-resolver-pending-batches-leak.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use `WeakMap` for `pendingBatches` instead of `Map`, to allow GC to collect resolvers diff --git a/repos/effect/.changeset/fix-retry-transient-autocomplete.md b/repos/effect/.changeset/fix-retry-transient-autocomplete.md new file mode 100644 index 0000000..0cdb6b2 --- /dev/null +++ b/repos/effect/.changeset/fix-retry-transient-autocomplete.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpClient.retryTransient` autocomplete leaking `Schedule` internals by splitting the `{...} | Schedule` union into separate overloads. diff --git a/repos/effect/.changeset/fix-rpc-http-requestids-finalizer.md b/repos/effect/.changeset/fix-rpc-http-requestids-finalizer.md new file mode 100644 index 0000000..8c295f8 --- /dev/null +++ b/repos/effect/.changeset/fix-rpc-http-requestids-finalizer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix request ID tracking in the RPC server HTTP protocol finalizer. diff --git a/repos/effect/.changeset/fix-rpc-json-id-edges.md b/repos/effect/.changeset/fix-rpc-json-id-edges.md new file mode 100644 index 0000000..6b915e0 --- /dev/null +++ b/repos/effect/.changeset/fix-rpc-json-id-edges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix JSON-RPC serialization for `id` values that are falsey but valid, including `0` and `""`, while still mapping `null` to Effect's internal notification sentinel. diff --git a/repos/effect/.changeset/fix-rpc-unknown-tag-isolation.md b/repos/effect/.changeset/fix-rpc-unknown-tag-isolation.md new file mode 100644 index 0000000..f8eb1b6 --- /dev/null +++ b/repos/effect/.changeset/fix-rpc-unknown-tag-isolation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Localize missing rpc method errors to the provided request id diff --git a/repos/effect/.changeset/fix-schedule-fixed-double-exec.md b/repos/effect/.changeset/fix-schedule-fixed-double-exec.md new file mode 100644 index 0000000..1909b59 --- /dev/null +++ b/repos/effect/.changeset/fix-schedule-fixed-double-exec.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Fix `Schedule.fixed` double-executing the effect due to clock jitter. + +The `elapsedSincePrevious > window` check included sleep time from the +previous step, so any timer imprecision (e.g. 1001ms for a 1000ms sleep) +triggered an immediate zero-delay re-execution. diff --git a/repos/effect/.changeset/fix-schedule-reduce-sync-state.md b/repos/effect/.changeset/fix-schedule-reduce-sync-state.md new file mode 100644 index 0000000..9c1d929 --- /dev/null +++ b/repos/effect/.changeset/fix-schedule-reduce-sync-state.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.reduce` to persist state updates when the combine function returns a synchronous value. diff --git a/repos/effect/.changeset/fix-schema-arbitrary-exclusive-bounds.md b/repos/effect/.changeset/fix-schema-arbitrary-exclusive-bounds.md new file mode 100644 index 0000000..b14291c --- /dev/null +++ b/repos/effect/.changeset/fix-schema-arbitrary-exclusive-bounds.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix schema arbitrary constraints for exclusive BigInt, Date, and integer number bounds. diff --git a/repos/effect/.changeset/fix-schema-bracket-prototype-pollution.md b/repos/effect/.changeset/fix-schema-bracket-prototype-pollution.md new file mode 100644 index 0000000..f570f34 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-bracket-prototype-pollution.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a bug where decoding bracket paths from FormData or URLSearchParams could mutate inherited object prototypes. diff --git a/repos/effect/.changeset/fix-schema-defect-message.md b/repos/effect/.changeset/fix-schema-defect-message.md new file mode 100644 index 0000000..15722dd --- /dev/null +++ b/repos/effect/.changeset/fix-schema-defect-message.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Schema.Defect JSON encoding for Error values whose message property is not a string. diff --git a/repos/effect/.changeset/fix-schema-encode-keys-property-keys.md b/repos/effect/.changeset/fix-schema-encode-keys-property-keys.md new file mode 100644 index 0000000..8e72551 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-encode-keys-property-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Struct` key renaming and `Schema.encodeKeys` to support symbol keys, and reject duplicate encoded keys. diff --git a/repos/effect/.changeset/fix-schema-encodekeys-class.md b/repos/effect/.changeset/fix-schema-encodekeys-class.md new file mode 100644 index 0000000..ebf61c2 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-encodekeys-class.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema.encodeKeys: relax input constraint from Struct to schemas with fields so Schema.Class works, closes #1412. diff --git a/repos/effect/.changeset/fix-schema-encodekeys-struct.md b/repos/effect/.changeset/fix-schema-encodekeys-struct.md new file mode 100644 index 0000000..fd884d1 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-encodekeys-struct.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.encodeKeys` to encode non-remapped struct fields during encoding. diff --git a/repos/effect/.changeset/fix-schema-encoding-checks.md b/repos/effect/.changeset/fix-schema-encoding-checks.md new file mode 100644 index 0000000..ba56790 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-encoding-checks.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Fix Schema handling of encoded-side checks for container ASTs. + +Checks added after `flip` are now preserved as `encodingChecks` across +`Declaration`, `Arrays`, `Objects`, and `Union`, even when rebuilding the AST +does not change child nodes. `toType` now projects those checks consistently, +and parsing applies encoded-side checks to the local encoded value when an +encoding chain is present without allowing encoded-side `parseOptions` +annotations to affect the current parser side. diff --git a/repos/effect/.changeset/fix-schema-identifier-expected-message.md b/repos/effect/.changeset/fix-schema-identifier-expected-message.md new file mode 100644 index 0000000..8db6496 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-identifier-expected-message.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use the `identifier` annotation as the expected message when available, closes #1458. diff --git a/repos/effect/.changeset/fix-schema-is-json-records.md b/repos/effect/.changeset/fix-schema-is-json-records.md new file mode 100644 index 0000000..5a71fc1 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-is-json-records.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SchemaAST.isJson` to reject class instances and other non-record objects. diff --git a/repos/effect/.changeset/fix-schema-is-uuid.md b/repos/effect/.changeset/fix-schema-is-uuid.md new file mode 100644 index 0000000..ecd2eb1 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-is-uuid.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.isUUID` so the `version` parameter is optional in its public signature. diff --git a/repos/effect/.changeset/fix-schema-json-tuple-allof.md b/repos/effect/.changeset/fix-schema-json-tuple-allof.md new file mode 100644 index 0000000..cbf1687 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-json-tuple-allof.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix JSON Schema `allOf` imports for tuple intersections and preserve primitive refinements when combining literal constraints. diff --git a/repos/effect/.changeset/fix-schema-option-non-schema-failures.md b/repos/effect/.changeset/fix-schema-option-non-schema-failures.md new file mode 100644 index 0000000..516ec89 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-option-non-schema-failures.md @@ -0,0 +1,17 @@ +--- +"effect": patch +--- + +Normalize error behavior for Schema and SchemaParser boundary APIs. + +`SchemaError` now extends `Data.TaggedError`, so it is also a native `Error`. +SchemaParser Promise APIs now reject an `Error` whose cause is the +`SchemaIssue.Issue` for schema failures. + +Schema and SchemaParser `Effect` and `Exit` adapters now preserve full causes +while mapping schema issue failures to their public error type. The `is`, +`asserts`, `Promise`, `Sync`, `Result`, `Option`, `make`, and `makeOption` +adapters now distinguish schema issues from non-schema causes. Schema-only +failures are converted to the adapter's normal representation (`false`, +rejected or thrown schema error, `Result.fail`, or `None`), while non-schema +causes throw or reject with an `Error` whose cause is the underlying `Cause`. diff --git a/repos/effect/.changeset/fix-schema-parser-checks.md b/repos/effect/.changeset/fix-schema-parser-checks.md new file mode 100644 index 0000000..fd93a6c --- /dev/null +++ b/repos/effect/.changeset/fix-schema-parser-checks.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Fix `Schema` encoding so container-level checks are validated against the decoded value instead of the encoded output. + +Disallow adding checks directly to `Schema.suspend(...)`; add the checks to the suspended schema instead. + +Fix `StructWithRest` so index signatures do not re-parse or overwrite fixed properties. diff --git a/repos/effect/.changeset/fix-schema-tuple-post-rest-indexing.md b/repos/effect/.changeset/fix-schema-tuple-post-rest-indexing.md new file mode 100644 index 0000000..8546cc8 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-tuple-post-rest-indexing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.toFormatter` and `Schema.toEquivalence` indexing for tuples with multiple post-rest elements. diff --git a/repos/effect/.changeset/fix-schema-union-dispatch-order.md b/repos/effect/.changeset/fix-schema-union-dispatch-order.md new file mode 100644 index 0000000..61876f4 --- /dev/null +++ b/repos/effect/.changeset/fix-schema-union-dispatch-order.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Fix union candidate selection and decoding order so that unions now: + +- consider matches from every sentinel key instead of dropping valid members after the first match; +- reject ambiguous `oneOf` inputs when members with different sentinel keys both match; +- preserve declared member order when combining discriminated members with non-discriminated fallbacks; +- commit concurrent decoding results in declaration order instead of completion order. + +Reserved SSE failure event names with non-`Cause` data are now emitted as application events instead of producing a runtime defect. diff --git a/repos/effect/.changeset/fix-searchparam-initial-decode.md b/repos/effect/.changeset/fix-searchparam-initial-decode.md new file mode 100644 index 0000000..f78d96a --- /dev/null +++ b/repos/effect/.changeset/fix-searchparam-initial-decode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Atom.searchParam: decode initial URL values correctly when a schema is provided diff --git a/repos/effect/.changeset/fix-serializable-wire-transfer.md b/repos/effect/.changeset/fix-serializable-wire-transfer.md new file mode 100644 index 0000000..64c580d --- /dev/null +++ b/repos/effect/.changeset/fix-serializable-wire-transfer.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Fix `Atom.serializable` encode/decode for wire transfer. + +Use `Schema.toCodecJson` instead of `Schema.encodeSync`/`Schema.decodeSync` directly, so that encoded values are plain JSON objects that survive serialization roundtrips (JSON, seroval, etc.). Previously, `AsyncResult.Schema` encode produced instances with custom prototypes that were lost after wire transfer, causing decode to fail with "Expected AsyncResult" errors during SSR hydration. diff --git a/repos/effect/.changeset/fix-sql-persisted-queue-lock-refresh.md b/repos/effect/.changeset/fix-sql-persisted-queue-lock-refresh.md new file mode 100644 index 0000000..19a9894 --- /dev/null +++ b/repos/effect/.changeset/fix-sql-persisted-queue-lock-refresh.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix SQL-backed persisted queues to refresh locks for actively acquired elements. diff --git a/repos/effect/.changeset/fix-stream-grouped-within-flush.md b/repos/effect/.changeset/fix-stream-grouped-within-flush.md new file mode 100644 index 0000000..fff052f --- /dev/null +++ b/repos/effect/.changeset/fix-stream-grouped-within-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.groupedWithin` dropping partial batches when the upstream ends or goes idle. diff --git a/repos/effect/.changeset/fix-stream-run-for-each-while.md b/repos/effect/.changeset/fix-stream-run-for-each-while.md new file mode 100644 index 0000000..91b9e1d --- /dev/null +++ b/repos/effect/.changeset/fix-stream-run-for-each-while.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Stream.runForEachWhile so it continues across chunk boundaries while the predicate returns true and stops when the predicate returns false. diff --git a/repos/effect/.changeset/fix-stream-scan-effect.md b/repos/effect/.changeset/fix-stream-scan-effect.md new file mode 100644 index 0000000..a1d821f --- /dev/null +++ b/repos/effect/.changeset/fix-stream-scan-effect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.scanEffect` hanging and repeatedly emitting the initial state. diff --git a/repos/effect/.changeset/fix-stream-scoped-scope.md b/repos/effect/.changeset/fix-stream-scoped-scope.md new file mode 100644 index 0000000..c4437e6 --- /dev/null +++ b/repos/effect/.changeset/fix-stream-scoped-scope.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.scoped` and `Channel.scoped` so pull effects run with the scoped resource scope. diff --git a/repos/effect/.changeset/fix-string-case-digits.md b/repos/effect/.changeset/fix-string-case-digits.md new file mode 100644 index 0000000..5aaa2cd --- /dev/null +++ b/repos/effect/.changeset/fix-string-case-digits.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `String.camelCase` and `String.pascalCase` handling of numeric word segments, and add `String.configCase` for configuration key casing. diff --git a/repos/effect/.changeset/fix-strip-approval-artifacts-multi-round.md b/repos/effect/.changeset/fix-strip-approval-artifacts-multi-round.md new file mode 100644 index 0000000..c091d03 --- /dev/null +++ b/repos/effect/.changeset/fix-strip-approval-artifacts-multi-round.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Fix LanguageModel stripping of resolved approval artifacts across multi-round conversations. + +Previously, `stripResolvedApprovals` only ran when there were pending approvals +in the current round. Stale artifacts from earlier rounds would leak to the +provider, causing errors. The stripping now runs unconditionally. + +In streaming mode, pre-resolved tool results are also emitted as stream parts +so `Chat.streamText` persists them to history, preventing re-resolution on +subsequent rounds. diff --git a/repos/effect/.changeset/fix-struct-utility-types-simplify.md b/repos/effect/.changeset/fix-struct-utility-types-simplify.md new file mode 100644 index 0000000..4607703 --- /dev/null +++ b/repos/effect/.changeset/fix-struct-utility-types-simplify.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Struct` utility return types (for example `pick`) to preserve the previous simplified shape instead of exposing raw utility types like `Pick`, closes #1855. diff --git a/repos/effect/.changeset/fix-structural-proto-equality.md b/repos/effect/.changeset/fix-structural-proto-equality.md new file mode 100644 index 0000000..eae88bf --- /dev/null +++ b/repos/effect/.changeset/fix-structural-proto-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix structural equality for request-style values when structural hashes collide. diff --git a/repos/effect/.changeset/fix-structwithrest-index-signatures.md b/repos/effect/.changeset/fix-structwithrest-index-signatures.md new file mode 100644 index 0000000..beee64e --- /dev/null +++ b/repos/effect/.changeset/fix-structwithrest-index-signatures.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/ai-openai": patch +--- + +Validate `Schema.StructWithRest` fixed fields against rest index signatures at the type level so schemas cannot be constructed with incompatible decoded, encoded, or make shapes. This keeps `StructWithRest` types sound and updates the generated OpenAI conversation-items request schema to keep accepting arbitrary additional fields under the stricter validation. diff --git a/repos/effect/.changeset/fix-tagged-union-class-sentinels.md b/repos/effect/.changeset/fix-tagged-union-class-sentinels.md new file mode 100644 index 0000000..f79912a --- /dev/null +++ b/repos/effect/.changeset/fix-tagged-union-class-sentinels.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.toTaggedUnion` discriminant detection for class-based schemas, including unique symbol tags, closes #1584. diff --git a/repos/effect/.changeset/fix-tagged-union-match-unify.md b/repos/effect/.changeset/fix-tagged-union-match-unify.md new file mode 100644 index 0000000..987abe5 --- /dev/null +++ b/repos/effect/.changeset/fix-tagged-union-match-unify.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `TaggedUnion.match` to use `Unify` for return types, allowing +branches to return distinct Effect types that are properly merged. diff --git a/repos/effect/.changeset/fix-to-tagged-union-isanyof-custom-tags.md b/repos/effect/.changeset/fix-to-tagged-union-isanyof-custom-tags.md new file mode 100644 index 0000000..17f0ec5 --- /dev/null +++ b/repos/effect/.changeset/fix-to-tagged-union-isanyof-custom-tags.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Fix `Schema.toTaggedUnion(...).isAnyOf` narrowing for custom discriminant keys, closes #2386. + +Previously, the type predicate always extracted union members by `_tag`, even +when `toTaggedUnion` was created with a different discriminant key. Runtime +behavior already used the supplied key, so this aligns the type-level narrowing +with the existing runtime behavior. diff --git a/repos/effect/.changeset/fix-tool-provider-defined-clone.md b/repos/effect/.changeset/fix-tool-provider-defined-clone.md new file mode 100644 index 0000000..6306c05 --- /dev/null +++ b/repos/effect/.changeset/fix-tool-provider-defined-clone.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Tool: preserve the tool kind when cloning provider-defined and dynamic tools. + +`Tool.addDependency`, `setParameters`, `setSuccess`, `setFailure`, `annotate`, and `annotateMerge` previously rebuilt the tool as a user-defined tool, which flipped `Tool.isProviderDefined` to `false`, corrupted the provider `id` (e.g. `anthropic.memory_20250818`), and crashed `Tool.getStrictMode`. These operations now clone the tool while preserving its prototype, `id`, and kind. Provider-defined tools also now carry an empty annotations context so `Tool.getStrictMode`/`annotate` work on them. Closes #2615. diff --git a/repos/effect/.changeset/fix-tuple-with-rest-post-rest-index-drift.md b/repos/effect/.changeset/fix-tuple-with-rest-post-rest-index-drift.md new file mode 100644 index 0000000..155671b --- /dev/null +++ b/repos/effect/.changeset/fix-tuple-with-rest-post-rest-index-drift.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix TupleWithRest post-rest validation to check each tail index sequentially. diff --git a/repos/effect/.changeset/fix-tuple-with-rest-post-rest-validation.md b/repos/effect/.changeset/fix-tuple-with-rest-post-rest-validation.md new file mode 100644 index 0000000..60e4272 --- /dev/null +++ b/repos/effect/.changeset/fix-tuple-with-rest-post-rest-validation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.TupleWithRest` incorrectly accepting inputs with missing post-rest elements, closes #1410. diff --git a/repos/effect/.changeset/fix-types-voidifempty.md b/repos/effect/.changeset/fix-types-voidifempty.md new file mode 100644 index 0000000..f481e77 --- /dev/null +++ b/repos/effect/.changeset/fix-types-voidifempty.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Types.VoidIfEmpty` to correctly detect empty object types. Remove deprecated `Types.MatchRecord` in favor of the simplified implementation, closes #1647. diff --git a/repos/effect/.changeset/fix-vitest-proxy-chained-helpers.md b/repos/effect/.changeset/fix-vitest-proxy-chained-helpers.md new file mode 100644 index 0000000..a6b5ecb --- /dev/null +++ b/repos/effect/.changeset/fix-vitest-proxy-chained-helpers.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Preserve chained vitest helpers like `it.describe.each` and `it.skip.each` when accessed through the `it` proxy. Previously the proxy returned bound copies of vitest's functions, which stripped their static helper properties and caused `TypeError: it.describe.each is not a function`. diff --git a/repos/effect/.changeset/fix-vitest-record-schema-arbitrary.md b/repos/effect/.changeset/fix-vitest-record-schema-arbitrary.md new file mode 100644 index 0000000..840708d --- /dev/null +++ b/repos/effect/.changeset/fix-vitest-record-schema-arbitrary.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Fix record-form property tests to convert Schema values to FastCheck arbitraries. diff --git a/repos/effect/.changeset/fix-void-response-encoding.md b/repos/effect/.changeset/fix-void-response-encoding.md new file mode 100644 index 0000000..bb84b57 --- /dev/null +++ b/repos/effect/.changeset/fix-void-response-encoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/httpapi HttpApiBuilder: fix void responses producing a non-empty body instead of `Response.empty`, closes #1628. diff --git a/repos/effect/.changeset/fix-workflow-defect-reply-serialization.md b/repos/effect/.changeset/fix-workflow-defect-reply-serialization.md new file mode 100644 index 0000000..461124a --- /dev/null +++ b/repos/effect/.changeset/fix-workflow-defect-reply-serialization.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix cluster workflow activity defect hydration diff --git a/repos/effect/.changeset/fix-workflow-entity-client-collision.md b/repos/effect/.changeset/fix-workflow-entity-client-collision.md new file mode 100644 index 0000000..b682dc9 --- /dev/null +++ b/repos/effect/.changeset/fix-workflow-entity-client-collision.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix ClusterWorkflowEngine partial workflow clients colliding with full workflow clients. diff --git a/repos/effect/.changeset/fix-workflow-proxy-rpc-handler-context.md b/repos/effect/.changeset/fix-workflow-proxy-rpc-handler-context.md new file mode 100644 index 0000000..cc1541b --- /dev/null +++ b/repos/effect/.changeset/fix-workflow-proxy-rpc-handler-context.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix workflow proxy RPC handlers to provide the context expected by RpcServer. diff --git a/repos/effect/.changeset/flat-chicken-remain.md b/repos/effect/.changeset/flat-chicken-remain.md new file mode 100644 index 0000000..17b0598 --- /dev/null +++ b/repos/effect/.changeset/flat-chicken-remain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix AI structured output schema generation for `Schema.Class` and `Schema.ErrorClass` by resolving top-level `$ref` entries before passing JSON Schema to providers and default codec transformers. diff --git a/repos/effect/.changeset/floppy-cows-spend.md b/repos/effect/.changeset/floppy-cows-spend.md new file mode 100644 index 0000000..8954238 --- /dev/null +++ b/repos/effect/.changeset/floppy-cows-spend.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow encoding Redacted by default, and add option to disallow encoding diff --git a/repos/effect/.changeset/floppy-items-admire.md b/repos/effect/.changeset/floppy-items-admire.md new file mode 100644 index 0000000..40c3132 --- /dev/null +++ b/repos/effect/.changeset/floppy-items-admire.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Change `Type_<>` implementation, from using `Exclude` type util to `keyof F as xx`, this implementation keeps IDE provenance link. This enables clicking "Go to definition (F12)" in VSCode on an object made from Schema Struct jumps to the correct Struct field definition. diff --git a/repos/effect/.changeset/floppy-pigs-kiss.md b/repos/effect/.changeset/floppy-pigs-kiss.md new file mode 100644 index 0000000..0dfba7b --- /dev/null +++ b/repos/effect/.changeset/floppy-pigs-kiss.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use Option instead of undefined | A diff --git a/repos/effect/.changeset/floppy-rats-leave.md b/repos/effect/.changeset/floppy-rats-leave.md new file mode 100644 index 0000000..cb520a1 --- /dev/null +++ b/repos/effect/.changeset/floppy-rats-leave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fixed Clock.sleep handling of large durations diff --git a/repos/effect/.changeset/floyd-warshall-null-edge-data.md b/repos/effect/.changeset/floyd-warshall-null-edge-data.md new file mode 100644 index 0000000..3687163 --- /dev/null +++ b/repos/effect/.changeset/floyd-warshall-null-edge-data.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve null edge data in Graph.floydWarshall costs. diff --git a/repos/effect/.changeset/fluffy-meals-matter.md b/repos/effect/.changeset/fluffy-meals-matter.md new file mode 100644 index 0000000..fa65612 --- /dev/null +++ b/repos/effect/.changeset/fluffy-meals-matter.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +add OpenAiClient.withWebSocketMode diff --git a/repos/effect/.changeset/fluffy-pumas-push.md b/repos/effect/.changeset/fluffy-pumas-push.md new file mode 100644 index 0000000..64e2b1e --- /dev/null +++ b/repos/effect/.changeset/fluffy-pumas-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +disable sql traces for EventLog, RunnerStorage diff --git a/repos/effect/.changeset/forked-memo-maps.md b/repos/effect/.changeset/forked-memo-maps.md new file mode 100644 index 0000000..37dd43f --- /dev/null +++ b/repos/effect/.changeset/forked-memo-maps.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/vitest": patch +--- + +Add forked memo maps so nested layer scopes can reuse parent allocations without leaking sibling-local layers. Update `@effect/vitest` to fork memo maps for nested `it.layer` suites, isolating sibling setup while preserving parent sharing. diff --git a/repos/effect/.changeset/forty-hounds-cheer.md b/repos/effect/.changeset/forty-hounds-cheer.md new file mode 100644 index 0000000..ffbf6d2 --- /dev/null +++ b/repos/effect/.changeset/forty-hounds-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore unsupported Ctrl key combinations in interactive CLI prompts to avoid rendering control characters such as Ctrl+L form feed into prompt input. diff --git a/repos/effect/.changeset/forty-otters-cry.md b/repos/effect/.changeset/forty-otters-cry.md new file mode 100644 index 0000000..e95732e --- /dev/null +++ b/repos/effect/.changeset/forty-otters-cry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `SqlSchema.findOne` from effect v3 to return `Option` on empty results and add `SqlSchema.single` for the fail-on-empty behavior. diff --git a/repos/effect/.changeset/forty-rings-film.md b/repos/effect/.changeset/forty-rings-film.md new file mode 100644 index 0000000..7c66c96 --- /dev/null +++ b/repos/effect/.changeset/forty-rings-film.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openai": patch +--- + +Refactor `OpenAiClient` to the handwritten minimal-schema path so `client` now exposes the configured `HttpClient`, `createResponse` / `createResponseStream` / `createEmbedding` use `OpenAiSchema` request-response types, and websocket mode no longer depends on generated-client internals. + +Also migrate OpenAI language and embedding model request-response typing to `OpenAiSchema` and make embedding decoding explicitly reject non-vector (string/base64) payloads with `InvalidOutputError`. diff --git a/repos/effect/.changeset/forty-signs-stay.md b/repos/effect/.changeset/forty-signs-stay.md new file mode 100644 index 0000000..089873c --- /dev/null +++ b/repos/effect/.changeset/forty-signs-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add rpc ConnectionHooks diff --git a/repos/effect/.changeset/forty-swans-divide.md b/repos/effect/.changeset/forty-swans-divide.md new file mode 100644 index 0000000..98e79f3 --- /dev/null +++ b/repos/effect/.changeset/forty-swans-divide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve Schema.TaggedUnion .match auto completion diff --git a/repos/effect/.changeset/forty-trees-pay.md b/repos/effect/.changeset/forty-trees-pay.md new file mode 100644 index 0000000..5e65939 --- /dev/null +++ b/repos/effect/.changeset/forty-trees-pay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `availableShardGroups` to ShardingConfig, to ensure advisory locks do not conflict diff --git a/repos/effect/.changeset/four-papayas-bow.md b/repos/effect/.changeset/four-papayas-bow.md new file mode 100644 index 0000000..a925f54 --- /dev/null +++ b/repos/effect/.changeset/four-papayas-bow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support toolkit unions in `LanguageModel` options. diff --git a/repos/effect/.changeset/four-points-repeat.md b/repos/effect/.changeset/four-points-repeat.md new file mode 100644 index 0000000..62f6b4b --- /dev/null +++ b/repos/effect/.changeset/four-points-repeat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add "Previously Known As" JSDoc migration notes for the `Semaphore` and `Latch` APIs extracted from `Effect`. diff --git a/repos/effect/.changeset/fresh-cats-smash.md b/repos/effect/.changeset/fresh-cats-smash.md new file mode 100644 index 0000000..d563083 --- /dev/null +++ b/repos/effect/.changeset/fresh-cats-smash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix stream requests in Entity.toLayerQueue diff --git a/repos/effect/.changeset/fresh-cycles-wait.md b/repos/effect/.changeset/fresh-cycles-wait.md new file mode 100644 index 0000000..f588122 --- /dev/null +++ b/repos/effect/.changeset/fresh-cycles-wait.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the `Schedule.both` APIs and add `Schedule.max` for combining schedules by their slowest delay. diff --git a/repos/effect/.changeset/fresh-emus-cheat.md b/repos/effect/.changeset/fresh-emus-cheat.md new file mode 100644 index 0000000..3ee2e11 --- /dev/null +++ b/repos/effect/.changeset/fresh-emus-cheat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Export `Effect` do notation APIs (`Do`, `bindTo`, `bind`, and `let`) from `effect/Effect` and add runtime and type-level coverage. diff --git a/repos/effect/.changeset/fresh-monkeys-smoke.md b/repos/effect/.changeset/fresh-monkeys-smoke.md new file mode 100644 index 0000000..523ab35 --- /dev/null +++ b/repos/effect/.changeset/fresh-monkeys-smoke.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +--- + +Add `Model.ModelName` and provide it from AI model constructors. diff --git a/repos/effect/.changeset/frozen-intrinsics-stack-trace-limit.md b/repos/effect/.changeset/frozen-intrinsics-stack-trace-limit.md new file mode 100644 index 0000000..04aa3ce --- /dev/null +++ b/repos/effect/.changeset/frozen-intrinsics-stack-trace-limit.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Avoid throwing when `Error.stackTraceLimit` is non-writable (frozen intrinsics / SES / deterministic sandboxes such as Temporal). + +Effect manipulates `Error.stackTraceLimit` in several internal spots to capture short or empty stack traces cheaply. In hardened environments where `Error` is frozen and `stackTraceLimit` is read-only, assigning to it throws, which broke Effect entirely. Stack-trace-limit manipulation is now best-effort and silently no-ops when the property cannot be modified, mirroring Node's own internal guard. Behavior in normal (writable) environments is unchanged. diff --git a/repos/effect/.changeset/fruity-houses-learn.md b/repos/effect/.changeset/fruity-houses-learn.md new file mode 100644 index 0000000..3e843ad --- /dev/null +++ b/repos/effect/.changeset/fruity-houses-learn.md @@ -0,0 +1,6 @@ +--- +"@effect/sql-mysql2": patch +"effect": patch +--- + +return resolvers directly from SqlModel.makeResolvers diff --git a/repos/effect/.changeset/full-adults-double.md b/repos/effect/.changeset/full-adults-double.md new file mode 100644 index 0000000..d1d236a --- /dev/null +++ b/repos/effect/.changeset/full-adults-double.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add RpcGroup.omit diff --git a/repos/effect/.changeset/funny-crabs-hang.md b/repos/effect/.changeset/funny-crabs-hang.md new file mode 100644 index 0000000..2fd2a4c --- /dev/null +++ b/repos/effect/.changeset/funny-crabs-hang.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"effect": patch +--- + +Ensure that OpenAI JSON schemas for tool calls and structured outputs are properly transformed diff --git a/repos/effect/.changeset/funny-forks-move.md b/repos/effect/.changeset/funny-forks-move.md new file mode 100644 index 0000000..b4f8cd5 --- /dev/null +++ b/repos/effect/.changeset/funny-forks-move.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Constrain `HttpServerRequest.source` to `object` and key server-side request weak caches by `request.source` so middleware request wrappers share the same cache entries. diff --git a/repos/effect/.changeset/fuzzy-camels-hunt.md b/repos/effect/.changeset/fuzzy-camels-hunt.md new file mode 100644 index 0000000..9690597 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-camels-hunt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Prompt.text` and related text prompts to initialize from `default` values so users can edit the default input directly. diff --git a/repos/effect/.changeset/fuzzy-cats-kill.md b/repos/effect/.changeset/fuzzy-cats-kill.md new file mode 100644 index 0000000..35bae2e --- /dev/null +++ b/repos/effect/.changeset/fuzzy-cats-kill.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix child process termination to escalate to `SIGKILL` when the initial signal does not stop the process within `forceKillAfter`. diff --git a/repos/effect/.changeset/fuzzy-crews-fold.md b/repos/effect/.changeset/fuzzy-crews-fold.md new file mode 100644 index 0000000..1bf7243 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-crews-fold.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add custom error callbacks to Effect.fromOption. diff --git a/repos/effect/.changeset/fuzzy-dodos-help.md b/repos/effect/.changeset/fuzzy-dodos-help.md new file mode 100644 index 0000000..9e2d5bd --- /dev/null +++ b/repos/effect/.changeset/fuzzy-dodos-help.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add a CI check job that runs `pnpm ai-docgen` and fails if it produces uncommitted changes. diff --git a/repos/effect/.changeset/fuzzy-lions-perform.md b/repos/effect/.changeset/fuzzy-lions-perform.md new file mode 100644 index 0000000..90ec370 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-lions-perform.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": minor +--- + +Add a new public `OpenAiSchema` module with minimal local schemas for responses, streaming SSE events (including unknown-event fallback), and embeddings. diff --git a/repos/effect/.changeset/fuzzy-pandas-smile.md b/repos/effect/.changeset/fuzzy-pandas-smile.md new file mode 100644 index 0000000..835fefa --- /dev/null +++ b/repos/effect/.changeset/fuzzy-pandas-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Preserve autocomplete for known OpenAI-compatible model config properties while allowing provider-specific properties. diff --git a/repos/effect/.changeset/fuzzy-planets-sneeze.md b/repos/effect/.changeset/fuzzy-planets-sneeze.md new file mode 100644 index 0000000..7e10603 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-planets-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix AtomRef notifications when a listener re-subscribes itself during notification. diff --git a/repos/effect/.changeset/fuzzy-stamps-care.md b/repos/effect/.changeset/fuzzy-stamps-care.md new file mode 100644 index 0000000..a627259 --- /dev/null +++ b/repos/effect/.changeset/fuzzy-stamps-care.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/repos/effect/.changeset/giant-jeans-float.md b/repos/effect/.changeset/giant-jeans-float.md new file mode 100644 index 0000000..3f0eb7e --- /dev/null +++ b/repos/effect/.changeset/giant-jeans-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure one-shot iterables work with Fiber apis diff --git a/repos/effect/.changeset/gold-meteors-move.md b/repos/effect/.changeset/gold-meteors-move.md new file mode 100644 index 0000000..62f8e50 --- /dev/null +++ b/repos/effect/.changeset/gold-meteors-move.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow using Duration.Input with accessors diff --git a/repos/effect/.changeset/gold-readers-hug.md b/repos/effect/.changeset/gold-readers-hug.md new file mode 100644 index 0000000..c1e74af --- /dev/null +++ b/repos/effect/.changeset/gold-readers-hug.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add .reactive to indexeddb .first queries diff --git a/repos/effect/.changeset/gold-rings-start.md b/repos/effect/.changeset/gold-rings-start.md new file mode 100644 index 0000000..a47549e --- /dev/null +++ b/repos/effect/.changeset/gold-rings-start.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Chat constructor types diff --git a/repos/effect/.changeset/good-tools-work.md b/repos/effect/.changeset/good-tools-work.md new file mode 100644 index 0000000..7da12bb --- /dev/null +++ b/repos/effect/.changeset/good-tools-work.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +HttpServerResponse: fix `fromWeb` to preserve Content-Type header when response has a body + +Previously, when converting a web `Response` to an `HttpServerResponse` via `fromWeb`, the `Content-Type` header was not passed to `Body.stream()`, causing it to default to `application/octet-stream`. This affected any code using `HttpApp.fromWebHandler` to wrap web handlers, as JSON responses would incorrectly have their Content-Type set to `application/octet-stream` instead of `application/json`. diff --git a/repos/effect/.changeset/good-trees-pull.md b/repos/effect/.changeset/good-trees-pull.md new file mode 100644 index 0000000..eefb42c --- /dev/null +++ b/repos/effect/.changeset/good-trees-pull.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +align ProcessInput.Input runtime field name with type definition on Prompt.custom diff --git a/repos/effect/.changeset/graph-acyclic-parallel-undirected-edges.md b/repos/effect/.changeset/graph-acyclic-parallel-undirected-edges.md new file mode 100644 index 0000000..5bb98ab --- /dev/null +++ b/repos/effect/.changeset/graph-acyclic-parallel-undirected-edges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.isAcyclic` to detect cycles formed by parallel undirected edges. diff --git a/repos/effect/.changeset/graph-algorithm-fixes.md b/repos/effect/.changeset/graph-algorithm-fixes.md new file mode 100644 index 0000000..21c18e2 --- /dev/null +++ b/repos/effect/.changeset/graph-algorithm-fixes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Graph.successors` and `Graph.predecessors`, deprecate `Graph.neighborsDirected`, and fix graph algorithm edge cases around reversal, undirected edge queries, shortest-path weight validation, topological sort initials, and strongly connected components. diff --git a/repos/effect/.changeset/graph-finalized-mutation-handle.md b/repos/effect/.changeset/graph-finalized-mutation-handle.md new file mode 100644 index 0000000..4f628c3 --- /dev/null +++ b/repos/effect/.changeset/graph-finalized-mutation-handle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject `Graph` mutation operations on mutable handles after `Graph.endMutation` finalizes them. diff --git a/repos/effect/.changeset/graph-guard-predicates.md b/repos/effect/.changeset/graph-guard-predicates.md new file mode 100644 index 0000000..da96b1a --- /dev/null +++ b/repos/effect/.changeset/graph-guard-predicates.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Graph.isGraph narrowing for mutable and undirected graphs. diff --git a/repos/effect/.changeset/graph-sync-mutation-callbacks.md b/repos/effect/.changeset/graph-sync-mutation-callbacks.md new file mode 100644 index 0000000..c006de9 --- /dev/null +++ b/repos/effect/.changeset/graph-sync-mutation-callbacks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject asynchronous `Graph` mutation callbacks and finalize scoped mutable handles when callbacks fail. diff --git a/repos/effect/.changeset/graph-undirected-edge-equality.md b/repos/effect/.changeset/graph-undirected-edge-equality.md new file mode 100644 index 0000000..25212ae --- /dev/null +++ b/repos/effect/.changeset/graph-undirected-edge-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix undirected `Graph` equality and hashing to ignore stored edge endpoint orientation. diff --git a/repos/effect/.changeset/graph-walker-iterator-receiver.md b/repos/effect/.changeset/graph-walker-iterator-receiver.md new file mode 100644 index 0000000..a44ad46 --- /dev/null +++ b/repos/effect/.changeset/graph-walker-iterator-receiver.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.Walker` iteration for receiver-sensitive iterables. diff --git a/repos/effect/.changeset/great-trains-mate.md b/repos/effect/.changeset/great-trains-mate.md new file mode 100644 index 0000000..043f0ee --- /dev/null +++ b/repos/effect/.changeset/great-trains-mate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Duration.toMillis regression diff --git a/repos/effect/.changeset/great-trams-report.md b/repos/effect/.changeset/great-trams-report.md new file mode 100644 index 0000000..ac69450 --- /dev/null +++ b/repos/effect/.changeset/great-trams-report.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +cleanup IndexedDb prototypes diff --git a/repos/effect/.changeset/green-beds-unref.md b/repos/effect/.changeset/green-beds-unref.md new file mode 100644 index 0000000..db4ec3b --- /dev/null +++ b/repos/effect/.changeset/green-beds-unref.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +--- + +Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. diff --git a/repos/effect/.changeset/green-chips-wash.md b/repos/effect/.changeset/green-chips-wash.md new file mode 100644 index 0000000..3b39643 --- /dev/null +++ b/repos/effect/.changeset/green-chips-wash.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Finalize the OpenAPI generator public migration by replacing the `typeOnly` option and `--type-only` CLI flag with the `format` option and `--format` flag, and by adding `httpapi` as a supported output alongside `httpclient` and `httpclient-type-only`. diff --git a/repos/effect/.changeset/green-moons-smile.md b/repos/effect/.changeset/green-moons-smile.md new file mode 100644 index 0000000..6f3a832 --- /dev/null +++ b/repos/effect/.changeset/green-moons-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `Atom.withReactivity(...)` refresh behavior when registry initial values seed the wrapped atom. diff --git a/repos/effect/.changeset/green-pugs-play.md b/repos/effect/.changeset/green-pugs-play.md new file mode 100644 index 0000000..4b13506 --- /dev/null +++ b/repos/effect/.changeset/green-pugs-play.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add defaults to indexeddb reactivity keys diff --git a/repos/effect/.changeset/green-rings-prove.md b/repos/effect/.changeset/green-rings-prove.md new file mode 100644 index 0000000..c7a2ea6 --- /dev/null +++ b/repos/effect/.changeset/green-rings-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update Model uuid helpers diff --git a/repos/effect/.changeset/happy-mirrors-dream.md b/repos/effect/.changeset/happy-mirrors-dream.md new file mode 100644 index 0000000..b944244 --- /dev/null +++ b/repos/effect/.changeset/happy-mirrors-dream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Socket.make diff --git a/repos/effect/.changeset/harden-httpapi-documentation-html.md b/repos/effect/.changeset/harden-httpapi-documentation-html.md new file mode 100644 index 0000000..774dbe3 --- /dev/null +++ b/repos/effect/.changeset/harden-httpapi-documentation-html.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Harden HttpApi documentation HTML rendering. + +Scalar descriptions and CDN versions were interpolated without attribute-safe escaping. Embedded OpenAPI JSON in Scalar and Swagger also handled only the exact `` sequence, not other valid [script end-tag forms](https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state). + +Attribute values and CDN versions are now encoded for their contexts, and embedded JSON escapes `<` so it cannot close its script element. diff --git a/repos/effect/.changeset/heavy-loops-cut.md b/repos/effect/.changeset/heavy-loops-cut.md new file mode 100644 index 0000000..0e9730f --- /dev/null +++ b/repos/effect/.changeset/heavy-loops-cut.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Unwrap `_Success` schema to enable field access. diff --git a/repos/effect/.changeset/heavy-trams-fix.md b/repos/effect/.changeset/heavy-trams-fix.md new file mode 100644 index 0000000..5077696 --- /dev/null +++ b/repos/effect/.changeset/heavy-trams-fix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make HttpClientResponse pipeable diff --git a/repos/effect/.changeset/hip-friends-kiss.md b/repos/effect/.changeset/hip-friends-kiss.md new file mode 100644 index 0000000..a5c6635 --- /dev/null +++ b/repos/effect/.changeset/hip-friends-kiss.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +added graph set operations for combining and comparing graphs + +- `Graph.make` - creates a graph constructor for a dynamically selected graph kind +- `Graph.compose` - composition of two graphs, merging nodes by identity +- `Graph.intersection` - intersection of two graphs, keeping only common nodes and edges +- `Graph.difference` - difference of two graphs, removing edges present in the second graph +- `Graph.symmetricDifference` - symmetric difference of two graphs, keeping edges present in exactly one graph diff --git a/repos/effect/.changeset/hip-socks-travel.md b/repos/effect/.changeset/hip-socks-travel.md new file mode 100644 index 0000000..3f67b06 --- /dev/null +++ b/repos/effect/.changeset/hip-socks-travel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +short circuit when Fiber.joinAll is called with an empty iterable diff --git a/repos/effect/.changeset/honest-pens-thank.md b/repos/effect/.changeset/honest-pens-thank.md new file mode 100644 index 0000000..d5f0ee6 --- /dev/null +++ b/repos/effect/.changeset/honest-pens-thank.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpClient.withRateLimiter` for integrating the `RateLimiter` service with HTTP clients, including optional response-header driven limit updates and automatic 429 retry behavior. diff --git a/repos/effect/.changeset/honest-rivers-notice.md b/repos/effect/.changeset/honest-rivers-notice.md new file mode 100644 index 0000000..f3da306 --- /dev/null +++ b/repos/effect/.changeset/honest-rivers-notice.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +seperate scheduler dispatch from yield decisions diff --git a/repos/effect/.changeset/hot-taxis-fry.md b/repos/effect/.changeset/hot-taxis-fry.md new file mode 100644 index 0000000..2563a31 --- /dev/null +++ b/repos/effect/.changeset/hot-taxis-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix issue with exported CLI `Completions` types diff --git a/repos/effect/.changeset/hot-teeth-clean.md b/repos/effect/.changeset/hot-teeth-clean.md new file mode 100644 index 0000000..c737634 --- /dev/null +++ b/repos/effect/.changeset/hot-teeth-clean.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +change rpc ids to string | number diff --git a/repos/effect/.changeset/httpapi-endpoint-relax-constraints.md b/repos/effect/.changeset/httpapi-endpoint-relax-constraints.md new file mode 100644 index 0000000..b7bc906 --- /dev/null +++ b/repos/effect/.changeset/httpapi-endpoint-relax-constraints.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +HttpApiEndpoint: relax `params`, `query`, and `headers` constraints to accept a full schema in addition to a record of fields. diff --git a/repos/effect/.changeset/httpapi-schema-service-types.md b/repos/effect/.changeset/httpapi-schema-service-types.md new file mode 100644 index 0000000..bc86d05 --- /dev/null +++ b/repos/effect/.changeset/httpapi-schema-service-types.md @@ -0,0 +1,6 @@ +--- +"@effect/openapi-generator": patch +"effect": patch +--- + +Add HTTP API streaming response support diff --git a/repos/effect/.changeset/huge-moons-rhyme.md b/repos/effect/.changeset/huge-moons-rhyme.md new file mode 100644 index 0000000..5488d58 --- /dev/null +++ b/repos/effect/.changeset/huge-moons-rhyme.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +feat: Support Reference classes diff --git a/repos/effect/.changeset/humble-pigs-dig.md b/repos/effect/.changeset/humble-pigs-dig.md new file mode 100644 index 0000000..212d889 --- /dev/null +++ b/repos/effect/.changeset/humble-pigs-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +align HttpClientRequest constructors with http method names diff --git a/repos/effect/.changeset/hungry-kings-look.md b/repos/effect/.changeset/hungry-kings-look.md new file mode 100644 index 0000000..602fb34 --- /dev/null +++ b/repos/effect/.changeset/hungry-kings-look.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +Add glob to filesystem diff --git a/repos/effect/.changeset/icy-flies-cross.md b/repos/effect/.changeset/icy-flies-cross.md new file mode 100644 index 0000000..a627259 --- /dev/null +++ b/repos/effect/.changeset/icy-flies-cross.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/repos/effect/.changeset/itchy-radios-poke.md b/repos/effect/.changeset/itchy-radios-poke.md new file mode 100644 index 0000000..0ab497b --- /dev/null +++ b/repos/effect/.changeset/itchy-radios-poke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Abort HTTP client requests when response streams are consumed only partially. diff --git a/repos/effect/.changeset/itchy-results-bet.md b/repos/effect/.changeset/itchy-results-bet.md new file mode 100644 index 0000000..d3998b7 --- /dev/null +++ b/repos/effect/.changeset/itchy-results-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure non-middleware http errors are correctly handled diff --git a/repos/effect/.changeset/itchy-shrimps-deny.md b/repos/effect/.changeset/itchy-shrimps-deny.md new file mode 100644 index 0000000..bb120fb --- /dev/null +++ b/repos/effect/.changeset/itchy-shrimps-deny.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add ErrorReporter module diff --git a/repos/effect/.changeset/itchy-toes-promise.md b/repos/effect/.changeset/itchy-toes-promise.md new file mode 100644 index 0000000..2c8c704 --- /dev/null +++ b/repos/effect/.changeset/itchy-toes-promise.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Revert `Effect.partition` to Effect v3 behavior by accumulating failures from the effect error channel and never failing. diff --git a/repos/effect/.changeset/k8s-last-transition-null.md b/repos/effect/.changeset/k8s-last-transition-null.md new file mode 100644 index 0000000..d55d7ec --- /dev/null +++ b/repos/effect/.changeset/k8s-last-transition-null.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow Kubernetes pod condition `lastTransitionTime` values to be null in K8sHttpClient schemas. diff --git a/repos/effect/.changeset/keep-httpapi-composition-immutable.md b/repos/effect/.changeset/keep-httpapi-composition-immutable.md new file mode 100644 index 0000000..a4f9793 --- /dev/null +++ b/repos/effect/.changeset/keep-httpapi-composition-immutable.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Keep HttpApi composition immutable. + +`HttpApi.addHttpApi` applied annotations from the added API by mutating its shared groups. It now creates annotated group copies, keeping the source API and independently annotated variants unchanged while preserving annotation precedence. diff --git a/repos/effect/.changeset/khaki-cats-learn.md b/repos/effect/.changeset/khaki-cats-learn.md new file mode 100644 index 0000000..5a017b6 --- /dev/null +++ b/repos/effect/.changeset/khaki-cats-learn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpServerRequest.fromClientRequest` for direct client-request-backed server request conversion. diff --git a/repos/effect/.changeset/khaki-melons-appear.md b/repos/effect/.changeset/khaki-melons-appear.md new file mode 100644 index 0000000..72a00cf --- /dev/null +++ b/repos/effect/.changeset/khaki-melons-appear.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix RpcWorker Protocol service key diff --git a/repos/effect/.changeset/kind-hounds-float.md b/repos/effect/.changeset/kind-hounds-float.md new file mode 100644 index 0000000..b108a20 --- /dev/null +++ b/repos/effect/.changeset/kind-hounds-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `Effect.retry` with `times` argument to propagate the original error. diff --git a/repos/effect/.changeset/kind-windows-fall.md b/repos/effect/.changeset/kind-windows-fall.md new file mode 100644 index 0000000..3786a89 --- /dev/null +++ b/repos/effect/.changeset/kind-windows-fall.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename WorkflowEngine.layer diff --git a/repos/effect/.changeset/late-hotels-rule.md b/repos/effect/.changeset/late-hotels-rule.md new file mode 100644 index 0000000..4d7fc9e --- /dev/null +++ b/repos/effect/.changeset/late-hotels-rule.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the `Queue.takeN` documentation example to end the queue before showing a partial batch. diff --git a/repos/effect/.changeset/late-lamps-care.md b/repos/effect/.changeset/late-lamps-care.md new file mode 100644 index 0000000..cabcf77 --- /dev/null +++ b/repos/effect/.changeset/late-lamps-care.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.DurationFromString` and `SchemaTransformation.durationFromString`, support `"Infinity"` and `"-Infinity"` in `Duration.fromInput`, and simplify config duration parsing around the shared schema codec, closes #2092. diff --git a/repos/effect/.changeset/late-rivers-applaud.md b/repos/effect/.changeset/late-rivers-applaud.md new file mode 100644 index 0000000..37170d6 --- /dev/null +++ b/repos/effect/.changeset/late-rivers-applaud.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: relax `asserts` and `is` constraints. diff --git a/repos/effect/.changeset/layer-map-dynamic-idle-ttl.md b/repos/effect/.changeset/layer-map-dynamic-idle-ttl.md new file mode 100644 index 0000000..8a2532a --- /dev/null +++ b/repos/effect/.changeset/layer-map-dynamic-idle-ttl.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support key-derived `idleTimeToLive` in `LayerMap` options (`make`, `fromRecord`, and `LayerMap.Service`) and add `LayerMap` tests for dynamic TTL behavior. diff --git a/repos/effect/.changeset/lazy-queens-rush.md b/repos/effect/.changeset/lazy-queens-rush.md new file mode 100644 index 0000000..616b7a5 --- /dev/null +++ b/repos/effect/.changeset/lazy-queens-rush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Context.mutate diff --git a/repos/effect/.changeset/lazy-recursive-forward-refs.md b/repos/effect/.changeset/lazy-recursive-forward-refs.md new file mode 100644 index 0000000..29e13a7 --- /dev/null +++ b/repos/effect/.changeset/lazy-recursive-forward-refs.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix generated schema declaration ordering when non-recursive schemas reference recursive schemas, preventing TypeScript use-before-declaration errors in generated clients and HttpApi modules. diff --git a/repos/effect/.changeset/lazy-timers-exist.md b/repos/effect/.changeset/lazy-timers-exist.md new file mode 100644 index 0000000..e66de3a --- /dev/null +++ b/repos/effect/.changeset/lazy-timers-exist.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove fiber-level keep-alive intervals and keep the process alive from `Runtime.makeRunMain` instead. diff --git a/repos/effect/.changeset/legal-pants-drop.md b/repos/effect/.changeset/legal-pants-drop.md new file mode 100644 index 0000000..9bd6f6c --- /dev/null +++ b/repos/effect/.changeset/legal-pants-drop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove body restriction for HttpClientRequest's diff --git a/repos/effect/.changeset/lemon-taxis-sin.md b/repos/effect/.changeset/lemon-taxis-sin.md new file mode 100644 index 0000000..750c325 --- /dev/null +++ b/repos/effect/.changeset/lemon-taxis-sin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make HttpApi schema errors defects unless transformed diff --git a/repos/effect/.changeset/light-kids-sneeze.md b/repos/effect/.changeset/light-kids-sneeze.md new file mode 100644 index 0000000..926ba7a --- /dev/null +++ b/repos/effect/.changeset/light-kids-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +omit scope from HttpApi handlers diff --git a/repos/effect/.changeset/little-dryers-allow.md b/repos/effect/.changeset/little-dryers-allow.md new file mode 100644 index 0000000..9211c90 --- /dev/null +++ b/repos/effect/.changeset/little-dryers-allow.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Persist MCP client capability context across HTTP requests by resolving initialized payloads through the standard `Mcp-Session-Id` HTTP header in `McpServer`. + +Adds a regression test that initializes an MCP HTTP client, verifies the MCP server echoes `Mcp-Session-Id`, and then checks a later tool call can still read `McpServer.clientCapabilities`. diff --git a/repos/effect/.changeset/long-cameras-think.md b/repos/effect/.changeset/long-cameras-think.md new file mode 100644 index 0000000..aafbd1f --- /dev/null +++ b/repos/effect/.changeset/long-cameras-think.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Forward `OpenAiLanguageModel` `reasoning` config into chat-completions requests. diff --git a/repos/effect/.changeset/lovely-cobras-change.md b/repos/effect/.changeset/lovely-cobras-change.md new file mode 100644 index 0000000..e15d453 --- /dev/null +++ b/repos/effect/.changeset/lovely-cobras-change.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-solid": patch +--- + +allow atoms to be computed in solid bindings diff --git a/repos/effect/.changeset/lovely-frogs-rescue.md b/repos/effect/.changeset/lovely-frogs-rescue.md new file mode 100644 index 0000000..eb7ae29 --- /dev/null +++ b/repos/effect/.changeset/lovely-frogs-rescue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow mcp errors to be encoded correctly diff --git a/repos/effect/.changeset/lucky-buttons-jump.md b/repos/effect/.changeset/lucky-buttons-jump.md new file mode 100644 index 0000000..771c41a --- /dev/null +++ b/repos/effect/.changeset/lucky-buttons-jump.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add an optional `message` field to `Effect.ignore` and `Effect.ignoreCause` for custom log output. diff --git a/repos/effect/.changeset/lucky-phones-listen.md b/repos/effect/.changeset/lucky-phones-listen.md new file mode 100644 index 0000000..0635e0a --- /dev/null +++ b/repos/effect/.changeset/lucky-phones-listen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpApiClient.urlBuilder` for type-safe endpoint URL construction from group + method/path keys. diff --git a/repos/effect/.changeset/lucky-worms-type.md b/repos/effect/.changeset/lucky-worms-type.md new file mode 100644 index 0000000..d4814cc --- /dev/null +++ b/repos/effect/.changeset/lucky-worms-type.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.HashMap` for decoding and encoding `HashMap` values. diff --git a/repos/effect/.changeset/major-chairs-design.md b/repos/effect/.changeset/major-chairs-design.md new file mode 100644 index 0000000..6e00b3c --- /dev/null +++ b/repos/effect/.changeset/major-chairs-design.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix RequestResolver interruption diff --git a/repos/effect/.changeset/many-badgers-obey.md b/repos/effect/.changeset/many-badgers-obey.md new file mode 100644 index 0000000..6ec45f5 --- /dev/null +++ b/repos/effect/.changeset/many-badgers-obey.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow schemas provided to CLI flags / arguments to utilize the environment required by the CLI diff --git a/repos/effect/.changeset/mean-dingos-share.md b/repos/effect/.changeset/mean-dingos-share.md new file mode 100644 index 0000000..2fd4aef --- /dev/null +++ b/repos/effect/.changeset/mean-dingos-share.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Command.annotate` and `Command.annotateMerge` to unstable CLI commands, and include command annotations in `HelpDoc` so custom help formatters can access command metadata. diff --git a/repos/effect/.changeset/mean-trains-smash.md b/repos/effect/.changeset/mean-trains-smash.md new file mode 100644 index 0000000..0054e60 --- /dev/null +++ b/repos/effect/.changeset/mean-trains-smash.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Add BrowserPersistence.layerIndexedDb for composing Persistence.layer with the IndexedDB backing layer, and export BrowserPersistence from the package barrel. diff --git a/repos/effect/.changeset/metal-nails-sneeze.md b/repos/effect/.changeset/metal-nails-sneeze.md new file mode 100644 index 0000000..148d8c6 --- /dev/null +++ b/repos/effect/.changeset/metal-nails-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add LayerRef module diff --git a/repos/effect/.changeset/metal-parts-yell.md b/repos/effect/.changeset/metal-parts-yell.md new file mode 100644 index 0000000..a6d6745 --- /dev/null +++ b/repos/effect/.changeset/metal-parts-yell.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify http logger disabling diff --git a/repos/effect/.changeset/mighty-games-matter.md b/repos/effect/.changeset/mighty-games-matter.md new file mode 100644 index 0000000..d536ab3 --- /dev/null +++ b/repos/effect/.changeset/mighty-games-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove use of bigint literals diff --git a/repos/effect/.changeset/modern-carrots-see.md b/repos/effect/.changeset/modern-carrots-see.md new file mode 100644 index 0000000..83937c3 --- /dev/null +++ b/repos/effect/.changeset/modern-carrots-see.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +RpcGroup.toHandlers is definition first diff --git a/repos/effect/.changeset/modern-uuid-guid-filter.md b/repos/effect/.changeset/modern-uuid-guid-filter.md new file mode 100644 index 0000000..034948a --- /dev/null +++ b/repos/effect/.changeset/modern-uuid-guid-filter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.isGUID` and update `Schema.isUUID` to accept the RFC 9562 max UUID. diff --git a/repos/effect/.changeset/multipart-onDone-clobbers-error.md b/repos/effect/.changeset/multipart-onDone-clobbers-error.md new file mode 100644 index 0000000..621db72 --- /dev/null +++ b/repos/effect/.changeset/multipart-onDone-clobbers-error.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix multipart parser limit violations being silently swallowed diff --git a/repos/effect/.changeset/nasty-geese-grow.md b/repos/effect/.changeset/nasty-geese-grow.md new file mode 100644 index 0000000..ed408dc --- /dev/null +++ b/repos/effect/.changeset/nasty-geese-grow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Scheduler.PreventSchedulerYield` and expose it via `References` so fibers can skip scheduler `shouldYield` checks when needed. diff --git a/repos/effect/.changeset/neat-goats-wave.md b/repos/effect/.changeset/neat-goats-wave.md new file mode 100644 index 0000000..bb3f2c4 --- /dev/null +++ b/repos/effect/.changeset/neat-goats-wave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Stream.service` and `Stream.serviceOption` for accessing services as single-element streams. diff --git a/repos/effect/.changeset/neat-kings-chew.md b/repos/effect/.changeset/neat-kings-chew.md new file mode 100644 index 0000000..57a18a6 --- /dev/null +++ b/repos/effect/.changeset/neat-kings-chew.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add a new `effect/unstable/http/HttpStaticServer` module for static file serving with MIME resolution, directory index fallback, SPA fallback, and safe path resolution. diff --git a/repos/effect/.changeset/neat-lions-rest.md b/repos/effect/.changeset/neat-lions-rest.md new file mode 100644 index 0000000..e0aa7c3 --- /dev/null +++ b/repos/effect/.changeset/neat-lions-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `WorkflowEngine.layer`, an in-memory layer for the unstable workflow engine. diff --git a/repos/effect/.changeset/neat-snails-wash.md b/repos/effect/.changeset/neat-snails-wash.md new file mode 100644 index 0000000..3ab4515 --- /dev/null +++ b/repos/effect/.changeset/neat-snails-wash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose a `chunkSize` option on `Stream.fromIterable` to control emitted chunk boundaries when constructing streams from iterables. diff --git a/repos/effect/.changeset/neat-taxis-notice.md b/repos/effect/.changeset/neat-taxis-notice.md new file mode 100644 index 0000000..ffdd50e --- /dev/null +++ b/repos/effect/.changeset/neat-taxis-notice.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Effect.forkScoped` data-first typings to include `Scope` in requirements. diff --git a/repos/effect/.changeset/neat-windows-buy.md b/repos/effect/.changeset/neat-windows-buy.md new file mode 100644 index 0000000..661cff0 --- /dev/null +++ b/repos/effect/.changeset/neat-windows-buy.md @@ -0,0 +1,10 @@ +--- +"@effect/sql-mssql": patch +--- + +Added some options for the MssqlClient: + +- multiSubnetFailover +- cancelTimeout +- connectionRetryInterval +- maxRetriesOnTransientErrors diff --git a/repos/effect/.changeset/new-dogs-swim.md b/repos/effect/.changeset/new-dogs-swim.md new file mode 100644 index 0000000..ef306ca --- /dev/null +++ b/repos/effect/.changeset/new-dogs-swim.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +make defects a retryable network error in websocket mode diff --git a/repos/effect/.changeset/new-toes-stop.md b/repos/effect/.changeset/new-toes-stop.md new file mode 100644 index 0000000..4270fd0 --- /dev/null +++ b/repos/effect/.changeset/new-toes-stop.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add idb stream and offset diff --git a/repos/effect/.changeset/ninety-geese-exist.md b/repos/effect/.changeset/ninety-geese-exist.md new file mode 100644 index 0000000..20e8ce1 --- /dev/null +++ b/repos/effect/.changeset/ninety-geese-exist.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add HttpApiSecurity.http for passing custom schemes diff --git a/repos/effect/.changeset/normalize-httpapi-payload-media-types.md b/repos/effect/.changeset/normalize-httpapi-payload-media-types.md new file mode 100644 index 0000000..ae6a0a7 --- /dev/null +++ b/repos/effect/.changeset/normalize-httpapi-payload-media-types.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Normalize HttpApi payload media types. + +Payload schemas were stored under their exact declared `Content-Type`, but the server lowercased the incoming header and removed its parameters before looking it up. For example, a schema declared as `Application/Vnd.Effect+JSON; profile=declared` was stored under that value, while the server looked for `application/vnd.effect+json`. This could produce a `415` response even when the generated client and server used the same API. + +The same mismatch allowed incompatible encodings for equivalent media types to bypass validation. Generated form-urlencoded requests also ignored custom content types and always used the default one. + +Payload maps now use normalized keys for matching and conflict checks, while each encoding keeps its declared content type. Generated requests and OpenAPI use the declared values, including every parameterized variant, and custom form-urlencoded content types are preserved. diff --git a/repos/effect/.changeset/o8drprcu-sqlite-node-node-sqlite.md b/repos/effect/.changeset/o8drprcu-sqlite-node-node-sqlite.md new file mode 100644 index 0000000..b184046 --- /dev/null +++ b/repos/effect/.changeset/o8drprcu-sqlite-node-node-sqlite.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-node": patch +--- + +Replace the `better-sqlite3` dependency with Node.js' built-in `node:sqlite` module. diff --git a/repos/effect/.changeset/odd-boats-think.md b/repos/effect/.changeset/odd-boats-think.md new file mode 100644 index 0000000..7319b91 --- /dev/null +++ b/repos/effect/.changeset/odd-boats-think.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `isMutableHashMap` and `isMutableHashSet`, and align nominal guard implementations and tests across collections and transactional data types. diff --git a/repos/effect/.changeset/odd-bulldogs-sleep.md b/repos/effect/.changeset/odd-bulldogs-sleep.md new file mode 100644 index 0000000..baf8808 --- /dev/null +++ b/repos/effect/.changeset/odd-bulldogs-sleep.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add dtslint coverage for `Stream.catchIf` to lock in predicate and refinement inference behavior in both data-first and data-last forms. diff --git a/repos/effect/.changeset/odd-fans-glow.md b/repos/effect/.changeset/odd-fans-glow.md new file mode 100644 index 0000000..ac7a1a8 --- /dev/null +++ b/repos/effect/.changeset/odd-fans-glow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.groupedWithin` to stop emitting empty arrays when schedule ticks fire while upstream is idle. diff --git a/repos/effect/.changeset/odd-forks-talk.md b/repos/effect/.changeset/odd-forks-talk.md new file mode 100644 index 0000000..150e017 --- /dev/null +++ b/repos/effect/.changeset/odd-forks-talk.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Add dynamic tool support to the Anthropic language model provider when preparing tool definitions for requests. diff --git a/repos/effect/.changeset/odd-laws-draw.md b/repos/effect/.changeset/odd-laws-draw.md new file mode 100644 index 0000000..695ffce --- /dev/null +++ b/repos/effect/.changeset/odd-laws-draw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schedule.duration`, a one-shot schedule that waits for the provided duration and then completes. diff --git a/repos/effect/.changeset/odd-owls-smoke.md b/repos/effect/.changeset/odd-owls-smoke.md new file mode 100644 index 0000000..7085a3b --- /dev/null +++ b/repos/effect/.changeset/odd-owls-smoke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Schedule.compose` in favor of `Schedule.both`, and update schedule examples to use `Schedule.both`. diff --git a/repos/effect/.changeset/odd-socks-boil.md b/repos/effect/.changeset/odd-socks-boil.md new file mode 100644 index 0000000..af72ca9 --- /dev/null +++ b/repos/effect/.changeset/odd-socks-boil.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Use a dedicated PostgreSQL client for LISTEN / UNLISTEN subscriptions instead of checking out a pooled connection for the listener lifecycle. diff --git a/repos/effect/.changeset/odd-suns-dance.md b/repos/effect/.changeset/odd-suns-dance.md new file mode 100644 index 0000000..226e74f --- /dev/null +++ b/repos/effect/.changeset/odd-suns-dance.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename HttpApiClient request option `withResponse` to `responseMode` and add support for `responseMode: "response-only"` to return the raw `HttpClientResponse` without decoding. diff --git a/repos/effect/.changeset/old-brooms-cry.md b/repos/effect/.changeset/old-brooms-cry.md new file mode 100644 index 0000000..525a7a8 --- /dev/null +++ b/repos/effect/.changeset/old-brooms-cry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Filter.reason api diff --git a/repos/effect/.changeset/old-facts-stand.md b/repos/effect/.changeset/old-facts-stand.md new file mode 100644 index 0000000..99a8116 --- /dev/null +++ b/repos/effect/.changeset/old-facts-stand.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fixed the Error Type on AtomHttpApiClient (Server errors were being incorrectly reported, and we could not determine _tag to handle) diff --git a/repos/effect/.changeset/old-mirrors-float.md b/repos/effect/.changeset/old-mirrors-float.md new file mode 100644 index 0000000..a50aab5 --- /dev/null +++ b/repos/effect/.changeset/old-mirrors-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make Layer.mock work with Stream and Channel diff --git a/repos/effect/.changeset/olive-poems-visit.md b/repos/effect/.changeset/olive-poems-visit.md new file mode 100644 index 0000000..f09144e --- /dev/null +++ b/repos/effect/.changeset/olive-poems-visit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Workflow.executionId` to use schema `makeUnsafe` instead of the removed `.make` API. diff --git a/repos/effect/.changeset/opaque-graph-interface.md b/repos/effect/.changeset/opaque-graph-interface.md new file mode 100644 index 0000000..47e27a0 --- /dev/null +++ b/repos/effect/.changeset/opaque-graph-interface.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make the public Graph interfaces opaque by hiding internal mutable storage fields from their TypeScript surface. diff --git a/repos/effect/.changeset/open-hotels-remain.md b/repos/effect/.changeset/open-hotels-remain.md new file mode 100644 index 0000000..633584c --- /dev/null +++ b/repos/effect/.changeset/open-hotels-remain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +extend McpSchema to work with extensions diff --git a/repos/effect/.changeset/openai-compat-empty-assistant-content.md b/repos/effect/.changeset/openai-compat-empty-assistant-content.md new file mode 100644 index 0000000..8426aa0 --- /dev/null +++ b/repos/effect/.changeset/openai-compat-empty-assistant-content.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Normalize empty assistant message content to an empty string for OpenAI-compatible providers that reject null content values. diff --git a/repos/effect/.changeset/openai-compat-nullable-tool-name.md b/repos/effect/.changeset/openai-compat-nullable-tool-name.md new file mode 100644 index 0000000..96bc6ca --- /dev/null +++ b/repos/effect/.changeset/openai-compat-nullable-tool-name.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Fix dropped streamed tool-call arguments when a provider sends `function.name: null` on continuation fragments. + +OpenAI-compatible providers such as Fireworks send the tool name only on the first streamed `tool_calls` fragment and `function.name: null` on the continuation fragments that carry the argument deltas. `ChatCompletionToolFunctionDelta.name` was `Schema.optionalKey(Schema.String)` (non-nullable), so chunk validation rejected every continuation and silently discarded its argument delta, leaving the assembled tool call with empty or partial params. `name` is now nullable (`Schema.NullOr(Schema.String)`). diff --git a/repos/effect/.changeset/openai-compat-reasoning.md b/repos/effect/.changeset/openai-compat-reasoning.md new file mode 100644 index 0000000..9a8e6b8 --- /dev/null +++ b/repos/effect/.changeset/openai-compat-reasoning.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Surface reasoning tokens from OpenAI-compatible chat completion responses. diff --git a/repos/effect/.changeset/openapi-generator-sse-constraint-decoder.md b/repos/effect/.changeset/openapi-generator-sse-constraint-decoder.md new file mode 100644 index 0000000..3584510 --- /dev/null +++ b/repos/effect/.changeset/openapi-generator-sse-constraint-decoder.md @@ -0,0 +1,7 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix the generated SSE `sseRequest` helper to reference `Schema.ConstraintDecoder` instead of the no-longer-exported `Schema.Decoder`. + +For specs with `text/event-stream` responses the generator emitted a helper typed as `Schema.Decoder`, but `Schema` exports that decode-only interface as `ConstraintDecoder`, so generated clients failed to compile (`'"effect/Schema"' has no exported member named 'Decoder'`). The emitted helper now uses `Schema.ConstraintDecoder`. diff --git a/repos/effect/.changeset/openrouter-input-audio.md b/repos/effect/.changeset/openrouter-input-audio.md new file mode 100644 index 0000000..8dc06d3 --- /dev/null +++ b/repos/effect/.changeset/openrouter-input-audio.md @@ -0,0 +1,9 @@ +--- +"@effect/ai-openrouter": patch +--- + +Convert audio file parts in prompts into OpenRouter `input_audio` content blocks. + +Previously, every non-image file part was converted into a generic `file` content block. OpenRouter only accepts audio as base64-encoded `input_audio` content parts, so audio attachments were rejected or mishandled by the upstream provider. + +Audio file parts with a recognized `mediaType` (aac, aiff, flac, m4a, mp3, ogg, pcm16, pcm24, and wav) are now converted into `input_audio` blocks. Unsupported audio media types and `URL` audio data fail with an `AiError` since OpenRouter requires base64-encoded audio data and does not fetch audio URLs. diff --git a/repos/effect/.changeset/opentelemetry-render-causes.md b/repos/effect/.changeset/opentelemetry-render-causes.md new file mode 100644 index 0000000..4ebcd6e --- /dev/null +++ b/repos/effect/.changeset/opentelemetry-render-causes.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Add `includeCauseInStack` option to `Cause.prettyErrors` diff --git a/repos/effect/.changeset/optimize-httpapi-handler-types.md b/repos/effect/.changeset/optimize-httpapi-handler-types.md new file mode 100644 index 0000000..af37995 --- /dev/null +++ b/repos/effect/.changeset/optimize-httpapi-handler-types.md @@ -0,0 +1,204 @@ +--- +"effect": patch +--- + +Improve unstable `HttpApi` type-level performance. + +The implementation now uses identifier-keyed maps and lighter structural +constraints in several hot type-level paths. Generated group clients consume the +concrete endpoint map directly instead of rebuilding it from the endpoint union. + +## New Features + +- Add `HttpApiBuilder.Handlers.handleAll`, which registers an identifier-keyed batch of endpoint handlers for a group. Each entry can be either a handler function or `{ handler, options }`, and the object can be supplied in multiple partial batches. Endpoint identifiers that were already handled are rejected across batches. +- `HttpApi.groups` now preserves the concrete group type for each group identifier. For example, `Api.groups.users` is typed as the `users` group instead of the full group union. +- `HttpApiGroup.endpoints` now preserves the concrete endpoint type for each endpoint identifier. For example, `Group.endpoints.getUser` is typed as the `getUser` endpoint instead of the full endpoint union. +- `HttpApiEndpoint` values can now be extended as classes, matching the class-like + runtime shape already used by `HttpApi` and `HttpApiGroup`. + +## Measured Type-Level Performance + +Main/current comparisons use identical generated fixtures compiled once per +revision with TypeScript 7.0.2. The recorded revisions are `main` at +`97fdaa9c1f52` and the branch source at `5798fc5fafcd`. The focused pre/post +curves below were captured with the regular `httpapi` regression suite during +development. The retained suite uses representative stress points instead of +rerunning every point in those historical curves. All numbers are +type-instantiation deltas over the corresponding shared baseline. + +Endpoint declaration costs now grow with a lower slope: + +| endpoints | main | current | +| --------: | ------: | ------: | +| 10 | 4,580 | 2,808 | +| 50 | 15,500 | 9,168 | +| 100 | 29,150 | 17,118 | +| 500 | 138,350 | 80,718 | + +Class-like endpoint declarations are slightly cheaper than inline endpoint +values in the same 500-endpoint fixture shape: + +| fixture | inline | class-like | +| ------------- | -----: | ---------: | +| 500 endpoints | 82,207 | 71,850 | + +`HttpApiBuilder` fluent handler registration avoids the previous non-linear +blow-up in the cross-ref comparison: + +| fixture | main | current | +| ---------------- | ---------: | --------: | +| 10 endpoints | 37,856 | 11,582 | +| 50 endpoints | 568,576 | 63,702 | +| 100 endpoints | 2,154,476 | 182,852 | +| 500 endpoints | 51,741,676 | 3,296,052 | +| 500 raw handlers | 51,734,176 | 3,294,550 | + +In the recorded regular-suite measurements, `handleAll` remains the scalable +alternative to the equivalent fluent chain: + +| fixture | fluent | `handleAll` | +| -------------------- | --------: | ----------: | +| 10 endpoints | 11,579 | 9,146 | +| 50 endpoints | 63,699 | 25,106 | +| 100 endpoints | 182,849 | 45,056 | +| 500 endpoints | 3,296,049 | 204,656 | +| 500 eps, two batches | 3,296,049 | 223,613 | + +Generated-client type production also improves for the hot method-building +paths: + +| fixture | main | current | +| --------------------------------------- | ------: | ------: | +| client methods, 500 endpoints | 245,795 | 176,850 | +| top-level client methods, 500 endpoints | 243,651 | 179,809 | +| client endpoint method, 500 endpoints | 56,738 | 46,294 | +| client groups, 100 groups x 5 endpoints | 49,019 | 25,893 | + +The following focused curves were captured immediately before and after each +isolated type-level change. + +The focused `Client.Group` curve shows the improvement from consuming the +identifier-keyed endpoint map directly: + +| endpoints | union remapping | endpoint map | +| --------: | --------------: | -----------: | +| 10 | 12,448 | 12,294 | +| 50 | 19,169 | 18,935 | +| 100 | 27,570 | 27,236 | +| 500 | 94,770 | 93,636 | + +The focused `Client.TopLevelMethods` curve improves by reading endpoint +identifiers directly from the endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 12,531 | 12,476 | +| 50 | 19,252 | 19,197 | +| 100 | 27,653 | 27,598 | +| 500 | 94,853 | 94,798 | + +The focused `HttpApiClient.endpoint` selection curve improves by reading +endpoint identifiers directly from the selected endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 7,666 | 7,588 | +| 50 | 8,707 | 8,629 | +| 100 | 10,008 | 9,930 | +| 500 | 20,408 | 20,330 | + +The focused `HttpApiBuilder.endpoint` selection curve improves by reading +endpoint identifiers directly from the selected endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 12,828 | 12,745 | +| 50 | 13,869 | 13,786 | +| 100 | 15,170 | 15,087 | +| 500 | 25,570 | 25,487 | + +URL builder types now avoid repeatedly expanding the full API/group shape: + +| fixture | main | current | +| ------------------------------------ | ------: | ------: | +| URL builder, 500 endpoints | 211,356 | 91,610 | +| top-level URL builder, 500 endpoints | 210,724 | 93,118 | +| builder endpoint, 500 endpoints | 62,894 | 51,952 | + +## Breaking Changes + +These changes affect unstable `HttpApi` type-level APIs and structural API, +group, and endpoint types. + +### Renamed Constraint Types + +- Broad structural constraint exports have been renamed to align with + `Schema.Constraint` terminology: `HttpApi.Any` to `HttpApi.Constraint`, + `HttpApi.AnyWithProps` to `HttpApi.Top`, `HttpApiGroup.Any` to + `HttpApiGroup.Constraint`, `HttpApiGroup.AnyWithProps` to `HttpApiGroup.Top`, + and `HttpApiEndpoint.Any` to `HttpApiEndpoint.Constraint`. +- `HttpApiEndpoint.AnyWithProps` has been replaced by `HttpApiEndpoint.Top`, whose + schema parameters are constrained to `Schema.Top`, including success and error + schemas. +- Type guards now expose the widened runtime-prop shapes: `HttpApi.isHttpApi` + returns `HttpApi.Top`, `HttpApiGroup.isHttpApiGroup` returns + `HttpApiGroup.Top`, and `HttpApiEndpoint.isHttpApiEndpoint` returns + `HttpApiEndpoint.Top`. +- `HttpApiGroup.ApiGroup` has been renamed to `HttpApiGroup.Service`. + +### API, Group, And Endpoint Shapes + +- `HttpApi.groups` is now typed as an identifier-keyed group map instead of + `ReadonlyRecord`, and `HttpApi` tracks its group union + invariantly. Dynamic string indexing must refine the key first or cast to a + broad runtime record. +- `HttpApiGroup.endpoints` is now typed as an identifier-keyed endpoint map instead of + `ReadonlyRecord`, and `HttpApiGroup` tracks its endpoint + union invariantly. Dynamic string indexing must refine the key first or cast to + a broad runtime record. +- `HttpApiEndpoint` now exposes its stable key as `identifier` instead of `name`, + aligning endpoints with APIs and groups and leaving `name` available for future + class-based endpoint patterns. +- `HttpApiEndpoint` values are now function objects instead of plain objects. + Runtime checks such as `typeof endpoint` now return `"function"`, and + `endpoint.name` is the native function name. Use `endpoint.identifier` for the + stable endpoint key. +- Identifier helper types have been renamed from `Name` / `WithName` to + `Identifier` / `WithIdentifier`; `HttpApiGroup.Service` now exposes + `identifier` instead of `name`. + +### Builder Handler Types + +- `HttpApiBuilder.Handlers` now tracks endpoints through an identifier-keyed endpoint map and a set of handled endpoint identifiers, instead of tracking the remaining endpoint union. Its public type parameters changed from `Handlers` to `Handlers`, and its phantom fields changed from `_Endpoints` to `~EndpointsByIdentifier` / `~HandledIdentifiers`. +- The unused `HttpApiBuilder.Handlers.Any` helper type has been removed. +- The exported `HttpApiBuilder.HandlersTypeId` symbol has been removed; `Handlers` + now uses a private string type id. +- Duplicate `handle` / `handleRaw` registrations for the same endpoint are rejected + at the call site, and `handleAll` rejects endpoint identifiers that were already + handled by an earlier batch. Missing endpoint handlers are still rejected by + the final `HttpApiBuilder.group` return validation. + +### Client Types + +- `HttpApiClient.Client.Group` now derives a client from a concrete group type: `Client.Group`. The previous group-union plus group-identifier form is no longer supported. +- `HttpApiClient.Client.TopLevelMethods` now returns an identifier-keyed method record instead of a union of `[identifier, method]` tuples. +- `HttpApiClient.makeWith` removes the default `HttpClientError.HttpClientError` from custom client error types in the returned `Client`, while preserving any additional custom client errors. + +### Endpoint Helper Types + +- `HttpApiEndpoint.HttpApiEndpoint` now stores lightweight phantom metadata for middleware and request shapes: `~Middleware`, `~MiddlewareServices`, `~Request`, and `~RequestRaw`. Its type identifier field is now `readonly [TypeId]: typeof TypeId`. +- `HttpApiEndpoint.Constraint` is now a lightweight structural endpoint constraint and does not extend `Pipeable`; values typed only as `HttpApiEndpoint.Constraint` do not expose `.pipe`. +- `HttpApiEndpoint.AddError` has been removed; it was not used internally by the `HttpApi` implementation. +- `HttpApiEndpoint.Json` and `HttpApiEndpoint.StringTree` have been removed in + favor of the canonical `Schema.toCodecJson` and `Schema.toCodecStringTree` + types. +- Omitted request-part metadata now remains `never` instead of being wrapped as + `Schema.toCodecStringTree`; codec metadata is applied only when + a params, query, payload, or headers schema is present. +- Success metadata now applies `Schema.toCodecJson` only to buffered + success schemas and preserves stream success schemas unchanged, including + mixed buffered and streaming success arrays. +- Handler request parts are now flattened with `Struct.Simplify`, improving + displayed request types while reducing handler instantiations. +- Endpoint helper types now read metadata fields directly instead of re-inferring all type parameters from the full `HttpApiEndpoint` interface. This affects helpers such as `Identifier`, `Success`, `Error`, `Params`, `Query`, `Payload`, `Headers`, `Middleware`, `MiddlewareServices`, `Errors`, `ErrorServicesEncode`, `ErrorServicesDecode`, `Request`, `RequestRaw`, `ServerServices`, and `ClientServices`. +- `HttpApiClient.Client.Method` and related generated-client helpers now require endpoint types that satisfy `HttpApiEndpoint.ConstraintRequest`. Endpoint-like structural types must include the lightweight request metadata fields to be accepted. diff --git a/repos/effect/.changeset/otel-resource-env-precedence.md b/repos/effect/.changeset/otel-resource-env-precedence.md new file mode 100644 index 0000000..c95fd27 --- /dev/null +++ b/repos/effect/.changeset/otel-resource-env-precedence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prefer OTEL resource environment variables over explicit `OtlpResource.fromConfig` options. diff --git a/repos/effect/.changeset/perfect-buckets-tickle.md b/repos/effect/.changeset/perfect-buckets-tickle.md new file mode 100644 index 0000000..a17e31a --- /dev/null +++ b/repos/effect/.changeset/perfect-buckets-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add default value support to `Prompt.file`. diff --git a/repos/effect/.changeset/petite-months-allow.md b/repos/effect/.changeset/petite-months-allow.md new file mode 100644 index 0000000..04ad2d0 --- /dev/null +++ b/repos/effect/.changeset/petite-months-allow.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +add dynamic tooling for openai and openai-compact language models diff --git a/repos/effect/.changeset/platform-crypto-service.md b/repos/effect/.changeset/platform-crypto-service.md new file mode 100644 index 0000000..afbf216 --- /dev/null +++ b/repos/effect/.changeset/platform-crypto-service.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-node-shared": patch +"@effect/platform-bun": patch +"@effect/platform-browser": patch +--- + +Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. diff --git a/repos/effect/.changeset/platform-node-shared-barrel.md b/repos/effect/.changeset/platform-node-shared-barrel.md new file mode 100644 index 0000000..f73cae6 --- /dev/null +++ b/repos/effect/.changeset/platform-node-shared-barrel.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Add the package root barrel export. diff --git a/repos/effect/.changeset/plenty-moons-pull.md b/repos/effect/.changeset/plenty-moons-pull.md new file mode 100644 index 0000000..adac8d0 --- /dev/null +++ b/repos/effect/.changeset/plenty-moons-pull.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +use encoded types for idb queries diff --git a/repos/effect/.changeset/polite-brooms-tickle.md b/repos/effect/.changeset/polite-brooms-tickle.md new file mode 100644 index 0000000..89a9b75 --- /dev/null +++ b/repos/effect/.changeset/polite-brooms-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `LogLevel.isEnabled` for checking a log level against `References.MinimumLogLevel`. diff --git a/repos/effect/.changeset/polite-pigs-speak.md b/repos/effect/.changeset/polite-pigs-speak.md new file mode 100644 index 0000000..44d427f --- /dev/null +++ b/repos/effect/.changeset/polite-pigs-speak.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use .toJSON for default .toString implementations diff --git a/repos/effect/.changeset/polite-tables-kneel.md b/repos/effect/.changeset/polite-tables-kneel.md new file mode 100644 index 0000000..e80db99 --- /dev/null +++ b/repos/effect/.changeset/polite-tables-kneel.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix tool calling for the Anthropic Effect AI SDK provider integration diff --git a/repos/effect/.changeset/port-react-hydration.md b/repos/effect/.changeset/port-react-hydration.md new file mode 100644 index 0000000..a7cd38e --- /dev/null +++ b/repos/effect/.changeset/port-react-hydration.md @@ -0,0 +1,8 @@ +--- +"effect": patch +"@effect/atom-react": patch +--- + +Port ReactHydration to effect-smol. + +Add `Hydration` module to `effect/unstable/reactivity` with `dehydrate`, `hydrate`, and `toValues` for SSR state serialization. Add `HydrationBoundary` React component to `@effect/atom-react` with two-phase hydration (new atoms in render, existing atoms after commit). diff --git a/repos/effect/.changeset/pre.json b/repos/effect/.changeset/pre.json new file mode 100644 index 0000000..6a8d38b --- /dev/null +++ b/repos/effect/.changeset/pre.json @@ -0,0 +1,737 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@effect/ai-anthropic": "3.0.0", + "@effect/ai-openai": "3.0.0", + "@effect/ai-openrouter": "3.0.0", + "@effect/atom-react": "3.0.0", + "@effect/atom-solid": "3.0.0", + "@effect/atom-vue": "3.0.0", + "effect": "3.0.0", + "@effect/opentelemetry": "3.0.0", + "@effect/platform-browser": "3.0.0", + "@effect/platform-bun": "3.0.0", + "@effect/platform-node": "3.0.0", + "@effect/platform-node-shared": "3.0.0", + "@effect/sql-clickhouse": "3.0.0", + "@effect/sql-d1": "3.0.0", + "@effect/sql-libsql": "3.0.0", + "@effect/sql-mssql": "3.0.0", + "@effect/sql-mysql2": "3.0.0", + "@effect/sql-pg": "3.0.0", + "@effect/sql-sqlite-bun": "3.0.0", + "@effect/sql-sqlite-do": "3.0.0", + "@effect/sql-sqlite-node": "3.0.0", + "@effect/sql-sqlite-react-native": "3.0.0", + "@effect/sql-sqlite-wasm": "3.0.0", + "@effect/openapi-generator": "3.0.0", + "@effect/oxc": "3.0.0", + "@effect/utils": "3.0.0", + "@effect/vitest": "3.0.0", + "scratchpad": "0.0.0", + "scripts": "0.0.0", + "@effect/ai-openai-compat": "3.0.0", + "@effect/ai-codegen": "0.0.0", + "@effect/bundle": "0.0.0", + "ai-docs": "0.0.0", + "@effect/ai-docgen": "0.0.0", + "@effect/sql-pglite": "4.0.0-beta.52", + "@effect/jsdocs": "0.0.0" + }, + "changesets": [ + "add-adaptive-rate-limiter-store", + "add-bigdecimal-sumall-multiplyall", + "add-chunk-schema", + "add-command-hidden", + "add-config-nested", + "add-flag-hidden", + "add-from-string-schemas", + "add-headers-remove-many", + "add-indexeddb-kvs-layer", + "add-make-msgpack", + "add-make-option", + "add-missing-tx-modules", + "add-newtype-module", + "add-scalar-show-operation-id", + "add-schedule-tap", + "add-schema-annotate-encoded", + "add-schema-array-ensure", + "add-schema-bigdecimal", + "add-schema-datetime", + "add-schema-error-module", + "add-schema-option-from-optional-nullor", + "add-schema-option-from-undefined-nullish", + "add-schema-string-encoding", + "add-sql-pglite", + "add-standard-jsdoc-rule", + "add-stream-broadcastn", + "add-unstable-encoding-export", + "add-values-unprepared", + "afraid-cobras-like", + "ai-openai-config-field-leak", + "ai-openai-file-nullable-fields", + "anthropic-4-6-structured-output", + "anthropic-open-model-enum", + "apply-httpapi-endpoint-client-transform", + "asyncresult-exhaustive", + "atom-stream-error-type", + "beige-paths-sort", + "better-apples-nail", + "better-rocks-arrive", + "better-shrimps-follow", + "big-pans-look", + "blue-dingos-greet", + "blue-ligers-cheat", + "blue-onions-smile", + "blue-ravens-type", + "blue-trams-kiss", + "bold-chairs-yawn", + "bold-planets-shout", + "breezy-meals-see", + "bright-bugs-bow", + "bright-canyons-clean", + "bright-dogs-fail", + "bright-flags-stand", + "bright-laws-teach", + "bright-lemons-dance", + "bright-planes-smash", + "bright-rats-attend", + "bright-toes-rush", + "bumpy-boxes-teach", + "busy-lions-sneeze", + "busy-maps-attend", + "calm-buckets-own", + "calm-carrots-march", + "calm-cars-rest", + "calm-masks-count", + "calm-panthers-nail", + "calm-ravens-reflect", + "calm-seas-smile", + "calm-squids-hug", + "calm-tracers-sample", + "chatty-poets-type", + "chilled-mice-wash", + "chilly-pumas-rule", + "chubby-buckets-feel", + "chubby-parents-flow", + "chubby-planets-fall", + "clean-balloons-tan", + "clean-bulldogs-care", + "clean-dryers-sneeze", + "clean-geese-work", + "clean-goats-wave", + "clean-needles-shake", + "clean-tires-guess", + "clear-hairs-pump", + "clear-spies-boil", + "cli-config-built-ins", + "cli-help-choices", + "cli-wizard-mode", + "cold-knives-lie", + "cold-rooms-show", + "cold-sloths-wave", + "compact-json-schema-enum", + "config-withdefault-eager", + "consolidate-encoding", + "consolidate-sql-error", + "crisp-seas-warn", + "cron-single-value-step", + "cron-testclock-infinity", + "cuddly-rooms-bet", + "curly-poems-talk", + "curly-spies-relax", + "curvy-apples-float", + "curvy-birds-float", + "custom-http-security-openapi-generator", + "cute-heads-thank", + "cyan-loops-grow", + "cyan-radios-switch", + "cyan-shirts-grin", + "deep-rivers-spend", + "dirty-lamps-trade", + "dirty-laws-wear", + "duration-temporal-object-input", + "eager-coats-cheat", + "early-birds-dream", + "early-donuts-argue", + "early-peaches-check", + "eff-691-default-logger-ordering", + "eff-693-rpcgroup-handler-deps", + "eff-694-cli-completions-module", + "eff-695-layer-mock-dual-api", + "eff-697-rpcserialization-json-array-decode", + "eff-698-rpcserialization-unreachable-branch", + "eff-700-httpapi-middleware-errors", + "eff-701-httpapierror-respondable", + "eff-704-stream-merge-predicate", + "eff-705-layer-tap-apis", + "eff-706-servicemap-mutate", + "eff-716-response-id-tracker-map", + "eff-717-openai-socket-cancel", + "eff-718-embedding-model-surface", + "eff-725-fix-catch-jsdoc", + "eff-726-model-dimensions", + "eff-727-cli-help-alignment", + "eff-730-language-model-incremental-fallback", + "eff-736-cached-with-ttl", + "eff-738-cron-prev", + "eff-739-openai-function-call-done", + "eff-740-missing-summary-parts", + "eff-742-http-client-request-web", + "eff-744-sqlite-migrator-lock", + "eff-746-fixed-iteration-catchup", + "eff-747-unify-effect", + "eff-754-url-builder-any", + "eff-755-references-core", + "eff-769-select-text-highlight", + "eff-774-mutable-list-append-all-empty-array", + "eff-777-schema-make-effect", + "eff-778-http-middleware-path-logger", + "eff-779-keyvaluestore-layer-sql", + "eff-780-layer-unify", + "eff-781-fix-stream-toqueue-types", + "eff-782-httpapi-status-literals", + "eff-783-atom-http-api-errors", + "eff-819-cluster-workflow-shard-groups", + "eff-849-transpose-option", + "eight-turkeys-own", + "eighty-lies-deny", + "eighty-poets-draw", + "eighty-swans-scream", + "eighty-teeth-sniff", + "eleven-apes-share", + "eleven-numbers-bake", + "empty-env-values-missing", + "empty-gifts-beg", + "empty-http-rpc-client", + "eventlog-unencrypted", + "every-olives-burn", + "expand-schema-filter-output", + "export-schema-encode-keys-interface", + "extract-semaphore-latch", + "fair-bees-relax", + "fair-birds-limit", + "fair-buttons-share", + "fair-cooks-stop", + "fair-cups-train", + "fair-dryers-speak", + "fair-forks-shake", + "fair-jobs-like", + "fair-pandas-prove", + "fair-pants-float", + "fair-poems-visit", + "famous-wolves-lead", + "fancy-glasses-grow", + "fast-graph-path-queues", + "fast-times-camp", + "few-birds-matter", + "few-cougars-dig", + "few-foxes-grin", + "few-mirrors-pull", + "few-socks-poke", + "fiber-runtime-start-metrics", + "fiery-jokes-care", + "fiery-mammals-call", + "fine-walls-decide", + "first-success-of", + "five-parents-relax", + "five-worms-rhyme", + "fix-1332", + "fix-1917", + "fix-1927", + "fix-1940", + "fix-1947", + "fix-2002", + "fix-2012", + "fix-2015", + "fix-2260", + "fix-2268", + "fix-2271", + "fix-2384", + "fix-2414", + "fix-2419", + "fix-2497", + "fix-2499", + "fix-6464", + "fix-ai-empty-params-structured-output", + "fix-ai-text-toolkit-typing", + "fix-anthropic-caller-toolid", + "fix-anthropic-memory-tool-requires-handler", + "fix-anthropic-memory-tool", + "fix-atom-kvs-async-write", + "fix-catch-orelse-error-erasure", + "fix-class-constructor-defaults", + "fix-cli-missing-flag-values", + "fix-cli-mixed-global-flag-context", + "fix-cli-subcommands-requirements", + "fix-cluster-entity-context-bleed", + "fix-config-array-default", + "fix-config-withDefault", + "fix-config-withdefault-filter", + "fix-cron-next-missing-day-overflow", + "fix-cron-parser-semantics", + "fix-datetime-gmt", + "fix-devtools-flush-on-teardown", + "fix-durable-race-replay", + "fix-duration-symmetric-rounding", + "fix-entity-manager-defect-replay", + "fix-entity-proxy-rpc-handler-context", + "fix-entity-proxy-server-path-params", + "fix-from-json-string-identifier", + "fix-from-readable-stream-cancel-defect", + "fix-graph-allocator-equality", + "fix-graph-bellman-ford-self-cycle", + "fix-graph-curried-getters", + "fix-graph-edge-transforms", + "fix-graph-finite-edge-weights", + "fix-graph-mutable-hash", + "fix-graph-mutable-topo", + "fix-graph-topo-types", + "fix-graph-undirected-traversal", + "fix-graph-walker-repeatability", + "fix-graphviz-dot-escaping", + "fix-has-interrupts-only-empty", + "fix-hashmap-bit31-ordering", + "fix-headers-proto-enumerability", + "fix-http-incoming-message-parse-options", + "fix-http-tracer-response-cause", + "fix-httpapi-authorization-decoding", + "fix-httpapi-client-error-content-type", + "fix-httpapi-endpoint-error-inference", + "fix-httpapi-malformed-json-400", + "fix-httpapi-runtime-shape", + "fix-httpapi-schema-types", + "fix-httpapi-security-middleware-cache", + "fix-is-json-dag", + "fix-json-schema-anyof-oneof-siblings", + "fix-json-schema-import-json", + "fix-keepalive-blocked-timers", + "fix-mcp-param-name-resolution", + "fix-mermaid-escape-special-chars", + "fix-mutable-list-filter-length", + "fix-number-remainder-scientific-notation", + "fix-object-keyword-json-schema", + "fix-one-shot-iterables", + "fix-openai-mcp-tool-names", + "fix-openapi-generator-form-urlencoded", + "fix-openapi-generator-swagger2openapi", + "fix-openapi-preserve-multiple-response-content-types", + "fix-openrouter-sparse-array", + "fix-otel-logger-clock-skew", + "fix-otel-logger-severity-number", + "fix-persisted-queue-attempt-accounting", + "fix-queue-collect-duplication", + "fix-random-string-seeds", + "fix-ratelimiter-tokenbucket-redis-ttl", + "fix-redis-persisted-queue", + "fix-remainder-scientific-notation", + "fix-request-resolver-pending-batches-leak", + "fix-retry-transient-autocomplete", + "fix-rpc-http-requestids-finalizer", + "fix-rpc-json-id-edges", + "fix-rpc-unknown-tag-isolation", + "fix-schedule-fixed-double-exec", + "fix-schedule-reduce-sync-state", + "fix-schema-arbitrary-exclusive-bounds", + "fix-schema-bracket-prototype-pollution", + "fix-schema-defect-message", + "fix-schema-encode-keys-property-keys", + "fix-schema-encodekeys-class", + "fix-schema-encodekeys-struct", + "fix-schema-encoding-checks", + "fix-schema-identifier-expected-message", + "fix-schema-is-json-records", + "fix-schema-is-uuid", + "fix-schema-json-tuple-allof", + "fix-schema-option-non-schema-failures", + "fix-schema-parser-checks", + "fix-schema-tuple-post-rest-indexing", + "fix-schema-union-dispatch-order", + "fix-searchparam-initial-decode", + "fix-serializable-wire-transfer", + "fix-sql-persisted-queue-lock-refresh", + "fix-stream-grouped-within-flush", + "fix-stream-run-for-each-while", + "fix-stream-scan-effect", + "fix-stream-scoped-scope", + "fix-string-case-digits", + "fix-strip-approval-artifacts-multi-round", + "fix-struct-utility-types-simplify", + "fix-structural-proto-equality", + "fix-structwithrest-index-signatures", + "fix-tagged-union-class-sentinels", + "fix-tagged-union-match-unify", + "fix-to-tagged-union-isanyof-custom-tags", + "fix-tool-provider-defined-clone", + "fix-tuple-with-rest-post-rest-index-drift", + "fix-tuple-with-rest-post-rest-validation", + "fix-types-voidifempty", + "fix-vitest-record-schema-arbitrary", + "fix-void-response-encoding", + "fix-workflow-defect-reply-serialization", + "fix-workflow-entity-client-collision", + "fix-workflow-proxy-rpc-handler-context", + "flat-chicken-remain", + "floppy-cows-spend", + "floppy-items-admire", + "floppy-pigs-kiss", + "floppy-rats-leave", + "floyd-warshall-null-edge-data", + "fluffy-meals-matter", + "fluffy-pumas-push", + "forked-memo-maps", + "forty-hounds-cheer", + "forty-otters-cry", + "forty-rings-film", + "forty-signs-stay", + "forty-swans-divide", + "forty-trees-pay", + "four-papayas-bow", + "four-points-repeat", + "fresh-cats-smash", + "fresh-cycles-wait", + "fresh-emus-cheat", + "fresh-monkeys-smoke", + "frozen-intrinsics-stack-trace-limit", + "fruity-houses-learn", + "full-adults-double", + "funny-crabs-hang", + "funny-forks-move", + "fuzzy-camels-hunt", + "fuzzy-cats-kill", + "fuzzy-crews-fold", + "fuzzy-dodos-help", + "fuzzy-lions-perform", + "fuzzy-pandas-smile", + "fuzzy-planets-sneeze", + "fuzzy-stamps-care", + "giant-jeans-float", + "gold-meteors-move", + "gold-readers-hug", + "gold-rings-start", + "good-tools-work", + "good-trees-pull", + "graph-acyclic-parallel-undirected-edges", + "graph-algorithm-fixes", + "graph-finalized-mutation-handle", + "graph-guard-predicates", + "graph-sync-mutation-callbacks", + "graph-undirected-edge-equality", + "graph-walker-iterator-receiver", + "great-trains-mate", + "great-trams-report", + "green-beds-unref", + "green-chips-wash", + "green-moons-smile", + "green-pugs-play", + "green-rings-prove", + "happy-mirrors-dream", + "harden-httpapi-documentation-html", + "heavy-loops-cut", + "heavy-trams-fix", + "hip-friends-kiss", + "hip-socks-travel", + "honest-pens-thank", + "honest-rivers-notice", + "hot-taxis-fry", + "hot-teeth-clean", + "httpapi-endpoint-relax-constraints", + "httpapi-schema-service-types", + "huge-moons-rhyme", + "humble-pigs-dig", + "hungry-kings-look", + "icy-flies-cross", + "itchy-radios-poke", + "itchy-results-bet", + "itchy-shrimps-deny", + "itchy-toes-promise", + "k8s-last-transition-null", + "keep-httpapi-composition-immutable", + "khaki-cats-learn", + "khaki-melons-appear", + "kind-hounds-float", + "kind-windows-fall", + "late-hotels-rule", + "late-lamps-care", + "late-rivers-applaud", + "layer-map-dynamic-idle-ttl", + "lazy-queens-rush", + "lazy-recursive-forward-refs", + "lazy-timers-exist", + "legal-pants-drop", + "lemon-taxis-sin", + "light-kids-sneeze", + "little-dryers-allow", + "long-cameras-think", + "lovely-cobras-change", + "lovely-frogs-rescue", + "lucky-buttons-jump", + "lucky-phones-listen", + "lucky-worms-type", + "major-chairs-design", + "many-badgers-obey", + "mean-dingos-share", + "mean-trains-smash", + "metal-nails-sneeze", + "metal-parts-yell", + "mighty-games-matter", + "modern-carrots-see", + "modern-uuid-guid-filter", + "multipart-onDone-clobbers-error", + "nasty-geese-grow", + "neat-goats-wave", + "neat-kings-chew", + "neat-lions-rest", + "neat-snails-wash", + "neat-taxis-notice", + "neat-windows-buy", + "new-dogs-swim", + "new-toes-stop", + "ninety-geese-exist", + "normalize-httpapi-payload-media-types", + "o8drprcu-sqlite-node-node-sqlite", + "odd-boats-think", + "odd-bulldogs-sleep", + "odd-fans-glow", + "odd-forks-talk", + "odd-laws-draw", + "odd-owls-smoke", + "odd-socks-boil", + "odd-suns-dance", + "old-brooms-cry", + "old-facts-stand", + "old-mirrors-float", + "olive-poems-visit", + "opaque-graph-interface", + "open-hotels-remain", + "openai-compat-empty-assistant-content", + "openai-compat-nullable-tool-name", + "openai-compat-reasoning", + "openapi-generator-sse-constraint-decoder", + "openrouter-input-audio", + "opentelemetry-render-causes", + "optimize-httpapi-handler-types", + "otel-resource-env-precedence", + "perfect-buckets-tickle", + "petite-months-allow", + "platform-crypto-service", + "platform-node-shared-barrel", + "plenty-moons-pull", + "polite-brooms-tickle", + "polite-pigs-speak", + "polite-tables-kneel", + "port-react-hydration", + "pretty-moments-clap", + "public-deer-ring", + "public-jeans-stop", + "pubsub-publish-false", + "puny-pens-clap", + "purple-bars-prove", + "purple-schools-float", + "purple-turtles-draw", + "quick-dragons-fix", + "quick-falcons-travel", + "quick-geese-relax", + "quick-lamps-dig", + "quick-lizards-fall", + "quick-trees-join", + "quiet-carpets-grin", + "quiet-files-hunt", + "quiet-lamps-jam", + "quiet-radios-wave", + "quiet-redis-scripts", + "quiet-tigers-yell", + "quiet-turtles-smile", + "random-choice", + "ready-olives-divide", + "real-trains-ring", + "red-pigs-repair", + "redacted-representation-options", + "refactor-cli-global-flags", + "refactor-config-provider", + "refactor-representation-references", + "remove-effect-transactionwith", + "remove-http-span-counter", + "remove-nullor", + "remove-openapi-fromapi-options", + "remove-schedule-apis", + "remove-schedule-either", + "remove-schedule-elapsed", + "remove-schedule-taps", + "remove-schema-stringtree-keep-declarations", + "remove-types-mergerecord", + "remove-unused-utils-apis", + "rename-rebuild-out", + "restore-schema-parse-options", + "retry-redis-script-load", + "reuse-httpapi-response-schemas", + "rich-dots-push", + "rich-hoops-nail", + "rich-sloths-draw", + "ripe-lies-battle", + "rpc-client-http-early-close", + "rpc-middleware-provides-fix", + "scalar-custom-fetch", + "schema-as-class", + "schema-asserts-signature", + "schema-clean-up-additionalProperties", + "schema-codec-narrowing", + "schema-datetime-utc-from-string", + "schema-decoding-defaults-services", + "schema-defaults-issue-channel", + "schema-dollar-prefix", + "schema-lazy-bottom", + "schema-missing-self-generic", + "schema-ordered-arbitrary-constraints", + "schema-parser-adapter-errors", + "schema-refactor-toCodecJson", + "schema-remove-annotate-in", + "schema-rename-makeUnsafe-to-make", + "schema-rename-parser-makeUnsafe", + "schema-result-combinators", + "schema-struct-simplify", + "semantic-matching", + "seven-mugs-marry", + "shaggy-birds-stay", + "shaggy-cities-push", + "shaggy-numbers-accept", + "shaky-beans-throw", + "sharp-emus-applaud", + "sharp-goats-wink", + "sharp-pandas-care", + "sharp-peas-march", + "sharp-rules-draw", + "sharp-singers-sort", + "shiny-trains-hug", + "short-cows-relate", + "short-foxes-admire", + "short-stamps-throw", + "shy-cycles-flow", + "shy-geckos-sniff", + "silent-geckos-matter", + "silent-needles-design", + "silent-plants-matter", + "silent-spoons-stare", + "silly-loops-tickle", + "silver-bulk-indexeddb", + "silver-emus-smoke", + "silver-kings-poke", + "silver-snails-sqlite", + "silver-wings-watch", + "six-cups-taste", + "six-pumas-take", + "sixty-mails-shout", + "sixty-socks-yell", + "slick-signs-wish", + "slick-toes-rush", + "slimy-planets-divide", + "slimy-turtles-juggle", + "slow-beans-battle", + "slow-berries-enjoy", + "small-bugs-hunt", + "small-crabs-care", + "small-pets-sit", + "smart-ducks-jump", + "smart-pillows-buy", + "smart-timers-fly", + "smart-tips-sort", + "social-hoops-knock", + "social-pumas-prove", + "soft-comics-wink", + "soft-delete-sqlmodel", + "soft-seals-allow", + "solid-cougars-attack", + "solid-doors-ring", + "solid-items-tease", + "solid-towns-smoke", + "sour-canyons-rescue", + "sparkly-bears-act", + "sparkly-coins-sit", + "spotty-comics-fry", + "sql-migrator-mjs-mts", + "sqlite-bun-prepare-error-channel", + "sqlite-do-durable-object-transactions", + "stale-dots-tell", + "stale-graph-traversal-skips", + "stale-laws-do", + "stale-snakes-know", + "strict-areas-end", + "strict-buckets-hug", + "strip-resolved-approvals", + "strong-balloons-tickle", + "strong-bees-queue", + "strong-insects-film", + "struct-record", + "sunny-ads-hang", + "sunny-bikes-sleep", + "sunny-rooms-invent", + "sweet-donuts-bet", + "sweet-hotels-give", + "sweet-schedules-matter", + "sweet-views-learn", + "swift-spiders-unpack", + "swift-symbols-stand", + "tagged-error-class-optional-empty-props", + "tall-hairs-return", + "tall-mails-listen", + "tall-queens-cheer", + "tall-wombats-wave", + "tangy-colts-lose", + "tasty-comics-send", + "tasty-moments-post", + "ten-kings-fry", + "thick-pandas-wait", + "thin-ducks-wonder", + "thirty-ducks-go", + "thirty-pans-love", + "three-corners-sort", + "three-ravens-jam", + "three-tomatoes-wave", + "tidy-foxes-own", + "tidy-icons-glow", + "tidy-int32-annotations", + "tidy-pandas-smile", + "tidy-stacks-encode", + "tidy-stars-drive", + "tiny-buckets-wave", + "tiny-lilies-flash", + "tiny-rabbits-smile", + "to-codec-json-schema", + "tocodecjson-return-json-type", + "tool-get-json-schema-tests", + "true-actors-battle", + "try-promise-catch-defect", + "twelve-dragons-move", + "twenty-buttons-cheer", + "twenty-facts-laugh", + "two-roses-double", + "unify-error-defect-stack-options", + "update-schema-arbitrary-report", + "upset-colts-stick", + "use-url-can-parse", + "validate-httpapi-handler-registration", + "validate-openapi-global-conflicts", + "vast-bananas-send", + "vast-deserts-travel", + "violet-peaches-feel", + "violet-tips-open", + "vitest-layer-top-level-options", + "wacky-grapes-poke", + "wacky-rice-add", + "warm-dolls-brake", + "warm-friends-tie", + "warm-snails-shop", + "wet-news-invent", + "wild-readers-clean", + "wild-suns-bearer-space", + "wise-ants-wave", + "wise-flags-shift", + "wise-oranges-stay", + "witty-lobsters-share", + "yellow-adults-study", + "yellow-clocks-dance", + "yellow-dingos-jump", + "young-doors-change" + ] +} diff --git a/repos/effect/.changeset/pretty-moments-clap.md b/repos/effect/.changeset/pretty-moments-clap.md new file mode 100644 index 0000000..0dd186e --- /dev/null +++ b/repos/effect/.changeset/pretty-moments-clap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure handler errors don't cause httpapi security middleware to fallback diff --git a/repos/effect/.changeset/public-deer-ring.md b/repos/effect/.changeset/public-deer-ring.md new file mode 100644 index 0000000..251292b --- /dev/null +++ b/repos/effect/.changeset/public-deer-ring.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add WorkflowEngine interruptUnsafe diff --git a/repos/effect/.changeset/public-jeans-stop.md b/repos/effect/.changeset/public-jeans-stop.md new file mode 100644 index 0000000..719c440 --- /dev/null +++ b/repos/effect/.changeset/public-jeans-stop.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +reset openai websocket on error diff --git a/repos/effect/.changeset/pubsub-publish-false.md b/repos/effect/.changeset/pubsub-publish-false.md new file mode 100644 index 0000000..577022b --- /dev/null +++ b/repos/effect/.changeset/pubsub-publish-false.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +PubSub.publish and PubSub.publishAll now return false on shutdown instead of interrupting, matching Queue.offer semantics. diff --git a/repos/effect/.changeset/puny-pens-clap.md b/repos/effect/.changeset/puny-pens-clap.md new file mode 100644 index 0000000..c3087e0 --- /dev/null +++ b/repos/effect/.changeset/puny-pens-clap.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"effect": patch +--- + +retry incremental prompt on invalid request diff --git a/repos/effect/.changeset/purple-bars-prove.md b/repos/effect/.changeset/purple-bars-prove.md new file mode 100644 index 0000000..ca16c96 --- /dev/null +++ b/repos/effect/.changeset/purple-bars-prove.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +bump undici versions diff --git a/repos/effect/.changeset/purple-schools-float.md b/repos/effect/.changeset/purple-schools-float.md new file mode 100644 index 0000000..caac67d --- /dev/null +++ b/repos/effect/.changeset/purple-schools-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve `Prompt.file` to support incremental filtering while typing, including backspace and ctrl-u handling. diff --git a/repos/effect/.changeset/purple-turtles-draw.md b/repos/effect/.changeset/purple-turtles-draw.md new file mode 100644 index 0000000..95ad9fd --- /dev/null +++ b/repos/effect/.changeset/purple-turtles-draw.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +add .reverse() to idb select diff --git a/repos/effect/.changeset/quick-dragons-fix.md b/repos/effect/.changeset/quick-dragons-fix.md new file mode 100644 index 0000000..ded3d77 --- /dev/null +++ b/repos/effect/.changeset/quick-dragons-fix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose the optional `orElse` fallback parameter in `Effect.catchTags`. diff --git a/repos/effect/.changeset/quick-falcons-travel.md b/repos/effect/.changeset/quick-falcons-travel.md new file mode 100644 index 0000000..62cc670 --- /dev/null +++ b/repos/effect/.changeset/quick-falcons-travel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Argument.variadic(argument)` so it supports direct calls without options. diff --git a/repos/effect/.changeset/quick-geese-relax.md b/repos/effect/.changeset/quick-geese-relax.md new file mode 100644 index 0000000..d0d00ec --- /dev/null +++ b/repos/effect/.changeset/quick-geese-relax.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a regression in `PubSub.shutdown` so shutting down a pubsub interrupts suspended subscribers (including `takeAll`) by ensuring subscriptions are scoped under the pubsub shutdown scope. diff --git a/repos/effect/.changeset/quick-lamps-dig.md b/repos/effect/.changeset/quick-lamps-dig.md new file mode 100644 index 0000000..c239844 --- /dev/null +++ b/repos/effect/.changeset/quick-lamps-dig.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add HttpApiMiddleware.layerSchemaErrorTransform diff --git a/repos/effect/.changeset/quick-lizards-fall.md b/repos/effect/.changeset/quick-lizards-fall.md new file mode 100644 index 0000000..10d387f --- /dev/null +++ b/repos/effect/.changeset/quick-lizards-fall.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Add rebuild api to idb databases diff --git a/repos/effect/.changeset/quick-trees-join.md b/repos/effect/.changeset/quick-trees-join.md new file mode 100644 index 0000000..6237751 --- /dev/null +++ b/repos/effect/.changeset/quick-trees-join.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove all non-regional service usage diff --git a/repos/effect/.changeset/quiet-carpets-grin.md b/repos/effect/.changeset/quiet-carpets-grin.md new file mode 100644 index 0000000..e3a4e43 --- /dev/null +++ b/repos/effect/.changeset/quiet-carpets-grin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add \`Ctrl-A\` and \`Ctrl-E\` key handling for editable CLI text prompts to move the cursor to the beginning or end of the current input line. diff --git a/repos/effect/.changeset/quiet-crons-report.md b/repos/effect/.changeset/quiet-crons-report.md new file mode 100644 index 0000000..d29a3bd --- /dev/null +++ b/repos/effect/.changeset/quiet-crons-report.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the diagnostic for cron step values above a field's maximum. diff --git a/repos/effect/.changeset/quiet-fibers-settle.md b/repos/effect/.changeset/quiet-fibers-settle.md new file mode 100644 index 0000000..81ca36e --- /dev/null +++ b/repos/effect/.changeset/quiet-fibers-settle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix fiber self-interuption from inside a running operation diff --git a/repos/effect/.changeset/quiet-files-hunt.md b/repos/effect/.changeset/quiet-files-hunt.md new file mode 100644 index 0000000..d534ce6 --- /dev/null +++ b/repos/effect/.changeset/quiet-files-hunt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +wrap httpapi request context with HttpRouter.Request diff --git a/repos/effect/.changeset/quiet-lamps-jam.md b/repos/effect/.changeset/quiet-lamps-jam.md new file mode 100644 index 0000000..139ce84 --- /dev/null +++ b/repos/effect/.changeset/quiet-lamps-jam.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add OTEL environment variable configuration for unstable OTLP observability. diff --git a/repos/effect/.changeset/quiet-radios-wave.md b/repos/effect/.changeset/quiet-radios-wave.md new file mode 100644 index 0000000..fdbce5a --- /dev/null +++ b/repos/effect/.changeset/quiet-radios-wave.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Guard transaction connection acquisition in `PgClient.fromPool` so acquire failures stay in the `SqlError` channel. diff --git a/repos/effect/.changeset/quiet-redis-scripts.md b/repos/effect/.changeset/quiet-redis-scripts.md new file mode 100644 index 0000000..c9d068c --- /dev/null +++ b/repos/effect/.changeset/quiet-redis-scripts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retry Redis scripts after `NOSCRIPT` and declare the token bucket refill key diff --git a/repos/effect/.changeset/quiet-tigers-yell.md b/repos/effect/.changeset/quiet-tigers-yell.md new file mode 100644 index 0000000..3c567ce --- /dev/null +++ b/repos/effect/.changeset/quiet-tigers-yell.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`. diff --git a/repos/effect/.changeset/quiet-turtles-smile.md b/repos/effect/.changeset/quiet-turtles-smile.md new file mode 100644 index 0000000..ef13d5b --- /dev/null +++ b/repos/effect/.changeset/quiet-turtles-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Move `UrlParams.makeUrl` to `Url.make` and return `Url.UrlError` for URL construction failures. diff --git a/repos/effect/.changeset/random-choice.md b/repos/effect/.changeset/random-choice.md new file mode 100644 index 0000000..b0bdd94 --- /dev/null +++ b/repos/effect/.changeset/random-choice.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Random.choice for selecting a random element from an iterable. diff --git a/repos/effect/.changeset/ready-olives-divide.md b/repos/effect/.changeset/ready-olives-divide.md new file mode 100644 index 0000000..2502b4d --- /dev/null +++ b/repos/effect/.changeset/ready-olives-divide.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +handle openai ws error events diff --git a/repos/effect/.changeset/real-trains-ring.md b/repos/effect/.changeset/real-trains-ring.md new file mode 100644 index 0000000..5cd40a0 --- /dev/null +++ b/repos/effect/.changeset/real-trains-ring.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow `HttpApiTest.groups` to accept an optional `baseUrl` override while preserving the existing default of `"http://localhost:3000"`. diff --git a/repos/effect/.changeset/red-pigs-repair.md b/repos/effect/.changeset/red-pigs-repair.md new file mode 100644 index 0000000..005ae5f --- /dev/null +++ b/repos/effect/.changeset/red-pigs-repair.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.annotateLogsScoped` to apply log annotations for the current scope and automatically restore previous annotations when the scope closes. diff --git a/repos/effect/.changeset/redacted-representation-options.md b/repos/effect/.changeset/redacted-representation-options.md new file mode 100644 index 0000000..da25298 --- /dev/null +++ b/repos/effect/.changeset/redacted-representation-options.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve `Schema.Redacted` options when roundtripping through schema representations. +This keeps `label` validation and `disallowJsonEncode` behavior intact when +schemas are revived from a representation or emitted through code generation. diff --git a/repos/effect/.changeset/refactor-cli-global-flags.md b/repos/effect/.changeset/refactor-cli-global-flags.md new file mode 100644 index 0000000..ccfecb9 --- /dev/null +++ b/repos/effect/.changeset/refactor-cli-global-flags.md @@ -0,0 +1,27 @@ +--- +"effect": patch +--- + +Refactor CLI built-in options to use Effect services with `GlobalFlag` + +Built-in CLI flags (`--help`, `--version`, `--completions`, `--log-level`) are now implemented as Effect services using `Context.Reference`. This provides: + +- **Visibility**: Built-in flags now appear in help output's "GLOBAL FLAGS" section +- **Extensibility**: Users can register custom global flags via `GlobalFlag.add` +- **Override capability**: Built-in flag behavior can be replaced or disabled +- **Composability**: Flags compose via Effect's service system + +New `GlobalFlag` module exports: + +- `Action` and `Setting` types for different flag behaviors +- `Help`, `Version`, `Completions`, `LogLevel` references for built-in flags +- `add`, `remove`, `clear` functions for managing global flags + +Example: + +```typescript +const app = Command.make("myapp") +Command.run(app, { version: "1.0.0" }).pipe( + GlobalFlag.add(CustomFlag, customFlagValue) +) +``` diff --git a/repos/effect/.changeset/refactor-config-provider.md b/repos/effect/.changeset/refactor-config-provider.md new file mode 100644 index 0000000..5e9a549 --- /dev/null +++ b/repos/effect/.changeset/refactor-config-provider.md @@ -0,0 +1,23 @@ +--- +"effect": patch +--- + +Fix config path composition and directory-backed lookup behavior. + +`ConfigProvider.orElse` now keeps each side's own `nested` and `mapInput` +behavior. Applying `nested` or `mapInput` to a combined provider now applies the +same transformation to both sides. + +`ConfigProvider` path transformations now compose as a single path function. +This makes `nested` and `mapInput` behave consistently with normal function +composition. + +`Config.nested` now tracks the logical config path in `Config` itself instead of +wrapping the provider. This keeps lookup paths and schema error paths aligned. +The low-level `Config.make` constructor is no longer exported; use config +constructors and combinators, or implement custom lookup behavior with +`ConfigProvider.make`. + +`ConfigProvider.fromDir` now returns `undefined` when neither a file nor a +directory exists at the requested path, so `orElse` can fall back instead of +failing with `SourceError`. diff --git a/repos/effect/.changeset/refactor-representation-references.md b/repos/effect/.changeset/refactor-representation-references.md new file mode 100644 index 0000000..e72607f --- /dev/null +++ b/repos/effect/.changeset/refactor-representation-references.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +SchemaRepresentation: only create references for recursive/mutually recursive schemas and schemas with an `identifier` annotation, closes #1560. diff --git a/repos/effect/.changeset/remove-effect-transactionwith.md b/repos/effect/.changeset/remove-effect-transactionwith.md new file mode 100644 index 0000000..5f5c63e --- /dev/null +++ b/repos/effect/.changeset/remove-effect-transactionwith.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `Effect.transaction` to `Effect.tx` and `Effect.retryTransaction` to `Effect.txRetry`, remove `Effect.transactionWith` / `Effect.withTxState`, make nested `Effect.tx` calls compose into the active transaction, and make the public `Tx*` APIs establish atomic transactions without requiring `Transaction` in common usage. diff --git a/repos/effect/.changeset/remove-http-span-counter.md b/repos/effect/.changeset/remove-http-span-counter.md new file mode 100644 index 0000000..e0458e7 --- /dev/null +++ b/repos/effect/.changeset/remove-http-span-counter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the auto-incrementing suffix from HTTP server logger log span names. diff --git a/repos/effect/.changeset/remove-nullor.md b/repos/effect/.changeset/remove-nullor.md new file mode 100644 index 0000000..28617d3 --- /dev/null +++ b/repos/effect/.changeset/remove-nullor.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove unused `effect/NullOr` module. diff --git a/repos/effect/.changeset/remove-openapi-fromapi-options.md b/repos/effect/.changeset/remove-openapi-fromapi-options.md new file mode 100644 index 0000000..97be4cd --- /dev/null +++ b/repos/effect/.changeset/remove-openapi-fromapi-options.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Remove the `options` parameter from `OpenApi.fromApi`. + +The parameter only carried `additionalProperties`, but the function caches results in a `WeakMap` keyed solely on the `api` instance. Passing different options across calls for the same api was silently ignored, making the parameter order-dependent and effectively single-shot. No call sites were using it, so the signature is now simply `fromApi(api)`. diff --git a/repos/effect/.changeset/remove-schedule-apis.md b/repos/effect/.changeset/remove-schedule-apis.md new file mode 100644 index 0000000..79d641e --- /dev/null +++ b/repos/effect/.changeset/remove-schedule-apis.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove some Schedule APIs: `collectInputs`, `collectOutputs`, `collectWhile`, `delays`, `reduce`, `satisfiesErrorType`, `satisfiesInputType`, `satisfiesOutputType`, `satisfiesServicesType`, and `unfold`. diff --git a/repos/effect/.changeset/remove-schedule-either.md b/repos/effect/.changeset/remove-schedule-either.md new file mode 100644 index 0000000..ee0be86 --- /dev/null +++ b/repos/effect/.changeset/remove-schedule-either.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the Schedule.either APIs and add Schedule.min for fastest-duration schedule composition. diff --git a/repos/effect/.changeset/remove-schedule-elapsed.md b/repos/effect/.changeset/remove-schedule-elapsed.md new file mode 100644 index 0000000..1863f14 --- /dev/null +++ b/repos/effect/.changeset/remove-schedule-elapsed.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Schedule.elapsed`. diff --git a/repos/effect/.changeset/remove-schedule-taps.md b/repos/effect/.changeset/remove-schedule-taps.md new file mode 100644 index 0000000..18572cb --- /dev/null +++ b/repos/effect/.changeset/remove-schedule-taps.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Schedule.tapInput` and `Schedule.tapOutput`. Use `Schedule.tap` instead. diff --git a/repos/effect/.changeset/remove-schema-stringtree-keep-declarations.md b/repos/effect/.changeset/remove-schema-stringtree-keep-declarations.md new file mode 100644 index 0000000..f09c22a --- /dev/null +++ b/repos/effect/.changeset/remove-schema-stringtree-keep-declarations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the `keepDeclarations` option from `Schema.toCodecStringTree`. diff --git a/repos/effect/.changeset/remove-types-mergerecord.md b/repos/effect/.changeset/remove-types-mergerecord.md new file mode 100644 index 0000000..6533e1b --- /dev/null +++ b/repos/effect/.changeset/remove-types-mergerecord.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the `Types.MergeRecord` alias. Use `Types.MergeLeft` instead. diff --git a/repos/effect/.changeset/remove-unused-utils-apis.md b/repos/effect/.changeset/remove-unused-utils-apis.md new file mode 100644 index 0000000..986dd0e --- /dev/null +++ b/repos/effect/.changeset/remove-unused-utils-apis.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove unused APIs from the `Utils` module. diff --git a/repos/effect/.changeset/rename-rebuild-out.md b/repos/effect/.changeset/rename-rebuild-out.md new file mode 100644 index 0000000..106bcc1 --- /dev/null +++ b/repos/effect/.changeset/rename-rebuild-out.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: rename `"~rebuild.out"` to `"Rebuild"` diff --git a/repos/effect/.changeset/restore-schema-parse-options.md b/repos/effect/.changeset/restore-schema-parse-options.md new file mode 100644 index 0000000..7a24454 --- /dev/null +++ b/repos/effect/.changeset/restore-schema-parse-options.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Restore support for passing schema parse options when creating decode and encode helpers, closes #2174. diff --git a/repos/effect/.changeset/retry-redis-script-load.md b/repos/effect/.changeset/retry-redis-script-load.md new file mode 100644 index 0000000..c4017e4 --- /dev/null +++ b/repos/effect/.changeset/retry-redis-script-load.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Redis script evaluation so transient `SCRIPT LOAD` failures are retried instead of being cached indefinitely. diff --git a/repos/effect/.changeset/reuse-httpapi-response-schemas.md b/repos/effect/.changeset/reuse-httpapi-response-schemas.md new file mode 100644 index 0000000..1c7d4c0 --- /dev/null +++ b/repos/effect/.changeset/reuse-httpapi-response-schemas.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Reuse HttpApi response schemas. + +`HttpApiBuilder` looked up cached response schemas by their source AST but stored them by the transformed AST, so the cache normally missed. It now uses the source AST consistently. diff --git a/repos/effect/.changeset/rich-dots-push.md b/repos/effect/.changeset/rich-dots-push.md new file mode 100644 index 0000000..3424b61 --- /dev/null +++ b/repos/effect/.changeset/rich-dots-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix `isNullish()` type predicate diff --git a/repos/effect/.changeset/rich-hoops-nail.md b/repos/effect/.changeset/rich-hoops-nail.md new file mode 100644 index 0000000..540a3d6 --- /dev/null +++ b/repos/effect/.changeset/rich-hoops-nail.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename DurationInput to Duration.Input diff --git a/repos/effect/.changeset/rich-sloths-draw.md b/repos/effect/.changeset/rich-sloths-draw.md new file mode 100644 index 0000000..98a0a99 --- /dev/null +++ b/repos/effect/.changeset/rich-sloths-draw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure envelope payloads are correctly encoded for notify path diff --git a/repos/effect/.changeset/ripe-lies-battle.md b/repos/effect/.changeset/ripe-lies-battle.md new file mode 100644 index 0000000..3f6b046 --- /dev/null +++ b/repos/effect/.changeset/ripe-lies-battle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +EventLog Identity string encodes to base 64 diff --git a/repos/effect/.changeset/rpc-client-http-early-close.md b/repos/effect/.changeset/rpc-client-http-early-close.md new file mode 100644 index 0000000..ce2b353 --- /dev/null +++ b/repos/effect/.changeset/rpc-client-http-early-close.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fail RpcClient HTTP requests with a defect when the response stream closes before the request receives a terminal response. diff --git a/repos/effect/.changeset/rpc-middleware-provides-fix.md b/repos/effect/.changeset/rpc-middleware-provides-fix.md new file mode 100644 index 0000000..30a2656 --- /dev/null +++ b/repos/effect/.changeset/rpc-middleware-provides-fix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Rpc.ExtractProvides` to use middleware service ID instead of constructor type. diff --git a/repos/effect/.changeset/scalar-custom-fetch.md b/repos/effect/.changeset/scalar-custom-fetch.md new file mode 100644 index 0000000..6a03fa1 --- /dev/null +++ b/repos/effect/.changeset/scalar-custom-fetch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add support for configuring Scalar API reference pages with a custom fetch implementation. diff --git a/repos/effect/.changeset/schema-as-class.md b/repos/effect/.changeset/schema-as-class.md new file mode 100644 index 0000000..7957231 --- /dev/null +++ b/repos/effect/.changeset/schema-as-class.md @@ -0,0 +1,17 @@ +--- +"effect": patch +--- + +Schema: add `asClass` API to turn any schema into a class with static method support. + +**Example** + +```ts +import { Schema } from "effect" + +class MyString extends Schema.asClass(Schema.String) { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) +} + +MyString.decodeUnknownSync("a") // "a" +``` diff --git a/repos/effect/.changeset/schema-asserts-signature.md b/repos/effect/.changeset/schema-asserts-signature.md new file mode 100644 index 0000000..173f658 --- /dev/null +++ b/repos/effect/.changeset/schema-asserts-signature.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Change `Schema.asserts` and `SchemaParser.asserts` to assert a value directly with `asserts(schema, input)` and remove `Schema.Codec.ToAsserts`. diff --git a/repos/effect/.changeset/schema-clean-up-additionalProperties.md b/repos/effect/.changeset/schema-clean-up-additionalProperties.md new file mode 100644 index 0000000..06d3a85 --- /dev/null +++ b/repos/effect/.changeset/schema-clean-up-additionalProperties.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Schema: `toJsonSchemaDocument` now emits JSON Schema `false` for unannotated +`Never` index signatures (including `additionalProperties`) instead of `{ not: {} }`. +Annotated `Never` still emits a schema object so metadata like `description` is preserved. diff --git a/repos/effect/.changeset/schema-codec-narrowing.md b/repos/effect/.changeset/schema-codec-narrowing.md new file mode 100644 index 0000000..c479898 --- /dev/null +++ b/repos/effect/.changeset/schema-codec-narrowing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `Schema.Decoder` and `Schema.Encoder`, and accept simpler schema types in APIs that only decode, only encode, or only need the basic schema shape, closes #2536 diff --git a/repos/effect/.changeset/schema-datetime-utc-from-string.md b/repos/effect/.changeset/schema-datetime-utc-from-string.md new file mode 100644 index 0000000..bd52ade --- /dev/null +++ b/repos/effect/.changeset/schema-datetime-utc-from-string.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: extract shared `dateTimeUtcFromString` transformation for `DateTimeUtc` and `DateTimeUtcFromString`. diff --git a/repos/effect/.changeset/schema-decoding-defaults-services.md b/repos/effect/.changeset/schema-decoding-defaults-services.md new file mode 100644 index 0000000..a3bd9dc --- /dev/null +++ b/repos/effect/.changeset/schema-decoding-defaults-services.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Allow Schema decoding defaults to require Effect services. + +The `Effect` passed to `Schema.withDecodingDefault`, `Schema.withDecodingDefaultKey`, `Schema.withDecodingDefaultType`, and `Schema.withDecodingDefaultTypeKey` now accepts a context `R` in its third type parameter. The required services are propagated into the resulting schema's `DecodingServices`. `SchemaGetter.withDefault` is widened in the same way. diff --git a/repos/effect/.changeset/schema-defaults-issue-channel.md b/repos/effect/.changeset/schema-defaults-issue-channel.md new file mode 100644 index 0000000..07a351b --- /dev/null +++ b/repos/effect/.changeset/schema-defaults-issue-channel.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Allow Schema constructor and decoding defaults to fail with `SchemaError`. + +The `Effect` passed to `Schema.withConstructorDefault`, `Schema.withDecodingDefault`, `Schema.withDecodingDefaultKey`, `Schema.withDecodingDefaultType`, and `Schema.withDecodingDefaultTypeKey` now accepts `SchemaError` in its error channel. When a default fails, the parser unwraps the underlying `SchemaIssue.Issue` and propagates it as a parse failure with the surrounding path attached. This makes it easy to use another schema's `makeEffect` / `decode*` as the default value. diff --git a/repos/effect/.changeset/schema-dollar-prefix.md b/repos/effect/.changeset/schema-dollar-prefix.md new file mode 100644 index 0000000..947fc16 --- /dev/null +++ b/repos/effect/.changeset/schema-dollar-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: rename `$` suffix to `$` prefix for type-level identifiers that conflict with built-in names (`Array$` → `$Array`, `Record$` → `$Record`, `ReadonlyMap$` → `$ReadonlyMap`, `ReadonlySet$` → `$ReadonlySet`). diff --git a/repos/effect/.changeset/schema-lazy-bottom.md b/repos/effect/.changeset/schema-lazy-bottom.md new file mode 100644 index 0000000..81ece19 --- /dev/null +++ b/repos/effect/.changeset/schema-lazy-bottom.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Improve Schema type-level performance by lazily computing schema views, +specializing common struct projections, and using lighter schema constraints at +API boundaries that do not need the full schema protocol. + +This also adds the Schema type-performance benchmark suite, introduces +`Schema.toCodecArrayFromSingle`, preserves canonical StringTree array codecs, +renames the arbitrary-generation annotation constraint for clarity, and updates +affected codec, parser, channel, SQL, HTTP API, persistence, RPC, AI, OpenAPI, +and workflow typings to match the refined Schema surface. diff --git a/repos/effect/.changeset/schema-missing-self-generic.md b/repos/effect/.changeset/schema-missing-self-generic.md new file mode 100644 index 0000000..3e87785 --- /dev/null +++ b/repos/effect/.changeset/schema-missing-self-generic.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `MissingSelfGeneric` compile-time error for `Class`, `TaggedClass`, `ErrorClass`, and `TaggedErrorClass` when the `Self` type parameter is omitted. diff --git a/repos/effect/.changeset/schema-ordered-arbitrary-constraints.md b/repos/effect/.changeset/schema-ordered-arbitrary-constraints.md new file mode 100644 index 0000000..2c9617d --- /dev/null +++ b/repos/effect/.changeset/schema-ordered-arbitrary-constraints.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Use generic ordered constraints for schema arbitrary derivation. + +Range checks such as `isGreaterThan`, `isLessThan`, and `isBetween` now populate `ctx.constraints.ordered` +instead of type-specific range fields on `number`, `date`, or `bigint` constraints. Custom `toArbitrary` +annotations that read range constraints should migrate to `ctx.constraints.ordered`. + +This also fixes BigDecimal arbitrary generation by adapting decimal bounds to the generated scale, avoiding +invalid fast-check bigint ranges for narrow decimal intervals. diff --git a/repos/effect/.changeset/schema-parser-adapter-errors.md b/repos/effect/.changeset/schema-parser-adapter-errors.md new file mode 100644 index 0000000..17aeb89 --- /dev/null +++ b/repos/effect/.changeset/schema-parser-adapter-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Align Schema adapter failures: `Schema` result, promise, and sync adapters now surface `SchemaError`, while `SchemaParser` result, promise, and sync adapters expose `SchemaIssue.Issue`. Mark `SchemaParser` option adapters as internal because their error details are discarded. diff --git a/repos/effect/.changeset/schema-refactor-toCodecJson.md b/repos/effect/.changeset/schema-refactor-toCodecJson.md new file mode 100644 index 0000000..3bd03e7 --- /dev/null +++ b/repos/effect/.changeset/schema-refactor-toCodecJson.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: improve `Schema.Unknown` / `Schema.ObjectKeyword` handling in `toCodecJson` and `toCodecStringTree` diff --git a/repos/effect/.changeset/schema-remove-annotate-in.md b/repos/effect/.changeset/schema-remove-annotate-in.md new file mode 100644 index 0000000..93dd5bb --- /dev/null +++ b/repos/effect/.changeset/schema-remove-annotate-in.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: remove `"~annotate.in"` type from `Bottom` interface, inlining it where needed diff --git a/repos/effect/.changeset/schema-rename-makeUnsafe-to-make.md b/repos/effect/.changeset/schema-rename-makeUnsafe-to-make.md new file mode 100644 index 0000000..f2fa96b --- /dev/null +++ b/repos/effect/.changeset/schema-rename-makeUnsafe-to-make.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Rename `Schema.makeUnsafe` instance method back to `Schema.make` on all schemas and schema-backed classes. + +Also remove the `static readonly make` override from `ShardId` to avoid conflicting with the inherited schema `make` method. The module-level `ShardId.make(group, id)` function is still available. diff --git a/repos/effect/.changeset/schema-rename-parser-makeUnsafe.md b/repos/effect/.changeset/schema-rename-parser-makeUnsafe.md new file mode 100644 index 0000000..7573aa8 --- /dev/null +++ b/repos/effect/.changeset/schema-rename-parser-makeUnsafe.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `SchemaParser.makeUnsafe` to `SchemaParser.make`. diff --git a/repos/effect/.changeset/schema-result-combinators.md b/repos/effect/.changeset/schema-result-combinators.md new file mode 100644 index 0000000..d313cf0 --- /dev/null +++ b/repos/effect/.changeset/schema-result-combinators.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `decodeUnknownResult` / `decodeResult` and `encodeUnknownResult` / `encodeResult` helpers for synchronous `Result`-based parsing. diff --git a/repos/effect/.changeset/schema-struct-simplify.md b/repos/effect/.changeset/schema-struct-simplify.md new file mode 100644 index 0000000..c86120a --- /dev/null +++ b/repos/effect/.changeset/schema-struct-simplify.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: allow using `Struct` type helpers directly, e.g. `Schema.Struct.Type` instead of `Schema.Schema.Type>`. diff --git a/repos/effect/.changeset/semantic-matching.md b/repos/effect/.changeset/semantic-matching.md new file mode 100644 index 0000000..edf23ce --- /dev/null +++ b/repos/effect/.changeset/semantic-matching.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Use semantic matching for TemplateLiteral parsing and index signature keys + +Replace regex-based TemplateLiteral parsing with backtracking segmentation over +template literal parts, applying part checks during matching. + +Use schema membership when selecting Record index signature keys, including +checked string, number, symbol, and TemplateLiteral parameters. Tighten valid +index signature parameters on both type and encoded sides, and preserve key +parameter semantics in codec transformations. diff --git a/repos/effect/.changeset/seven-mugs-marry.md b/repos/effect/.changeset/seven-mugs-marry.md new file mode 100644 index 0000000..fd3dd9d --- /dev/null +++ b/repos/effect/.changeset/seven-mugs-marry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix: update Service interface to use 'this: void' in 'of' method signatures diff --git a/repos/effect/.changeset/shaggy-birds-stay.md b/repos/effect/.changeset/shaggy-birds-stay.md new file mode 100644 index 0000000..5834231 --- /dev/null +++ b/repos/effect/.changeset/shaggy-birds-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow passing void for request constructors diff --git a/repos/effect/.changeset/shaggy-cities-push.md b/repos/effect/.changeset/shaggy-cities-push.md new file mode 100644 index 0000000..dfff5ea --- /dev/null +++ b/repos/effect/.changeset/shaggy-cities-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use NoInfer in Layer constructors to prevent type erasure diff --git a/repos/effect/.changeset/shaggy-numbers-accept.md b/repos/effect/.changeset/shaggy-numbers-accept.md new file mode 100644 index 0000000..df7340a --- /dev/null +++ b/repos/effect/.changeset/shaggy-numbers-accept.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the type of the schema parameter accepted by the `fileSchema` methods in the CLI to be `Schema.Decoder` diff --git a/repos/effect/.changeset/shaky-beans-throw.md b/repos/effect/.changeset/shaky-beans-throw.md new file mode 100644 index 0000000..e0fa124 --- /dev/null +++ b/repos/effect/.changeset/shaky-beans-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +accept UrlParams.Input in some UrlParams apis diff --git a/repos/effect/.changeset/sharp-emus-applaud.md b/repos/effect/.changeset/sharp-emus-applaud.md new file mode 100644 index 0000000..aa91f34 --- /dev/null +++ b/repos/effect/.changeset/sharp-emus-applaud.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Command.withShortDescription` and use short descriptions for CLI subcommand listings, with fallback to the full command description. diff --git a/repos/effect/.changeset/sharp-goats-wink.md b/repos/effect/.changeset/sharp-goats-wink.md new file mode 100644 index 0000000..19588ff --- /dev/null +++ b/repos/effect/.changeset/sharp-goats-wink.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow unstable CLI fallback prompts to be created dynamically from an `Effect`. diff --git a/repos/effect/.changeset/sharp-pandas-care.md b/repos/effect/.changeset/sharp-pandas-care.md new file mode 100644 index 0000000..21c1cb9 --- /dev/null +++ b/repos/effect/.changeset/sharp-pandas-care.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix TestClock adjustment when its layer is provided to programs run without an ambient Scope. diff --git a/repos/effect/.changeset/sharp-peas-march.md b/repos/effect/.changeset/sharp-peas-march.md new file mode 100644 index 0000000..7bce281 --- /dev/null +++ b/repos/effect/.changeset/sharp-peas-march.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix AI tool handler error typing so `LanguageModel.generateText` with a toolkit exposes wrapped `AiError` values rather than leaking raw `AiErrorReason` in the error channel. diff --git a/repos/effect/.changeset/sharp-rules-draw.md b/repos/effect/.changeset/sharp-rules-draw.md new file mode 100644 index 0000000..8246dde --- /dev/null +++ b/repos/effect/.changeset/sharp-rules-draw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure each sql client gets a unique transaction service diff --git a/repos/effect/.changeset/sharp-singers-sort.md b/repos/effect/.changeset/sharp-singers-sort.md new file mode 100644 index 0000000..75c4997 --- /dev/null +++ b/repos/effect/.changeset/sharp-singers-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix string messages and annotations being double-quoted by simple and logfmt loggers. diff --git a/repos/effect/.changeset/shiny-trains-hug.md b/repos/effect/.changeset/shiny-trains-hug.md new file mode 100644 index 0000000..4512084 --- /dev/null +++ b/repos/effect/.changeset/shiny-trains-hug.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add module-level helpers for `Semaphore`, `Latch`, and extracted `PartitionedSemaphore` operations. diff --git a/repos/effect/.changeset/short-cows-relate.md b/repos/effect/.changeset/short-cows-relate.md new file mode 100644 index 0000000..2eb2ae0 --- /dev/null +++ b/repos/effect/.changeset/short-cows-relate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Set `Schema.TaggedErrorClass` instance `name` to the tag value, matching `Data.TaggedError` behavior. diff --git a/repos/effect/.changeset/short-foxes-admire.md b/repos/effect/.changeset/short-foxes-admire.md new file mode 100644 index 0000000..2da4d3a --- /dev/null +++ b/repos/effect/.changeset/short-foxes-admire.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Atom.swr` to `effect/unstable/reactivity` for staleTime-gated stale-while-revalidate reads, optional mount and window-focus revalidation, and forceful manual refresh. diff --git a/repos/effect/.changeset/short-stamps-throw.md b/repos/effect/.changeset/short-stamps-throw.md new file mode 100644 index 0000000..1948e67 --- /dev/null +++ b/repos/effect/.changeset/short-stamps-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +do not use performance.timeOrigin and calculate origins lazily diff --git a/repos/effect/.changeset/shy-cycles-flow.md b/repos/effect/.changeset/shy-cycles-flow.md new file mode 100644 index 0000000..60188e0 --- /dev/null +++ b/repos/effect/.changeset/shy-cycles-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Channel.decodeText corrupting UTF-8 characters split across chunk boundaries. diff --git a/repos/effect/.changeset/shy-geckos-sniff.md b/repos/effect/.changeset/shy-geckos-sniff.md new file mode 100644 index 0000000..bf5f07d --- /dev/null +++ b/repos/effect/.changeset/shy-geckos-sniff.md @@ -0,0 +1,30 @@ +--- +"@effect/sql-sqlite-react-native": major +"@effect/openapi-generator": major +"@effect/platform-node-shared": major +"@effect/platform-browser": major +"@effect/sql-sqlite-node": major +"@effect/sql-sqlite-wasm": major +"@effect/sql-clickhouse": major +"@effect/sql-sqlite-bun": major +"@effect/opentelemetry": major +"@effect/platform-node": major +"@effect/sql-sqlite-do": major +"@effect/ai-anthropic": major +"@effect/ai-openai-compat": major +"@effect/ai-openrouter": major +"@effect/platform-bun": major +"@effect/atom-react": major +"@effect/atom-solid": major +"@effect/sql-libsql": major +"@effect/sql-mysql2": major +"@effect/ai-openai": major +"@effect/sql-mssql": major +"@effect/atom-vue": major +"effect": major +"@effect/sql-d1": major +"@effect/sql-pg": major +"@effect/vitest": major +--- + +v4 beta diff --git a/repos/effect/.changeset/silent-geckos-matter.md b/repos/effect/.changeset/silent-geckos-matter.md new file mode 100644 index 0000000..df5aadf --- /dev/null +++ b/repos/effect/.changeset/silent-geckos-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +compare transaction connections by reference diff --git a/repos/effect/.changeset/silent-needles-design.md b/repos/effect/.changeset/silent-needles-design.md new file mode 100644 index 0000000..ee84c0e --- /dev/null +++ b/repos/effect/.changeset/silent-needles-design.md @@ -0,0 +1,6 @@ +--- +"@effect/opentelemetry": patch +"effect": patch +--- + +Fix spans never having parent span diff --git a/repos/effect/.changeset/silent-plants-matter.md b/repos/effect/.changeset/silent-plants-matter.md new file mode 100644 index 0000000..e9609ea --- /dev/null +++ b/repos/effect/.changeset/silent-plants-matter.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-node-shared": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"effect": patch +--- + +improve http body consumption diff --git a/repos/effect/.changeset/silent-spoons-stare.md b/repos/effect/.changeset/silent-spoons-stare.md new file mode 100644 index 0000000..6d45025 --- /dev/null +++ b/repos/effect/.changeset/silent-spoons-stare.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +catch errors in pullIntoWritable diff --git a/repos/effect/.changeset/silly-loops-tickle.md b/repos/effect/.changeset/silly-loops-tickle.md new file mode 100644 index 0000000..a5493c8 --- /dev/null +++ b/repos/effect/.changeset/silly-loops-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +DateTime.distance now returns a Duration diff --git a/repos/effect/.changeset/silver-bulk-indexeddb.md b/repos/effect/.changeset/silver-bulk-indexeddb.md new file mode 100644 index 0000000..7137730 --- /dev/null +++ b/repos/effect/.changeset/silver-bulk-indexeddb.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix IndexedDB bulk writes so `insertAll` and `upsertAll` resume when used inside `withTransaction`. diff --git a/repos/effect/.changeset/silver-emus-smoke.md b/repos/effect/.changeset/silver-emus-smoke.md new file mode 100644 index 0000000..19f10e0 --- /dev/null +++ b/repos/effect/.changeset/silver-emus-smoke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove placeholder fallback behavior from CLI prompt inputs now that default values are prefilled. diff --git a/repos/effect/.changeset/silver-kings-poke.md b/repos/effect/.changeset/silver-kings-poke.md new file mode 100644 index 0000000..11d14a6 --- /dev/null +++ b/repos/effect/.changeset/silver-kings-poke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +simplify SubscriptionRef diff --git a/repos/effect/.changeset/silver-snails-sqlite.md b/repos/effect/.changeset/silver-snails-sqlite.md new file mode 100644 index 0000000..dc4ddd4 --- /dev/null +++ b/repos/effect/.changeset/silver-snails-sqlite.md @@ -0,0 +1,17 @@ +--- +"effect": patch +"@effect/sql-libsql": patch +"@effect/sql-mssql": patch +"@effect/sql-mysql2": patch +"@effect/sql-pg": patch +"@effect/sql-pglite": patch +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-do": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +--- + +Add `UniqueViolation` as a new SQL error reason. Supported unique constraint violations now classify as `UniqueViolation` instead of the broader `ConstraintError` reason. + +This covers PostgreSQL, PGlite, MySQL, MSSQL, and the shared SQLite classification used by the SQLite-family clients. `UniqueViolation.constraint` contains the best available constraint, index, or key identifier and falls back to exactly `"unknown"` when no reliable identifier is available. diff --git a/repos/effect/.changeset/silver-wings-watch.md b/repos/effect/.changeset/silver-wings-watch.md new file mode 100644 index 0000000..3a1d49e --- /dev/null +++ b/repos/effect/.changeset/silver-wings-watch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +require a option to make AtomRpc.query atoms serializatable diff --git a/repos/effect/.changeset/six-cups-taste.md b/repos/effect/.changeset/six-cups-taste.md new file mode 100644 index 0000000..71d570b --- /dev/null +++ b/repos/effect/.changeset/six-cups-taste.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add args to Stdio service diff --git a/repos/effect/.changeset/six-pumas-take.md b/repos/effect/.changeset/six-pumas-take.md new file mode 100644 index 0000000..a58d9a0 --- /dev/null +++ b/repos/effect/.changeset/six-pumas-take.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +add advanced graph set operations for deriving related graph structures + +- `Graph.complement` - complement over the existing node set, adding missing edges between distinct nodes +- `Graph.neighborhood` - induced subgraph containing nodes within a radius of a node +- `Graph.sum` - disjoint union of two graphs without merging equal node data diff --git a/repos/effect/.changeset/sixty-mails-shout.md b/repos/effect/.changeset/sixty-mails-shout.md new file mode 100644 index 0000000..b4bff83 --- /dev/null +++ b/repos/effect/.changeset/sixty-mails-shout.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix VariantSchema.Union diff --git a/repos/effect/.changeset/sixty-socks-yell.md b/repos/effect/.changeset/sixty-socks-yell.md new file mode 100644 index 0000000..1441984 --- /dev/null +++ b/repos/effect/.changeset/sixty-socks-yell.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update `HttpApiClient.urlBuilder` to mirror client shape, and encode params/query via endpoint schemas before building URLs. diff --git a/repos/effect/.changeset/slick-signs-wish.md b/repos/effect/.changeset/slick-signs-wish.md new file mode 100644 index 0000000..d7490cb --- /dev/null +++ b/repos/effect/.changeset/slick-signs-wish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `isOpen` to `Latch` to allow querying the latch's open state diff --git a/repos/effect/.changeset/slick-toes-rush.md b/repos/effect/.changeset/slick-toes-rush.md new file mode 100644 index 0000000..ff74b18 --- /dev/null +++ b/repos/effect/.changeset/slick-toes-rush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +render causes in OtlpTracer exception events diff --git a/repos/effect/.changeset/slimy-planets-divide.md b/repos/effect/.changeset/slimy-planets-divide.md new file mode 100644 index 0000000..aed2e85 --- /dev/null +++ b/repos/effect/.changeset/slimy-planets-divide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow assigning Temporal types to DateTime & Duration input diff --git a/repos/effect/.changeset/slimy-turtles-juggle.md b/repos/effect/.changeset/slimy-turtles-juggle.md new file mode 100644 index 0000000..2cdb299 --- /dev/null +++ b/repos/effect/.changeset/slimy-turtles-juggle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +refactor SqlSchema apis diff --git a/repos/effect/.changeset/slow-beans-battle.md b/repos/effect/.changeset/slow-beans-battle.md new file mode 100644 index 0000000..e3a80a3 --- /dev/null +++ b/repos/effect/.changeset/slow-beans-battle.md @@ -0,0 +1,28 @@ +--- +"@effect/sql-sqlite-react-native": patch +"@effect/openapi-generator": patch +"@effect/platform-node-shared": patch +"@effect/ai-openai-compat": patch +"@effect/platform-browser": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-wasm": patch +"@effect/sql-clickhouse": patch +"@effect/sql-sqlite-bun": patch +"@effect/ai-openrouter": patch +"@effect/opentelemetry": patch +"@effect/platform-node": patch +"@effect/sql-sqlite-do": patch +"@effect/ai-anthropic": patch +"@effect/platform-bun": patch +"@effect/atom-react": patch +"@effect/sql-libsql": patch +"@effect/sql-mysql2": patch +"@effect/ai-openai": patch +"@effect/sql-mssql": patch +"effect": patch +"@effect/sql-d1": patch +"@effect/sql-pg": patch +"@effect/vitest": patch +--- + +Rename the `ServiceMap` module to `Context` across exports, docs, and tests. diff --git a/repos/effect/.changeset/slow-berries-enjoy.md b/repos/effect/.changeset/slow-berries-enjoy.md new file mode 100644 index 0000000..de89cc7 --- /dev/null +++ b/repos/effect/.changeset/slow-berries-enjoy.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Simplify internal and documented request usage by passing request resolvers directly to `Effect.request` instead of wrapping them with `Effect.succeed`. diff --git a/repos/effect/.changeset/small-bugs-hunt.md b/repos/effect/.changeset/small-bugs-hunt.md new file mode 100644 index 0000000..bad2315 --- /dev/null +++ b/repos/effect/.changeset/small-bugs-hunt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix JSON-RPC serialization to return an object for non-batched requests while preserving array responses for true batch requests. diff --git a/repos/effect/.changeset/small-crabs-care.md b/repos/effect/.changeset/small-crabs-care.md new file mode 100644 index 0000000..c79ad5c --- /dev/null +++ b/repos/effect/.changeset/small-crabs-care.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Refine unstable CLI parent/subcommand flag composition. + +- Add `Command.withSharedFlags` conflict validation against existing subcommands, including the `withSubcommands(...).withSharedFlags(...)` composition order. +- Reorder `Command` type parameters to `Command` for clearer parent-context modeling. +- Make `Command.withSubcommands` input typing sound for downstream input-based combinators by reflecting that subcommand paths only carry parent context input. diff --git a/repos/effect/.changeset/small-pets-sit.md b/repos/effect/.changeset/small-pets-sit.md new file mode 100644 index 0000000..5fd8650 --- /dev/null +++ b/repos/effect/.changeset/small-pets-sit.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Don't transform Tool result schemas, as they aren't sent to the providers as +json schemas diff --git a/repos/effect/.changeset/smart-ducks-jump.md b/repos/effect/.changeset/smart-ducks-jump.md new file mode 100644 index 0000000..2322e4f --- /dev/null +++ b/repos/effect/.changeset/smart-ducks-jump.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `Schedule.andThenResult` to initialize the right schedule only after the left schedule completes. +This removes the extra immediate transition tick and correctly completes when the right schedule is finite. diff --git a/repos/effect/.changeset/smart-pillows-buy.md b/repos/effect/.changeset/smart-pillows-buy.md new file mode 100644 index 0000000..f4ac640 --- /dev/null +++ b/repos/effect/.changeset/smart-pillows-buy.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/sql-pg": patch +--- + +Fix `ChildProcess` options type and implement `PgMigrator` diff --git a/repos/effect/.changeset/smart-timers-fly.md b/repos/effect/.changeset/smart-timers-fly.md new file mode 100644 index 0000000..e9a6427 --- /dev/null +++ b/repos/effect/.changeset/smart-timers-fly.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Cookies.expireCookie` / `expireCookieUnsafe` and `HttpServerResponse.expireCookie` / `expireCookieUnsafe` for emitting expired cookies. diff --git a/repos/effect/.changeset/smart-tips-sort.md b/repos/effect/.changeset/smart-tips-sort.md new file mode 100644 index 0000000..c23b25c --- /dev/null +++ b/repos/effect/.changeset/smart-tips-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix Latch.release diff --git a/repos/effect/.changeset/social-hoops-knock.md b/repos/effect/.changeset/social-hoops-knock.md new file mode 100644 index 0000000..c91f7ae --- /dev/null +++ b/repos/effect/.changeset/social-hoops-knock.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Cleanup internals of CLI package diff --git a/repos/effect/.changeset/social-pumas-prove.md b/repos/effect/.changeset/social-pumas-prove.md new file mode 100644 index 0000000..1e71087 --- /dev/null +++ b/repos/effect/.changeset/social-pumas-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +disable tracer propagation for otlp exporter diff --git a/repos/effect/.changeset/soft-comics-wink.md b/repos/effect/.changeset/soft-comics-wink.md new file mode 100644 index 0000000..e93cf52 --- /dev/null +++ b/repos/effect/.changeset/soft-comics-wink.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Default `Effect.context()` to `Effect.context()` when no type parameter is provided. diff --git a/repos/effect/.changeset/soft-delete-sqlmodel.md b/repos/effect/.changeset/soft-delete-sqlmodel.md new file mode 100644 index 0000000..08a90e5 --- /dev/null +++ b/repos/effect/.changeset/soft-delete-sqlmodel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add optional soft delete column support to SqlModel repositories and resolvers. diff --git a/repos/effect/.changeset/soft-seals-allow.md b/repos/effect/.changeset/soft-seals-allow.md new file mode 100644 index 0000000..54e3bc7 --- /dev/null +++ b/repos/effect/.changeset/soft-seals-allow.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +cache base idb query builders diff --git a/repos/effect/.changeset/solid-cougars-attack.md b/repos/effect/.changeset/solid-cougars-attack.md new file mode 100644 index 0000000..199bb36 --- /dev/null +++ b/repos/effect/.changeset/solid-cougars-attack.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add -v alias for version flag diff --git a/repos/effect/.changeset/solid-doors-ring.md b/repos/effect/.changeset/solid-doors-ring.md new file mode 100644 index 0000000..6178bf7 --- /dev/null +++ b/repos/effect/.changeset/solid-doors-ring.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow creating standalone http handlers from HttpApiEndpoints diff --git a/repos/effect/.changeset/solid-items-tease.md b/repos/effect/.changeset/solid-items-tease.md new file mode 100644 index 0000000..ed43ea9 --- /dev/null +++ b/repos/effect/.changeset/solid-items-tease.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix atom node timeout cleanup diff --git a/repos/effect/.changeset/solid-towns-smoke.md b/repos/effect/.changeset/solid-towns-smoke.md new file mode 100644 index 0000000..44300aa --- /dev/null +++ b/repos/effect/.changeset/solid-towns-smoke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +rename Model.Generated to Model.GeneratedByDb diff --git a/repos/effect/.changeset/sour-canyons-rescue.md b/repos/effect/.changeset/sour-canyons-rescue.md new file mode 100644 index 0000000..7499eba --- /dev/null +++ b/repos/effect/.changeset/sour-canyons-rescue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `useCodecs` option to HttpClientEndpoint constructors diff --git a/repos/effect/.changeset/sparkly-bears-act.md b/repos/effect/.changeset/sparkly-bears-act.md new file mode 100644 index 0000000..655477a --- /dev/null +++ b/repos/effect/.changeset/sparkly-bears-act.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add `Effect.acquireDisposable` diff --git a/repos/effect/.changeset/sparkly-coins-sit.md b/repos/effect/.changeset/sparkly-coins-sit.md new file mode 100644 index 0000000..2de14b6 --- /dev/null +++ b/repos/effect/.changeset/sparkly-coins-sit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +expose mcp client capabilities diff --git a/repos/effect/.changeset/spotty-comics-fry.md b/repos/effect/.changeset/spotty-comics-fry.md new file mode 100644 index 0000000..496c87a --- /dev/null +++ b/repos/effect/.changeset/spotty-comics-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix OpenApi Multipart file upload schema generation diff --git a/repos/effect/.changeset/sql-migrator-mjs-mts.md b/repos/effect/.changeset/sql-migrator-mjs-mts.md new file mode 100644 index 0000000..071d942 --- /dev/null +++ b/repos/effect/.changeset/sql-migrator-mjs-mts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Accept `.mjs` and `.mts` migration files in SQL migrator loaders. diff --git a/repos/effect/.changeset/sqlite-bun-prepare-error-channel.md b/repos/effect/.changeset/sqlite-bun-prepare-error-channel.md new file mode 100644 index 0000000..ab744c5 --- /dev/null +++ b/repos/effect/.changeset/sqlite-bun-prepare-error-channel.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Fail with a typed `SqlError` when Bun SQLite statement preparation throws (for example a missing table or a syntax error), instead of letting the driver error escape as a defect, closes #2385. diff --git a/repos/effect/.changeset/sqlite-do-durable-object-transactions.md b/repos/effect/.changeset/sqlite-do-durable-object-transactions.md new file mode 100644 index 0000000..b0016a9 --- /dev/null +++ b/repos/effect/.changeset/sqlite-do-durable-object-transactions.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Support Cloudflare Durable Object SQLite transactions by allowing `SqliteClient` to be configured with `DurableObjectStorage` and routing `withTransaction` through `storage.transaction`. diff --git a/repos/effect/.changeset/stale-dots-tell.md b/repos/effect/.changeset/stale-dots-tell.md new file mode 100644 index 0000000..c6609de --- /dev/null +++ b/repos/effect/.changeset/stale-dots-tell.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve HttpClient.withRateLimiter initial state tracking diff --git a/repos/effect/.changeset/stale-graph-traversal-skips.md b/repos/effect/.changeset/stale-graph-traversal-skips.md new file mode 100644 index 0000000..6bf2694 --- /dev/null +++ b/repos/effect/.changeset/stale-graph-traversal-skips.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Graph BFS, topological sort, and DFS postorder iterators to skip nodes removed from a MutableGraph without recursive self-calls. diff --git a/repos/effect/.changeset/stale-laws-do.md b/repos/effect/.changeset/stale-laws-do.md new file mode 100644 index 0000000..f114fbe --- /dev/null +++ b/repos/effect/.changeset/stale-laws-do.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +optimize bun stream reading diff --git a/repos/effect/.changeset/stale-snakes-know.md b/repos/effect/.changeset/stale-snakes-know.md new file mode 100644 index 0000000..a627259 --- /dev/null +++ b/repos/effect/.changeset/stale-snakes-know.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/repos/effect/.changeset/strict-areas-end.md b/repos/effect/.changeset/strict-areas-end.md new file mode 100644 index 0000000..c469085 --- /dev/null +++ b/repos/effect/.changeset/strict-areas-end.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Track ManagedRuntime fibers in a scope diff --git a/repos/effect/.changeset/strict-buckets-hug.md b/repos/effect/.changeset/strict-buckets-hug.md new file mode 100644 index 0000000..73164ef --- /dev/null +++ b/repos/effect/.changeset/strict-buckets-hug.md @@ -0,0 +1,8 @@ +--- +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +--- + +allow undefined for ai config diff --git a/repos/effect/.changeset/strip-resolved-approvals.md b/repos/effect/.changeset/strip-resolved-approvals.md new file mode 100644 index 0000000..4cdf441 --- /dev/null +++ b/repos/effect/.changeset/strip-resolved-approvals.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Strip resolved tool approval artifacts from prompt before sending to provider, preventing errors when providers reject pre-resolved approval requests. diff --git a/repos/effect/.changeset/strong-balloons-tickle.md b/repos/effect/.changeset/strong-balloons-tickle.md new file mode 100644 index 0000000..c778264 --- /dev/null +++ b/repos/effect/.changeset/strong-balloons-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include toolkit tool handler requirements in AI generation API environment inference. diff --git a/repos/effect/.changeset/strong-bees-queue.md b/repos/effect/.changeset/strong-bees-queue.md new file mode 100644 index 0000000..d7c4018 --- /dev/null +++ b/repos/effect/.changeset/strong-bees-queue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add the unstable workflow DurableQueue module. diff --git a/repos/effect/.changeset/strong-insects-film.md b/repos/effect/.changeset/strong-insects-film.md new file mode 100644 index 0000000..075c42f --- /dev/null +++ b/repos/effect/.changeset/strong-insects-film.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure WithTransaction wraps entire rpc handler diff --git a/repos/effect/.changeset/struct-record.md b/repos/effect/.changeset/struct-record.md new file mode 100644 index 0000000..9a6d46a --- /dev/null +++ b/repos/effect/.changeset/struct-record.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Struct: add `Struct.Record` constructor for creating records with the given keys and value. diff --git a/repos/effect/.changeset/sunny-ads-hang.md b/repos/effect/.changeset/sunny-ads-hang.md new file mode 100644 index 0000000..d51143e --- /dev/null +++ b/repos/effect/.changeset/sunny-ads-hang.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add ClusterSchema.WithTransaction annotation diff --git a/repos/effect/.changeset/sunny-bikes-sleep.md b/repos/effect/.changeset/sunny-bikes-sleep.md new file mode 100644 index 0000000..79412b3 --- /dev/null +++ b/repos/effect/.changeset/sunny-bikes-sleep.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +default ws close codes to 1001 in case they are undefined diff --git a/repos/effect/.changeset/sunny-rooms-invent.md b/repos/effect/.changeset/sunny-rooms-invent.md new file mode 100644 index 0000000..4e63901 --- /dev/null +++ b/repos/effect/.changeset/sunny-rooms-invent.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Refine `ExtractServices` to omit tool handler requirements when automatic tool resolution is explicitly disabled through the `disableToolCallResolution` option. diff --git a/repos/effect/.changeset/sweet-donuts-bet.md b/repos/effect/.changeset/sweet-donuts-bet.md new file mode 100644 index 0000000..3d09822 --- /dev/null +++ b/repos/effect/.changeset/sweet-donuts-bet.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add graceful shutdown to http servers diff --git a/repos/effect/.changeset/sweet-hotels-give.md b/repos/effect/.changeset/sweet-hotels-give.md new file mode 100644 index 0000000..a54bdde --- /dev/null +++ b/repos/effect/.changeset/sweet-hotels-give.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +make EntityResource lazy by default diff --git a/repos/effect/.changeset/sweet-schedules-matter.md b/repos/effect/.changeset/sweet-schedules-matter.md new file mode 100644 index 0000000..dcc8d4a --- /dev/null +++ b/repos/effect/.changeset/sweet-schedules-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update `Schedule.addDelay` and `Schedule.modifyDelay` to receive full schedule metadata instead of separate output and delay arguments. diff --git a/repos/effect/.changeset/sweet-views-learn.md b/repos/effect/.changeset/sweet-views-learn.md new file mode 100644 index 0000000..dda17ba --- /dev/null +++ b/repos/effect/.changeset/sweet-views-learn.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure workflow failures are not squashed by suspension interrupts diff --git a/repos/effect/.changeset/swift-spiders-unpack.md b/repos/effect/.changeset/swift-spiders-unpack.md new file mode 100644 index 0000000..3f1f957 --- /dev/null +++ b/repos/effect/.changeset/swift-spiders-unpack.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Layer.suspend` as a lazy constructor for dynamically choosing a layer while preserving normal layer sharing. diff --git a/repos/effect/.changeset/swift-symbols-stand.md b/repos/effect/.changeset/swift-symbols-stand.md new file mode 100644 index 0000000..2dbec99 --- /dev/null +++ b/repos/effect/.changeset/swift-symbols-stand.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +remove rpc client nesting to improve type performance diff --git a/repos/effect/.changeset/tagged-error-class-optional-empty-props.md b/repos/effect/.changeset/tagged-error-class-optional-empty-props.md new file mode 100644 index 0000000..7b63b56 --- /dev/null +++ b/repos/effect/.changeset/tagged-error-class-optional-empty-props.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +`Schema.TaggedErrorClass`, `Schema.Class`, and `Schema.ErrorClass` constructors now allow omitting the props argument when all fields have constructor defaults (e.g. `new MyError()` instead of `new MyError({})`). diff --git a/repos/effect/.changeset/tall-hairs-return.md b/repos/effect/.changeset/tall-hairs-return.md new file mode 100644 index 0000000..551d144 --- /dev/null +++ b/repos/effect/.changeset/tall-hairs-return.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpServerResponse.toClientResponse` for converting server responses into `HttpClientResponse` values. diff --git a/repos/effect/.changeset/tall-mails-listen.md b/repos/effect/.changeset/tall-mails-listen.md new file mode 100644 index 0000000..96d7af4 --- /dev/null +++ b/repos/effect/.changeset/tall-mails-listen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.catchNoSuchElement`, a renamed port of v3 `Effect.optionFromOptional` that converts `NoSuchElementError` failures into `Option.none`. diff --git a/repos/effect/.changeset/tall-queens-cheer.md b/repos/effect/.changeset/tall-queens-cheer.md new file mode 100644 index 0000000..cffebd7 --- /dev/null +++ b/repos/effect/.changeset/tall-queens-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `UrlParams.Input` usage to accept interface-typed records in HTTP client and server helpers while keeping coercion constraints for url parameter values. diff --git a/repos/effect/.changeset/tall-wombats-wave.md b/repos/effect/.changeset/tall-wombats-wave.md new file mode 100644 index 0000000..8bf297c --- /dev/null +++ b/repos/effect/.changeset/tall-wombats-wave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix ai LanguageModel streaming finish parts so finish events are always emitted when a toolkit is provided. diff --git a/repos/effect/.changeset/tangy-colts-lose.md b/repos/effect/.changeset/tangy-colts-lose.md new file mode 100644 index 0000000..9678f53 --- /dev/null +++ b/repos/effect/.changeset/tangy-colts-lose.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +simplify NodeChildSpawner stdout streams diff --git a/repos/effect/.changeset/tasty-comics-send.md b/repos/effect/.changeset/tasty-comics-send.md new file mode 100644 index 0000000..78bed2e --- /dev/null +++ b/repos/effect/.changeset/tasty-comics-send.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +improve runSync error when executing async effects diff --git a/repos/effect/.changeset/tasty-moments-post.md b/repos/effect/.changeset/tasty-moments-post.md new file mode 100644 index 0000000..8a2d4e1 --- /dev/null +++ b/repos/effect/.changeset/tasty-moments-post.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effect.setContext for fully replacing the fiber context diff --git a/repos/effect/.changeset/ten-kings-fry.md b/repos/effect/.changeset/ten-kings-fry.md new file mode 100644 index 0000000..593dc94 --- /dev/null +++ b/repos/effect/.changeset/ten-kings-fry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix empty body decoding in HttpApiBuilder diff --git a/repos/effect/.changeset/thick-pandas-wait.md b/repos/effect/.changeset/thick-pandas-wait.md new file mode 100644 index 0000000..5ddc891 --- /dev/null +++ b/repos/effect/.changeset/thick-pandas-wait.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add default value support to CLI integer prompts. diff --git a/repos/effect/.changeset/thin-ducks-wonder.md b/repos/effect/.changeset/thin-ducks-wonder.md new file mode 100644 index 0000000..2176226 --- /dev/null +++ b/repos/effect/.changeset/thin-ducks-wonder.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `HttpClient.retryTransient` option `mode` to `retryOn` and rename `"both"` to `"errors-and-responses"`. diff --git a/repos/effect/.changeset/thirty-ducks-go.md b/repos/effect/.changeset/thirty-ducks-go.md new file mode 100644 index 0000000..b3f3aa1 --- /dev/null +++ b/repos/effect/.changeset/thirty-ducks-go.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +HttpClient.withRateLimiter adds delay from retry-after headers diff --git a/repos/effect/.changeset/thirty-pans-love.md b/repos/effect/.changeset/thirty-pans-love.md new file mode 100644 index 0000000..0d0a4f9 --- /dev/null +++ b/repos/effect/.changeset/thirty-pans-love.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +allow Model.Class for indexeddb schemas diff --git a/repos/effect/.changeset/three-corners-sort.md b/repos/effect/.changeset/three-corners-sort.md new file mode 100644 index 0000000..ceb67c6 --- /dev/null +++ b/repos/effect/.changeset/three-corners-sort.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +port Url module from v3 diff --git a/repos/effect/.changeset/three-ravens-jam.md b/repos/effect/.changeset/three-ravens-jam.md new file mode 100644 index 0000000..ba528cc --- /dev/null +++ b/repos/effect/.changeset/three-ravens-jam.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.findFirst` and `Effect.findFirstFilter` for short-circuiting effectful searches over iterables. diff --git a/repos/effect/.changeset/three-tomatoes-wave.md b/repos/effect/.changeset/three-tomatoes-wave.md new file mode 100644 index 0000000..886f44f --- /dev/null +++ b/repos/effect/.changeset/three-tomatoes-wave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `unstable/sql/SqlSchema` request input typing so `findAll` and `findNonEmpty` accept `Request["Type"]` instead of `Request["Encoded"]`. diff --git a/repos/effect/.changeset/tidy-foxes-own.md b/repos/effect/.changeset/tidy-foxes-own.md new file mode 100644 index 0000000..8a4f4b8 --- /dev/null +++ b/repos/effect/.changeset/tidy-foxes-own.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +clean up otlp config diff --git a/repos/effect/.changeset/tidy-icons-glow.md b/repos/effect/.changeset/tidy-icons-glow.md new file mode 100644 index 0000000..dde7979 --- /dev/null +++ b/repos/effect/.changeset/tidy-icons-glow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add a `Config.literals` convenience constructor for `Schema.Literals`, closes #2091. diff --git a/repos/effect/.changeset/tidy-int32-annotations.md b/repos/effect/.changeset/tidy-int32-annotations.md new file mode 100644 index 0000000..ca99cdd --- /dev/null +++ b/repos/effect/.changeset/tidy-int32-annotations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `isInt32` to apply custom annotations only to its filter group. diff --git a/repos/effect/.changeset/tidy-pandas-smile.md b/repos/effect/.changeset/tidy-pandas-smile.md new file mode 100644 index 0000000..73f4b88 --- /dev/null +++ b/repos/effect/.changeset/tidy-pandas-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpApiError.UnprocessableEntity` and `HttpApiError.UnprocessableEntityNoContent` for status 422 responses. diff --git a/repos/effect/.changeset/tidy-stacks-encode.md b/repos/effect/.changeset/tidy-stacks-encode.md new file mode 100644 index 0000000..d9e9a24 --- /dev/null +++ b/repos/effect/.changeset/tidy-stacks-encode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Encode HTTP API client path parameters when building request URLs. diff --git a/repos/effect/.changeset/tidy-stars-drive.md b/repos/effect/.changeset/tidy-stars-drive.md new file mode 100644 index 0000000..b31ea08 --- /dev/null +++ b/repos/effect/.changeset/tidy-stars-drive.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TestClock.currentTimeNanosUnsafe()` to floor fractional millisecond instants before converting them to `BigInt`. diff --git a/repos/effect/.changeset/tiny-buckets-wave.md b/repos/effect/.changeset/tiny-buckets-wave.md new file mode 100644 index 0000000..b01e82b --- /dev/null +++ b/repos/effect/.changeset/tiny-buckets-wave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Underline the active label in CLI multi-select prompts and add a scratchpad example for manual verification. diff --git a/repos/effect/.changeset/tiny-lilies-flash.md b/repos/effect/.changeset/tiny-lilies-flash.md new file mode 100644 index 0000000..f636865 --- /dev/null +++ b/repos/effect/.changeset/tiny-lilies-flash.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +fix idb entries transaction diff --git a/repos/effect/.changeset/tiny-rabbits-smile.md b/repos/effect/.changeset/tiny-rabbits-smile.md new file mode 100644 index 0000000..9cc27e3 --- /dev/null +++ b/repos/effect/.changeset/tiny-rabbits-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Relax `Ndjson` byte-stream channel signatures to accept plain `Uint8Array`. diff --git a/repos/effect/.changeset/to-codec-json-schema.md b/repos/effect/.changeset/to-codec-json-schema.md new file mode 100644 index 0000000..2cee85e --- /dev/null +++ b/repos/effect/.changeset/to-codec-json-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose the original input schema on `Schema.toType`, `Schema.toEncoded`, `Schema.toCodecJson`, and `Schema.toCodecStringTree` results via the `schema` property. This aligns these schema wrappers with other wrappers that retain their source schema for type-level and runtime introspection. diff --git a/repos/effect/.changeset/tocodecjson-return-json-type.md b/repos/effect/.changeset/tocodecjson-return-json-type.md new file mode 100644 index 0000000..401d58c --- /dev/null +++ b/repos/effect/.changeset/tocodecjson-return-json-type.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/platform-browser": patch +"@effect/platform-bun": patch +"@effect/platform-node": patch +--- + +Schema: `toCodecJson` now returns `Codec` instead of `Codec`. + +Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. diff --git a/repos/effect/.changeset/tool-get-json-schema-tests.md b/repos/effect/.changeset/tool-get-json-schema-tests.md new file mode 100644 index 0000000..f94d0cf --- /dev/null +++ b/repos/effect/.changeset/tool-get-json-schema-tests.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Tool.make` type and runtime behavior when `parameters` is not provided. diff --git a/repos/effect/.changeset/true-actors-battle.md b/repos/effect/.changeset/true-actors-battle.md new file mode 100644 index 0000000..b93e185 --- /dev/null +++ b/repos/effect/.changeset/true-actors-battle.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +fix fs.stat when blksize is undefined diff --git a/repos/effect/.changeset/try-promise-catch-defect.md b/repos/effect/.changeset/try-promise-catch-defect.md new file mode 100644 index 0000000..0d6d52e --- /dev/null +++ b/repos/effect/.changeset/try-promise-catch-defect.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix `Effect.try` thunk usage and `Effect.tryPromise` mapper and signal handling defects. + +`Effect.try` now supports passing a thunk directly, matching `Effect.tryPromise`. Thrown values from direct-thunk usage are mapped to `Cause.UnknownError`. + +When a promise handled by `Effect.tryPromise` rejected and the custom `catch` mapper threw while mapping that rejection, the effect could remain pending and produce an unhandled rejection. The mapper is now guarded consistently with the synchronous throw path, so a thrown mapper error becomes an Effect defect. The JSDoc for `Effect.try` and `Effect.tryPromise` was also corrected. + +`Effect.tryPromise` now also only creates an `AbortController` when the wrapped thunk declares an `AbortSignal` parameter. diff --git a/repos/effect/.changeset/twelve-dragons-move.md b/repos/effect/.changeset/twelve-dragons-move.md new file mode 100644 index 0000000..bf9c5af --- /dev/null +++ b/repos/effect/.changeset/twelve-dragons-move.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +deduplicate SqlResolver.findById requests diff --git a/repos/effect/.changeset/twenty-buttons-cheer.md b/repos/effect/.changeset/twenty-buttons-cheer.md new file mode 100644 index 0000000..d1b709b --- /dev/null +++ b/repos/effect/.changeset/twenty-buttons-cheer.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +expose more atom Node properties diff --git a/repos/effect/.changeset/twenty-facts-laugh.md b/repos/effect/.changeset/twenty-facts-laugh.md new file mode 100644 index 0000000..7430b4f --- /dev/null +++ b/repos/effect/.changeset/twenty-facts-laugh.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use Sets to track atom relationships diff --git a/repos/effect/.changeset/two-roses-double.md b/repos/effect/.changeset/two-roses-double.md new file mode 100644 index 0000000..6efc5f5 --- /dev/null +++ b/repos/effect/.changeset/two-roses-double.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +improve openai websocket error status diff --git a/repos/effect/.changeset/unify-error-defect-stack-options.md b/repos/effect/.changeset/unify-error-defect-stack-options.md new file mode 100644 index 0000000..6c032f6 --- /dev/null +++ b/repos/effect/.changeset/unify-error-defect-stack-options.md @@ -0,0 +1,23 @@ +--- +"effect": patch +--- + +Replace the `Schema.Error` and `Schema.Defect` schema constants with constructor +functions, `Schema.Error()` and `Schema.Defect()`. + +Unify `Schema.ErrorWithStack` into `Schema.Error({ includeStack: true })` and +`Schema.DefectWithStack` into `Schema.Defect({ includeStack: true })`. + +Error causes are encoded by default using the same JSON defect encoding +semantics used by `Schema.Defect`; pass `{ excludeCause: true }` to omit nested +cause data. + +Equivalent `Schema.Error` and `Schema.Defect` options are canonicalized, so +repeated constructor calls with the same option values reuse the same schema. + +`Schema.Defect()` now models defects as `unknown` values with a JSON encoded +form. Error-shaped JSON objects with a string `message` decode to JavaScript +`Error` values, so non-`Error` objects such as `{ message: "boom" }` do not +round-trip unchanged. Other non-`Error` values are normalized through JSON +serialization, with non-JSON values falling back to Effect's formatted string +representation. diff --git a/repos/effect/.changeset/update-schema-arbitrary-report.md b/repos/effect/.changeset/update-schema-arbitrary-report.md new file mode 100644 index 0000000..ed3b794 --- /dev/null +++ b/repos/effect/.changeset/update-schema-arbitrary-report.md @@ -0,0 +1,22 @@ +--- +"effect": patch +--- + +Update Schema arbitrary derivation to use the new filter metadata, candidate generation, optional derivation reports, recursion-aware generation, and the renamed `OrderedConstraint` model. + +Migration from the previous v4 API: + +- Replace filter annotations from `toArbitraryConstraint: constraint` to `arbitrary: { constraint }`. When a filter cannot be described as a constraint, use `arbitrary: { candidate }` to add a weighted source that is still checked by the filter. +- Replace bucketed constraints with the flat `Schema.Annotations.ToArbitrary.Constraint` shape: + - `string.minLength`, `array.minLength`, object property counts, collection sizes -> `minLength` + - `string.maxLength`, `array.maxLength`, object property counts, collection sizes -> `maxLength` + - `string.patterns` -> `patterns` + - `number.isInteger` -> `integer` + - `number.noNaN` -> `noNaN` + - `number.noDefaultInfinity` -> `noInfinity` + - `date.noInvalidDate` -> `valid` + - `array.comparator` for uniqueness -> `unique` using Effect equality + - `ordered.min` / `minExcluded` / `max` / `maxExcluded` -> `ordered.minimum` / `exclusiveMinimum` / `maximum` / `exclusiveMaximum` +- In arbitrary hooks, read `context.constraint` instead of `context.constraints`. Replace `context.isSuspend` with `context.recursion`; when combining finite and recursive branches, pass `context.recursion` to `fc.oneof` with the finite branch first. +- Generic declaration hooks now receive type parameters as `{ arbitrary, terminal }`. Atomic declarations may still return a bare `FastCheck.Arbitrary`, but generic declarations should return `{ arbitrary, terminal }` when they can preserve a finite terminal branch. +- `Schema.toArbitrary(schema, { report: true })` now returns `{ value, report }`; without `{ report: true }`, it keeps returning the arbitrary directly. `Schema.toArbitraryLazy` always returns a lazy arbitrary. diff --git a/repos/effect/.changeset/upset-colts-stick.md b/repos/effect/.changeset/upset-colts-stick.md new file mode 100644 index 0000000..5e87fdd --- /dev/null +++ b/repos/effect/.changeset/upset-colts-stick.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add missing Equivalence.Date diff --git a/repos/effect/.changeset/use-url-can-parse.md b/repos/effect/.changeset/use-url-can-parse.md new file mode 100644 index 0000000..959e2e6 --- /dev/null +++ b/repos/effect/.changeset/use-url-can-parse.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use `URL.canParse` to validate URL string schema decoding before constructing a `URL`. This avoids relying on thrown exceptions for routine validation while preserving the same invalid URL issue and successful decode output. diff --git a/repos/effect/.changeset/validate-httpapi-handler-registration.md b/repos/effect/.changeset/validate-httpapi-handler-registration.md new file mode 100644 index 0000000..a2896b0 --- /dev/null +++ b/repos/effect/.changeset/validate-httpapi-handler-registration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject unknown and duplicate HttpApi handler registrations with descriptive errors. diff --git a/repos/effect/.changeset/validate-openapi-global-conflicts.md b/repos/effect/.changeset/validate-openapi-global-conflicts.md new file mode 100644 index 0000000..4859757 --- /dev/null +++ b/repos/effect/.changeset/validate-openapi-global-conflicts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject duplicate OpenAPI operations and operation identifiers, and reject incompatible security schemes that reuse a name. diff --git a/repos/effect/.changeset/vast-bananas-send.md b/repos/effect/.changeset/vast-bananas-send.md new file mode 100644 index 0000000..815d683 --- /dev/null +++ b/repos/effect/.changeset/vast-bananas-send.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +clean up ShardId diff --git a/repos/effect/.changeset/vast-deserts-travel.md b/repos/effect/.changeset/vast-deserts-travel.md new file mode 100644 index 0000000..262cd54 --- /dev/null +++ b/repos/effect/.changeset/vast-deserts-travel.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Add `endOnDone` option to Stdio stdout / stderr diff --git a/repos/effect/.changeset/violet-peaches-feel.md b/repos/effect/.changeset/violet-peaches-feel.md new file mode 100644 index 0000000..b4e2ca2 --- /dev/null +++ b/repos/effect/.changeset/violet-peaches-feel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effectable module diff --git a/repos/effect/.changeset/violet-tips-open.md b/repos/effect/.changeset/violet-tips-open.md new file mode 100644 index 0000000..e26432f --- /dev/null +++ b/repos/effect/.changeset/violet-tips-open.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Fix `PgClient.makeClient` to connect the underlying `pg.Client` during resource acquisition. diff --git a/repos/effect/.changeset/vitest-layer-top-level-options.md b/repos/effect/.changeset/vitest-layer-top-level-options.md new file mode 100644 index 0000000..2ef279d --- /dev/null +++ b/repos/effect/.changeset/vitest-layer-top-level-options.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Allow top-level `it.layer` to accept `memoMap`, `timeout`, and `excludeTestServices` options, matching the standalone `layer` export. Nested `it.layer` calls remain restricted to `timeout` only. diff --git a/repos/effect/.changeset/wacky-grapes-poke.md b/repos/effect/.changeset/wacky-grapes-poke.md new file mode 100644 index 0000000..6e9284d --- /dev/null +++ b/repos/effect/.changeset/wacky-grapes-poke.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove superfluous error from SqlSchema.findAll signature diff --git a/repos/effect/.changeset/wacky-rice-add.md b/repos/effect/.changeset/wacky-rice-add.md new file mode 100644 index 0000000..9de72c4 --- /dev/null +++ b/repos/effect/.changeset/wacky-rice-add.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +tighten Schema on \_meta fields in McpSchema; closes #1463 diff --git a/repos/effect/.changeset/warm-dolls-brake.md b/repos/effect/.changeset/warm-dolls-brake.md new file mode 100644 index 0000000..dc177bf --- /dev/null +++ b/repos/effect/.changeset/warm-dolls-brake.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add meta-var that shows log level and bash options in command line. diff --git a/repos/effect/.changeset/warm-friends-tie.md b/repos/effect/.changeset/warm-friends-tie.md new file mode 100644 index 0000000..b600cb2 --- /dev/null +++ b/repos/effect/.changeset/warm-friends-tie.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix cli subcommand context diff --git a/repos/effect/.changeset/warm-snails-shop.md b/repos/effect/.changeset/warm-snails-shop.md new file mode 100644 index 0000000..907de8b --- /dev/null +++ b/repos/effect/.changeset/warm-snails-shop.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-openrouter": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +--- + +Fix the generated schemas for ai providers diff --git a/repos/effect/.changeset/wet-news-invent.md b/repos/effect/.changeset/wet-news-invent.md new file mode 100644 index 0000000..f13ab2e --- /dev/null +++ b/repos/effect/.changeset/wet-news-invent.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add a `requireServicesAt` option to `PersistedCache.make` so lookup-service requirements can be configured like `Cache`. diff --git a/repos/effect/.changeset/wild-readers-clean.md b/repos/effect/.changeset/wild-readers-clean.md new file mode 100644 index 0000000..40b517e --- /dev/null +++ b/repos/effect/.changeset/wild-readers-clean.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +mcp http request with no session header is 404 response diff --git a/repos/effect/.changeset/wild-suns-bearer-space.md b/repos/effect/.changeset/wild-suns-bearer-space.md new file mode 100644 index 0000000..71a6805 --- /dev/null +++ b/repos/effect/.changeset/wild-suns-bearer-space.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApiSecurity` bearer/http credential decoding diff --git a/repos/effect/.changeset/wise-ants-wave.md b/repos/effect/.changeset/wise-ants-wave.md new file mode 100644 index 0000000..e496672 --- /dev/null +++ b/repos/effect/.changeset/wise-ants-wave.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `AtomHttpApi.query` to forward v4 `params` / `query` request fields to `HttpApiClient` at runtime. +Also align `AtomHttpApi` endpoint type inference with v4 `HttpApiEndpoint` params/query naming and add a regression test. diff --git a/repos/effect/.changeset/wise-flags-shift.md b/repos/effect/.changeset/wise-flags-shift.md new file mode 100644 index 0000000..c470c58 --- /dev/null +++ b/repos/effect/.changeset/wise-flags-shift.md @@ -0,0 +1,22 @@ +--- +"effect": patch +--- + +Refactor unstable CLI global flags to command-scoped declarations. + +### Breaking changes + +- Remove `GlobalFlag.add`, `GlobalFlag.remove`, and `GlobalFlag.clear` +- Add `Command.withGlobalFlags(...)` as the declaration API for command/subcommand scope +- Change `GlobalFlag.setting` constructor to curried form which carries type-level identifier: + - before: `GlobalFlag.setting({ flag, ... })` + - after: `GlobalFlag.setting("id")({ flag })` +- Change setting context identity to a stable type-level string: + - `effect/unstable/cli/GlobalFlag/${id}` + +### Behavior changes + +- Global flags are now scoped by command path (root-to-leaf declarations) +- Out-of-scope global flags are rejected for the selected subcommand path +- Help now renders only global flags active for the requested command path +- Setting defaults are sourced from `Flag` combinators (`optional`, `withDefault`) rather than setting constructor defaults diff --git a/repos/effect/.changeset/wise-oranges-stay.md b/repos/effect/.changeset/wise-oranges-stay.md new file mode 100644 index 0000000..263143b --- /dev/null +++ b/repos/effect/.changeset/wise-oranges-stay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +allow Context.Key to be covariant diff --git a/repos/effect/.changeset/witty-lobsters-share.md b/repos/effect/.changeset/witty-lobsters-share.md new file mode 100644 index 0000000..c786da6 --- /dev/null +++ b/repos/effect/.changeset/witty-lobsters-share.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Replace the default HttpApi schema-validation error with `HttpApiError.BadRequestNoContent`. diff --git a/repos/effect/.changeset/yellow-adults-study.md b/repos/effect/.changeset/yellow-adults-study.md new file mode 100644 index 0000000..015ba9d --- /dev/null +++ b/repos/effect/.changeset/yellow-adults-study.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +openai ws tweaks diff --git a/repos/effect/.changeset/yellow-clocks-dance.md b/repos/effect/.changeset/yellow-clocks-dance.md new file mode 100644 index 0000000..707dfbc --- /dev/null +++ b/repos/effect/.changeset/yellow-clocks-dance.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Command.withAlias` for unstable CLI commands, including subcommand parsing by alias and help output that renders aliases as `name, alias` in subcommand listings. diff --git a/repos/effect/.changeset/yellow-dingos-jump.md b/repos/effect/.changeset/yellow-dingos-jump.md new file mode 100644 index 0000000..1a6af4e --- /dev/null +++ b/repos/effect/.changeset/yellow-dingos-jump.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApi.prefix` so it updates endpoint path types the same way `HttpApiGroup.prefix` does. diff --git a/repos/effect/.changeset/young-doors-change.md b/repos/effect/.changeset/young-doors-change.md new file mode 100644 index 0000000..c5ec50d --- /dev/null +++ b/repos/effect/.changeset/young-doors-change.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Random.shuffle` to shuffle iterables with seeded randomness support. diff --git a/repos/effect/.envrc b/repos/effect/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/repos/effect/.envrc @@ -0,0 +1 @@ +use flake diff --git a/repos/effect/.github/actions/setup/action.yaml b/repos/effect/.github/actions/setup/action.yaml new file mode 100644 index 0000000..f728378 --- /dev/null +++ b/repos/effect/.github/actions/setup/action.yaml @@ -0,0 +1,33 @@ +name: Setup +description: Perform standard setup and install dependencies using pnpm. +inputs: + deno-version: + description: The version of Deno to install + required: false + bun-version: + description: The version of Bun to install + required: false + +runs: + using: composite + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install node + uses: actions/setup-node@v6 + with: + cache: pnpm + node-version: 26.4.0 + - name: Install deno + uses: denoland/setup-deno@v2 + if: ${{ inputs.deno-version != '' }} + with: + deno-version: ${{ inputs.deno-version }} + - name: Install bun + uses: oven-sh/setup-bun@v2 + if: ${{ inputs.bun-version != '' }} + with: + bun-version: ${{ inputs.bun-version }} + - name: Install dependencies + shell: bash + run: pnpm install diff --git a/repos/effect/.github/workflows/ai-codegen.yml b/repos/effect/.github/workflows/ai-codegen.yml new file mode 100644 index 0000000..47c5832 --- /dev/null +++ b/repos/effect/.github/workflows/ai-codegen.yml @@ -0,0 +1,66 @@ +name: Nightly AI Codegen + +on: + schedule: + - cron: "0 0 * * *" # Midnight UTC daily + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + codegen: + name: AI Codegen + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + uses: ./.github/actions/setup + + - name: Run AI Codegen + run: node packages/tools/ai-codegen/src/bin.ts generate + + - name: Check for changes + id: changes + run: | + # Only consider changes in packages/ai/ + providers=$(git diff --name-only | grep "^packages/ai/" | cut -d'/' -f3 | sort -u | sed 's/^/- /' || true) + if [ -z "$providers" ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + { + echo "updated_providers<> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: chore/ai-codegen-update + delete-branch: true + title: "chore: update AI codegen generated files" + body: | + Automated update of AI provider generated code. + + ## Updated Providers + + ${{ steps.changes.outputs.updated_providers }} + + --- + + *This PR was automatically generated by the nightly codegen workflow.* + commit-message: "chore: regenerate AI provider code" + labels: automated diff --git a/repos/effect/.github/workflows/bundle-comment.yml b/repos/effect/.github/workflows/bundle-comment.yml new file mode 100644 index 0000000..3ce7040 --- /dev/null +++ b/repos/effect/.github/workflows/bundle-comment.yml @@ -0,0 +1,72 @@ +name: Bundle Size Comment +on: + workflow_run: + workflows: ["Check"] + types: + - completed + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + comment: + name: Bundle + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: write + timeout-minutes: 1 + steps: + - name: Download Artifact + uses: actions/download-artifact@v8 + with: + name: bundle-stats + path: bundle-stats + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Get stats + id: stats + run: | + { + echo 'stats<> $GITHUB_OUTPUT + # https://github.com/orgs/community/discussions/25220#discussioncomment-11300118 + - name: Get PR number + id: pr-context + env: + GH_TOKEN: ${{ github.token }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" --json 'number' --jq '"number=\(.number)"' >> "${GITHUB_OUTPUT}" + - name: Find Comment + id: find-comment + uses: peter-evans/find-comment@v4 + with: + issue-number: ${{ steps.pr-context.outputs.number }} + comment-author: "github-actions[bot]" + body-includes: + - name: Create Comment + id: comment + uses: peter-evans/create-or-update-comment@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUNDLE_STATS: "${{ steps.stats.outputs.stats }}" + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ steps.pr-context.outputs.number }} + edit-mode: replace + body: | + + ## Bundle Size Analysis + ${{ env.BUNDLE_STATS }} diff --git a/repos/effect/.github/workflows/check.yml b/repos/effect/.github/workflows/check.yml new file mode 100644 index 0000000..28382bb --- /dev/null +++ b/repos/effect/.github/workflows/check.yml @@ -0,0 +1,195 @@ +name: Check +on: + workflow_dispatch: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - run: pnpm lint + + types: + name: Types + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - run: pnpm check + - run: pnpm test-types --target '>=5.9' + + build: + name: Build + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Set strip internals config + run: | + sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json + - run: pnpm build + + types-deno: + name: Types on Deno + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + with: + deno-version: v2.8.3 + - name: Set strip internals config + run: | + sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json + - run: deno check . + + bundle: + name: Bundle + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pull-requests: write + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Clone base ref + uses: actions/checkout@v6 + with: + path: base + ref: ${{ github.event.pull_request.base.ref }} + - name: Set strip internals config + run: | + sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json + sed -i 's/"stripInternal": false/"stripInternal": true/' base/tsconfig.base.json + - name: Build + run: | + pnpm build & + cd base && pnpm install && pnpm build & + wait + - name: Compare bundle size + run: node ./packages/tools/bundle/src/bin.ts compare --base-dir base/packages/tools/bundle/fixtures + - name: Upload stats artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: bundle-stats + path: stats.txt + if-no-files-found: error + + test: + name: Test + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shard: [1/2, 2/2] + runtime: [Node, Deno] + steps: + - uses: actions/checkout@v6 + + - name: Pre-pull test container images + run: | + docker pull testcontainers/ryuk:0.14.0 & + docker pull ghcr.io/tursodatabase/libsql-server:main & + docker pull postgres:alpine & + docker pull mysql:lts & + docker pull vitess/vttestserver:mysql80 & + docker pull redis:alpine & + wait + + - name: Install dependencies + if: matrix.runtime == 'Node' + uses: ./.github/actions/setup + - name: Test + if: matrix.runtime == 'Node' + run: pnpm test --shard ${{ matrix.shard }} + + - name: Install dependencies + if: matrix.runtime == 'Deno' + uses: ./.github/actions/setup + with: + deno-version: v2.8.3 + - name: Test + if: matrix.runtime == 'Deno' + run: deno task test --shard ${{ matrix.shard }} + + docgen: + name: Documentation Generation + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Generate Documentation + run: pnpm docgen + + ai-docgen: + name: AI Documentation Generation + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Generate AI Documentation + run: pnpm ai-docgen + - name: Verify AI Documentation is up-to-date + run: | + if [ -n "$(git status --short)" ]; then + git status --short + echo "Run 'pnpm ai-docgen' and commit generated changes." + exit 1 + fi + + circular: + name: Circular Dependencies + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Check for circular dependencies + run: pnpm circular diff --git a/repos/effect/.github/workflows/release.yml b/repos/effect/.github/workflows/release.yml new file mode 100644 index 0000000..bdda95e --- /dev/null +++ b/repos/effect/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Release +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false # Prevent newer push from interrupting active release + +permissions: {} + +jobs: + release: + if: github.repository_owner == 'Effect-Ts' + name: Release + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v6 + with: + # This is required to ensure the `GITHUB_TOKEN` we provide below is + # **always** used when pushing updates to the changesets release branch. + # Otherwise the default token will be used, and actions on that versioned + # release branch will not be triggered. + persist-credentials: false + - name: Install dependencies + uses: ./.github/actions/setup + - name: Upgrade npm for OIDC support + run: npm install -g npm@11 + - name: Set strip internals config + run: | + sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json + - name: Create Release Pull Request or Publish + uses: changesets/action@v1 + with: + version: pnpm changeset-version + publish: pnpm changeset-publish + env: + # Use a personal access token instead of the one that GitHub generates + # automatically to ensure workflows get triggered on the changesets + # release branch. + GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }} diff --git a/repos/effect/.github/workflows/snapshot.yml b/repos/effect/.github/workflows/snapshot.yml new file mode 100644 index 0000000..f4b04b0 --- /dev/null +++ b/repos/effect/.github/workflows/snapshot.yml @@ -0,0 +1,34 @@ +name: Snapshot +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + snapshot: + name: Snapshot + if: github.repository_owner == 'Effect-Ts' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Set strip internals config + run: | + sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json + - name: Run codemods + run: pnpm codemod + - name: Build package + run: pnpm build + - name: Create snapshot + id: snapshot + run: pnpx pkg-pr-new@0.0.78 publish --pnpm --comment=off ./packages/* ./packages/atom/* ./packages/ai/* ./packages/sql/* ./packages/tools/* diff --git a/repos/effect/.gitignore b/repos/effect/.gitignore new file mode 100644 index 0000000..ecc1cb8 --- /dev/null +++ b/repos/effect/.gitignore @@ -0,0 +1,43 @@ +# Generated by Direnv +.direnv/ +.env + +# Generated by TypeScript +dist/ +build/ +**/*.tsbuildinfo + +# Generated by Pnpm +node_modules/ +.pnpm-store/ + +# Auto-generated from scripts +coverage/ +docs/ +tmp/ + +# Generated by MacOS +.DS_Store + +# scratchpad files +scratchpad/**/* + +# lalph +.lalph/ +.repos/ + +# ralph auto loop (runtime output) +.ralph-auto/ + +# Claude Code +.claude/ + +# OpenCode local tooling +.opencode/ +opencode.json + +# Repositories +.repos/ + +# oxlint data +.data/ diff --git a/repos/effect/.oxlintrc.json b/repos/effect/.oxlintrc.json new file mode 100644 index 0000000..645b341 --- /dev/null +++ b/repos/effect/.oxlintrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "extends": ["./packages/tools/oxc/oxlintrc.json"], + "ignorePatterns": [ + "LLMS.md", + "**/dist", + "**/build", + "**/docs", + "**/.tsbuildinfo", + "packages/effect/typeperf/**/*", + "packages/**/CHANGELOG.md", + "!scratchpad/**/*", + ".agents/**/*", + ".context/**/*", + ".specs/**/*" + ], + "jsPlugins": ["@effect/oxc/oxlint"], + "overrides": [{ + "files": ["**/{test,typetest,examples,ai-docs,benchmark,bundle,scripts,scratchpad}/**"], + "rules": { + "effect/no-bigint-literals": "off", + "eslint/no-console": "off", + "effect/no-import-from-barrel-package": "off" + } + }] +} diff --git a/repos/effect/.patterns/effect.md b/repos/effect/.patterns/effect.md new file mode 100644 index 0000000..a778ae8 --- /dev/null +++ b/repos/effect/.patterns/effect.md @@ -0,0 +1,102 @@ +# Effect Library Development Patterns + +## NEVER: try-catch in Effect.gen + +**REASON**: Effect generators handle errors through the Effect type system, not JavaScript exceptions. + +```typescript +// ❌ WRONG - This will cause runtime errors +Effect.gen(function*() { + try { + const result = yield* someEffect + return result + } catch (error) { + // This will never be reached and breaks Effect semantics + console.error(error) + } +}) + +// ✅ CORRECT - Use Effect's built-in error handling +Effect.gen(function*() { + const result = yield* Effect.result(someEffect) + if (result._tag === "Failure") { + // Handle error case properly + console.error("Effect failed:", result.cause) + return yield* Effect.fail("Handled error") + } + return result.value +}) +``` + +## return yield* Pattern for Errors + +**CRITICAL**: Always use `return yield*` when yielding terminal effects. + +```typescript +// ✅ CORRECT - Makes termination explicit +Effect.gen(function*() { + if (invalidCondition) { + return yield* Effect.fail("Validation failed") + } + + if (shouldInterrupt) { + return yield* Effect.interrupt + } + + // Continue with normal flow + const result = yield* someOtherEffect + return result +}) + +// ❌ WRONG - Missing return keyword leads to unreachable code +Effect.gen(function*() { + if (invalidCondition) { + yield* Effect.fail("Validation failed") // Missing return! + // Unreachable code after error! + } +}) +``` + +## `Effect.gen` and `Effect.fnUntraced` + +Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. + +```typescript +// ❌ AVOID - Function only wraps Effect.gen +const fn = (param: string) => + Effect.gen(function*() { + // ... + }) + +// ✅ PREFER - Reusable untraced Effect function +const fn = Effect.fnUntraced(function*(param: string) { + // ... +}) +``` + +## When to Use What + +**Use `Effect.gen`** when: + +- Writing inline effect composition +- One-off operations that don't need to be reused +- Inside other functions already being traced + +**Use `Effect.fnUntraced`** when: + +- Building library implementations +- Performance is critical (hot paths) +- Function is called many times per operation +- Tracing overhead is unacceptable + +## `Context.Service` + +Prefer the class syntax when working with `Context.Service`. + +```typescript +import { Context } from "effect" + +class MyService extends Context.Service number +}>()("MyService") {} +``` diff --git a/repos/effect/.patterns/jsdoc.md b/repos/effect/.patterns/jsdoc.md new file mode 100644 index 0000000..9f2377c --- /dev/null +++ b/repos/effect/.patterns/jsdoc.md @@ -0,0 +1,53 @@ +# JSDoc Patterns + +## `@category` Guidance + +When adding or vetting JSDoc categories in public source files: + +- Use exactly one `@category` tag for each public JSDoc block that represents a documented API. +- Use shared categories consistently across the repository. Domain-specific categories are allowed when they improve navigation within a file or package, but avoid one-off categories unless they name an important API/domain concept. +- Prefer lowercase category names by default, plural nouns for API buckets, and gerunds for operation families. +- Preserve canonical casing for acronyms and proper API/domain names, such as `type IDs`, `DateTime`, `Undici`, and `HttpAgent`. +- Prefer shared API-shape categories for common Effect/library patterns, and use domain-topic categories only when they provide clearer navigation. +- Avoid vague fallback categories. Do not use `utils`, `common`, or `misc`; pick a specific shared or domain category instead. + +## Common Shared Categories + +- API shapes: `constructors`, `destructors`, `models`, `schemas`, `guards`, `predicates`, `getters`, `accessors`, `instances`, `constants`, `protocols`, `prototypes`, `re-exports`, `unsafe`, `testing` +- Effect/service concepts: `services`, `tags`, `layers`, `context`, `resource management`, `running` +- Type-level APIs: `utility types` for type-level helpers/contracts; use `models` for exported type/interface/class shapes that represent domain data +- Error APIs: `errors` for error models/classes/types, `error handling` for recovery/catching/mapping APIs +- Operations: `combinators`, `filtering`, `mapping`, `sequencing`, `zipping`, `combining`, `merging`, `converting`, `transforming`, `folding`, `splitting`, `repetition` +- Encoding/data formats: `encoding`, `decoding`, `serialization` +- Observability: `tracing`, `metrics`, `logging` +- Other common concepts: `annotations`, `references`, `symbols`, `type IDs`, `configuration`, `math`, `comparisons`, `ordering` + +## Category Normalization + +Normalize category names before adding or reviewing JSDoc: + +- Lowercase plain category names. Preserve established acronyms and proper + names, such as `type IDs`, `DateTime`, `JSON getters`, `Base64 getters`, and + `Standard Schema`. +- Prefer shared plural buckets when the meaning is the same, such as + `constructors`, `models`, `schemas`, `guards`, `getters`, `services`, + `layers`, `generators`, `subscriptions`, `cookies`, and `sizes`. +- Prefer shared operation families over narrow synonyms when precision is not + important, such as `combining`, `mapping`, `filtering`, `folding`, + `converting`, `transforming`, `sequencing`, and `repetition`. +- Replace vague fallback categories such as `utils`, `common`, `misc`, or + `helpers` with a specific shared or domain category. +- Use `services` for `Context.Service` and `Context.Reference` exports, and + use `tags` only for `Context.Tag` exports. +- Fix obvious typos and compact variants during cleanup, such as + `transferables`, `re-exports`, `resource management`, and `Standard Schema`. + +## Distinctions + +Keep these distinctions: + +- `services` are `Context.Service` / `Context.Reference` exports and service contracts/shapes, `tags` are `Context.Tag` exports, and `layers` provide services. +- `getters` retrieve values/properties, while `accessors` are contextual service or environment access helpers. +- `errors` are error data types, while `error handling` is for APIs that handle failures. +- `models` describe domain/API data structures, while `schemas` are schema values/combinators and `utility types` are type-level helpers/contracts. +- `guards` are TypeScript type guards, `predicates` are boolean tests, and `filtering` is for filtering operations. diff --git a/repos/effect/.patterns/testing.md b/repos/effect/.patterns/testing.md new file mode 100644 index 0000000..af6cc49 --- /dev/null +++ b/repos/effect/.patterns/testing.md @@ -0,0 +1,44 @@ +# Testing Patterns + +## Testing Framework Selection + +Use `it.effect` for tests that return Effects. + +```typescript +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" + +it.effect("should work with Effects", () => + Effect.gen(function*() { + const result = yield* someEffect + assert.strictEqual(result, expectedValue) + })) +``` + +Use regular `it` for pure synchronous TypeScript functions. + +```typescript +import { assert, describe, it } from "@effect/vitest" + +it("should work with pure functions", () => { + const result = pureFunction(input) + assert.strictEqual(result, expectedValue) +}) +``` + +## Testing Rules + +- Never use `Effect.runSync` in tests +- Never use `expect` from Vitest; use `assert` methods instead +- Always use `TestClock` for time-dependent operations +- Group related tests using `describe` + +## Type-Level Tests + +Type-level tests are located in `packages/*/typetest/` and use Tstyche. + +Run targeted type-level tests with: + +```sh +pnpm test-types +``` diff --git a/repos/effect/.specs/README.md b/repos/effect/.specs/README.md new file mode 100644 index 0000000..c6abd99 --- /dev/null +++ b/repos/effect/.specs/README.md @@ -0,0 +1,2 @@ +# Specifications + diff --git a/repos/effect/.vscode/extensions.json b/repos/effect/.vscode/extensions.json new file mode 100644 index 0000000..1a118e9 --- /dev/null +++ b/repos/effect/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "oxc.oxc-vscode", + "dprint.dprint" + ] +} diff --git a/repos/effect/.vscode/settings.json b/repos/effect/.vscode/settings.json new file mode 100644 index 0000000..7e6c0d2 --- /dev/null +++ b/repos/effect/.vscode/settings.json @@ -0,0 +1,44 @@ +{ + "dprint.path": "node_modules/.bin/dprint", + "editor.formatOnSave": true, + "editor.defaultFormatter": "dprint.dprint", + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + }, + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": false + }, + "editor.acceptSuggestionOnCommitCharacter": true, + "editor.acceptSuggestionOnEnter": "on", + "editor.quickSuggestionsDelay": 10, + "editor.suggestOnTriggerCharacters": true, + "editor.tabCompletion": "off", + "editor.suggest.localityBonus": true, + "editor.suggestSelection": "recentlyUsed", + "editor.wordBasedSuggestions": "matchingDocuments", + "editor.parameterHints.enabled": true, + "files.insertFinalNewline": true, + "[typescript]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[javascript]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[json]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[jsonc]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[markdown]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "deno.enable": false, + "js/ts.tsdk.path": "node_modules/typescript/lib" +} diff --git a/repos/effect/AGENTS.md b/repos/effect/AGENTS.md new file mode 100644 index 0000000..9e0211b --- /dev/null +++ b/repos/effect/AGENTS.md @@ -0,0 +1,148 @@ +This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. + +## Overview + +- The git base branch is `main`. +- Use `pnpm` as the package manager. +- Keep changes focused and follow established patterns in the repository. +- Before writing code, read the relevant files in `./.patterns/` and inspect similar existing code. + +## Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: + +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: + +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: + +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: + +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: + +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +## Workflow + +1. Inspect nearby implementation, tests, and pattern docs before editing. +2. Prefer existing abstractions and conventions over introducing new ones. +3. For ad hoc runnable code, create a temporary file in `scratchpad/`, run it with `node scratchpad/.ts`, and delete it when done. + The local runtime is Node 24, which can run TypeScript files directly; use plain `node` for local TypeScript probes instead of `tsx` unless `node` fails. +4. Run the validation appropriate to the change type. +5. Report which validation commands were run and any commands that could not be run. + +## Validation + +Use the narrowest validation that still covers the change: + +| Change type | Validation | +| -------------------------------- | ---------------------------------------------------------------------------------- | +| Code changes | `pnpm lint-fix`, targeted `pnpm test `, `pnpm check` | +| Tests-only changes | `pnpm lint-fix`, targeted `pnpm test `, `pnpm check` | +| Type-level/API type changes | Targeted `pnpm test-types `, plus `pnpm check` when source types changed | +| JSDoc text/category/link changes | `pnpm lint` | +| JSDoc example changes | `pnpm lint`; from the changed package directory, run `pnpm docgen` | +| Docs-only changes | `pnpm lint-fix`; no tests required unless examples or code changed | + +## Bundle Size Preview + +When asked to show bundle-size impact for a commit, use the existing bundle comparison workflow: + +1. For the latest commit, run `pnpm bundle-compare HEAD~1`. + For another base, run `pnpm bundle-compare `. +2. Read the Markdown report from `tmp/bundle-stats.txt` and summarize the non-zero differences. +3. Leave `tmp/bundle-base` in place unless cleanup is requested. To clean it up, run `git worktree remove --force tmp/bundle-base`. + +## Coding Patterns + +Read `.patterns/effect.md` before changing Effect code. In particular: + +- Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. +- Prefer class syntax for `Context.Service`. +- Do not use `async` / `await` or `try` / `catch`; use Effect APIs such as `Effect.gen`, `Effect.fnUntraced`, and `Effect.tryPromise`. +- Do not use `Date.now` or `new Date`; use `Clock`, and use `TestClock` in tests. + +## Testing + +Read `.patterns/testing.md` before writing or changing tests. + +- Test files are located in `packages/*/test/`. +- Main Effect library tests are in `packages/effect/test/`. +- Use `it.effect` for Effect-returning tests. +- Use regular `it` for pure synchronous tests. +- Do not use `Effect.runSync` in tests. +- Do not use `expect` from Vitest; use `assert` from `@effect/vitest`. +- Type-level tests are in `packages/*/typetest/` and run with `pnpm test-types `. + +## Documentation + +- For AI documentation, read `ai-docs/README.md` very carefully before writing examples. +- AI documentation changes may include explanatory comments when useful. +- For public JSDoc `@category` guidance, read `.patterns/jsdoc.md`. +- When JSDoc examples are localized to a single package, run `pnpm docgen` from that package directory instead of the repository root. + +## Generated Files + +Do not hand-edit generated files. Run the appropriate generator instead. + +- `index.ts` barrel files are generated; run `pnpm codegen` after adding or removing modules. + +## Changesets + +Create a changeset in `.changeset/` for runtime behavior changes or exported type/API changes: + +```md +--- +"package-name": patch/minor/major +--- + +A description of the change. +``` + +Tests-only changes, internal refactors, docs-only changes, and JSDoc-only maintenance may skip changesets by maintainer decision. diff --git a/repos/effect/LICENSE b/repos/effect/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/LLMS.md b/repos/effect/LLMS.md new file mode 100644 index 0000000..2aac305 --- /dev/null +++ b/repos/effect/LLMS.md @@ -0,0 +1,383 @@ +# Effect library documentation + +This documentation resides in the Effect monorepo, which contains the source +code for the Effect library and its related packages. + +When you need to find any information about the Effect library, only use this +documentation and the source code found in `./packages`. Do not use +`node_modules` or any other external documentation, as it may be outdated or +incorrect. + +**Note**: The examples in this documentation contain comments for illustration +purposes. In practice, you would not include these comments in your code. + +## Writing `Effect` code + +Prefer writing Effect code with `Effect.gen` & `Effect.fn("name")`. Then attach +additional behaviour with combinators. This style is more readable and easier to +maintain than using combinators alone. + +### Using Effect.gen + +Use `Effect.gen` to write code in an imperative style similar to async await. +You can use `yield*` to access the result of an effect. + +```ts +import { Effect, Schema } from "effect" + +Effect.gen(function*() { + yield* Effect.log("Starting the file processing...") + yield* Effect.log("Reading file...") + + // Always return when raising an error, to ensure typescript understands that + // the function will not continue executing. + return yield* new FileProcessingError({ message: "Failed to read the file" }) +}).pipe( + // Add additional functionality with .pipe + Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), + Effect.withSpan("fileProcessing", { + attributes: { + method: "Effect.gen" + } + }) +) + +// Use Schema.TaggedErrorClass to define a custom error +export class FileProcessingError extends Schema.TaggedErrorClass()("FileProcessingError", { + message: Schema.String +}) {} +``` + +### Using Effect.fn + +When writing functions that return an Effect, use `Effect.fn` to use the +generator syntax. + +**Avoid creating functions that return an Effect.gen**, use `Effect.fn` +instead. + +```ts +import { Effect, Schema } from "effect" + +// Pass a string to Effect.fn, which will improve stack traces and also +// attach a tracing span (using Effect.withSpan behind the scenes). +// +// The name string should match the function name. +// +export const effectFunction = Effect.fn("effectFunction")( + // You can use `Effect.fn.Return` to specify the return type of the function. + // It accepts the same type parameters as `Effect.Effect`. + function*(n: number): Effect.fn.Return { + yield* Effect.logInfo("Received number:", n) + + // Always return when raising an error, to ensure typescript understands that + // the function will not continue executing. + return yield* new SomeError({ message: "Failed to read the file" }) + }, + // Add additional functionality by passing in additional arguments. + // **Do not** use .pipe with Effect.fn + Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), + Effect.annotateLogs({ + method: "effectFunction" + }) +) + +// Use Schema.TaggedErrorClass to define a custom error +export class SomeError extends Schema.TaggedErrorClass()("SomeError", { + message: Schema.String +}) {} +``` + +### More examples + +- **[Creating effects from common sources](./ai-docs/src/01_effect/01_basics/10_creating-effects.ts)**: + Learn how to create effects from various sources, including plain values, + synchronous code, Promise APIs, optional values, and callback-based APIs. + +## Defining schemas and domain models + +All validation and domain modeling in Effect is done with `Schema`. + +**AVOID using predicates or manual parsing**, instead use `Schema` to parse untrusted data and validate it. + +For a comprehensive guide, see [packages/effect/SCHEMA.md](./packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. + +- **[Schema basics](./ai-docs/src/01_effect/02_schema/10_schema-basics.ts)**: + Define `Schema.Class`s, decode unknown input into typed values, and + encode typed values back into their external representation. + +## Writing Effect services + +Effect services are the most common way to structure Effect code. Prefer using +services to encapsulate behaviour over other approaches, as it ensures that your +code is modular, testable, and maintainable. + +### Context.Service + +The default way to define a service is to extend `Context.Service`, +passing in the service interface as a type parameter. + +```ts +// file: src/db/Database.ts +import { Context, Effect, Layer, Schema } from "effect" + +// Pass in the service class name as the first type parameter, and the service +// interface as the second type parameter. +export class Database extends Context.Service, DatabaseError> +}>()( + // The string identifier for the service, which should include the package + // name and the subdirectory path to the service file. + "myapp/db/Database" +) { + // Attach a static layer to the service, which will be used to provide an + // implementation of the service. + static readonly layer = Layer.effect( + Database, + Effect.gen(function*() { + // Define the service methods using Effect.fn + const query = Effect.fn("Database.query")(function*(sql: string) { + yield* Effect.log("Executing SQL query:", sql) + return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }] + }) + + // Return an instance of the service using Database.of, passing in an + // object that implements the service interface. + return Database.of({ + query + }) + }) + ) +} + +export class DatabaseError extends Schema.TaggedErrorClass()("DatabaseError", { + cause: Schema.Defect() +}) {} + +// If you ever need to access the service type, use `Database["Service"]` +export type DatabaseService = Database["Service"] +``` + +### More examples + +- **[Context.Reference](./ai-docs/src/01_effect/03_services/10_reference.ts)**: For defining configuration values, feature flags, or any other service that has a default value. +- **[Composing services with the Layer module](./ai-docs/src/01_effect/03_services/20_layer-composition.ts)**: + Build focused service layers, then compose them with `Layer.provide` and + `Layer.provideMerge` based on what services you want to expose. +- **[Creating Layers from configuration and/or Effects](./ai-docs/src/01_effect/03_services/20_layer-unwrap.ts)**: Build a layer dynamically from an Effect / Config with `Layer.unwrap`. + +## Error handling + +### Error handling basics + +Defining custom errors and handling them with Effect.catch and Effect.catchTag. + +```ts +import { Effect, Schema } from "effect" + +// Define custom errors using Schema.TaggedErrorClass +export class ParseError extends Schema.TaggedErrorClass()("ParseError", { + input: Schema.String, + message: Schema.String +}) {} + +export class ReservedPortError extends Schema.TaggedErrorClass()("ReservedPortError", { + port: Schema.Number +}) {} + +declare const loadPort: (input: string) => Effect.Effect + +export const recovered = loadPort("80").pipe( + // Catch multiple errors with Effect.catchTag, and return a default port number. + Effect.catchTag(["ParseError", "ReservedPortError"], (_) => Effect.succeed(3000)) +) + +export const withFinalFallback = loadPort("invalid").pipe( + // Catch a specific error with Effect.catchTag + Effect.catchTag("ReservedPortError", (_) => Effect.succeed(3000)), + // Catch all errors with Effect.catch + Effect.catch((_) => Effect.succeed(3000)) +) +``` + +### More examples + +- **[Catch multiple errors with Effect.catchTags](./ai-docs/src/01_effect/04_errors/10_catch-tags.ts)**: Use `Effect.catchTags` to handle several tagged errors in one place. +- **[Creating and handling errors with reasons](./ai-docs/src/01_effect/04_errors/20_reason-errors.ts)**: + Define a tagged error with a tagged `reason` field, then recover with + `Effect.catchReason`, `Effect.catchReasons`, or by unwrapping the reason into + the error channel with `Effect.unwrapReason`. + +## Managing resources and `Scope`s + +Learn how to safely manage resources in Effect using `Scope`s and finalizers. + +- **[Acquiring resources with Effect.acquireRelease](./ai-docs/src/01_effect/05_resources/10_acquire-release.ts)**: + Define a service that uses `Effect.acquireRelease` to manage the lifecycle of + a resource, ensuring that it is properly cleaned up when the service is no + longer needed. +- **[Creating Layers that run background tasks](./ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts)**: Use Layer.effectDiscard to encapsulate background tasks without a service interface. +- **[Dynamic resources with LayerMap](./ai-docs/src/01_effect/05_resources/30_layer-map.ts)**: + Use `LayerMap.Service` to dynamically build and manage resources that are + keyed by some identifier, such as a tenant ID. + +## Running Effect programs + +- **[Running effects with NodeRuntime and BunRuntime](./ai-docs/src/01_effect/06_running/10_run-main.ts)**: Use `NodeRuntime.runMain` to run an Effect program as your process entrypoint. +- **[Using Layer.launch as the application entry point](./ai-docs/src/01_effect/06_running/20_layer-launch.ts)**: Use `Layer.launch` to run a long-running Effect program as your process entrypoint. + +## Broadcasting messages with PubSub + +Use `PubSub` when you need one producer to fan out messages to many consumers. + +- **[Broadcasting domain events with PubSub](./ai-docs/src/01_effect/07_pubsub/10_pubsub.ts)**: Build an in-process event bus with `PubSub` and expose it as a service. + +## Working with Streams + +Effect Streams represent effectful, pull-based sequences of values over time. +They let you model finite or infinite data sources. + +- **[Creating streams from common data sources](./ai-docs/src/03_stream/10_creating-streams.ts)**: + Learn how to create streams from various data sources. Includes: + + - `Stream.fromIterable` for arrays and other iterables + - `Stream.fromEffectSchedule` for polling effects + - `Stream.paginate` for paginated APIs + - `Stream.fromAsyncIterable` for async iterables + - `Stream.fromEventListener` for DOM events + - `Stream.callback` for any callback-based API + - `NodeStream.fromReadable` for Node.js readable streams +- **[Consuming and transforming streams](./ai-docs/src/03_stream/20_consuming-streams.ts)**: How to transform and consume streams using operators like `map`, `flatMap`, `filter`, `mapEffect`, and various `run*` methods. +- **[Decoding and encoding streams](./ai-docs/src/03_stream/30_encoding.ts)**: + Use `Stream.pipeThroughChannel` with the `Ndjson` & `Msgpack` modules to + decode and encode streams of structured data. + +## Integrating Effect into existing applications + +`ManagedRuntime` bridges Effect programs with non-Effect code. Build one runtime +from your application Layer, then use it anywhere you need imperative execution, +like web handlers, framework hooks, worker queues, or legacy callback APIs. + +- **[Using ManagedRuntime with Hono](./ai-docs/src/04_integration/10_managed-runtime.ts)**: Use `ManagedRuntime` to run Effect programs from external frameworks while keeping your domain logic in services and Layers. + +## Batching external requests + +Learn how to batch multiple requests into fewer external calls. + +- **[Batching requests with RequestResolver](./ai-docs/src/05_batching/10_request-resolver.ts)**: Define request types with `Request.Class`, resolve them in batches with `RequestResolver`. + +## Working with Schedules + +Schedules define recurring patterns for retries, repeats and polling. + +- **[Working with the Schedule module](./ai-docs/src/06_schedule/10_schedules.ts)**: Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`. + +## Working with DateTime + +When working with dates and time, use the `DateTime` module instead of `Date` and `Date.now`. + +Use it when your Effect programs need testable current time, safe parsing, stable ISO formatting, time-zone conversion, or calendar arithmetic. + +- **[Creating and formatting DateTime values](./ai-docs/src/07_datetime/10_creating-and-formatting.ts)**: + Parse incoming date values safely, use Clock-powered current time, and format + instants for API payloads or user-facing labels. +- **[Working with time zones](./ai-docs/src/07_datetime/20_time-zones.ts)**: + Attach IANA zones to instants, render zoned ISO strings, and provide a + CurrentTimeZone service for code that should use the workspace/user zone. + +## Observability + +Effect has built-in support for structured logging, distributed tracing, and +metrics. For exporting telemetry, use the lightweight Otlp modules from +`effect/unstable/observability` in new projects, or use +`@effect/opentelemetry` NodeSdk when integrating with an existing OpenTelemetry +setup. + +- **[Customizing logging](./ai-docs/src/08_observability/10_logging.ts)**: Configure loggers & log-level filtering for production applications. +- **[Setting up tracing with Otlp modules](./ai-docs/src/08_observability/20_otlp-tracing.ts)**: Configure Otlp tracing + log export with a reusable observability layer. + +## Testing Effect programs + +- **[Writing Effect tests with @effect/vitest](./ai-docs/src/09_testing/10_effect-tests.ts)**: Using `it.effect` for Effect-based tests. +- **[Testing services with shared layers](./ai-docs/src/09_testing/20_layer-tests.ts)**: How to test Effect services that depend on other services. + +## Runtime type guards + +The `Predicate` module contains small, reusable runtime checks. + +**NEVER** write your own helper functions like `isRecord` or `isString`, instead +use the helpers from the `Predicate` module. + +Predicates can be composed with apis such as `Predicate.and`, +`Predicate.or`, `Predicate.not`, and `Predicate.compose`. + +### Using the Predicate module + + + +```ts +import { Predicate } from "effect" + +const thing: unknown = { + a: 1 +} + +if (Predicate.isObject(thing)) { + if (Predicate.isNumber(thing.a)) { + console.log("number", thing.a) + } +} +``` + +## Effect HttpClient + +Build http clients with the `HttpClient` module. + +- **[Getting started with HttpClient](./ai-docs/src/50_http-client/10_basics.ts)**: Define a service that uses the HttpClient module to fetch data from an external API + +## Building HttpApi servers + +`HttpApi` gives you schema-first, type-safe HTTP APIs with runtime validation, typed clients, and OpenAPI docs from one definition. + +- **[Getting started with HttpApi](./ai-docs/src/51_http-server/10_basics.ts)**: + Define a schema-first API, implement handlers, secure endpoints with + middleware, serve it over HTTP, and call it using a generated typed client. + +## Working with child processes + +Use the `effect/unstable/process` modules to define child processes and run them with `ChildProcessSpawner`. + +- **[Working with child processes](./ai-docs/src/60_child-process/10_working-with-child-processes.ts)**: This example shows how to collect process output, compose pipelines, and stream long-running command output. + +## Building CLI applications + +Use the "effect/unstable/cli" modules to build CLI applications. These modules +provide utilities for parsing command-line arguments, handling user input, and +managing the flow of a CLI application. + +- **[Getting started with Effect CLI modules](./ai-docs/src/70_cli/10_basics.ts)**: + Build a command-line app with typed arguments and flags, then wire subcommand + handlers into a single executable command. + +## Working with AI modules + +Effect's AI modules provide a provider-agnostic interface for language models. +You can generate text, decode structured objects with `Schema` and stream partial +responses. + +- **[Using LanguageModel for text, objects, and streams](./ai-docs/src/71_ai/10_language-model.ts)**: + Configure a provider once, then use `LanguageModel` for plain text + generation, schema-validated object generation, and streaming responses. +- **[Defining and using AI tools](./ai-docs/src/71_ai/20_tools.ts)**: + Define tools with schemas, group them into toolkits, implement handlers, + and pass them to `LanguageModel.generateText`. +- **[Stateful chat sessions](./ai-docs/src/71_ai/30_chat.ts)**: + The AI `Chat` module maintains conversation history automatically. Build + AI agents or chat assistants. + +## Building distributed applications with cluster + +The cluster modules let you model stateful services as entities and distribute +them across multiple machines. + +- **[Defining cluster entities](./ai-docs/src/80_cluster/10_entities.ts)**: Define distributed entity RPCs and run them in a cluster. \ No newline at end of file diff --git a/repos/effect/MIGRATION.md b/repos/effect/MIGRATION.md new file mode 100644 index 0000000..902888a --- /dev/null +++ b/repos/effect/MIGRATION.md @@ -0,0 +1,83 @@ +# Migrating from Effect v3 to Effect v4 + +> **Note:** Effect v4 is currently in beta. APIs may change between beta +> releases. This guide will evolve as the beta progresses and community +> feedback is incorporated. + +## Background + +Effect v4 is a major release with structural and organizational changes across +the ecosystem. The core programming model — `Effect`, `Layer`, `Schema`, +`Stream`, etc. — remains the same, but how packages are organized, versioned, +and imported has changed significantly. + +### Versioning + +All Effect ecosystem packages now share a **single version number** and are +released together. In v3, packages were versioned independently (e.g. +`effect@3.x`, `@effect/platform@0.x`, `@effect/sql@0.x`), making compatibility +between packages difficult to track. In v4, if you use `effect@4.0.0-beta.0`, +the matching SQL package is `@effect/sql-pg@4.0.0-beta.0`. + +### Package Consolidation + +Many previously separate packages have been merged into the core `effect` +package. Functionality from `@effect/platform`, `@effect/rpc`, +`@effect/cluster`, and others now lives directly in `effect`. + +Packages that remain separate are platform-specific, provider-specific, or +technology-specific: + +- `@effect/platform-*` — platform packages +- `@effect/sql-*` — SQL driver packages +- `@effect/ai-*` — AI provider packages +- `@effect/opentelemetry` — OpenTelemetry integration +- `@effect/atom-*` — framework-specific atom bindings +- `@effect/vitest` — Vitest testing utilities + +These packages must be bumped to matching v4 beta versions alongside `effect`. + +### Unstable Module System + +v4 introduces **unstable modules** under `effect/unstable/*` import paths. +These modules may receive breaking changes in minor releases, while modules +outside `unstable/` follow strict semver. + +Unstable modules include: `ai`, `cli`, `cluster`, `devtools`, `eventlog`, +`http`, `httpapi`, `jsonschema`, `observability`, `persistence`, `process`, +`reactivity`, `rpc`, `schema`, `socket`, `sql`, `workflow`, `workers`. + +As these modules stabilize, they graduate to the top-level `effect/*` namespace. + +### Performance and Bundle Size + +The fiber runtime has been rewritten for reduced memory overhead and faster +execution. The core `effect` package supports aggressive tree-shaking — a +minimal Effect program bundles to ~6.3 KB (minified + gzipped). With Schema, +~15 KB. + +--- + +## Migration Guides + +### Import and API Rename Maps + +- [v3 to v4 Import and API Rename Maps](./migration/v3-to-v4.md) + +### Core + +- [Services: `Context.Tag` → `Context.Service`](./migration/services.md) +- [Cause: Flattened Structure](./migration/cause.md) +- [Error Handling: `catch*` Renamings](./migration/error-handling.md) +- [Forking: Renamed Combinators and New Options](./migration/forking.md) +- [Effect Subtyping → Yieldable](./migration/yieldable.md) +- [Fiber Keep-Alive: Automatic Process Lifetime Management](./migration/fiber-keep-alive.md) +- [Layer Memoization Across `Effect.provide` Calls](./migration/layer-memoization.md) +- [FiberRef: `FiberRef` → `Context.Reference`](./migration/fiberref.md) +- [Runtime: `Runtime` Removed](./migration/runtime.md) +- [Scope](./migration/scope.md) +- [Equality](./migration/equality.md) + +### Modules + +- [Schema v4 Migration Guide](./migration/schema.md) diff --git a/repos/effect/README.md b/repos/effect/README.md new file mode 100644 index 0000000..efdb0b1 --- /dev/null +++ b/repos/effect/README.md @@ -0,0 +1,36 @@ + + +[![pkg.pr.new](https://img.shields.io/badge/pkg.pr.new-Effect--TS%2Feffect-black)](https://pkg.pr.new/~/Effect-TS/effect) + +# Effect + +Effect is a library for building robust, maintainable, type-safe, and production grade applications in TypeScript. + +> **Effect V4 is currently in beta.** The `main` branch contains v4 development. + +## Install V4 Beta + +```sh +npm install effect@beta +``` + +## Effect v3 + +The Effect v3 source code is available on the [`v3`](https://github.com/Effect-TS/effect/tree/v3) branch. + +```sh +npm install effect@latest +``` + +Issues and pull requests meant for Effect v3 should target the [`v3`](https://github.com/Effect-TS/effect/tree/v3) branch. + +## Resources + +- Documentation (https://effect.website) +- Discord (https://discord.gg/effect-ts) +- Effect v3 source (https://github.com/Effect-TS/effect/tree/v3) +- Effect v4 source (https://github.com/Effect-TS/effect/tree/main) + +## License + +MIT diff --git a/repos/effect/ai-docs/README.md b/repos/effect/ai-docs/README.md new file mode 100644 index 0000000..dd4488c --- /dev/null +++ b/repos/effect/ai-docs/README.md @@ -0,0 +1,44 @@ +# AI docs + +`LLMS.md` is generated from `ai-docs/src`. + +## Add content + +1. Add or update markdown in `ai-docs/src/**/index.md` for section intro text. +2. Add examples as `.ts` files in the same folder. +3. Run `pnpm ai-docgen` to regenerate `LLMS.md`. + +## Source file conventions + +- Use numeric filename prefixes to control ordering (`10_`, `20_`, etc). Avoid starting with `0` unless explicity requested to do so. +- Use a top JSDoc block with `@title` and optional description to control rendered title/description. +- `fixtures` directories are ignored and not included in the generated + documentation. Use them for any supporting code or data needed for examples. + +## Example guidelines + +Before writing an example, look at the existing examples in `ai-docs/src` to +learn the style and conventions used in this project. Also read the current +`LLMS.md` to understand the content and style of the documentation. + +**All code examples should be well commented** explaining the how and why of the +code, not just what the code is doing. The goal is to teach users how to use the +API. + +**Code must represent real world usage and best practices.** +Do not include toy examples that are not representative of how the API should be +used in practice. + +- **Prefer using the service style** of structuring code, as this represents real + world usage and best practices. +- Use the `fixtures` directory to illustrate / suggest best practices for + project structure, file organization, and code organization. + +Pull requests with only ai documentation changes **DO NOT** need a changeset. + +## Regeneration + +- One-shot: `pnpm ai-docgen` +- Watch mode: `pnpm ai-docgen:watch` + +`pnpm ai-docgen` regenerates `LLMS.md` files from content in `ai-docs/src`. diff --git a/repos/effect/ai-docs/package.json b/repos/effect/ai-docs/package.json new file mode 100644 index 0000000..10fef9d --- /dev/null +++ b/repos/effect/ai-docs/package.json @@ -0,0 +1,36 @@ +{ + "name": "ai-docs", + "private": true, + "type": "module", + "version": "0.0.0", + "dependencies": { + "@effect/ai-anthropic": "workspace:*", + "@effect/ai-openai": "workspace:*", + "@effect/ai-openai-compat": "workspace:*", + "@effect/ai-openrouter": "workspace:*", + "@effect/atom-react": "workspace:*", + "@effect/atom-solid": "workspace:*", + "@effect/atom-vue": "workspace:*", + "@effect/opentelemetry": "workspace:*", + "@effect/platform-bun": "workspace:*", + "@effect/platform-node": "workspace:*", + "@effect/sql-clickhouse": "workspace:*", + "@effect/sql-d1": "workspace:*", + "@effect/sql-libsql": "workspace:*", + "@effect/sql-mssql": "workspace:*", + "@effect/sql-mysql2": "workspace:*", + "@effect/sql-pg": "workspace:*", + "@effect/sql-sqlite-bun": "workspace:*", + "@effect/sql-sqlite-do": "workspace:*", + "@effect/sql-sqlite-node": "workspace:*", + "@effect/sql-sqlite-react-native": "workspace:*", + "@effect/sql-sqlite-wasm": "workspace:*", + "@effect/vitest": "workspace:*", + "effect": "workspace:*", + "hono": "^4.12.28", + "nodemailer": "^9.0.3" + }, + "devDependencies": { + "@types/nodemailer": "^8.0.1" + } +} diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts b/repos/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts new file mode 100644 index 0000000..4e45a89 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts @@ -0,0 +1,30 @@ +/** + * @title Using Effect.gen + * + * Use `Effect.gen` to write code in an imperative style similar to async await. + * You can use `yield*` to access the result of an effect. + */ + +import { Effect, Schema } from "effect" + +Effect.gen(function*() { + yield* Effect.log("Starting the file processing...") + yield* Effect.log("Reading file...") + + // Always return when raising an error, to ensure typescript understands that + // the function will not continue executing. + return yield* new FileProcessingError({ message: "Failed to read the file" }) +}).pipe( + // Add additional functionality with .pipe + Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), + Effect.withSpan("fileProcessing", { + attributes: { + method: "Effect.gen" + } + }) +) + +// Use Schema.TaggedErrorClass to define a custom error +export class FileProcessingError extends Schema.TaggedErrorClass()("FileProcessingError", { + message: Schema.String +}) {} diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts b/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts new file mode 100644 index 0000000..dcbfe6d --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts @@ -0,0 +1,39 @@ +/** + * @title Using Effect.fn + * + * When writing functions that return an Effect, use `Effect.fn` to use the + * generator syntax. + * + * **Avoid creating functions that return an Effect.gen**, use `Effect.fn` + * instead. + */ + +import { Effect, Schema } from "effect" + +// Pass a string to Effect.fn, which will improve stack traces and also +// attach a tracing span (using Effect.withSpan behind the scenes). +// +// The name string should match the function name. +// +export const effectFunction = Effect.fn("effectFunction")( + // You can use `Effect.fn.Return` to specify the return type of the function. + // It accepts the same type parameters as `Effect.Effect`. + function*(n: number): Effect.fn.Return { + yield* Effect.logInfo("Received number:", n) + + // Always return when raising an error, to ensure typescript understands that + // the function will not continue executing. + return yield* new SomeError({ message: "Failed to read the file" }) + }, + // Add additional functionality by passing in additional arguments. + // **Do not** use .pipe with Effect.fn + Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)), + Effect.annotateLogs({ + method: "effectFunction" + }) +) + +// Use Schema.TaggedErrorClass to define a custom error +export class SomeError extends Schema.TaggedErrorClass()("SomeError", { + message: Schema.String +}) {} diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts b/repos/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts new file mode 100644 index 0000000..86082db --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts @@ -0,0 +1,74 @@ +/** + * @title Creating effects from common sources + * + * Learn how to create effects from various sources, including plain values, + * synchronous code, Promise APIs, optional values, and callback-based APIs. + */ +import { Effect, Schema } from "effect" + +class InvalidPayload extends Schema.TaggedErrorClass()("InvalidPayload", { + input: Schema.String, + cause: Schema.Defect() +}) {} + +class UserLookupError extends Schema.TaggedErrorClass()("UserLookupError", { + userId: Schema.Number, + cause: Schema.Defect() +}) {} + +class MissingWorkspaceId extends Schema.TaggedErrorClass()("MissingWorkspaceId", {}) {} + +// Some request fields are optional and may be absent. +const requestHeaders = new Map([ + ["x-request-id", "req_1"] +]) + +// `Effect.succeed` wraps values you already have in memory. +export const fromValue = Effect.succeed({ env: "prod", retries: 3 }) + +// `Effect.sync` wraps synchronous side effects that should not throw. +export const fromSyncSideEffect = Effect.sync(() => Date.now()) + +// `Effect.try` wraps synchronous code that may throw. +export const parsePayload = Effect.fn("parsePayload")((input: string) => + Effect.try({ + try: () => JSON.parse(input) as { readonly userId: number }, + catch: (cause) => new InvalidPayload({ input, cause }) + }) +) + +const users = new Map([ + [1, { id: 1, name: "Ada" }], + [2, { id: 2, name: "Lin" }] +]) + +// `Effect.tryPromise` wraps Promise-based APIs that can reject or throw. +export const fetchUser = Effect.fn("fetchUser")((userId: number) => + Effect.tryPromise({ + async try() { + const user = users.get(userId) + if (!user) { + throw new Error(`Missing user ${userId}`) + } + return user + }, + catch: (cause) => new UserLookupError({ userId, cause }) + }) +) + +// `Effect.fromNullishOr` turns nullable values into a typed effect. +export const fromNullishHeader = Effect.fromNullishOr(requestHeaders.get("x-workspace-id")).pipe( + Effect.mapError(() => new MissingWorkspaceId()) +) + +// `Effect.callback` wraps callback-style asynchronous APIs. +export const fromCallback = Effect.callback((resume) => { + const timeoutId = setTimeout(() => { + resume(Effect.succeed(200)) + }, 10) + + // Return a finalizer so interruption can cancel the callback source. + return Effect.sync(() => { + clearTimeout(timeoutId) + }) +}) diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/index.md b/repos/effect/ai-docs/src/01_effect/01_basics/index.md new file mode 100644 index 0000000..ae13d7a --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/01_basics/index.md @@ -0,0 +1,5 @@ +## Writing `Effect` code + +Prefer writing Effect code with `Effect.gen` & `Effect.fn("name")`. Then attach +additional behaviour with combinators. This style is more readable and easier to +maintain than using combinators alone. diff --git a/repos/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts b/repos/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts new file mode 100644 index 0000000..35b6c5c --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts @@ -0,0 +1,43 @@ +/** + * @title Schema basics + * + * Define `Schema.Class`s, decode unknown input into typed values, and + * encode typed values back into their external representation. + */ +import { Effect, Schema } from "effect" + +// Schema.Class defines both a runtime validator and a TypeScript class. +// This is useful for domain models that should only be constructed from valid +// data. +// +// The static `Type` and `Encoded` members are available when you need +// the decoded or encoded TypeScript representation. +export class User extends Schema.Class("path/to/module/User")({ + id: Schema.Number, + name: Schema.NonEmptyString, + email: Schema.String, + role: Schema.Literals(["admin", "member"]) +}) {} + +// `UserType` will be the type `User`, as schema classes use the class type as +// the validated type. +export type UserType = typeof User["Type"] + +// Access the encoded type with `typeof YourSchema["Encoded"]`. +export type UserEncoded = typeof User["Encoded"] + +// Reuse parsers at the edges of your application instead of rebuilding them for +// every request. Use the Effect-returning APIs when you are already inside +// Effect code so validation errors remain typed in the error channel. +export const decodeUser = Schema.decodeUnknownEffect(User) +export const encodeUser = Schema.encodeEffect(User) + +export class InvalidUserPayload extends Schema.TaggedErrorClass()("InvalidUserPayload", { + message: Schema.String +}) {} + +export const parseUserPayload = Effect.fn("parseUserPayload")((input: unknown) => + decodeUser(input).pipe( + Effect.mapError((error) => new InvalidUserPayload({ message: error.message })) + ) +) diff --git a/repos/effect/ai-docs/src/01_effect/02_schema/index.md b/repos/effect/ai-docs/src/01_effect/02_schema/index.md new file mode 100644 index 0000000..7819e4f --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/02_schema/index.md @@ -0,0 +1,7 @@ +## Defining schemas and domain models + +All validation and domain modeling in Effect is done with `Schema`. + +**AVOID using predicates or manual parsing**, instead use `Schema` to parse untrusted data and validate it. + +For a comprehensive guide, see [packages/effect/SCHEMA.md](./packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. diff --git a/repos/effect/ai-docs/src/01_effect/03_services/01_service.ts b/repos/effect/ai-docs/src/01_effect/03_services/01_service.ts new file mode 100644 index 0000000..24db9b0 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/03_services/01_service.ts @@ -0,0 +1,45 @@ +/** + * @title Context.Service + * + * The default way to define a service is to extend `Context.Service`, + * passing in the service interface as a type parameter. + */ + +// file: src/db/Database.ts +import { Context, Effect, Layer, Schema } from "effect" + +// Pass in the service class name as the first type parameter, and the service +// interface as the second type parameter. +export class Database extends Context.Service, DatabaseError> +}>()( + // The string identifier for the service, which should include the package + // name and the subdirectory path to the service file. + "myapp/db/Database" +) { + // Attach a static layer to the service, which will be used to provide an + // implementation of the service. + static readonly layer = Layer.effect( + Database, + Effect.gen(function*() { + // Define the service methods using Effect.fn + const query = Effect.fn("Database.query")(function*(sql: string) { + yield* Effect.log("Executing SQL query:", sql) + return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }] + }) + + // Return an instance of the service using Database.of, passing in an + // object that implements the service interface. + return Database.of({ + query + }) + }) + ) +} + +export class DatabaseError extends Schema.TaggedErrorClass()("DatabaseError", { + cause: Schema.Defect() +}) {} + +// If you ever need to access the service type, use `Database["Service"]` +export type DatabaseService = Database["Service"] diff --git a/repos/effect/ai-docs/src/01_effect/03_services/10_reference.ts b/repos/effect/ai-docs/src/01_effect/03_services/10_reference.ts new file mode 100644 index 0000000..17d71d9 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/03_services/10_reference.ts @@ -0,0 +1,10 @@ +/** + * @title Context.Reference + * + * For defining configuration values, feature flags, or any other service that has a default value. + */ +import { Context } from "effect" + +export const FeatureFlag = Context.Reference("myapp/FeatureFlag", { + defaultValue: () => false +}) diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts new file mode 100644 index 0000000..58ff127 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts @@ -0,0 +1,70 @@ +/** + * @title Composing services with the Layer module + * + * Build focused service layers, then compose them with `Layer.provide` and + * `Layer.provideMerge` based on what services you want to expose. + */ + +import { PgClient } from "@effect/sql-pg" +import { Array, Config, Context, Effect, Layer, type Option, Schema } from "effect" +import { SqlClient, SqlError } from "effect/unstable/sql" + +// Define a layer for the SqlClient service +export const SqlClientLayer: Layer.Layer< + PgClient.PgClient | SqlClient.SqlClient, + Config.ConfigError | SqlError.SqlError +> = PgClient.layerConfig({ + url: Config.redacted("DATABASE_URL") +}) + +export class UserRespositoryError extends Schema.TaggedErrorClass()("UserRespositoryError", { + reason: SqlError.SqlError +}) {} + +export class UserRepository extends Context.Service, + UserRespositoryError + > +}>()("myapp/UserRepository") { + // Implement the layer for the UserRepository service, which depends on the + // SqlClient service + static readonly layerNoDeps: Layer.Layer< + UserRepository, + never, + SqlClient.SqlClient + > = Layer.effect( + UserRepository, + Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + + const findById = Effect.fn("UserRepository.findById")(function*(id: string) { + const results = yield* sql<{ + readonly id: string + readonly name: string + }>`SELECT * FROM users WHERE id = '${id}'` + return Array.head(results) + }, Effect.mapError((reason) => new UserRespositoryError({ reason }))) + + return UserRepository.of({ findById }) + }) + ) + + // Use Layer.provide to compose the UserRepository layer with the SqlClient + // layer, exposing only the UserRepository service + static readonly layer: Layer.Layer< + UserRepository, + Config.ConfigError | SqlError.SqlError + > = this.layerNoDeps.pipe( + Layer.provide(SqlClientLayer) + ) + + // Use Layer.provideMerge to compose the UserRepository layer with the SqlClient + // layer, exposing both the UserRepository and SqlClient services + static readonly layerWithSqlClient: Layer.Layer< + UserRepository | SqlClient.SqlClient, + Config.ConfigError | SqlError.SqlError + > = this.layerNoDeps.pipe( + Layer.provideMerge(SqlClientLayer) + ) +} diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts new file mode 100644 index 0000000..01324ac --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts @@ -0,0 +1,66 @@ +/** + * @title Creating Layers from configuration and/or Effects + * + * Build a layer dynamically from an Effect / Config with `Layer.unwrap`. + */ +import { Config, Context, Effect, Layer, Schema } from "effect" + +export class MessageStoreError extends Schema.TaggedErrorClass()("MessageStoreError", { + cause: Schema.Defect() +}) {} + +export class MessageStore extends Context.Service + readonly all: Effect.Effect> +}>()("myapp/MessageStore") { + static readonly layerInMemory = Layer.effect( + MessageStore, + Effect.sync(() => { + const messages: Array = [] + + return MessageStore.of({ + append: (message) => + Effect.sync(() => { + messages.push(message) + }), + all: Effect.sync(() => [...messages]) + }) + }) + ) + + static readonly layerRemote = (url: URL) => + Layer.effect( + MessageStore, + Effect.try({ + try: () => { + // In a real app this is where you would open a network connection. + const messages: Array = [] + + return MessageStore.of({ + append: (message) => + Effect.sync(() => { + messages.push(`[${url.host}] ${message}`) + }), + all: Effect.sync(() => [...messages]) + }) + }, + catch: (cause) => new MessageStoreError({ cause }) + }) + ) + + static readonly layer = Layer.unwrap( + Effect.gen(function*() { + // Read config inside an Effect, then choose which concrete layer to use. + const useInMemory = yield* Config.boolean("MESSAGE_STORE_IN_MEMORY").pipe( + Config.withDefault(false) + ) + + if (useInMemory) { + return MessageStore.layerInMemory + } + + const remoteUrl = yield* Config.url("MESSAGE_STORE_URL") + return MessageStore.layerRemote(remoteUrl) + }) + ) +} diff --git a/repos/effect/ai-docs/src/01_effect/03_services/index.md b/repos/effect/ai-docs/src/01_effect/03_services/index.md new file mode 100644 index 0000000..ac938fc --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/03_services/index.md @@ -0,0 +1,5 @@ +## Writing Effect services + +Effect services are the most common way to structure Effect code. Prefer using +services to encapsulate behaviour over other approaches, as it ensures that your +code is modular, testable, and maintainable. diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts b/repos/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts new file mode 100644 index 0000000..c507118 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts @@ -0,0 +1,30 @@ +/** + * @title Error handling basics + * + * Defining custom errors and handling them with Effect.catch and Effect.catchTag. + */ +import { Effect, Schema } from "effect" + +// Define custom errors using Schema.TaggedErrorClass +export class ParseError extends Schema.TaggedErrorClass()("ParseError", { + input: Schema.String, + message: Schema.String +}) {} + +export class ReservedPortError extends Schema.TaggedErrorClass()("ReservedPortError", { + port: Schema.Number +}) {} + +declare const loadPort: (input: string) => Effect.Effect + +export const recovered = loadPort("80").pipe( + // Catch multiple errors with Effect.catchTag, and return a default port number. + Effect.catchTag(["ParseError", "ReservedPortError"], (_) => Effect.succeed(3000)) +) + +export const withFinalFallback = loadPort("invalid").pipe( + // Catch a specific error with Effect.catchTag + Effect.catchTag("ReservedPortError", (_) => Effect.succeed(3000)), + // Catch all errors with Effect.catch + Effect.catch((_) => Effect.succeed(3000)) +) diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts b/repos/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts new file mode 100644 index 0000000..020473d --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts @@ -0,0 +1,24 @@ +/** + * @title Catch multiple errors with Effect.catchTags + * + * Use `Effect.catchTags` to handle several tagged errors in one place. + */ + +import { Effect, Schema } from "effect" + +export class ValidationError extends Schema.TaggedErrorClass()("ValidationError", { + message: Schema.String +}) {} + +export class NetworkError extends Schema.TaggedErrorClass()("NetworkError", { + statusCode: Schema.Number +}) {} + +declare const fetchUser: (id: string) => Effect.Effect + +export const userOrFallback = fetchUser("123").pipe( + Effect.catchTags({ + ValidationError: (error) => Effect.succeed(`Validation failed: ${error.message}`), + NetworkError: (error) => Effect.succeed(`Network request failed with status ${error.statusCode}`) + }) +) diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts b/repos/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts new file mode 100644 index 0000000..bced427 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts @@ -0,0 +1,64 @@ +/** + * @title Creating and handling errors with reasons + * + * Define a tagged error with a tagged `reason` field, then recover with + * `Effect.catchReason`, `Effect.catchReasons`, or by unwrapping the reason into + * the error channel with `Effect.unwrapReason`. + */ + +import { Effect, Schema } from "effect" + +export class RateLimitError extends Schema.TaggedErrorClass()("RateLimitError", { + retryAfter: Schema.Number +}) {} + +export class QuotaExceededError extends Schema.TaggedErrorClass()("QuotaExceededError", { + limit: Schema.Number +}) {} + +export class SafetyBlockedError extends Schema.TaggedErrorClass()("SafetyBlockedError", { + category: Schema.String +}) {} + +export class AiError extends Schema.TaggedErrorClass()("AiError", { + reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError]) +}) {} + +declare const callModel: Effect.Effect + +export const handleOneReason = callModel.pipe( + // Use `Effect.catchReason` to handle a specific reason type + Effect.catchReason( + "AiError", // The parent error _tag to catch + "RateLimitError", // The reason _tag to catch + // The handler for the caught reason + (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`), + // Optionally handle all the other reasons with a catch-all handler + (reason) => Effect.succeed(`Model call failed for reason: ${reason._tag}`) + ) +) + +export const handleMultipleReasons = callModel.pipe( + // Use `Effect.catchReasons` to handle multiple reason types for a given error + // in one go + Effect.catchReasons( + "AiError", + { + RateLimitError: (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`), + QuotaExceededError: (reason) => Effect.succeed(`Quota exceeded at ${reason.limit} tokens`) + } + // Optionally handle all the other reasons with a catch-all handler + // (reason) => Effect.succeed(`Unhandled reason: ${reason._tag}`) + ) +) + +export const unwrapAndHandle = callModel.pipe( + // Use `Effect.unwrapReason` to move the reasons into the error channel, then + // handle them all with `Effect.catchTags` or other error handling combinators + Effect.unwrapReason("AiError"), + Effect.catchTags({ + RateLimitError: (reason) => Effect.succeed(`Back off for ${reason.retryAfter} seconds`), + QuotaExceededError: (reason) => Effect.succeed(`Increase quota beyond ${reason.limit}`), + SafetyBlockedError: (reason) => Effect.succeed(`Blocked by safety category: ${reason.category}`) + }) +) diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/index.md b/repos/effect/ai-docs/src/01_effect/04_errors/index.md new file mode 100644 index 0000000..14d1614 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/04_errors/index.md @@ -0,0 +1 @@ +## Error handling diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts b/repos/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts new file mode 100644 index 0000000..afff2b1 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts @@ -0,0 +1,105 @@ +/** + * @title Acquiring resources with Effect.acquireRelease + * + * Define a service that uses `Effect.acquireRelease` to manage the lifecycle of + * a resource, ensuring that it is properly cleaned up when the service is no + * longer needed. + */ +import { Config, Context, Effect, Layer, Redacted, Schema } from "effect" +import * as NodeMailer from "nodemailer" + +export class SmtpError extends Schema.ErrorClass("SmtpError")({ + cause: Schema.Defect() +}) {} + +export class Smtp extends Context.Service +}>()("app/Smtp") { + static readonly layer = Layer.effect( + Smtp, + Effect.gen(function*() { + const user = yield* Config.string("SMTP_USER") + const pass = yield* Config.redacted("SMTP_PASS") + + // Use `Effect.acquireRelease` to manage the lifecycle of the SMTP + // transporter. + // + // When the Layer is built, the transporter will be created. When the + // Layer is torn down, the transporter will be closed, ensuring that + // resources are always cleaned up properly. + const transporter = yield* Effect.acquireRelease( + Effect.sync(() => + NodeMailer.createTransport({ + host: "smtp.example.com", + port: 587, + secure: false, + auth: { user, pass: Redacted.value(pass) } + }) + ), + (transporter) => Effect.sync(() => transporter.close()) + ) + + const send = Effect.fn("Smtp.send")((message: { + readonly to: string + readonly subject: string + readonly body: string + }) => + Effect.tryPromise({ + try: () => + transporter.sendMail({ + from: "Acme Cloud ", + to: message.to, + subject: message.subject, + text: message.body + }), + catch: (cause) => new SmtpError({ cause }) + }).pipe( + Effect.asVoid + ) + ) + + return Smtp.of({ send }) + }) + ) +} + +// We can then use the `Smtp` service in another service, and the transporter +// will be properly managed by the Layer system. + +export class MailerError extends Schema.TaggedErrorClass()("MailerError", { + reason: SmtpError +}) {} + +export class Mailer extends Context.Service +}>()("app/Mailer") { + static readonly layerNoDeps = Layer.effect( + Mailer, + Effect.gen(function*() { + const smtp = yield* Smtp + + const sendWelcomeEmail = Effect.fn("Mailer.sendWelcomeEmail")(function*(to: string) { + yield* smtp.send({ + to, + subject: "Welcome to Acme Cloud!", + body: "Thanks for signing up for Acme Cloud. We're glad to have you!" + }).pipe( + Effect.mapError((reason) => new MailerError({ reason })) + ) + yield* Effect.logInfo(`Sent welcome email to ${to}`) + }) + + return Mailer.of({ sendWelcomeEmail }) + }) + ) + + // Locally provide the Smtp layer to the Mailer layer, to eliminate all the + // requirements + static readonly layer = this.layerNoDeps.pipe( + Layer.provide(Smtp.layer) + ) +} diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts b/repos/effect/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts new file mode 100644 index 0000000..3d3dfb8 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts @@ -0,0 +1,31 @@ +/** + * @title Creating Layers that run background tasks + * + * Use Layer.effectDiscard to encapsulate background tasks without a service interface. + */ +import { NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" + +// Use Layer.effectDiscard when you want to create a layer that runs an effect +// but does not provide any services. +const BackgroundTask = Layer.effectDiscard(Effect.gen(function*() { + yield* Effect.logInfo("Starting background task...") + + yield* Effect.gen(function*() { + while (true) { + yield* Effect.sleep("5 seconds") + yield* Effect.logInfo("Background task running...") + } + }).pipe( + Effect.onInterrupt(() => Effect.logInfo("Background task interrupted: layer scope closed")), + Effect.forkScoped + ) +})) + +// Run the background task layer. It will start when the layer is launched and +// will be automatically interrupted when the layer scope is closed (e.g. when +// the program exits). +BackgroundTask.pipe( + Layer.launch, + NodeRuntime.runMain +) diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts b/repos/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts new file mode 100644 index 0000000..4dbecb3 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts @@ -0,0 +1,86 @@ +/** + * @title Dynamic resources with LayerMap + * + * Use `LayerMap.Service` to dynamically build and manage resources that are + * keyed by some identifier, such as a tenant ID. + */ +import { Context, Effect, Layer, LayerMap, Schema } from "effect" + +class DatabaseQueryError extends Schema.TaggedErrorClass()("DatabaseQueryError", { + tenantId: Schema.String, + cause: Schema.Defect() +}) {} + +type UserRecord = { + readonly id: number + readonly email: string +} + +let nextConnectionId = 0 + +export class DatabasePool extends Context.Service Effect.Effect, DatabaseQueryError> +}>()("app/DatabasePool") { + // A layer factory that builds one pool per tenant. + static readonly layer = (tenantId: string) => + Layer.effect( + DatabasePool, + Effect.acquireRelease( + Effect.sync(() => { + const connectionId = ++nextConnectionId + + return DatabasePool.of({ + tenantId, + connectionId, + query: Effect.fn("DatabasePool.query")((_sql: string) => + Effect.succeed([ + { id: 1, email: `admin@${tenantId}.example.com` }, + { id: 2, email: `ops@${tenantId}.example.com` } + ]) + ) + }) + }), + (pool) => Effect.logInfo(`Closing tenant pool ${pool.tenantId}#${pool.connectionId}`) + ) + ) +} + +// extend `LayerMap.Service` to create a `LayerMap` service +export class PoolMap extends LayerMap.Service()("app/PoolMap", { + // `lookup` tells LayerMap how to build a layer for each tenant key. + lookup: (tenantId: string) => DatabasePool.layer(tenantId), + + // You can also use the layers option for a static set of layers + // layers: { + // acme: DatabasePool.layer("acme"), + // globex: DatabasePool.layer("globex") + // }, + + // If a pool is not used for this duration, it is released automatically. + idleTimeToLive: "1 minute" +}) {} + +const queryUsersForCurrentTenant = Effect.gen(function*() { + // Run a query agnostic of the tenant. The correct pool will be provided by + // the LayerMap. + const pool = yield* DatabasePool + return yield* pool.query("SELECT id, email FROM users ORDER BY id") +}) + +export const program = Effect.gen(function*() { + yield* queryUsersForCurrentTenant.pipe( + // Use `PoolMap.get` to access the pool for a specific tenant. The first + // time this is called for a tenant, the pool will be built using the + // `lookup` function defined in `PoolMap`. Subsequent calls will reuse the + // cached pool until it is idle for too long or invalidated. + Effect.provide(PoolMap.get("acme")) + ) + + // `PoolMap.invalidate` forces a key to rebuild on the next access. + yield* PoolMap.invalidate("acme") +}).pipe( + // Provide the `PoolMap` layer to the entire program. + Effect.provide(PoolMap.layer) +) diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/index.md b/repos/effect/ai-docs/src/01_effect/05_resources/index.md new file mode 100644 index 0000000..cda2f50 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/05_resources/index.md @@ -0,0 +1,3 @@ +## Managing resources and `Scope`s + +Learn how to safely manage resources in Effect using `Scope`s and finalizers. diff --git a/repos/effect/ai-docs/src/01_effect/06_running/10_run-main.ts b/repos/effect/ai-docs/src/01_effect/06_running/10_run-main.ts new file mode 100644 index 0000000..3e1786c --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/06_running/10_run-main.ts @@ -0,0 +1,30 @@ +/** + * @title Running effects with NodeRuntime and BunRuntime + * + * Use `NodeRuntime.runMain` to run an Effect program as your process entrypoint. + */ +import { BunRuntime } from "@effect/platform-bun" +import { NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" + +const Worker = Layer.effectDiscard(Effect.gen(function*() { + yield* Effect.logInfo("Starting worker...") + yield* Effect.forkScoped(Effect.gen(function*() { + while (true) { + yield* Effect.logInfo("Working...") + yield* Effect.sleep("1 second") + } + })) +})) + +const program = Layer.launch(Worker) + +// `runMain` installs SIGINT / SIGTERM handlers and interrupts running fibers +// for graceful shutdown. +NodeRuntime.runMain(program, { + // Disable automatic error reporting if your app already centralizes it. + disableErrorReporting: true +}) + +// Bun has the same API shape: +BunRuntime.runMain(program, { disableErrorReporting: true }) diff --git a/repos/effect/ai-docs/src/01_effect/06_running/20_layer-launch.ts b/repos/effect/ai-docs/src/01_effect/06_running/20_layer-launch.ts new file mode 100644 index 0000000..e92e124 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/06_running/20_layer-launch.ts @@ -0,0 +1,27 @@ +/** + * @title Using Layer.launch as the application entry point + * + * Use `Layer.launch` to run a long-running Effect program as your process entrypoint. + */ +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { HttpRouter, HttpServerResponse } from "effect/unstable/http" +import { createServer } from "node:http" + +// Build a tiny HTTP app with a health-check endpoint. +export const HealthRoutes = HttpRouter.use(Effect.fn(function*(router) { + yield* router.add("GET", "/health", Effect.succeed(HttpServerResponse.text("ok"))) + yield* router.add("GET", "/healthz", Effect.succeed(HttpServerResponse.text("ok"))) +})) + +// Turn the routes into a server layer and provide the Node HTTP server backend. +export const HttpServerLive = HttpRouter.serve(HealthRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +// `Layer.launch` converts the layer into a long-running Effect. +export const main = Layer.launch(HttpServerLive) + +// This entrypoint pattern works well when the whole app is represented as +// layers (for example: HTTP server + background workers). +NodeRuntime.runMain(main) diff --git a/repos/effect/ai-docs/src/01_effect/06_running/index.md b/repos/effect/ai-docs/src/01_effect/06_running/index.md new file mode 100644 index 0000000..e4c2c87 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/06_running/index.md @@ -0,0 +1 @@ +## Running Effect programs diff --git a/repos/effect/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts b/repos/effect/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts new file mode 100644 index 0000000..6bb876a --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts @@ -0,0 +1,56 @@ +/** + * @title Broadcasting domain events with PubSub + * + * Build an in-process event bus with `PubSub` and expose it as a service. + */ +import { Context, Effect, Layer, PubSub, Stream } from "effect" + +export type OrderEvent = + | { readonly _tag: "OrderPlaced"; readonly orderId: string } + | { readonly _tag: "PaymentCaptured"; readonly orderId: string } + | { readonly _tag: "OrderShipped"; readonly orderId: string } + +export class OrderEvents extends Context.Service + publishAll(events: ReadonlyArray): Effect.Effect + readonly subscribe: Stream.Stream +}>()("acme/OrderEvents") { + static readonly layer = Layer.effect( + OrderEvents, + Effect.gen(function*() { + // Use PubSub.bounded to create a PubSub with backpressure support. + // You can also use PubSub.unbounded if you don't need backpressure. + const pubsub = yield* PubSub.bounded({ + capacity: 256, + // Optionally add a replay buffer to let late subscribers catch up on + // recent events after restarts. + replay: 50 + }) + + // Ensure the PubSub is properly shut down when the service is no longer + // needed. + yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub)) + + const publish = Effect.fn("OrderEvents.publish")(function*(event: OrderEvent) { + yield* PubSub.publish(pubsub, event) + }) + + const publishAll = Effect.fn("OrderEvents.publishAll")(function*(events: ReadonlyArray) { + yield* PubSub.publishAll(pubsub, events) + }) + + // Create a Stream that emits events published to the PubSub. + // + // Each subscriber will receive all events published after they subscribe, + // and if a replay buffer is configured, they will also receive the most + // recent events that were published before they subscribed. + const subscribe = Stream.fromPubSub(pubsub) + + return OrderEvents.of({ + publish, + publishAll, + subscribe + }) + }) + ) +} diff --git a/repos/effect/ai-docs/src/01_effect/07_pubsub/index.md b/repos/effect/ai-docs/src/01_effect/07_pubsub/index.md new file mode 100644 index 0000000..22b0ce0 --- /dev/null +++ b/repos/effect/ai-docs/src/01_effect/07_pubsub/index.md @@ -0,0 +1,3 @@ +## Broadcasting messages with PubSub + +Use `PubSub` when you need one producer to fan out messages to many consumers. diff --git a/repos/effect/ai-docs/src/03_stream/10_creating-streams.ts b/repos/effect/ai-docs/src/03_stream/10_creating-streams.ts new file mode 100644 index 0000000..272b338 --- /dev/null +++ b/repos/effect/ai-docs/src/03_stream/10_creating-streams.ts @@ -0,0 +1,103 @@ +/** + * @title Creating streams from common data sources + * + * Learn how to create streams from various data sources. Includes: + * + * - `Stream.fromIterable` for arrays and other iterables + * - `Stream.fromEffectSchedule` for polling effects + * - `Stream.paginate` for paginated APIs + * - `Stream.fromAsyncIterable` for async iterables + * - `Stream.fromEventListener` for DOM events + * - `Stream.callback` for any callback-based API + * - `NodeStream.fromReadable` for Node.js readable streams + */ +import { NodeStream } from "@effect/platform-node" +import { Array, Effect, Queue, Schedule, Schema, Stream } from "effect" +import * as Option from "effect/Option" +import { Readable } from "node:stream" + +// `Stream.fromIterable` turns any iterable into a stream. +export const numbers = Stream.fromIterable([1, 2, 3, 4, 5]) + +// `Stream.fromEffectSchedule` turns a single effect into a polling stream. +// This is useful for metrics, health checks, and cache refresh loops. +export const samples = Stream.fromEffectSchedule( + Effect.succeed(3), + Schedule.spaced("30 seconds") +).pipe( + // Stream.take limits the number of elements emitted by the stream. + Stream.take(3) +) + +// Use `Stream.paginate` when reading APIs that return one page at a time. +// The function returns the current page of values and optionally the next +// cursor. +export const fetchJobsPage = Stream.paginate( + 0, // start with page 0 (the cursor) + Effect.fn(function*(page) { + // Simulate network latency + yield* Effect.sleep("50 millis") + + const results = Array.range(0, 100).map((i) => `Job ${i + 1 + page * 100}`) + + // only return 10 pages of results + const nextPage = page <= 10 + ? Option.some(page + 1) + : Option.none() + + return [results, nextPage] as const + }) +) + +class LetterError extends Schema.TaggedErrorClass()("LetterError", { + cause: Schema.Defect() +}) {} + +async function* asyncIterable() { + yield "a" + yield "b" + yield "c" +} + +// Create a stream from an async iterable. +// The second argument is a function that converts any errors thrown by the +// async iterable into a typed error. +export const letters = Stream.fromAsyncIterable( + asyncIterable(), + (cause) => new LetterError({ cause }) +) + +const button = document.getElementById("my-button")! + +// `Stream.fromEventListener` creates a stream from an event listener. +export const events = Stream.fromEventListener(button, "click") + +// You can also use `Stream.callback` to create a stream from any callback-based +// API. +export const callbackStream = Stream.callback(Effect.fn(function*(queue) { + // You can use the `Queue` apis to emit values into the stream from the + // callback. + function onEvent(event: PointerEvent) { + Queue.offerUnsafe(queue, event) + } + // register the event listener and add a finalizer to unregister it when the + // stream is finished. + yield* Effect.acquireRelease( + Effect.sync(() => button.addEventListener("click", onEvent)), + () => Effect.sync(() => button.removeEventListener("click", onEvent)) + ) +})) + +export class NodeStreamError extends Schema.TaggedErrorClass()("NodeStreamError", { + cause: Schema.Defect() +}) {} + +// Create a stream from a Node.js readable stream. +// +// It takes options to convert any errors emitted by the stream into a typed +// error, and to evaluate the stream lazily. +export const nodeStream = NodeStream.fromReadable({ + evaluate: () => Readable.from(["Hello", " ", "world", "!"]), + onError: (cause) => new NodeStreamError({ cause }), + closeOnDone: true // true by default +}) diff --git a/repos/effect/ai-docs/src/03_stream/20_consuming-streams.ts b/repos/effect/ai-docs/src/03_stream/20_consuming-streams.ts new file mode 100644 index 0000000..cfe8b92 --- /dev/null +++ b/repos/effect/ai-docs/src/03_stream/20_consuming-streams.ts @@ -0,0 +1,137 @@ +/** + * @title Consuming and transforming streams + * + * How to transform and consume streams using operators like `map`, `flatMap`, `filter`, `mapEffect`, and various `run*` methods. + */ +import { Effect, Sink, Stream } from "effect" + +interface Order { + readonly id: string + readonly customerId: string + readonly status: "paid" | "refunded" + readonly subtotalCents: number + readonly shippingCents: number + readonly country: "US" | "CA" | "NZ" +} + +interface NormalizedOrder extends Order { + readonly totalCents: number +} + +interface EnrichedOrder extends NormalizedOrder { + readonly taxCents: number + readonly grandTotalCents: number + readonly priority: "normal" | "high" +} + +// Start with structured order events from an in-memory source. +export const orderEvents = Stream.succeed({ + id: "ord_1001", + customerId: "cus_1", + status: "paid", + subtotalCents: 4_500, + shippingCents: 500, + country: "US" +}) + +// Use `Stream.map` for pure per-element transforms. +export const normalizedOrders = orderEvents.pipe( + Stream.map((order): NormalizedOrder => ({ + ...order, + totalCents: order.subtotalCents + order.shippingCents + })) +) + +// `Stream.filter` lets you exclude elements that don't match a predicate. +export const paidOrders = normalizedOrders.pipe( + Stream.filter((order) => order.status === "paid") +) + +// Use `Stream.flatMap` to transform each element into a stream, and flatten the +// results. +export const allOrders = Stream.make("US", "CA", "NZ").pipe( + Stream.flatMap( + (country) => + Stream.range(1, 50).pipe( + Stream.map((i): Order => ({ + id: `ord_${country}_${i}`, + customerId: `cus_${i}`, + status: i % 10 === 0 ? "refunded" : "paid", + subtotalCents: Math.round(Math.random() * 100_000), + shippingCents: Math.round(Math.random() * 10_000), + country + })) + ), + // Optionally control the concurrency of the flatMap with the second argument. + { concurrency: 2 } + ) +) + +const enrichOrder = Effect.fn(function*(order: NormalizedOrder): Effect.fn.Return { + // Simulate effectful enrichment (for example, tax/risk lookup). + yield* Effect.sleep("5 millis") + + const taxRate = order.country === "US" ? 0.08 : 0.13 + const taxCents = Math.round(order.totalCents * taxRate) + + return { + ...order, + taxCents, + grandTotalCents: order.totalCents + taxCents, + priority: order.totalCents >= 20_000 ? "high" : "normal" + } +}) + +// `Stream.mapEffect` performs effectful per-element transforms with concurrency control. +export const enrichedPaidOrders = paidOrders.pipe( + Stream.mapEffect(enrichOrder, { concurrency: 4 }) +) + +// `runCollect` gathers all stream outputs into an immutable array. +export const collectedOrders = Stream.runCollect(enrichedPaidOrders) + +// `runDrain` runs the stream for its effects, ignoring all outputs. +export const drained = Stream.runDrain(enrichedPaidOrders) + +// `runForEach` executes an effectful consumer for every element. +export const logOrders = enrichedPaidOrders.pipe( + Stream.runForEach((order) => Effect.logInfo(`Order ${order.id} total=$${(order.grandTotalCents / 100).toFixed(2)}`)) +) + +// `runFold` reduces the stream to one accumulated value. +export const totalRevenueCents = enrichedPaidOrders.pipe( + Stream.runFold(() => 0, (acc: number, order) => acc + order.grandTotalCents) +) + +// `run` lets you consume a stream through any Sink. +export const totalRevenueViaSink = enrichedPaidOrders.pipe( + Stream.map((order) => order.grandTotalCents), + Stream.run(Sink.sum) +) + +// `runHead` and `runLast` capture edge elements as Option values. +export const firstLargeOrder = enrichedPaidOrders.pipe( + Stream.filter((order) => order.priority === "high"), + Stream.runHead +) + +export const lastLargeOrder = enrichedPaidOrders.pipe( + Stream.filter((order) => order.priority === "high"), + Stream.runLast +) + +// Windowing-style operators help shape what downstream consumers see. +export const firstTwoOrders = enrichedPaidOrders.pipe( + Stream.take(2), + Stream.runCollect +) + +export const afterWarmupOrder = enrichedPaidOrders.pipe( + Stream.drop(1), + Stream.runCollect +) + +export const untilLargeOrder = enrichedPaidOrders.pipe( + Stream.takeWhile((order) => order.priority === "normal"), + Stream.runCollect +) diff --git a/repos/effect/ai-docs/src/03_stream/30_encoding.ts b/repos/effect/ai-docs/src/03_stream/30_encoding.ts new file mode 100644 index 0000000..16a6b6a --- /dev/null +++ b/repos/effect/ai-docs/src/03_stream/30_encoding.ts @@ -0,0 +1,165 @@ +/** + * @title Decoding and encoding streams + * + * Use `Stream.pipeThroughChannel` with the `Ndjson` & `Msgpack` modules to + * decode and encode streams of structured data. + */ +import { DateTime, Schema, Stream } from "effect" +import { Msgpack, Ndjson } from "effect/unstable/encoding" + +// All of the examples below can also be done with Msgpack by replacing `Ndjson` +// with `Msgpack` and using the appropriate channels (`Msgpack.decode()`, +// `Msgpack.encode()`, etc.). +export const msgpackDecoder = Msgpack.decodeSchema(Schema.Struct({ + id: Schema.Number, + name: Schema.String +})) + +// --------------------------------------------------------------------------- +// Domain +// --------------------------------------------------------------------------- + +// A log entry schema representing structured log events. In practice these +// would come from a file, HTTP body, or socket connection. +// `DateTimeUtcFromString` decodes an ISO-8601 string into a `DateTime.Utc`. +class LogEntry extends Schema.Class("LogEntry")({ + timestamp: Schema.DateTimeUtcFromString, + level: Schema.Literals(["info", "warn", "error"]), + message: Schema.String +}) {} + +// --------------------------------------------------------------------------- +// Decoding NDJSON strings → objects +// --------------------------------------------------------------------------- + +// Suppose we receive raw NDJSON text from a file or network socket. +// `Ndjson.decodeString()` is a Channel that splits incoming strings on +// newlines and `JSON.parse`s each line. +// Pipe the stream through the channel with `Stream.pipeThroughChannel`. +export const decodeUntyped = Stream.make( + "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" + + "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n" +).pipe( + Stream.pipeThroughChannel(Ndjson.decodeString()), + Stream.runCollect +) + +// When you need schema validation on top of the raw JSON parse, use +// `Ndjson.decodeSchemaString(Schema)()`. This decodes each line, parses the +// JSON, and then validates each value against the schema — all in one channel. +export const decodeTyped = Stream.make( + "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" + + "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n" +).pipe( + Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()), + Stream.runCollect +) + +// --------------------------------------------------------------------------- +// Encoding objects → NDJSON strings +// --------------------------------------------------------------------------- + +// `Ndjson.encodeString()` serialises each value to a JSON line. +// The resulting stream emits ready-to-write NDJSON strings. +export const encodeUntyped = Stream.make( + { timestamp: "2025-06-01T00:00:00Z", level: "info", message: "start" }, + { timestamp: "2025-06-01T00:00:01Z", level: "error", message: "oops" } +).pipe( + Stream.pipeThroughChannel(Ndjson.encodeString()), + Stream.runCollect +) + +// `Ndjson.encodeSchemaString(Schema)()` encodes each value through the schema +// first (applying any transformations such as date formatting), then +// serialises it to an NDJSON line. +export const encodeTyped = Stream.make( + new LogEntry({ + timestamp: DateTime.makeUnsafe("2025-06-01T00:00:00Z"), + level: "info", + message: "start" + }), + new LogEntry({ + timestamp: DateTime.makeUnsafe("2025-06-01T00:00:01Z"), + level: "error", + message: "oops" + }) +).pipe( + Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()), + Stream.runCollect +) + +// --------------------------------------------------------------------------- +// Binary (Uint8Array) variants +// --------------------------------------------------------------------------- + +// When working with binary I/O (e.g. TCP sockets, file descriptors) use the +// non-string variants. `Ndjson.decode()` expects `Uint8Array` chunks and +// handles text decoding internally. `Ndjson.encode()` produces `Uint8Array` +// output. +const enc = new TextEncoder() + +export const decodeBinary = Stream.make( + enc.encode("{\"level\":\"info\",\"message\":\"binary\"}\n") +).pipe( + Stream.pipeThroughChannel(Ndjson.decode()), + Stream.runCollect +) + +export const encodeBinary = Stream.make( + { level: "info", message: "binary" } +).pipe( + Stream.pipeThroughChannel(Ndjson.encode()), + Stream.runCollect +) + +// --------------------------------------------------------------------------- +// Handling empty lines +// --------------------------------------------------------------------------- + +// NDJSON files sometimes contain blank lines (e.g. trailing newlines or +// pretty-printed output). Pass `{ ignoreEmptyLines: true }` to skip them +// instead of raising an `NdjsonError`. +export const decodeIgnoringBlanks = Stream.make( + "{\"ok\":true}\n\n{\"ok\":false}\n" +).pipe( + Stream.pipeThroughChannel(Ndjson.decodeString({ ignoreEmptyLines: true })), + Stream.runCollect +) + +// --------------------------------------------------------------------------- +// Error handling +// --------------------------------------------------------------------------- + +// `Ndjson.NdjsonError` is raised when encoding (`kind: "Pack"`) or decoding +// (`kind: "Unpack"`) fails. You can catch it with `Stream.catchTag` or +// `Effect.catchTag`. +export const handleDecodeErrors = Stream.make("not-valid-json\n").pipe( + Stream.pipeThroughChannel(Ndjson.decodeString()), + Stream.catchTag("NdjsonError", (err) => + // The `kind` field indicates whether the error occurred during + // encoding ("Pack") or decoding ("Unpack"), and `cause` contains + // the underlying exception. + Stream.succeed({ recovered: true, kind: err.kind })), + Stream.runCollect +) + +// --------------------------------------------------------------------------- +// Realistic pipeline: decode → transform → re-encode +// --------------------------------------------------------------------------- + +// A common pattern is to read NDJSON, transform each record, and write it +// back as NDJSON. This example filters error-level log entries and re-encodes +// them. +const ndjsonInput = "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"ok\"}\n" + + "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"fail\"}\n" + + "{\"timestamp\":\"2025-06-01T00:00:02Z\",\"level\":\"warn\",\"message\":\"slow\"}\n" + +export const filterAndReencode = Stream.make(ndjsonInput).pipe( + // Decode each line into a validated LogEntry + Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()), + // Keep only error-level entries + Stream.filter((entry) => entry.level === "error"), + // Re-encode the filtered entries back to NDJSON strings + Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()), + Stream.runCollect +) diff --git a/repos/effect/ai-docs/src/03_stream/index.md b/repos/effect/ai-docs/src/03_stream/index.md new file mode 100644 index 0000000..710e143 --- /dev/null +++ b/repos/effect/ai-docs/src/03_stream/index.md @@ -0,0 +1,4 @@ +## Working with Streams + +Effect Streams represent effectful, pull-based sequences of values over time. +They let you model finite or infinite data sources. diff --git a/repos/effect/ai-docs/src/04_integration/10_managed-runtime.ts b/repos/effect/ai-docs/src/04_integration/10_managed-runtime.ts new file mode 100644 index 0000000..ed29f83 --- /dev/null +++ b/repos/effect/ai-docs/src/04_integration/10_managed-runtime.ts @@ -0,0 +1,129 @@ +/** + * @title Using ManagedRuntime with Hono + * + * Use `ManagedRuntime` to run Effect programs from external frameworks while keeping your domain logic in services and Layers. + */ +import { Context, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect" +import { Hono } from "hono" + +class Todo extends Schema.Class("Todo")({ + id: Schema.Number, + title: Schema.String, + completed: Schema.Boolean +}) {} + +class CreateTodoPayload extends Schema.Class("CreateTodoPayload")({ + title: Schema.String +}) {} + +class TodoNotFound extends Schema.TaggedErrorClass()("TodoNotFound", { + id: Schema.Number +}) {} + +export class TodoRepo extends Context.Service> + getById(id: number): Effect.Effect + create(payload: CreateTodoPayload): Effect.Effect +}>()("app/TodoRepo") { + static readonly layer = Layer.effect( + TodoRepo, + Effect.gen(function*() { + const store = new Map() + const nextId = yield* Ref.make(1) + + const getAll = Effect.gen(function*() { + return Array.from(store.values()) + }).pipe( + Effect.withSpan("TodoRepo.getAll") + ) + + const getById = Effect.fn("TodoRepo.getById")(function*(id: number) { + const todo = store.get(id) + if (todo === undefined) { + return yield* new TodoNotFound({ id }) + } + return todo + }) + + const create = Effect.fn("TodoRepo.create")(function*(payload: CreateTodoPayload) { + const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1) + const todo = new Todo({ id, title: payload.title, completed: false }) + store.set(id, todo) + return todo + }) + + return TodoRepo.of({ getAll, getById, create }) + }) + ) +} + +// Create a global memo map that can be shared across the app. This is necessary +// for memoization to work correctly across ManagedRuntime instances. +export const appMemoMap = Layer.makeMemoMapUnsafe() + +// Create a ManagedRuntime for the TodoRepo layer. This runtime can be shared +// across all handlers in the app, and it will manage the lifecycle of the +// TodoRepo service and any resources it uses. +export const runtime = ManagedRuntime.make(TodoRepo.layer, { + memoMap: appMemoMap +}) + +export const app = new Hono() + +app.get("/todos", async (context) => { + const todos = await runtime.runPromise( + TodoRepo.use((repo) => repo.getAll) + ) + return context.json(todos) +}) + +app.get("/todos/:id", async (context) => { + const id = Number(context.req.param("id")) + if (!Number.isFinite(id)) { + return context.json({ message: "Todo id must be a number" }, 400) + } + + const todo = await runtime.runPromise( + TodoRepo.use((repo) => repo.getById(id)).pipe( + Effect.catchTag("TodoNotFound", () => Effect.succeed(null)) + ) + ) + + if (todo === null) { + return context.json({ message: "Todo not found" }, 404) + } + + return context.json(todo) +}) + +const decodeCreateTodoPayload = Schema.decodeUnknownSync(CreateTodoPayload) + +app.post("/todos", async (context) => { + const body = await context.req.json() + + let payload: CreateTodoPayload + try { + payload = decodeCreateTodoPayload(body) + } catch { + return context.json({ message: "Invalid request body" }, 400) + } + + const todo = await runtime.runPromise( + TodoRepo.use((repo) => repo.create(payload)) + ) + + return context.json(todo, 201) +}) + +// The same bridge pattern works for Express, Fastify, Koa, and other frameworks. +// Use `runtime.runSync` for synchronous edges or `runtime.runCallback` for +// callback-only APIs. + +// When the process receives a shutdown signal, dispose the runtime to clean up +// any resources used by the TodoRepo service and its dependencies. +const shutdown = () => { + void runtime.dispose() +} + +process.once("SIGINT", shutdown) +process.once("SIGTERM", shutdown) diff --git a/repos/effect/ai-docs/src/04_integration/index.md b/repos/effect/ai-docs/src/04_integration/index.md new file mode 100644 index 0000000..dd01cdb --- /dev/null +++ b/repos/effect/ai-docs/src/04_integration/index.md @@ -0,0 +1,5 @@ +## Integrating Effect into existing applications + +`ManagedRuntime` bridges Effect programs with non-Effect code. Build one runtime +from your application Layer, then use it anywhere you need imperative execution, +like web handlers, framework hooks, worker queues, or legacy callback APIs. diff --git a/repos/effect/ai-docs/src/05_batching/10_request-resolver.ts b/repos/effect/ai-docs/src/05_batching/10_request-resolver.ts new file mode 100644 index 0000000..156e653 --- /dev/null +++ b/repos/effect/ai-docs/src/05_batching/10_request-resolver.ts @@ -0,0 +1,89 @@ +/** + * @title Batching requests with RequestResolver + * + * Define request types with `Request.Class`, resolve them in batches with `RequestResolver`. + */ +import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect" + +export class User extends Schema.Class("User")({ + id: Schema.Number, + name: Schema.String, + email: Schema.String +}) {} + +export class UserNotFound extends Schema.TaggedErrorClass()("UserNotFound", { + id: Schema.Number +}) {} + +export class Users extends Context.Service +}>()("app/Users") { + static readonly layer = Layer.effect( + Users, + Effect.gen(function*() { + // Request classes model a single external lookup. + class GetUserById extends Request.Class< + { readonly id: number }, + User, // The success type of the request + UserNotFound, // The error type of the request + never // The requirements type of the request, if any + > {} + + // Simulate an external data source that supports batched lookup. + const usersTable = new Map([ + [1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })], + [2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })], + [3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })] + ]) + + const resolver = yield* RequestResolver.make(Effect.fn(function*(entries) { + for (const entry of entries) { + const user = usersTable.get(entry.request.id) + + // If the request had requirements, you can access them with + // `entry.context` + const requestSpan = Context.getOption(entry.context, Tracer.ParentSpan) + console.log("Request span", requestSpan) + + if (user) { + // Complete requests with .completeUnsafe and pass in an Exit value + entry.completeUnsafe(Exit.succeed(user)) + } else { + entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id }))) + } + } + })).pipe( + // Control the delay before the resolver is executed. This allows more + // requests to be batched together, but also adds latency to the first + // request. + RequestResolver.setDelay("10 millis"), + // RequestResolver.withSpan adds a span around the resolver execution, + // and also sets up span links for each request + RequestResolver.withSpan("Users.getUserById.resolver"), + // RequestResolver.withCache adds a simple LRU cache to avoid repeated + // lookups for the same ID. + RequestResolver.withCache({ capacity: 1024 }) + ) + + // Wrap the resolver in a service method. The resolver batches calls to + // `getUserById` that occur within the delay window. + const getUserById = (id: number) => + Effect.request(new GetUserById({ id }), resolver).pipe( + Effect.withSpan("Users.getUserById", { attributes: { userId: id } }) + ) + + return { getUserById } as const + }) + ) +} + +// Run multiple lookups concurrently. The resolver receives one batch and +// internally deduplicates repeated IDs for the external call. +export const batchedLookupExample = Effect.gen(function*() { + const { getUserById } = yield* Users + + // This will only trigger a single call to the resolver with the unique IDs [1, 2, 3]. + yield* Effect.forEach([1, 2, 1, 3, 2], getUserById, { + concurrency: "unbounded" + }) +}) diff --git a/repos/effect/ai-docs/src/05_batching/index.md b/repos/effect/ai-docs/src/05_batching/index.md new file mode 100644 index 0000000..4166039 --- /dev/null +++ b/repos/effect/ai-docs/src/05_batching/index.md @@ -0,0 +1,3 @@ +## Batching external requests + +Learn how to batch multiple requests into fewer external calls. diff --git a/repos/effect/ai-docs/src/06_schedule/10_schedules.ts b/repos/effect/ai-docs/src/06_schedule/10_schedules.ts new file mode 100644 index 0000000..e6764d4 --- /dev/null +++ b/repos/effect/ai-docs/src/06_schedule/10_schedules.ts @@ -0,0 +1,110 @@ +/** + * @title Working with the Schedule module + * + * Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`. + */ +import { Duration, Effect, Random, Schedule, Schema } from "effect" + +// Production pattern: capped exponential backoff with jitter and max attempts. +// Delays start at 250ms, grow exponentially with jitter, and are capped at 10s. +export const productionRetrySchedule = Schedule.min([ + Schedule.exponential("250 millis"), + // Cap the delay at 10 seconds to avoid excessively long waits. + Schedule.spaced("10 seconds") +]).pipe( + Schedule.jittered, + Schedule.setInputType(), + Schedule.while(({ input }) => input.retryable) +) + +export const fetchUserProfile = Effect.fn("fetchUserProfile")( + function*(userId: string) { + const random = yield* Random.next + const status = random > 0.7 + ? 200 + : random > 0.3 + ? 503 + : 401 + + if (status !== 200) { + return yield* new HttpError({ + message: `Request for ${userId} failed`, + status, + retryable: status >= 500 + }) + } + + return { + id: userId, + name: "Ada Lovelace" + } as const + } +) + +// Use the schedule with `Effect.retry` to retry failures. +export const loadUserWithRetry = fetchUserProfile("user-123").pipe( + Effect.retry(productionRetrySchedule), + // If the effect still fails after exhausting the schedule, turn the error + // into a fatal one. + Effect.orDie +) + +export const loadUserWithInferredInput = fetchUserProfile("user-123").pipe( + // You can also pass a schedule builder function that assists with inferring + // the input type. This is especially useful when the schedule needs to + // inspect the error to determine retryability. + Effect.retry(($) => + $(Schedule.spaced("1 seconds")).pipe( + Schedule.while(({ input }) => input.retryable) + ) + ), + Effect.orDie +) + +export class HttpError extends Schema.TaggedErrorClass()("HttpError", { + message: Schema.String, + status: Schema.Number, + retryable: Schema.Boolean +}) {} + +// Start with a few schedule constructors. +export const maxRetries = Schedule.recurs(5) +export const spacedPolling = Schedule.spaced("30 seconds") +export const exponentialBackoff = Schedule.exponential("200 millis") + +// `Schedule.max` continues only while all schedules continue and outputs +// the slowest delay. It is useful for combining a delay pattern with a hard +// attempt cap. +export const retryBackoffWithLimit = Schedule.max([ + Schedule.exponential("250 millis"), + Schedule.recurs(6) +]) + +// `Schedule.min` continues while any schedule continues and outputs +// the fastest delay. It is useful for fallback behavior. +// +// This schedule will start at 500ms, exponentially increase, and will be capped +// at 2 seconds. +export const keepTryingUpToTwoSeconds = Schedule.min([ + Schedule.exponential("500 millis"), + Schedule.spaced("2 seconds") +]) + +// Use `Schedule.while` to continue only for retryable failures. +// This lets non-retryable errors fail fast, even if attempts remain. +export const retryableOnly = Schedule.exponential("200 millis").pipe( + // You can use `setInputType` to specify the type of input the schedule will + // receive. + Schedule.setInputType(), + Schedule.while(({ input }) => input.retryable) +) + +// `tap` is useful for performing side effects like logging or metrics. +export const instrumentedRetrySchedule = retryableOnly.pipe( + Schedule.setInputType(), + Schedule.tap((meta) => + Effect.logDebug( + `Retrying after ${meta.input.status}: ${meta.input.message}. Next retry in ${Duration.toMillis(meta.duration)}ms` + ) + ) +) diff --git a/repos/effect/ai-docs/src/06_schedule/index.md b/repos/effect/ai-docs/src/06_schedule/index.md new file mode 100644 index 0000000..b56aa23 --- /dev/null +++ b/repos/effect/ai-docs/src/06_schedule/index.md @@ -0,0 +1,3 @@ +## Working with Schedules + +Schedules define recurring patterns for retries, repeats and polling. diff --git a/repos/effect/ai-docs/src/07_datetime/10_creating-and-formatting.ts b/repos/effect/ai-docs/src/07_datetime/10_creating-and-formatting.ts new file mode 100644 index 0000000..6e5a774 --- /dev/null +++ b/repos/effect/ai-docs/src/07_datetime/10_creating-and-formatting.ts @@ -0,0 +1,30 @@ +/** + * @title Creating and formatting DateTime values + * + * Parse incoming date values safely, use Clock-powered current time, and format + * instants for API payloads or user-facing labels. + */ +import { DateTime, Effect, Option } from "effect" + +Effect.gen(function*() { + // Use DateTime.now to get the current time from Effect's Clock service. + // Using the Clock service ensures tests can use the `TestClock` module to + // control time. + const now = yield* DateTime.now + + // Use DateTime.make to parse a date input, such as a user-entered string or a + // epoch timestamp. It returns an Option depending on whether the input was + // valid. + const parsedOption: Option.Option = DateTime.make("2024-06-15T14:30:00.000Z") + + // you can then use the Option apis to unwrap the value + Option.getOrUndefined(parsedOption) + + // Calendar/date-time math returns a new DateTime value; the original value is + // immutable. + const endsAt = now.pipe(DateTime.add({ hours: 2 })) + + // The DateTime.format* functions can be used to convert a DateTime value to + // differen formats. + yield* Effect.log("ISO string:", DateTime.formatIso(endsAt)) +}) diff --git a/repos/effect/ai-docs/src/07_datetime/20_time-zones.ts b/repos/effect/ai-docs/src/07_datetime/20_time-zones.ts new file mode 100644 index 0000000..7ceed42 --- /dev/null +++ b/repos/effect/ai-docs/src/07_datetime/20_time-zones.ts @@ -0,0 +1,44 @@ +/** + * @title Working with time zones + * + * Attach IANA zones to instants, render zoned ISO strings, and provide a + * CurrentTimeZone service for code that should use the workspace/user zone. + */ +import { NodeRuntime } from "@effect/platform-node" +import { DateTime, Effect, Option } from "effect" + +Effect.gen(function*() { + // Use DateTime.now to get the current time from Effect's Clock service. + const now = yield* DateTime.now + + // To attach a named IANA zone to a DateTime value + const nowInAuckland = now.pipe( + // Use DateTime.setZoneNamedUnsafe when you know the zone is valid. + DateTime.setZoneNamedUnsafe("Pacific/Auckland") + ) + yield* Effect.log("Now in Auckland:", nowInAuckland) + + // Use DateTime.setZoneNamed when you don't know the zone is valid. + const nowInSydneyOption: Option.Option = now.pipe( + DateTime.setZoneNamed("Australia/Sydney") + ) + + yield* Effect.log("Now in Sydney:", Option.getOrUndefined(nowInSydneyOption)) + + // To generate a `DateTime.Zoned` in the `DateTime.CurrentTimeZone` + const nowInNewYork = yield* DateTime.nowInCurrentZone + yield* Effect.log("Now in New York:", nowInNewYork) + + // If you have a date string that you know is in a particular IANA zone, you + // can convert it to a DateTime.Zoned to ensure the instant is correct + const dateInAuckland: DateTime.Zoned = DateTime.makeZonedUnsafe("2026-06-05", { + timeZone: "Pacific/Auckland", + // adjustForTimeZone will adjust the input to the given zone, otherwise it + // will be treated as UTC. + adjustForTimeZone: true + }) + yield* Effect.log("Date in Auckland:", dateInAuckland) +}).pipe( + Effect.provide(DateTime.layerCurrentZoneNamed("America/New_York")), + NodeRuntime.runMain +) diff --git a/repos/effect/ai-docs/src/07_datetime/index.md b/repos/effect/ai-docs/src/07_datetime/index.md new file mode 100644 index 0000000..08d30a9 --- /dev/null +++ b/repos/effect/ai-docs/src/07_datetime/index.md @@ -0,0 +1,5 @@ +## Working with DateTime + +When working with dates and time, use the `DateTime` module instead of `Date` and `Date.now`. + +Use it when your Effect programs need testable current time, safe parsing, stable ISO formatting, time-zone conversion, or calendar arithmetic. diff --git a/repos/effect/ai-docs/src/08_observability/10_logging.ts b/repos/effect/ai-docs/src/08_observability/10_logging.ts new file mode 100644 index 0000000..7cff7a2 --- /dev/null +++ b/repos/effect/ai-docs/src/08_observability/10_logging.ts @@ -0,0 +1,66 @@ +/** + * @title Customizing logging + * + * Configure loggers & log-level filtering for production applications. + */ +import { NodeFileSystem } from "@effect/platform-node" +import { Config, Effect, Layer, Logger, References } from "effect" + +// Build a logger layer that emits one JSON line per log entry. +export const JsonLoggerLayer = Logger.layer([Logger.consoleJson]) + +// Raise the minimum level to "Warn" to skip debug/info logs. +export const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, "Warn") + +// There is a built-in logger for writing to a file +export const FileLoggerLayer = Logger.layer([ + Logger.toFile(Logger.formatSimple, "app.log") +]).pipe( + Layer.provide(NodeFileSystem.layer) +) + +// Define a custom logger for app-specific formatting and routing. +export const appLogger = Effect.gen(function*() { + // Here you could initialize a connection to an external logging service, set + // up log file rotation, etc. + yield* Effect.logDebug("initializing app logger") + + return yield* Logger.batched(Logger.formatStructured, { + window: "1 second", + flush: Effect.fn(function*(batch) { + // In a real implementation, this is where you would send the batch of log entries to an external logging service or write them to a file. + console.log(`Flushing ${batch.length} log entries`) + }) + }) +}) + +export const AppLoggerLayer = Logger.layer([appLogger]).pipe( + Layer.provideMerge(WarnAndAbove) // Start with "Warn" level for the app logger. +) + +// Create a logger layer that uses the default logger for development, and the +// custom logger for production +export const LoggerLayer = Layer.unwrap(Effect.gen(function*() { + const env = yield* Config.string("NODE_ENV").pipe(Config.withDefault("development")) + if (env === "production") { + return AppLoggerLayer + } + return Logger.layer([Logger.defaultLogger]) +})) + +// Example effect that logs at various levels during a checkout flow. +export const logCheckoutFlow = Effect.gen(function*() { + yield* Effect.logDebug("loading checkout state") + + yield* Effect.logInfo("validating cart") + yield* Effect.logWarning("inventory is low for one line item") + yield* Effect.logError("payment provider timeout") +}).pipe( + // Attach structured metadata to all log lines emitted by this effect. + Effect.annotateLogs({ + service: "checkout-api", + route: "POST /checkout" + }), + // Add a duration span so each log line includes checkout=ms metadata. + Effect.withLogSpan("checkout") +) diff --git a/repos/effect/ai-docs/src/08_observability/20_otlp-tracing.ts b/repos/effect/ai-docs/src/08_observability/20_otlp-tracing.ts new file mode 100644 index 0000000..121e795 --- /dev/null +++ b/repos/effect/ai-docs/src/08_observability/20_otlp-tracing.ts @@ -0,0 +1,95 @@ +/** + * @title Setting up tracing with Otlp modules + * + * Configure Otlp tracing + log export with a reusable observability layer. + */ +import { NodeRuntime } from "@effect/platform-node" +import { Context, Effect, Layer } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability" + +// Configure OTLP span export. +export const OtlpTracingLayer = OtlpTracer.layer({ + url: "http://localhost:4318/v1/traces", + resource: { + serviceName: "checkout-api", + serviceVersion: "1.0.0", + attributes: { + "deployment.environment": "staging" + } + } +}) + +// Configure OTLP log export. +export const OtlpLoggingLayer = OtlpLogger.layer({ + url: "http://localhost:4318/v1/logs", + resource: { + serviceName: "checkout-api", + serviceVersion: "1.0.0" + } +}) + +// Reusable app-wide observability layer. +// +// - OtlpTracer/OtlpLogger require an OTLP serializer and an HttpClient. +// - FetchHttpClient.layer provides the HttpClient used by the exporter. +export const ObservabilityLayer = Layer.merge(OtlpTracingLayer, OtlpLoggingLayer).pipe( + Layer.provide(OtlpSerialization.layerJson), + Layer.provide(FetchHttpClient.layer) +) + +export class Checkout extends Context.Service +}>()("acme/Checkout") { + static readonly layer = Layer.effect( + Checkout, + Effect.gen(function*() { + yield* Effect.logInfo("setting up checkout service") + + return Checkout.of({ + processCheckout: Effect.fn("Checkout.processCheckout")(function*(orderId: string) { + yield* Effect.logInfo("starting checkout", { orderId }) + + yield* Effect.sleep("50 millis").pipe( + Effect.withSpan("checkout.charge-card"), + Effect.annotateSpans({ + "checkout.order_id": orderId, + "checkout.provider": "acme-pay" + }) + ) + + yield* Effect.sleep("20 millis").pipe( + Effect.withSpan("checkout.persist-order") + ) + + yield* Effect.logInfo("checkout completed", { orderId }) + }) + }) + }) + ) +} + +// Example usage of the Checkout service. +const CheckoutTest = Layer.effectDiscard( + Effect.gen(function*() { + const checkout = yield* Checkout + yield* checkout.processCheckout("ord_123") + }).pipe( + Effect.withSpan("checkout-test-run") + ) +).pipe( + // You can also attach spans to Layers + Layer.withSpan("checkout-test"), + Layer.provide(Checkout.layer) +) + +const Main = CheckoutTest.pipe( + // Provide the observability layer at the very end, so that all spans created + // by the app are exported. + Layer.provide(ObservabilityLayer) +) + +// Launch the app +Layer.launch(Main).pipe( + NodeRuntime.runMain +) diff --git a/repos/effect/ai-docs/src/08_observability/index.md b/repos/effect/ai-docs/src/08_observability/index.md new file mode 100644 index 0000000..df0399a --- /dev/null +++ b/repos/effect/ai-docs/src/08_observability/index.md @@ -0,0 +1,7 @@ +## Observability + +Effect has built-in support for structured logging, distributed tracing, and +metrics. For exporting telemetry, use the lightweight Otlp modules from +`effect/unstable/observability` in new projects, or use +`@effect/opentelemetry` NodeSdk when integrating with an existing OpenTelemetry +setup. diff --git a/repos/effect/ai-docs/src/09_testing/10_effect-tests.ts b/repos/effect/ai-docs/src/09_testing/10_effect-tests.ts new file mode 100644 index 0000000..393d29d --- /dev/null +++ b/repos/effect/ai-docs/src/09_testing/10_effect-tests.ts @@ -0,0 +1,55 @@ +/** + * @title Writing Effect tests with @effect/vitest + * + * Using `it.effect` for Effect-based tests. + */ +import { assert, describe, it } from "@effect/vitest" +import { Effect, Fiber, Schema } from "effect" +import { TestClock } from "effect/testing" + +describe("@effect/vitest basics", () => { + it.effect("runs Effect code with assert helpers", () => + Effect.gen(function*() { + const upper = ["ada", "lin"].map((name) => name.toUpperCase()) + assert.deepStrictEqual(upper, ["ADA", "LIN"]) + assert.strictEqual(upper.length, 2) + assert.isTrue(upper.includes("ADA")) + })) + + it.effect.each([ + { input: " Ada ", expected: "ada" }, + { input: " Lin ", expected: "lin" }, + { input: " Nia ", expected: "nia" } + ])("parameterized normalization %#", ({ input, expected }) => + Effect.gen(function*() { + assert.strictEqual(input.trim().toLowerCase(), expected) + })) + + it.effect("controls time with TestClock", () => + Effect.gen(function*() { + const fiber = yield* Effect.forkChild( + Effect.sleep(60_000).pipe(Effect.as("done" as const)) + ) + + // Move virtual time forward to complete sleeping fibers immediately. + yield* TestClock.adjust(60_000) + + const value = yield* Fiber.join(fiber) + assert.strictEqual(value, "done") + })) + + it.live("uses real runtime services", () => + Effect.gen(function*() { + const startedAt = Date.now() + yield* Effect.sleep(1) + assert.isTrue(Date.now() >= startedAt) + })) + + // For property-based testing, use `it.effect.prop` with Schema-based + // arbitraries + it.effect.prop("reversing twice is identity", [Schema.String], ([value]) => + Effect.gen(function*() { + const reversedTwice = value.split("").reverse().reverse().join("") + assert.strictEqual(reversedTwice, value) + })) +}) diff --git a/repos/effect/ai-docs/src/09_testing/20_layer-tests.ts b/repos/effect/ai-docs/src/09_testing/20_layer-tests.ts new file mode 100644 index 0000000..556dc8d --- /dev/null +++ b/repos/effect/ai-docs/src/09_testing/20_layer-tests.ts @@ -0,0 +1,138 @@ +/** + * @title Testing services with shared layers + * + * How to test Effect services that depend on other services. + */ +import { assert, describe, it, layer } from "@effect/vitest" +import { Array, Context, Effect, Layer, Ref } from "effect" + +export interface Todo { + readonly id: number + readonly title: string +} + +// Create a test ref service that can be used to store and manipulate test data +// in layers. +export class TodoRepoTestRef extends Context.Service>>()("app/TodoRepoTestRef") { + static readonly layer = Layer.effect(TodoRepoTestRef, Ref.make(Array.empty())) +} + +class TodoRepo extends Context.Service + readonly list: Effect.Effect> +}>()("app/TodoRepo") { + static readonly layerTest = Layer.effect( + TodoRepo, + Effect.gen(function*() { + const store = yield* TodoRepoTestRef + + const create = Effect.fn("TodoRepo.create")(function*(title: string) { + const todos = yield* Ref.get(store) + const todo = { id: todos.length + 1, title } + yield* Ref.set(store, [...todos, todo]) + return todo + }) + + const list = Ref.get(store) + + return TodoRepo.of({ + create, + list + }) + }) + ).pipe( + // Provide the test ref layer as a dependency for the test repo layer. + // Use Layer.provideMerge so the tests can also access the test ref directly + // if needed. + Layer.provideMerge(TodoRepoTestRef.layer) + ) +} + +class TodoService extends Context.Service + readonly titles: Effect.Effect> +}>()("app/TodoService") { + static readonly layerNoDeps = Layer.effect( + TodoService, + Effect.gen(function*() { + const repo = yield* TodoRepo + + const addAndCount = Effect.fn("TodoService.addAndCount")(function*(title: string) { + yield* repo.create(title) + const todos = yield* repo.list + return todos.length + }) + + const titles = repo.list.pipe( + Effect.map((todos) => todos.map((todo) => todo.title)) + ) + + return TodoService.of({ + addAndCount, + titles + }) + }) + ) + + // You would also add a live layer here that provides real dependencies for + // production code. + // + // static readonly layer = Layer.effect(TodoService, ...).pipe( + // Layer.provide(TodoRepo.layer) + // ) + + static readonly layerTest = this.layerNoDeps.pipe( + // Provide the test repo layer as a dependency for the test service layer. + // Use `Layer.provideMerge` so the tests can also access the test repo + // directly if needed, as well as the test ref through the repo layer. + Layer.provideMerge(TodoRepo.layerTest) + ) +} + +// `layer(...)` creates one shared layer for the block and tears it down in +// `afterAll`, so all tests inside can access the same service context. +layer(TodoRepo.layerTest)("TodoRepo", (it) => { + it.effect("tests repository behavior", () => + Effect.gen(function*() { + const repo = yield* TodoRepo + const before = (yield* repo.list).length + assert.strictEqual(before, 0) + + yield* repo.create("Write docs") + + const after = (yield* repo.list).length + assert.strictEqual(after, 1) + })) + + it.effect("layer is shared", () => + Effect.gen(function*() { + const repo = yield* TodoRepo + const before = (yield* repo.list).length + assert.strictEqual(before, 1) + + yield* repo.create("Write docs again") + + // because the layer is shared between tests, the todo created in the + // previous test is still present, so the count should be 2, not 1 + const after = (yield* repo.list).length + assert.strictEqual(after, 2) + })) +}) + +describe("TodoService", () => { + it.effect("tests higher-level service logic", () => + Effect.gen(function*() { + const ref = yield* TodoRepoTestRef + const service = yield* TodoService + const count = yield* service.addAndCount("Review docs") + const titles = yield* service.titles + + assert.isTrue(count >= 1) + assert.isTrue(titles.some((title) => title.includes("Review docs"))) + + // You can also access the test ref directly to make assertions about the + // underlying data. + const todos = yield* Ref.get(ref) + assert.isTrue(todos.length >= 1) + }).pipe(Effect.provide(TodoService.layerTest))) +}) diff --git a/repos/effect/ai-docs/src/09_testing/index.md b/repos/effect/ai-docs/src/09_testing/index.md new file mode 100644 index 0000000..5df1fab --- /dev/null +++ b/repos/effect/ai-docs/src/09_testing/index.md @@ -0,0 +1 @@ +## Testing Effect programs diff --git a/repos/effect/ai-docs/src/10_predicate/01_basics.ts b/repos/effect/ai-docs/src/10_predicate/01_basics.ts new file mode 100644 index 0000000..8ff7832 --- /dev/null +++ b/repos/effect/ai-docs/src/10_predicate/01_basics.ts @@ -0,0 +1,14 @@ +/** + * @title Using the Predicate module + */ +import { Predicate } from "effect" + +const thing: unknown = { + a: 1 +} + +if (Predicate.isObject(thing)) { + if (Predicate.isNumber(thing.a)) { + console.log("number", thing.a) + } +} diff --git a/repos/effect/ai-docs/src/10_predicate/index.md b/repos/effect/ai-docs/src/10_predicate/index.md new file mode 100644 index 0000000..5149c32 --- /dev/null +++ b/repos/effect/ai-docs/src/10_predicate/index.md @@ -0,0 +1,9 @@ +## Runtime type guards + +The `Predicate` module contains small, reusable runtime checks. + +**NEVER** write your own helper functions like `isRecord` or `isString`, instead +use the helpers from the `Predicate` module. + +Predicates can be composed with apis such as `Predicate.and`, +`Predicate.or`, `Predicate.not`, and `Predicate.compose`. diff --git a/repos/effect/ai-docs/src/50_http-client/10_basics.ts b/repos/effect/ai-docs/src/50_http-client/10_basics.ts new file mode 100644 index 0000000..747e886 --- /dev/null +++ b/repos/effect/ai-docs/src/50_http-client/10_basics.ts @@ -0,0 +1,102 @@ +/** + * @title Getting started with HttpClient + * + * Define a service that uses the HttpClient module to fetch data from an external API + */ +import { Context, Effect, flow, Layer, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +class Todo extends Schema.Class("Todo")({ + userId: Schema.Number, + id: Schema.Number, + title: Schema.String, + completed: Schema.Boolean +}) {} + +export class JsonPlaceholder extends Context.Service, JsonPlaceholderError> + getTodo(id: number): Effect.Effect + createTodo(todo: Omit): Effect.Effect +}>()("app/JsonPlaceholder") { + static readonly layer = Layer.effect( + JsonPlaceholder, + Effect.gen(function*() { + // Access the HttpClient service, and apply some common middleware to all + // requests: + const client = (yield* HttpClient.HttpClient).pipe( + // Add a base URL to all requests made with this client, and set the + // Accept header to expect JSON responses + HttpClient.mapRequest(flow( + HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"), + HttpClientRequest.acceptJson + )), + // Fail if the response status is not 2xx + HttpClient.filterStatusOk, + // Retry transient errors (network issues, 5xx responses) with an + // exponential backoff. + // + // See the schedule documentation for more complex retry strategies. + HttpClient.retryTransient({ + schedule: Schedule.exponential(100), + times: 3 + }) + ) + + const allTodos = client.get("/todos").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(Todo))), + Effect.mapError((cause) => new JsonPlaceholderError({ cause })), + Effect.withSpan("JsonPlaceholder.allTodos") + ) + + // Use the HttpClient to fetch a todo item by id, and decode the response + // using the Todo schema. + const getTodo = Effect.fn("JsonPlaceholder.getTodo")(function*(id: number) { + // Annotate the current span with the id of the todo being fetched, so + // that it shows up in telemetry for this request. + yield* Effect.annotateCurrentSpan({ id }) + + const todo = yield* client.get(`/todos/${id}`, { + // You can pass additional options to individual requests. + // There are options for query parameters, request body, headers, and + // more. + urlParams: { format: "json" } + }).pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.mapError((cause) => new JsonPlaceholderError({ cause })) + ) + + return todo + }) + + // You can use the HttpClientRequest module to build up more complex + // requests: + const createTodo = Effect.fn("JsonPlaceholder.createTodo")(function*(todo: Omit) { + yield* Effect.annotateCurrentSpan({ title: todo.title }) + + const createdTodo = yield* HttpClientRequest.post("/todos").pipe( + // The HttpClientRequest module has many helper functions for building requests. + HttpClientRequest.setUrlParams({ format: "json" }), + HttpClientRequest.bodyJsonUnsafe(todo), + client.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.mapError((cause) => new JsonPlaceholderError({ cause })) + ) + + return createdTodo + }) + + return JsonPlaceholder.of({ + allTodos, + getTodo, + createTodo + }) + }) + ).pipe( + // Provide the fetch-based HttpClient implementation + Layer.provide(FetchHttpClient.layer) + ) +} + +export class JsonPlaceholderError extends Schema.TaggedErrorClass()("JsonPlaceholderError", { + cause: Schema.Defect() +}) {} diff --git a/repos/effect/ai-docs/src/50_http-client/index.md b/repos/effect/ai-docs/src/50_http-client/index.md new file mode 100644 index 0000000..7baa901 --- /dev/null +++ b/repos/effect/ai-docs/src/50_http-client/index.md @@ -0,0 +1,3 @@ +## Effect HttpClient + +Build http clients with the `HttpClient` module. diff --git a/repos/effect/ai-docs/src/51_http-server/10_basics.ts b/repos/effect/ai-docs/src/51_http-server/10_basics.ts new file mode 100644 index 0000000..79e92c9 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/10_basics.ts @@ -0,0 +1,116 @@ +/** + * @title Getting started with HttpApi + * + * Define a schema-first API, implement handlers, secure endpoints with + * middleware, serve it over HTTP, and call it using a generated typed client. + */ +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Context, Effect, flow, Layer, Schedule } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpApiBuilder, HttpApiClient, HttpApiMiddleware, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" +// Api definitions should **always** be seperate from the server implementation, +// so that they can be shared between the server and client without leaking +// server code into clients. +// Ideally, the would use a seperate package in a monorepo. +import { Api } from "./fixtures/api/Api.ts" +import { Authorization } from "./fixtures/api/Authorization.ts" +import { UsersApiHandlers } from "./fixtures/server/Users/http.ts" + +// This walkthrough focuses on runtime wiring and typed client usage. +// See the fixture files for the API schemas, endpoint definitions and handlers: + +const SystemApiHandlers = HttpApiBuilder.group( + Api, + "system", + Effect.fn(function*(handlers) { + return handlers.handle("health", () => Effect.void) + }) +) + +const ApiRoutes = HttpApiBuilder.layer(Api, { + openapiPath: "/openapi.json" +}).pipe( + // Provide all the handler Layers for the API. + Layer.provide([UsersApiHandlers, SystemApiHandlers]) +) + +// Define a /docs route that serves scalar documentation +const DocsRoute = HttpApiScalar.layer(Api, { + path: "/docs" +}) + +// Merge all the http routes together +const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute) + +// Create an HTTP server Layer that serves the API routes. +// +// Here we are using the NodeHttpServer, but you could also use the +// BunHttpServer +export const HttpServerLayer = HttpRouter.serve(AllRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +// Then run the server using Layer.launch +Layer.launch(HttpServerLayer).pipe( + NodeRuntime.runMain +) + +// Or create a web handler, which can be used in serverless environments +export const { handler, dispose } = HttpRouter.toWebHandler(AllRoutes.pipe( + Layer.provide(HttpServer.layerServices) +)) + +// ----------------- +// Client side setup +// ----------------- + +export const AuthorizationClient = HttpApiMiddleware.layerClient( + Authorization, + Effect.fn(function*({ next, request }) { + // Here you can modify the request and pass it down the middleware chain. + // This is where you would add authentication tokens, custom headers, etc. + // For this example, we just add a hardcoded bearer token to all requests. + return yield* next(HttpClientRequest.bearerToken(request, "dev-token")) + }) +) + +// Define the HttpApiClient service, which will be used to make requests to the +// API. +export class ApiClient extends Context.Service>()("acme/ApiClient") { + static readonly layer = Layer.effect( + ApiClient, + HttpApiClient.make(Api, { + // Use transformClient to apply middleware to the generated client. This + // is useful for settings the base url and applying retry policies. + transformClient: (client) => + client.pipe( + HttpClient.mapRequest(flow( + HttpClientRequest.prependUrl("http://localhost:3000") + )), + HttpClient.retryTransient({ + schedule: Schedule.exponential(100), + times: 3 + }) + ) + }) + ).pipe( + // Provide the client implementation of the Authorization middleware, which + // is required. + Layer.provide(AuthorizationClient), + // Supply a HttpClient implementation to use for making requests. Here we + // use the FetchHttpClient, but you could also use the NodeHttpClient or + // BunHttpClient. + Layer.provide(FetchHttpClient.layer) + ) +} + +// The generated client mirrors your API definition, so renames and schema +// changes are checked end-to-end at compile time. +export const callApi = Effect.gen(function*() { + const client = yield* ApiClient + + yield* client.health() +}).pipe( + Effect.provide(ApiClient.layer) +) diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Api.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Api.ts new file mode 100644 index 0000000..20140ae --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Api.ts @@ -0,0 +1,14 @@ +import { HttpApi, OpenApi } from "effect/unstable/httpapi" +import { SystemApi } from "./System.ts" +import { UsersApiGroup } from "./Users.ts" + +// Defined the root API, which combines all of the groups together. This is the +// API that you will serve and generate clients for. You can also annotate the +// API with OpenAPI metadata. +export class Api extends HttpApi.make("user-api") + .add(UsersApiGroup) + .add(SystemApi) + .annotateMerge(OpenApi.annotations({ + title: "Acme User API" + })) +{} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts new file mode 100644 index 0000000..8b25c04 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts @@ -0,0 +1,36 @@ +import { Context, Schema } from "effect" +import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import type { User } from "../domain/User.ts" + +export class CurrentUser extends Context.Service()("acme/HttpApi/Authorization/CurrentUser") {} + +export class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { + message: Schema.String + }, + // You can define error status codes directly on the error class + { httpApiStatus: 401 } +) {} + +export class Authorization extends HttpApiMiddleware.Service()("acme/HttpApi/Authorization", { + // This middleware requires clients to also provide an implementation, to + // inject a api key + requiredForClient: true, + // Middleware can optionally define security schemes, which are used to + // generate OpenAPI docs and decode credientials from incoming requests for + // you. + security: { + bearer: HttpApiSecurity.bearer + }, + // Middlware can specify errors that it may raise + error: Unauthorized +}) {} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/System.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/api/System.ts new file mode 100644 index 0000000..c66565b --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/api/System.ts @@ -0,0 +1,10 @@ +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" + +// Top level groups are added to the root of the derived HttpApiClient. +// +// `client.health()` +export class SystemApi extends HttpApiGroup.make("system", { topLevel: true }).add( + HttpApiEndpoint.get("health", "/health", { + success: HttpApiSchema.NoContent + }) +) {} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Users.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Users.ts new file mode 100644 index 0000000..f3b56d4 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/api/Users.ts @@ -0,0 +1,91 @@ +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { User, UserId } from "../domain/User.ts" +import { SearchQueryTooShort, UserNotFound } from "../domain/UserErrors.ts" +import { Authorization } from "./Authorization.ts" + +export class UsersApiGroup extends HttpApiGroup.make("users") + .add( + HttpApiEndpoint.get("list", "/", { + query: { + search: Schema.optional(Schema.String) + }, + success: Schema.Array(User) + }), + HttpApiEndpoint.get("search", "/search", { + // For get requests, payload uses the query string + payload: { + search: Schema.String + }, + success: [ + Schema.Array(User), + Schema.String.pipe(HttpApiSchema.asText({ + contentType: "text/csv" + })) + ], + error: [ + SearchQueryTooShort.pipe( + // If you want an error to return no content, you can use + // `HttpApiSchema.asNoContent` and provide a decoder that transforms the + // error into the appropriate type. + HttpApiSchema.asNoContent({ + decode: () => new SearchQueryTooShort() + }) + ), + // You can also add some of the built in `HttpApiError`s to handle common + // error cases like bad requests, unauthorized, etc. + HttpApiError.RequestTimeoutNoContent + ] + }), + HttpApiEndpoint.get("getById", "/:id", { + params: { + // Path parameter schemas need to be able to decode from strings. + // Schema.decodeTo can be used to "bridge" between schemas + id: Schema.FiniteFromString.pipe( + Schema.decodeTo(UserId) + ) + }, + success: User, + error: UserNotFound.pipe( + // If you want an error to return no content, you can use + // `HttpApiSchema.asNoContent` and provide a decoder that transforms the + // error into the appropriate type. + HttpApiSchema.asNoContent({ + decode: () => new UserNotFound() + }) + ) + }), + HttpApiEndpoint.post("create", "/", { + // For post requests, payload uses the request body. It defaults to JSON, + // but you can specify other content types as well using + // `HttpApiSchema.asText`, `HttpApiSchema.asMultipart`, etc. + payload: Schema.Struct({ + name: Schema.String, + email: Schema.String + }), + success: User + }), + HttpApiEndpoint.get("me", "/me", { + success: User, + error: UserNotFound.pipe(HttpApiSchema.status(404)) + }) + ) + // You can apply middleware to entire groups, which is useful for things like + // authentication and authorization. + // + // You can also apply middleware to individual endpoints if you need more + // fine-grained control. + .middleware(Authorization) + // To add a common prefix to all endpoints in a group, you can use the `prefix` + // method. This is useful for grouping related endpoints together under a common + // path segment. In this case, all endpoints in the `UsersApiGroup` will be + // prefixed with `/users`. + .prefix("/users") + // You can add OpenAPI annotations to groups, endpoints, and even parameters and + // request bodies. These will be merged together to generate the final OpenAPI + // docs for the API + .annotateMerge(OpenApi.annotations({ + title: "Users", + description: "User management endpoints" + })) +{} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/domain/User.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/domain/User.ts new file mode 100644 index 0000000..852e201 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/domain/User.ts @@ -0,0 +1,12 @@ +import { Schema } from "effect" + +export const UserId = Schema.Int.pipe( + Schema.brand("UserId") +) +export type UserId = typeof UserId.Type + +export class User extends Schema.Class("User")({ + id: UserId, + name: Schema.String, + email: Schema.String +}) {} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts new file mode 100644 index 0000000..fa2a757 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts @@ -0,0 +1,22 @@ +import { Schema } from "effect" + +export class UserNotFound extends Schema.TaggedErrorClass()( + "UserNotFound", + {}, + // You can specify the status code for this error inline + { httpApiStatus: 404 } +) {} + +export class SearchQueryTooShort + extends Schema.TaggedErrorClass()("SearchQueryTooShort", {}, { httpApiStatus: 422 }) +{ + static readonly minimumLength = 2 +} + +// Create a wrapper error class for all errors in the Users API. +// +// This prevents adding too many error types to services / endpoint definitions. +// +export class UsersError extends Schema.TaggedErrorClass()("UsersError", { + reason: Schema.Union([UserNotFound, SearchQueryTooShort]) +}) {} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Authorization.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Authorization.ts new file mode 100644 index 0000000..bbdbed3 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Authorization.ts @@ -0,0 +1,36 @@ +import { Effect, Layer, Redacted } from "effect" +import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts" +import { User, UserId } from "../domain/User.ts" + +// The implementation of the Authorization middleware. It is seperate from the +// service definition to avoid leaking it into a client. +export const AuthorizationLayer = Layer.effect( + Authorization, + Effect.gen(function*() { + // Here you could access services required by the middleware, like a + // database or an external auth provider. + yield* Effect.logInfo("Starting Authorization middleware") + + return Authorization.of({ + bearer: Effect.fn(function*(httpEffect, { credential }) { + // Validate the token and return an Unauthorized error if it's invalid. + const token = Redacted.value(credential) + if (token !== "dev-token") { + return yield* new Unauthorized({ message: "Missing or invalid bearer token" }) + } + + // Provide the current user to the rest of the stack. This will be + // available in any endpoint or middleware that runs after this one. + return yield* Effect.provideService( + httpEffect, + CurrentUser, + new User({ + id: UserId.make(1), + name: "Dev User", + email: "dev@acme.com" + }) + ) + }) + }) + }) +) diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users.ts new file mode 100644 index 0000000..a983376 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users.ts @@ -0,0 +1,62 @@ +import { Context, Effect, Layer, Ref } from "effect" +import { User, UserId } from "../domain/User.ts" +import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts" + +export class Users extends Context.Service, UsersError> + getById(id: UserId): Effect.Effect + create(input: { readonly name: string; readonly email: string }): Effect.Effect +}>()("acme/Users") { + static readonly layer = Layer.effect( + Users, + Effect.gen(function*() { + const users = new Map([ + [ + 1, + new User({ + id: UserId.make(1), + name: "Admin", + email: "admin@acme.dev" + }) + ] + ]) + const nextId = yield* Ref.make(2) + + const list = Effect.fn("UsersRepo.list")(function*(search: string | undefined) { + const allUsers = Array.from(users.values()) + if (search === undefined || search.length === 0) { + return allUsers + } else if (search.length < SearchQueryTooShort.minimumLength) { + return yield* new UsersError({ + reason: new SearchQueryTooShort() + }) + } + yield* Effect.annotateCurrentSpan({ search }) + const normalized = search.toLowerCase() + return allUsers.filter((user) => + user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized) + ) + }) + + const getById = Effect.fn("UsersRepo.getById")(function*(id: UserId) { + yield* Effect.annotateCurrentSpan({ id }) + const user = users.get(id) + if (user === undefined) { + return yield* new UsersError({ + reason: new UserNotFound() + }) + } + return user + }) + + const create = Effect.fn("UsersRepo.create")(function*(input: { readonly name: string; readonly email: string }) { + const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1) + const user = new User({ id: UserId.make(id), ...input }) + users.set(user.id, user) + return user + }) + + return Users.of({ list, getById, create }) + }) + ) +} diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts new file mode 100644 index 0000000..a1b7b3e --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts @@ -0,0 +1,71 @@ +import { Effect, Layer } from "effect" +import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" +import { Api } from "../../api/Api.ts" +import { CurrentUser } from "../../api/Authorization.ts" +import { AuthorizationLayer } from "../Authorization.ts" +import { Users } from "../Users.ts" + +export const UsersApiHandlers = HttpApiBuilder.group( + Api, + "users", + Effect.fn(function*(handlers) { + const users = yield* Users + + return handlers + .handle("list", ({ query }) => + users.list(query.search).pipe( + // The list endpoint expects no errors, so we convert any potential + // errors into a 500 Internal Server Error. + Effect.orDie + )) + .handle( + "search", + Effect.fn(function*({ payload }) { + if (payload.search === "bad-request") { + // You can use the built in error types like any other + // Schema.TaggedErrorClass + return yield* new HttpApiError.RequestTimeout() + } + return yield* users.list(payload.search).pipe( + Effect.catchReason( + "UsersError", + "SearchQueryTooShort", + // Re-fail the "SearchQueryTooShort" reason + Effect.fail, + // All other reasons are unexpected, so we convert them into a 500 + // Internal Server Error. + Effect.die + ) + ) + }) + ) + .handle("getById", ({ params }) => + users.getById(params.id).pipe( + // You can also use Effect.catchReasons to handle multiple error + // reasons at once + Effect.catchReasons("UsersError", { + UserNotFound: (e) => Effect.fail(e) + }, Effect.die) + )) + .handle("create", ({ payload }) => + users.create(payload).pipe( + Effect.orDie + // You could alse use Effect.unwrapReason to moves rror reasons up to + // the top level, so you can handle them with Effect.catch or + // Effect.catchTag etc. + // + // Effect.unwrapReason("UsersError"), + // Effect.catchTags({ + // UserNotFound: Effect.die, + // SearchQueryTooShort: Effect.die + // }) + )) + .handle("me", () => + // The Authorization middleware provides the CurrentUser service, so we + // can access it here. + CurrentUser) + }) +).pipe( + // Provide the dependencies for the handlers. + Layer.provide([Users.layer, AuthorizationLayer]) +) diff --git a/repos/effect/ai-docs/src/51_http-server/index.md b/repos/effect/ai-docs/src/51_http-server/index.md new file mode 100644 index 0000000..e926d99 --- /dev/null +++ b/repos/effect/ai-docs/src/51_http-server/index.md @@ -0,0 +1,3 @@ +## Building HttpApi servers + +`HttpApi` gives you schema-first, type-safe HTTP APIs with runtime validation, typed clients, and OpenAPI docs from one definition. diff --git a/repos/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts b/repos/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts new file mode 100644 index 0000000..3d2866a --- /dev/null +++ b/repos/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts @@ -0,0 +1,117 @@ +/** + * @title Working with child processes + * + * This example shows how to collect process output, compose pipelines, and stream long-running command output. + */ +import { NodeServices } from "@effect/platform-node" +import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" + +export class DevToolsError extends Schema.TaggedErrorClass()("DevToolsError", { + cause: Schema.Defect() +}) {} + +export class DevTools extends Context.Service + readonly recentCommitSubjects: Effect.Effect, DevToolsError> + readonly runLintFix: Effect.Effect + changedTypeScriptFiles(baseRef: string): Effect.Effect, DevToolsError> +}>()("docs/DevTools") { + static readonly layer = Layer.effect( + DevTools, + Effect.gen(function*() { + // To run child processes, we need access to a `ChildProcessSpawner`. + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + // Use `spawner.string` when you want to collect the entire output of a + // command as a string. This runs `node --version` and collects the + // output. + const nodeVersion = spawner.string( + ChildProcess.make("node", ["--version"]) + ).pipe( + Effect.map(String.trim), + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + const changedTypeScriptFiles = Effect.fn("DevTools.changedTypeScriptFiles")(function*(baseRef: string) { + yield* Effect.annotateCurrentSpan({ baseRef }) + + // `spawner.lines` is a convenience helper for line-oriented command + // output. + const files = yield* spawner.lines( + ChildProcess.make("git", ["diff", "--name-only", `${baseRef}...HEAD`]) + ).pipe( + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + return files.filter((file) => file.endsWith(".ts")) + }) + + // Build a pipeline from two command values. This runs: + // `git log --pretty=format:%s -n 20 | head -n 5` + const recentCommitSubjects = spawner.lines( + ChildProcess.make("git", ["log", "--pretty=format:%s", "-n", "20"]).pipe( + ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"])) + ) + ).pipe( + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + const runLintFix = Effect.gen(function*() { + // Use `spawn` when you want the process handle and stream output while + // the process is still running. + const handle = yield* spawner.spawn( + ChildProcess.make("pnpm", ["lint-fix"], { + env: { FORCE_COLOR: "1" }, + extendEnv: true + }) + ).pipe( + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + yield* handle.all.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.runForEach((line) => Console.log(`[lint-fix] ${line}`)), + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + const exitCode = yield* handle.exitCode.pipe( + Effect.mapError((cause) => new DevToolsError({ cause })) + ) + + if (exitCode !== ChildProcessSpawner.ExitCode(0)) { + return yield* new DevToolsError({ + cause: new Error(`pnpm lint-fix failed with exit code ${exitCode}`) + }) + } + }).pipe( + // `spawner.spawn` adds a `Scope` requirement to manage the lifecycle of + // the child process. We can use `Effect.scoped` to provide a `Scope` + // and close it when the effect completes. + Effect.scoped + ) + + return DevTools.of({ + nodeVersion, + changedTypeScriptFiles, + recentCommitSubjects, + runLintFix + }) + }) + ).pipe( + // Provide the `ChildProcessSpawner` dependency from `NodeServices.layer`. + Layer.provide(NodeServices.layer) + ) +} + +export const program = Effect.gen(function*() { + const tools = yield* DevTools + + const version = yield* tools.nodeVersion + yield* Effect.log(`node=${version}`) +}).pipe( + // `ChildProcess` requires a platform implementation of + // `ChildProcessSpawner`. In Node.js, `NodeServices.layer` provides it. + Effect.provide(DevTools.layer) +) diff --git a/repos/effect/ai-docs/src/60_child-process/index.md b/repos/effect/ai-docs/src/60_child-process/index.md new file mode 100644 index 0000000..ae0016b --- /dev/null +++ b/repos/effect/ai-docs/src/60_child-process/index.md @@ -0,0 +1,3 @@ +## Working with child processes + +Use the `effect/unstable/process` modules to define child processes and run them with `ChildProcessSpawner`. diff --git a/repos/effect/ai-docs/src/70_cli/10_basics.ts b/repos/effect/ai-docs/src/70_cli/10_basics.ts new file mode 100644 index 0000000..a4fe368 --- /dev/null +++ b/repos/effect/ai-docs/src/70_cli/10_basics.ts @@ -0,0 +1,136 @@ +/** + * @title Getting started with Effect CLI modules + * + * Build a command-line app with typed arguments and flags, then wire subcommand + * handlers into a single executable command. + */ +import { NodeRuntime, NodeServices } from "@effect/platform-node" +import { Console, Effect } from "effect" +import { Argument, Command, Flag } from "effect/unstable/cli" + +// You can define flags outside of commands and reuse them across multiple +// commands. +const workspace = Flag.string("workspace").pipe( + Flag.withAlias("w"), + Flag.withDescription("Workspace to operate on"), + Flag.withDefault("personal") +) + +// Start with a root command and explicitly share the parent flags that should +// be available to all subcommands. +const tasks = Command.make("tasks").pipe( + Command.withSharedFlags({ + workspace, + verbose: Flag.boolean("verbose").pipe( + Flag.withAlias("v"), + Flag.withDescription("Print diagnostic output") + ) + }), + Command.withDescription("Track and manage tasks") +) + +const create = Command.make( + "create", + { + title: Argument.string("title").pipe( + Argument.withDescription("Task title") + ), + priority: Flag.choice("priority", ["low", "normal", "high"]).pipe( + Flag.withDescription("Priority for the new task"), + Flag.withDefault("normal") + ) + }, + Effect.fn(function*({ title, priority }) { + // Subcommands can read parent command input by yielding the parent command. + const root = yield* tasks + + if (root.verbose) { + yield* Console.log(`workspace=${root.workspace} action=create`) + } + + yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`) + }) +).pipe( + Command.withDescription("Create a task"), + Command.withExamples([ + { + command: "tasks create \"Ship 4.0\" --priority high", + description: "Create a high-priority task" + } + ]) +) + +const list = Command.make( + "list", + { + status: Flag.choice("status", ["open", "done", "all"]).pipe( + Flag.withDescription("Filter tasks by status"), + Flag.withDefault("open") + ), + json: Flag.boolean("json").pipe( + Flag.withDescription("Print machine-readable output") + ) + }, + Effect.fn(function*({ status, json }) { + const root = yield* tasks + const items = [ + { title: "Ship 4.0", status: "open" }, + { title: "Update onboarding guide", status: "done" } + ] as const + const filtered = status === "all" + ? items + : items.filter((item) => item.status === status) + + if (root.verbose) { + yield* Console.log(`workspace=${root.workspace} action=list`) + } + + if (json) { + yield* Console.log(JSON.stringify( + { + workspace: root.workspace, + status, + items: filtered + }, + null, + 2 + )) + return + } + + yield* Console.log(`Listing ${status} tasks in ${root.workspace}`) + if (filtered.length === 0) { + yield* Console.log("- No tasks found") + return + } + + for (const item of filtered) { + yield* Console.log(`- ${item.title}`) + } + }) +).pipe( + Command.withDescription("List tasks"), + Command.withAlias("ls"), + Command.withExamples([ + { + command: "tasks --workspace team-a list --status open", + description: "List open tasks in a specific workspace" + }, + { + command: "tasks --workspace team-b ls --status open", + description: "List open tasks in another workspace" + } + ]) +) + +// Finally, compose the subcommands into a single command and then run it. +tasks.pipe( + Command.withSubcommands([create, list]), + Command.run({ + version: "1.0.0" + }), + // Provide the services for the platform you are targeting. In this case, + // Node.js + Effect.provide(NodeServices.layer), + NodeRuntime.runMain +) diff --git a/repos/effect/ai-docs/src/70_cli/index.md b/repos/effect/ai-docs/src/70_cli/index.md new file mode 100644 index 0000000..96a3b75 --- /dev/null +++ b/repos/effect/ai-docs/src/70_cli/index.md @@ -0,0 +1,5 @@ +## Building CLI applications + +Use the "effect/unstable/cli" modules to build CLI applications. These modules +provide utilities for parsing command-line arguments, handling user input, and +managing the flow of a CLI application. diff --git a/repos/effect/ai-docs/src/71_ai/10_language-model.ts b/repos/effect/ai-docs/src/71_ai/10_language-model.ts new file mode 100644 index 0000000..2f89956 --- /dev/null +++ b/repos/effect/ai-docs/src/71_ai/10_language-model.ts @@ -0,0 +1,156 @@ +/** + * @title Using LanguageModel for text, objects, and streams + * + * Configure a provider once, then use `LanguageModel` for plain text + * generation, schema-validated object generation, and streaming responses. + */ +import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic" +import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" +import { Config, Context, Effect, ExecutionPlan, Layer, Schema, Stream } from "effect" +import { AiError, LanguageModel, Model, type Response } from "effect/unstable/ai" +import { FetchHttpClient } from "effect/unstable/http" +import { LaunchPlan } from "./fixtures/domain/LaunchPlan.ts" + +// You can use Config to create ai clients +const AnthropicClientLayer = AnthropicClient.layerConfig({ + apiKey: Config.redacted("ANTHROPIC_API_KEY") +}).pipe( + // Providers typically require an HttpClient, but you can choose which one to + // use. + Layer.provide(FetchHttpClient.layer) +) + +const OpenAiClientLayer = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") +}).pipe( + Layer.provide(FetchHttpClient.layer) +) + +export class AiWriterError extends Schema.TaggedErrorClass()("AiWriterError", { + // AiErrorReason is a Schema, so we can include it directly in our custom + // error schema. + reason: AiError.AiErrorReason +}) { + static fromAiError(error: AiError.AiError) { + return new AiWriterError({ + reason: error.reason + }) + } +} + +// You can use `ExecutionPlan` to define a strategy for trying multiple +// providers with different configurations. In this example, we try a cheaper +// OpenAI model first, then fall back to a more expensive Anthropic model if the +// first one fails. +const DraftPlan = ExecutionPlan.make( + { + provide: OpenAiLanguageModel.model("gpt-5.2"), + // Attempt to use the openai model up to 3 times before falling back to the + // anthropic model. + attempts: 3 + }, + { + provide: AnthropicLanguageModel.model("claude-opus-4-6"), + attempts: 2 + } +) + +export class AiWriter extends Context.Service + extractLaunchPlan(notes: string): Effect.Effect + streamReleaseHighlights(version: string): Stream.Stream +}>()("docs/AiWriter") { + static readonly layer = Layer.effect( + AiWriter, + Effect.gen(function*() { + // Calling `captureRequirements` on an `ExecutionPlan` will move the + // requirements of the plan (in this case the ai clients) into the Layer + // requirements. + const draftsModel = yield* DraftPlan.captureRequirements + + // Use a different model for the launch plan extraction + const launchPlanModel = yield* OpenAiLanguageModel.model("gpt-4.1").captureRequirements + + const draftAnnouncement = Effect.fn("AiWriter.draftAnnouncement")( + function*(product: string) { + const model = yield* LanguageModel.LanguageModel + const provider = yield* Model.ProviderName + const response = yield* model.generateText({ + prompt: `Write a short launch announcement for ${product}. ` + + "Keep it concise and include one concrete user benefit." + }) + + // `LanguageModel.generateText` exposes convenience fields so you can + // inspect usage and finish reason without parsing content parts. + yield* Effect.logInfo( + `${provider} finished with ${response.finishReason}. outputTokens=${response.usage.outputTokens.total}` + ) + + return { + provider, + text: response.text + } + }, + // To apply an `ExecutionPlan`, we use `Effect.withExecutionPlan` + Effect.withExecutionPlan(draftsModel), + // Map AiError into our custom error type + Effect.mapError((error) => AiWriterError.fromAiError(error)) + ) + + const extractLaunchPlan = Effect.fn("AiWriter.extractLaunchPlan")( + function*(notes: string) { + const model = yield* LanguageModel.LanguageModel + const response = yield* model.generateObject({ + objectName: "launch_plan", + prompt: + "Convert these notes into a launch plan object with audience, channels, launchDate, summary, and keyRisks:\n" + + notes, + // The generated object is validated and decoded through this schema. + schema: LaunchPlan + }) + + return response.value + }, + // The .model(...) apis return a Layer that can be used with + // Effect.provide + Effect.provide(launchPlanModel), + // Map AiError into our custom error type + Effect.mapError((error) => AiWriterError.fromAiError(error)) + ) + + const streamReleaseHighlights = (version: string) => + LanguageModel.streamText({ + prompt: `Write release highlights for version ${version} as a short bulleted list.` + }).pipe( + Stream.filter((part): part is Response.TextDeltaPart => part.type === "text-delta"), + Stream.map((part) => part.delta), + Stream.provide(launchPlanModel), + // Map AiError into our custom error type + Stream.mapError((error) => AiWriterError.fromAiError(error)) + ) + + return AiWriter.of({ + draftAnnouncement, + extractLaunchPlan, + streamReleaseHighlights + }) + }) + ).pipe( + // This Layer has requirements for both the OpenAI and Anthropic clients, + // since the ExecutionPlan includes models from both providers. + Layer.provide([OpenAiClientLayer, AnthropicClientLayer]) + ) +} + +// We can now use `AiWriter` like any other Effect service. +export const program: Effect.Effect< + void, + AiWriterError, + AiWriter +> = Effect.gen(function*() { + const writer = yield* AiWriter + yield* writer.draftAnnouncement("Effect Cloud") +}) diff --git a/repos/effect/ai-docs/src/71_ai/20_tools.ts b/repos/effect/ai-docs/src/71_ai/20_tools.ts new file mode 100644 index 0000000..0acd8e9 --- /dev/null +++ b/repos/effect/ai-docs/src/71_ai/20_tools.ts @@ -0,0 +1,226 @@ +/** + * @title Defining and using AI tools + * + * Define tools with schemas, group them into toolkits, implement handlers, + * and pass them to `LanguageModel.generateText`. + */ +import { OpenAiClient, OpenAiLanguageModel, OpenAiTool } from "@effect/ai-openai" +import { Config, Context, Effect, Layer, Schema } from "effect" +import { AiError, LanguageModel, Tool, Toolkit } from "effect/unstable/ai" +import { FetchHttpClient } from "effect/unstable/http" + +// --------------------------------------------------------------------------- +// 1. Defining tools +// --------------------------------------------------------------------------- + +const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({ + description: "A unique identifier for a product, e.g. 'p-123'" +}) + +class Product extends Schema.Class("acme/domain/Product")({ + id: ProductId, + name: Schema.String, + price: Schema.Number +}) {} + +// Each tool has a name, an optional description, a parameters schema that the +// model fills in, and a success schema for the handler result. The description +// is shown to the model to help it decide when to call the tool. +const SearchProducts = Tool.make("SearchProducts", { + description: "Search the product catalog by keyword", + parameters: Schema.Struct({ + query: Schema.String.annotate({ + // Add a description to individual parameters for even better model + // guidance. + description: "The search query, e.g. 'wireless headphones'" + }), + maxResults: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(10))).annotate({ + description: "The maximum number of results to return" + }) + }), + success: Schema.Array(Product), + // The strategy used for handling errors returned from tool call handler + // execution. + // + // If set to `"error"` (the default), errors that occur during tool call handler + // execution will be returned in the error channel of the calling effect. + // + // If set to `"return"`, errors that occur during tool call handler execution + // will be captured and returned as part of the tool call result. + failureMode: "error" +}) + +const GetInventory = Tool.make("GetInventory", { + description: "Check current stock level for a product", + parameters: Schema.Struct({ + productId: ProductId + }), + success: Schema.Struct({ + productId: ProductId, + available: Schema.Number + }) +}) + +// --------------------------------------------------------------------------- +// 2. Grouping tools into a Toolkit +// --------------------------------------------------------------------------- + +// `Toolkit.make` accepts any number of tools and produces a typed toolkit that +// knows the names and schemas of every tool it contains. +const ProductToolkit = Toolkit.make(SearchProducts, GetInventory) + +// --------------------------------------------------------------------------- +// 3. Implementing handlers via toLayer +// --------------------------------------------------------------------------- + +// `toLayer` returns a `Layer` that satisfies the handler requirements for every +// tool in the toolkit. Each handler receives the decoded parameters and returns +// an Effect producing the success type. +const ProductToolkitLayer = ProductToolkit.toLayer(Effect.gen(function*() { + yield* Effect.log("Initializing ProductToolkitLive") + // Here you could access other services or resources needed to implement the + // handlers, e.g. a database client or external API client. + // + // const client = yield* SomeDatabaseClient + return ProductToolkit.of({ + SearchProducts: Effect.fn("ProductToolkit.SearchProducts")(function*({ query, maxResults }) { + return [ + new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }), + new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 }) + ].slice(0, maxResults) + }), + GetInventory: Effect.fn("ProductToolkit.GetInventory")(function*({ productId }) { + return { productId, available: 42 } + }) + }) +})) + +// --------------------------------------------------------------------------- +// 4. Using tools with LanguageModel +// --------------------------------------------------------------------------- + +// Provider setup (same pattern as the language-model example). +const OpenAiClientLayer = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") +}).pipe(Layer.provide(FetchHttpClient.layer)) + +export class ProductAssistantError extends Schema.TaggedErrorClass()( + "ProductAssistantError", + { reason: AiError.AiErrorReason } +) {} + +// Wrap tool-enabled generation in a service +export class ProductAssistant extends Context.Service +}>()("docs/ProductAssistant") { + static readonly layer = Layer.effect( + ProductAssistant, + Effect.gen(function*() { + // Access the toolkit's handlers by yielding the toolkit definition. + const toolkit = yield* ProductToolkit + + // Choose a model to use + const model = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements + + const answer = Effect.fn("ProductAssistant.answer")( + function*(question: string) { + // Pass the toolkit to `generateText`. The model can call any tool in + // the toolkit; the framework resolves parameters, invokes handlers, + // and feeds results back automatically. + const response = yield* LanguageModel.generateText({ + prompt: question, + toolkit, + // You can set `toolChoice` to "required" to force the model to call + // a tool before responding with text. + // + // By default it is set to "auto" + toolChoice: "required" + }) + + // ------------------------------------------------------------------- + // 5. Inspecting tool calls and results + // ------------------------------------------------------------------- + + // `response.toolCalls` lists every tool the model invoked, each with + // the tool name, a unique id, and the decoded parameters. + for (const call of response.toolCalls) { + yield* Effect.log(`Tool call: ${call.name} id=${call.id}`) + } + + // `response.toolResults` lists the resolved results, each with the + // tool name, id, decoded result, and an `isFailure` flag. + for (const result of response.toolResults) { + yield* Effect.log( + `Tool result: ${result.name} id=${result.id} isFailure=${result.isFailure}` + ) + } + + return { + text: response.text, + toolCallCount: response.toolCalls.length + } + }, + // Provide the chosen model to use + Effect.provide(model), + (_) => _, + // Map AI errors into our domain error type + Effect.catchTag( + "AiError", + (error) => + Effect.fail( + new ProductAssistantError({ + reason: error.reason + }) + ), + // For unexpected errors, die with the original error + (e) => Effect.die(e) + ) + ) + + return ProductAssistant.of({ answer }) + }) + ).pipe( + // The toolkit handler layer must be provided so the framework can invoke + // the tool handlers when the model makes tool calls. + Layer.provide(ProductToolkitLayer), + // Also provide the openai client required by OpenAiLanguageModel.model + Layer.provide(OpenAiClientLayer) + ) +} + +// --------------------------------------------------------------------------- +// 6. Provider-defined tools +// --------------------------------------------------------------------------- + +// Some providers offer built-in tools (web search, code interpreter, etc.) +// that run server-side. Use `Tool.providerDefined` or the pre-built +// definitions from provider packages. + +// OpenAI's web search tool is pre-defined in `@effect/ai-openai`. Calling it +// produces a tool instance that can be merged into any toolkit. +const webSearch = OpenAiTool.WebSearch({ + search_context_size: "medium" +}) + +// Combine user-defined and provider-defined tools in a single toolkit. +const AssistantToolkit = Toolkit.make(SearchProducts, GetInventory, webSearch) + +// Only user-defined tools that require handlers appear in `toLayer`. The +// provider-defined `WebSearch` is executed server-side by the provider. +export const AssistantToolkitLayer = AssistantToolkit.toLayer(Effect.gen(function*() { + yield* Effect.log("Initializing AssistantToolkitLive") + return AssistantToolkit.of({ + SearchProducts: Effect.fn("AssistantToolkit.SearchProducts")(function*({ query, maxResults }) { + return [ + new Product({ id: ProductId.make("p-1"), name: `${query} widget`, price: 19.99 }), + new Product({ id: ProductId.make("p-2"), name: `${query} gadget`, price: 29.99 }) + ].slice(0, maxResults) + }), + GetInventory: Effect.fn("AssistantToolkit.GetInventory")(function*({ productId }) { + return { productId, available: 42 } + }) + }) +})) diff --git a/repos/effect/ai-docs/src/71_ai/30_chat.ts b/repos/effect/ai-docs/src/71_ai/30_chat.ts new file mode 100644 index 0000000..6000170 --- /dev/null +++ b/repos/effect/ai-docs/src/71_ai/30_chat.ts @@ -0,0 +1,158 @@ +/** + * @title Stateful chat sessions + * + * The AI `Chat` module maintains conversation history automatically. Build + * AI agents or chat assistants. + */ +import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" +import { Config, Context, DateTime, Effect, Layer, Ref, Schema } from "effect" +import { AiError, Chat, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { FetchHttpClient } from "effect/unstable/http" + +// --------------------------------------------------------------------------- +// Provider setup +// --------------------------------------------------------------------------- + +const OpenAiClientLayer = OpenAiClient.layerConfig({ + apiKey: Config.redacted("OPENAI_API_KEY") +}).pipe(Layer.provide(FetchHttpClient.layer)) + +// --------------------------------------------------------------------------- +// Tools for the agentic loop +// --------------------------------------------------------------------------- + +const Tools = Toolkit.make(Tool.make("getCurrentTime", { + description: "Get the current time in ISO format", + parameters: Schema.Struct({ + id: Schema.String + }), + success: Schema.String +})) + +const ToolsLayer = Tools.toLayer(Effect.gen(function*() { + yield* Effect.logDebug("Initializing tools...") + return Tools.of({ + getCurrentTime: Effect.fn("Tools.getCurrentTime")(function*(_) { + const now = yield* DateTime.now + return DateTime.formatIso(now) + }) + }) +})) + +// --------------------------------------------------------------------------- +// Service that wraps Chat for a domain use-case +// --------------------------------------------------------------------------- + +export class AiAssistantError extends Schema.TaggedErrorClass()("AiAssistantError", { + reason: AiError.AiErrorReason +}) { + static fromAiError(error: AiError.AiError) { + return new AiAssistantError({ reason: error.reason }) + } +} + +export class AiAssistant extends Context.Service + // Ask a question and use an agentic loop with tool calls to answer it. + agent(question: string): Effect.Effect +}>()("acme/AiAssistant") { + static readonly layer = Layer.effect( + AiAssistant, + Effect.gen(function*() { + // Choose the model you want to use for the chat sessions. + const modelLayer = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements + + // --------------------------------------------------------------------------- + // 1. Chat.empty — basic multi-turn conversation + // --------------------------------------------------------------------------- + + // Create a new chat session with `Chat.empty` or `Chat.fromPrompt`. The + // session maintains conversation history automatically, so you can focus on + // the current turn without having to manage context. + const newSession = yield* Chat.fromPrompt(Prompt.empty.pipe( + Prompt.setSystem("You are a helpful assistant that answers questions.") + )) + + // You can also create a chat using a json export. + const json = yield* newSession.exportJson + const session = yield* Chat.fromJson(json) + + const chat = Effect.fn("AiAssistant.chat")( + function*(message: string) { + // Create a new turn in the conversation by passing the user's message + // to `session.generateText`. + const response = yield* session.generateText({ prompt: message }).pipe( + // Provide the model layer to use. + // You could potentially use different models for different turns, + // or even switch models in the middle of a conversation. + Effect.provide(modelLayer) + ) + + // You can inspect the accumulated history at any point through the + // `history` ref on the chat instance. + const history = yield* Ref.get(session.history) + yield* Effect.logInfo( + `Conversation has ${history.content.length} messages` + ) + + return response.text + }, + Effect.mapError((error) => AiAssistantError.fromAiError(error)) + ) + + // --------------------------------------------------------------------------- + // 2. Create agentic loops with tools + // --------------------------------------------------------------------------- + + const tools = yield* Tools + const agent = Effect.fn("AiAssistant.agent")( + function*(question: string) { + // We start the agent with a system prompt and the user question. The + // agent can then call tools in a loop until it decides to return a + // final answer. + const session = yield* Chat.fromPrompt([ + { role: "system", content: "You are an assistant that can use tools to answer questions." }, + { role: "user", content: question } + ]) + + while (true) { + const response = yield* session.generateText({ + prompt: [], // No additional prompt — the model has full access to the conversation history + toolkit: tools // Provide the tools to the model + }).pipe( + // Provide the model layer to use. + // You could potentially use different models for different turns, + // or even switch models in the middle of a conversation. + Effect.provide(modelLayer) + ) + if (response.toolCalls.length > 0) { + // If the model called any tools, execute them and the Chat module + // will automatically add the tool results to the conversation + // history before the next turn. + continue + } + // If there are no tool calls, the model has returned a final answer + // and we can exit the loop. + return response.text + } + }, + // Remap AI errors to our domain-specific error type, but die on + // unexpected errors. + Effect.catchTag( + "AiError", + (error) => Effect.fail(AiAssistantError.fromAiError(error)), + (e) => Effect.die(e) + ) + ) + + return AiAssistant.of({ + chat, + agent + }) + }) + ).pipe( + // Provide the OpenAI client and tools layers to the AiAssistant service. + Layer.provide([OpenAiClientLayer, ToolsLayer]) + ) +} diff --git a/repos/effect/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts b/repos/effect/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts new file mode 100644 index 0000000..6dd8d33 --- /dev/null +++ b/repos/effect/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts @@ -0,0 +1,9 @@ +import { Schema } from "effect" + +export class LaunchPlan extends Schema.Class("LaunchPlan")({ + audience: Schema.Literals(["developers", "operators", "platform teams"]), + channels: Schema.Array(Schema.String), + launchDate: Schema.String, + summary: Schema.String, + keyRisks: Schema.Array(Schema.String) +}) {} diff --git a/repos/effect/ai-docs/src/71_ai/index.md b/repos/effect/ai-docs/src/71_ai/index.md new file mode 100644 index 0000000..425f4b4 --- /dev/null +++ b/repos/effect/ai-docs/src/71_ai/index.md @@ -0,0 +1,5 @@ +## Working with AI modules + +Effect's AI modules provide a provider-agnostic interface for language models. +You can generate text, decode structured objects with `Schema` and stream partial +responses. diff --git a/repos/effect/ai-docs/src/80_cluster/10_entities.ts b/repos/effect/ai-docs/src/80_cluster/10_entities.ts new file mode 100644 index 0000000..a1be374 --- /dev/null +++ b/repos/effect/ai-docs/src/80_cluster/10_entities.ts @@ -0,0 +1,97 @@ +/** + * @title Defining cluster entities + * + * Define distributed entity RPCs and run them in a cluster. + */ +import { NodeClusterSocket, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Ref, Schema } from "effect" +import { ClusterSchema, Entity, TestRunner } from "effect/unstable/cluster" +import { Rpc } from "effect/unstable/rpc" +import type { SqlClient } from "effect/unstable/sql" + +export const Increment = Rpc.make("Increment", { + payload: { amount: Schema.Number }, + success: Schema.Number +}) + +export const GetCount = Rpc.make("GetCount", { + success: Schema.Number +}) + // If you want GetCount messages to be persisted, you can annotate the RPC + // schema with `ClusterSchema.Persisted`. + // + // By default, messages are volatile and only sent over a network. + .annotate(ClusterSchema.Persisted, true) + +// `Entity.make` takes an array of Rpc definitions +export const Counter = Entity.make("Counter", [Increment, GetCount]) + +// Entity handlers can keep in-memory state while the entity is active. +// `maxIdleTime` controls passivation: if the entity is idle long enough, it is +// stopped and later recreated on demand. +export const CounterEntityLayer = Counter.toLayer( + Effect.gen(function*() { + const count = yield* Ref.make(0) + + return Counter.of({ + Increment: ({ payload }) => Ref.updateAndGet(count, (current) => current + payload.amount), + GetCount: () => + Ref.get(count).pipe( + // Add Rpc.fork to allow the GetCount handler to run concurrently with + // Increment handlers. + // + // This opts-out of the default behavior where all handlers for a + // given entity run sequentially. + Rpc.fork + ) + }) + }), + { maxIdleTime: "5 minutes" } +) + +// If you ever need to access an entity client, you can use the `client` +// property on the entity definition. +export const useCounter = Effect.gen(function*() { + const clientFor = yield* Counter.client + const counter = clientFor("counter-123") + + const afterIncrement = yield* counter.Increment({ amount: 1 }) + const currentCount = yield* counter.GetCount() + + console.log(`Count after increment: ${afterIncrement}, current count: ${currentCount}`) +}) + +// `SingleRunner.layer` is useful for local development / tests where you still +// want the cluster entity runtime model. +declare const SqlClientLayer: Layer.Layer + +// Create the cluster layer using `NodeClusterSocket.layer` +const ClusterLayer = NodeClusterSocket.layer().pipe( + Layer.provide(SqlClientLayer) +) + +// You can also use `TestRunner.layer` to run your entities in a single process, +// without any network communication and in-memory storage. This is useful for testing and +// development. +const ClusterLayerTest = TestRunner.layer + +// Merge all your entity layers together and provide the cluster layer to run +// them in a cluster. +const EntitiesLayer = Layer.mergeAll( + CounterEntityLayer +) + +const ProductionLayer = EntitiesLayer.pipe( + Layer.provide(ClusterLayer) +) + +export const TestLayer = EntitiesLayer.pipe( + // For testing, we can use `Layer.provideMerge` to tests can access storage + // and other cluster services directly. + Layer.provideMerge(ClusterLayerTest) +) + +// Finally, run your app with the entities layer. +Layer.launch(ProductionLayer).pipe( + NodeRuntime.runMain +) diff --git a/repos/effect/ai-docs/src/80_cluster/index.md b/repos/effect/ai-docs/src/80_cluster/index.md new file mode 100644 index 0000000..1cc42cb --- /dev/null +++ b/repos/effect/ai-docs/src/80_cluster/index.md @@ -0,0 +1,4 @@ +## Building distributed applications with cluster + +The cluster modules let you model stateful services as entities and distribute +them across multiple machines. diff --git a/repos/effect/ai-docs/src/index.md b/repos/effect/ai-docs/src/index.md new file mode 100644 index 0000000..092209d --- /dev/null +++ b/repos/effect/ai-docs/src/index.md @@ -0,0 +1,12 @@ +# Effect library documentation + +This documentation resides in the Effect monorepo, which contains the source +code for the Effect library and its related packages. + +When you need to find any information about the Effect library, only use this +documentation and the source code found in `./packages`. Do not use +`node_modules` or any other external documentation, as it may be outdated or +incorrect. + +**Note**: The examples in this documentation contain comments for illustration +purposes. In practice, you would not include these comments in your code. diff --git a/repos/effect/ai-docs/tsconfig.json b/repos/effect/ai-docs/tsconfig.json new file mode 100644 index 0000000..94d6035 --- /dev/null +++ b/repos/effect/ai-docs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.base.json", + "include": ["src/**/*.ts", "src/**/*.tsx"], + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "types": ["node"], + "plugins": [ + { + "name": "@effect/language-service", + "namespaceImportPackages": [], + "diagnosticSeverity": { + "floatingEffect": "off", + "preferSchemaOverJson": "off" + } + } + ] + } +} diff --git a/repos/effect/cookbooks/schedule.md b/repos/effect/cookbooks/schedule.md new file mode 100644 index 0000000..9991181 --- /dev/null +++ b/repos/effect/cookbooks/schedule.md @@ -0,0 +1,563 @@ +# Schedule Cookbook + +Use this cookbook when you need to define a `Schedule` value. The examples are +ordered from small single-purpose policies to larger real-world policies that +combine timing, input classification, output shaping, and observation. + +This cookbook intentionally defines schedules only. It does not apply them with +`Effect.retry`, `Effect.repeat`, streams, or channels. + +## Before Choosing A Schedule + +- `Schedule.recurs(n)` counts recurrences after the first run. +- `Schedule.spaced` waits after each completed run; `Schedule.fixed` uses an + aligned cadence; `Schedule.windowed` recurs on window boundaries. +- `Schedule.duration` performs exactly one recurrence after the duration. +- `Schedule.during` is an elapsed-time budget, not a delay by itself. +- Schedule output is policy output. Use `Schedule.passthrough` to preserve the + latest input, or `Schedule.map` to derive a new output from schedule metadata. +- `Schedule.max` continues only while all schedules continue and outputs the slowest delay. +- `Schedule.min` continues while any schedule can continue and outputs the fastest delay. +- `Schedule.jittered` spreads callers out. It does not add a recurrence limit. +- `Schedule.addDelay` adds extra delay based on schedule metadata. +- `Schedule.modifyDelay` replaces or adjusts the selected delay. +- Leave unbounded schedules to explicitly owned background work. + +## Choose By Problem Shape + +| Problem shape | Start with | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Bounded retry | `Schedule.exponential` or `Schedule.fibonacci`, then `Schedule.recurs` and optionally `Schedule.during` | +| Poll latest status | `Schedule.spaced` or `Schedule.fixed`, then `Schedule.setInputType`, `Schedule.passthrough`, and `Schedule.while` | +| Adapt delay from metadata | `Schedule.addDelay` | +| Replace or cap selected delay | `Schedule.modifyDelay` | +| Run phases in sequence | `Schedule.andThen` | +| Preserve phase in output | `Schedule.andThenResult` | +| Continue while all policies continue | `Schedule.max` | +| Continue while any policy continues | `Schedule.min` | +| Shape output from metadata | `Schedule.map` | +| Observe decisions without changing output | `Schedule.tap` | + +## Table Of Contents + +1. [Single-Policy Schedules](#single-policy-schedules) +2. [Shape Schedule Outputs](#shape-schedule-outputs) +3. [Combine Policies](#combine-policies) +4. [Work With Inputs](#work-with-inputs) +5. [Adapt Delays](#adapt-delays) +6. [Observe Schedule Decisions](#observe-schedule-decisions) +7. [Realistic Policies](#realistic-policies) + +## Single-Policy Schedules + +### Retry A Profile Fetch Three Times + +Goal: Create a retry policy for loading a user profile that allows at most 3 +recurrences and outputs the recurrence count. + +```ts +import { Schedule } from "effect" + +const profileRetry = Schedule.recurs(3) +``` + +### Poll A Queue Once Per Second + +Goal: Create a cadence for polling queue depth about 1 second after each +completed check. + +```ts +import { Schedule } from "effect" + +const queueDepthPolling = Schedule.spaced("1 second") +``` + +### Run A Heartbeat On An Aligned Cadence + +Goal: Create a heartbeat cadence that runs on aligned 30-second boundaries +instead of waiting 30 seconds after each run finishes. + +```ts +import { Schedule } from "effect" + +const heartbeatCadence = Schedule.fixed("30 seconds") +``` + +### Warm A Cache Once After Deployment + +Goal: Create a follow-up cache warmup that recurs exactly once after about 1 +minute. + +```ts +import { Schedule } from "effect" + +const cacheWarmupFollowUp = Schedule.duration("1 minute") +``` + +### Keep A Health Probe Inside A Time Budget + +Goal: Create a budget that allows health probes to continue only while about 1 +minute has elapsed or less. + +```ts +import { Schedule } from "effect" + +const healthProbeBudget = Schedule.during("1 minute") +``` + +### Retry A Config Fetch With Exponential Backoff + +Goal: Create a retry policy for fetching remote configuration, starting with a +100 millisecond exponential backoff. + +```ts +import { Schedule } from "effect" + +const configFetchBackoff = Schedule.exponential("100 millis") +``` + +### Probe A Cold Replica With Fibonacci Backoff + +Goal: Create a gentler startup probe for a cold search replica, using Fibonacci +backoff and taking the first 4 outputs. + +```ts +import { Schedule } from "effect" + +const searchReplicaWarmup = Schedule.fibonacci("100 millis").pipe( + Schedule.upTo({ times: 4 }) +) +``` + +### Run A Worker Loop Forever + +Goal: Create an unbounded worker-loop counter with no added delay. + +```ts +import { Schedule } from "effect" + +const workerLoopCounter = Schedule.forever +``` + +### Trigger A Nightly Report + +Goal: Create a schedule for a nightly billing report at 02:00. + +```ts +import { Schedule } from "effect" + +const nightlyBillingReport = Schedule.cron("0 2 * * *") +``` + +### Sample Five-Minute Windows + +Goal: Create a schedule that recurs on 5-minute window boundaries. + +```ts +import { Schedule } from "effect" + +const fiveMinuteWindows = Schedule.windowed("5 minutes") +``` + +## Shape Schedule Outputs + +### Echo Feature Flag Inputs + +Goal: Create a schedule for feature flag snapshots that immediately outputs +each input unchanged and takes the first 3 samples. + +```ts +import { Schedule } from "effect" + +type FeatureFlagSnapshot = { readonly enabled: boolean } + +const featureFlagSamples = Schedule.identity().pipe( + Schedule.upTo({ times: 3 }) +) +``` + +### Label Retry Attempts + +Goal: Create a retry-attempt schedule that turns recurrence count metadata into +labels such as `attempt-1`, `attempt-2`, and `attempt-3`. + +```ts +import { Schedule } from "effect" + +const retryAttemptLabels = Schedule.recurs(3).pipe( + Schedule.map(({ output: count }) => `attempt-${count + 1}`) +) +``` + +Explanation: `Schedule.map` receives the full step metadata. Destructure +`output` when you only need the schedule output, or use fields such as `input`, +`attempt`, `duration`, and `elapsed` when the new output needs more context. + +## Combine Policies + +### Add Jitter To Webhook Backoff + +Goal: Create a webhook retry backoff that starts at 200 milliseconds, adds +jitter, and takes 3 outputs. + +```ts +import { Schedule } from "effect" + +const jitteredWebhookBackoff = Schedule.exponential("200 millis").pipe( + Schedule.jittered, + Schedule.upTo({ times: 3 }) +) +``` + +### Stop Deployment Hook Retries By Count And Time + +Goal: Create a deployment hook retry budget that uses jittered exponential +backoff, allows at most 5 recurrences, and also stops after about 20 seconds. + +```ts +import { Schedule } from "effect" + +const deploymentHookRetryBudget = Schedule.max([ + Schedule.exponential("200 millis").pipe(Schedule.jittered), + Schedule.recurs(5), + Schedule.during("20 seconds") +]) +``` + +Explanation: `Schedule.max` stops when any schedule stops and outputs the +slowest selected delay for each recurrence. + +### Continue While Any Probe Is Active + +Goal: Create a service readiness policy that continues while either 2 immediate +warmup probes or a slower 500 millisecond probe schedule still wants to recur, +using the fastest selected delay. + +```ts +import { Schedule } from "effect" + +const readinessWarmupOrSlowProbe = Schedule.min([ + Schedule.recurs(2), + Schedule.spaced("500 millis").pipe(Schedule.upTo({ times: 5 })) +]) +``` + +Explanation: `Schedule.min` keeps recurring while at least one schedule can +continue and outputs the fastest selected delay among schedules that are still +recurring. + +### Warm Up Fast, Then Settle Into Maintenance + +Goal: Create a cache invalidation sequence that runs 2 quick recurrences 100 +milliseconds apart, then 3 slower recurrences 30 seconds apart, then stops. + +```ts +import { Schedule } from "effect" + +const cacheInvalidationSequence = Schedule.spaced("100 millis").pipe( + Schedule.upTo({ times: 2 }), + Schedule.andThen(Schedule.spaced("30 seconds").pipe(Schedule.upTo({ times: 3 }))) +) +``` + +### Preserve Warmup And Steady Phases + +Goal: Create a retry classifier with a fast exponential phase and a steady +Fibonacci phase, preserving the phase in the output. + +```ts +import { Result, Schedule } from "effect" + +const phasedRetryClassifier = Schedule.exponential("100 millis").pipe( + Schedule.upTo({ times: 2 }), + Schedule.andThenResult(Schedule.fibonacci("500 millis").pipe(Schedule.upTo({ times: 3 }))), + Schedule.map(({ output: result }) => + Result.match(result, { + onFailure: (delay) => ({ phase: "fast", delay }), + onSuccess: (delay) => ({ phase: "steady", delay }) + }) + ) +) +``` + +Explanation: `Schedule.andThenResult` keeps phase information in the output. +The first schedule is represented by the failure side, and the second schedule +is represented by the success side. + +## Work With Inputs + +### Poll Upload Progress Until Complete + +Goal: Create an upload-progress schedule that waits about 1 second between +checks, outputs the latest progress object, and continues only while the upload +is incomplete. + +```ts +import { Schedule } from "effect" + +type UploadProgress = { readonly percent: number } + +const uploadProgressUntilComplete = Schedule.spaced("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.percent < 100) +) +``` + +Explanation: `Schedule.setInputType` tells TypeScript which input each step +receives. `Schedule.passthrough` makes the output the latest input, so a polling +schedule can return the final status instead of a counter. + +## Adapt Delays + +### Slow A Queue Consumer Under Backpressure + +Goal: Create a queue backpressure schedule that outputs each queue snapshot, +continues while the queue is not paused, adds 5 seconds of delay when depth is +above 1000, adds 500 milliseconds otherwise, and takes 10 outputs. + +```ts +import { Effect, Schedule } from "effect" + +type QueueSnapshot = { readonly depth: number; readonly paused: boolean } + +const queueBackpressureSchedule = Schedule.identity().pipe( + Schedule.while(({ input }) => !input.paused), + Schedule.addDelay(({ output: snapshot }) => Effect.succeed(snapshot.depth > 1000 ? "5 seconds" : "500 millis")), + Schedule.upTo({ times: 10 }) +) +``` + +Explanation: `Schedule.addDelay` receives the full step metadata and adds the +returned delay to the selected delay. It is a good fit for input-driven pacing +when the output is already the latest input. + +### Cap WebSocket Reconnect Delays + +Goal: Create a reconnect policy that uses jittered exponential backoff, caps +selected delays at 5 seconds, allows at most 8 recurrences, and outputs the +selected delay. + +```ts +import { Duration, Effect, Schedule } from "effect" + +const websocketReconnectDelays = Schedule.max([ + Schedule.exponential("100 millis").pipe( + Schedule.jittered, + Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.seconds(5)))) + ), + Schedule.recurs(8) +]) +``` + +Explanation: `Schedule.modifyDelay` receives the full step metadata, including +the selected delay as `duration`, and returns the replacement delay. Use it for +caps, floors, clamps, or provider-provided delay hints. + +## Observe Schedule Decisions + +### Log Heartbeat Inputs + +Goal: Create a heartbeat schedule that runs on an aligned 10-second cadence, +logs each service id input, outputs the input unchanged, and takes 2 outputs. + +```ts +import { Console, Schedule } from "effect" + +type HeartbeatStatus = { readonly id: string } + +const heartbeatInputLogs = Schedule.fixed("10 seconds").pipe( + Schedule.setInputType(), + Schedule.tap(({ input }) => Console.log(`heartbeat:${input.id}`)), + Schedule.passthrough, + Schedule.upTo({ times: 2 }) +) +``` + +### Record Backoff Delays + +Goal: Create a retry schedule that uses Fibonacci backoff, takes 5 outputs, and +logs each selected delay without changing the schedule output. + +```ts +import { Console, Schedule } from "effect" + +const loggedBackoffDelays = Schedule.fibonacci("200 millis").pipe( + Schedule.upTo({ times: 5 }), + Schedule.tap(({ output: delay }) => Console.log(delay)) +) +``` + +### Log Attempt Metadata + +Goal: Create a telemetry backoff that logs each attempt number and selected +delay in milliseconds without changing the schedule output. + +```ts +import { Console, Duration, Schedule } from "effect" + +const telemetryBackoffPolicy = Schedule.exponential("250 millis").pipe( + Schedule.upTo({ times: 5 }), + Schedule.tap(({ attempt, output }) => Console.log(`attempt-${attempt}: ${Duration.toMillis(output)}ms`)) +) +``` + +Explanation: use `Schedule.tap` to observe inputs, outputs, and metadata such as +attempt number or selected duration without changing the schedule output. + +## Realistic Policies + +### Retry An HTTP Gateway With A Delay Envelope + +Goal: Create an HTTP gateway retry schedule. Retry only network failures, status +429, and status 500, 502, or 503. Use jittered exponential backoff starting at +100 milliseconds, cap selected delays at 2 seconds, allow at most 6 recurrences, +and output the selected delay. + +```ts +import { Duration, Effect, Schedule } from "effect" + +type GraphqlGatewayError = + | { readonly _tag: "Network" } + | { readonly _tag: "HttpStatus"; readonly status: number } + | { readonly _tag: "BadRequest" } + +const isRetryableGraphqlGatewayError = ( + error: GraphqlGatewayError +): boolean => + error._tag === "Network" || + (error._tag === "HttpStatus" && + (error.status === 429 || + error.status === 500 || + error.status === 502 || + error.status === 503)) + +const graphqlGatewayRetry = Schedule.max([ + Schedule.exponential("100 millis").pipe( + Schedule.jittered, + Schedule.setInputType(), + Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.seconds(2)))) + ), + Schedule.recurs(6) +]).pipe( + Schedule.while(({ input }) => isRetryableGraphqlGatewayError(input)) +) +``` + +### Poll A Rollout With A Deadline + +Goal: Create a rollout watcher that starts from an aligned 1-second cadence, +jitters the selected delay, outputs the latest status, continues only while the +rollout is running, and stops after about 2 minutes. + +```ts +import { Duration, Schedule } from "effect" + +type RolloutStatus = { + readonly state: "running" | "succeeded" | "failed" +} + +const rolloutStatusWatcher = Schedule.fixed("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.jittered, + Schedule.while(({ input, elapsed }) => + input.state === "running" && + Duration.isLessThanOrEqualTo(Duration.millis(elapsed), Duration.minutes(2)) + ) +) +``` + +### Respect A Provider Retry-After Header + +Goal: Create a provider retry schedule. Retry status 429, 500, and 503. Use +exponential backoff starting at 1 second. When `retryAfter` is present, use it as +a lower bound. Cap selected delays at 1 minute, allow at most 6 recurrences, and +output the selected delay. + +```ts +import { Duration, Effect, Schedule } from "effect" + +type PushProviderResponse = { + readonly status: 429 | 500 | 503 | 400 + readonly retryAfter: Duration.Duration | undefined +} + +const pushNotificationProviderRetry = Schedule.max([ + Schedule.exponential("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.modifyDelay(({ output: response, duration }) => + Effect.succeed( + Duration.min( + response.retryAfter === undefined + ? duration + : Duration.max(duration, response.retryAfter), + Duration.minutes(1) + ) + ) + ) + ), + Schedule.recurs(6) +]).pipe( + Schedule.while(({ input }) => input.status === 429 || input.status === 500 || input.status === 503) +) +``` + +### Poll An OAuth Device Code Flow + +Goal: Create an OAuth device-code polling schedule. Poll every 5 seconds, add +another 5 seconds for `slow_down`, output the latest input, continue only for +`authorization_pending` and `slow_down`, and stop after about 15 minutes. + +```ts +import { Duration, Effect, Schedule } from "effect" + +type OAuthDeviceCodeStatus = { + readonly error: + | "authorization_pending" + | "slow_down" + | "access_denied" + | "expired_token" +} + +const oauthDeviceCodePolling = Schedule.spaced("5 seconds").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.addDelay(({ output: status }) => Effect.succeed(status.error === "slow_down" ? "5 seconds" : "0 millis")), + Schedule.while(({ input, elapsed }) => + (input.error === "authorization_pending" || input.error === "slow_down") && + Duration.isLessThanOrEqualTo(Duration.millis(elapsed), Duration.minutes(15)) + ) +) +``` + +### Escalate Incident Notifications In Phases + +Goal: Create an incident escalation cadence that emits 3 recurrences spaced 1 +minute apart, then 3 recurrences spaced 5 minutes apart, then switches to an +aligned 15-minute cadence. + +```ts +import { Schedule } from "effect" + +const incidentEscalationCadence = Schedule.spaced("1 minute").pipe( + Schedule.upTo({ times: 3 }), + Schedule.andThen(Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 }))), + Schedule.andThen(Schedule.fixed("15 minutes")) +) +``` + +### Run Maintenance After A Warmup + +Goal: Create a maintenance schedule that performs one warmup recurrence after +about 30 seconds, then switches to a cron schedule that recurs every day at +03:00. + +```ts +import { Schedule } from "effect" + +const maintenanceCronAfterWarmup = Schedule.duration("30 seconds").pipe( + Schedule.andThen(Schedule.cron("0 3 * * *")) +) +``` diff --git a/repos/effect/deno.json b/repos/effect/deno.json new file mode 100644 index 0000000..835202d --- /dev/null +++ b/repos/effect/deno.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json", + "nodeModulesDir": "manual", + "unstable": ["bare-node-builtins", "node-globals"], + "workspace": ["./packages/*"], + "exclude": [ + "**/*.mjs", + "**/*.cjs", + "**/*.js", + "**/*.d.ts", + "**/vitest.*.ts", + "**/.tsbuildinfo/", + "**/node_modules/", + "**/dist/", + "ai-docs/", + "bundle/", + "examples/", + "scripts/", + "scratchpad/", + "packages/*/typetest/", + "packages/*/typeperf/", + "packages/*/benchmark/", + "packages/ai", + "packages/atom", + "packages/effect/test/cluster/", + "packages/opentelemetry/", + "packages/platform-browser/", + "packages/platform-bun/", + "packages/platform-node/", + "packages/platform-node-shared/", + "packages/tools/", + "packages/sql" + ] +} diff --git a/repos/effect/docker-compose.yaml b/repos/effect/docker-compose.yaml new file mode 100644 index 0000000..1a41e02 --- /dev/null +++ b/repos/effect/docker-compose.yaml @@ -0,0 +1,18 @@ +services: + pg: + image: postgres:alpine # Using a lightweight Postgres image + environment: + POSTGRES_DB: effect_cluster + POSTGRES_USER: cluster + POSTGRES_PASSWORD: cluster + ports: + - "5432:5432" # Map host port 5432 to container port 5432 + volumes: + - db_data:/var/lib/postgresql/data # Persist data in a named volume + redis: + image: redis:alpine + ports: + - "6379:6379" + +volumes: + db_data: # Define the named volume diff --git a/repos/effect/dprint.json b/repos/effect/dprint.json new file mode 100644 index 0000000..a500b54 --- /dev/null +++ b/repos/effect/dprint.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://dprint.dev/schemas/v0.json", + "incremental": false, + "includes": ["**/*.{ts,tsx,js,jsx,json,md}"], + "indentWidth": 2, + "lineWidth": 120, + "newLineKind": "lf", + "typescript": { + "semiColons": "asi", + "quoteStyle": "alwaysDouble", + "trailingCommas": "never", + "operatorPosition": "maintain", + "arrowFunction.useParentheses": "force" + }, + "excludes": [ + "LLMS.md", + "**/dist", + "**/build", + "**/docs", + "**/coverage", + "packages/**/CHANGELOG.md", + "!scratchpad/**/*", + ".agents", + ".context", + ".specs" + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.93.4.wasm", + "https://plugins.dprint.dev/markdown-0.20.0.wasm", + "https://plugins.dprint.dev/json-0.21.1.wasm" + ] +} diff --git a/repos/effect/flake.lock b/repos/effect/flake.lock new file mode 100644 index 0000000..44f1b5b --- /dev/null +++ b/repos/effect/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1783279667, + "narHash": "sha256-/NAkDSsve+GNM0Bt6tleJdCGfsTlK89nPjkVOzZMo0s=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "f205b5574fd0cb7da5b702a2da51507b7f4fdd1b", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/repos/effect/flake.nix b/repos/effect/flake.nix new file mode 100644 index 0000000..7685bee --- /dev/null +++ b/repos/effect/flake.nix @@ -0,0 +1,24 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + }; + outputs = {nixpkgs, ...}: let + forAllSystems = function: + nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( + system: function nixpkgs.legacyPackages.${system} + ); + in { + formatter = forAllSystems (pkgs: pkgs.alejandra); + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = with pkgs; [ + bun + deno + corepack + nodejs_26 + python3 + ]; + }; + }); + }; +} diff --git a/repos/effect/jsdocs.config.json b/repos/effect/jsdocs.config.json new file mode 100644 index 0000000..07ea6b9 --- /dev/null +++ b/repos/effect/jsdocs.config.json @@ -0,0 +1,16 @@ +{ + "tsconfig": "tsconfig.packages.json", + "include": [ + "packages/**/src/*.ts", + "packages/**/src/**/*.ts" + ], + "exclude": [ + "**/node_modules/**", + "packages/tools/**", + "packages/**/src/index.ts", + "packages/**/src/*Generated.ts", + "packages/**/src/internal/**", + "packages/**/src/**/internal/**" + ], + "output": ".data/jsdocs.json" +} diff --git a/repos/effect/migration/cause.md b/repos/effect/migration/cause.md new file mode 100644 index 0000000..0c223cd --- /dev/null +++ b/repos/effect/migration/cause.md @@ -0,0 +1,163 @@ +# Cause: Flattened Structure + +In v3, `Cause` was a recursive tree with six variants: + +``` +Empty | Fail | Die | Interrupt | Sequential | Parallel +``` + +The `Sequential` and `Parallel` variants composed causes into a tree to +represent errors from finalizers or concurrent operations. + +In v4, `Cause` has been flattened to a simple wrapper around an array of +`Reason` values: + +```ts +interface Cause { + readonly reasons: ReadonlyArray> +} + +type Reason = Fail | Die | Interrupt +``` + +There are only three reason variants — `Fail`, `Die`, and `Interrupt`. The +`Empty`, `Sequential`, and `Parallel` variants have been removed. An empty +cause is represented by an empty `reasons` array. Multiple failures (from +concurrent or sequential composition) are collected into a flat array. + +## Accessing Reasons + +**v3** — pattern match on the recursive tree structure: + +```ts +import { Cause } from "effect" + +const handle = (cause: Cause.Cause) => { + switch (cause._tag) { + case "Fail": + return cause.error + case "Die": + return cause.defect + case "Empty": + return undefined + case "Sequential": + return handle(cause.left) + case "Parallel": + return handle(cause.left) + case "Interrupt": + return cause.fiberId + } +} +``` + +**v4** — iterate over the flat `reasons` array: + +```ts +import { Cause } from "effect" + +const handle = (cause: Cause.Cause) => { + for (const reason of cause.reasons) { + switch (reason._tag) { + case "Fail": + return reason.error + case "Die": + return reason.defect + case "Interrupt": + return reason.fiberId + } + } +} +``` + +## Reason Guards + +The v3 type-level guards (`isFailType`, `isDieType`, `isInterruptType`, etc.) +have been replaced by reason-level guards: + +| v3 | v4 | +| ------------------------------- | --------------------------------- | +| `Cause.isEmptyType(cause)` | `cause.reasons.length === 0` | +| `Cause.isFailType(cause)` | `Cause.isFailReason(reason)` | +| `Cause.isDieType(cause)` | `Cause.isDieReason(reason)` | +| `Cause.isInterruptType(cause)` | `Cause.isInterruptReason(reason)` | +| `Cause.isSequentialType(cause)` | Removed | +| `Cause.isParallelType(cause)` | Removed | + +## Cause-Level Predicates + +| v3 | v4 | +| -------------------------------- | -------------------------------- | +| `Cause.isFailure(cause)` | `Cause.hasFails(cause)` | +| `Cause.isDie(cause)` | `Cause.hasDies(cause)` | +| `Cause.isInterrupted(cause)` | `Cause.hasInterrupts(cause)` | +| `Cause.isInterruptedOnly(cause)` | `Cause.hasInterruptsOnly(cause)` | + +## Constructors + +| v3 | v4 | +| ------------------------------- | ---------------------------- | +| `Cause.empty` | `Cause.empty` | +| `Cause.fail(error)` | `Cause.fail(error)` | +| `Cause.die(defect)` | `Cause.die(defect)` | +| `Cause.interrupt(fiberId)` | `Cause.interrupt(fiberId)` | +| `Cause.sequential(left, right)` | `Cause.combine(left, right)` | +| `Cause.parallel(left, right)` | `Cause.combine(left, right)` | + +In v4, `Cause.combine` concatenates the `reasons` arrays of two causes. The +distinction between sequential and parallel composition is no longer +represented in the data structure. + +## Extractors + +| v3 | v4 | +| ------------------------------ | ------------------------------------------ | +| `Cause.failureOption(cause)` | `Cause.findErrorOption(cause)` | +| `Cause.failureOrCause(cause)` | `Cause.findError(cause)` | +| `Cause.dieOption(cause)` | `Cause.findDefect(cause)` | +| `Cause.interruptOption(cause)` | `Cause.findInterrupt(cause)` | +| `Cause.failures(cause)` | `cause.reasons.filter(Cause.isFailReason)` | +| `Cause.defects(cause)` | `cause.reasons.filter(Cause.isDieReason)` | +| `Cause.interruptors(cause)` | `Cause.interruptors(cause)` | + +Note: `findError` and `findDefect` return `Result.Result` instead of `Option`. +Use `findErrorOption` for the `Option`-based variant. + +## Error Classes + +All `*Exception` classes have been renamed to `*Error`: + +| v3 | v4 | +| -------------------------------------- | ----------------------------- | +| `Cause.NoSuchElementException` | `Cause.NoSuchElementError` | +| `Cause.TimeoutException` | `Cause.TimeoutError` | +| `Cause.IllegalArgumentException` | `Cause.IllegalArgumentError` | +| `Cause.ExceededCapacityException` | `Cause.ExceededCapacityError` | +| `Cause.UnknownException` | `Cause.UnknownError` | +| `Cause.RuntimeException` | Removed | +| `Cause.InterruptedException` | Removed | +| `Cause.InvalidPubSubCapacityException` | Removed | + +The corresponding guards follow the same pattern: + +| v3 | v4 | +| -------------------------------------- | ---------------------------------- | +| `Cause.isNoSuchElementException(u)` | `Cause.isNoSuchElementError(u)` | +| `Cause.isTimeoutException(u)` | `Cause.isTimeoutError(u)` | +| `Cause.isIllegalArgumentException(u)` | `Cause.isIllegalArgumentError(u)` | +| `Cause.isExceededCapacityException(u)` | `Cause.isExceededCapacityError(u)` | +| `Cause.isUnknownException(u)` | `Cause.isUnknownError(u)` | + +## New in v4 + +- **`Cause.fromReasons(reasons)`** — construct a `Cause` from an array of + `Reason` values. +- **`Cause.makeFailReason(error)`**, **`Cause.makeDieReason(defect)`**, + **`Cause.makeInterruptReason(fiberId)`** — construct individual `Reason` + values. +- **`Cause.annotate(cause, annotations)`** — attach annotations to a `Cause`. +- **`Cause.findFail(cause)`**, **`Cause.findDie(cause)`**, + **`Cause.findInterrupt(cause)`** — extract specific reason types using + the `Result` module. +- **`Cause.filterInterruptors(cause)`** — extract interrupting fiber IDs as + a `Result`. +- **`Cause.Done`** — a graceful completion signal for queues and streams. diff --git a/repos/effect/migration/equality.md b/repos/effect/migration/equality.md new file mode 100644 index 0000000..d5e2352 --- /dev/null +++ b/repos/effect/migration/equality.md @@ -0,0 +1,72 @@ +# Equality + +## Structural Equality by Default + +In v3, `Equal.equals` used **reference equality** for plain objects and arrays. +Structural comparison was only available inside a `structuralRegion`, which +temporarily enabled deep comparison. Outside a structural region, two distinct +objects with identical contents were not considered equal: + +```ts +// v3 +import { Equal } from "effect" + +Equal.equals({ a: 1 }, { a: 1 }) // false — reference equality +Equal.equals([1, 2], [1, 2]) // false — reference equality +``` + +In v4, `Equal.equals` uses **structural equality** by default. Plain objects, +arrays, `Map`s, `Set`s, `Date`s, and `RegExp`s are compared by value without +opting in: + +```ts +// v4 +import { Equal } from "effect" + +Equal.equals({ a: 1 }, { a: 1 }) // true +Equal.equals([1, [2, 3]], [1, [2, 3]]) // true +Equal.equals(new Map([["a", 1]]), new Map([["a", 1]])) // true +Equal.equals(new Set([1, 2]), new Set([1, 2])) // true +``` + +Objects that implement the `Equal` interface continue to use their custom +equality logic, same as v3. + +## Opting Out: `byReference` + +If you need reference equality for a specific object, v4 provides +`Equal.byReference` and `Equal.byReferenceUnsafe`: + +```ts +import { Equal } from "effect" + +const obj = Equal.byReference({ a: 1 }) +Equal.equals(obj, { a: 1 }) // false — reference equality +``` + +- **`byReference(obj)`** — creates a `Proxy` that uses reference equality, + leaving the original object unchanged. +- **`byReferenceUnsafe(obj)`** — marks the object itself for reference + equality without creating a proxy. More performant but permanently changes + how the object is compared. + +## `NaN` Equality + +In v3, `Equal.equals(NaN, NaN)` returned `false` (following IEEE 754). +In v4, `NaN` is considered equal to `NaN`: + +```ts +Equal.equals(NaN, NaN) // v3: false, v4: true +``` + +## `equivalence` → `asEquivalence` + +The function that wraps `equals` as an `Equivalence` has been renamed: + +```ts +// v3 +Equal.equivalence() + +// v4 +Equal.asEquivalence() +``` diff --git a/repos/effect/migration/error-handling.md b/repos/effect/migration/error-handling.md new file mode 100644 index 0000000..93ab919 --- /dev/null +++ b/repos/effect/migration/error-handling.md @@ -0,0 +1,106 @@ +# Error Handling: `catch*` Renamings + +The `catch` combinators on `Effect` have been renamed in v4. The general +pattern: `catchAll*` is shortened to `catch*`, and the `catchSome*` family is +replaced by `catchFilter` / `catchCauseFilter`. + +## Renamings + +| v3 | v4 | +| ------------------------ | ------------------------------ | +| `Effect.catchAll` | `Effect.catch` | +| `Effect.catchAllCause` | `Effect.catchCause` | +| `Effect.catchAllDefect` | `Effect.catchDefect` | +| `Effect.catchTag` | `Effect.catchTag` (unchanged) | +| `Effect.catchTags` | `Effect.catchTags` (unchanged) | +| `Effect.catchIf` | `Effect.catchIf` (unchanged) | +| `Effect.catchSome` | `Effect.catchFilter` | +| `Effect.catchSomeCause` | `Effect.catchCauseFilter` | +| `Effect.catchSomeDefect` | Removed | + +## `Effect.catchAll` → `Effect.catch` + +**v3** + +```ts +import { Effect } from "effect" + +const program = Effect.fail("error").pipe( + Effect.catchAll((error) => Effect.succeed(`recovered: ${error}`)) +) +``` + +**v4** + +```ts +import { Effect } from "effect" + +const program = Effect.fail("error").pipe( + Effect.catch((error) => Effect.succeed(`recovered: ${error}`)) +) +``` + +## `Effect.catchAllCause` → `Effect.catchCause` + +**v3** + +```ts +import { Effect } from "effect" + +const program = Effect.die("defect").pipe( + Effect.catchAllCause((cause) => Effect.succeed("recovered")) +) +``` + +**v4** + +```ts +import { Cause, Effect } from "effect" + +const program = Effect.die("defect").pipe( + Effect.catchCause((cause) => Effect.succeed("recovered")) +) +``` + +## `Effect.catchSome` → `Effect.catchFilter` + +In v3, `catchSome` took a function returning `Option`. In v4, +`catchFilter` uses the `Filter` module instead. + +**v3** + +```ts +import { Effect, Option } from "effect" + +const program = Effect.fail(42).pipe( + Effect.catchSome((error) => + error === 42 + ? Option.some(Effect.succeed("caught")) + : Option.none() + ) +) +``` + +**v4** + +```ts +import { Effect, Filter } from "effect" + +const program = Effect.fail(42).pipe( + Effect.catchFilter( + Filter.fromPredicate((error: number) => error === 42), + (error) => Effect.succeed("caught") + ) +) +``` + +## New in v4 + +- **`Effect.catchReason(errorTag, reasonTag, handler)`** — catches a specific + `reason` within a tagged error without removing the parent error from the + error channel. Useful for handling nested error causes (e.g. an `AiError` + with a `reason: RateLimitError | QuotaExceededError`). +- **`Effect.catchReasons(errorTag, cases)`** — like `catchReason` but handles + multiple reason tags at once via an object of handlers. +- **`Effect.catchEager(handler)`** — an optimization variant of `catch` that + evaluates synchronous recovery effects immediately. diff --git a/repos/effect/migration/fiber-keep-alive.md b/repos/effect/migration/fiber-keep-alive.md new file mode 100644 index 0000000..fe8efa3 --- /dev/null +++ b/repos/effect/migration/fiber-keep-alive.md @@ -0,0 +1,74 @@ +# Fiber Keep-Alive: Automatic Process Lifetime Management + +In v3, the core `effect` runtime did **not** keep the Node.js process alive while +fibers were suspended on certain asynchronous operations. If a fiber was waiting on +something like `Deferred.await` and there was no other work scheduled on the +event loop, the process would exit immediately — the fiber's suspension did not +register as pending work from Node.js's perspective. + +The only way to prevent this was to use `runMain` from `@effect/platform-node` +(or `@effect/platform-bun`), which installed a long-lived `setInterval` timer +to hold the process open until the root fiber completed. + +In v4, **the keep-alive mechanism is built into the core runtime**. + +## The Problem in v3 + +Consider the following program: + +```ts +import { Deferred, Effect } from "effect" + +const program = Effect.gen(function*() { + const deferred = yield* Deferred.make() + + yield* Deferred.await(deferred) +}) + +Effect.runPromise(program) +``` + +In v3, when the main fiber reached `yield* Deferred.await(deferred)`, it suspended +while waiting for the worker fiber to complete the deferred. However, from the +JavaScript runtime's perspective, the event loop had no more work to do. Thus, +the process would exit. + +The workaround was to use `runMain` from the platform package, which installs +a timer that holds the process open until the root fiber completes: + +```ts +import { NodeRuntime } from "@effect/platform-node" + +NodeRuntime.runMain(program) +``` + +## What Changed in v4 + +In v4, the Effect fiber runtime automatically manages a reference-counted +keep-alive timer. + +This means the following program works in v4 **without** `runMain`: + +```ts +import { Deferred, Effect, Fiber } from "effect" + +const program = Effect.gen(function*() { + const deferred = yield* Deferred.make() + + // The process stays alive while waiting — no runMain needed + yield* Deferred.await(deferred) +}) + +Effect.runPromise(program) +``` + +## `runMain` Is Still Recommended + +Even though the core runtime now handles keep-alive, `runMain` from the platform +packages is still the recommended way to run Effect programs. It provides: + +- **Signal handling** — listens for `SIGINT` / `SIGTERM` and interrupts the + root fiber gracefully. +- **Exit code management** — calls `process.exit(code)` when the program fails + or receives a signal. +- **Error reporting** — reports unhandled errors to the console. diff --git a/repos/effect/migration/fiberref.md b/repos/effect/migration/fiberref.md new file mode 100644 index 0000000..51a0886 --- /dev/null +++ b/repos/effect/migration/fiberref.md @@ -0,0 +1,109 @@ +# FiberRef: `FiberRef` → `Context.Reference` + +In v4, `FiberRef`, `FiberRefs`, `FiberRefsPatch`, and `Differ` have been removed. +Fiber-local state is now handled by `Context.Reference` — the same mechanism +used for services with default values. + +## Built-in References + +v3's built-in `FiberRef` values are now `Context.Reference` values exported +from `References` and related modules. + +| v3 FiberRef | v4 Reference | +| ----------------------------------- | ---------------------------------- | +| `FiberRef.currentConcurrency` | `References.CurrentConcurrency` | +| `FiberRef.currentLogLevel` | `References.CurrentLogLevel` | +| `FiberRef.currentMinimumLogLevel` | `References.MinimumLogLevel` | +| `FiberRef.currentLogAnnotations` | `References.CurrentLogAnnotations` | +| `FiberRef.currentLogSpan` | `References.CurrentLogSpans` | +| `FiberRef.currentScheduler` | `References.Scheduler` | +| `FiberRef.currentMaxOpsBeforeYield` | `References.MaxOpsBeforeYield` | +| `FiberRef.currentTracerEnabled` | `References.TracerEnabled` | +| `FiberRef.unhandledErrorLogLevel` | `References.UnhandledLogLevel` | + +## Reading References + +In v3, `FiberRef.get` retrieved the current value. In v4, references are +services — `yield*` them directly. + +**v3** + +```ts +import { Effect, FiberRef } from "effect" + +const program = Effect.gen(function*() { + const level = yield* FiberRef.get(FiberRef.currentLogLevel) + console.log(level) +}) +``` + +**v4** + +```ts +import { Effect, References } from "effect" + +const program = Effect.gen(function*() { + const level = yield* References.CurrentLogLevel + console.log(level) // "Info" (default) +}) +``` + +## Scoped Updates (`Effect.locally` → `Effect.provideService`) + +v3's `Effect.locally` set a `FiberRef` value for the duration of an effect. In +v4, use `Effect.provideService` with the reference. + +**v3** + +```ts +import { Effect, FiberRef, LogLevel } from "effect" + +const program = Effect.locally( + myEffect, + FiberRef.currentLogLevel, + LogLevel.Debug +) +``` + +**v4** + +```ts +import { Effect, References } from "effect" + +const program = Effect.provideService( + myEffect, + References.CurrentLogLevel, + "Debug" +) +``` + +## Writing References + +v3's `FiberRef.set` mutated the current fiber's ref value. In v4, references are +set via `Effect.provideService`, which scopes the value to the provided effect. + +**v3** + +```ts +import { Effect, FiberRef } from "effect" + +const program = Effect.gen(function*() { + yield* FiberRef.set(FiberRef.currentConcurrency, 10) + // subsequent code sees concurrency = 10 +}) +``` + +**v4** + +```ts +import { Effect, References } from "effect" + +const program = Effect.provideService( + Effect.gen(function*() { + const concurrency = yield* References.CurrentConcurrency + console.log(concurrency) // 10 + }), + References.CurrentConcurrency, + 10 +) +``` diff --git a/repos/effect/migration/forking.md b/repos/effect/migration/forking.md new file mode 100644 index 0000000..d924272 --- /dev/null +++ b/repos/effect/migration/forking.md @@ -0,0 +1,94 @@ +# Forking: Renamed Combinators and New Options + +The `fork*` family of combinators has been renamed in v4 for clarity, and all +variants now accept an options object for controlling fiber startup behavior. + +## Renamings + +| v3 | v4 | Description | +| ----------------------------- | ------------------- | -------------------------------------------- | +| `Effect.fork` | `Effect.forkChild` | Fork as a child of the current fiber | +| `Effect.forkDaemon` | `Effect.forkDetach` | Fork detached from parent lifecycle | +| `Effect.forkScoped` | `Effect.forkScoped` | Fork tied to the current `Scope` (unchanged) | +| `Effect.forkIn` | `Effect.forkIn` | Fork in a specific `Scope` (unchanged) | +| `Effect.forkAll` | — | Removed | +| `Effect.forkWithErrorHandler` | — | Removed | + +## `Effect.fork` → `Effect.forkChild` + +**v3** + +```ts +import { Effect } from "effect" + +const fiber = Effect.fork(myEffect) +``` + +**v4** + +```ts +import { Effect } from "effect" + +const fiber = Effect.forkChild(myEffect) +``` + +## `Effect.forkDaemon` → `Effect.forkDetach` + +**v3** + +```ts +import { Effect } from "effect" + +const fiber = Effect.forkDaemon(myEffect) +``` + +**v4** + +```ts +import { Effect } from "effect" + +const fiber = Effect.forkDetach(myEffect) +``` + +## Fork Options + +In v4, `forkChild`, `forkDetach`, `forkScoped`, and `forkIn` all accept an +optional options object with the following fields: + +```ts +{ + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined +} +``` + +- **`startImmediately`** — When `true`, the forked fiber begins executing + immediately rather than being deferred. Defaults to `undefined` (deferred). +- **`uninterruptible`** — Controls whether the forked fiber can be interrupted. + `true` makes it uninterruptible, `"inherit"` inherits the parent's + interruptibility, and `undefined` uses the default behavior. + +**Usage as data-last (curried)** + +```ts +import { Effect } from "effect" + +const fiber = myEffect.pipe( + Effect.forkChild({ startImmediately: true }) +) +``` + +**Usage as data-first** + +```ts +import { Effect } from "effect" + +const fiber = Effect.forkChild(myEffect, { startImmediately: true }) +``` + +## Removed Combinators + +**`Effect.forkAll`** and **`Effect.forkWithErrorHandler`** have been removed in +v4. For `forkAll`, fork effects individually with `forkChild` or use +higher-level concurrency combinators. For error handling on forked fibers, +observe the fiber's result via `Fiber.join` or `Fiber.await`. diff --git a/repos/effect/migration/generators.md b/repos/effect/migration/generators.md new file mode 100644 index 0000000..363f715 --- /dev/null +++ b/repos/effect/migration/generators.md @@ -0,0 +1,32 @@ +# Generators + +## `Effect.gen`: Passing `this` + +In v3, you could pass a `self` value directly as the first argument to +`Effect.gen`. In v4, `self` must be wrapped in an options object. + +**v3** + +```ts +import { Effect } from "effect" + +class MyService { + readonly local = 1 + compute = Effect.gen(this, function*() { + return yield* Effect.succeed(this.local + 1) + }) +} +``` + +**v4** + +```ts +import { Effect } from "effect" + +class MyService { + readonly local = 1 + compute = Effect.gen({ self: this }, function*() { + return yield* Effect.succeed(this.local + 1) + }) +} +``` diff --git a/repos/effect/migration/layer-memoization.md b/repos/effect/migration/layer-memoization.md new file mode 100644 index 0000000..a67cc6e --- /dev/null +++ b/repos/effect/migration/layer-memoization.md @@ -0,0 +1,98 @@ +# Layer Memoization + +In v3, each call to `Effect.provide` created its own memoization scope. Layers +were memoized / deduplicated within a single `Effect.provide` call, but would +**not** be shared across separate calls — so two `Effect.provide` calls with +overlapping layers would silently build those layers twice. + +In v4, the underlying `MemoMap` data structure which facilitates memoization of +`Layer`s is shared between `Effect.provide` calls (unless explicitly disabled +via the `{ local: true }` option). Thus, layers are automatically memoized / +deduplicated across `Effect.provide` calls. + +## Example + +```ts +import { Console, Context, Effect, Layer } from "effect" + +const MyService = Context.Service<{ readonly value: string }>("MyService") + +const MyServiceLayer = Layer.effect( + MyService, + Effect.gen(function*() { + yield* Console.log("Building MyService") + return { value: "hello" } + }) +) + +const program = Effect.gen(function*() { + const a = yield* MyService + return a.value +}) + +// Same layer provided twice in separate provide calls +const main = program.pipe( + Effect.provide(MyServiceLayer), + Effect.provide(MyServiceLayer) +) + +// Effect v3: "Building MyService" is logged TWICE +// Effect v4: "Building MyService" is logged ONCE +Effect.runPromise(main) +``` + +## Prefer Layer Composition Over Multipl Provides + +Even though v4 memoizes across `provide` calls, **composing layers before +providing is still the recommended pattern**. Layer composition makes your +dependency graph explicit and lets you see the full structure in one place: + +```ts +// Preferred — provide once +const main = program.pipe(Effect.provide(MyServiceLayer)) +``` + +The auto-memoization feature is a safety net to avoid the footguns associated +with multiple `Effect.provide` calls present in v3. It is **NOT** a substitute +for proper layer composition. + +## Opting Out of Shared Memoization + +There are cases where you **want** a layer to be built fresh — for example, +test isolation or creating independent resource pools. v4 provides two +mechanisms: + +### `Layer.fresh` + +Wraps a layer so it always builds with a fresh memo map, bypassing the shared +cache. This existed in v3 as well. + +```ts +import { Effect, Layer } from "effect" + +const main = program.pipe( + Effect.provide(MyServiceLayer), + Effect.provide(Layer.fresh(MyServiceLayer)) +) +// "Building MyService" is logged TWICE — fresh bypasses the shared cache +``` + +### `Effect.provide` with `{ local: true }` + +New in v4. Builds the provided layer with a **local memo map** instead of the +fiber's shared one. The layer and all its sublayers are built from scratch and +are not shared with other `provide` calls. + +```ts +import { Effect } from "effect" + +const main = program.pipe( + Effect.provide(MyServiceLayer), + Effect.provide(MyServiceLayer, { local: true }) +) +// "Building MyService" is logged TWICE — local creates its own memo map +``` + +Use `local: true` when you need an entire layer subtree to be isolated — for +example, when providing layers in a test harness where each test should get +independent resources. diff --git a/repos/effect/migration/runtime.md b/repos/effect/migration/runtime.md new file mode 100644 index 0000000..11b64ff --- /dev/null +++ b/repos/effect/migration/runtime.md @@ -0,0 +1,88 @@ +# Runtime: `Runtime` Removed + +In v3, `Runtime` bundled a `Context`, `RuntimeFlags`, and `FiberRefs` +into a single value used to execute effects: + +```ts +// v3 +interface Runtime { + readonly context: Context.Context + readonly runtimeFlags: RuntimeFlags + readonly fiberRefs: FiberRefs +} +``` + +In v4, this type no longer exists and you can use `Context` instead. +Run functions live directly on `Effect`, and the `Runtime` module is reduced to +process lifecycle utilities. + +## `Runtime.runFork(runtime)` -> `Effect.runForkWith(services)` + +In v3, running an effect with dependencies usually meant pulling the current +runtime from `Effect.runtime()` and calling `Runtime.runFork(runtime)` inside +the main effect. + +**v3** + +```ts +import { Context, Effect, Runtime } from "effect" + +class Logger extends Context.Tag("Logger") void +}>() {} + +const program = Effect.gen(function*() { + const logger = yield* Logger + logger.log("Hello from Logger") +}) + +const main = Effect.gen(function*() { + const runtime = yield* Effect.runtime() + return Runtime.runFork(runtime)(program) +}).pipe( + Effect.provideService(Logger, { + log: (message) => console.log(message) + }) +) + +const fiber = Effect.runFork(main) +``` + +In v4, use the same pattern with `Effect.context()`, then run with +`Effect.runForkWith(services)`: + +**v4** + +```ts +import { Context, Effect } from "effect" + +class Logger extends Context.Service void +}>()("Logger") {} + +const program = Effect.gen(function*() { + const logger = yield* Logger + logger.log("Hello from Logger") +}) + +const main = Effect.gen(function*() { + const services = yield* Effect.context() + return Effect.runForkWith(services)(program) +}).pipe( + Effect.provideContext(Context.make(Logger, { + log: (message) => console.log(message) + })) +) + +const fiber = Effect.runFork(main) +``` + +If your effect has no service requirements, use `Effect.runFork(effect)`. + +## `Runtime` Module Contents + +The `Runtime` module now only contains: + +- `Teardown` — interface for handling process exit +- `defaultTeardown` — default teardown implementation +- `makeRunMain` — creates platform-specific main runners diff --git a/repos/effect/migration/schema.md b/repos/effect/migration/schema.md new file mode 100644 index 0000000..5cbbd72 --- /dev/null +++ b/repos/effect/migration/schema.md @@ -0,0 +1,1071 @@ +# Schema: Migration from v3 + +This document maps v3 Schema APIs to their v4 equivalents. Simple renames and argument changes are covered in the summary table below. More complex migrations have dedicated sections with code examples. + +## Migration types + +- **auto** — mechanical find-and-replace, safe to auto-apply +- **semi-auto** — follows a clear pattern but needs structural changes +- **manual** — requires case-by-case decisions, flag for human review +- **removed** — no v4 equivalent + +## Summary table + +| v3 API | v4 API | Migration type | +| ----------------------------------------------- | ----------------------------------------------------------------------------- | ----------------- | +| `asSchema(schema)` | `revealCodec(schema)` | rename | +| `encodedSchema(schema)` | `toEncoded(schema)` | rename | +| `typeSchema(schema)` | `toType(schema)` | rename | +| `compose(schemaB)` | `decodeTo(schemaB)` | rename | +| `annotations(ann)` | `annotate(ann)` | rename | +| `decodingFallback` annotation | `catchDecoding(...)` | rename | +| `parseJson()` | `UnknownFromJsonString` | rename | +| `parseJson(schema)` | `fromJsonString(schema)` | rename | +| `pattern(regex)` | `check(isPattern(regex))` | rename | +| `nonEmptyString` | `isNonEmpty` | rename | +| `BigIntFromSelf` | `BigInt` | rename | +| `SymbolFromSelf` | `Symbol` | rename | +| `URLFromSelf` | `URL` | rename | +| `RedactedFromSelf` | `Redacted` | rename | +| `Redacted` | `RedactedFromValue` | rename | +| `EitherFromSelf` | `Result` | rename | +| `DateFromNumber` | `DateFromMillis` | rename | +| `TaggedError` | `TaggedErrorClass` | rename | +| `decodeUnknown` | `decodeUnknownEffect` | rename | +| `decode` | `decodeEffect` | rename | +| `decodeUnknownEither` | `decodeUnknownExit` | rename | +| `decodeEither` | `decodeExit` | rename | +| `encodeUnknown` | `encodeUnknownEffect` | rename | +| `encode` | `encodeEffect` | rename | +| `encodeUnknownEither` | `encodeUnknownExit` | rename | +| `encodeEither` | `encodeExit` | rename | +| `asserts(schema)(input)` | `asserts(schema, input)` | semi-auto | +| `Literal(null)` | `Null` | restructure | +| `Literal("a", "b")` | `Literals(["a", "b"])` | variadic-to-array | +| `pickLiteral("a", "b")` | `Literals(...).pick(["a", "b"])` | restructure | +| `Union(A, B)` | `Union([A, B])` | variadic-to-array | +| `Tuple(A, B)` | `Tuple([A, B])` | variadic-to-array | +| `TemplateLiteral(A, B)` | `TemplateLiteral([A, B])` | variadic-to-array | +| `TemplateLiteralParser(A, B)` | `TemplateLiteralParser(schema.parts)` | restructure | +| `Record({ key, value })` | `Record(key, value)` | restructure | +| `filter(predicate)` | `check(makeFilter(predicate))` | restructure | +| `filter(refinement)` | `refine(refinement)` | restructure | +| `UUID` | `String.check(isUUID())` | restructure | +| `ULID` | `String.check(isULID())` | restructure | +| `pick("a")` | `mapFields(Struct.pick(["a"]))` | restructure | +| `omit("a")` | `mapFields(Struct.omit(["a"]))` | restructure | +| `partial` | `mapFields(Struct.map(Schema.optional))` | restructure | +| `partialWith({ exact: true })` | `mapFields(Struct.map(Schema.optionalKey))` | restructure | +| `required(schema)` | `schema.mapFields(Struct.map(Schema.requiredKey))` | restructure | +| `extend(structB)` | `mapFields(Struct.assign(fieldsB))` or `fieldsAssign(fieldsB)` | restructure | +| `transform(from, to, { decode, encode })` | `from.pipe(decodeTo(to, SchemaTransformation.transform({ decode, encode })))` | restructure | +| `transformOrFail(from, to, { decode, encode })` | `from.pipe(decodeTo(to, { decode: SchemaGetter.transformOrFail(...), ... }))` | restructure | +| `transformLiteral(from, to)` | `Literal(from).transform(to)` | restructure | +| `transformLiterals([0,"a"], [1,"b"])` | `Literals([0, 1]).transform(["a", "b"])` | restructure | +| `attachPropertySignature("k", "v")` | `mapFields(f => ({...f, k: tagDefaultOmit("v")}))` | restructure | +| `validate*` | removed (use `decode*` + `toType`) | removed | +| `keyof` | — | removed | +| `NonEmptyArrayEnsure` | — | removed | +| `withDefaults` | — | removed | +| `Data(schema)` | — | removed | +| `optionalWith(schema, opts)` | varies by options (see [optionalWith](#optionalwith)) | manual | +| `optionalToOptional` | see [optional field transformations](#optional-field-transformations) | manual | +| `optionalToRequired` | see [optional field transformations](#optional-field-transformations) | manual | +| `requiredToOptional` | see [optional field transformations](#optional-field-transformations) | manual | +| `filterEffect` | see [filterEffect](#filtereffect) | manual | +| `fromKey` | see [rename](#rename) | manual | +| `rename({ a: "c" })` | see [rename](#rename) | manual | +| `format(schema)` | see [format](#format) | manual | +| `ParseResult.ArrayFormatter.formatError(error)` | see [ParseResult formatters](#parseresult-formatters) | manual | +| `declare` | see [declare](#declare) | manual | + +## Additional rename notes + +### `*FromSelf` renames + +The following `*FromSelf` schemas have been renamed to drop the suffix: + +`DateFromSelf` → `Date`, `DurationFromSelf` → `Duration`, `ChunkFromSelf` → `Chunk`, `ReadonlyMapFromSelf` → `ReadonlyMap`, `ReadonlySetFromSelf` → `ReadonlySet`, `HashMapFromSelf` → `HashMap`, `HashSetFromSelf` → `HashSet`, `BigDecimalFromSelf` → `BigDecimal`, `CauseFromSelf` → `Cause`, `ExitFromSelf` → `Exit`, `OptionFromSelf` → `Option`, `RegExpFromSelf` → `RegExp` + +### Filter renames + +All filters have been renamed with an `is` prefix and now use `check(...)` or `pipe(Schema.check(...))`: + +`greaterThan` → `isGreaterThan`, `greaterThanOrEqualTo` → `isGreaterThanOrEqualTo`, `lessThan` → `isLessThan`, `lessThanOrEqualTo` → `isLessThanOrEqualTo`, `between` → `isBetween`, `int` → `isInt`, `multipleOf` → `isMultipleOf`, `finite` → `isFinite`, `minLength` → `isMinLength`, `maxLength` → `isMaxLength`, `length` → `isLengthBetween` + +Note: `positive`, `negative`, `nonNegative`, `nonPositive` have been removed in v4. + +### Utility renames + +`equivalence` → `toEquivalence`, `arbitrary` → `toArbitrary`, `pretty` → `toFormatter`, `standardSchemaV1` → `toStandardSchemaV1` + +## Detailed migrations + +### Redacted + +**Migration: rename with behavior distinction** + +In v3, `Schema.Redacted(value)` decoded the raw encoded value and wrapped the decoded value in `Redacted`. + +In v4, that behavior is named `Schema.RedactedFromValue(value)`. + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Redacted(Schema.String) +const decode = Schema.decodeSync(schema) + +decode("secret") +``` + +v4 + +```ts +import { Redacted, Schema } from "effect" + +const schema = Schema.RedactedFromValue(Schema.String) +const decode = Schema.decodeSync(schema) + +const redacted = decode("secret") +console.log(Redacted.value(redacted)) +// secret +``` + +`Schema.Redacted(value)` in v4 is the replacement for v3 `Schema.RedactedFromSelf(value)`: it expects the input to already be a `Redacted` value, so both `Type` and `Encoded` are `Redacted<...>`. + +### asserts signature + +**Migration: semi-auto** + +`Schema.asserts` now asserts an input directly instead of returning an assertion function. + +v3 + +```ts +import { Schema } from "effect" + +const assertString = Schema.asserts(Schema.String) +assertString(input) +``` + +v4 + +```ts +import { Schema } from "effect" + +Schema.asserts(Schema.String, input) +``` + +### validate* removal + +**Migration: removed** + +The `validate`, `validateEither`, `validatePromise`, `validateSync`, and `validateOption` APIs have been removed. Use `Schema.decode*` + `Schema.toType` instead. + +```ts +import { Schema } from "effect" + +// v3: Schema.validateSync(Schema.String)(input) +// v4: +const validateSync = Schema.decodeSync(Schema.toType(Schema.String)) +``` + +### Data removal + +**Migration: removed** + +`Schema.Data` has no v4 equivalent. Remove it. `Equal.equals` performs deep structural comparison on objects by default in v4, so `Schema.Data` is unnecessary. + +### pickLiterals + +**Migration: auto** + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Literal("a", "b", "c").pipe(Schema.pickLiteral("a", "b")) +``` + +v4 + +```ts +import { Schema } from "effect" + +const schema = Schema.Literals(["a", "b", "c"]).pick(["a", "b"]) +``` + +### TemplateLiteralParser + +**Migration: semi-auto** + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.TemplateLiteral(Schema.String, ".", Schema.String) +const parser = Schema.TemplateLiteralParser(Schema.String, ".", Schema.String) +``` + +v4 + +```ts +import { Schema } from "effect" + +const schema = Schema.TemplateLiteral([Schema.String, ".", Schema.String]) +// use the `parts` property instead of repeating the template parts +const parser = Schema.TemplateLiteralParser(schema.parts) +``` + +Behavior note: `TemplateLiteral` and `TemplateLiteralParser` match parts semantically. Checks on string, number, and bigint schema parts are applied while matching each segment, so refined parts can reject strings that would match the broader primitive shape. + +### format + +**Migration: manual** + +**New imports:** `SchemaRepresentation` + +v3 + +```ts +import { Schema } from "effect" + +console.log(Schema.format(Schema.String)) +// string +``` + +v4 + +```ts +import { Schema, SchemaRepresentation } from "effect" + +const doc = SchemaRepresentation.fromAST(Schema.String.ast) +const multi = SchemaRepresentation.toMultiDocument(doc) +const codeDoc = SchemaRepresentation.toCodeDocument(multi) +console.log(codeDoc.codes[0].Type) +// string +``` + +### ParseResult formatters + +**Migration: manual** + +**New imports:** `SchemaIssue` + +In v4, schema parsing fails with `Schema.SchemaError`, which contains a nested `SchemaIssue` in its `issue` field. + +Use `SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues` for the v3 `ParseResult.ArrayFormatter.formatError(error)` equivalent. + +v3 + +```ts +import { Either, ParseResult, Schema } from "effect" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Number +}) + +const decode = Schema.decodeUnknownEither(Person) + +const result = decode({}) +if (Either.isLeft(result)) { + console.error("Decoding failed:") + console.error(ParseResult.ArrayFormatter.formatErrorSync(result.left)) +} +/* +Decoding failed: +[ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' } ] +*/ +``` + +v4 + +```ts +import { Schema, SchemaIssue } from "effect" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Number +}) + +const decode = Schema.decodeUnknownSync(Person) + +try { + decode({}) +} catch (error) { + if (error instanceof Error) { + console.error("Decoding failed:") + if (SchemaIssue.isIssue(error.cause)) { + console.error(SchemaIssue.makeFormatterStandardSchemaV1()(error.cause).issues) + } + } +} +/* +Decoding failed: +[ { path: [ 'name' ], message: 'Missing key' } ] +*/ +``` + +### Record + +**Migration: auto** + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) +``` + +v4 + +```ts +import { Schema } from "effect" + +const schema = Schema.Record(Schema.String, Schema.Number) +``` + +Behavior note: dynamic record key schemas select matching own properties before the value schema is applied. Refined key schemas such as `Schema.String.check(...)`, `Schema.Int`, or checked template literals ignore properties that do not match the key schema; they do not validate the value at those ignored keys. For transformed key schemas, selection is based on encoded property names before selected keys are decoded. + +### pick / omit + +**Migration: semi-auto** + +**New imports:** `Struct` + +v3 + +```ts +import { Schema } from "effect" + +const picked = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.pick("a")) +const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.omit("b")) +``` + +v4 + +```ts +import { Schema, Struct } from "effect" + +const picked = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.pick(["a"])) +const omitted = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.omit(["b"])) +``` + +### partial / partialWith + +**Migration: semi-auto** + +**New imports:** `Struct` + +- `Schema.partial` → `mapFields(Struct.map(Schema.optional))` (allows `undefined`) +- `Schema.partialWith({ exact: true })` → `mapFields(Struct.map(Schema.optionalKey))` (exact) + +```ts +import { Schema, Struct } from "effect" + +const struct = Schema.Struct({ a: Schema.String, b: Schema.Number }) + +// v3: struct.pipe(Schema.partial) +const withUndefined = struct.mapFields(Struct.map(Schema.optional)) + +// v3: struct.pipe(Schema.partialWith({ exact: true })) +const exact = struct.mapFields(Struct.map(Schema.optionalKey)) +``` + +You can also make a subset of fields partial: + +```ts +import { Schema, Struct } from "effect" + +const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }) + .mapFields(Struct.mapPick(["a"], Schema.optional)) +``` + +### required + +**Migration: semi-auto** + +**New imports:** `Struct` + +- `Schema.requiredKey`: makes `optionalKey` fields required; makes `optional` fields required as `T | undefined` +- `Schema.required`: makes `optional` fields required (removes `undefined`) + +```ts +import { Schema, Struct } from "effect" + +const original = Schema.Struct({ + a: Schema.optionalKey(Schema.String), + b: Schema.optionalKey(Schema.Number) +}) + +// v3: Schema.required(original) +const schema = original.mapFields(Struct.map(Schema.requiredKey)) +// { readonly a: string; readonly b: number; } +``` + +### optional field transformations + +**Migration: manual** + +**New imports:** `SchemaGetter` + +`optionalToOptional`, `optionalToRequired`, and `requiredToOptional` are all replaced by `Schema.decodeTo` + `SchemaGetter.transformOptional`. + +The pattern: start with the encoded optionality (`optionalKey` or required), pipe to `decodeTo` with the decoded optionality, and provide `transformOptional` functions for decode/encode. + +**Example** (v3 `optionalToRequired`: setting `null` as default for missing field) + +v3 + +```ts +import { Option, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalToRequired(Schema.String, Schema.NullOr(Schema.String), { + decode: Option.getOrElse(() => null), + encode: Option.liftPredicate((value) => value !== null) + }) +}) +``` + +v4 + +```ts +import { Option, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.String).pipe( + Schema.decodeTo(Schema.NullOr(Schema.String), { + decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), + encode: SchemaGetter.transformOptional(Option.filter((value) => value !== null)) + }) + ) +}) +``` + +**Example** (v3 `requiredToOptional`: empty string as missing value) + +v3 + +```ts +import { Option, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.requiredToOptional(Schema.String, Schema.String, { + decode: Option.liftPredicate((s) => s !== ""), + encode: Option.getOrElse(() => "") + }) +}) +``` + +v4 + +```ts +import { Option, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.String.pipe( + Schema.decodeTo(Schema.optionalKey(Schema.String), { + decode: SchemaGetter.transformOptional(Option.filter((value) => value !== "")), + encode: SchemaGetter.transformOptional(Option.orElseSome(() => "")) + }) + ) +}) +``` + +### optionalWith + +**Migration: manual** + +**New imports:** `SchemaGetter`, `Predicate` (for nullable variants) + +#### Decision tree + +| v3 options | v4 pattern | +| ------------------------------------------ | ----------------------------------------------------------------------- | +| `{ exact: true }` | `optionalKey(schema)` | +| `{ default }` | `schema.pipe(withDecodingDefaultType(...))` | +| `{ exact: true, default }` | `schema.pipe(withDecodingDefaultTypeKey(...))` | +| `{ nullable: true }` | `optional(NullOr(schema))` + `decodeTo` + filter null | +| `{ nullable: true, exact: true }` | `optionalKey(NullOr(schema))` + `decodeTo` + filter null | +| `{ nullable: true, default }` | `optional(NullOr(schema))` + `decodeTo` + filter null + `orElseSome` | +| `{ nullable: true, exact: true, default }` | `optionalKey(NullOr(schema))` + `decodeTo` + filter null + `orElseSome` | + +Key rules: + +- `exact: true` → use `optionalKey` instead of `optional` +- `nullable: true` → wrap inner schema in `NullOr` and filter nulls via `Option.filter(Predicate.isNotNull)` +- `default` → use `withDecodingDefaultType` (or `withDecodingDefaultTypeKey` with `exact: true`) + +#### Example: `{ exact: true }` (simplest case) + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.NumberFromString, { exact: true }) +}) +``` + +v4 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.NumberFromString) +}) +``` + +#### Example: `{ default }` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { default: () => "" }) +}) +``` + +v4 + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String.pipe(Schema.withDecodingDefaultType(Effect.succeed(""))) +}) +``` + +#### Example: `{ exact: true, default }` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.String, { exact: true, default: () => "" }) +}) +``` + +v4 + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(""))) +}) +``` + +#### Example: `{ nullable: true, exact: true, default }` (most complex case) + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => -1, exact: true }) +}) +``` + +v4 + +```ts +import { Option, Predicate, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.NullOr(Schema.NumberFromString)).pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transformOptional((o) => + o.pipe(Option.filter(Predicate.isNotNull), Option.orElseSome(() => -1)) + ), + encode: SchemaGetter.required() + }) + ) +}) +``` + +### pluck + +**Migration: manual** + +**New imports:** `SchemaGetter`, `Struct` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.pluck("a")) +``` + +v4 + +```ts +import { Schema, SchemaGetter, Struct } from "effect" + +function pluck

(key: P) { + return ( + schema: Schema.Struct<{ [K in P]: S }> + ): Schema.decodeTo, Schema.Struct<{ [K in P]: S }>> => { + return schema.mapFields(Struct.pick([key])).pipe( + Schema.decodeTo(Schema.toType(schema.fields[key]), { + decode: SchemaGetter.transform((whole: any) => whole[key]), + encode: SchemaGetter.transform((value) => ({ [key]: value } as any)) + }) + ) + } +} + +const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(pluck("a")) +``` + +### extend + +**Migration: semi-auto** + +**New imports:** `Struct` (Struct case), `Tuple` (Union case) + +#### Struct extends Struct + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.extend(Schema.Struct({ c: Schema.Number }))) +``` + +v4 + +```ts +import { Schema, Struct } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields(Struct.assign({ c: Schema.Number })) + +// or more succinctly +const schema2 = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.fieldsAssign({ c: Schema.Number })) +``` + +#### Union extends Struct + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Union( + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) +).pipe(Schema.extend(Schema.Struct({ c: Schema.Boolean }))) +``` + +v4 + +```ts +import { Schema, Tuple } from "effect" + +const schema = Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) +]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number }))) +``` + +### filter + +**Migration: semi-auto** + +v3 + +```ts +import { Schema } from "effect" + +// inline filter +const a = Schema.String.pipe(Schema.filter((s) => s.length > 0)) + +// refinement +const b = Schema.Option(Schema.String).pipe(Schema.filter(Option.isSome)) +``` + +v4 + +```ts +import { Option, Schema } from "effect" + +// inline filter +const a = Schema.String.check(Schema.makeFilter((s) => s.length > 0)) + +// refinement +const b = Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome)) +``` + +In v4, a `makeFilter` predicate can return any of the shapes described by `Schema.FilterOutput`: + +- `undefined` / `true` — success +- `false` — generic failure +- `string` — failure with that message +- `SchemaIssue.Issue` — a fully-formed issue +- `{ path, issue }` — failure at a nested path (`issue` is a `string` or `SchemaIssue.Issue`) +- `ReadonlyArray` — several failures reported together (empty array = success, single element is unwrapped, otherwise grouped into an `Issue.Composite`) + +**Example** (Failure at a nested path) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check( + Schema.makeFilter((o) => + o.password === o.confirmPassword + ? undefined + : { path: ["password"], issue: "password and confirmPassword must match" } + ) +) + +console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" }))) +// Failure(Cause([Fail(SchemaError: password and confirmPassword must match +// at ["password"])])) +``` + +**Example** (Reporting multiple failures at once) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check( + Schema.makeFilter((o) => { + const issues: Array = [] + if (o.a > 0) { + if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" }) + if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" }) + } + return issues + }) +) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 }))) +// Failure(Cause([Fail(SchemaError: b must be greater than 0 +// at ["b"] +// c must be greater than 0 +// at ["c"])])) +``` + +### filterEffect + +**Migration: manual** + +**New imports:** `SchemaGetter`, `Result` + +v3 + +```ts +import { Effect, Schema } from "effect" + +async function validateUsername(username: string) { + return Promise.resolve(username === "gcanti") +} + +const ValidUsername = Schema.String.pipe( + Schema.filterEffect((username) => + Effect.promise(() => validateUsername(username).then((valid) => valid || "Invalid username")) + ) +) +``` + +v4 + +```ts +import { Effect, Result, Schema, SchemaGetter } from "effect" + +async function validateUsername(username: string) { + return Promise.resolve(username === "gcanti") +} + +const ValidUsername = Schema.String.pipe( + Schema.decode({ + decode: SchemaGetter.checkEffect((username) => + Effect.promise(() => validateUsername(username).then((valid) => valid || "Invalid username")) + ), + encode: SchemaGetter.passthrough() + }) +) +``` + +### transform + +**Migration: semi-auto** + +**New imports:** `SchemaTransformation` + +v3 + +```ts +import { Schema } from "effect" + +const BooleanFromString = Schema.transform(Schema.Literal("on", "off"), Schema.Boolean, { + strict: true, + decode: (literal) => literal === "on", + encode: (bool) => (bool ? "on" : "off") +}) +``` + +v4 + +```ts +import { Schema, SchemaTransformation } from "effect" + +const BooleanFromString = Schema.Literals(["on", "off"]).pipe( + Schema.decodeTo( + Schema.Boolean, + SchemaTransformation.transform({ + decode: (literal) => literal === "on", + encode: (bool) => (bool ? "on" : "off") + }) + ) +) +``` + +### transformOrFail + +**Migration: semi-auto** + +**New imports:** `SchemaGetter`, `SchemaIssue` + +v3 + +```ts +import { ParseResult, Schema } from "effect" + +const NumberFromString = Schema.transformOrFail(Schema.String, Schema.Number, { + strict: true, + decode: (input, _, ast) => { + const parsed = parseFloat(input) + if (isNaN(parsed)) { + return ParseResult.fail(new ParseResult.Type(ast, input, "Failed to convert string to number")) + } + return ParseResult.succeed(parsed) + }, + encode: (input) => ParseResult.succeed(input.toString()) +}) +``` + +v4 + +```ts +import { Effect, Number, Option, Schema, SchemaGetter, SchemaIssue } from "effect" + +const NumberFromString = Schema.String.pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transformOrFail((s) => { + const n = Number.parse(s) + if (n === undefined) { + return Effect.fail(new SchemaIssue.InvalidValue(Option.some(s))) + } + return Effect.succeed(n) + }), + encode: SchemaGetter.String() + }) +) +``` + +### transformLiteral / transformLiterals + +**Migration: auto** + +v3 + +```ts +import { Schema } from "effect" + +const a = Schema.transformLiteral(0, "a") +const b = Schema.transformLiterals([0, "a"], [1, "b"], [2, "c"]) +``` + +v4 + +```ts +import { Schema } from "effect" + +const a = Schema.Literal(0).transform("a") +const b = Schema.Literals([0, 1, 2]).transform(["a", "b", "c"]) +``` + +### attachPropertySignature + +**Migration: semi-auto** + +v3 + +```ts +import { Schema } from "effect" + +const Circle = Schema.Struct({ radius: Schema.Number }) +const Square = Schema.Struct({ sideLength: Schema.Number }) + +const DiscriminatedShape = Schema.Union( + Circle.pipe(Schema.attachPropertySignature("kind", "circle")), + Square.pipe(Schema.attachPropertySignature("kind", "square")) +) +``` + +v4 + +```ts +import { Schema } from "effect" + +const Circle = Schema.Struct({ radius: Schema.Number }) +const Square = Schema.Struct({ sideLength: Schema.Number }) + +const DiscriminatedShape = Schema.Union([ + Circle.mapFields((fields) => ({ ...fields, kind: Schema.tagDefaultOmit("circle") })), + Square.mapFields((fields) => ({ ...fields, kind: Schema.tagDefaultOmit("square") })) +]) +``` + +### decodingFallback + +**Migration: auto** + +v3 + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.String.annotations({ + decodingFallback: () => Effect.succeed("a") +}) +``` + +v4 + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.String.pipe(Schema.catchDecoding(() => Effect.succeedSome("a"))) +``` + +### rename + +**Migration: manual** + +**New imports:** `SchemaTransformation` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.rename({ a: "c" })) +``` + +v4 + +```ts +import { Schema } from "effect" + +// experimental API +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.encodeKeys({ a: "c" })) +``` + +### Capitalize / Lowercase / Uppercase / Uncapitalize + +**Migration: semi-auto** + +**New imports:** `SchemaTransformation` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Capitalize +``` + +v4 + +```ts +import { Schema, SchemaTransformation } from "effect" + +const schema = Schema.String.pipe( + Schema.decodeTo(Schema.String.check(Schema.isCapitalized()), SchemaTransformation.capitalize()) +) +``` + +### NonEmptyTrimmedString + +**Migration: semi-auto** + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.NonEmptyTrimmedString +``` + +v4 + +```ts +import { Schema } from "effect" + +const schema = Schema.Trimmed.check(Schema.isNonEmpty()) +``` + +### split + +**Migration: manual** + +**New imports:** `SchemaTransformation` + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.split(",") +``` + +v4 + +```ts +import { Schema, SchemaTransformation } from "effect" + +function split(separator: string) { + return Schema.String.pipe( + Schema.decodeTo( + Schema.Array(Schema.String), + SchemaTransformation.transform({ + decode: (s) => s.split(separator) as ReadonlyArray, + encode: (as) => as.join(separator) + }) + ) + ) +} +``` diff --git a/repos/effect/migration/scope.md b/repos/effect/migration/scope.md new file mode 100644 index 0000000..4dd5f13 --- /dev/null +++ b/repos/effect/migration/scope.md @@ -0,0 +1,50 @@ +# Scope + +## `Scope.extend` → `Scope.provide` + +`Scope.extend` has been renamed to `Scope.provide` in v4. The behavior is +identical: it provides a `Scope` to an effect that requires one, removing +`Scope` from the effect's requirements without closing the scope when the +effect completes. + +The new name better reflects the operation — you are providing a service (the +`Scope`) to an effect, consistent with how other services are provided in +Effect. + +**v3** + +```ts +import { Effect, Scope } from "effect" + +const program = Effect.gen(function*() { + const scope = yield* Scope.make() + yield* Scope.extend(myEffect, scope) +}) +``` + +**v4** + +```ts +import { Effect, Scope } from "effect" + +const program = Effect.gen(function*() { + const scope = yield* Scope.make() + yield* Scope.provide(scope)(myEffect) +}) +``` + +Both data-first and data-last (curried) forms are supported: + +```ts +// data-first +Scope.provide(myEffect, scope) + +// data-last (curried) +myEffect.pipe(Scope.provide(scope)) +``` + +## Quick Reference + +| v3 | v4 | +| -------------- | --------------- | +| `Scope.extend` | `Scope.provide` | diff --git a/repos/effect/migration/services.md b/repos/effect/migration/services.md new file mode 100644 index 0000000..9ee34d6 --- /dev/null +++ b/repos/effect/migration/services.md @@ -0,0 +1,235 @@ +# Services: `Context.Tag` → `Context.Service` + +In v3, services were defined using `Context.Tag`, `Context.GenericTag`, +`Effect.Tag`, or `Effect.Service`. In v4, all of these have been replaced by +`Context.Service`. + +The underlying runtime data structure is a typed map from service identifiers to +their implementations. + +## Defining Services + +**v3: `Context.GenericTag`** + +```ts +import { Context } from "effect" + +interface Database { + readonly query: (sql: string) => string +} + +const Database = Context.GenericTag("Database") +``` + +**v4: `Context.Service` (function syntax)** + +```ts +import { Context } from "effect" + +interface Database { + readonly query: (sql: string) => string +} + +const Database = Context.Service("Database") +``` + +## Class-Based Services + +**v3: `Context.Tag` class syntax** + +```ts +import { Context } from "effect" + +class Database extends Context.Tag("Database") string +}>() {} +``` + +**v4: `Context.Service` class syntax** + +```ts +import { Context } from "effect" + +class Database extends Context.Service string +}>()("Database") {} +``` + +Note the difference in argument order: in v3, the identifier string is passed to +`Context.Tag(id)` before the type parameters. In v4, the type parameters come +first via `Context.Service()` and the identifier string is +passed to the returned constructor `(id)`. + +## `Effect.Tag` Accessors → `Context.Service` with `use` + +v3's `Effect.Tag` provided proxy access to service methods as static properties +on the tag class (accessors). This allowed calling service methods directly +without first yielding the service: + +```ts +// v3 — static accessor proxy +const program = Notifications.notify("hello") +``` + +This pattern had significant limitations. The proxy was implemented via mapped +types over the service shape, which meant **generic methods lost their type +parameters**. A service method like `get(key: string): Effect` would +have its generic erased when accessed through the proxy, collapsing to +`get(key: string): Effect`. For the same reason, overloaded signatures +were not preserved. + +In v4, accessors are removed. The most direct replacement is `Service.use`, +which receives the service instance and runs a callback: + +**v3** + +```ts +import { Effect } from "effect" + +class Notifications extends Effect.Tag("Notifications") Effect.Effect +}>() {} + +// Static proxy access +const program = Notifications.notify("hello") +``` + +**v4 — `use`** + +```ts +import { Context, Effect } from "effect" + +class Notifications extends Context.Service Effect.Effect +}>()("Notifications") {} + +// use: access the service and call a method in one step +const program = Notifications.use((n) => n.notify("hello")) +``` + +`use` takes an effectful callback `(service: Shape) => Effect` and +returns an `Effect`. `useSync` takes a pure callback +`(service: Shape) => A` and returns an `Effect`. Both +return Effects — `useSync` just allows the accessor function itself to be +synchronous: + +```ts +// ┌─── Effect +// ▼ +const program = Notifications.use((n) => n.notify("hello")) + +// ┌─── Effect +// ▼ +const port = Config.useSync((c) => c.port) +``` + +**Prefer `yield*` over `use` in most cases.** While `use` is a convenient +one-liner, it makes it easy to accidentally leak service dependencies into +return values. When you call `use`, the service is available inside the +callback but the dependency is not visible at the call site — making it harder +to track which services your code depends on. Using `yield*` in a generator +makes dependencies explicit and keeps service access co-located with the rest +of your effect logic: + +```ts +const program = Effect.gen(function*() { + const notifications = yield* Notifications + yield* notifications.notify("hello") + yield* notifications.notify("world") +}) +``` + +## `Effect.Service` → `Context.Service` with `make` + +v3's `Effect.Service` allowed defining a service with an effectful constructor +and dependencies inline. In v4, use `Context.Service` with a `make` option. + +**v3** + +In v3, `Effect.Service` automatically generated a `.Default` layer from the +provided constructor, and wired `dependencies` into it: + +```ts +import { Effect, Layer } from "effect" + +class Logger extends Effect.Service()("Logger", { + effect: Effect.gen(function*() { + const config = yield* Config + return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) } + }), + dependencies: [Config.Default] +}) {} + +// Logger.Default is auto-generated: Layer +// (dependencies are already wired in) +const program = Effect.gen(function*() { + const logger = yield* Logger + yield* logger.log("hello") +}).pipe(Effect.provide(Logger.Default)) +``` + +**v4** + +In v4, `Context.Service` with `make` stores the constructor effect on the +class but does **not** auto-generate a layer. Define layers explicitly using +`Layer.effect`: + +```ts +import { Context, Effect, Layer } from "effect" + +class Logger extends Context.Service()("Logger", { + make: Effect.gen(function*() { + const config = yield* Config + return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) } + }) +}) { + // Build the layer yourself from the make effect + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Config.layer) + ) +} +``` + +The `dependencies` option no longer exists. Wire dependencies via +`Layer.provide` as shown above. + +Note: v4 adopts the convention of naming layers with `layer` (e.g. +`Logger.layer`) instead of v3's `Default` or `Live`. Use `layer` +for the primary layer and descriptive suffixes for variants (e.g. +`layerTest`, `layerConfig`). + +## References (Services with Defaults) + +**v3: `Context.Reference`** + +```ts +import { Context } from "effect" + +class LogLevel extends Context.Reference()("LogLevel", { + defaultValue: () => "info" as const +}) {} +``` + +**v4: `Context.Reference`** + +```ts +import { Context } from "effect" + +const LogLevel = Context.Reference<"info" | "warn" | "error">("LogLevel", { + defaultValue: () => "info" as const +}) +``` + +## Quick Reference + +| v3 | v4 | +| ------------------------------------- | --------------------------------------- | +| `Context.GenericTag(id)` | `Context.Service(id)` | +| `Context.Tag(id)()` | `Context.Service()(id)` | +| `Effect.Tag(id)()` | `Context.Service()(id)` | +| `Effect.Service()(id, opts)` | `Context.Service()(id, { make })` | +| `Context.Reference()(id, opts)` | `Context.Reference(id, opts)` | +| `Context.make(tag, impl)` | `Context.make(tag, impl)` | +| `Context.get(ctx, tag)` | `Context.get(map, tag)` | +| `Context.add(ctx, tag, impl)` | `Context.add(map, tag, impl)` | +| `Context.mergeAll(...)` | `Context.mergeAll(...)` | diff --git a/repos/effect/migration/v3-to-v4.md b/repos/effect/migration/v3-to-v4.md new file mode 100644 index 0000000..5e3f3c7 --- /dev/null +++ b/repos/effect/migration/v3-to-v4.md @@ -0,0 +1,421 @@ +# v3 to v4 Import and API Rename Maps + +Mapped modules: 290 +No counterpart: 43 +API renames: 53 + +This file is intended for migration agents. It contains user-facing import +specifier mappings and API rename mappings. + +Use the import map when rewriting import declarations. Use the API renames when +rewriting renamed symbols. + +## Import Map + +Each line is `v3 import -> v4 direct module import`. When a grouped v4 barrel +exists, the suggested barrel import is shown in parentheses. + +```text +@effect/platform/ChannelSchema -> effect/ChannelSchema (barrel: effect) +@effect/platform/FileSystem -> effect/FileSystem (barrel: effect) +effect/JSONSchema -> effect/JsonSchema (barrel: effect) +@effect/platform/Path -> effect/Path (barrel: effect) +@effect/platform/Error -> effect/PlatformError (barrel: effect) +effect/Either -> effect/Result (barrel: effect) +@effect/platform/Terminal -> effect/Terminal (barrel: effect) +effect/TDeferred -> effect/TxDeferred (barrel: effect) +effect/TMap -> effect/TxHashMap (barrel: effect) +effect/TSet -> effect/TxHashSet (barrel: effect) +effect/TPriorityQueue -> effect/TxPriorityQueue (barrel: effect) +effect/TPubSub -> effect/TxPubSub (barrel: effect) +effect/TQueue -> effect/TxQueue (barrel: effect) +effect/TReentrantLock -> effect/TxReentrantLock (barrel: effect) +effect/TRef -> effect/TxRef (barrel: effect) +effect/TSemaphore -> effect/TxSemaphore (barrel: effect) +effect/TSubscriptionRef -> effect/TxSubscriptionRef (barrel: effect) +effect/FastCheck -> effect/testing/FastCheck (barrel: effect/testing) +effect/TestClock -> effect/testing/TestClock (barrel: effect/testing) +@effect/cli/Args -> effect/unstable/cli/Argument (barrel: effect/unstable/cli) +@effect/cli/ValidationError -> effect/unstable/cli/CliError (barrel: effect/unstable/cli) +@effect/cli/Command -> effect/unstable/cli/Command (barrel: effect/unstable/cli) +@effect/cli/CommandDescriptor -> effect/unstable/cli/Completions (barrel: effect/unstable/cli) +@effect/cli/Options -> effect/unstable/cli/Flag (barrel: effect/unstable/cli) +@effect/cli/BuiltInOptions -> effect/unstable/cli/GlobalFlag (barrel: effect/unstable/cli) +@effect/cli/HelpDoc -> effect/unstable/cli/HelpDoc (barrel: effect/unstable/cli) +@effect/cli/Primitive -> effect/unstable/cli/Primitive (barrel: effect/unstable/cli) +@effect/cli/Prompt -> effect/unstable/cli/Prompt (barrel: effect/unstable/cli) +@effect/cluster/ClusterCron -> effect/unstable/cluster/ClusterCron (barrel: effect/unstable/cluster) +@effect/cluster/ClusterError -> effect/unstable/cluster/ClusterError (barrel: effect/unstable/cluster) +@effect/cluster/ClusterMetrics -> effect/unstable/cluster/ClusterMetrics (barrel: effect/unstable/cluster) +@effect/cluster/ClusterSchema -> effect/unstable/cluster/ClusterSchema (barrel: effect/unstable/cluster) +@effect/cluster/ClusterWorkflowEngine -> effect/unstable/cluster/ClusterWorkflowEngine (barrel: effect/unstable/cluster) +@effect/cluster/DeliverAt -> effect/unstable/cluster/DeliverAt (barrel: effect/unstable/cluster) +@effect/cluster/Entity -> effect/unstable/cluster/Entity (barrel: effect/unstable/cluster) +@effect/cluster/EntityAddress -> effect/unstable/cluster/EntityAddress (barrel: effect/unstable/cluster) +@effect/cluster/EntityId -> effect/unstable/cluster/EntityId (barrel: effect/unstable/cluster) +@effect/cluster/EntityProxy -> effect/unstable/cluster/EntityProxy (barrel: effect/unstable/cluster) +@effect/cluster/EntityProxyServer -> effect/unstable/cluster/EntityProxyServer (barrel: effect/unstable/cluster) +@effect/cluster/EntityResource -> effect/unstable/cluster/EntityResource (barrel: effect/unstable/cluster) +@effect/cluster/EntityType -> effect/unstable/cluster/EntityType (barrel: effect/unstable/cluster) +@effect/cluster/Envelope -> effect/unstable/cluster/Envelope (barrel: effect/unstable/cluster) +@effect/cluster/HttpRunner -> effect/unstable/cluster/HttpRunner (barrel: effect/unstable/cluster) +@effect/cluster/K8sHttpClient -> effect/unstable/cluster/K8sHttpClient (barrel: effect/unstable/cluster) +@effect/cluster/MachineId -> effect/unstable/cluster/MachineId (barrel: effect/unstable/cluster) +@effect/cluster/Message -> effect/unstable/cluster/Message (barrel: effect/unstable/cluster) +@effect/cluster/MessageStorage -> effect/unstable/cluster/MessageStorage (barrel: effect/unstable/cluster) +@effect/cluster/Reply -> effect/unstable/cluster/Reply (barrel: effect/unstable/cluster) +@effect/cluster/Runner -> effect/unstable/cluster/Runner (barrel: effect/unstable/cluster) +@effect/cluster/RunnerAddress -> effect/unstable/cluster/RunnerAddress (barrel: effect/unstable/cluster) +@effect/cluster/RunnerHealth -> effect/unstable/cluster/RunnerHealth (barrel: effect/unstable/cluster) +@effect/cluster/RunnerServer -> effect/unstable/cluster/RunnerServer (barrel: effect/unstable/cluster) +@effect/cluster/RunnerStorage -> effect/unstable/cluster/RunnerStorage (barrel: effect/unstable/cluster) +@effect/cluster/Runners -> effect/unstable/cluster/Runners (barrel: effect/unstable/cluster) +@effect/cluster/ShardId -> effect/unstable/cluster/ShardId (barrel: effect/unstable/cluster) +@effect/cluster/Sharding -> effect/unstable/cluster/Sharding (barrel: effect/unstable/cluster) +@effect/cluster/ShardingConfig -> effect/unstable/cluster/ShardingConfig (barrel: effect/unstable/cluster) +@effect/cluster/ShardingRegistrationEvent -> effect/unstable/cluster/ShardingRegistrationEvent (barrel: effect/unstable/cluster) +@effect/cluster/SingleRunner -> effect/unstable/cluster/SingleRunner (barrel: effect/unstable/cluster) +@effect/cluster/Singleton -> effect/unstable/cluster/Singleton (barrel: effect/unstable/cluster) +@effect/cluster/SingletonAddress -> effect/unstable/cluster/SingletonAddress (barrel: effect/unstable/cluster) +@effect/cluster/Snowflake -> effect/unstable/cluster/Snowflake (barrel: effect/unstable/cluster) +@effect/cluster/SocketRunner -> effect/unstable/cluster/SocketRunner (barrel: effect/unstable/cluster) +@effect/cluster/SqlMessageStorage -> effect/unstable/cluster/SqlMessageStorage (barrel: effect/unstable/cluster) +@effect/cluster/SqlRunnerStorage -> effect/unstable/cluster/SqlRunnerStorage (barrel: effect/unstable/cluster) +@effect/cluster/TestRunner -> effect/unstable/cluster/TestRunner (barrel: effect/unstable/cluster) +@effect/experimental/DevTools -> effect/unstable/devtools/DevTools (barrel: effect/unstable/devtools) +@effect/experimental/DevTools/Client -> effect/unstable/devtools/DevToolsClient (barrel: effect/unstable/devtools) +@effect/experimental/DevTools/Domain -> effect/unstable/devtools/DevToolsSchema (barrel: effect/unstable/devtools) +@effect/experimental/DevTools/Server -> effect/unstable/devtools/DevToolsServer (barrel: effect/unstable/devtools) +@effect/platform/MsgPack -> effect/unstable/encoding/Msgpack (barrel: effect/unstable/encoding) +@effect/platform/Ndjson -> effect/unstable/encoding/Ndjson (barrel: effect/unstable/encoding) +@effect/experimental/Sse -> effect/unstable/encoding/Sse (barrel: effect/unstable/encoding) +@effect/ai/AiError -> effect/unstable/ai/AiError (barrel: effect/unstable/ai) +@effect/ai/Chat -> effect/unstable/ai/Chat (barrel: effect/unstable/ai) +@effect/ai/EmbeddingModel -> effect/unstable/ai/EmbeddingModel (barrel: effect/unstable/ai) +@effect/ai/IdGenerator -> effect/unstable/ai/IdGenerator (barrel: effect/unstable/ai) +@effect/ai/LanguageModel -> effect/unstable/ai/LanguageModel (barrel: effect/unstable/ai) +@effect/ai/McpSchema -> effect/unstable/ai/McpSchema (barrel: effect/unstable/ai) +@effect/ai/McpServer -> effect/unstable/ai/McpServer (barrel: effect/unstable/ai) +@effect/ai/Model -> effect/unstable/ai/Model (barrel: effect/unstable/ai) +@effect/ai/Prompt -> effect/unstable/ai/Prompt (barrel: effect/unstable/ai) +@effect/ai/Response -> effect/unstable/ai/Response (barrel: effect/unstable/ai) +@effect/ai/Telemetry -> effect/unstable/ai/Telemetry (barrel: effect/unstable/ai) +@effect/ai/Tokenizer -> effect/unstable/ai/Tokenizer (barrel: effect/unstable/ai) +@effect/ai/Tool -> effect/unstable/ai/Tool (barrel: effect/unstable/ai) +@effect/ai/Toolkit -> effect/unstable/ai/Toolkit (barrel: effect/unstable/ai) +@effect/experimental/Event -> effect/unstable/eventlog/Event (barrel: effect/unstable/eventlog) +@effect/experimental/EventGroup -> effect/unstable/eventlog/EventGroup (barrel: effect/unstable/eventlog) +@effect/experimental/EventJournal -> effect/unstable/eventlog/EventJournal (barrel: effect/unstable/eventlog) +@effect/experimental/EventLog -> effect/unstable/eventlog/EventLog (barrel: effect/unstable/eventlog) +@effect/experimental/EventLogEncryption -> effect/unstable/eventlog/EventLogEncryption (barrel: effect/unstable/eventlog) +@effect/experimental/EventLogRemote -> effect/unstable/eventlog/EventLogMessage (barrel: effect/unstable/eventlog) +@effect/experimental/EventLogRemote -> effect/unstable/eventlog/EventLogRemote (barrel: effect/unstable/eventlog) +@effect/experimental/EventLogServer -> effect/unstable/eventlog/EventLogServer (barrel: effect/unstable/eventlog) +@effect/experimental/EventLogServer -> effect/unstable/eventlog/EventLogServerEncrypted (barrel: effect/unstable/eventlog) +@effect/sql/SqlEventJournal -> effect/unstable/eventlog/SqlEventJournal (barrel: effect/unstable/eventlog) +@effect/sql/SqlEventLogServer -> effect/unstable/eventlog/SqlEventLogServerEncrypted (barrel: effect/unstable/eventlog) +@effect/platform/Cookies -> effect/unstable/http/Cookies (barrel: effect/unstable/http) +@effect/platform/Etag -> effect/unstable/http/Etag (barrel: effect/unstable/http) +@effect/platform/FetchHttpClient -> effect/unstable/http/FetchHttpClient (barrel: effect/unstable/http) +@effect/platform/Headers -> effect/unstable/http/Headers (barrel: effect/unstable/http) +@effect/platform/HttpBody -> effect/unstable/http/HttpBody (barrel: effect/unstable/http) +@effect/platform/HttpClient -> effect/unstable/http/HttpClient (barrel: effect/unstable/http) +@effect/platform/HttpClientError -> effect/unstable/http/HttpClientError (barrel: effect/unstable/http) +@effect/platform/HttpClientRequest -> effect/unstable/http/HttpClientRequest (barrel: effect/unstable/http) +@effect/platform/HttpClientResponse -> effect/unstable/http/HttpClientResponse (barrel: effect/unstable/http) +@effect/platform/HttpApp -> effect/unstable/http/HttpEffect (barrel: effect/unstable/http) +@effect/platform/HttpIncomingMessage -> effect/unstable/http/HttpIncomingMessage (barrel: effect/unstable/http) +@effect/platform/HttpMethod -> effect/unstable/http/HttpMethod (barrel: effect/unstable/http) +@effect/platform/HttpMiddleware -> effect/unstable/http/HttpMiddleware (barrel: effect/unstable/http) +@effect/platform/HttpPlatform -> effect/unstable/http/HttpPlatform (barrel: effect/unstable/http) +@effect/platform/HttpRouter -> effect/unstable/http/HttpRouter (barrel: effect/unstable/http) +@effect/platform/HttpServer -> effect/unstable/http/HttpServer (barrel: effect/unstable/http) +@effect/platform/HttpServerError -> effect/unstable/http/HttpServerError (barrel: effect/unstable/http) +@effect/platform/HttpServerRequest -> effect/unstable/http/HttpServerRequest (barrel: effect/unstable/http) +@effect/platform/HttpServerRespondable -> effect/unstable/http/HttpServerRespondable (barrel: effect/unstable/http) +@effect/platform/HttpServerResponse -> effect/unstable/http/HttpServerResponse (barrel: effect/unstable/http) +@effect/platform/HttpTraceContext -> effect/unstable/http/HttpTraceContext (barrel: effect/unstable/http) +@effect/platform/Multipart -> effect/unstable/http/Multipart (barrel: effect/unstable/http) +@effect/platform/Template -> effect/unstable/http/Template (barrel: effect/unstable/http) +@effect/platform/Url -> effect/unstable/http/Url (barrel: effect/unstable/http) +@effect/platform/UrlParams -> effect/unstable/http/UrlParams (barrel: effect/unstable/http) +@effect/platform/HttpApi -> effect/unstable/httpapi/HttpApi (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiBuilder -> effect/unstable/httpapi/HttpApiBuilder (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiClient -> effect/unstable/httpapi/HttpApiClient (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiEndpoint -> effect/unstable/httpapi/HttpApiEndpoint (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiError -> effect/unstable/httpapi/HttpApiError (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiGroup -> effect/unstable/httpapi/HttpApiGroup (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiMiddleware -> effect/unstable/httpapi/HttpApiMiddleware (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiScalar -> effect/unstable/httpapi/HttpApiScalar (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiSchema -> effect/unstable/httpapi/HttpApiSchema (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiSecurity -> effect/unstable/httpapi/HttpApiSecurity (barrel: effect/unstable/httpapi) +@effect/platform/HttpApiSwagger -> effect/unstable/httpapi/HttpApiSwagger (barrel: effect/unstable/httpapi) +@effect/platform/OpenApi -> effect/unstable/httpapi/OpenApi (barrel: effect/unstable/httpapi) +@effect/opentelemetry/Otlp -> effect/unstable/observability/Otlp (barrel: effect/unstable/observability) +@effect/opentelemetry/internal/otlpExporter -> effect/unstable/observability/OtlpExporter (barrel: effect/unstable/observability) +@effect/opentelemetry/OtlpLogger -> effect/unstable/observability/OtlpLogger (barrel: effect/unstable/observability) +@effect/opentelemetry/OtlpMetrics -> effect/unstable/observability/OtlpMetrics (barrel: effect/unstable/observability) +@effect/opentelemetry/OtlpResource -> effect/unstable/observability/OtlpResource (barrel: effect/unstable/observability) +@effect/opentelemetry/OtlpSerialization -> effect/unstable/observability/OtlpSerialization (barrel: effect/unstable/observability) +@effect/opentelemetry/OtlpTracer -> effect/unstable/observability/OtlpTracer (barrel: effect/unstable/observability) +@effect/platform/KeyValueStore -> effect/unstable/persistence/KeyValueStore (barrel: effect/unstable/persistence) +@effect/experimental/Persistence -> effect/unstable/persistence/Persistable (barrel: effect/unstable/persistence) +@effect/experimental/PersistedCache -> effect/unstable/persistence/PersistedCache (barrel: effect/unstable/persistence) +@effect/experimental/PersistedQueue -> effect/unstable/persistence/PersistedQueue (barrel: effect/unstable/persistence) +@effect/experimental/Persistence -> effect/unstable/persistence/Persistence (barrel: effect/unstable/persistence) +@effect/experimental/RateLimiter -> effect/unstable/persistence/RateLimiter (barrel: effect/unstable/persistence) +@effect/platform/Command -> effect/unstable/process/ChildProcess (barrel: effect/unstable/process) +@effect/platform/CommandExecutor -> effect/unstable/process/ChildProcessSpawner (barrel: effect/unstable/process) +@effect/experimental/Reactivity -> effect/unstable/reactivity/Reactivity (barrel: effect/unstable/reactivity) +@effect/rpc/Rpc -> effect/unstable/rpc/Rpc (barrel: effect/unstable/rpc) +@effect/rpc/RpcClient -> effect/unstable/rpc/RpcClient (barrel: effect/unstable/rpc) +@effect/rpc/RpcClientError -> effect/unstable/rpc/RpcClientError (barrel: effect/unstable/rpc) +@effect/rpc/RpcGroup -> effect/unstable/rpc/RpcGroup (barrel: effect/unstable/rpc) +@effect/rpc/RpcMessage -> effect/unstable/rpc/RpcMessage (barrel: effect/unstable/rpc) +@effect/rpc/RpcMiddleware -> effect/unstable/rpc/RpcMiddleware (barrel: effect/unstable/rpc) +@effect/rpc/RpcSchema -> effect/unstable/rpc/RpcSchema (barrel: effect/unstable/rpc) +@effect/rpc/RpcSerialization -> effect/unstable/rpc/RpcSerialization (barrel: effect/unstable/rpc) +@effect/rpc/RpcServer -> effect/unstable/rpc/RpcServer (barrel: effect/unstable/rpc) +@effect/rpc/RpcTest -> effect/unstable/rpc/RpcTest (barrel: effect/unstable/rpc) +@effect/rpc/RpcWorker -> effect/unstable/rpc/RpcWorker (barrel: effect/unstable/rpc) +@effect/sql/Model -> effect/unstable/schema/Model (barrel: effect/unstable/schema) +@effect/experimental/VariantSchema -> effect/unstable/schema/VariantSchema (barrel: effect/unstable/schema) +@effect/platform/Socket -> effect/unstable/socket/Socket (barrel: effect/unstable/socket) +@effect/platform/SocketServer -> effect/unstable/socket/SocketServer (barrel: effect/unstable/socket) +@effect/sql/Migrator -> effect/unstable/sql/Migrator (barrel: effect/unstable/sql) +@effect/sql/SqlClient -> effect/unstable/sql/SqlClient (barrel: effect/unstable/sql) +@effect/sql/SqlConnection -> effect/unstable/sql/SqlConnection (barrel: effect/unstable/sql) +@effect/sql/SqlError -> effect/unstable/sql/SqlError (barrel: effect/unstable/sql) +@effect/sql/Model -> effect/unstable/sql/SqlModel (barrel: effect/unstable/sql) +@effect/sql/SqlResolver -> effect/unstable/sql/SqlResolver (barrel: effect/unstable/sql) +@effect/sql/SqlSchema -> effect/unstable/sql/SqlSchema (barrel: effect/unstable/sql) +@effect/sql/SqlStream -> effect/unstable/sql/SqlStream (barrel: effect/unstable/sql) +@effect/sql/Statement -> effect/unstable/sql/Statement (barrel: effect/unstable/sql) +@effect/platform/Transferable -> effect/unstable/workers/Transferable (barrel: effect/unstable/workers) +@effect/platform/Worker -> effect/unstable/workers/Worker (barrel: effect/unstable/workers) +@effect/platform/WorkerError -> effect/unstable/workers/WorkerError (barrel: effect/unstable/workers) +@effect/platform/WorkerRunner -> effect/unstable/workers/WorkerRunner (barrel: effect/unstable/workers) +@effect/workflow/Activity -> effect/unstable/workflow/Activity (barrel: effect/unstable/workflow) +@effect/workflow/DurableClock -> effect/unstable/workflow/DurableClock (barrel: effect/unstable/workflow) +@effect/workflow/DurableDeferred -> effect/unstable/workflow/DurableDeferred (barrel: effect/unstable/workflow) +@effect/workflow/DurableQueue -> effect/unstable/workflow/DurableQueue (barrel: effect/unstable/workflow) +@effect/workflow/Workflow -> effect/unstable/workflow/Workflow (barrel: effect/unstable/workflow) +@effect/workflow/WorkflowEngine -> effect/unstable/workflow/WorkflowEngine (barrel: effect/unstable/workflow) +@effect/workflow/WorkflowProxy -> effect/unstable/workflow/WorkflowProxy (barrel: effect/unstable/workflow) +@effect/workflow/WorkflowProxyServer -> effect/unstable/workflow/WorkflowProxyServer (barrel: effect/unstable/workflow) +effect/Array -> effect/Array (barrel: effect) +effect/BigDecimal -> effect/BigDecimal (barrel: effect) +effect/BigInt -> effect/BigInt (barrel: effect) +effect/Boolean -> effect/Boolean (barrel: effect) +effect/Brand -> effect/Brand (barrel: effect) +effect/Cache -> effect/Cache (barrel: effect) +effect/Cause -> effect/Cause (barrel: effect) +effect/Channel -> effect/Channel (barrel: effect) +effect/Chunk -> effect/Chunk (barrel: effect) +effect/Clock -> effect/Clock (barrel: effect) +@effect/typeclass/Semigroup -> effect/Combiner (barrel: effect) +effect/Config -> effect/Config (barrel: effect) +effect/ConfigProvider -> effect/ConfigProvider (barrel: effect) +effect/Console -> effect/Console (barrel: effect) +effect/Context -> effect/Context (barrel: effect) +effect/Cron -> effect/Cron (barrel: effect) +effect/Data -> effect/Data (barrel: effect) +effect/DateTime -> effect/DateTime (barrel: effect) +effect/Deferred -> effect/Deferred (barrel: effect) +effect/Differ -> effect/Differ (barrel: effect) +effect/Duration -> effect/Duration (barrel: effect) +effect/Effect -> effect/Effect (barrel: effect) +effect/Effectable -> effect/Effectable (barrel: effect) +effect/Encoding -> effect/Encoding (barrel: effect) +effect/Equal -> effect/Equal (barrel: effect) +effect/Equivalence -> effect/Equivalence (barrel: effect) +effect/ExecutionPlan -> effect/ExecutionPlan (barrel: effect) +effect/Exit -> effect/Exit (barrel: effect) +effect/Fiber -> effect/Fiber (barrel: effect) +effect/FiberHandle -> effect/FiberHandle (barrel: effect) +effect/FiberMap -> effect/FiberMap (barrel: effect) +effect/FiberSet -> effect/FiberSet (barrel: effect) +effect/Inspectable -> effect/Formatter (barrel: effect) +effect/Function -> effect/Function (barrel: effect) +effect/Graph -> effect/Graph (barrel: effect) +effect/HKT -> effect/HKT (barrel: effect) +effect/Hash -> effect/Hash (barrel: effect) +effect/HashMap -> effect/HashMap (barrel: effect) +effect/HashRing -> effect/HashRing (barrel: effect) +effect/HashSet -> effect/HashSet (barrel: effect) +effect/Inspectable -> effect/Inspectable (barrel: effect) +effect/Iterable -> effect/Iterable (barrel: effect) +effect/Layer -> effect/Layer (barrel: effect) +effect/LayerMap -> effect/LayerMap (barrel: effect) +effect/LogLevel -> effect/LogLevel (barrel: effect) +effect/Logger -> effect/Logger (barrel: effect) +effect/ManagedRuntime -> effect/ManagedRuntime (barrel: effect) +effect/Match -> effect/Match (barrel: effect) +effect/Metric -> effect/Metric (barrel: effect) +effect/MutableHashMap -> effect/MutableHashMap (barrel: effect) +effect/MutableHashSet -> effect/MutableHashSet (barrel: effect) +effect/MutableList -> effect/MutableList (barrel: effect) +effect/MutableRef -> effect/MutableRef (barrel: effect) +effect/NonEmptyIterable -> effect/NonEmptyIterable (barrel: effect) +effect/Number -> effect/Number (barrel: effect) +effect/Option -> effect/Option (barrel: effect) +effect/Order -> effect/Order (barrel: effect) +effect/Ordering -> effect/Ordering (barrel: effect) +effect/PartitionedSemaphore -> effect/PartitionedSemaphore (barrel: effect) +effect/Pipeable -> effect/Pipeable (barrel: effect) +effect/Pool -> effect/Pool (barrel: effect) +effect/Predicate -> effect/Predicate (barrel: effect) +effect/PrimaryKey -> effect/PrimaryKey (barrel: effect) +effect/PubSub -> effect/PubSub (barrel: effect) +effect/Queue -> effect/Queue (barrel: effect) +effect/Random -> effect/Random (barrel: effect) +effect/RcMap -> effect/RcMap (barrel: effect) +effect/RcRef -> effect/RcRef (barrel: effect) +effect/Record -> effect/Record (barrel: effect) +effect/Inspectable -> effect/Redactable (barrel: effect) +effect/Redacted -> effect/Redacted (barrel: effect) +@effect/typeclass/Monoid -> effect/Reducer (barrel: effect) +effect/Ref -> effect/Ref (barrel: effect) +effect/FiberRef -> effect/References (barrel: effect) +effect/RegExp -> effect/RegExp (barrel: effect) +effect/Request -> effect/Request (barrel: effect) +effect/RequestResolver -> effect/RequestResolver (barrel: effect) +effect/Resource -> effect/Resource (barrel: effect) +effect/Runtime -> effect/Runtime (barrel: effect) +effect/Schedule -> effect/Schedule (barrel: effect) +effect/Scheduler -> effect/Scheduler (barrel: effect) +effect/Schema -> effect/Schema (barrel: effect) +effect/SchemaAST -> effect/SchemaAST (barrel: effect) +effect/ParseResult -> effect/SchemaIssue (barrel: effect) +effect/ParseResult -> effect/SchemaParser (barrel: effect) +effect/Schema -> effect/SchemaTransformation (barrel: effect) +effect/Scope -> effect/Scope (barrel: effect) +effect/ScopedCache -> effect/ScopedCache (barrel: effect) +effect/ScopedRef -> effect/ScopedRef (barrel: effect) +effect/Sink -> effect/Sink (barrel: effect) +effect/Stream -> effect/Stream (barrel: effect) +effect/String -> effect/String (barrel: effect) +effect/Struct -> effect/Struct (barrel: effect) +effect/SubscriptionRef -> effect/SubscriptionRef (barrel: effect) +effect/Symbol -> effect/Symbol (barrel: effect) +effect/SynchronizedRef -> effect/SynchronizedRef (barrel: effect) +effect/Take -> effect/Take (barrel: effect) +effect/Tracer -> effect/Tracer (barrel: effect) +effect/Trie -> effect/Trie (barrel: effect) +effect/Tuple -> effect/Tuple (barrel: effect) +effect/Types -> effect/Types (barrel: effect) +effect/Unify -> effect/Unify (barrel: effect) +effect/Utils -> effect/Utils (barrel: effect) +``` + +## No Counterpart Imports + +These v4 modules did not have a mapped v3 module. Treat them as v4-only unless a +more specific migration guide says otherwise. + +```text +effect/ErrorReporter (barrel: effect) +effect/Filter (barrel: effect) +effect/JsonPatch (barrel: effect) +effect/JsonPointer (barrel: effect) +effect/Latch (barrel: effect) +effect/Newtype (barrel: effect) +effect/Optic (barrel: effect) +effect/Pull (barrel: effect) +effect/SchemaGetter (barrel: effect) +effect/SchemaRepresentation (barrel: effect) +effect/SchemaUtils (barrel: effect) +effect/Semaphore (barrel: effect) +effect/Stdio (barrel: effect) +effect/TxChunk (barrel: effect) +effect/UndefinedOr (barrel: effect) +effect/testing/TestConsole (barrel: effect/testing) +effect/testing/TestSchema (barrel: effect/testing) +effect/unstable/ai/AnthropicStructuredOutput (barrel: effect/unstable/ai) +effect/unstable/ai/OpenAiStructuredOutput (barrel: effect/unstable/ai) +effect/unstable/ai/ResponseIdTracker (barrel: effect/unstable/ai) +effect/unstable/cli/CliOutput (barrel: effect/unstable/cli) +effect/unstable/cli/Param (barrel: effect/unstable/cli) +effect/unstable/eventlog/EventLogServerUnencrypted (barrel: effect/unstable/eventlog) +effect/unstable/eventlog/EventLogSessionAuth (barrel: effect/unstable/eventlog) +effect/unstable/eventlog/SqlEventLogServerUnencrypted (barrel: effect/unstable/eventlog) +effect/unstable/http/FindMyWay (barrel: effect/unstable/http) +effect/unstable/http/HttpStaticServer (barrel: effect/unstable/http) +effect/unstable/http/Multipasta (barrel: effect/unstable/http) +effect/unstable/http/Multipasta/HeadersParser (barrel: effect/unstable/http) +effect/unstable/http/Multipasta/Node (barrel: effect/unstable/http) +effect/unstable/http/Multipasta/Search (barrel: effect/unstable/http) +effect/unstable/http/Multipasta/Web (barrel: effect/unstable/http) +effect/unstable/httpapi/HttpApiTest (barrel: effect/unstable/httpapi) +effect/unstable/observability/PrometheusMetrics (barrel: effect/unstable/observability) +effect/unstable/persistence/Redis (barrel: effect/unstable/persistence) +effect/unstable/reactivity/AsyncResult (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/Atom (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/AtomHttpApi (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/AtomRef (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/AtomRegistry (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/AtomRpc (barrel: effect/unstable/reactivity) +effect/unstable/reactivity/Hydration (barrel: effect/unstable/reactivity) +effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) +``` + +## API Renames + +Each line is `v3 API -> v4 API`. Use these mappings when rewriting renamed +symbols from v3 source code to v4. + +```text +Effect.async -> Effect.callback +Effect.zipRight -> Effect.andThen +Effect.zipLeft -> Effect.tap +Effect.either -> Effect.result +Effect.catchAll -> Effect.catch +Effect.catchAllCause -> Effect.catchCause +Effect.catchAllDefect -> Effect.catchDefect +Effect.catchSome -> Effect.catchIf +Effect.catchIf -> Effect.catchIf +Effect.optionFromOptional -> Effect.catchNoSuchElement +Effect.catchSomeCause -> Effect.catchCauseIf +Effect.tapErrorCause -> Effect.tapCause +Effect.ignoreLogged -> Effect.ignore +Effect.makeLatchUnsafe -> Latch.makeUnsafe +Effect.makeLatch -> Latch.make +Layer.scoped -> Layer.effect +Layer.scopedDiscard -> Layer.effectDiscard +Layer.tapErrorCause -> Layer.tapCause +Mailbox -> Queue.Queue +Mailbox.make -> Queue.make +Either -> Result.Result +Either.right -> Result.succeed +Either.left -> Result.fail +Scope.extend -> Scope.provide +Effect.makeSemaphoreUnsafe -> Semaphore.makeUnsafe +Effect.makeSemaphore -> Semaphore.make +Stream.Context -> Stream.Services +StreamHaltStrategy.HaltStrategy -> Stream.HaltStrategy +Stream.repeatEffect -> Stream.fromEffectRepeat +Stream.repeatEffectWithSchedule -> Stream.fromEffectSchedule +Stream.async -> Stream.callback +Stream.asyncEffect -> Stream.callback +Stream.asyncPush -> Stream.callback +Stream.asyncScoped -> Stream.callback +Stream.repeatEffectChunk -> Stream.fromIterableEffectRepeat +Stream.fromChunk -> Stream.fromArray +Stream.fromChunks -> Stream.fromArrays +Stream.mapChunks -> Stream.mapArray +Stream.mapChunksEffect -> Stream.mapArrayEffect +Stream.either -> Stream.result +Stream.flattenChunks -> Stream.flattenArray +Stream.flattenIterables -> Stream.flattenIterable +Stream.mergeEither -> Stream.mergeResult +Stream.zipWithChunks -> Stream.zipWithArray +Stream.bufferChunks -> Stream.bufferArray +Stream.catchAllCause -> Stream.catchCause +Stream.tapErrorCause -> Stream.tapCause +Stream.catchAll -> Stream.catch +Stream.catchSome -> Stream.catchIf +Stream.catchSomeCause -> Stream.catchCauseIf +Stream.combineChunks -> Stream.combineArray +provideSomeLayer -> Stream.provide +provideSomeContext -> Stream.provide +``` diff --git a/repos/effect/migration/yieldable.md b/repos/effect/migration/yieldable.md new file mode 100644 index 0000000..f577813 --- /dev/null +++ b/repos/effect/migration/yieldable.md @@ -0,0 +1,173 @@ +# Effect Subtyping (v3) → Yieldable (v4) + +In v3, many types were structural subtypes of `Effect` — they carried the +Effect type ID at runtime and could be used anywhere an `Effect` was expected. +This included `Ref`, `Deferred`, `Fiber`, `FiberRef`, `Config`, `Option`, +`Either`, `Context.Tag`, and others. + +While convenient, this created a class of subtle bugs. Because these types +_were_ Effects, they could be silently passed to Effect combinators when you +intended to pass the value itself. For example, passing a `Ref` where you meant +to pass the value inside the `Ref`, or accidentally mapping over a `Deferred` +as an Effect instead of awaiting it. + +v4 replaces this with the **`Yieldable`** trait: a narrower contract that +allows `yield*` in generators but does **not** make the type assignable to +`Effect`. + +## The `Yieldable` Interface + +```ts +interface Yieldable { + asEffect(): Effect + [Symbol.iterator](): EffectIterator +} +``` + +Some example types that implement `Yieldable`: + +- `Effect` itself +- `Option` — yields the value or fails with `NoSuchElementError` +- `Result` — yields the success or fails with the error +- `Config` — yields the config value or fails with `ConfigError` +- `Context.Service` — yields the service from the environment + +Some example types that are **no longer** Effect subtypes and do **not** +implement `Yieldable`: + +- `Ref` — use `Ref.get(ref)` to read +- `Deferred` — use `Deferred.await(deferred)` to wait +- `Fiber` — use `Fiber.join(fiber)` to await + +## `yield*` Still Works + +`yield*` in `Effect.gen` works with any `Yieldable`. The runtime calls +`.asEffect()` internally when yielding. + +```ts +import { Effect, Option } from "effect" + +// The type of program is `Effect` +const program = Effect.gen(function*() { + // yield* works with Yieldable types — same as v3 + const value = yield* Option.some(42) + return value // 42 +}) +``` + +## Effect Combinators Require `.asEffect()` + +In v3, you could pass a `Yieldable` type directly to Effect combinators because +it was a subtype of `Effect`. In v4, you must explicitly convert with +`.asEffect()`. + +**v3** — Option is an Effect subtype, so this compiles: + +```ts +import { Effect, Option } from "effect" + +// Option is assignable to Effect +const program = Effect.map(Option.some(42), (n) => n + 1) +``` + +**v4** — Option is not an Effect, so you must convert explicitly: + +```ts +import { Effect, Option } from "effect" + +// Option is Yieldable but not Effect — use .asEffect() +const program = Effect.map(Option.some(42).asEffect(), (n) => n + 1) + +// Or more idiomatically, use a generator: +const program2 = Effect.gen(function*() { + const n = yield* Option.some(42) + return n + 1 +}) +``` + +## Types No Longer Subtypes of Effect + +Several types that extended `Effect` in v3 no longer do so in v4. Use the +appropriate module functions instead. + +**v3** — `Ref` extends `Effect`, yielding the current value: + +```ts +import { Effect, Ref } from "effect" + +const program = Effect.gen(function*() { + const ref = yield* Ref.make(0) + const value = yield* ref // Ref is an Effect +}) +``` + +**v4** — `Ref` is a plain value, use `Ref.get`: + +```ts +import { Effect, Ref } from "effect" + +const program = Effect.gen(function*() { + const ref = yield* Ref.make(0) + const value = yield* Ref.get(ref) +}) +``` + +**v3** — `Deferred` extends `Effect`, resolving when completed: + +```ts +import { Deferred, Effect } from "effect" + +const program = Effect.gen(function*() { + const deferred = yield* Deferred.make() + const value = yield* deferred // Deferred is an Effect +}) +``` + +**v4** — `Deferred` is a plain value, use `Deferred.await`: + +```ts +import { Deferred, Effect } from "effect" + +const program = Effect.gen(function*() { + const deferred = yield* Deferred.make() + const value = yield* Deferred.await(deferred) +}) +``` + +**v3** — `Fiber` extends `Effect`, joining on yield: + +```ts +import { Effect, Fiber } from "effect" + +const program = Effect.gen(function*() { + const fiber = yield* Effect.fork(task) + const result = yield* fiber // Fiber is an Effect +}) +``` + +**v4** — `Fiber` is a plain value, use `Fiber.join`: + +```ts +import { Effect, Fiber } from "effect" + +const program = Effect.gen(function*() { + const fiber = yield* Effect.forkChild(task) + const result = yield* Fiber.join(fiber) +}) +``` + +## Why This Changed + +The v3 subtyping approach meant the type system could not distinguish between +"I have a Ref" and "I have an Effect that reads the Ref." This ambiguity led +to bugs that were difficult to diagnose: + +- Passing a `Ref` to `Effect.map` would read the ref's value rather than + transforming the ref itself — often not the intended behavior. +- A `Deferred` in a data structure could silently be treated as an Effect, + causing unexpected awaits. +- Combinators like `Effect.all` would accept an array of `Ref` values and + silently read all of them, instead of producing a type error. + +The `Yieldable` trait preserves the ergonomic `yield*` syntax in generators +while making the conversion to `Effect` explicit everywhere else. diff --git a/repos/effect/package.json b/repos/effect/package.json new file mode 100644 index 0000000..1378e35 --- /dev/null +++ b/repos/effect/package.json @@ -0,0 +1,102 @@ +{ + "private": true, + "type": "module", + "packageManager": "pnpm@10.17.1", + "scripts": { + "prepare": "effect-tsgo patch", + "clean": "node scripts/clean.mjs", + "codegen": "pnpm --recursive --parallel --filter \"./packages/**/*\" run codegen", + "codemod": "node scripts/codemod.mjs", + "build": "tsc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", + "bundle-analyze": "bash scripts/bundle-analyze.sh", + "bundle-compare": "bash scripts/bundle-compare.sh", + "bundle-compare-selected": "bash scripts/bundle-compare-selected.sh", + "circular": "node scripts/circular.mjs", + "test": "vitest", + "coverage": "vitest --coverage", + "check": "tsc -b tsconfig.json", + "typeperf": "pnpm --dir packages/effect exec node typeperf/run.mjs", + "typeperf-compare": "pnpm --dir packages/effect exec node typeperf/compare.mjs", + "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tsc -b tsconfig.json", + "jsdocs": "effect-jsdocs", + "lint": "pnpm jsdocs && oxlint -f unix && dprint check", + "lint-fix": "pnpm jsdocs && oxlint --fix && dprint fmt", + "docgen": "pnpm --recursive --filter \"./packages/**/*\" exec docgen && node scripts/docs.mjs", + "ai-docgen": "effect-ai-docgen ai-docs/src -o LLMS.md", + "ai-docgen:watch": "pnpm ai-docgen --watch", + "test-types": "tstyche --target '>=5.9'", + "changeset-version": "changeset version", + "changeset-publish": "pnpm codemod && pnpm build && changeset publish" + }, + "devDependencies": { + "@babel/cli": "^7.28.6", + "@babel/core": "^7.29.0", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@changesets/changelog-github": "^0.7.0", + "@changesets/cli": "^2.31.0", + "@effect/ai-docgen": "workspace:^", + "@effect/bundle": "workspace:^", + "@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5", + "@effect/jsdocs": "workspace:^", + "@effect/oxc": "workspace:^", + "@effect/tsgo": "^0.21.0", + "@effect/utils": "workspace:^", + "@effect/vitest": "workspace:^", + "@faker-js/faker": "^10.5.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@types/jscodeshift": "^17.3.0", + "@types/node": "^25.7.0", + "@vitest/browser": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/expect": "4.1.10", + "@vitest/web-worker": "4.1.10", + "ast-types": "^0.14.2", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "dprint": "^0.55.1", + "glob": "^13.0.6", + "happy-dom": "^20.10.6", + "jscodeshift": "^17.3.0", + "lalph": "^0.3.139", + "madge": "^8.0.0", + "oxlint": "1.42.0", + "playwright": "^1.61.1", + "rollup": "^4.62.2", + "rollup-plugin-bundle-stats": "^4.22.2", + "rollup-plugin-esbuild": "^6.2.1", + "rollup-plugin-visualizer": "^7.0.1", + "terser": "^5.49.0", + "tstyche": "^7.2.1", + "typescript": "^7.0.0", + "vite": "^7.2.7", + "vite-tsconfig-paths": "^6.1.1", + "vitest": "4.1.10", + "vitest-websocket-mock": "^0.5.0", + "zod": "^4.4.3" + }, + "pnpm": { + "patchedDependencies": { + "@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch", + "@changesets/get-github-info": "patches/@changesets__get-github-info.patch" + }, + "ignoredBuiltDependencies": [ + "core-js", + "cpu-features", + "dprint", + "esbuild", + "lmdb", + "msgpackr-extract", + "msw", + "protobufjs", + "sharp", + "ssh2", + "unrs-resolver", + "workerd" + ], + "onlyBuiltDependencies": [ + "better-sqlite3" + ] + } +} diff --git a/repos/effect/packages/ai/anthropic/CHANGELOG.md b/repos/effect/packages/ai/anthropic/CHANGELOG.md new file mode 100644 index 0000000..a06868a --- /dev/null +++ b/repos/effect/packages/ai/anthropic/CHANGELOG.md @@ -0,0 +1,736 @@ +# @effect/ai-anthropic + +## 4.0.0-beta.99 + +### Patch Changes + +- [#6371](https://github.com/Effect-TS/effect/pull/6371) [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd) Thanks @polRk! - Fix client-executed provider tools (Memory, Text Editor, Computer Use, Bash) which were unusable on the wire. + - `makeResponse` (and the streaming equivalents) now map a provider `tool_use` wire name (e.g. `"memory"`) back to the tool's custom name (e.g. `"AnthropicMemory"`) that the toolkit is keyed by, instead of raising `ToolNotFoundError`. + - `AnthropicTool.MemoryCreateCommand` now includes the required `file_text` field, so a `create` command no longer drops the file body. + - Optional parameters on client-executed provider tools now use `Schema.optionalKey` instead of `Schema.optional`, which the Anthropic codec rejected with "Unsupported AST Undefined": `Memory`/`TextEditor` `view_range`, `ComputerUse` `coordinate`, and `Bash` `restart`. + + Closes [#2615](https://github.com/Effect-TS/effect/issues/2615). + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- [#2568](https://github.com/Effect-TS/effect-smol/pull/2568) [`97a4138`](https://github.com/Effect-TS/effect-smol/commit/97a4138f9e9d31d38026f935b52d423f5ccadc4f) Thanks @K-Mistele! - Widen the Anthropic `Model` schema to accept both known model identifiers as well + as any string to allow for newer models +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- [#2531](https://github.com/Effect-TS/effect-smol/pull/2531) [`bdca354`](https://github.com/Effect-TS/effect-smol/commit/bdca35449d5dfce5b4433da75ec0a88d0a9b2b27) Thanks @polRk! - Fix `Memory_20250818` provider-defined tool missing `requiresHandler: true`. Like the other client-executed tools (`TextEditor_20250728`, `Bash_2025*`, `ComputerUse_2025*`), the memory tool requires the application to implement its execution (view/create/str_replace/insert/delete/rename over `/memories/*`). Without this flag, `Tool.HandlersFor` excluded it from the required handlers, making it impossible to type-check a handler for `Memory_20250818` in `Toolkit.toLayer`. + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2450](https://github.com/Effect-TS/effect-smol/pull/2450) [`7b8a549`](https://github.com/Effect-TS/effect-smol/commit/7b8a5493950aa6f9e20024cd7e588838722218f0) Thanks @DomiR! - Fix non-streaming Anthropic responses throwing when a tool call carries `caller` metadata. The mapper emitted `caller.toolId: undefined`, but `ProviderMetadata` is `Record(String, NullOr(Json))` and `undefined` is not a valid Json value, so decoding the model's own response threw `Expected JSON value`. Emit `null` instead, matching the streaming mappers. + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- [#2357](https://github.com/Effect-TS/effect-smol/pull/2357) [`3efe308`](https://github.com/Effect-TS/effect-smol/commit/3efe308fcc6ac642c65560efbd1b3afc68052553) Thanks @jbmusso! - Mark the Claude 4-6 generation as supporting native structured output in `getModelCapabilities`. `claude-opus-4-6` and `claude-sonnet-4-6` support Anthropic's constrained-decoding structured output (verified against the live API), but were classified as `supportsStructuredOutput: false`, so `generateObject` fell back to a forced JSON tool instead of requesting `output_config.format` (`json_schema`). `claude-opus-4-7` / `claude-opus-4-8` are classified the same way for when the generated `Model` enum picks them up. + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- [#1775](https://github.com/Effect-TS/effect-smol/pull/1775) [`954e6d6`](https://github.com/Effect-TS/effect-smol/commit/954e6d655cd32d329b0bfeb872bb654f88b48a13) Thanks @tim-smart! - Add dynamic tool support to the Anthropic language model provider when preparing tool definitions for requests. + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- [#1763](https://github.com/Effect-TS/effect-smol/pull/1763) [`2fa940d`](https://github.com/Effect-TS/effect-smol/commit/2fa940d0709769c5fd1337c1f88400867f91989b) Thanks @teeverc! - Remove duplicate `ToolApprovalResponsePartOptions` from Anthropic package + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- [#1529](https://github.com/Effect-TS/effect-smol/pull/1529) [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8) Thanks @tim-smart! - Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. + +- [#1528](https://github.com/Effect-TS/effect-smol/pull/1528) [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34) Thanks @tim-smart! - Add `Model.ModelName` and provide it from AI model constructors. + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- [#1502](https://github.com/Effect-TS/effect-smol/pull/1502) [`285b7e6`](https://github.com/Effect-TS/effect-smol/commit/285b7e667167566d5788367d5155b19c79f1bf22) Thanks @tim-smart! - allow undefined for ai config + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- [#1354](https://github.com/Effect-TS/effect-smol/pull/1354) [`b94962c`](https://github.com/Effect-TS/effect-smol/commit/b94962c249d46cf96cdf2e41188dc9feda41536a) Thanks @IMax153! - Fix the generated schemas for ai providers + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- [#1333](https://github.com/Effect-TS/effect-smol/pull/1333) [`96260bf`](https://github.com/Effect-TS/effect-smol/commit/96260bf5331fad6e38d2a79e94d70fd9a502ec29) Thanks @IMax153! - Fix tool calling for the Anthropic Effect AI SDK provider integration + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/ai/anthropic/codegen.yml b/repos/effect/packages/ai/anthropic/codegen.yml new file mode 100644 index 0000000..dd23c4b --- /dev/null +++ b/repos/effect/packages/ai/anthropic/codegen.yml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=../../tools/ai-codegen/codegen.schema.json +spec: + type: stainless-stats + statsUrl: https://raw.githubusercontent.com/anthropics/anthropic-sdk-typescript/main/.stats.yml +output: src/Generated.ts +name: AnthropicClient +patches: + # Make iterations and speed optional in BetaUsage, iterations optional in BetaMessageDeltaUsage (API may omit these fields) + - '[{"op":"remove","path":"/components/schemas/BetaUsage/required/9"},{"op":"remove","path":"/components/schemas/BetaUsage/required/5"},{"op":"remove","path":"/components/schemas/BetaMessageDeltaUsage/required/3"}]' +header: | + /** + * @since 1.0.0 + */ +excludeAnnotations: + - examples +replacements: + # Schema.Unknown doesn't work with Schema.toCodecJson (used by HttpClientResponse.schemaBodyJson) + # Replace with Schema.Json which properly handles arbitrary JSON values + - from: "Schema.Record(Schema.String, Schema.Unknown)" + to: "Schema.Record(Schema.String, Schema.Json)" + - from: "{ readonly [x: string]: unknown }" + to: "{ readonly [x: string]: Schema.Json }" + # Index signature in struct rest pattern + - from: "readonly [x: string]: unknown" + to: "readonly [x: string]: Schema.Json" + # Make citations optional in ResponseTextBlock (API may omit this field) + - from: 'export type ResponseTextBlock = { readonly "citations":' + to: 'export type ResponseTextBlock = { readonly "citations"?:' + - from: "export const ResponseTextBlock = Schema.Struct({ \"citations\": Schema.Union([Schema.Array(Schema.Union([ResponseCharLocationCitation, ResponsePageLocationCitation, ResponseContentBlockLocationCitation, ResponseWebSearchResultLocationCitation, ResponseSearchResultLocationCitation], { mode: \"oneOf\" })), Schema.Null]).annotate({ \"title\": \"Citations\", \"description\": \"Citations supporting the text block.\\n\\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.\", \"default\": null }), \"text\":" + to: "export const ResponseTextBlock = Schema.Struct({ \"citations\": Schema.optionalKey(Schema.Union([Schema.Array(Schema.Union([ResponseCharLocationCitation, ResponsePageLocationCitation, ResponseContentBlockLocationCitation, ResponseWebSearchResultLocationCitation, ResponseSearchResultLocationCitation], { mode: \"oneOf\" })), Schema.Null]).annotate({ \"title\": \"Citations\", \"description\": \"Citations supporting the text block.\\n\\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.\", \"default\": null })), \"text\":" + # Make citations optional in BetaResponseTextBlock (API may omit this field) + - from: 'export type BetaResponseTextBlock = { readonly "citations":' + to: 'export type BetaResponseTextBlock = { readonly "citations"?:' + - from: "export const BetaResponseTextBlock = Schema.Struct({ \"citations\": Schema.Union([Schema.Array(Schema.Union([BetaResponseCharLocationCitation, BetaResponsePageLocationCitation, BetaResponseContentBlockLocationCitation, BetaResponseWebSearchResultLocationCitation, BetaResponseSearchResultLocationCitation], { mode: \"oneOf\" })), Schema.Null]).annotate({ \"title\": \"Citations\", \"description\": \"Citations supporting the text block.\\n\\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.\", \"default\": null }), \"text\":" + to: "export const BetaResponseTextBlock = Schema.Struct({ \"citations\": Schema.optionalKey(Schema.Union([Schema.Array(Schema.Union([BetaResponseCharLocationCitation, BetaResponsePageLocationCitation, BetaResponseContentBlockLocationCitation, BetaResponseWebSearchResultLocationCitation, BetaResponseSearchResultLocationCitation], { mode: \"oneOf\" })), Schema.Null]).annotate({ \"title\": \"Citations\", \"description\": \"Citations supporting the text block.\\n\\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.\", \"default\": null })), \"text\":" + # Make server_tool_use optional in usage (API may omit this field) + - from: 'readonly "server_tool_use": ServerToolUsage | null' + to: 'readonly "server_tool_use"?: ServerToolUsage | null' + - from: 'readonly "server_tool_use": BetaServerToolUsage | null' + to: 'readonly "server_tool_use"?: BetaServerToolUsage | null' + - from: '"server_tool_use": Schema.Union([ServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null }),' + to: '"server_tool_use": Schema.optionalKey(Schema.Union([ServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null })),' + - from: '"server_tool_use": Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null }),' + to: '"server_tool_use": Schema.optionalKey(Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null })),' + # Without trailing comma (for last field in struct) + - from: '"server_tool_use": Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null }) })' + to: '"server_tool_use": Schema.optionalKey(Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null })) })' + - from: '"server_tool_use": Schema.Union([ServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null }) })' + to: '"server_tool_use": Schema.optionalKey(Schema.Union([ServerToolUsage, Schema.Null]).annotate({ "description": "The number of server tool requests.", "default": null })) })' + # Make context_management and container optional in BetaMessage (API may omit these fields) + - from: 'readonly "context_management": BetaResponseContextManagement | null' + to: 'readonly "context_management"?: BetaResponseContextManagement | null' + - from: 'readonly "container": BetaContainer | null' + to: 'readonly "container"?: BetaContainer | null' + - from: "\"context_management\": Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ \"description\": \"Context management response.\\n\\nInformation about context management strategies applied during the request.\", \"default\": null })," + to: "\"context_management\": Schema.optionalKey(Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ \"description\": \"Context management response.\\n\\nInformation about context management strategies applied during the request.\", \"default\": null }))," + - from: "\"container\": Schema.Union([BetaContainer, Schema.Null]).annotate({ \"description\": \"Information about the container used in this request.\\n\\nThis will be non-null if a container tool (e.g. code execution) was used.\", \"default\": null })" + to: "\"container\": Schema.optionalKey(Schema.Union([BetaContainer, Schema.Null]).annotate({ \"description\": \"Information about the container used in this request.\\n\\nThis will be non-null if a container tool (e.g. code execution) was used.\", \"default\": null }))" + # Make context_management optional in BetaMessageDeltaEvent (API may omit this field) + - from: '"context_management": Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ "description": "Information about context management strategies applied during the request", "default": null }),' + to: '"context_management": Schema.optionalKey(Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ "description": "Information about context management strategies applied during the request", "default": null })),' + diff --git a/repos/effect/packages/ai/anthropic/docgen.json b/repos/effect/packages/ai/anthropic/docgen.json new file mode 100644 index 0000000..500517e --- /dev/null +++ b/repos/effect/packages/ai/anthropic/docgen.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/anthropic/src/", + "exclude": ["src/internal/**/*.ts", "src/Generated.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["node"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/ai/anthropic/package.json b/repos/effect/packages/ai/anthropic/package.json new file mode 100644 index 0000000..549b119 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/package.json @@ -0,0 +1,66 @@ +{ + "name": "@effect/ai-anthropic", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "An Anthropic provider integration for Effect AI SDK", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/anthropic" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "ai", + "anthropic" + ], + "keywords": [ + "typescript", + "ai", + "anthropic" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts b/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts new file mode 100644 index 0000000..646fead --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts @@ -0,0 +1,432 @@ +/** + * The `AnthropicClient` module defines the low-level Effect service for + * Anthropic's Messages API. It builds a generated Anthropic HTTP client with + * authentication headers, API version headers, response decoding, and error + * mapping, then exposes helpers for regular and streaming message requests. + * + * @since 4.0.0 + */ +import * as Array from "effect/Array" +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import type * as AiError from "effect/unstable/ai/AiError" +import * as Sse from "effect/unstable/encoding/Sse" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpClient from "effect/unstable/http/HttpClient" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import { AnthropicConfig } from "./AnthropicConfig.ts" +import * as Generated from "./Generated.ts" +import * as Errors from "./internal/errors.ts" + +// ============================================================================= +// Service Interface +// ============================================================================= + +/** + * Represents the Anthropic client service with methods for the Messages API, including regular and streaming message + * creation. + * + * @category models + * @since 4.0.0 + */ +export interface Service { + /** + * The underlying generated Anthropic client that exposes all API endpoints. + */ + readonly client: Generated.AnthropicClient + + /** + * Executes a low-level streaming HTTP request and decodes the Server-Sent Events response using the provided schema. + */ + readonly streamRequest: ( + schema: S + ) => (request: HttpClientRequest.HttpClientRequest) => Stream.Stream< + S["Type"], + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + S["DecodingServices"] + > + + /** + * Creates a message using the Anthropic Messages API and maps all errors to the unified `AiError` type. + */ + readonly createMessage: (options: { + readonly payload: typeof Generated.BetaCreateMessageParams.Encoded + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + }) => Effect.Effect< + [body: typeof Generated.BetaMessage.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > + + /** + * Creates a streaming message using the Anthropic Messages API and maps all errors to the unified `AiError` type. + * + * **Details** + * + * The returned Effect yields the HTTP response and a stream of events as the model generates its response. The stream + * automatically terminates when a `message_stop` event is received. + */ + readonly createMessageStream: (options: { + readonly payload: Omit + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + }) => Effect.Effect< + [response: HttpClientResponse.HttpClientResponse, stream: Stream.Stream], + AiError.AiError + > +} + +/** + * Represents an event received from the Anthropic Messages API during a streaming request. + * + * **Details** + * + * Events include: + * - `message_start`: Initial event containing message metadata + * - `message_delta`: Incremental updates to the message (e.g., stop reason) + * - `message_stop`: Final event indicating the message is complete + * - `content_block_start`: Start of a content block + * - `content_block_delta`: Incremental content updates (text, tool use, etc.) + * - `content_block_stop`: End of a content block + * - `error`: Error events with type and message + * + * @category models + * @since 4.0.0 + */ +export type MessageStreamEvent = + | typeof Generated.BetaMessageStartEvent.Type + | typeof Generated.BetaMessageDeltaEvent.Type + | typeof Generated.BetaMessageStopEvent.Type + | typeof Generated.BetaContentBlockStartEvent.Type + | typeof Generated.BetaContentBlockDeltaEvent.Type + | typeof Generated.BetaContentBlockStopEvent.Type + | typeof Generated.BetaErrorResponse.Type + +// ============================================================================= +// Service Identifier +// ============================================================================= + +/** + * Service tag for the Anthropic client. + * + * **When to use** + * + * Use when accessing or providing the Anthropic client service through Effect's + * context. + * + * @see {@link make} for constructing an Anthropic client effectfully + * @see {@link layer} for providing a client from explicit options + * @see {@link layerConfig} for providing a client from `Config` + * + * @category services + * @since 4.0.0 + */ +export class AnthropicClient extends Context.Service()( + "@effect/ai-anthropic/AnthropicClient" +) {} + +// ============================================================================= +// Options +// ============================================================================= + +/** + * Configuration for creating an Anthropic client. + * + * **When to use** + * + * Use when the Anthropic client settings are already available as values and + * should be passed directly to `make` or `layer`. + * + * **Details** + * + * These options configure the base Anthropic URL, the `x-api-key` + * authentication header, the `anthropic-version` header, and an optional + * transformation of the underlying `HttpClient`. + * + * @see {@link make} for constructing an Anthropic client from explicit options + * @see {@link layer} for providing an Anthropic client from explicit options + * @see {@link layerConfig} for loading Anthropic client settings from `Config` + * + * @category options + * @since 4.0.0 + */ +export type Options = { + /** + * The Anthropic API key for authentication. Requests are made without authentication when this is omitted, which is + * useful for proxied setups or testing. + */ + readonly apiKey?: Redacted.Redacted | undefined + + /** + * The base URL for the Anthropic API. Override this to use a proxy or a different API-compatible endpoint. + * + * @default "https://api.anthropic.com" + */ + readonly apiUrl?: string | undefined + + /** + * The Anthropic API version header value. This controls which version of the API to use. + * + * @default "2023-06-01" + */ + readonly apiVersion?: string | undefined + + /** + * Optional transformer for the underlying HTTP client, such as middleware, logging, or custom request/response + * handling. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +// ============================================================================= +// Constructor +// ============================================================================= + +const RedactedAnthropicHeaders = { + AnthropicApiKey: "x-api-key" +} + +/** + * Creates an Anthropic client service with the given options. + * + * **When to use** + * + * Use when you have explicit configuration values and need an `Effect` that + * constructs the Anthropic client service, rather than providing it as a `Layer`. + * + * **Details** + * + * The client handles API key authentication via the `x-api-key` header, API versioning via the `anthropic-version` + * header, error mapping to the unified `AiError` type, and request/response transformations via `AnthropicConfig`. It + * requires an `HttpClient` in the context. + * + * @see {@link layer} for providing the client as a `Layer` from explicit options + * @see {@link layerConfig} for providing the client as a `Layer` with `Config`-based settings + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*(options: Options): Effect.fn.Return { + const baseClient = yield* HttpClient.HttpClient + const apiVersion = options.apiVersion ?? "2023-06-01" + + const httpClient = baseClient.pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.anthropic.com"), + Predicate.isNotUndefined(options.apiKey) + ? HttpClientRequest.setHeader( + RedactedAnthropicHeaders.AnthropicApiKey, + Redacted.value(options.apiKey) + ) + : identity, + HttpClientRequest.setHeader("anthropic-version", apiVersion), + HttpClientRequest.acceptJson + ) + ), + Predicate.isNotUndefined(options.transformClient) + ? options.transformClient + : identity + ) + + const client = Generated.make(httpClient, { + transformClient: Effect.fnUntraced(function*(client) { + const config = yield* AnthropicConfig.getOrUndefined + if (Predicate.isNotUndefined(config?.transformClient)) { + return config.transformClient(client) + } + return client + }) + }) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const streamRequest = + (schema: S) => + (request: HttpClientRequest.HttpClientRequest): Stream.Stream< + S["Type"], + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + S["DecodingServices"] + > => + httpClientOk.execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText, + Stream.pipeThroughChannel(Sse.decodeSchema(schema)) + ) + + const createMessage = (options: { + readonly payload: typeof Generated.BetaCreateMessageParams.Encoded + readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined + }): Effect.Effect< + [body: typeof Generated.BetaMessage.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > => + client.betaMessagesPost({ ...options, config: { includeResponse: true } }).pipe( + Effect.catchTags({ + BetaMessagesPost4XX: (error) => Effect.fail(Errors.mapClientError(error, "createMessage")), + HttpClientError: (error) => Errors.mapHttpClientError(error, "createMessage"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createMessage")) + }) + ) + + const PingEvent = Schema.Struct({ + type: Schema.Literal("ping") + }) + + const MessageEvent = Schema.Union([ + PingEvent, + Generated.BetaMessageStartEvent, + Generated.BetaMessageDeltaEvent, + Generated.BetaMessageStopEvent, + Generated.BetaContentBlockStartEvent, + Generated.BetaContentBlockDeltaEvent, + Generated.BetaContentBlockStopEvent, + Generated.BetaErrorResponse + ]) + + const buildMessageStream = ( + response: HttpClientResponse.HttpClientResponse + ): [HttpClientResponse.HttpClientResponse, Stream.Stream] => { + const stream = response.stream.pipe( + Stream.decodeText, + Stream.pipeThroughChannel(Sse.decodeDataSchema(MessageEvent)), + Stream.takeUntil((event) => event.data.type === "message_stop"), + Stream.map((event) => event.data), + Stream.filter((event): event is MessageStreamEvent => event.type !== "ping"), + Stream.catchTags({ + // TODO: handle SSE retries + Retry: (error) => Stream.die(error), + HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createMessageStream")), + SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createMessageStream")) + }) + ) as any + return [response, stream] + } + + const createMessageStream: Service["createMessageStream"] = (options) => { + const request = HttpClientRequest.post("/v1/messages", { + headers: Headers.fromInput({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? apiVersion + }), + body: HttpBody.jsonUnsafe({ + ...options.payload, + stream: true + }) + }) + return httpClientOk.execute(request).pipe( + Effect.map(buildMessageStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createMessageStream") + ) + ) + } + + return AnthropicClient.of({ + client, + streamRequest, + createMessage, + createMessageStream + }) + }, + Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedAnthropicHeaders)) + ) +) + +// ============================================================================= +// Layers +// ============================================================================= + +/** + * Creates a layer for the Anthropic client with the given options. + * + * **When to use** + * + * Use when you already have explicit `Options` values, such as an API key or + * custom API URL, and want to provide `AnthropicClient` as a `Layer`. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(AnthropicClient, make(options)) + +/** + * Creates a layer for the Anthropic client, loading the requisite configuration + * via Effect's `Config` module. + * + * **When to use** + * + * Use when you want to provide the Anthropic client as a `Layer` with + * configuration loaded from Effect's `Config` module, such as from environment + * variables or a secrets provider. + * + * @see {@link layer} for providing the client from explicit options instead of `Config` + * @see {@link make} for constructing the client service effectfully + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = (options?: { + /** + * The Anthropic API key for authentication. Requests are made without authentication when this is omitted, which is + * useful for proxied setups or testing. + */ + readonly apiKey?: Config.Config | undefined> | undefined + + /** + * The base URL for the Anthropic API. Override this to use a proxy or a different API-compatible endpoint. + * + * @default "https://api.anthropic.com" + */ + readonly apiUrl?: Config.Config | undefined + + /** + * The Anthropic API version header value. This controls which version of the API to use. + * + * @default "2023-06-01" + */ + readonly apiVersion?: Config.Config | undefined + + /** + * Optional transformer for the underlying HTTP client, such as middleware, logging, or custom request/response + * handling. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + AnthropicClient, + Effect.gen(function*() { + const apiKey = Predicate.isNotUndefined(options?.apiKey) + ? yield* options.apiKey : + undefined + const apiUrl = Predicate.isNotUndefined(options?.apiUrl) + ? yield* options.apiUrl : + undefined + const apiVersion = Predicate.isNotUndefined(options?.apiVersion) + ? yield* options.apiVersion : + undefined + return yield* make({ + apiKey, + apiUrl, + apiVersion, + transformClient: options?.transformClient + }) + }) + ) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts b/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts new file mode 100644 index 0000000..26e3b21 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts @@ -0,0 +1,84 @@ +/** + * The `AnthropicConfig` module lets a workflow temporarily customize the HTTP + * client used by generated Anthropic requests. It is used by + * `AnthropicClient` when request helpers run, so code can add middleware, + * logging, or other client changes without rebuilding the client layer. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * Service tag for Anthropic client configuration overrides, such as transformations applied to the generated HTTP client. + * + * **When to use** + * + * Use when you need to provide or read Anthropic client configuration through + * Effect's context from a layer or integration. + * + * @see {@link withClientTransform} for scoping an HTTP client transformation + * + * @category services + * @since 4.0.0 + */ +export class AnthropicConfig extends Context.Service< + AnthropicConfig, + AnthropicConfig.Service +>()("@effect/ai-anthropic/AnthropicConfig") { + /** + * Gets the configured Anthropic service from the current context when present. + * + * @since 4.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (services) => services.mapUnsafe.get(AnthropicConfig.key) + ) +} + +/** + * Namespace containing types associated with the `AnthropicConfig` service. + * + * @since 4.0.0 + */ +export declare namespace AnthropicConfig { + /** + * Configuration provided through `AnthropicConfig`. + * + * **Details** + * + * Use `transformClient` to wrap or replace the `HttpClient` used by generated Anthropic API requests. + * + * @category models + * @since 4.0.0 + */ + export interface Service { + readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined + } +} + +/** + * Runs an effect with an `AnthropicConfig` override that transforms the underlying `HttpClient` used by generated Anthropic requests. + * + * **When to use** + * + * Use when you need to apply a temporary `HttpClient` transformation, such as adding middleware or logging, to a + * specific scope of an effectful program. + * + * @category configuration + * @since 4.0.0 + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + transformClient: (client: HttpClient) => HttpClient +) => + Effect.flatMap( + AnthropicConfig.getOrUndefined, + (config) => Effect.provideService(self, AnthropicConfig, { ...config, transformClient }) + )) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicError.ts b/repos/effect/packages/ai/anthropic/src/AnthropicError.ts new file mode 100644 index 0000000..d7a5219 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicError.ts @@ -0,0 +1,212 @@ +/** + * Anthropic error metadata augmentation. + * + * Provides Anthropic-specific metadata fields for AI error types through module + * augmentation, enabling typed access to Anthropic error details. + * + * @since 4.0.0 + */ + +/** + * Anthropic-specific error metadata fields. + * + * **Details** + * + * Contains the Anthropic error type and request identifier copied from provider + * error responses when available. Either field may be `null` when Anthropic + * does not include it or the response cannot be decoded. + * + * @see {@link AnthropicRateLimitMetadata} for rate-limit responses that also include parsed Anthropic rate-limit headers + * + * @category models + * @since 4.0.0 + */ +export type AnthropicErrorMetadata = { + /** + * The Anthropic error type returned by the API. + */ + readonly errorType: string | null + /** + * The unique request ID for debugging with Anthropic support. + */ + readonly requestId: string | null +} + +/** + * Anthropic-specific rate limit metadata fields. + * + * **Details** + * + * Extends base error metadata with rate limit-specific information from Anthropic's rate limit headers. + * + * @category models + * @since 4.0.0 + */ +export type AnthropicRateLimitMetadata = AnthropicErrorMetadata & { + /** + * Number of requests allowed in the current period. + */ + readonly requestsLimit: number | null + /** + * Number of requests remaining in the current period. + */ + readonly requestsRemaining: number | null + /** + * Time when the request rate limit resets. + */ + readonly requestsReset: string | null + /** + * Number of tokens allowed in the current period. + */ + readonly tokensLimit: number | null + /** + * Number of tokens remaining in the current period. + */ + readonly tokensRemaining: number | null + /** + * Time when the token rate limit resets. + */ + readonly tokensReset: string | null +} + +declare module "effect/unstable/ai/AiError" { + /** + * Anthropic metadata attached to `RateLimitError` values. + * + * **Details** + * + * Includes request identifiers, Anthropic error types, and parsed request or token limit headers when the provider rejects a request due to rate limits. + * + * @category configuration + * @since 4.0.0 + */ + export interface RateLimitErrorMetadata { + readonly anthropic?: AnthropicRateLimitMetadata | null + } + + /** + * Anthropic metadata attached to `QuotaExhaustedError` values. + * + * **Details** + * + * Captures the Anthropic error type and request identifier for failures where the account or workspace has exhausted its available quota. + * + * @category configuration + * @since 4.0.0 + */ + export interface QuotaExhaustedErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `AuthenticationError` values. + * + * **Details** + * + * Preserves Anthropic error details for missing, invalid, or unauthorized API credentials while keeping the error in the shared AI error model. + * + * @category configuration + * @since 4.0.0 + */ + export interface AuthenticationErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `ContentPolicyError` values. + * + * **Details** + * + * Records Anthropic error details returned when a request or response is rejected by Anthropic safety or content policy enforcement. + * + * @category configuration + * @since 4.0.0 + */ + export interface ContentPolicyErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `InvalidRequestError` values. + * + * **Details** + * + * Provides the Anthropic error type and request identifier for malformed or unsupported requests rejected before model execution. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidRequestErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `InternalProviderError` values. + * + * **Details** + * + * Preserves Anthropic request correlation data for provider-side failures that should be reported or investigated with Anthropic support. + * + * @category configuration + * @since 4.0.0 + */ + export interface InternalProviderErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `InvalidOutputError` values. + * + * **Details** + * + * Describes Anthropic-specific context for responses that could not be decoded or interpreted as valid AI output. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidOutputErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `StructuredOutputError` values. + * + * **Details** + * + * Captures Anthropic error details for structured-output failures, including request correlation data useful when diagnosing schema-related responses. + * + * @category configuration + * @since 4.0.0 + */ + export interface StructuredOutputErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `UnsupportedSchemaError` values. + * + * **Details** + * + * Provides Anthropic error details for schemas that cannot be represented by or submitted to the Anthropic API. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnsupportedSchemaErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } + + /** + * Anthropic metadata attached to `UnknownError` values. + * + * **Details** + * + * Retains the Anthropic error type and request identifier when a provider response cannot be classified as a more specific AI error. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnknownErrorMetadata { + readonly anthropic?: AnthropicErrorMetadata | null + } +} diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts new file mode 100644 index 0000000..ab37b1c --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -0,0 +1,3105 @@ +/** + * The `AnthropicLanguageModel` module provides the Anthropic implementation of + * Effect AI's `LanguageModel` service. It translates Effect AI prompts, tools, + * files, reasoning content, and Anthropic-specific options into Messages API + * requests, then converts normal and streaming Anthropic responses back into + * Effect AI response content with provider metadata. + * + * @since 4.0.0 + */ +/** @effect-diagnostics preferSchemaOverJson:skip-file */ +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as SchemaAST from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Mutable, Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput" +import * as IdGenerator from "effect/unstable/ai/IdGenerator" +import * as LanguageModel from "effect/unstable/ai/LanguageModel" +import * as AiModel from "effect/unstable/ai/Model" +import type * as Prompt from "effect/unstable/ai/Prompt" +import type * as Response from "effect/unstable/ai/Response" +import * as Tool from "effect/unstable/ai/Tool" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import { AnthropicClient, type MessageStreamEvent } from "./AnthropicClient.ts" +import { addGenAIAnnotations } from "./AnthropicTelemetry.ts" +import type { AnthropicTool } from "./AnthropicTool.ts" +import type * as Generated from "./Generated.ts" +import * as InternalUtilities from "./internal/utilities.ts" + +/** + * Known Anthropic Claude model identifiers exposed by the generated Anthropic schema. + * + * @category models + * @since 4.0.0 + */ +export type Model = (typeof Generated.Model)["members"][1]["Encoded"] + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Context service for Anthropic language model configuration. + * + * **When to use** + * + * Use when you need scoped Anthropic model request defaults or per-operation + * overrides from Effect context. + * + * **Details** + * + * The service stores request fields that are merged into Anthropic Messages API + * requests. Scoped configuration overrides defaults supplied to `model`, + * `make`, or `layer`. + * + * @category configuration + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + Simplify< + & Partial< + Omit< + typeof Generated.BetaCreateMessageParams.Encoded, + "messages" | "output_config" | "tools" | "tool_choice" | "stream" + > + > + & { + readonly output_config?: { + readonly effort?: "low" | "medium" | "high" | null + } + /** + * Disables Claude's ability to use multiple tools to respond to a query. + */ + readonly disableParallelToolCalls?: boolean | undefined + /** + * Whether to use strict JSON schema validation for tool calls. + * + * **Details** + * + * Only applies to models that support structured outputs. Defaults to + * `true` when structured outputs are supported. + */ + readonly strictJsonSchema?: boolean | undefined + } + > +>()("@effect/ai-anthropic/AnthropicLanguageModel/Config") {} + +// ============================================================================= +// Provider Options / Metadata +// ============================================================================= + +declare module "effect/unstable/ai/Prompt" { + /** + * Anthropic-specific options for system messages. + * + * **Details** + * + * These options are used when translating system messages into Anthropic + * request content. + * + * @category request + * @since 4.0.0 + */ + export interface SystemMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for user messages. + * + * **Details** + * + * These options are used when translating user messages into Anthropic + * request content. + * + * @category request + * @since 4.0.0 + */ + export interface UserMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for assistant messages. + * + * **Details** + * + * These options are used when replaying assistant messages in Anthropic + * conversation history. + * + * @category request + * @since 4.0.0 + */ + export interface AssistantMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for tool messages. + * + * **Details** + * + * These options are used when converting tool results into Anthropic user + * content blocks. + * + * @category request + * @since 4.0.0 + */ + export interface ToolMessageOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for text prompt parts. + * + * **When to use** + * + * Use when you use these options to control how text blocks are sent to Anthropic. + * + * @category request + * @since 4.0.0 + */ + export interface TextPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for reasoning prompt parts. + * + * **Details** + * + * Preserves Claude thinking metadata when reasoning content is sent back to + * Anthropic in later turns. + * + * @category request + * @since 4.0.0 + */ + export interface ReasoningPartOptions extends ProviderOptions { + readonly anthropic?: { + readonly info?: { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Anthropic's API. + */ + readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded + } | { + readonly type: "redacted_thinking" + /** + * Thinking content which was flagged by Anthropic's safety systems, and + * was therefore encrypted. + */ + readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded + } | null + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for file prompt parts. + * + * **Details** + * + * Controls document metadata, citations, and prompt caching for files sent to + * Anthropic. + * + * @category request + * @since 4.0.0 + */ + export interface FilePartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + /** + * Whether or not citations should be enabled for the file part. + */ + readonly citations?: typeof Generated.RequestCitationsConfig.Encoded | null + /** + * A custom title to provide to the document. If omitted, the file part's + * `fileName` property will be used. + */ + readonly documentTitle?: string | null + /** + * Additional context about the document that will be forwarded to the + * large language model, but will not be used towards cited content. + * + * **When to use** + * + * Use when storing additional document metadata as text or stringified JSON. + */ + readonly documentContext?: string | null + } | null + } + + /** + * Anthropic-specific options for tool call prompt parts. + * + * **Details** + * + * Carries Anthropic tool caller metadata, MCP metadata, and cache control for + * tool use blocks. + * + * @category request + * @since 4.0.0 + */ + export interface ToolCallPartOptions extends ProviderOptions { + readonly anthropic?: { + readonly caller?: { + readonly type: string + readonly toolId?: string | null + } | null + /** + * Contains details about the MCP tool that was called. + */ + readonly mcp_tool?: { + /** + * The name of the MCP server + */ + readonly server: string + } | null + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for tool result prompt parts. + * + * **Details** + * + * Controls Anthropic prompt caching for tool result content. + * + * @category request + * @since 4.0.0 + */ + export interface ToolResultPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for tool approval request prompt parts. + * + * **Details** + * + * Controls prompt caching for human approval requests in conversations. + * + * @category request + * @since 4.0.0 + */ + export interface ToolApprovalRequestPartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } + + /** + * Anthropic-specific options for tool approval response prompt parts. + * + * **Details** + * + * Controls prompt caching for human approval responses in conversations. + * + * @category request + * @since 4.0.0 + */ + export interface ToolApprovalResponsePartOptions extends ProviderOptions { + readonly anthropic?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null + } | null + } +} + +declare module "effect/unstable/ai/Response" { + /** + * Anthropic metadata attached when a reasoning block begins. + * + * **Details** + * + * Includes Claude thinking metadata needed to continue reasoning-aware + * conversations. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningStartPartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly info?: { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Anthropic's API. + */ + readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded + } | { + readonly type: "redacted_thinking" + /** + * Thinking content which was flagged by Anthropic's safety systems, and + * was therefore encrypted. + */ + readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded + } | null + } | null + } + + /** + * Anthropic metadata attached to streaming reasoning deltas. + * + * **Details** + * + * Includes the signature for streamed Claude thinking content when available. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly info?: { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Anthropic's API. + */ + readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded + } | null + } | null + } + + /** + * Anthropic metadata attached to completed reasoning parts. + * + * **Details** + * + * Preserves Claude thinking or redacted thinking information for later turns. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly info?: { + readonly type: "thinking" + /** + * Thinking content as an encrypted string, which is used to verify + * that thinking content was indeed generated by Anthropic's API. + */ + readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded + } | { + readonly type: "redacted_thinking" + /** + * Thinking content which was flagged by Anthropic's safety systems, and + * was therefore encrypted. + */ + readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded + } | null + } | null + } + + /** + * Anthropic metadata attached to tool call response parts. + * + * **Details** + * + * Identifies Anthropic caller details and MCP tool metadata emitted by the + * provider. + * + * @category response + * @since 4.0.0 + */ + export interface ToolCallPartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly caller?: { + readonly type: string + readonly toolId?: string | null + } | null + /** + * Contains details about the MCP tool that was called. + */ + readonly mcp_tool?: { + /** + * The name of the MCP server + */ + readonly server: string + } | null + } | null + } + + /** + * Anthropic metadata attached to tool result response parts. + * + * **Details** + * + * Identifies MCP tool metadata associated with provider-executed tool + * results. + * + * @category response + * @since 4.0.0 + */ + export interface ToolResultPartMetadata extends ProviderMetadata { + readonly anthropic?: { + /** + * Contains details about the MCP tool that was called. + */ + readonly mcp_tool?: { + /** + * The name of the MCP server + */ + readonly server: string + } | null + } | null + } + + /** + * Anthropic metadata for document citations in model responses. + * + * **Details** + * + * Records the cited document span by character position or page number. + * + * @category response + * @since 4.0.0 + */ + export interface DocumentSourcePartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly source: "document" + readonly type: "char_location" + /** + * The text that was cited in the response. + */ + readonly citedText: string + /** + * The 0-indexed starting position of the characters that were cited. + */ + readonly startCharIndex: number + /** + * The exclusive ending position of the characters that were cited. + */ + readonly endCharIndex: number + } | { + readonly source: "document" + readonly type: "page_location" + /** + * The text that was cited in the response. + */ + readonly citedText: string + /** + * The 1-indexed starting page of pages that were cited. + */ + readonly startPageNumber: number + /** + * The exclusive ending position of the pages that were cited. + */ + readonly endPageNumber: number + } | null + } + + /** + * Anthropic metadata for URL and web citations in model responses. + * + * **Details** + * + * Records cited URL text or web-search source freshness information. + * + * @category response + * @since 4.0.0 + */ + export interface UrlSourcePartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly source: "url" + /** + * Up to 150 characters of the text content that was referenced from the + * URL source material. + */ + readonly citedText: string + /** + * An internal reference that must be passed back to the Anthropic API + * during multi-turn conversations. + */ + readonly encryptedIndex: string + } | { + readonly source: "web" + readonly pageAge: string | null + } | null + } + + /** + * Anthropic metadata attached to the finish part of a response. + * + * **Details** + * + * Includes container state, context management information, stop details, and + * token usage reported by Anthropic. + * + * @category response + * @since 4.0.0 + */ + export interface FinishPartMetadata extends ProviderMetadata { + readonly anthropic?: { + readonly container: typeof Generated.BetaContainer.Encoded | null + readonly contextManagement: typeof Generated.BetaResponseContextManagement.Encoded | null + readonly stopSequence: string | null + readonly usage: typeof Generated.BetaMessage.Encoded["usage"] | null + } | null + } + + /** + * Anthropic metadata attached to error response parts. + * + * **Details** + * + * Includes the provider request identifier when Anthropic returns one. + * + * @category response + * @since 4.0.0 + */ + export interface ErrorPartMetadata extends ProviderMetadata { + readonly anthropic?: { + requestId?: string | null + } | null + } +} + +// ============================================================================= +// Language Model +// ============================================================================= + +/** + * Creates an Anthropic model descriptor that can be provided with `Effect.provide`. + * + * **When to use** + * + * Use when you want an Anthropic Claude model value that carries provider and + * model metadata and can be supplied directly to an Effect program. + * + * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly + * @see {@link make} for constructing the language model service effectfully + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"anthropic", LanguageModel.LanguageModel, AnthropicClient> => + AiModel.make("anthropic", model, layer({ model, config })) + +/** + * Creates an Anthropic `LanguageModel` service from a model identifier and optional request defaults. + * + * **When to use** + * + * Use when you need to construct a `LanguageModel.Service` value backed by + * `AnthropicClient` inside an Effect. + * + * **Details** + * + * The returned effect requires `AnthropicClient`. Request defaults from the + * `config` option are merged with any `Config` service in the context, with + * context values taking precedence. + * + * @see {@link layer} for providing the service as a `Layer` + * @see {@link model} for creating a model descriptor for `AiModel.provide` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Effect.fn.Return { + const client = yield* AnthropicClient + + const makeConfig: Effect.Effect = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + const makeRequest = Effect.fnUntraced( + function*>({ config, options, toolNameMapper }: { + readonly config: typeof Config.Service & { readonly model: string } + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return<{ + readonly params: typeof Generated.BetaMessagesPostParams.Encoded + readonly payload: typeof Generated.BetaCreateMessageParams.Encoded + }, AiError.AiError> { + const betas = new Set() + const capabilities = getModelCapabilities(config.model!) + const { messages, system } = yield* prepareMessages({ betas, options, toolNameMapper }) + const outputFormat = yield* getOutputFormat({ capabilities, options }) + const { tools, toolChoice } = yield* prepareTools({ betas, capabilities, config, options }) + const params: Mutable = {} + if (betas.size > 0) { + params["anthropic-beta"] = Array.from(betas).join(",") + } + const { disableParallelToolCalls: _, output_config, ...requestConfig } = config + const payload: Mutable = { + ...requestConfig, + max_tokens: requestConfig.max_tokens ?? capabilities.maxOutputTokens, + messages, + ...(Predicate.isNotUndefined(system) ? { system } : undefined), + ...(Predicate.isNotUndefined(tools) ? { tools } : undefined), + ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined) + } + const outputConfig: Mutable = {} + if (Predicate.isNotUndefined(outputFormat)) { + outputConfig.format = outputFormat + } + if (Predicate.isNotUndefined(output_config?.effort)) { + outputConfig.effort = output_config.effort + } + if (Object.keys(outputConfig).length > 0) { + payload.output_config = outputConfig + } + return { params, payload } + } + ) + + return yield* LanguageModel.make({ + codecTransformer: toCodecAnthropic, + generateText: Effect.fnUntraced(function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request.payload) + const [rawResponse, response] = yield* client.createMessage(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse({ options, rawResponse, response, toolNameMapper }) + }), + streamText: Effect.fnUntraced(function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request.payload) + const [response, stream] = yield* client.createMessageStream(request) + return yield* makeStreamResponse({ stream, response, options, toolNameMapper }) + }, (effect, options) => + effect.pipe( + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + )) + }) +}) + +/** + * Creates a layer for the Anthropic language model. + * + * **When to use** + * + * Use when composing application layers and you want Anthropic to satisfy + * `LanguageModel.LanguageModel` while supplying `AnthropicClient` from another + * layer. + * + * @see {@link make} for constructing the language model service effectfully + * @see {@link model} for creating a model service directly + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make(options)) + +/** + * Provides config overrides for Anthropic language model operations. + * + * **When to use** + * + * Use to apply Anthropic request configuration to one effect without changing + * the model's default configuration. + * + * **Details** + * + * The overrides are merged with any existing `Config` service for the duration + * of the supplied effect. Fields in `overrides` take precedence over existing + * config, and the helper supports both `effect.pipe(withConfigOverride(overrides))` + * and `withConfigOverride(effect, overrides)`. + * + * @see {@link Config} for available Anthropic request configuration fields + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages = Effect.fnUntraced( + function*>({ betas, options, toolNameMapper }: { + readonly betas: Set + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return<{ + readonly system: ReadonlyArray | undefined + readonly messages: ReadonlyArray + }, AiError.AiError> { + const groups = groupMessages(options.prompt) + + let system: Array | undefined = undefined + const messages: Array = [] + + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + const isLastGroup = i === groups.length - 1 + + switch (group.type) { + case "system": { + system = group.messages.map((message) => ({ + type: "text", + text: message.content, + cache_control: getCacheControl(message) + })) + break + } + + case "user": { + const content: Array = [] + + for (const message of group.messages) { + switch (message.role) { + case "user": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + const isLastPart = j === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : null + ) + + switch (part.type) { + case "text": { + content.push({ + type: "text", + text: part.text, + cache_control: cacheControl + }) + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const mediaType: typeof Generated.Base64ImageSource.Type["media_type"] = + (part.mediaType === "image/*" ? "image/jpeg" : part.mediaType) as any + + const source = isUrlData(part.data) + ? { type: "url", url: getUrlString(part.data) } as const + : { type: "base64", media_type: mediaType, data: Encoding.encodeBase64(part.data) } as const + + content.push({ type: "image", source, cache_control: cacheControl }) + } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") { + betas.add("pdfs-2024-09-25") + + const enableCitations = areCitationsEnabled(part) + const documentOptions = getDocumentMetadata(part) + + const source = isUrlData(part.data) + ? { + type: "url", + url: getUrlString(part.data) + } as const + : part.mediaType === "application/pdf" + ? { + type: "base64", + media_type: "application/pdf", + data: typeof part.data === "string" ? part.data : Encoding.encodeBase64(part.data) + } as const + : { + type: "text", + media_type: "text/plain", + data: typeof part.data === "string" ? part.data : Encoding.encodeBase64(part.data) + } as const + + content.push({ + type: "document", + source, + title: documentOptions?.title ?? part.fileName ?? null, + cache_control: cacheControl, + ...(documentOptions?.context ? { context: documentOptions.context } : undefined), + ...(enableCitations ? { citations: { enabled: true } } : undefined) + }) + } else { + return yield* new AiError.AiError({ + module: "AnthropicLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidUserInputError({ + description: `Detected unsupported media type for file: '${part.mediaType}'` + }) + }) + } + + break + } + } + } + break + } + + case "tool": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + + // Skip evaluation of tool approval parts + if (part.type === "tool-approval-response") { + continue + } + + const isLastPart = j === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : null + ) + + content.push({ + type: "tool_result", + tool_use_id: part.id, + content: JSON.stringify(part.result), + is_error: part.isFailure, + cache_control: cacheControl + }) + } + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const content: Array = [] + const mcpToolIds = new Set() + + for (let j = 0; j < group.messages.length; j++) { + const message = group.messages[j] + const isLastMessage = j === group.messages.length - 1 + + for (let k = 0; k < message.content.length; k++) { + const part = message.content[k] + + if (part.type === "file" || part.type === "tool-approval-request") { + continue + } + + const isLastPart = k === message.content.length - 1 + + // Attempt to get the cache control from the part first. If + // the part does not have cache control defined and we are + // evaluating the last part for this message, also check the + // message for cache control. + const cacheControl = getCacheControl(part) ?? ( + isLastPart ? getCacheControl(message) : undefined + ) + + // TODO: use cache_control in content blocks + void cacheControl + + switch (part.type) { + case "text": { + content.push({ + type: "text", + // Anthropic does not allow trailing whitespace in assistant + // content blocks + text: isLastGroup && isLastMessage && isLastPart + ? part.text.trim() + : part.text + }) + break + } + case "reasoning": { + // TODO: make sending reasoning configurable + const info = part.options.anthropic?.info + if (Predicate.isNotNullish(info)) { + if (info.type === "thinking") { + content.push({ + type: "thinking", + thinking: part.text, + signature: info.signature + }) + } else { + content.push({ + type: "redacted_thinking", + data: info.redactedData + }) + } + } + break + } + + case "tool-call": { + if (part.providerExecuted) { + const toolName = toolNameMapper.getProviderName(part.name) + + const isMcpTool = Predicate.isNotNullish(part.options.anthropic?.mcp_tool) + + if (isMcpTool) { + const { server } = part.options.anthropic.mcp_tool + + mcpToolIds.add(part.id) + + content.push({ + type: "mcp_tool_use", + id: part.id, + name: part.name, + input: part.params as any, + server_name: server + }) + } else if ( + toolName === "code_execution" && + Predicate.hasProperty(part.params, "type") && + ( + part.params.type === "bash_code_execution" || + part.params.type === "text_editor_code_execution" + ) + ) { + content.push({ + type: "server_tool_use", + id: part.id, + name: part.params.type, + input: part.params as any + }) + } else if ( + // code execution 20250825 programmatic tool calling: + // Strip the fake 'programmatic-tool-call' type before sending to Anthropic + toolName === "code_execution" && + Predicate.hasProperty(part.params, "type") && + part.params.type === "programmatic-tool-call" + ) { + const { type, ...params } = part.params + content.push({ + type: "server_tool_use", + id: part.id, + name: toolName, + input: params as any + }) + } else { + if ( + // code execution 20250522 + toolName === "code_execution" || + toolName === "tool_search_tool_regex" || + toolName === "tool_search_tool_bm25" || + toolName === "web_fetch" || + toolName === "web_search" + ) { + content.push({ + type: "server_tool_use", + id: part.id, + name: toolName, + input: part.params as any + }) + } + } + } else { + // Extract caller info from provider options for programmatic tool calling + const options = part.options.anthropic + const caller = Predicate.isNotNullish(options?.caller) + ? ( + options.caller.type === "code_execution_20250825" && + Predicate.isNotNullish(options.caller.toolId) + ) + ? { + type: "code_execution_20250825", + tool_id: options.caller.toolId + } as const + : options.caller.type === "direct" + ? { + type: "direct" + } as const + : undefined + : undefined + + content.push({ + type: "tool_use", + id: part.id, + name: part.name, + input: part.params as any, + ...(Predicate.isNotUndefined(caller) ? { caller } : undefined) + }) + } + + break + } + + case "tool-result": { + const toolName = toolNameMapper.getProviderName(part.name) + + if (mcpToolIds.has(part.id)) { + content.push({ + type: "mcp_tool_result", + tool_use_id: part.id, + is_error: part.isFailure, + content: part.result as any + }) + break + } + + if (toolName === "code_execution" && Predicate.hasProperty(part.result, "type")) { + if (part.result.type === "code_execution_result") { + content.push({ + type: "code_execution_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + } else if ( + part.result.type === "bash_code_execution_result" || + part.result.type === "bash_code_execution_tool_result_error" + ) { + content.push({ + type: "bash_code_execution_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + } else if ( + part.result.type === "text_editor_code_execution_tool_result" || + part.result.type === "text_editor_code_execution_tool_result_error" + ) { + content.push({ + type: "text_editor_code_execution_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + } + break + } + + if (toolName === "web_fetch") { + content.push({ + type: "web_fetch_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + break + } + + if (toolName === "web_search") { + content.push({ + type: "web_search_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + break + } + + if ( + toolName === "tool_search_tool_regex" || + toolName === "tool_search_tool_bm25" + ) { + content.push({ + type: "tool_search_tool_result", + tool_use_id: part.id, + content: part.result as any + }) + break + } + + break + } + } + } + } + + messages.push({ role: "assistant", content }) + + break + } + } + } + + return { + system, + messages + } + } +) + +// ============================================================================= +// Tool Conversion +// ============================================================================= + +/** + * Encoded Anthropic custom tool definition that can be sent in a Messages API request. + * + * **When to use** + * + * Use when you need to type or inspect the provider-specific request payload for + * a custom Anthropic tool. + * + * **Details** + * + * This type aliases the encoded `Generated.BetaTool` schema used for Effect + * user-defined and dynamic tools after conversion. It contains the tool `name`, + * optional `description`, and `input_schema`, plus Anthropic-specific fields + * such as `strict` and `cache_control`. + * + * @see {@link AnthropicProviderDefinedTool} for the request shape used by Anthropic built-in provider tools + * + * @category tools + * @since 4.0.0 + */ +export type AnthropicUserDefinedTool = typeof Generated.BetaTool.Encoded + +/** + * Represents a provider-defined tool that can be passed to the Anthropic API. + * + * **Details** + * + * These include Anthropic's built-in tools like computer use, code execution, + * web search, and text editing. + * + * @category tools + * @since 4.0.0 + */ +export type AnthropicProviderDefinedTool = + | typeof Generated.BetaBashTool_20241022.Encoded + | typeof Generated.BetaBashTool_20250124.Encoded + | typeof Generated.BetaCodeExecutionTool_20250522.Encoded + | typeof Generated.BetaCodeExecutionTool_20250825.Encoded + | typeof Generated.BetaComputerUseTool_20241022.Encoded + | typeof Generated.BetaComputerUseTool_20250124.Encoded + | typeof Generated.BetaComputerUseTool_20251124.Encoded + | typeof Generated.BetaMemoryTool_20250818.Encoded + | typeof Generated.BetaTextEditor_20241022.Encoded + | typeof Generated.BetaTextEditor_20250124.Encoded + | typeof Generated.BetaTextEditor_20250429.Encoded + | typeof Generated.BetaTextEditor_20250728.Encoded + | typeof Generated.BetaToolSearchToolBM25_20251119.Encoded + | typeof Generated.BetaToolSearchToolRegex_20251119.Encoded + | typeof Generated.BetaWebFetchTool_20250910.Encoded + | typeof Generated.BetaWebSearchTool_20250305.Encoded + +const prepareTools = Effect.fnUntraced( + function*({ betas, capabilities, config, options }: { + readonly betas: Set + readonly capabilities: ModelCapabilities + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + }): Effect.fn.Return<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined + }, AiError.AiError> { + if (options.tools.length === 0 || options.toolChoice === "none") { + return { tools: undefined, toolChoice: undefined } + } + + // Return a JSON response tool when using non-native structured outputs + if (options.responseFormat.type === "json" && !capabilities.supportsStructuredOutput) { + const input_schema = yield* tryJsonSchema(options.responseFormat.schema, "prepareTools") + const userDescription = SchemaAST.resolveDescription(options.responseFormat.schema.ast) + const description = Predicate.isNotUndefined(userDescription) ? `${userDescription} - ` : "" + return { + tools: [{ + name: options.responseFormat.objectName, + description: `${description}You MUST respond with a JSON object.`, + input_schema: input_schema as any + }], + toolChoice: { + type: "tool", + name: options.responseFormat.objectName, + disable_parallel_tool_use: true + } + } + } + + const userTools: Array = [] + const providerTools: Array = [] + + for (const tool of options.tools) { + if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) { + const description = Tool.getDescription(tool) + const input_schema = yield* tryToolJsonSchema(tool, "prepareTools") + const toolStrict = Tool.getStrictMode(tool) + const strict = capabilities.supportsStructuredOutput + ? (toolStrict ?? config.strictJsonSchema ?? true) + : undefined + + userTools.push({ + name: tool.name, + input_schema: input_schema as any, + ...(Predicate.isNotUndefined(description) ? { description } : undefined), + ...(Predicate.isNotUndefined(strict) ? { strict } : undefined) + }) + + if (capabilities.supportsStructuredOutput === true) { + betas.add("structured-outputs-2025-11-13") + } + } + + if (Tool.isProviderDefined(tool)) { + const providerTool = tool as AnthropicTool + switch (providerTool.id) { + case "anthropic.bash_20241022": { + betas.add("computer-use-2024-10-22") + providerTools.push({ name: "bash", type: "bash_20241022" }) + break + } + + case "anthropic.bash_20250124": { + betas.add("computer-use-2025-01-24") + providerTools.push({ name: "bash", type: "bash_20250124" }) + break + } + + case "anthropic.code_execution_20250522": { + betas.add("code-execution-2025-05-22") + providerTools.push({ name: "code_execution", type: "code_execution_20250522" }) + break + } + + case "anthropic.code_execution_20250825": { + betas.add("code-execution-2025-08-25") + providerTools.push({ name: "code_execution", type: "code_execution_20250825" }) + break + } + + case "anthropic.computer_use_20241022": { + betas.add("computer-use-2024-10-22") + providerTools.push({ + name: "computer", + type: "computer_20241022", + display_height_px: providerTool.args.displayHeightPx, + display_width_px: providerTool.args.displayWidthPx, + display_number: providerTool.args.displayNumber ?? null + }) + break + } + + case "anthropic.computer_20250124": { + betas.add("computer-use-2025-01-24") + providerTools.push({ + name: "computer", + type: "computer_20250124", + display_height_px: providerTool.args.displayHeightPx, + display_width_px: providerTool.args.displayWidthPx, + display_number: providerTool.args.displayNumber ?? null + }) + break + } + + case "anthropic.computer_20251124": { + betas.add("computer-use-2025-11-24") + providerTools.push({ + name: "computer", + type: "computer_20251124", + display_height_px: providerTool.args.displayHeightPx, + display_width_px: providerTool.args.displayWidthPx, + display_number: providerTool.args.displayNumber ?? null, + enable_zoom: providerTool.args.enableZoom ?? false + }) + break + } + + case "anthropic.memory_20250818": { + betas.add("context-management-2025-06-27") + providerTools.push({ name: "memory", type: "memory_20250818" }) + break + } + + case "anthropic.text_editor_20241022": { + betas.add("computer-use-2024-10-22") + providerTools.push({ name: "str_replace_editor", type: "text_editor_20241022" }) + break + } + + case "anthropic.text_editor_20250124": { + betas.add("computer-use-2025-01-24") + providerTools.push({ name: "str_replace_editor", type: "text_editor_20250124" }) + break + } + + case "anthropic.text_editor_20250429": { + betas.add("computer-use-2025-01-24") + providerTools.push({ name: "str_replace_based_edit_tool", type: "text_editor_20250429" }) + break + } + + case "anthropic.text_editor_20250728": { + providerTools.push({ + name: "str_replace_based_edit_tool", + type: "text_editor_20250728", + max_characters: providerTool.args.max_characters ?? null + }) + break + } + + case "anthropic.tool_search_tool_bm25_20251119": { + betas.add("advanced-tool-use-2025-11-20") + providerTools.push({ name: "tool_search_tool_bm25", type: "tool_search_tool_bm25_20251119" }) + break + } + + case "anthropic.tool_search_tool_regex_20251119": { + providerTools.push({ name: "tool_search_tool_regex", type: "tool_search_tool_regex_20251119" }) + break + } + + case "anthropic.web_search_20250305": { + providerTools.push({ + name: "web_search", + type: "web_search_20250305", + max_uses: providerTool.args.maxUses ?? null, + allowed_domains: providerTool.args.allowedDomains ?? null, + blocked_domains: providerTool.args.blockedDomains ?? null, + user_location: Predicate.isNotUndefined(providerTool.args.userLocation) + ? { + type: providerTool.args.userLocation.type, + region: providerTool.args.userLocation.region ?? null, + city: providerTool.args.userLocation.city ?? null, + country: providerTool.args.userLocation.country ?? null, + timezone: providerTool.args.userLocation.timezone ?? null + } + : null + }) + break + } + + case "anthropic.web_fetch_20250910": { + betas.add("web-fetch-2025-09-10") + providerTools.push({ + name: "web_fetch", + type: "web_fetch_20250910", + max_uses: providerTool.args.maxUses ?? null, + allowed_domains: providerTool.args.allowedDomains ?? null, + blocked_domains: providerTool.args.blockedDomains ?? null, + citations: providerTool.args.citations ?? null, + max_content_tokens: providerTool.args.maxContentTokens ?? null + }) + break + } + + default: { + return yield* AiError.make({ + module: "AnthropicLanguageModel", + method: "prepareTools", + reason: new AiError.InvalidUserInputError({ + description: `Received request to call unknown provider-defined tool '${tool.name}'` + }) + }) + } + } + } + } + + let tools = [...userTools, ...providerTools] + let toolChoice: Mutable | undefined = undefined + + if (options.toolChoice === "auto") { + toolChoice = { type: "auto" } + } else if (options.toolChoice === "required") { + toolChoice = { type: "any" } + } else if ("tool" in options.toolChoice) { + toolChoice = { type: "tool", name: options.toolChoice.tool } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.name)) + toolChoice = { type: options.toolChoice.mode === "required" ? "any" : "auto" } + } + + if ( + Predicate.isNotUndefined(config.disableParallelToolCalls) && + Predicate.isNotUndefined(toolChoice) && + toolChoice.type !== "none" + ) { + toolChoice.disable_parallel_tool_use = config.disableParallelToolCalls + } + + return { + tools, + toolChoice + } + } +) + +// ============================================================================= +// HTTP Details +// ============================================================================= + +const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +const buildHttpResponseDetails = ( + response: HttpClientResponse.HttpClientResponse +): typeof Response.HttpResponseDetails.Type => ({ + status: response.status, + headers: Redactable.redact(response.headers) as Record +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse = Effect.fnUntraced( + function*>({ + options, + rawResponse, + response, + toolNameMapper + }: { + readonly options: LanguageModel.ProviderOptions + readonly rawResponse: Generated.BetaMessage + readonly response: HttpClientResponse.HttpClientResponse + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Array, + AiError.AiError, + IdGenerator.IdGenerator + > { + const parts: Array = [] + const mcpToolCalls: Map = new Map() + const serverToolCalls: Map = new Map() + const citableDocuments = extractCitableDocuments(options.prompt) + + parts.push({ + type: "response-metadata", + id: rawResponse.id, + modelId: rawResponse.model, + timestamp: DateTime.formatIso(yield* DateTime.now), + request: buildHttpRequestDetails(response.request) + }) + + for (const part of rawResponse.content) { + switch (part.type) { + case "text": { + // Text parts are added for both text and json response formats. + // For native structured output (json_schema), the JSON comes directly + // in a text content block. For tool-based structured output, text may + // also be present alongside the tool_use. + parts.push({ + type: "text", + text: part.text + }) + + if (Predicate.isNotNullish(part.citations)) { + for (const citation of part.citations) { + const source = yield* processCitation(citation, citableDocuments) + if (Predicate.isNotUndefined(source)) { + parts.push(source) + } + } + } + + break + } + + case "thinking": { + const metadata = { + info: { type: "thinking", signature: part.signature } + } as const + + parts.push({ + type: "reasoning", + text: part.thinking, + metadata: { anthropic: metadata } + }) + break + } + + case "redacted_thinking": { + const metadata = { + info: { type: "redacted_thinking", redactedData: part.data } + } as const + + parts.push({ + type: "reasoning", + text: "", + metadata: { anthropic: metadata } + }) + break + } + + case "tool_use": { + // When the `"json"` response format is requested, the JSON we need + // is returned by a tool call injected into the request + if (options.responseFormat.type === "json") { + parts.push({ + type: "text", + text: JSON.stringify(part.input) + }) + } else { + // Extract caller info if present + const caller = (part as any).caller + const callerInfo = Predicate.isNotNullish(caller) + ? { + type: caller.type, + toolId: "tool_id" in caller ? caller.tool_id : null + } + : undefined + + // Map the provider wire name (e.g. "memory") back to the tool's + // custom name (e.g. "AnthropicMemory") that the toolkit is keyed by + const toolName = toolNameMapper.getCustomName(part.name) + const params = yield* transformToolCallParams(options.tools, toolName, part.input) + + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params, + ...(Predicate.isNotUndefined(callerInfo) + ? { metadata: { anthropic: { caller: callerInfo } } } + : undefined) + }) + } + + break + } + + case "server_tool_use": { + const toolName = toolNameMapper.getCustomName(part.name) + + if ( + part.name === "bash_code_execution" || + part.name === "text_editor_code_execution" + ) { + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: { type: part.name, ...part.input }, + providerExecuted: true + }) + } else if ( + part.name === "code_execution" || + part.name === "web_fetch" || + part.name === "web_search" + ) { + const toolParams: Record = { ...part.input } + + // Inject `type: "programmatic-tool-call"` when the input parameters + // has the format `{ code: ... }` + if ( + part.name === "code_execution" && + Predicate.hasProperty(part.input, "code") && + !Predicate.hasProperty(part.input, "type") + ) { + toolParams.type = "programmatic-tool-call" + } + + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: toolParams, + providerExecuted: true + }) + } else if ( + part.name === "tool_search_tool_bm25" || + part.name === "tool_search_tool_regex" + ) { + serverToolCalls.set(part.id, part.name) + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: part.input, + providerExecuted: true + }) + } + + break + } + + case "mcp_tool_use": { + const toolCall: Response.ToolCallPartEncoded = { + type: "tool-call", + id: part.id, + name: part.name, + params: part.input, + providerExecuted: true, + metadata: { anthropic: { mcp_tool: { server: part.server_name } } } + } + + mcpToolCalls.set(part.id, toolCall) + + parts.push(toolCall) + + break + } + + case "mcp_tool_result": { + const toolCall = mcpToolCalls.get(part.tool_use_id) + const mcpMetadata = toolCall?.metadata?.anthropic?.mcp_tool + + if (Predicate.isNotUndefined(toolCall)) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolCall.name, + isFailure: part.is_error, + result: part.content, + metadata: { + anthropic: { + ...(Predicate.isNotNullish(mcpMetadata) + ? { mcp_tool: mcpMetadata } : + undefined) + } + } + }) + } + + break + } + + // Code Execution 20250522 + case "code_execution_tool_result": { + const toolName = toolNameMapper.getCustomName("code_execution") + + if (part.content.type === "code_execution_result") { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + + break + } + + // Code Execution 20250825 + case "bash_code_execution_tool_result": + case "text_editor_code_execution_tool_result": { + const toolName = toolNameMapper.getCustomName("code_execution") + + if ( + part.content.type === "bash_code_execution_tool_result_error" || + part.content.type === "text_editor_code_execution_tool_result_error" + ) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } + + break + } + + case "tool_search_tool_result": { + let providerName = serverToolCalls.get(part.tool_use_id) + + if (Predicate.isUndefined(providerName)) { + const bm25Name = toolNameMapper.getCustomName("tool_search_tool_bm25") + const regexName = toolNameMapper.getCustomName("tool_search_tool_regex") + + if (bm25Name !== "tool_search_tool_bm25") { + providerName = "tool_search_tool_bm25" + } else if (regexName !== "tool_search_tool_regex") { + providerName = "tool_search_tool_regex" + } + } + + const toolName = toolNameMapper.getCustomName(providerName!) + + if (part.content.type === "tool_search_tool_search_result") { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content.tool_references + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + + break + } + + case "web_fetch_tool_result": { + const toolName = toolNameMapper.getCustomName("web_fetch") + + if (part.content.type === "web_fetch_result") { + citableDocuments.push({ + title: part.content.content.title ?? part.content.url, + mediaType: part.content.content.source.media_type + }) + + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + + break + } + + case "web_search_tool_result": { + const toolName = toolNameMapper.getCustomName("web_search") + + if (Predicate.hasProperty(part.content, "type")) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } else { + const idGenerator = yield* IdGenerator.IdGenerator + + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + + const content = part.content as ReadonlyArray + + for (const result of content) { + const id = yield* idGenerator.generateId() + + parts.push({ + type: "source", + sourceType: "url", + id, + url: result.url, + title: result.title, + metadata: { anthropic: { source: "web", pageAge: result.page_age } } + }) + } + } + + break + } + } + } + + // Anthropic always returns a non-null `stop_reason` for non-streaming responses + const finishReason = InternalUtilities.resolveFinishReason( + rawResponse.stop_reason!, + options.responseFormat.type === "json" + ) + + const inputTokens = rawResponse.usage.input_tokens + const outputTokens = rawResponse.usage.output_tokens + const cacheWriteTokens = rawResponse.usage.cache_creation_input_tokens ?? 0 + const cacheReadTokens = rawResponse.usage.cache_read_input_tokens ?? 0 + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: { + uncached: inputTokens, + total: inputTokens + cacheWriteTokens + cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: cacheWriteTokens + }, + outputTokens: { + total: outputTokens, + text: undefined, + reasoning: undefined + } + }, + response: buildHttpResponseDetails(response), + metadata: { + anthropic: { + container: rawResponse.container ?? null, + contextManagement: rawResponse.context_management ?? null, + usage: rawResponse.usage, + stopSequence: rawResponse.stop_sequence + } + } + }) + + return parts + } +) + +const makeStreamResponse = Effect.fnUntraced( + function*>({ + stream, + response, + options, + toolNameMapper + }: { + readonly stream: Stream.Stream + readonly response: HttpClientResponse.HttpClientResponse + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Stream.Stream, + AiError.AiError + > { + const citableDocuments = extractCitableDocuments(options.prompt) + + let container: typeof Generated.BetaContainer.Encoded | null = null + let contextManagement: typeof Generated.BetaResponseContextManagement.Encoded | null = null + let finishReason: Response.FinishReason = "unknown" + let stopSequence: string | null = null + let rawUsage: typeof Generated.BetaMessage.Encoded["usage"] | null = null + const mcpToolCalls: Map = new Map() + const serverToolCalls: Map = new Map() + const contentBlocks: Map< + number, + | { + readonly type: "text" + } + | { + readonly type: "reasoning" + } + | { + readonly type: "tool-call" + readonly id: string + readonly name: string + params: string + firstDelta: boolean + readonly providerName?: string | undefined + readonly providerExecuted?: boolean | undefined + readonly caller?: { type: string; toolId: string | null } | undefined + } + > = new Map() + const usage: Mutable<{ + inputTokens: number + outputTokens: number + cacheReadInputTokens: number + cacheWriteInputTokens: number + }> = { + inputTokens: 0, + outputTokens: 0, + cacheReadInputTokens: 0, + cacheWriteInputTokens: 0 + } + + let blockType: typeof Generated.BetaContentBlockStartEvent.Encoded["content_block"]["type"] | undefined = undefined + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + switch (event.type) { + case "message_start": { + rawUsage = { ...event.message.usage } + usage.inputTokens = event.message.usage.input_tokens + usage.cacheReadInputTokens = event.message.usage.cache_read_input_tokens ?? 0 + usage.cacheWriteInputTokens = event.message.usage.cache_creation_input_tokens ?? 0 + + if (Predicate.isNotNullish(event.message.container)) { + container = event.message.container + } + + if (Predicate.isNotNull(event.message.stop_sequence)) { + stopSequence = event.message.stop_sequence + } + + if (Predicate.isNotNull(event.message.stop_reason)) { + finishReason = InternalUtilities.resolveFinishReason(event.message.stop_reason) + } + + parts.push({ + type: "response-metadata", + id: event.message.id, + modelId: event.message.model, + timestamp: DateTime.formatIso(yield* DateTime.now), + request: buildHttpRequestDetails(response.request) + }) + + // Process pre-populated content blocks + if (Predicate.isNotNullish(event.message.content) && event.message.content.length > 0) { + for (let i = 0; i < event.message.content.length; i++) { + const part = event.message.content[i] + + if (part.type === "tool_use") { + const callerInfo = Predicate.isNotUndefined(part.caller) + ? { + type: part.caller.type, + toolId: "tool_id" in part.caller ? part.caller.tool_id : null + } + : undefined + + // Map the provider wire name (e.g. "memory") back to the + // tool's custom name (e.g. "AnthropicMemory") that the toolkit + // is keyed by + const toolName = toolNameMapper.getCustomName(part.name) + + parts.push({ + type: "tool-params-start", + id: part.id, + name: toolName + }) + + parts.push({ + type: "tool-params-delta", + id: part.id, + delta: JSON.stringify(part.input ?? {}) + }) + + parts.push({ + type: "tool-params-end", + id: part.id + }) + + const params = yield* transformToolCallParams(options.tools, toolName, part.input) + + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params, + ...(Predicate.isNotUndefined(callerInfo) + ? { metadata: { anthropic: { caller: callerInfo } } } + : undefined) + }) + } + } + } + + break + } + + case "message_delta": { + rawUsage = { ...rawUsage, ...event.usage } as any + + if ( + Predicate.isNotNull(event.usage.input_tokens) && + usage.inputTokens !== event.usage.input_tokens + ) { + usage.inputTokens = event.usage.input_tokens + } + usage.outputTokens = event.usage.output_tokens + + if ( + Predicate.isNotNull(event.usage.cache_read_input_tokens) && + usage.cacheReadInputTokens !== event.usage.cache_read_input_tokens + ) { + usage.cacheReadInputTokens = event.usage.cache_read_input_tokens + } + if ( + Predicate.isNotNull(event.usage.cache_creation_input_tokens) && + usage.cacheWriteInputTokens !== event.usage.cache_creation_input_tokens + ) { + usage.cacheWriteInputTokens = event.usage.cache_creation_input_tokens + } + + if (Predicate.isNotNullish(event.delta.container)) { + container = event.delta.container + } + + if (Predicate.isNotNullish(event.context_management)) { + contextManagement = event.context_management + } + + if (Predicate.isNotNull(event.delta.stop_reason)) { + finishReason = InternalUtilities.resolveFinishReason(event.delta.stop_reason) + } + + if (Predicate.isNotNull(event.delta.stop_sequence)) { + stopSequence = event.delta.stop_sequence + } + + break + } + + case "message_stop": { + const metadata: Response.FinishPartMetadata = { + anthropic: { + container, + contextManagement, + stopSequence, + usage: rawUsage + } + } + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: { + uncached: usage.inputTokens, + total: usage.inputTokens + usage.cacheWriteInputTokens + usage.cacheReadInputTokens, + cacheRead: usage.cacheReadInputTokens, + cacheWrite: usage.cacheWriteInputTokens + }, + outputTokens: { + total: usage.outputTokens, + text: undefined, + reasoning: undefined + } + }, + response: buildHttpResponseDetails(response), + metadata + }) + + break + } + + case "content_block_start": { + blockType = event.content_block.type + + switch (event.content_block.type) { + case "text": { + contentBlocks.set(event.index, { type: "text" }) + + parts.push({ + type: "text-start", + id: event.index.toString() + }) + + break + } + + case "thinking": { + contentBlocks.set(event.index, { type: "reasoning" }) + + parts.push({ + type: "reasoning-start", + id: event.index.toString() + }) + + break + } + + case "redacted_thinking": { + contentBlocks.set(event.index, { type: "reasoning" }) + + const metadata: Response.ReasoningStartPartMetadata = { + anthropic: { + info: { + type: "redacted_thinking", + redactedData: event.content_block.data + } + } + } + + parts.push({ + type: "reasoning-start", + id: event.index.toString(), + metadata + }) + + break + } + + case "tool_use": { + const part = event.content_block + + const caller = Predicate.isNotUndefined(part.caller) + ? { + type: part.caller.type, + toolId: "tool_id" in part.caller ? part.caller.tool_id : null + } + : undefined + + const hasParams = Object.keys(part.input).length > 0 + const initialParams = hasParams ? JSON.stringify(part.input) : "" + // Map the provider wire name (e.g. "memory") back to the tool's + // custom name (e.g. "AnthropicMemory") that the toolkit is keyed + // by. The mapped name flows to the finalized tool-call via + // `contentBlock.name` on `content_block_stop`. + const toolName = toolNameMapper.getCustomName(part.name) + contentBlocks.set(event.index, { + type: "tool-call", + id: part.id, + name: toolName, + params: initialParams, + firstDelta: initialParams.length > 0, + ...(Predicate.isNotUndefined(caller) ? { caller } : undefined) + }) + + parts.push({ + type: "tool-params-start", + id: part.id, + name: toolName + }) + + break + } + + case "server_tool_use": { + const part = event.content_block + + if ( + part.name === "code_execution" || + part.name === "bash_code_execution" || + part.name === "text_editor_code_execution" || + part.name === "web_fetch" || + part.name === "web_search" + ) { + const toolName = toolNameMapper.getCustomName( + part.name === "bash_code_execution" || part.name === "text_editor_code_execution" + ? "code_execution" + : part.name + ) + + contentBlocks.set(event.index, { + type: "tool-call", + id: part.id, + name: toolName, + params: "", + firstDelta: true, + providerName: part.name, + providerExecuted: true + }) + + parts.push({ + type: "tool-params-start", + id: part.id, + name: toolName, + providerExecuted: true + }) + } else if ( + part.name === "tool_search_tool_bm25" || + part.name === "tool_search_tool_regex" + ) { + serverToolCalls.set(part.id, part.name) + + const toolName = toolNameMapper.getCustomName(part.name) + + contentBlocks.set(event.index, { + type: "tool-call", + id: part.id, + name: toolName, + params: "", + firstDelta: true, + providerName: part.name, + providerExecuted: true + }) + + parts.push({ + type: "tool-params-start", + id: part.id, + name: toolName, + providerExecuted: true + }) + } + + break + } + + case "web_fetch_tool_result": { + const part = event.content_block + const toolName = toolNameMapper.getCustomName("web_fetch") + + if (part.content.type === "web_fetch_result") { + citableDocuments.push({ + title: part.content.content.title ?? part.content.url, + mediaType: part.content.content.source.media_type + }) + + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + + break + } + + case "web_search_tool_result": { + const part = event.content_block + const toolName = toolNameMapper.getCustomName("web_search") + + if (Predicate.hasProperty(part.content, "type")) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } else { + const idGenerator = yield* IdGenerator.IdGenerator + + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + + const content = part.content as ReadonlyArray + + for (const result of content) { + const id = yield* idGenerator.generateId() + + parts.push({ + type: "source", + sourceType: "url", + id, + url: result.url, + title: result.title, + metadata: { anthropic: { source: "web", pageAge: result.page_age } } + }) + } + } + break + } + + case "code_execution_tool_result": { + const part = event.content_block + const toolName = toolNameMapper.getCustomName("code_execution") + + if (part.content.type === "code_execution_result") { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + break + } + + case "bash_code_execution_tool_result": + case "text_editor_code_execution_tool_result": { + const part = event.content_block + const toolName = toolNameMapper.getCustomName("code_execution") + + if ( + part.content.type === "bash_code_execution_tool_result_error" || + part.content.type === "text_editor_code_execution_tool_result_error" + ) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content + }) + } + break + } + + case "tool_search_tool_result": { + const part = event.content_block + let providerName = serverToolCalls.get(part.tool_use_id) + + if (Predicate.isUndefined(providerName)) { + const bm25Name = toolNameMapper.getCustomName("tool_search_tool_bm25") + const regexName = toolNameMapper.getCustomName("tool_search_tool_regex") + + if (bm25Name !== "tool_search_tool_bm25") { + providerName = "tool_search_tool_bm25" + } else if (regexName !== "tool_search_tool_regex") { + providerName = "tool_search_tool_regex" + } + } + + const toolName = toolNameMapper.getCustomName(providerName!) + + if (part.content.type === "tool_search_tool_search_result") { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: false, + result: part.content.tool_references + }) + } else { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolName, + isFailure: true, + result: part.content + }) + } + break + } + + case "mcp_tool_use": { + const part = event.content_block + + const toolCall: Response.ToolCallPartEncoded = { + type: "tool-call", + id: part.id, + name: part.name, + params: part.input, + providerExecuted: true, + metadata: { anthropic: { mcp_tool: { server: part.server_name } } } + } + + mcpToolCalls.set(part.id, toolCall) + + parts.push(toolCall) + + break + } + + case "mcp_tool_result": { + const part = event.content_block + const toolCall = mcpToolCalls.get(part.tool_use_id) + const mcpMetadata = toolCall?.metadata?.anthropic?.mcp_tool + + if (Predicate.isNotUndefined(toolCall)) { + parts.push({ + type: "tool-result", + id: part.tool_use_id, + name: toolCall.name, + isFailure: part.is_error, + result: part.content, + metadata: { + anthropic: { + ...(Predicate.isNotNullish(mcpMetadata) + ? { mcp_tool: mcpMetadata } : + undefined) + } + } + }) + } + + break + } + } + + break + } + + case "content_block_delta": { + const part = event.delta + + switch (part.type) { + case "text_delta": { + parts.push({ + type: "text-delta", + id: event.index.toString(), + delta: part.text + }) + + break + } + + case "thinking_delta": { + parts.push({ + type: "reasoning-delta", + id: event.index.toString(), + delta: part.thinking + }) + + break + } + + case "signature_delta": { + if (blockType === "thinking") { + parts.push({ + type: "reasoning-delta", + id: event.index.toString(), + delta: "", + metadata: { + anthropic: { + info: { + type: "thinking", + signature: part.signature + } + } + } + }) + } + + break + } + + case "input_json_delta": { + let delta = part.partial_json + + // Skip empty deltas + if (delta.length === 0) { + break + } + + const contentBlock = contentBlocks.get(event.index) + + // Skip invalid deltas + if (Predicate.isUndefined(contentBlock)) { + break + } + + // Skip non-tool-call deltas + if (contentBlock.type !== "tool-call") { + break + } + + if ( + contentBlock.firstDelta && + (contentBlock.providerName === "bash_code_execution" || + contentBlock.providerName === "text_editor_code_execution") + ) { + delta = `{"type":${contentBlock.providerName},${delta.substring(1)}}` + } + + parts.push({ + type: "tool-params-delta", + id: contentBlock.id, + delta + }) + + contentBlock.params += delta + contentBlock.firstDelta = false + + break + } + + case "citations_delta": { + const source = yield* processCitation(part.citation, citableDocuments) + + if (Predicate.isNotUndefined(source)) { + parts.push(source) + } + + break + } + } + + break + } + + case "content_block_stop": { + const contentBlock = contentBlocks.get(event.index) + + if (Predicate.isNotUndefined(contentBlock)) { + switch (contentBlock.type) { + case "text": { + parts.push({ + type: "text-end", + id: event.index.toString() + }) + + break + } + + case "reasoning": { + parts.push({ + type: "reasoning-end", + id: event.index.toString() + }) + + break + } + + case "tool-call": { + parts.push({ + type: "tool-params-end", + id: contentBlock.id + }) + + // For code execution, inject the `programmatic-tool-call` type + // when the input format is `{ code }` + let finalParams = contentBlock.params.length === 0 ? "{}" : contentBlock.params + + if (contentBlock.providerName === "code_execution") { + // @effect-diagnostics-next-line tryCatchInEffectGen:off + try { + const params = Tool.unsafeSecureJsonParse(finalParams) + if (Predicate.hasProperty(params, "code") && !Predicate.hasProperty(params, "type")) { + finalParams = JSON.stringify({ type: "programmatic-tool-call", ...params }) + } + } catch { + // Ignore errors and use original tool call parameters + } + } + + const params = contentBlock.providerExecuted === true + ? finalParams + : yield* transformToolCallParams( + options.tools, + contentBlock.name, + Tool.unsafeSecureJsonParse(finalParams) + ) + + parts.push({ + type: "tool-call", + id: contentBlock.id, + name: contentBlock.name, + params, + ...(Predicate.isNotUndefined(contentBlock.providerExecuted) + ? { providerExecuted: contentBlock.providerExecuted } + : undefined), + ...(Predicate.isNotUndefined(contentBlock.caller) + ? { metadata: { anthropic: { caller: contentBlock.caller } } } + : undefined) + }) + } + } + + contentBlocks.delete(event.index) + } + + blockType = undefined + + break + } + + case "error": { + parts.push({ + type: "error", + error: event.error, + metadata: { anthropic: { requestId: event.request_id } } + }) + + break + } + } + + return parts + })), + Stream.flattenIterable + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof Generated.BetaCreateMessageParams.Encoded +): void => { + addGenAIAnnotations(span, { + system: "anthropic", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.temperature, + topK: request.top_k, + topP: request.top_p, + maxTokens: request.max_tokens, + stopSequences: Arr.ensure(request.stop_sequences).filter( + Predicate.isNotNullish + ) + } + }) +} + +const annotateResponse = (span: Span, response: Generated.BetaMessage): void => { + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model, + finishReasons: response.stop_reason ? [response.stop_reason] : undefined + }, + usage: { + inputTokens: response.usage.input_tokens, + outputTokens: response.usage.output_tokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens.uncached, + outputTokens: part.usage.outputTokens.total + } + }) + } +} + +// ============================================================================= +// Internal Utilities +// ============================================================================= + +type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup + +interface SystemMessageGroup { + readonly type: "system" + readonly messages: Array +} + +interface AssistantMessageGroup { + readonly type: "assistant" + readonly messages: Array +} + +interface UserMessageGroup { + readonly type: "user" + readonly messages: Array +} + +const groupMessages = (prompt: Prompt.Prompt): Array => { + const messages: Array = [] + let current: ContentGroup | undefined = undefined + for (const message of prompt.content) { + switch (message.role) { + case "system": { + if (current?.type !== "system") { + current = { type: "system", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "assistant": { + if (current?.type !== "assistant") { + current = { type: "assistant", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "tool": + case "user": { + if (current?.type !== "user") { + current = { type: "user", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + } + } + return messages +} + +/** + * Checks whether data is a URL (either a URL object or a URL string). + */ +const isUrlData = ( + data: typeof Prompt.FilePart.Type["data"] +): data is URL => data instanceof URL || isUrlString(data) + +const isUrlString = (data: typeof Prompt.FilePart.Type["data"]): boolean => + typeof data === "string" && /^https?:\/\//i.test(data) + +const getUrlString = (data: string | URL): string => data instanceof URL ? data.toString() : data + +const getCacheControl = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage + | Prompt.UserMessagePart + | Prompt.AssistantMessagePart + | Prompt.ToolMessagePart +): typeof Generated.CacheControlEphemeral.Encoded | null => part.options.anthropic?.cacheControl ?? null + +const getDocumentMetadata = (part: Prompt.FilePart): { + readonly title: string | null + readonly context: string | null +} | null => { + const options = part.options.anthropic + if (Predicate.isNotUndefined(options)) { + return { + title: options?.documentTitle ?? null, + context: options?.documentContext ?? null + } + } + return null +} + +const areCitationsEnabled = (part: Prompt.FilePart): boolean => part.options.anthropic?.citations?.enabled ?? false + +const isCitationPart = (part: Prompt.UserMessagePart): part is Prompt.FilePart => + part.type === "file" && (part.mediaType === "application/pdf" || part.mediaType === "text/plain") + ? areCitationsEnabled(part) + : false + +interface CitableDocument { + readonly title: string + readonly fileName?: string | undefined + readonly mediaType: string +} + +const extractCitableDocuments = (prompt: Prompt.Prompt): Array => { + const citableDocuments: Array = [] + + for (const message of prompt.content) { + if (message.role === "user") { + for (const part of message.content) { + if (isCitationPart(part)) { + citableDocuments.push({ + title: part.fileName ?? "Untitled Document", + fileName: part.fileName, + mediaType: part.mediaType + }) + } + } + } + } + + return citableDocuments +} + +const processCitation = Effect.fnUntraced( + function*( + citation: + | Generated.ResponseCharLocationCitation + | Generated.ResponsePageLocationCitation + | Generated.ResponseContentBlockLocationCitation + | Generated.ResponseWebSearchResultLocationCitation + | Generated.ResponseSearchResultLocationCitation, + citableDocuments: ReadonlyArray + ): Effect.fn.Return< + Response.DocumentSourcePartEncoded | Response.UrlSourcePartEncoded | undefined, + never, + IdGenerator.IdGenerator + > { + const idGenerator = yield* IdGenerator.IdGenerator + + if (citation.type === "page_location" || citation.type === "char_location") { + const citedDocument = citableDocuments[citation.document_index] + if (Predicate.isNotUndefined(citedDocument)) { + const id = yield* idGenerator.generateId() + const metadata = citation.type === "char_location" + ? { + source: "document", + type: citation.type, + citedText: citation.cited_text, + startCharIndex: citation.start_char_index, + endCharIndex: citation.end_char_index + } as const + : { + source: "document", + type: citation.type, + citedText: citation.cited_text, + startPageNumber: citation.start_page_number, + endPageNumber: citation.end_page_number + } as const + + return { + type: "source", + sourceType: "document", + id, + mediaType: citedDocument.mediaType, + title: citation.document_title ?? citedDocument.title, + ...(Predicate.isNotUndefined(citedDocument.fileName) + ? { fileName: citedDocument.fileName } + : undefined), + metadata: { anthropic: metadata } + } + } + } + + if (citation.type === "web_search_result_location") { + const id = yield* idGenerator.generateId() + + const metadata = { + source: "url", + citedText: citation.cited_text, + encryptedIndex: citation.encrypted_index + } as const + + return { + type: "source", + sourceType: "url", + id, + url: citation.url, + title: citation.title ?? "Untitled", + metadata: { anthropic: metadata } + } + } + + return undefined + } +) + +interface ModelCapabilities { + readonly maxOutputTokens: number + readonly supportsStructuredOutput: boolean + readonly isKnownModel: boolean +} + +/** + * Returns the capabilities of a Claude model that are used for defaults and feature selection. + * + * @see https://docs.claude.com/en/docs/about-claude/models/overview#model-comparison-table + * @see https://platform.claude.com/docs/en/build-with-claude/structured-outputs + */ +const getModelCapabilities = (modelId: string): ModelCapabilities => { + if ( + modelId.includes("claude-sonnet-4-5") || + modelId.includes("claude-opus-4-5") || + modelId.includes("claude-haiku-4-5") || + modelId.includes("claude-opus-4-6") || + modelId.includes("claude-sonnet-4-6") || + modelId.includes("claude-opus-4-7") || + modelId.includes("claude-opus-4-8") + ) { + return { + maxOutputTokens: 64000, + supportsStructuredOutput: true, + isKnownModel: true + } + } else if (modelId.includes("claude-opus-4-1")) { + return { + maxOutputTokens: 32000, + supportsStructuredOutput: true, + isKnownModel: true + } + } else if ( + modelId.includes("claude-sonnet-4-") || + modelId.includes("claude-3-7-sonnet") + ) { + return { + maxOutputTokens: 64000, + supportsStructuredOutput: false, + isKnownModel: true + } + } else if (modelId.includes("claude-opus-4-")) { + return { + maxOutputTokens: 32000, + supportsStructuredOutput: false, + isKnownModel: true + } + } else if (modelId.includes("claude-3-5-haiku")) { + return { + maxOutputTokens: 8192, + supportsStructuredOutput: false, + isKnownModel: true + } + } else if (modelId.includes("claude-3-haiku")) { + return { + maxOutputTokens: 4096, + supportsStructuredOutput: false, + isKnownModel: true + } + } else { + return { + maxOutputTokens: 4096, + supportsStructuredOutput: false, + isKnownModel: false + } + } +} + +const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError => + AiError.make({ + module: "AnthropicLanguageModel", + method, + reason: new AiError.UnsupportedSchemaError({ + description: error instanceof Error ? error.message : String(error) + }) + }) + +const tryCodecTransform = (schema: S, method: string) => + Effect.try({ + try: () => toCodecAnthropic(schema), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const tryJsonSchema = (schema: S, method: string) => + Effect.try({ + try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecAnthropic }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const tryToolJsonSchema = (tool: T, method: string) => + Effect.try({ + try: () => Tool.getJsonSchema(tool, { transformer: toCodecAnthropic }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const getOutputFormat = Effect.fnUntraced(function*({ capabilities, options }: { + readonly capabilities: ModelCapabilities + readonly options: LanguageModel.ProviderOptions +}): Effect.fn.Return { + if (options.responseFormat.type === "json" && capabilities.supportsStructuredOutput) { + const jsonSchema = yield* tryJsonSchema(options.responseFormat.schema, "getOutputFormat") + return { + type: "json_schema", + schema: jsonSchema as any + } + } + return undefined +}) + +const transformToolCallParams = Effect.fnUntraced(function*>( + tools: Tools, + toolName: string, + toolParams: unknown +): Effect.fn.Return { + const tool = tools.find((tool) => tool.name === toolName) + + if (Predicate.isUndefined(tool)) { + return yield* AiError.make({ + module: "AnthropicLanguageModel", + method: "makeResponse", + reason: new AiError.ToolNotFoundError({ + toolName, + availableTools: tools.map((tool) => tool.name) + }) + }) + } + + const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse") + + const transform = Schema.decodeEffect(codec) + + return yield* ( + transform(toolParams) as Effect.Effect + ).pipe(Effect.mapError((error) => + AiError.make({ + module: "AnthropicLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams, + description: error.issue.toString() + }) + }) + )) +}) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTelemetry.ts b/repos/effect/packages/ai/anthropic/src/AnthropicTelemetry.ts new file mode 100644 index 0000000..d100551 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicTelemetry.ts @@ -0,0 +1,132 @@ +/** + * The `AnthropicTelemetry` module defines Anthropic-specific telemetry + * attributes and a helper for adding them to a tracing span. It keeps the + * standard GenAI telemetry attributes and adds request and response metadata + * under the `gen_ai.anthropic.*` OpenTelemetry namespaces. + * + * @since 4.0.0 + */ +import { dual } from "effect/Function" +import * as String from "effect/String" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import * as Telemetry from "effect/unstable/ai/Telemetry" + +/** + * The attributes used to describe telemetry in the context of Generative + * Artificial Intelligence (GenAI) Models requests and responses. + * + * **Details** + * + * These attributes follow the OpenTelemetry generative AI semantic + * conventions: + * https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/ + * + * @category models + * @since 4.0.0 + */ +export type AnthropicTelemetryAttributes = Simplify< + & Telemetry.GenAITelemetryAttributes + & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix +> + +/** + * All telemetry attributes which are part of the GenAI specification, + * including the Anthropic-specific attributes. + * + * @category models + * @since 4.0.0 + */ +export type AllAttributes = Telemetry.AllAttributes & RequestAttributes & ResponseAttributes + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.anthropic.request`. + * + * @category models + * @since 4.0.0 + */ +export interface RequestAttributes { + /** + * Whether extended thinking is enabled. + */ + readonly extendedThinking?: boolean | null | undefined + /** + * The budget tokens for extended thinking. + */ + readonly thinkingBudgetTokens?: number | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.anthropic.response`. + * + * @category models + * @since 4.0.0 + */ +export interface ResponseAttributes { + /** + * The stop reason from the response. + */ + readonly stopReason?: string | null | undefined + /** + * Number of cache creation input tokens. + */ + readonly cacheCreationInputTokens?: number | null | undefined + /** + * Number of cache read input tokens. + */ + readonly cacheReadInputTokens?: number | null | undefined +} + +/** + * Options accepted by `addGenAIAnnotations`, combining standard GenAI telemetry attributes with optional Anthropic request and response attributes. + * + * @category options + * @since 4.0.0 + */ +export type AnthropicTelemetryAttributeOptions = Telemetry.GenAITelemetryAttributeOptions & { + anthropic?: { + request?: RequestAttributes | undefined + response?: ResponseAttributes | undefined + } | undefined +} + +const addAnthropicRequestAttributes = Telemetry.addSpanAttributes("gen_ai.anthropic.request", String.camelToSnake)< + RequestAttributes +> +const addAnthropicResponseAttributes = Telemetry.addSpanAttributes("gen_ai.anthropic.response", String.camelToSnake)< + ResponseAttributes +> + +/** + * Applies the specified Anthropic GenAI telemetry attributes to the provided + * `Span`. + * + * **When to use** + * + * Use to annotate an Anthropic model span with standard GenAI telemetry + * attributes and Anthropic-specific request or response metadata. + * + * **Gotchas** + * + * This method mutates the `Span` in place. + * + * @category annotations + * @since 4.0.0 + */ +export const addGenAIAnnotations: { + (options: AnthropicTelemetryAttributeOptions): (span: Span) => void + (span: Span, options: AnthropicTelemetryAttributeOptions): void +} = dual(2, (span: Span, options: AnthropicTelemetryAttributeOptions) => { + Telemetry.addGenAIAnnotations(span, options) + if (options.anthropic != null) { + if (options.anthropic.request != null) { + addAnthropicRequestAttributes(span, options.anthropic.request) + } + if (options.anthropic.response != null) { + addAnthropicResponseAttributes(span, options.anthropic.response) + } + } +}) diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts b/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts new file mode 100644 index 0000000..ac0b7c5 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts @@ -0,0 +1,2518 @@ +/** + * The `AnthropicTool` module defines Anthropic provider tools and the schemas + * for their inputs and results. It covers Anthropic-owned tools such as Bash, + * Code Execution, Computer Use, Memory, Text Editor, Web Search, Web Fetch, and + * Tool Search, which can be attached to Anthropic-backed Effect AI language + * model requests. + * + * @since 4.0.0 + */ +import * as Schema from "effect/Schema" +import * as Tool from "effect/unstable/ai/Tool" +import * as Generated from "./Generated.ts" + +/** + * Union of all Anthropic provider-defined tool definitions exported by this module. + * + * **When to use** + * + * Use when a helper, collection, or option accepts any Anthropic + * provider-defined tool value created by this module. + * + * **Details** + * + * The union is built from the return types of the exported constructors, + * including Bash, Code Execution, Computer Use, Memory, Text Editor, Tool + * Search, Web Fetch, and Web Search tool versions. + * + * @category models + * @since 4.0.0 + */ +export type AnthropicTool = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + +// ============================================================================= +// Bash +// ============================================================================= + +/** + * Defines the Anthropic Bash tool (2024-10-22 version). + * + * **When to use** + * + * Use when you want the model to execute bash commands with the 2024-10-22 + * Anthropic computer-use beta. + * + * **Details** + * + * Allows the model to execute bash commands in a sandboxed environment. + * Requires the "computer-use-2024-10-22" beta header. + * + * @see {@link Bash_20250124} for the newer 2025-01-24 version of the bash tool + * + * @category Bash + * @since 4.0.0 + */ +export const Bash_20241022 = Tool.providerDefined({ + id: "anthropic.bash_20241022", + customName: "AnthropicBash", + providerName: "bash", + requiresHandler: true, + success: Schema.String, + parameters: Schema.Struct({ + command: Schema.String, + restart: Schema.optionalKey(Schema.Boolean) + }) +}) + +/** + * Defines the Anthropic Bash tool (2025-01-24 version). + * + * **When to use** + * + * Use when you want the model to execute bash commands with the 2025-01-24 + * Anthropic computer-use beta. + * + * **Details** + * + * Allows the model to execute bash commands in a sandboxed environment. + * Requires the "computer-use-2025-01-24" beta header. + * + * @see {@link Bash_20241022} for the older 2024-10-22 version of the bash tool + * + * @category Bash + * @since 4.0.0 + */ +export const Bash_20250124 = Tool.providerDefined({ + id: "anthropic.bash_20250124", + customName: "AnthropicBash", + providerName: "bash", + requiresHandler: true, + success: Schema.String, + parameters: Schema.Struct({ + command: Schema.String, + restart: Schema.optionalKey(Schema.Boolean) + }) +}) + +// ============================================================================= +// Code Execution +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Code Execution 20250522 Parameters +// ----------------------------------------------------------------------------- + +/** + * Schema for a code execution request that asks Anthropic to run source code as a programmatic tool call. + * + * **When to use** + * + * Use when constructing or validating a programmatic tool call for the Anthropic + * Code Execution tool. + * + * @see {@link CodeExecution_20250522} for the parent tool definition + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecutionProgrammaticToolCall = Schema.Struct({ + type: Schema.Literal("programmatic-tool-call"), + /** + * The code to execute. + */ + code: Schema.String +}) +/** + * Input payload for a programmatic code execution tool call, including the source code to execute. + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecutionProgrammaticToolCall = typeof CodeExecutionProgrammaticToolCall.Type + +/** + * Schema for the `bash_code_execution` input variant of Anthropic Code Execution. + * + * **When to use** + * + * Use when validating or constructing a bash command request for + * `CodeExecution_20250522`. + * + * **Details** + * + * The schema requires `type` to be `"bash_code_execution"` and `command` to + * contain the bash command sent to Anthropic. + * + * @see {@link CodeExecution_20250522} for the provider-defined tool that consumes this input variant + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecutionBashCommand = Schema.Struct({ + type: Schema.Literal("bash_code_execution"), + /** + * The bash command to execute. + */ + command: Schema.String +}) +/** + * Input payload for a bash command routed through the Anthropic code execution tool. + * + * **When to use** + * + * Use when representing a provider-executed bash command request for the + * 2025-05-22 code execution tool. + * + * **Details** + * + * The payload uses `type: "bash_code_execution"` to distinguish bash execution + * from programmatic code and text editor operations, and `command` contains the + * bash command to run. + * + * @see {@link CodeExecutionProgrammaticToolCall} for programmatic code execution input + * @see {@link CodeExecutionTextEditorView} for viewing files through text editor code execution + * @see {@link CodeExecutionTextEditorCreate} for creating files through text editor code execution + * @see {@link CodeExecutionTextEditorStrReplace} for replacing text through text editor code execution + * @see {@link CodeExecution_20250522} for the provider-defined tool that consumes this payload + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecutionBashCommand = typeof CodeExecutionBashCommand.Type + +/** + * Schema for a code execution text editor request that views a file by path. + * + * **When to use** + * + * Use when you need the schema for provider-bound code-execution view requests + * before distinguishing them from create or replace text-editor commands. + * + * **Details** + * + * The encoded payload uses `type: "text_editor_code_execution"`, + * `command: "view"`, and a `path` string. + * + * @see {@link CodeExecutionTextEditorCreate} for the command that creates a file + * @see {@link CodeExecutionTextEditorStrReplace} for the command that replaces text in a file + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecutionTextEditorView = Schema.Struct({ + type: Schema.Literal("text_editor_code_execution"), + command: Schema.Literal("view"), + /** + * Path to the file to view. + */ + path: Schema.String +}) +/** + * Input payload for the `view` command of Anthropic's text editor code execution tool. + * + * **When to use** + * + * Use when working at the Anthropic protocol boundary and the code-execution + * view request must be distinguished from standalone text-editor view requests. + * + * **Details** + * + * The payload is discriminated by `type: "text_editor_code_execution"` and + * `command: "view"`. The `path` field identifies the file to view. + * + * **Gotchas** + * + * This code execution view payload does not include `view_range`; line ranges + * are part of the standalone text editor view payload, not this code execution + * payload. + * + * @see {@link CodeExecution_20250522} for the provider-defined code execution tool that includes this payload + * @see {@link TextEditorViewCommand} for the standalone text editor view payload + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecutionTextEditorView = typeof CodeExecutionTextEditorView.Type + +/** + * Schema for a text editor code execution request that creates a file at a path. + * + * **When to use** + * + * Use when validating or constructing an Anthropic `text_editor_code_execution` + * tool call that should create a file. + * + * **Details** + * + * The request is discriminated by `type: "text_editor_code_execution"` and + * `command: "create"`. It requires `path` and accepts optional `file_text`; the + * schema allows `file_text` to be omitted, `null`, or a string. + * + * @see {@link CodeExecution_20250522} for the provider-defined tool that consumes this request + * @see {@link CodeExecutionTextEditorView} for the matching view request + * @see {@link CodeExecutionTextEditorStrReplace} for the matching replace request + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecutionTextEditorCreate = Schema.Struct({ + type: Schema.Literal("text_editor_code_execution"), + command: Schema.Literal("create"), + /** + * Path where the file should be created. + */ + path: Schema.String, + /** + * The content to write to the new file. + */ + file_text: Schema.optional(Schema.NullOr(Schema.String)) +}) +/** + * Input payload for creating a file through the text editor code execution tool, optionally including initial file text. + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecutionTextEditorCreate = typeof CodeExecutionTextEditorCreate.Type + +/** + * Schema for a code execution text editor request that replaces one exact string in a file. + * + * **When to use** + * + * Use when validating or constructing the `str_replace` text editor operation + * for the 2025-05-22 Anthropic code execution tool. + * + * **Gotchas** + * + * The `old_str` must match the file contents exactly, including whitespace and + * indentation, and must identify a single occurrence. + * + * @see {@link CodeExecutionTextEditorView} for reading file contents before choosing the replacement text + * @see {@link CodeExecution_20250522} for the provider-defined tool that consumes this payload + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecutionTextEditorStrReplace = Schema.Struct({ + type: Schema.Literal("text_editor_code_execution"), + command: Schema.Literal("str_replace"), + /** + * Path to the file to modify. + */ + path: Schema.String, + /** + * The text to replace. + */ + old_str: Schema.String, + /** + * The replacement text. + */ + new_str: Schema.String +}) +/** + * Input payload for replacing text in a file through the text editor code execution tool. + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecutionTextEditorStrReplace = typeof CodeExecutionTextEditorStrReplace.Type + +const CodeExecution_20250522_Parameters = Schema.Union([ + CodeExecutionProgrammaticToolCall, + CodeExecutionBashCommand, + CodeExecutionTextEditorView, + CodeExecutionTextEditorCreate, + CodeExecutionTextEditorStrReplace +]) + +// ----------------------------------------------------------------------------- +// Code Execution 20250825 Parameters +// ----------------------------------------------------------------------------- + +/** + * Schema for the 2025-08-25 code execution tool input, containing the code to execute. + * + * **When to use** + * + * Use when you need the schema for code-execution input at the Anthropic + * protocol boundary before sending source code to the 2025-08-25 tool. + * + * @see {@link CodeExecution_20250825} for the provider-defined tool that consumes this schema + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecution_20250825_Parameters = Schema.Struct({ + /** + * The code to execute. + */ + code: Schema.String +}) +/** + * Input payload for the 2025-08-25 Anthropic code execution tool. + * + * **When to use** + * + * Use when exposing the 2025-08-25 code-execution payload separately from the + * provider tool definition, such as at a transport or persistence boundary. + * + * **Details** + * + * The payload has a single `code` field containing the source code string to + * execute. + * + * @see {@link CodeExecution_20250825} for the provider-defined tool that consumes this payload + * + * @category Code Execution + * @since 4.0.0 + */ +export type CodeExecution_20250825_Parameters = typeof CodeExecution_20250825_Parameters.Type + +// ----------------------------------------------------------------------------- +// Code Execution Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the Anthropic Code Execution tool (2025-05-22 version). + * + * **When to use** + * + * Use when you want the model to execute code in a sandboxed environment with + * the 2025-05-22 Anthropic code-execution beta. + * + * **Details** + * + * Allows the model to execute code in a sandboxed environment with support + * for multiple execution types including programmatic tool calls, bash + * execution, and text editor operations. + * + * @see {@link CodeExecutionProgrammaticToolCall} for the programmatic tool call schema + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecution_20250522 = Tool.providerDefined({ + id: "anthropic.code_execution_20250522", + customName: "AnthropicCodeExecution", + providerName: "code_execution", + parameters: CodeExecution_20250522_Parameters, + success: Generated.BetaResponseCodeExecutionResultBlock, + failure: Generated.BetaResponseCodeExecutionToolResultError +}) + +/** + * Defines the Anthropic Code Execution tool (2025-08-25 version). + * + * **When to use** + * + * Use when you want the model to execute code in a sandboxed environment with + * the 2025-08-25 Anthropic code-execution beta. + * + * **Details** + * + * Requires the `code-execution-2025-08-25` beta header and uses + * `CodeExecution_20250825_Parameters` as its input schema. + * + * @see {@link CodeExecution_20250522} for the older 2025-05-22 code execution tool + * @see {@link CodeExecution_20250825_Parameters} for the input schema consumed by this tool + * + * @category Code Execution + * @since 4.0.0 + */ +export const CodeExecution_20250825 = Tool.providerDefined({ + id: "anthropic.code_execution_20250825", + customName: "AnthropicCodeExecution", + providerName: "code_execution", + parameters: CodeExecution_20250825_Parameters, + success: Schema.Union([ + Generated.BetaResponseCodeExecutionResultBlock, + Generated.BetaResponseBashCodeExecutionResultBlock, + Generated.BetaResponseTextEditorCodeExecutionViewResultBlock, + Generated.BetaResponseTextEditorCodeExecutionCreateResultBlock, + Generated.BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + ]), + failure: Schema.Union([ + Generated.BetaResponseCodeExecutionToolResultError, + Generated.BetaResponseBashCodeExecutionToolResultError, + Generated.BetaResponseTextEditorCodeExecutionToolResultError + ]) +}) + +// ============================================================================= +// Computer Use +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Common Types +// ----------------------------------------------------------------------------- + +/** + * Schema for an `[x, y]` screen coordinate in pixels. + * + * **When to use** + * + * Use when validating computer-use action payloads that carry a single screen + * position and provider-side bounds checks remain acceptable. + * + * **Details** + * + * This is a two-number tuple used by computer-use actions that accept screen + * positions. + * + * **Gotchas** + * + * This schema validates tuple shape only and does not check display bounds. + * + * @category computer use + * @since 4.0.0 + */ +export const Coordinate = Schema.Tuple([Schema.Number, Schema.Number]) +/** + * An `[x, y]` screen coordinate in pixels. + * + * @category computer use + * @since 4.0.0 + */ +export type Coordinate = typeof Coordinate.Type + +/** + * Schema for an `[x1, y1, x2, y2]` screen region in pixels. + * + * **When to use** + * + * Use when validating computer-use action payloads that carry a rectangular + * screen region and provider-side bounds checks remain acceptable. + * + * **Details** + * + * The tuple represents top-left and bottom-right corners. + * + * **Gotchas** + * + * This schema validates four numbers only and does not check coordinate ordering + * or display bounds. + * + * @category computer use + * @since 4.0.0 + */ +export const Region = Schema.Tuple([Schema.Number, Schema.Number, Schema.Number, Schema.Number]) +/** + * An `[x1, y1, x2, y2]` screen region in pixels, from top-left to bottom-right. + * + * @category computer use + * @since 4.0.0 + */ +export type Region = typeof Region.Type + +/** + * Schema for scroll direction literals: `"up"`, `"down"`, `"left"`, or `"right"`. + * + * @see {@link ComputerUseScrollAction} for the action payload that consumes this schema + * + * @category computer use + * @since 4.0.0 + */ +export const ScrollDirection = Schema.Literals(["up", "down", "left", "right"]) +/** + * Direction used by computer-use scroll actions: `"up"`, `"down"`, `"left"`, or `"right"`. + * + * @category computer use + * @since 4.0.0 + */ +export type ScrollDirection = typeof ScrollDirection.Type + +/** + * Schema for modifier key literals. + * + * **Details** + * + * Allowed values are `"alt"`, `"ctrl"`, `"meta"`, and `"shift"`. + * + * @category computer use + * @since 4.0.0 + */ +export const ModifierKey = Schema.Literals(["alt", "ctrl", "meta", "shift"]) +/** + * Modifier key literals. + * + * **Details** + * + * Allowed values are `"alt"`, `"ctrl"`, `"meta"`, and `"shift"`. + * + * @category computer use + * @since 4.0.0 + */ +export type ModifierKey = typeof ModifierKey.Type + +// ----------------------------------------------------------------------------- +// ComputerUse_20241022_Args +// ----------------------------------------------------------------------------- + +const ComputerUse_20241022_Args = Schema.Struct({ + /** + * The width of the display being controlled by the model in pixels. + */ + displayWidthPx: Schema.Number, + + /** + * The height of the display being controlled by the model in pixels. + */ + displayHeightPx: Schema.Number, + + /** + * The display number to control (only relevant for X11 environments). If + * specified, the tool will be provided a display number in the tool + * definition. + */ + displayNumber: Schema.optional(Schema.Number) +}) + +const ComputerUse_20251124_Args = Schema.Struct({ + ...ComputerUse_20241022_Args.fields, + enableZoom: Schema.optional(Schema.Boolean) +}) + +// ----------------------------------------------------------------------------- +// Computer Use 20241022 Actions +// ----------------------------------------------------------------------------- + +/** + * Schema for a computer-use action that presses a key or key combination, such + * as `"Return"`, `"ctrl+c"`, or `"ctrl+s"`. + * + * **When to use** + * + * Use when validating or constructing a computer-use action for keyboard + * shortcuts or non-text key presses. + * + * @see {@link TypeAction} for entering ordinary text strings + * @see {@link ComputerUseHoldKeyAction} for holding a key for a duration + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseKeyAction = Schema.Struct({ + action: Schema.Literal("key"), + /** + * The key to press. + */ + text: Schema.String +}) +/** + * Computer-use action payload for pressing a key or key combination. + * + * **When to use** + * + * Use when typing parsed computer-use key action payloads after schema + * validation, where provider-specific key-name validation is handled outside + * TypeScript. + * + * **Details** + * + * The payload uses `action: "key"` and stores the key or key combination to + * press in `text`, such as `"Return"`, `"ctrl+c"`, or `"ctrl+s"`. + * + * **Gotchas** + * + * `text` is typed as `string`; the paired schema does not validate + * provider-specific key names or key combinations. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseKeyAction = typeof ComputerUseKeyAction.Type + +/** + * Schema for a computer-use action that performs a left click. + * + * **When to use** + * + * Use to validate or construct an Anthropic computer-use payload for clicking + * once at the current mouse position or at a specific screen coordinate. + * + * **Details** + * + * The encoded payload uses `action: "left_click"`. The optional `coordinate` + * field supplies the `[x, y]` pixel position; when omitted, the action uses the + * current mouse position. + * + * **Gotchas** + * + * The coordinate schema only checks that the value is a two-number tuple. It + * does not validate that the point falls within the configured display + * dimensions. + * + * @see {@link ComputerUseDoubleClickAction} for performing a double click + * @see {@link ComputerUseMouseMoveAction} for moving the mouse without clicking + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseLeftClickAction = Schema.Struct({ + action: Schema.Literal("left_click"), + /** + * The `[x, y]` coordinate on the screen to left click (defaults to the current + * mouse position if omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for performing a left click, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseLeftClickAction = typeof ComputerUseLeftClickAction.Type + +/** + * Schema for a computer-use action that moves the mouse cursor to a required + * `[x, y]` screen coordinate. + * + * **When to use** + * + * Use to validate or construct a mouse movement action for an Anthropic + * computer-use tool call. + * + * **Details** + * + * The encoded payload has action `"mouse_move"` and a required `coordinate` + * field containing the target `[x, y]` pixel position. + * + * **Gotchas** + * + * The coordinate schema only checks that the value is a two-number tuple. It + * does not validate that the point falls within the configured display + * dimensions. + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseMouseMoveAction = Schema.Struct({ + action: Schema.Literal("mouse_move"), + /** + * The `[x, y]` coordinate on the screen to move to. + */ + coordinate: Coordinate +}) +/** + * Computer-use action payload for moving the mouse cursor to a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseMouseMoveAction = typeof ComputerUseMouseMoveAction.Type + +/** + * Schema for a computer-use action that requests a screenshot of the current display. + * + * **When to use** + * + * Use to validate or construct a computer-use tool action that asks the handler + * to capture the full current display. + * + * **Details** + * + * The payload contains only `action: "screenshot"` and does not include + * coordinates or other options. + * + * @see {@link ComputerUseZoomAction} for requesting a zoomed-in screenshot of a specific screen region with the 2025-11-24 computer-use tool + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseScreenshotAction = Schema.Struct({ + action: Schema.Literal("screenshot") +}) +/** + * Computer-use action payload for capturing the current display. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseScreenshotAction = typeof ComputerUseScreenshotAction.Type + +/** + * Schema for a computer-use action that enters text. + * + * **When to use** + * + * Use to validate or construct a computer-use action for entering ordinary text + * strings. + * + * **Details** + * + * The payload uses `action: "type"` and a `text` string containing the text to + * enter. + * + * @see {@link ComputerUseKeyAction} for key presses and keyboard shortcuts + * + * @category computer use + * @since 4.0.0 + */ +export const TypeAction = Schema.Struct({ + action: Schema.Literal("type"), + /** + * The text to type. + */ + text: Schema.String +}) +/** + * Computer-use action payload for typing a text string. + * + * **Details** + * + * The payload uses `action: "type"` and a `text` string containing the text to + * enter. + * + * @category computer use + * @since 4.0.0 + */ +export type TypeAction = typeof TypeAction.Type + +const ComputerUse_20241022_Actions = Schema.Union([ + ComputerUseKeyAction, + ComputerUseLeftClickAction, + ComputerUseMouseMoveAction, + ComputerUseScreenshotAction, + TypeAction +]) + +// ----------------------------------------------------------------------------- +// Computer Use 20250124 Actions +// ----------------------------------------------------------------------------- + +/** + * Schema for a computer-use action that performs a double click. + * + * **When to use** + * + * Use to validate or construct an Anthropic computer-use payload for double + * clicking at the current mouse position or at a specific screen coordinate. + * + * **Details** + * + * The encoded payload uses `action: "double_click"`. The optional + * `coordinate` field supplies the `[x, y]` pixel position; when omitted, the + * action uses the current mouse position. + * + * **Gotchas** + * + * The coordinate schema only checks that the value is a two-number tuple. It + * does not validate that the point falls within the configured display + * dimensions. + * + * @see {@link ComputerUseLeftClickAction} for performing a single left click + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseDoubleClickAction = Schema.Struct({ + action: Schema.Literal("double_click"), + /** + * The coordinate to double click (defaults to the current mouse position if + * omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for performing a double click, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseDoubleClickAction = typeof ComputerUseDoubleClickAction.Type + +/** + * Keeps a key pressed for a specified duration during computer-use execution. + * + * **When to use** + * + * Use to keep a keyboard key depressed for a fixed number of seconds in a + * computer-use action sequence. + * + * **Details** + * + * The schema describes objects with `action: "hold_key"`, a `text` field + * containing the key to hold, and a `duration` field containing the number of + * seconds to hold it. + * + * **Gotchas** + * + * The schema only checks that `duration` is a number; it does not require a + * positive value. + * + * @see {@link ComputerUseKeyAction} for pressing a key or key combination without holding it + * @see {@link ComputerUseWaitAction} for pausing between actions without holding a key + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseHoldKeyAction = Schema.Struct({ + action: Schema.Literal("hold_key"), + /** + * The key to hold (e.g. `"shift"`, `"ctrl"`). + */ + text: Schema.String, + /** + * The number of seconds to hold the key. + */ + duration: Schema.Number +}) +/** + * Computer-use action payload for holding a key for a specified duration. + * + * **When to use** + * + * Use to represent a key that should remain pressed for a measured interval. + * + * **Details** + * + * Set `action` to `"hold_key"`, `text` to the key to hold, and `duration` to + * the number of seconds to hold it. + * + * @see {@link ComputerUseKeyAction} for a single key press or key combination without a hold duration + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseHoldKeyAction = typeof ComputerUseHoldKeyAction.Type + +/** + * Schema for a computer-use action that drags with the left mouse button. + * + * **When to use** + * + * Use to validate or construct an Anthropic computer-use payload for dragging + * from one screen coordinate to another in a single action. + * + * **Details** + * + * The encoded payload uses `action: "left_click_drag"` and requires both + * `start_coordinate` and `coordinate` as `[x, y]` pixel positions. + * + * **Gotchas** + * + * The coordinate schema only checks that each value is a two-number tuple. It + * does not validate that either point falls within the configured display + * dimensions. + * + * @see {@link ComputerUseLeftMouseDownAction} for starting a manual drag sequence + * @see {@link ComputerUseLeftMouseUpAction} for ending a manual drag sequence + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseLeftClickDragAction = Schema.Struct({ + action: Schema.Literal("left_click_drag"), + /** + * The `[x, y]` coordinate to start dragging from. + */ + start_coordinate: Coordinate, + /** + * The `[x, y]` coordinate to drag to. + */ + coordinate: Coordinate +}) +/** + * Computer-use action payload for dragging from a start coordinate to an end coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseLeftClickDragAction = typeof ComputerUseLeftClickDragAction.Type + +/** + * Starts a left mouse button press without releasing it. + * + * **When to use** + * + * Use when constructing a manual click or drag sequence that should press and + * hold the left mouse button before a later release. + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseLeftMouseDownAction = Schema.Struct({ + action: Schema.Literal("left_mouse_down"), + /** + * The coordinate at which the left mouse button should be held down (defaults + * to the current mouse position if omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for pressing and holding the left mouse button, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseLeftMouseDownAction = typeof ComputerUseLeftMouseDownAction.Type + +/** + * Releases the left mouse button. + * + * **When to use** + * + * Use when constructing a manual click or drag sequence that should release the + * left mouse button after it was previously held down. + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseLeftMouseUpAction = Schema.Struct({ + action: Schema.Literal("left_mouse_up"), + /** + * The coordinate at which the left mouse button should be released (defaults + * to the current mouse position if omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for releasing the left mouse button, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseLeftMouseUpAction = typeof ComputerUseLeftMouseUpAction.Type + +/** + * Schema for a computer-use action that performs a middle click. + * + * **When to use** + * + * Use to validate or construct a middle-button click action for Anthropic + * computer use, optionally targeting a specific screen coordinate. + * + * **Details** + * + * The payload must use `action: "middle_click"`. When `coordinate` is omitted, + * the click occurs at the current mouse position. + * + * **Gotchas** + * + * This action is available in the 2025-01-24 computer-use action set and later; + * it is not part of `ComputerUse_20241022`. + * + * @see {@link ComputerUse_20250124} for the provider-defined tool version that first accepts this action + * @see {@link ComputerUseLeftClickAction} for primary-button clicks + * @see {@link ComputerUseRightClickAction} for secondary-button clicks + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseMiddleClickAction = Schema.Struct({ + action: Schema.Literal("middle_click"), + /** + * The coordinate to middle click (defaults to the current mouse position if + * omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for performing a middle click, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseMiddleClickAction = typeof ComputerUseMiddleClickAction.Type + +/** + * Schema for a computer-use action that performs a right click, optionally at a + * specific screen coordinate. + * + * **When to use** + * + * Use to validate or construct the `right_click` action for an Anthropic + * computer-use tool call. + * + * **Details** + * + * The optional `coordinate` field is an `[x, y]` screen coordinate in pixels. + * When omitted, the right click is performed at the current mouse position. + * + * @see {@link ComputerUse_20250124} for the provider-defined computer-use tool version that introduced this action + * @see {@link ComputerUseLeftClickAction} for the corresponding left-click action + * @see {@link ComputerUseMiddleClickAction} for the corresponding middle-click action + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseRightClickAction = Schema.Struct({ + action: Schema.Literal("right_click"), + /** + * The coordinate to right click (defaults to the current mouse position if + * omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for performing a right click, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseRightClickAction = typeof ComputerUseRightClickAction.Type + +/** + * Schema for a computer-use scroll action. + * + * **When to use** + * + * Use when validating or constructing Anthropic computer-use scroll payloads. + * + * **Details** + * + * The encoded payload uses `action: "scroll"`, an optional `coordinate`, + * `scroll_direction`, and `scroll_amount`. + * + * **Gotchas** + * + * `coordinate` only checks a two-number tuple, and `scroll_amount` is only + * `Schema.Number`. + * + * @see {@link ComputerUse_20250124} for the tool version that accepts this action + * @see {@link ScrollDirection} for the accepted direction literals + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseScrollAction = Schema.Struct({ + action: Schema.Literal("scroll"), + /** + * The coordinate to start scrolling from (defaults to the current mouse + * position if omitted). + */ + coordinate: Schema.optionalKey(Coordinate), + /** + * The direction to scroll. + */ + scroll_direction: ScrollDirection, + /** + * The amount to scroll (in pixels or scroll units). + */ + scroll_amount: Schema.Number +}) +/** + * Computer-use action payload for scrolling by a specified amount in a specified direction, optionally from a coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseScrollAction = typeof ComputerUseScrollAction.Type + +/** + * Schema for a computer-use triple-click action. + * + * **When to use** + * + * Use when validating or constructing Anthropic computer-use triple-click + * payloads at the current pointer position or an optional coordinate. + * + * **Details** + * + * The encoded payload uses `action: "triple_click"` and an optional + * `coordinate`. + * + * **Gotchas** + * + * `coordinate` only validates as a two-number tuple and does not check display + * bounds. + * + * @see {@link ComputerUse_20250124} for the tool version that accepts this action + * @see {@link ComputerUseDoubleClickAction} for the two-click variant + * @see {@link ComputerUseLeftClickAction} for a single left click + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseTripleClickAction = Schema.Struct({ + action: Schema.Literal("triple_click"), + /** + * The coordinate to triple click (defaults to the current mouse position if + * omitted). + */ + coordinate: Schema.optionalKey(Coordinate) +}) +/** + * Computer-use action payload for performing a triple click, optionally at a specific coordinate. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseTripleClickAction = typeof ComputerUseTripleClickAction.Type + +/** + * Schema for a computer-use wait action. + * + * **When to use** + * + * Use when validating or constructing Anthropic computer-use payloads that pause + * between actions. + * + * **Details** + * + * The encoded payload uses `action: "wait"` and a required `duration` in + * seconds. + * + * **Gotchas** + * + * `duration` is only `Schema.Number`; it is not constrained to positive or + * finite values. + * + * @see {@link ComputerUseHoldKeyAction} for another duration-based computer-use action + * @see {@link ComputerUse_20250124} for the tool version that accepts this action + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseWaitAction = Schema.Struct({ + action: Schema.Literal("wait"), + /** + * The number of seconds to wait. + */ + duration: Schema.Number +}) +/** + * Computer-use action payload for pausing for a specified duration. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseWaitAction = typeof ComputerUseWaitAction.Type + +const ComputerUse_20250124_Actions = Schema.Union([ + ...ComputerUse_20241022_Actions.members, + ComputerUseDoubleClickAction, + ComputerUseHoldKeyAction, + ComputerUseLeftClickDragAction, + ComputerUseLeftMouseDownAction, + ComputerUseLeftMouseUpAction, + ComputerUseMiddleClickAction, + ComputerUseRightClickAction, + ComputerUseScrollAction, + ComputerUseTripleClickAction, + ComputerUseWaitAction +]) + +// ----------------------------------------------------------------------------- +// Computer Use 20251124 Actions +// ----------------------------------------------------------------------------- + +/** + * Zooms into a specific region of the screen at full resolution. + * + * **When to use** + * + * Use when building or validating the 2025-11-24 computer-use action for a + * zoom-enabled tool definition. + * + * **Details** + * + * The encoded payload uses `action: "zoom"` and a `region` tuple. + * + * **Gotchas** + * + * Requires `enableZoom: true` in the tool definition. `region` is only a + * four-number tuple and does not validate corner ordering or display bounds. + * + * @see {@link ComputerUse_20251124} for the tool version that accepts this action + * @see {@link ComputerUseScreenshotAction} for capturing the full screen instead + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUseZoomAction = Schema.Struct({ + action: Schema.Literal("zoom"), + /** + * Region to zoom into, defined as `[x1, y1, x2, y2]` coordinates where + * `(x1, y1)` is the top-left corner and `(x2, y2)` is the bottom-right corner. + */ + region: Region +}) +/** + * Computer-use action payload for zooming into a specific screen region. + * + * **Gotchas** + * + * The enclosing computer-use tool must be configured with `enableZoom: true`. + * `region` is only a four-number tuple and does not validate corner ordering or + * display bounds. + * + * @category computer use + * @since 4.0.0 + */ +export type ComputerUseZoomAction = typeof ComputerUseZoomAction.Type + +const ComputerUse_20251124_Actions = Schema.Union([ + ...ComputerUse_20250124_Actions.members, + ComputerUseZoomAction +]) + +// ----------------------------------------------------------------------------- +// Computer Use Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the deprecated computer-use tool for Claude 3.5 Sonnet v2. + * + * **Details** + * + * Requires the "computer-use-2024-10-22" beta header. + * Basic actions only: screenshot, left_click, type, key, mouse_move. + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUse_20241022 = Tool.providerDefined({ + id: "anthropic.computer_use_20241022", + customName: "AnthropicComputerUse", + providerName: "computer_use", + requiresHandler: true, + args: ComputerUse_20241022_Args, + parameters: ComputerUse_20241022_Actions, + success: Schema.String +}) + +/** + * Defines the computer-use tool for Claude 4 models and Claude Sonnet 3.7. + * + * **When to use** + * + * Use when you need Anthropic computer use for Claude 4 models or Claude + * Sonnet 3.7 with the 2025-01-24 action set. + * + * **Details** + * + * Requires the "computer-use-2025-01-24" beta header. + * Includes basic actions plus enhanced actions: scroll, left_click_drag, + * right_click, middle_click, double_click, triple_click, left_mouse_down, + * left_mouse_up, hold_key, wait. + * + * @see {@link ComputerUse_20241022} for the older basic action set + * @see {@link ComputerUse_20251124} for the newer zoom-capable version + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUse_20250124 = Tool.providerDefined({ + id: "anthropic.computer_20250124", + customName: "AnthropicComputerUse", + providerName: "computer", + requiresHandler: true, + args: ComputerUse_20241022_Args, + parameters: ComputerUse_20250124_Actions, + success: Schema.String +}) + +/** + * Defines the computer-use tool for Claude Opus 4.5 only. + * + * **When to use** + * + * Use when you need Anthropic computer use for Claude Opus 4.5 with the + * 2025-11-24 action set and zoom-capable screen inspection. + * + * **Details** + * + * Requires the "computer-use-2025-11-24" beta header. + * Includes all actions from computer_20250124 plus the zoom action for + * detailed screen region inspection. + * + * **Gotchas** + * + * Zoom actions require `enableZoom: true` in args. + * + * @see {@link ComputerUse_20250124} for the previous action set without zoom + * @see {@link ComputerUseZoomAction} for the zoom action payload + * + * @category computer use + * @since 4.0.0 + */ +export const ComputerUse_20251124 = Tool.providerDefined({ + id: "anthropic.computer_20251124", + customName: "AnthropicComputerUse", + providerName: "computer", + requiresHandler: true, + args: ComputerUse_20251124_Args, + parameters: ComputerUse_20251124_Actions, + success: Schema.String +}) + +// ============================================================================= +// Memory +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Common Types +// ----------------------------------------------------------------------------- + +/** + * Defines a `[start, end]` line range for viewing file contents. + * + * **When to use** + * + * Use when constructing or validating `view_range` for memory or text editor + * view commands. + * + * **Details** + * + * Lines are 1-indexed. Use `-1` for end to read to the end of the file. For + * example, `[1, 50]` views lines 1-50 and `[100, -1]` views from line 100 to + * the end of the file. + * + * @see {@link MemoryViewCommand} for memory view payloads that use this range + * @see {@link TextEditorViewCommand} for text editor view payloads that use this range + * + * @category memory + * @since 4.0.0 + */ +export const ViewRange = Schema.Tuple([Schema.Number, Schema.Number]) +/** + * A `[start, end]` 1-indexed line range for viewing file contents, using `-1` as the end value to read through the end of the file. + * + * **When to use** + * + * Use when typing `view_range` for memory or text editor view commands. + * + * @category memory + * @since 4.0.0 + */ +export type ViewRange = typeof ViewRange.Type + +// ----------------------------------------------------------------------------- +// Memory 20250818 Commands +// ----------------------------------------------------------------------------- + +/** + * Schema for the memory tool command that creates a new file at a path. + * + * **Details** + * + * The payload contains `command: "create"`, a `path` string, and the + * `file_text` content to write to the file. + * + * @category memory + * @since 4.0.0 + */ +export const MemoryCreateCommand = Schema.Struct({ + command: Schema.Literal("create"), + /** + * The path to the file that should be created. + */ + path: Schema.String, + /** + * The content to write to the file. + */ + file_text: Schema.String +}) +/** + * Memory tool command payload for creating a new file at a path. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryCreateCommand = typeof MemoryCreateCommand.Type + +/** + * Schema for a memory command that deletes a file or directory. + * + * @category memory + * @since 4.0.0 + */ +export const MemoryDeleteCommand = Schema.Struct({ + command: Schema.Literal("delete"), + /** + * The path to the file or directory to delete. + */ + path: Schema.String +}) +/** + * Memory tool command payload for deleting a file or directory at a path. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryDeleteCommand = typeof MemoryDeleteCommand.Type + +/** + * Schema for the memory `insert` command. + * + * **When to use** + * + * Use when validating or constructing `insert` payloads for `Memory_20250818`. + * + * **Details** + * + * The payload is discriminated by `command: "insert"` and requires `path`, + * `insert_line`, and `insert_text`. + * + * @see {@link Memory_20250818} for the provider-defined tool that consumes this command + * @see {@link MemoryStrReplaceCommand} for replacing existing text instead + * + * @category memory + * @since 4.0.0 + */ +export const MemoryInsertCommand = Schema.Struct({ + command: Schema.Literal("insert"), + /** + * The path to the file to insert text into. + */ + path: Schema.String, + /** + * The line at which the text should be inserted. + */ + insert_line: Schema.Number, + /** + * The text to insert. + */ + insert_text: Schema.String +}) +/** + * Memory tool command payload for inserting text at a specific line in a file. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryInsertCommand = typeof MemoryInsertCommand.Type + +/** + * Schema for the memory command that renames or moves a file or directory. + * + * **Details** + * + * The payload uses `command: "rename"` and requires `old_path` as the current + * path plus `new_path` as the new destination path. + * + * @category memory + * @since 4.0.0 + */ +export const MemoryRenameCommand = Schema.Struct({ + command: Schema.Literal("rename"), + /** + * The old path to the file or directory. + */ + old_path: Schema.String, + /** + * The new path to the file or directory. + */ + new_path: Schema.String +}) +/** + * Memory tool command payload for renaming or moving a file or directory. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryRenameCommand = typeof MemoryRenameCommand.Type + +/** + * Schema for the memory `str_replace` command. + * + * **When to use** + * + * Use when validating or constructing `str_replace` payloads for + * `Memory_20250818`. + * + * **Details** + * + * The payload is discriminated by `command: "str_replace"` and requires `path`, + * `old_str`, and `new_str`. + * + * @see {@link Memory_20250818} for the provider-defined tool that consumes this command + * + * @category memory + * @since 4.0.0 + */ +export const MemoryStrReplaceCommand = Schema.Struct({ + command: Schema.Literal("str_replace"), + /** + * The path to the file in which the replacement should occur. + */ + path: Schema.String, + /** + * The text to replace. + */ + old_str: Schema.String, + /** + * The replacement text. + */ + new_str: Schema.String +}) +/** + * Memory tool command payload for replacing text in a file. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryStrReplaceCommand = typeof MemoryStrReplaceCommand.Type + +/** + * Shows directory contents or file contents with optional line ranges. + * + * **Details** + * + * When used on a file, returns file contents optionally limited by `view_range`. + * When used on a directory, lists contents. + * + * @category memory + * @since 4.0.0 + */ +export const MemoryViewCommand = Schema.Struct({ + command: Schema.Literal("view"), + /** + * The path to the file or directory to view. + */ + path: Schema.String, + /** + * The specific lines to view. + */ + view_range: Schema.optionalKey(ViewRange) +}) +/** + * Memory tool command payload for viewing a file or directory, optionally with a file line range. + * + * @category memory + * @since 4.0.0 + */ +export type MemoryViewCommand = typeof MemoryViewCommand.Type + +const Memory_20250818_Commands = Schema.Union([ + MemoryCreateCommand, + MemoryDeleteCommand, + MemoryInsertCommand, + MemoryRenameCommand, + MemoryStrReplaceCommand, + MemoryViewCommand +]) + +// ----------------------------------------------------------------------------- +// Memory Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the memory tool for persistent file operations across conversations. + * + * **Details** + * + * Provides commands for creating, viewing, editing, renaming, and deleting + * files within the model's memory space. + * + * @category memory + * @since 4.0.0 + */ +export const Memory_20250818 = Tool.providerDefined({ + id: "anthropic.memory_20250818", + customName: "AnthropicMemory", + providerName: "memory", + requiresHandler: true, + parameters: Memory_20250818_Commands, + success: Schema.String +}) + +// ============================================================================= +// Text Editor +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Text Editor Commands +// ----------------------------------------------------------------------------- + +/** + * Reads the contents of a file or lists directory contents. + * + * **When to use** + * + * Use when validating or constructing the standalone Anthropic Text Editor + * `view` command. + * + * **Details** + * + * When used on a file, returns the file contents, optionally limited to a line + * range. When used on a directory, lists all files and subdirectories. + * `view_range` is a 1-indexed `[start, end]` tuple where `-1` means through + * the end of the file. + * + * @see {@link CodeExecutionTextEditorView} for the code-execution variant without `view_range` + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditorViewCommand = Schema.Struct({ + command: Schema.Literal("view"), + /** + * Absolute or relative path to the file or directory to view. + */ + path: Schema.String, + /** + * Optional line range to view (only applies to files, not directories). + * Lines are 1-indexed. Use -1 for end to read to end of file. + */ + view_range: Schema.optionalKey(ViewRange) +}) +/** + * Text editor command payload for viewing file contents or listing directory contents. + * + * **Details** + * + * `view_range` is a 1-indexed `[start, end]` tuple where `-1` means through + * the end of the file. + * + * @category text editor + * @since 4.0.0 + */ +export type TextEditorViewCommand = typeof TextEditorViewCommand.Type + +/** + * Create a new file with specified content. + * + * **When to use** + * + * Use when validating or constructing an Anthropic text editor `create` + * command. + * + * **Details** + * + * The payload is discriminated by `command: "create"` and requires both `path` + * and `file_text`. + * + * **Gotchas** + * + * Fails if the file already exists. Parent directories must exist. + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditorCreateCommand = Schema.Struct({ + command: Schema.Literal("create"), + /** + * Path where the file should be created. + */ + path: Schema.String, + /** + * The content to write to the new file. + */ + file_text: Schema.String +}) +/** + * Text editor command payload for creating a new file with the specified content. + * + * **When to use** + * + * Use when typing parsed text-editor create command payloads after schema + * validation and before dispatching to Anthropic tool handlers. + * + * **Gotchas** + * + * The command fails if the file already exists or if parent directories are missing. + * + * @category text editor + * @since 4.0.0 + */ +export type TextEditorCreateCommand = typeof TextEditorCreateCommand.Type + +/** + * Replaces a specific string in a file with a new string. + * + * **When to use** + * + * Use when validating or constructing standalone Anthropic text editor + * `str_replace` commands. + * + * **Details** + * + * The payload uses `command: "str_replace"`, `path`, `old_str`, and `new_str`. + * `new_str` may be empty to delete text. + * + * **Gotchas** + * + * The `old_str` must match exactly (including whitespace and indentation) + * and must be unique in the file. + * + * @see {@link TextEditorViewCommand} for reading contents before choosing `old_str` + * @see {@link CodeExecutionTextEditorStrReplace} for the code-execution variant + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditorStrReplaceCommand = Schema.Struct({ + command: Schema.Literal("str_replace"), + /** + * Path to the file to modify. + */ + path: Schema.String, + /** + * The exact string to find and replace (must be unique in the file). + */ + old_str: Schema.String, + /** + * The string to replace old_str with (can be empty to delete). + */ + new_str: Schema.String +}) +/** + * Text editor command payload for replacing one exact, unique string in a file. + * + * **When to use** + * + * Use when typing parsed text-editor replace command payloads that must carry + * one exact `old_str` match. + * + * **Gotchas** + * + * The `old_str` must match exactly, including whitespace and indentation, and + * must be unique in the file. + * + * @category text editor + * @since 4.0.0 + */ +export type TextEditorStrReplaceCommand = typeof TextEditorStrReplaceCommand.Type + +/** + * Inserts text at a specific line number in a file. + * + * **Details** + * + * Inserts the new text after the specified line number. Use `0` to insert at + * the beginning of the file; other values are 1-indexed. + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditorInsertCommand = Schema.Struct({ + command: Schema.Literal("insert"), + /** + * Path to the file to modify. + */ + path: Schema.String, + /** + * The line number after which to insert (0 = beginning, 1-indexed). + */ + insert_line: Schema.Number, + /** + * The text to insert. + */ + new_str: Schema.String +}) +/** + * Text editor command payload for inserting text after a specific line number in a file. + * + * @category text editor + * @since 4.0.0 + */ +export type TextEditorInsertCommand = typeof TextEditorInsertCommand.Type + +/** + * Undoes the last edit made to a file. + * + * **Details** + * + * Reverts the most recent `str_replace`, `insert`, or `create` operation on the + * file. + * + * **Gotchas** + * + * This command is available in `text_editor_20241022` and + * `text_editor_20250124`, but not in `text_editor_20250429` or + * `text_editor_20250728`. + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditorUndoEditCommand = Schema.Struct({ + command: Schema.Literal("undo_edit"), + /** + * Path to the file to undo the last edit on. + */ + path: Schema.String +}) +/** + * Text editor command payload for undoing the most recent edit to a file. + * + * **Gotchas** + * + * Available for `text_editor_20241022` and `text_editor_20250124`, but not for + * `text_editor_20250429` or `text_editor_20250728`. + * + * @category text editor + * @since 4.0.0 + */ +export type TextEditorUndoEditCommand = typeof TextEditorUndoEditCommand.Type + +const TextEditor_StrReplaceEditor_Commands = Schema.Union([ + TextEditorViewCommand, + TextEditorCreateCommand, + TextEditorStrReplaceCommand, + TextEditorInsertCommand, + TextEditorUndoEditCommand +]) + +const TextEditor_StrReplaceBasedEdit_Commands = Schema.Union([ + TextEditorViewCommand, + TextEditorCreateCommand, + TextEditorStrReplaceCommand, + TextEditorInsertCommand +]) + +// ----------------------------------------------------------------------------- +// Text Editor Args +// ----------------------------------------------------------------------------- + +const TextEditor_StrReplaceBasedEdit_Args = Schema.Struct({ + /** + * Maximum number of characters to return when viewing large files. + * When a file exceeds this limit, it will be truncated. + */ + max_characters: Schema.optional(Schema.Number) +}) + +// ----------------------------------------------------------------------------- +// Text Editor Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the deprecated text editor tool for Claude 3.5 Sonnet. + * + * **When to use** + * + * Use when you need the 2024-10-22 `str_replace_editor` compatibility path for + * Claude 3.5 Sonnet. + * + * **Details** + * + * Requires the "computer-use-2024-10-22" beta header and supports `view`, + * `create`, `str_replace`, `insert`, and `undo_edit` commands. + * + * @see {@link TextEditor_20250124} for the newer `str_replace_editor` version + * @see {@link TextEditor_20250728} for the Claude 4 `str_replace_based_edit_tool` line + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditor_20241022 = Tool.providerDefined({ + id: "anthropic.text_editor_20241022", + customName: "AnthropicTextEditor", + providerName: "str_replace_editor", + requiresHandler: true, + parameters: TextEditor_StrReplaceEditor_Commands, + success: Schema.String +}) + +/** + * Defines the text editor tool for deprecated Claude Sonnet 3.7. + * + * **When to use** + * + * Use when you need the 2025-01-24 Claude Sonnet 3.7 text editor tool using + * `str_replace_editor`. + * + * **Details** + * + * Requires the "computer-use-2025-01-24" beta header, requires a handler, and + * supports `view`, `create`, `str_replace`, `insert`, and `undo_edit` commands. + * + * @see {@link TextEditor_20241022} for the older `str_replace_editor` version + * @see {@link TextEditor_20250429} for the Claude 4 `str_replace_based_edit_tool` line + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditor_20250124 = Tool.providerDefined({ + id: "anthropic.text_editor_20250124", + customName: "AnthropicTextEditor", + providerName: "str_replace_editor", + requiresHandler: true, + parameters: TextEditor_StrReplaceEditor_Commands, + success: Schema.String +}) + +/** + * Defines the text editor tool for Claude 4 models using Anthropic's `str_replace_based_edit_tool`. + * + * **When to use** + * + * Use when you need the 2025-04-29 Claude 4 `str_replace_based_edit_tool` + * version. + * + * **Details** + * + * Requires the "computer-use-2025-01-24" beta header. + * + * **Gotchas** + * + * This version does not support the `undo_edit` command. + * + * @see {@link TextEditor_20250124} for the previous `str_replace_editor` version + * @see {@link TextEditor_20250728} for the later Claude 4 text editor version + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditor_20250429 = Tool.providerDefined({ + id: "anthropic.text_editor_20250429", + customName: "AnthropicTextEditor", + providerName: "str_replace_based_edit_tool", + requiresHandler: true, + args: TextEditor_StrReplaceBasedEdit_Args, + parameters: TextEditor_StrReplaceBasedEdit_Commands, + success: Schema.String +}) + +/** + * Defines the text editor tool for Claude 4 models. + * + * **Details** + * + * Uses Anthropic's `str_replace_based_edit_tool`. `max_characters` can limit + * file-view output for this version. + * + * **Gotchas** + * + * This version does not support the `undo_edit` command. + * + * @category text editor + * @since 4.0.0 + */ +export const TextEditor_20250728 = Tool.providerDefined({ + id: "anthropic.text_editor_20250728", + customName: "AnthropicTextEditor", + providerName: "str_replace_based_edit_tool", + requiresHandler: true, + args: TextEditor_StrReplaceBasedEdit_Args, + parameters: TextEditor_StrReplaceBasedEdit_Commands, + success: Schema.String +}) + +// ============================================================================= +// Web Search +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Web Search Types +// ----------------------------------------------------------------------------- + +/** + * Describes user location for localizing search results. + * + * **When to use** + * + * Use when you need to localize search results for location-dependent queries + * like weather, local businesses, or events. + * + * **Details** + * + * The schema uses `type: "approximate"` plus optional `city`, `region`, + * `country`, and `timezone`. `country` is an ISO 3166-1 alpha-2 code, and + * `timezone` is an IANA time zone identifier. + * + * @see {@link WebSearch_20250305_Args} for the argument schema that consumes this location + * + * @category Web Search + * @since 4.0.0 + */ +export const WebSearchUserLocation = Schema.Struct({ + /** + * Location type - currently only "approximate" is supported. + */ + type: Schema.Literal("approximate"), + /** + * City name. + */ + city: Schema.optional(Schema.String), + /** + * Region/state/province name. + */ + region: Schema.optional(Schema.String), + /** + * ISO 3166-1 alpha-2 country code. + */ + country: Schema.optional(Schema.String), + /** + * IANA timezone identifier. + */ + timezone: Schema.optional(Schema.String) +}) + +// ----------------------------------------------------------------------------- +// Web Search Args +// ----------------------------------------------------------------------------- + +/** + * Defines configuration arguments for the web search tool. + * + * **When to use** + * + * Use when you need to configure `WebSearch_20250305` with search limits, + * domain filters, or user location. + * + * **Details** + * + * The payload can set `maxUses`, `allowedDomains`, `blockedDomains`, and + * `userLocation`. + * + * **Gotchas** + * + * `allowedDomains` and `blockedDomains` are mutually exclusive. + * + * @see {@link WebSearch_20250305} for the provider-defined tool that consumes these arguments + * @see {@link WebSearchUserLocation} for localizing search results + * + * @category Web Search + * @since 4.0.0 + */ +export const WebSearch_20250305_Args = Schema.Struct({ + /** + * Maximum number of searches allowed per API request. + */ + maxUses: Schema.optional(Schema.Number), + /** + * Restrict search results to only these domains. + * + * Cannot be used together with `blockedDomains`. + */ + allowedDomains: Schema.optional(Schema.Array(Schema.String)), + /** + * Exclude results from these domains. + * + * Cannot be used together with `allowedDomains`. + */ + blockedDomains: Schema.optional(Schema.Array(Schema.String)), + /** + * User location for localizing search results. + */ + userLocation: Schema.optional(WebSearchUserLocation) +}) +/** + * Configuration arguments for the Anthropic web search tool, including usage limits, domain filters, and optional user location. + * + * **Gotchas** + * + * `allowedDomains` and `blockedDomains` are mutually exclusive. + * + * @category Web Search + * @since 4.0.0 + */ +export type WebSearch_20250305_Args = typeof WebSearch_20250305_Args.Type + +// ----------------------------------------------------------------------------- +// Web Search Parameters +// ----------------------------------------------------------------------------- + +/** + * Schema for Claude-supplied web search tool parameters. + * + * **Details** + * + * The payload contains the generated `query` string and is consumed by + * `WebSearch_20250305`. + * + * @see {@link WebSearch_20250305} for the provider-defined tool that consumes this payload + * + * @category Web Search + * @since 4.0.0 + */ +export const WebSearchParameters = Schema.Struct({ + /** + * The search query generated by Claude. + */ + query: Schema.String +}) +/** + * Type of the parameters Claude supplies when invoking the Anthropic web search tool. + * + * **Details** + * + * Contains the generated search query used by `WebSearch_20250305`. + * + * @see {@link WebSearch_20250305} for the provider-defined tool that consumes this payload + * + * @category Web Search + * @since 4.0.0 + */ +export type WebSearchParameters = typeof WebSearchParameters.Type + +// ----------------------------------------------------------------------------- +// Web Search Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the web search tool for Claude models. + * + * **When to use** + * + * Use when you want Claude to search the web for real-time information. + * + * **Details** + * + * Enables Claude to search the web for real-time information. This is a + * server-side tool executed by Anthropic's infrastructure. + * Generally available (no beta header required). + * + * @see {@link WebFetch_20250910} for retrieving known URLs after discovery + * + * @category Web Search + * @since 4.0.0 + */ +export const WebSearch_20250305 = Tool.providerDefined({ + id: "anthropic.web_search_20250305", + customName: "AnthropicWebSearch", + providerName: "web_search", + args: WebSearch_20250305_Args, + parameters: WebSearchParameters, + success: Schema.Array(Generated.BetaResponseWebSearchResultBlock), + failure: Generated.BetaResponseWebSearchToolResultError +}) + +// ============================================================================= +// Web Fetch +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Web Fetch Types +// ----------------------------------------------------------------------------- + +/** + * Defines citation configuration for web fetch. + * + * **When to use** + * + * Use when you need to enable or disable citations on web fetch results. + * + * **Details** + * + * The payload contains the `enabled` flag. `citations` is optional on + * `WebFetch_20250910_Args`, and citations are disabled by default. + * + * @see {@link WebFetch_20250910_Args} for the argument schema that consumes this configuration + * + * @category Web Fetch + * @since 4.0.0 + */ +export const WebFetchCitationsConfig = Schema.Struct({ + /** + * Enable citations for fetched content. + */ + enabled: Schema.Boolean +}) +/** + * Configuration payload for enabling or disabling citations on web fetch results. + * + * **When to use** + * + * Use when typing parsed web-fetch citation configuration shared between + * request arguments and handler code. + * + * **Details** + * + * The payload contains the `enabled` flag. `citations` is optional on + * `WebFetch_20250910_Args`, and citations are disabled by default. + * + * @see {@link WebFetch_20250910_Args} for the argument schema that consumes this configuration + * + * @category Web Fetch + * @since 4.0.0 + */ +export type WebFetchCitationsConfig = typeof WebFetchCitationsConfig.Type + +// ----------------------------------------------------------------------------- +// Web Fetch Args +// ----------------------------------------------------------------------------- + +/** + * Defines configuration arguments for the web fetch tool. + * + * **When to use** + * + * Use when you need to configure `WebFetch_20250910` with usage limits, domain + * filters, citations, or content token limits. + * + * **Details** + * + * The payload can set `maxUses`, domain filters, `citations`, and + * `maxContentTokens`, which map to Anthropic web fetch request fields. + * + * **Gotchas** + * + * `allowedDomains` and `blockedDomains` are mutually exclusive. + * `maxContentTokens` is approximate and does not apply to binary content such + * as PDFs. + * + * @see {@link WebFetch_20250910} for the provider-defined tool that consumes these arguments + * @see {@link WebFetchCitationsConfig} for configuring citations + * + * @category Web Fetch + * @since 4.0.0 + */ +export const WebFetch_20250910_Args = Schema.Struct({ + /** + * Maximum number of fetches allowed per API request. + */ + maxUses: Schema.optional(Schema.Number), + /** + * Restrict fetches to only these domains. + * + * Cannot be used together with `blockedDomains`. + */ + allowedDomains: Schema.optional(Schema.Array(Schema.String)), + /** + * Exclude fetches from these domains. + * + * Cannot be used together with `allowedDomains`. + */ + blockedDomains: Schema.optional(Schema.Array(Schema.String)), + /** + * Enable citations for fetched content. + */ + citations: Schema.optional(WebFetchCitationsConfig), + /** + * Maximum content length in tokens. + */ + maxContentTokens: Schema.optional(Schema.Number) +}) +/** + * Configuration arguments for the Anthropic web fetch tool, including usage limits, domain filters, citation settings, and token limits. + * + * **When to use** + * + * Use when typing parsed web-fetch tool configuration shared by the + * provider-defined tool and request-building code. + * + * **Gotchas** + * + * `allowedDomains` and `blockedDomains` are mutually exclusive. + * `maxContentTokens` is approximate and does not apply to binary content such + * as PDFs. + * + * @category Web Fetch + * @since 4.0.0 + */ +export type WebFetch_20250910_Args = typeof WebFetch_20250910_Args.Type + +// ----------------------------------------------------------------------------- +// Web Fetch Parameters +// ----------------------------------------------------------------------------- + +/** + * Schema for Claude-supplied web fetch parameters. + * + * **When to use** + * + * Use when validating or constructing the `url` payload consumed by + * `WebFetch_20250910`. + * + * **Details** + * + * The payload contains the single `url` parameter for Anthropic web fetch. + * + * **Gotchas** + * + * The URL must be user-provided or from prior search/fetch results. Maximum URL + * length is 250 characters. + * + * @see {@link WebFetch_20250910} for the provider-defined tool that consumes this payload + * + * @category Web Fetch + * @since 4.0.0 + */ +export const WebFetchParameters = Schema.Struct({ + /** + * URL to fetch. Must be a URL provided by the user or from prior search/fetch + * results. Maximum URL length: 250 characters. + */ + url: Schema.String +}) +/** + * Type of the parameters Claude supplies when invoking the Anthropic web fetch tool. + * + * **When to use** + * + * Use when typing Claude-supplied web-fetch tool parameters after schema + * validation, before enforcing URL provenance or length constraints. + * + * **Details** + * + * The payload contains the single `url` parameter for Anthropic web fetch. + * + * **Gotchas** + * + * The URL must be user-provided or from prior search/fetch results. Maximum URL + * length is 250 characters. + * + * @category Web Fetch + * @since 4.0.0 + */ +export type WebFetchParameters = typeof WebFetchParameters.Type + +// ----------------------------------------------------------------------------- +// Web Fetch Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines the web fetch tool for Claude models. + * + * **When to use** + * + * Use when you want Claude to retrieve the content of a specific web page or + * PDF. + * + * **Details** + * + * Allows Claude to retrieve full content from web pages and PDF documents. + * This is a server-side tool executed by Anthropic's infrastructure. Selecting + * this tool adds the "web-fetch-2025-09-10" beta header. + * + * @see {@link WebSearch_20250305} for discovering URLs before fetching specific content + * + * @category Web Fetch + * @since 4.0.0 + */ +export const WebFetch_20250910 = Tool.providerDefined({ + id: "anthropic.web_fetch_20250910", + customName: "AnthropicWebFetch", + providerName: "web_fetch", + args: WebFetch_20250910_Args, + parameters: WebFetchParameters, + success: Generated.BetaResponseWebFetchResultBlock, + failure: Generated.BetaResponseWebFetchToolResultError +}) + +// ============================================================================= +// Tool Search +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Tool Search Parameters +// ----------------------------------------------------------------------------- + +/** + * Schema for regex-based tool search input parameters. + * + * **Details** + * + * Claude constructs regex patterns using Python's `re.search()` syntax. + * Maximum query length: 200 characters. + * + * @category tool search + * @since 4.0.0 + */ +export const ToolSearchRegexParameters = Schema.Struct({ + /** + * Python regex pattern to search for tools. + */ + query: Schema.String +}) +/** + * Type of the parameters Claude supplies when invoking regex-based Anthropic tool search. + * + * **Details** + * + * Claude constructs regex patterns using Python's `re.search()` syntax. + * Maximum query length: 200 characters. + * + * @category tool search + * @since 4.0.0 + */ +export type ToolSearchRegexParameters = typeof ToolSearchRegexParameters.Type + +/** + * Defines input parameters for BM25/natural language tool search. + * + * **When to use** + * + * Use when validating or constructing the natural-language query payload for + * `ToolSearchBM25_20251119`. + * + * **Details** + * + * The payload contains Claude's natural-language `query`. BM25 searches tool + * names, descriptions, argument names, and argument descriptions. + * + * @see {@link ToolSearchBM25_20251119} for the provider-defined tool that consumes these parameters + * + * @category tool search + * @since 4.0.0 + */ +export const ToolSearchBM25Parameters = Schema.Struct({ + /** + * Natural language query to search for tools. + */ + query: Schema.String +}) +/** + * Type of the parameters Claude supplies when invoking BM25 natural-language Anthropic tool search. + * + * @category tool search + * @since 4.0.0 + */ +export type ToolSearchBM25Parameters = typeof ToolSearchBM25Parameters.Type + +// ----------------------------------------------------------------------------- +// Tool Search Tool Definitions +// ----------------------------------------------------------------------------- + +/** + * Defines regex-based tool search for Claude models. + * + * **Details** + * + * Claude constructs regex patterns using Python's `re.search()` syntax to + * find tools. The regex is matched against tool names, descriptions, + * argument names, and argument descriptions. + * Requires the "advanced-tool-use-2025-11-20" beta header. + * + * @category tool search + * @since 4.0.0 + */ +export const ToolSearchRegex_20251119 = Tool.providerDefined({ + id: "anthropic.tool_search_tool_regex_20251119", + customName: "AnthropicToolSearchRegex", + providerName: "tool_search_tool_regex", + parameters: ToolSearchRegexParameters, + success: Schema.Array(Generated.BetaRequestToolReferenceBlock), + failure: Generated.BetaResponseToolSearchToolResultError +}) + +/** + * Defines BM25/natural language tool search for Claude models. + * + * **When to use** + * + * Use when you want Claude to find relevant tools from a natural-language query + * instead of a regex pattern. + * + * **Details** + * + * Claude uses natural language queries to search for tools using the + * BM25 algorithm. The search is performed against tool names, descriptions, + * argument names, and argument descriptions. + * Requires the "advanced-tool-use-2025-11-20" beta header. + * + * @see {@link ToolSearchRegex_20251119} for the regex-pattern alternative + * + * @category tool search + * @since 4.0.0 + */ +export const ToolSearchBM25_20251119 = Tool.providerDefined({ + id: "anthropic.tool_search_tool_bm25_20251119", + customName: "AnthropicToolSearchBM25", + providerName: "tool_search_tool_bm25", + parameters: ToolSearchBM25Parameters, + success: Schema.Array(Generated.BetaRequestToolReferenceBlock), + failure: Generated.BetaResponseToolSearchToolResultError +}) diff --git a/repos/effect/packages/ai/anthropic/src/Generated.ts b/repos/effect/packages/ai/anthropic/src/Generated.ts new file mode 100644 index 0000000..319aeb2 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/Generated.ts @@ -0,0 +1,12592 @@ +/** + * @since 4.0.0 + */ + +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { SchemaError } from "effect/Schema" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +// non-recursive definitions +export type APIError = { readonly "message": string; readonly "type": "api_error" } +export const APIError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Internal server error" }), + "type": Schema.Literal("api_error").annotate({ "title": "Type", "default": "api_error" }) +}).annotate({ "title": "APIError" }) +export type AuthenticationError = { readonly "message": string; readonly "type": "authentication_error" } +export const AuthenticationError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Authentication error" }), + "type": Schema.Literal("authentication_error").annotate({ "title": "Type", "default": "authentication_error" }) +}).annotate({ "title": "AuthenticationError" }) +export type Base64ImageSource = { + readonly "data": string + readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" + readonly "type": "base64" +} +export const Base64ImageSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data", "format": "byte" }), + "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]).annotate({ + "title": "Media Type" + }), + "type": Schema.Literal("base64").annotate({ "title": "Type" }) +}).annotate({ "title": "Base64ImageSource" }) +export type Base64PDFSource = { + readonly "data": string + readonly "media_type": "application/pdf" + readonly "type": "base64" +} +export const Base64PDFSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data", "format": "byte" }), + "media_type": Schema.Literal("application/pdf").annotate({ "title": "Media Type" }), + "type": Schema.Literal("base64").annotate({ "title": "Type" }) +}).annotate({ "title": "Base64PDFSource" }) +export type BashCodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "output_file_too_large" +export const BashCodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "output_file_too_large" +]).annotate({ "title": "BashCodeExecutionToolResultErrorCode" }) +export type BetaAPIError = { readonly "message": string; readonly "type": "api_error" } +export const BetaAPIError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Internal server error" }), + "type": Schema.Literal("api_error").annotate({ "title": "Type", "default": "api_error" }) +}).annotate({ "title": "APIError" }) +export type BetaAllThinkingTurns = { readonly "type": "all" } +export const BetaAllThinkingTurns = Schema.Struct({ "type": Schema.Literal("all").annotate({ "title": "Type" }) }) + .annotate({ "title": "AllThinkingTurns" }) +export type BetaAuthenticationError = { readonly "message": string; readonly "type": "authentication_error" } +export const BetaAuthenticationError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Authentication error" }), + "type": Schema.Literal("authentication_error").annotate({ "title": "Type", "default": "authentication_error" }) +}).annotate({ "title": "AuthenticationError" }) +export type BetaBase64ImageSource = { + readonly "data": string + readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" + readonly "type": "base64" +} +export const BetaBase64ImageSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data", "format": "byte" }), + "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]).annotate({ + "title": "Media Type" + }), + "type": Schema.Literal("base64").annotate({ "title": "Type" }) +}).annotate({ "title": "Base64ImageSource" }) +export type BetaBase64PDFSource = { + readonly "data": string + readonly "media_type": "application/pdf" + readonly "type": "base64" +} +export const BetaBase64PDFSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data", "format": "byte" }), + "media_type": Schema.Literal("application/pdf").annotate({ "title": "Media Type" }), + "type": Schema.Literal("base64").annotate({ "title": "Type" }) +}).annotate({ "title": "Base64PDFSource" }) +export type BetaBashCodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "output_file_too_large" +export const BetaBashCodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "output_file_too_large" +]).annotate({ "title": "BashCodeExecutionToolResultErrorCode" }) +export type BetaBillingError = { readonly "message": string; readonly "type": "billing_error" } +export const BetaBillingError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Billing error" }), + "type": Schema.Literal("billing_error").annotate({ "title": "Type", "default": "billing_error" }) +}).annotate({ "title": "BillingError" }) +export type BetaBody_create_skill_v1_skills_post = { + readonly "display_title"?: string | null + readonly "files"?: ReadonlyArray | null +} +export const BetaBody_create_skill_v1_skills_post = Schema.Struct({ + "display_title": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }) + ), + "files": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String.annotate({ "format": "binary" })), Schema.Null]).annotate({ + "title": "Files", + "description": + "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory." + }) + ) +}).annotate({ "title": "Body_create_skill_v1_skills_post" }) +export type BetaBody_create_skill_version_v1_skills__skill_id__versions_post = { + readonly "files"?: ReadonlyArray | null +} +export const BetaBody_create_skill_version_v1_skills__skill_id__versions_post = Schema.Struct({ + "files": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String.annotate({ "format": "binary" })), Schema.Null]).annotate({ + "title": "Files", + "description": + "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory." + }) + ) +}).annotate({ "title": "Body_create_skill_version_v1_skills__skill_id__versions_post" }) +export type BetaCacheControlEphemeral = { readonly "ttl"?: "5m" | "1h"; readonly "type": "ephemeral" } +export const BetaCacheControlEphemeral = Schema.Struct({ + "ttl": Schema.optionalKey( + Schema.Literals(["5m", "1h"]).annotate({ + "title": "Ttl", + "description": + "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`." + }) + ), + "type": Schema.Literal("ephemeral").annotate({ "title": "Type" }) +}).annotate({ "title": "CacheControlEphemeral" }) +export type BetaCacheCreation = { + readonly "ephemeral_1h_input_tokens": number + readonly "ephemeral_5m_input_tokens": number +} +export const BetaCacheCreation = Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.Number.annotate({ + "title": "Ephemeral 1H Input Tokens", + "description": "The number of input tokens used to create the 1 hour cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "ephemeral_5m_input_tokens": Schema.Number.annotate({ + "title": "Ephemeral 5M Input Tokens", + "description": "The number of input tokens used to create the 5 minute cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "CacheCreation" }) +export type BetaCanceledResult = { readonly "type": "canceled" } +export const BetaCanceledResult = Schema.Struct({ + "type": Schema.Literal("canceled").annotate({ "title": "Type", "default": "canceled" }) +}).annotate({ "title": "CanceledResult" }) +export type BetaCodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" +export const BetaCodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded" +]).annotate({ "title": "CodeExecutionToolResultErrorCode" }) +export type BetaCompactionContentBlockDelta = { readonly "content": string | null; readonly "type": "compaction_delta" } +export const BetaCompactionContentBlockDelta = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Content" }), + "type": Schema.Literal("compaction_delta").annotate({ "title": "Type", "default": "compaction_delta" }) +}).annotate({ "title": "CompactionContentBlockDelta" }) +export type BetaContentBlockStopEvent = { readonly "index": number; readonly "type": "content_block_stop" } +export const BetaContentBlockStopEvent = Schema.Struct({ + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_stop").annotate({ "title": "Type", "default": "content_block_stop" }) +}).annotate({ "title": "ContentBlockStopEvent" }) +export type BetaContextManagementResponse = { readonly "original_input_tokens": number } +export const BetaContextManagementResponse = Schema.Struct({ + "original_input_tokens": Schema.Number.annotate({ + "title": "Original Input Tokens", + "description": "The original token count before context management was applied" + }).check(Schema.isInt()) +}).annotate({ "title": "ContextManagementResponse" }) +export type BetaCreateSkillResponse = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const BetaCreateSkillResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "CreateSkillResponse" }) +export type BetaCreateSkillVersionResponse = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const BetaCreateSkillVersionResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "CreateSkillVersionResponse" }) +export type BetaDeleteMessageBatchResponse = { readonly "id": string; readonly "type": "message_batch_deleted" } +export const BetaDeleteMessageBatchResponse = Schema.Struct({ + "id": Schema.String.annotate({ "title": "Id", "description": "ID of the Message Batch." }), + "type": Schema.Literal("message_batch_deleted").annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Message Batches, this is always `\"message_batch_deleted\"`.", + "default": "message_batch_deleted" + }) +}).annotate({ "title": "DeleteMessageBatchResponse" }) +export type BetaDeleteSkillResponse = { readonly "id": string; readonly "type": string } +export const BetaDeleteSkillResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Skills, this is always `\"skill_deleted\"`.", + "default": "skill_deleted" + }) +}).annotate({ "title": "DeleteSkillResponse" }) +export type BetaDeleteSkillVersionResponse = { readonly "id": string; readonly "type": string } +export const BetaDeleteSkillVersionResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Skill Versions, this is always `\"skill_version_deleted\"`.", + "default": "skill_version_deleted" + }) +}).annotate({ "title": "DeleteSkillVersionResponse" }) +export type BetaDirectCaller = { readonly "type": "direct" } +export const BetaDirectCaller = Schema.Struct({ "type": Schema.Literal("direct").annotate({ "title": "Type" }) }) + .annotate({ "title": "DirectCaller", "description": "Tool invocation directly from the model." }) +export type BetaEffortLevel = "low" | "medium" | "high" | "max" +export const BetaEffortLevel = Schema.Literals(["low", "medium", "high", "max"]).annotate({ + "title": "EffortLevel", + "description": "All possible effort levels." +}) +export type BetaExpiredResult = { readonly "type": "expired" } +export const BetaExpiredResult = Schema.Struct({ + "type": Schema.Literal("expired").annotate({ "title": "Type", "default": "expired" }) +}).annotate({ "title": "ExpiredResult" }) +export type BetaFileDeleteResponse = { readonly "id": string; readonly "type"?: "file_deleted" } +export const BetaFileDeleteResponse = Schema.Struct({ + "id": Schema.String.annotate({ "title": "Id", "description": "ID of the deleted file." }), + "type": Schema.optionalKey( + Schema.Literal("file_deleted").annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor file deletion, this is always `\"file_deleted\"`.", + "default": "file_deleted" + }) + ) +}).annotate({ "title": "FileDeleteResponse" }) +export type BetaFileDocumentSource = { readonly "file_id": string; readonly "type": "file" } +export const BetaFileDocumentSource = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("file").annotate({ "title": "Type" }) +}).annotate({ "title": "FileDocumentSource" }) +export type BetaFileImageSource = { readonly "file_id": string; readonly "type": "file" } +export const BetaFileImageSource = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("file").annotate({ "title": "Type" }) +}).annotate({ "title": "FileImageSource" }) +export type BetaFileMetadataSchema = { + readonly "created_at": string + readonly "downloadable"?: boolean + readonly "filename": string + readonly "id": string + readonly "mime_type": string + readonly "size_bytes": number + readonly "type": "file" +} +export const BetaFileMetadataSchema = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "RFC 3339 datetime string representing when the file was created.", + "format": "date-time" + }), + "downloadable": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Downloadable", + "description": "Whether the file can be downloaded.", + "default": false + }) + ), + "filename": Schema.String.annotate({ "title": "Filename", "description": "Original filename of the uploaded file." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "mime_type": Schema.String.annotate({ "title": "Mime Type", "description": "MIME type of the file." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(255)), + "size_bytes": Schema.Number.annotate({ "title": "Size Bytes", "description": "Size of the file in bytes." }).check( + Schema.isInt() + ).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("file").annotate({ + "title": "Type", + "description": "Object type.\n\nFor files, this is always `\"file\"`." + }) +}).annotate({ "title": "FileMetadataSchema" }) +export type BetaGatewayTimeoutError = { readonly "message": string; readonly "type": "timeout_error" } +export const BetaGatewayTimeoutError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Request timeout" }), + "type": Schema.Literal("timeout_error").annotate({ "title": "Type", "default": "timeout_error" }) +}).annotate({ "title": "GatewayTimeoutError" }) +export type BetaGetSkillResponse = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const BetaGetSkillResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "GetSkillResponse" }) +export type BetaGetSkillVersionResponse = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const BetaGetSkillVersionResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "GetSkillVersionResponse" }) +export type BetaInputJsonContentBlockDelta = { readonly "partial_json": string; readonly "type": "input_json_delta" } +export const BetaInputJsonContentBlockDelta = Schema.Struct({ + "partial_json": Schema.String.annotate({ "title": "Partial Json" }), + "type": Schema.Literal("input_json_delta").annotate({ "title": "Type", "default": "input_json_delta" }) +}).annotate({ "title": "InputJsonContentBlockDelta" }) +export type BetaInputTokensClearAtLeast = { readonly "type": "input_tokens"; readonly "value": number } +export const BetaInputTokensClearAtLeast = Schema.Struct({ + "type": Schema.Literal("input_tokens").annotate({ "title": "Type" }), + "value": Schema.Number.annotate({ "title": "Value" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "InputTokensClearAtLeast" }) +export type BetaInputTokensTrigger = { readonly "type": "input_tokens"; readonly "value": number } +export const BetaInputTokensTrigger = Schema.Struct({ + "type": Schema.Literal("input_tokens").annotate({ "title": "Type" }), + "value": Schema.Number.annotate({ "title": "Value" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) +}).annotate({ "title": "InputTokensTrigger" }) +export type BetaInvalidRequestError = { readonly "message": string; readonly "type": "invalid_request_error" } +export const BetaInvalidRequestError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Invalid request" }), + "type": Schema.Literal("invalid_request_error").annotate({ "title": "Type", "default": "invalid_request_error" }) +}).annotate({ "title": "InvalidRequestError" }) +export type BetaJsonOutputFormat = { + readonly "schema": { readonly [x: string]: Schema.Json } + readonly "type": "json_schema" +} +export const BetaJsonOutputFormat = Schema.Struct({ + "schema": Schema.Record(Schema.String, Schema.Json).annotate({ + "title": "Schema", + "description": "The JSON schema of the format" + }), + "type": Schema.Literal("json_schema").annotate({ "title": "Type" }) +}).annotate({ "title": "JsonOutputFormat" }) +export type BetaJsonValue = unknown +export const BetaJsonValue = Schema.Unknown +export type BetaMCPToolConfig = { readonly "defer_loading"?: boolean; readonly "enabled"?: boolean } +export const BetaMCPToolConfig = Schema.Struct({ + "defer_loading": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Defer Loading" })), + "enabled": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Enabled" })) +}).annotate({ "title": "MCPToolConfig", "description": "Configuration for a specific tool in an MCP toolset." }) +export type BetaMessageBatch = { + readonly "archived_at": string | null + readonly "cancel_initiated_at": string | null + readonly "created_at": string + readonly "ended_at": string | null + readonly "expires_at": string + readonly "id": string + readonly "processing_status": "in_progress" | "canceling" | "ended" + readonly "request_counts": { + readonly "canceled": number + readonly "errored": number + readonly "expired": number + readonly "processing": number + readonly "succeeded": number + } + readonly "results_url": string | null + readonly "type": "message_batch" +} +export const BetaMessageBatch = Schema.Struct({ + "archived_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Archived At", + "description": + "RFC 3339 datetime string representing the time at which the Message Batch was archived and its results became unavailable." + }), + "cancel_initiated_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Cancel Initiated At", + "description": + "RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated." + }), + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "RFC 3339 datetime string representing the time at which the Message Batch was created.", + "format": "date-time" + }), + "ended_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Ended At", + "description": + "RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends.\n\nProcessing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired." + }), + "expires_at": Schema.String.annotate({ + "title": "Expires At", + "description": + "RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation.", + "format": "date-time" + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "processing_status": Schema.Literals(["in_progress", "canceling", "ended"]).annotate({ + "title": "Processing Status", + "description": "Processing status of the Message Batch." + }), + "request_counts": Schema.Struct({ + "canceled": Schema.Number.annotate({ + "title": "Canceled", + "description": + "Number of requests in the Message Batch that have been canceled.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "errored": Schema.Number.annotate({ + "title": "Errored", + "description": + "Number of requests in the Message Batch that encountered an error.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "expired": Schema.Number.annotate({ + "title": "Expired", + "description": + "Number of requests in the Message Batch that have expired.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "processing": Schema.Number.annotate({ + "title": "Processing", + "description": "Number of requests in the Message Batch that are processing.", + "default": 0 + }).check(Schema.isInt()), + "succeeded": Schema.Number.annotate({ + "title": "Succeeded", + "description": + "Number of requests in the Message Batch that have completed successfully.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()) + }).annotate({ + "title": "RequestCounts", + "description": + "Tallies requests within the Message Batch, categorized by their status.\n\nRequests start as `processing` and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch." + }), + "results_url": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Results Url", + "description": + "URL to a `.jsonl` file containing the results of the Message Batch requests. Specified only once processing ends.\n\nResults in the file are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests." + }), + "type": Schema.Literal("message_batch").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Message Batches, this is always `\"message_batch\"`.", + "default": "message_batch" + }) +}).annotate({ "title": "MessageBatch" }) +export type BetaMessageStopEvent = { readonly "type": "message_stop" } +export const BetaMessageStopEvent = Schema.Struct({ + "type": Schema.Literal("message_stop").annotate({ "title": "Type", "default": "message_stop" }) +}).annotate({ "title": "MessageStopEvent" }) +export type BetaModelInfo = { + readonly "created_at": string + readonly "display_name": string + readonly "id": string + readonly "type": "model" +} +export const BetaModelInfo = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": + "RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown.", + "format": "date-time" + }), + "display_name": Schema.String.annotate({ + "title": "Display Name", + "description": "A human-readable name for the model." + }), + "id": Schema.String.annotate({ "title": "Id", "description": "Unique model identifier." }), + "type": Schema.Literal("model").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Models, this is always `\"model\"`.", + "default": "model" + }) +}).annotate({ "title": "ModelInfo" }) +export type BetaNotFoundError = { readonly "message": string; readonly "type": "not_found_error" } +export const BetaNotFoundError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Not found" }), + "type": Schema.Literal("not_found_error").annotate({ "title": "Type", "default": "not_found_error" }) +}).annotate({ "title": "NotFoundError" }) +export type BetaOverloadedError = { readonly "message": string; readonly "type": "overloaded_error" } +export const BetaOverloadedError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Overloaded" }), + "type": Schema.Literal("overloaded_error").annotate({ "title": "Type", "default": "overloaded_error" }) +}).annotate({ "title": "OverloadedError" }) +export type BetaPermissionError = { readonly "message": string; readonly "type": "permission_error" } +export const BetaPermissionError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Permission denied" }), + "type": Schema.Literal("permission_error").annotate({ "title": "Type", "default": "permission_error" }) +}).annotate({ "title": "PermissionError" }) +export type BetaPlainTextSource = { + readonly "data": string + readonly "media_type": "text/plain" + readonly "type": "text" +} +export const BetaPlainTextSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "media_type": Schema.Literal("text/plain").annotate({ "title": "Media Type" }), + "type": Schema.Literal("text").annotate({ "title": "Type" }) +}).annotate({ "title": "PlainTextSource" }) +export type BetaRateLimitError = { readonly "message": string; readonly "type": "rate_limit_error" } +export const BetaRateLimitError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Rate limited" }), + "type": Schema.Literal("rate_limit_error").annotate({ "title": "Type", "default": "rate_limit_error" }) +}).annotate({ "title": "RateLimitError" }) +export type BetaRequestBashCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "bash_code_execution_output" +} +export const BetaRequestBashCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("bash_code_execution_output").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionOutputBlock" }) +export type BetaRequestCharLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "start_char_index": number + readonly "type": "char_location" +} +export const BetaRequestCharLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_char_index": Schema.Number.annotate({ "title": "End Char Index" }).check(Schema.isInt()), + "start_char_index": Schema.Number.annotate({ "title": "Start Char Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("char_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCharLocationCitation" }) +export type BetaRequestCitationsConfig = { readonly "enabled"?: boolean } +export const BetaRequestCitationsConfig = Schema.Struct({ + "enabled": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Enabled" })) +}).annotate({ "title": "RequestCitationsConfig" }) +export type BetaRequestCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "code_execution_output" +} +export const BetaRequestCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("code_execution_output").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionOutputBlock" }) +export type BetaRequestContentBlockLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const BetaRequestContentBlockLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("content_block_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestContentBlockLocationCitation" }) +export type BetaRequestMCPServerToolConfiguration = { + readonly "allowed_tools"?: ReadonlyArray | null + readonly "enabled"?: boolean | null +} +export const BetaRequestMCPServerToolConfiguration = Schema.Struct({ + "allowed_tools": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Allowed Tools" }) + ), + "enabled": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null]).annotate({ "title": "Enabled" })) +}).annotate({ "title": "RequestMCPServerToolConfiguration" }) +export type BetaRequestPageLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "start_page_number": number + readonly "type": "page_location" +} +export const BetaRequestPageLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_page_number": Schema.Number.annotate({ "title": "End Page Number" }).check(Schema.isInt()), + "start_page_number": Schema.Number.annotate({ "title": "Start Page Number" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(1) + ), + "type": Schema.Literal("page_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestPageLocationCitation" }) +export type BetaRequestSearchResultLocationCitation = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const BetaRequestSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "search_result_index": Schema.Number.annotate({ "title": "Search Result Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "source": Schema.String.annotate({ "title": "Source" }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("search_result_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestSearchResultLocationCitation" }) +export type BetaRequestTextEditorCodeExecutionCreateResultBlock = { + readonly "is_file_update": boolean + readonly "type": "text_editor_code_execution_create_result" +} +export const BetaRequestTextEditorCodeExecutionCreateResultBlock = Schema.Struct({ + "is_file_update": Schema.Boolean.annotate({ "title": "Is File Update" }), + "type": Schema.Literal("text_editor_code_execution_create_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionCreateResultBlock" }) +export type BetaRequestTextEditorCodeExecutionStrReplaceResultBlock = { + readonly "lines"?: ReadonlyArray | null + readonly "new_lines"?: number | null + readonly "new_start"?: number | null + readonly "old_lines"?: number | null + readonly "old_start"?: number | null + readonly "type": "text_editor_code_execution_str_replace_result" +} +export const BetaRequestTextEditorCodeExecutionStrReplaceResultBlock = Schema.Struct({ + "lines": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Lines" })), + "new_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "New Lines" }) + ), + "new_start": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "New Start" }) + ), + "old_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Old Lines" }) + ), + "old_start": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Old Start" }) + ), + "type": Schema.Literal("text_editor_code_execution_str_replace_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionStrReplaceResultBlock" }) +export type BetaRequestTextEditorCodeExecutionViewResultBlock = { + readonly "content": string + readonly "file_type": "text" | "image" | "pdf" + readonly "num_lines"?: number | null + readonly "start_line"?: number | null + readonly "total_lines"?: number | null + readonly "type": "text_editor_code_execution_view_result" +} +export const BetaRequestTextEditorCodeExecutionViewResultBlock = Schema.Struct({ + "content": Schema.String.annotate({ "title": "Content" }), + "file_type": Schema.Literals(["text", "image", "pdf"]).annotate({ "title": "File Type" }), + "num_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Num Lines" }) + ), + "start_line": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Start Line" }) + ), + "total_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Total Lines" }) + ), + "type": Schema.Literal("text_editor_code_execution_view_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionViewResultBlock" }) +export type BetaRequestWebSearchResultBlock = { + readonly "encrypted_content": string + readonly "page_age"?: string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string +} +export const BetaRequestWebSearchResultBlock = Schema.Struct({ + "encrypted_content": Schema.String.annotate({ "title": "Encrypted Content" }), + "page_age": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Page Age" })), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "RequestWebSearchResultBlock" }) +export type BetaRequestWebSearchResultLocationCitation = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" + readonly "url": string +} +export const BetaRequestWebSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "encrypted_index": Schema.String.annotate({ "title": "Encrypted Index" }), + "title": Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(512)), Schema.Null]) + .annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result_location").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2048)) +}).annotate({ "title": "RequestWebSearchResultLocationCitation" }) +export type BetaResponseBashCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "bash_code_execution_output" +} +export const BetaResponseBashCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("bash_code_execution_output").annotate({ + "title": "Type", + "default": "bash_code_execution_output" + }) +}).annotate({ "title": "ResponseBashCodeExecutionOutputBlock" }) +export type BetaResponseCharLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "file_id": string | null + readonly "start_char_index": number + readonly "type": "char_location" +} +export const BetaResponseCharLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_char_index": Schema.Number.annotate({ "title": "End Char Index" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_char_index": Schema.Number.annotate({ "title": "Start Char Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("char_location").annotate({ "title": "Type", "default": "char_location" }) +}).annotate({ "title": "ResponseCharLocationCitation" }) +export type BetaResponseCitationsConfig = { readonly "enabled": boolean } +export const BetaResponseCitationsConfig = Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "title": "Enabled", "default": false }) +}).annotate({ "title": "ResponseCitationsConfig" }) +export type BetaResponseClearThinking20251015Edit = { + readonly "cleared_input_tokens": number + readonly "cleared_thinking_turns": number + readonly "type": "clear_thinking_20251015" +} +export const BetaResponseClearThinking20251015Edit = Schema.Struct({ + "cleared_input_tokens": Schema.Number.annotate({ + "title": "Cleared Input Tokens", + "description": "Number of input tokens cleared by this edit." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "cleared_thinking_turns": Schema.Number.annotate({ + "title": "Cleared Thinking Turns", + "description": "Number of thinking turns that were cleared." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("clear_thinking_20251015").annotate({ + "title": "Type", + "description": "The type of context management edit applied.", + "default": "clear_thinking_20251015" + }) +}).annotate({ "title": "ResponseClearThinking20251015Edit" }) +export type BetaResponseClearToolUses20250919Edit = { + readonly "cleared_input_tokens": number + readonly "cleared_tool_uses": number + readonly "type": "clear_tool_uses_20250919" +} +export const BetaResponseClearToolUses20250919Edit = Schema.Struct({ + "cleared_input_tokens": Schema.Number.annotate({ + "title": "Cleared Input Tokens", + "description": "Number of input tokens cleared by this edit." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "cleared_tool_uses": Schema.Number.annotate({ + "title": "Cleared Tool Uses", + "description": "Number of tool uses that were cleared." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("clear_tool_uses_20250919").annotate({ + "title": "Type", + "description": "The type of context management edit applied.", + "default": "clear_tool_uses_20250919" + }) +}).annotate({ "title": "ResponseClearToolUses20250919Edit" }) +export type BetaResponseCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "code_execution_output" +} +export const BetaResponseCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("code_execution_output").annotate({ "title": "Type", "default": "code_execution_output" }) +}).annotate({ "title": "ResponseCodeExecutionOutputBlock" }) +export type BetaResponseCompactionBlock = { readonly "content": string | null; readonly "type": "compaction" } +export const BetaResponseCompactionBlock = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Content", + "description": "Summary of compacted content, or null if compaction failed" + }), + "type": Schema.Literal("compaction").annotate({ "title": "Type", "default": "compaction" }) +}).annotate({ + "title": "ResponseCompactionBlock", + "description": + "A compaction block returned when autocompact is triggered.\n\nWhen content is None, it indicates the compaction failed to produce a valid\nsummary (e.g., malformed output from the model). Clients may round-trip\ncompaction blocks with null content; the server treats them as no-ops." +}) +export type BetaResponseContainerUploadBlock = { readonly "file_id": string; readonly "type": "container_upload" } +export const BetaResponseContainerUploadBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("container_upload").annotate({ "title": "Type", "default": "container_upload" }) +}).annotate({ + "title": "ResponseContainerUploadBlock", + "description": "Response model for a file uploaded to the container." +}) +export type BetaResponseContentBlockLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "file_id": string | null + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const BetaResponseContentBlockLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("content_block_location").annotate({ "title": "Type", "default": "content_block_location" }) +}).annotate({ "title": "ResponseContentBlockLocationCitation" }) +export type BetaResponseMCPToolUseBlock = { + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "server_name": string + readonly "type": "mcp_tool_use" +} +export const BetaResponseMCPToolUseBlock = Schema.Struct({ + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name", "description": "The name of the MCP tool" }), + "server_name": Schema.String.annotate({ "title": "Server Name", "description": "The name of the MCP server" }), + "type": Schema.Literal("mcp_tool_use").annotate({ "title": "Type", "default": "mcp_tool_use" }) +}).annotate({ "title": "ResponseMCPToolUseBlock" }) +export type BetaResponsePageLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "file_id": string | null + readonly "start_page_number": number + readonly "type": "page_location" +} +export const BetaResponsePageLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_page_number": Schema.Number.annotate({ "title": "End Page Number" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_page_number": Schema.Number.annotate({ "title": "Start Page Number" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(1) + ), + "type": Schema.Literal("page_location").annotate({ "title": "Type", "default": "page_location" }) +}).annotate({ "title": "ResponsePageLocationCitation" }) +export type BetaResponseRedactedThinkingBlock = { readonly "data": string; readonly "type": "redacted_thinking" } +export const BetaResponseRedactedThinkingBlock = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "type": Schema.Literal("redacted_thinking").annotate({ "title": "Type", "default": "redacted_thinking" }) +}).annotate({ "title": "ResponseRedactedThinkingBlock" }) +export type BetaResponseSearchResultLocationCitation = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const BetaResponseSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "search_result_index": Schema.Number.annotate({ "title": "Search Result Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "source": Schema.String.annotate({ "title": "Source" }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("search_result_location").annotate({ "title": "Type", "default": "search_result_location" }) +}).annotate({ "title": "ResponseSearchResultLocationCitation" }) +export type BetaResponseTextEditorCodeExecutionCreateResultBlock = { + readonly "is_file_update": boolean + readonly "type": "text_editor_code_execution_create_result" +} +export const BetaResponseTextEditorCodeExecutionCreateResultBlock = Schema.Struct({ + "is_file_update": Schema.Boolean.annotate({ "title": "Is File Update" }), + "type": Schema.Literal("text_editor_code_execution_create_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_create_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionCreateResultBlock" }) +export type BetaResponseTextEditorCodeExecutionStrReplaceResultBlock = { + readonly "lines": ReadonlyArray | null + readonly "new_lines": number | null + readonly "new_start": number | null + readonly "old_lines": number | null + readonly "old_start": number | null + readonly "type": "text_editor_code_execution_str_replace_result" +} +export const BetaResponseTextEditorCodeExecutionStrReplaceResultBlock = Schema.Struct({ + "lines": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Lines", "default": null }), + "new_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "New Lines", + "default": null + }), + "new_start": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "New Start", + "default": null + }), + "old_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Old Lines", + "default": null + }), + "old_start": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Old Start", + "default": null + }), + "type": Schema.Literal("text_editor_code_execution_str_replace_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_str_replace_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionStrReplaceResultBlock" }) +export type BetaResponseTextEditorCodeExecutionViewResultBlock = { + readonly "content": string + readonly "file_type": "text" | "image" | "pdf" + readonly "num_lines": number | null + readonly "start_line": number | null + readonly "total_lines": number | null + readonly "type": "text_editor_code_execution_view_result" +} +export const BetaResponseTextEditorCodeExecutionViewResultBlock = Schema.Struct({ + "content": Schema.String.annotate({ "title": "Content" }), + "file_type": Schema.Literals(["text", "image", "pdf"]).annotate({ "title": "File Type" }), + "num_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Num Lines", + "default": null + }), + "start_line": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Start Line", + "default": null + }), + "total_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Total Lines", + "default": null + }), + "type": Schema.Literal("text_editor_code_execution_view_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_view_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionViewResultBlock" }) +export type BetaResponseThinkingBlock = { + readonly "signature": string + readonly "thinking": string + readonly "type": "thinking" +} +export const BetaResponseThinkingBlock = Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking").annotate({ "title": "Type", "default": "thinking" }) +}).annotate({ "title": "ResponseThinkingBlock" }) +export type BetaResponseToolReferenceBlock = { readonly "tool_name": string; readonly "type": "tool_reference" } +export const BetaResponseToolReferenceBlock = Schema.Struct({ + "tool_name": Schema.String.annotate({ "title": "Tool Name" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(256) + ).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,256}$"))), + "type": Schema.Literal("tool_reference").annotate({ "title": "Type", "default": "tool_reference" }) +}).annotate({ "title": "ResponseToolReferenceBlock" }) +export type BetaResponseWebSearchResultBlock = { + readonly "encrypted_content": string + readonly "page_age": string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string +} +export const BetaResponseWebSearchResultBlock = Schema.Struct({ + "encrypted_content": Schema.String.annotate({ "title": "Encrypted Content" }), + "page_age": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Page Age", "default": null }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result").annotate({ "title": "Type", "default": "web_search_result" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "ResponseWebSearchResultBlock" }) +export type BetaResponseWebSearchResultLocationCitation = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" + readonly "url": string +} +export const BetaResponseWebSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "encrypted_index": Schema.String.annotate({ "title": "Encrypted Index" }), + "title": Schema.Union([Schema.String.check(Schema.isMaxLength(512)), Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result_location").annotate({ + "title": "Type", + "default": "web_search_result_location" + }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "ResponseWebSearchResultLocationCitation" }) +export type BetaServerToolCaller = { readonly "tool_id": string; readonly "type": "code_execution_20250825" } +export const BetaServerToolCaller = Schema.Struct({ + "tool_id": Schema.String.annotate({ "title": "Tool Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_20250825").annotate({ "title": "Type" }) +}).annotate({ "title": "ServerToolCaller", "description": "Tool invocation generated by a server-side tool." }) +export type BetaServerToolCaller_20260120 = { readonly "tool_id": string; readonly "type": "code_execution_20260120" } +export const BetaServerToolCaller_20260120 = Schema.Struct({ + "tool_id": Schema.String.annotate({ "title": "Tool Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_20260120").annotate({ "title": "Type" }) +}).annotate({ "title": "ServerToolCaller_20260120" }) +export type BetaServerToolUsage = { readonly "web_fetch_requests": number; readonly "web_search_requests": number } +export const BetaServerToolUsage = Schema.Struct({ + "web_fetch_requests": Schema.Number.annotate({ + "title": "Web Fetch Requests", + "description": "The number of web fetch tool requests.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "web_search_requests": Schema.Number.annotate({ + "title": "Web Search Requests", + "description": "The number of web search tool requests.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "ServerToolUsage" }) +export type BetaSignatureContentBlockDelta = { readonly "signature": string; readonly "type": "signature_delta" } +export const BetaSignatureContentBlockDelta = Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "type": Schema.Literal("signature_delta").annotate({ "title": "Type", "default": "signature_delta" }) +}).annotate({ "title": "SignatureContentBlockDelta" }) +export type BetaSkill = { + readonly "skill_id": string + readonly "type": "anthropic" | "custom" + readonly "version": string +} +export const BetaSkill = Schema.Struct({ + "skill_id": Schema.String.annotate({ "title": "Skill Id", "description": "Skill ID" }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)), + "type": Schema.Literals(["anthropic", "custom"]).annotate({ + "title": "Type", + "description": "Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined)" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": "Skill version or 'latest' for most recent version" + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)) +}).annotate({ "title": "Skill", "description": "A skill that was loaded in a container (response model)." }) +export type BetaSkillParams = { + readonly "skill_id": string + readonly "type": "anthropic" | "custom" + readonly "version"?: string +} +export const BetaSkillParams = Schema.Struct({ + "skill_id": Schema.String.annotate({ "title": "Skill Id", "description": "Skill ID" }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)), + "type": Schema.Literals(["anthropic", "custom"]).annotate({ + "title": "Type", + "description": "Type of skill - either 'anthropic' (built-in) or 'custom' (user-defined)" + }), + "version": Schema.optionalKey( + Schema.String.annotate({ "title": "Version", "description": "Skill version or 'latest' for most recent version" }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)) + ) +}).annotate({ + "title": "SkillParams", + "description": "Specification for a skill to be loaded in a container (request model)." +}) +export type BetaSkillVersion = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const BetaSkillVersion = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "SkillVersion" }) +export type BetaSpeed = "standard" | "fast" +export const BetaSpeed = Schema.Literals(["standard", "fast"]).annotate({ "title": "Speed" }) +export type BetaTextContentBlockDelta = { readonly "text": string; readonly "type": "text_delta" } +export const BetaTextContentBlockDelta = Schema.Struct({ + "text": Schema.String.annotate({ "title": "Text" }), + "type": Schema.Literal("text_delta").annotate({ "title": "Type", "default": "text_delta" }) +}).annotate({ "title": "TextContentBlockDelta" }) +export type BetaTextEditorCodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "file_not_found" +export const BetaTextEditorCodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "file_not_found" +]).annotate({ "title": "TextEditorCodeExecutionToolResultErrorCode" }) +export type BetaThinkingConfigAdaptive = { readonly "type": "adaptive" } +export const BetaThinkingConfigAdaptive = Schema.Struct({ + "type": Schema.Literal("adaptive").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigAdaptive" }) +export type BetaThinkingConfigDisabled = { readonly "type": "disabled" } +export const BetaThinkingConfigDisabled = Schema.Struct({ + "type": Schema.Literal("disabled").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigDisabled" }) +export type BetaThinkingConfigEnabled = { readonly "budget_tokens": number; readonly "type": "enabled" } +export const BetaThinkingConfigEnabled = Schema.Struct({ + "budget_tokens": Schema.Number.annotate({ + "title": "Budget Tokens", + "description": + "Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality.\n\nMust be ≥1024 and less than `max_tokens`.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1024)), + "type": Schema.Literal("enabled").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigEnabled" }) +export type BetaThinkingContentBlockDelta = { readonly "thinking": string; readonly "type": "thinking_delta" } +export const BetaThinkingContentBlockDelta = Schema.Struct({ + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking_delta").annotate({ "title": "Type", "default": "thinking_delta" }) +}).annotate({ "title": "ThinkingContentBlockDelta" }) +export type BetaThinkingTurns = { readonly "type": "thinking_turns"; readonly "value": number } +export const BetaThinkingTurns = Schema.Struct({ + "type": Schema.Literal("thinking_turns").annotate({ "title": "Type" }), + "value": Schema.Number.annotate({ "title": "Value" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) +}).annotate({ "title": "ThinkingTurns" }) +export type BetaToolChoiceAny = { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "any" } +export const BetaToolChoiceAny = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output exactly one tool use." + }) + ), + "type": Schema.Literal("any").annotate({ "title": "Type" }) +}).annotate({ "title": "ToolChoiceAny", "description": "The model will use any available tools." }) +export type BetaToolChoiceAuto = { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "auto" } +export const BetaToolChoiceAuto = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output at most one tool use." + }) + ), + "type": Schema.Literal("auto").annotate({ "title": "Type" }) +}).annotate({ "title": "ToolChoiceAuto", "description": "The model will automatically decide whether to use tools." }) +export type BetaToolChoiceNone = { readonly "type": "none" } +export const BetaToolChoiceNone = Schema.Struct({ "type": Schema.Literal("none").annotate({ "title": "Type" }) }) + .annotate({ "title": "ToolChoiceNone", "description": "The model will not be allowed to use tools." }) +export type BetaToolChoiceTool = { + readonly "disable_parallel_tool_use"?: boolean + readonly "name": string + readonly "type": "tool" +} +export const BetaToolChoiceTool = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output exactly one tool use." + }) + ), + "name": Schema.String.annotate({ "title": "Name", "description": "The name of the tool to use." }), + "type": Schema.Literal("tool").annotate({ "title": "Type" }) +}).annotate({ + "title": "ToolChoiceTool", + "description": "The model will use the specified tool with `tool_choice.name`." +}) +export type BetaToolSearchToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" +export const BetaToolSearchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded" +]).annotate({ "title": "ToolSearchToolResultErrorCode" }) +export type BetaToolUsesKeep = { readonly "type": "tool_uses"; readonly "value": number } +export const BetaToolUsesKeep = Schema.Struct({ + "type": Schema.Literal("tool_uses").annotate({ "title": "Type" }), + "value": Schema.Number.annotate({ "title": "Value" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "ToolUsesKeep" }) +export type BetaToolUsesTrigger = { readonly "type": "tool_uses"; readonly "value": number } +export const BetaToolUsesTrigger = Schema.Struct({ + "type": Schema.Literal("tool_uses").annotate({ "title": "Type" }), + "value": Schema.Number.annotate({ "title": "Value" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) +}).annotate({ "title": "ToolUsesTrigger" }) +export type BetaURLImageSource = { readonly "type": "url"; readonly "url": string } +export const BetaURLImageSource = Schema.Struct({ + "type": Schema.Literal("url").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "URLImageSource" }) +export type BetaURLPDFSource = { readonly "type": "url"; readonly "url": string } +export const BetaURLPDFSource = Schema.Struct({ + "type": Schema.Literal("url").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "URLPDFSource" }) +export type BetaUserLocation = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type": "approximate" +} +export const BetaUserLocation = Schema.Struct({ + "city": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "City", + "description": "The city of the user." + }) + ), + "country": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(2)).check(Schema.isMaxLength(2)), Schema.Null]).annotate({ + "title": "Country", + "description": "The two letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of the user." + }) + ), + "region": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "Region", + "description": "The region of the user." + }) + ), + "timezone": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "Timezone", + "description": "The [IANA timezone](https://nodatime.org/TimeZones) of the user." + }) + ), + "type": Schema.Literal("approximate").annotate({ "title": "Type" }) +}).annotate({ "title": "UserLocation" }) +export type BetaWebFetchToolResultErrorCode = + | "invalid_tool_input" + | "url_too_long" + | "url_not_allowed" + | "url_not_accessible" + | "unsupported_content_type" + | "too_many_requests" + | "max_uses_exceeded" + | "unavailable" +export const BetaWebFetchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "url_too_long", + "url_not_allowed", + "url_not_accessible", + "unsupported_content_type", + "too_many_requests", + "max_uses_exceeded", + "unavailable" +]).annotate({ "title": "WebFetchToolResultErrorCode" }) +export type BetaWebSearchToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + | "request_too_large" +export const BetaWebSearchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long", + "request_too_large" +]).annotate({ "title": "WebSearchToolResultErrorCode" }) +export type Betaapi__schemas__skills__Skill = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const Betaapi__schemas__skills__Skill = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "Skill" }) +export type BillingError = { readonly "message": string; readonly "type": "billing_error" } +export const BillingError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Billing error" }), + "type": Schema.Literal("billing_error").annotate({ "title": "Type", "default": "billing_error" }) +}).annotate({ "title": "BillingError" }) +export type Body_create_skill_v1_skills_post = { + readonly "display_title"?: string | null + readonly "files"?: ReadonlyArray | null +} +export const Body_create_skill_v1_skills_post = Schema.Struct({ + "display_title": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }) + ), + "files": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String.annotate({ "format": "binary" })), Schema.Null]).annotate({ + "title": "Files", + "description": + "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory." + }) + ) +}).annotate({ "title": "Body_create_skill_v1_skills_post" }) +export type Body_create_skill_version_v1_skills__skill_id__versions_post = { + readonly "files"?: ReadonlyArray | null +} +export const Body_create_skill_version_v1_skills__skill_id__versions_post = Schema.Struct({ + "files": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String.annotate({ "format": "binary" })), Schema.Null]).annotate({ + "title": "Files", + "description": + "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory." + }) + ) +}).annotate({ "title": "Body_create_skill_version_v1_skills__skill_id__versions_post" }) +export type CacheControlEphemeral = { readonly "ttl"?: "5m" | "1h"; readonly "type": "ephemeral" } +export const CacheControlEphemeral = Schema.Struct({ + "ttl": Schema.optionalKey( + Schema.Literals(["5m", "1h"]).annotate({ + "title": "Ttl", + "description": + "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`." + }) + ), + "type": Schema.Literal("ephemeral").annotate({ "title": "Type" }) +}).annotate({ "title": "CacheControlEphemeral" }) +export type CacheCreation = { + readonly "ephemeral_1h_input_tokens": number + readonly "ephemeral_5m_input_tokens": number +} +export const CacheCreation = Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.Number.annotate({ + "title": "Ephemeral 1H Input Tokens", + "description": "The number of input tokens used to create the 1 hour cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "ephemeral_5m_input_tokens": Schema.Number.annotate({ + "title": "Ephemeral 5M Input Tokens", + "description": "The number of input tokens used to create the 5 minute cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "CacheCreation" }) +export type CanceledResult = { readonly "type": "canceled" } +export const CanceledResult = Schema.Struct({ + "type": Schema.Literal("canceled").annotate({ "title": "Type", "default": "canceled" }) +}).annotate({ "title": "CanceledResult" }) +export type CodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" +export const CodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded" +]).annotate({ "title": "CodeExecutionToolResultErrorCode" }) +export type Container = { readonly "expires_at": string; readonly "id": string } +export const Container = Schema.Struct({ + "expires_at": Schema.String.annotate({ + "title": "Expires At", + "description": "The time at which the container will expire.", + "format": "date-time" + }), + "id": Schema.String.annotate({ "title": "Id", "description": "Identifier for the container used in this request" }) +}).annotate({ + "title": "Container", + "description": "Information about the container used in the request (for the code execution tool)" +}) +export type ContentBlockStopEvent = { readonly "index": number; readonly "type": "content_block_stop" } +export const ContentBlockStopEvent = Schema.Struct({ + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_stop").annotate({ "title": "Type", "default": "content_block_stop" }) +}).annotate({ "title": "ContentBlockStopEvent" }) +export type CountMessageTokensResponse = { readonly "input_tokens": number } +export const CountMessageTokensResponse = Schema.Struct({ + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The total number of tokens across the provided list of messages, system prompt, and tools." + }).check(Schema.isInt()) +}).annotate({ "title": "CountMessageTokensResponse" }) +export type CreateSkillResponse = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const CreateSkillResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "CreateSkillResponse" }) +export type CreateSkillVersionResponse = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const CreateSkillVersionResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "CreateSkillVersionResponse" }) +export type DeleteMessageBatchResponse = { readonly "id": string; readonly "type": "message_batch_deleted" } +export const DeleteMessageBatchResponse = Schema.Struct({ + "id": Schema.String.annotate({ "title": "Id", "description": "ID of the Message Batch." }), + "type": Schema.Literal("message_batch_deleted").annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Message Batches, this is always `\"message_batch_deleted\"`.", + "default": "message_batch_deleted" + }) +}).annotate({ "title": "DeleteMessageBatchResponse" }) +export type DeleteSkillResponse = { readonly "id": string; readonly "type": string } +export const DeleteSkillResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Skills, this is always `\"skill_deleted\"`.", + "default": "skill_deleted" + }) +}).annotate({ "title": "DeleteSkillResponse" }) +export type DeleteSkillVersionResponse = { readonly "id": string; readonly "type": string } +export const DeleteSkillVersionResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor Skill Versions, this is always `\"skill_version_deleted\"`.", + "default": "skill_version_deleted" + }) +}).annotate({ "title": "DeleteSkillVersionResponse" }) +export type DirectCaller = { readonly "type": "direct" } +export const DirectCaller = Schema.Struct({ "type": Schema.Literal("direct").annotate({ "title": "Type" }) }).annotate({ + "title": "DirectCaller", + "description": "Tool invocation directly from the model." +}) +export type EffortLevel = "low" | "medium" | "high" | "max" +export const EffortLevel = Schema.Literals(["low", "medium", "high", "max"]).annotate({ + "title": "EffortLevel", + "description": "All possible effort levels." +}) +export type ExpiredResult = { readonly "type": "expired" } +export const ExpiredResult = Schema.Struct({ + "type": Schema.Literal("expired").annotate({ "title": "Type", "default": "expired" }) +}).annotate({ "title": "ExpiredResult" }) +export type FileDeleteResponse = { readonly "id": string; readonly "type"?: "file_deleted" } +export const FileDeleteResponse = Schema.Struct({ + "id": Schema.String.annotate({ "title": "Id", "description": "ID of the deleted file." }), + "type": Schema.optionalKey( + Schema.Literal("file_deleted").annotate({ + "title": "Type", + "description": "Deleted object type.\n\nFor file deletion, this is always `\"file_deleted\"`.", + "default": "file_deleted" + }) + ) +}).annotate({ "title": "FileDeleteResponse" }) +export type FileMetadataSchema = { + readonly "created_at": string + readonly "downloadable"?: boolean + readonly "filename": string + readonly "id": string + readonly "mime_type": string + readonly "size_bytes": number + readonly "type": "file" +} +export const FileMetadataSchema = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "RFC 3339 datetime string representing when the file was created.", + "format": "date-time" + }), + "downloadable": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Downloadable", + "description": "Whether the file can be downloaded.", + "default": false + }) + ), + "filename": Schema.String.annotate({ "title": "Filename", "description": "Original filename of the uploaded file." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "mime_type": Schema.String.annotate({ "title": "Mime Type", "description": "MIME type of the file." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(255)), + "size_bytes": Schema.Number.annotate({ "title": "Size Bytes", "description": "Size of the file in bytes." }).check( + Schema.isInt() + ).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("file").annotate({ + "title": "Type", + "description": "Object type.\n\nFor files, this is always `\"file\"`." + }) +}).annotate({ "title": "FileMetadataSchema" }) +export type GatewayTimeoutError = { readonly "message": string; readonly "type": "timeout_error" } +export const GatewayTimeoutError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Request timeout" }), + "type": Schema.Literal("timeout_error").annotate({ "title": "Type", "default": "timeout_error" }) +}).annotate({ "title": "GatewayTimeoutError" }) +export type GetSkillResponse = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const GetSkillResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "GetSkillResponse" }) +export type GetSkillVersionResponse = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const GetSkillVersionResponse = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "GetSkillVersionResponse" }) +export type InputJsonContentBlockDelta = { readonly "partial_json": string; readonly "type": "input_json_delta" } +export const InputJsonContentBlockDelta = Schema.Struct({ + "partial_json": Schema.String.annotate({ "title": "Partial Json" }), + "type": Schema.Literal("input_json_delta").annotate({ "title": "Type", "default": "input_json_delta" }) +}).annotate({ "title": "InputJsonContentBlockDelta" }) +export type InvalidRequestError = { readonly "message": string; readonly "type": "invalid_request_error" } +export const InvalidRequestError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Invalid request" }), + "type": Schema.Literal("invalid_request_error").annotate({ "title": "Type", "default": "invalid_request_error" }) +}).annotate({ "title": "InvalidRequestError" }) +export type JsonOutputFormat = { + readonly "schema": { readonly [x: string]: Schema.Json } + readonly "type": "json_schema" +} +export const JsonOutputFormat = Schema.Struct({ + "schema": Schema.Record(Schema.String, Schema.Json).annotate({ + "title": "Schema", + "description": "The JSON schema of the format" + }), + "type": Schema.Literal("json_schema").annotate({ "title": "Type" }) +}).annotate({ "title": "JsonOutputFormat" }) +export type JsonValue = unknown +export const JsonValue = Schema.Unknown +export type MessageBatch = { + readonly "archived_at": string | null + readonly "cancel_initiated_at": string | null + readonly "created_at": string + readonly "ended_at": string | null + readonly "expires_at": string + readonly "id": string + readonly "processing_status": "in_progress" | "canceling" | "ended" + readonly "request_counts": { + readonly "canceled": number + readonly "errored": number + readonly "expired": number + readonly "processing": number + readonly "succeeded": number + } + readonly "results_url": string | null + readonly "type": "message_batch" +} +export const MessageBatch = Schema.Struct({ + "archived_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Archived At", + "description": + "RFC 3339 datetime string representing the time at which the Message Batch was archived and its results became unavailable." + }), + "cancel_initiated_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Cancel Initiated At", + "description": + "RFC 3339 datetime string representing the time at which cancellation was initiated for the Message Batch. Specified only if cancellation was initiated." + }), + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "RFC 3339 datetime string representing the time at which the Message Batch was created.", + "format": "date-time" + }), + "ended_at": Schema.Union([Schema.String.annotate({ "format": "date-time" }), Schema.Null]).annotate({ + "title": "Ended At", + "description": + "RFC 3339 datetime string representing the time at which processing for the Message Batch ended. Specified only once processing ends.\n\nProcessing ends when every request in a Message Batch has either succeeded, errored, canceled, or expired." + }), + "expires_at": Schema.String.annotate({ + "title": "Expires At", + "description": + "RFC 3339 datetime string representing the time at which the Message Batch will expire and end processing, which is 24 hours after creation.", + "format": "date-time" + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "processing_status": Schema.Literals(["in_progress", "canceling", "ended"]).annotate({ + "title": "Processing Status", + "description": "Processing status of the Message Batch." + }), + "request_counts": Schema.Struct({ + "canceled": Schema.Number.annotate({ + "title": "Canceled", + "description": + "Number of requests in the Message Batch that have been canceled.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "errored": Schema.Number.annotate({ + "title": "Errored", + "description": + "Number of requests in the Message Batch that encountered an error.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "expired": Schema.Number.annotate({ + "title": "Expired", + "description": + "Number of requests in the Message Batch that have expired.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()), + "processing": Schema.Number.annotate({ + "title": "Processing", + "description": "Number of requests in the Message Batch that are processing.", + "default": 0 + }).check(Schema.isInt()), + "succeeded": Schema.Number.annotate({ + "title": "Succeeded", + "description": + "Number of requests in the Message Batch that have completed successfully.\n\nThis is zero until processing of the entire Message Batch has ended.", + "default": 0 + }).check(Schema.isInt()) + }).annotate({ + "title": "RequestCounts", + "description": + "Tallies requests within the Message Batch, categorized by their status.\n\nRequests start as `processing` and move to one of the other statuses only once processing of the entire batch ends. The sum of all values always matches the total number of requests in the batch." + }), + "results_url": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Results Url", + "description": + "URL to a `.jsonl` file containing the results of the Message Batch requests. Specified only once processing ends.\n\nResults in the file are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests." + }), + "type": Schema.Literal("message_batch").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Message Batches, this is always `\"message_batch\"`.", + "default": "message_batch" + }) +}).annotate({ "title": "MessageBatch" }) +export type MessageStopEvent = { readonly "type": "message_stop" } +export const MessageStopEvent = Schema.Struct({ + "type": Schema.Literal("message_stop").annotate({ "title": "Type", "default": "message_stop" }) +}).annotate({ "title": "MessageStopEvent" }) +export type ModelInfo = { + readonly "created_at": string + readonly "display_name": string + readonly "id": string + readonly "type": "model" +} +export const ModelInfo = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": + "RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown.", + "format": "date-time" + }), + "display_name": Schema.String.annotate({ + "title": "Display Name", + "description": "A human-readable name for the model." + }), + "id": Schema.String.annotate({ "title": "Id", "description": "Unique model identifier." }), + "type": Schema.Literal("model").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Models, this is always `\"model\"`.", + "default": "model" + }) +}).annotate({ "title": "ModelInfo" }) +export type NotFoundError = { readonly "message": string; readonly "type": "not_found_error" } +export const NotFoundError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Not found" }), + "type": Schema.Literal("not_found_error").annotate({ "title": "Type", "default": "not_found_error" }) +}).annotate({ "title": "NotFoundError" }) +export type OverloadedError = { readonly "message": string; readonly "type": "overloaded_error" } +export const OverloadedError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Overloaded" }), + "type": Schema.Literal("overloaded_error").annotate({ "title": "Type", "default": "overloaded_error" }) +}).annotate({ "title": "OverloadedError" }) +export type PermissionError = { readonly "message": string; readonly "type": "permission_error" } +export const PermissionError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Permission denied" }), + "type": Schema.Literal("permission_error").annotate({ "title": "Type", "default": "permission_error" }) +}).annotate({ "title": "PermissionError" }) +export type PlainTextSource = { readonly "data": string; readonly "media_type": "text/plain"; readonly "type": "text" } +export const PlainTextSource = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "media_type": Schema.Literal("text/plain").annotate({ "title": "Media Type" }), + "type": Schema.Literal("text").annotate({ "title": "Type" }) +}).annotate({ "title": "PlainTextSource" }) +export type RateLimitError = { readonly "message": string; readonly "type": "rate_limit_error" } +export const RateLimitError = Schema.Struct({ + "message": Schema.String.annotate({ "title": "Message", "default": "Rate limited" }), + "type": Schema.Literal("rate_limit_error").annotate({ "title": "Type", "default": "rate_limit_error" }) +}).annotate({ "title": "RateLimitError" }) +export type RequestBashCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "bash_code_execution_output" +} +export const RequestBashCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("bash_code_execution_output").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionOutputBlock" }) +export type RequestCharLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "start_char_index": number + readonly "type": "char_location" +} +export const RequestCharLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_char_index": Schema.Number.annotate({ "title": "End Char Index" }).check(Schema.isInt()), + "start_char_index": Schema.Number.annotate({ "title": "Start Char Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("char_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCharLocationCitation" }) +export type RequestCitationsConfig = { readonly "enabled"?: boolean } +export const RequestCitationsConfig = Schema.Struct({ + "enabled": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Enabled" })) +}).annotate({ "title": "RequestCitationsConfig" }) +export type RequestCodeExecutionOutputBlock = { readonly "file_id": string; readonly "type": "code_execution_output" } +export const RequestCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("code_execution_output").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionOutputBlock" }) +export type RequestContentBlockLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const RequestContentBlockLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("content_block_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestContentBlockLocationCitation" }) +export type RequestPageLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "start_page_number": number + readonly "type": "page_location" +} +export const RequestPageLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([ + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + Schema.Null + ]).annotate({ "title": "Document Title" }), + "end_page_number": Schema.Number.annotate({ "title": "End Page Number" }).check(Schema.isInt()), + "start_page_number": Schema.Number.annotate({ "title": "Start Page Number" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(1) + ), + "type": Schema.Literal("page_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestPageLocationCitation" }) +export type RequestSearchResultLocationCitation = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const RequestSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "search_result_index": Schema.Number.annotate({ "title": "Search Result Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "source": Schema.String.annotate({ "title": "Source" }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("search_result_location").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestSearchResultLocationCitation" }) +export type RequestTextEditorCodeExecutionCreateResultBlock = { + readonly "is_file_update": boolean + readonly "type": "text_editor_code_execution_create_result" +} +export const RequestTextEditorCodeExecutionCreateResultBlock = Schema.Struct({ + "is_file_update": Schema.Boolean.annotate({ "title": "Is File Update" }), + "type": Schema.Literal("text_editor_code_execution_create_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionCreateResultBlock" }) +export type RequestTextEditorCodeExecutionStrReplaceResultBlock = { + readonly "lines"?: ReadonlyArray | null + readonly "new_lines"?: number | null + readonly "new_start"?: number | null + readonly "old_lines"?: number | null + readonly "old_start"?: number | null + readonly "type": "text_editor_code_execution_str_replace_result" +} +export const RequestTextEditorCodeExecutionStrReplaceResultBlock = Schema.Struct({ + "lines": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Lines" })), + "new_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "New Lines" }) + ), + "new_start": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "New Start" }) + ), + "old_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Old Lines" }) + ), + "old_start": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Old Start" }) + ), + "type": Schema.Literal("text_editor_code_execution_str_replace_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionStrReplaceResultBlock" }) +export type RequestTextEditorCodeExecutionViewResultBlock = { + readonly "content": string + readonly "file_type": "text" | "image" | "pdf" + readonly "num_lines"?: number | null + readonly "start_line"?: number | null + readonly "total_lines"?: number | null + readonly "type": "text_editor_code_execution_view_result" +} +export const RequestTextEditorCodeExecutionViewResultBlock = Schema.Struct({ + "content": Schema.String.annotate({ "title": "Content" }), + "file_type": Schema.Literals(["text", "image", "pdf"]).annotate({ "title": "File Type" }), + "num_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Num Lines" }) + ), + "start_line": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Start Line" }) + ), + "total_lines": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ "title": "Total Lines" }) + ), + "type": Schema.Literal("text_editor_code_execution_view_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionViewResultBlock" }) +export type RequestWebSearchResultBlock = { + readonly "encrypted_content": string + readonly "page_age"?: string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string +} +export const RequestWebSearchResultBlock = Schema.Struct({ + "encrypted_content": Schema.String.annotate({ "title": "Encrypted Content" }), + "page_age": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Page Age" })), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "RequestWebSearchResultBlock" }) +export type RequestWebSearchResultLocationCitation = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" + readonly "url": string +} +export const RequestWebSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "encrypted_index": Schema.String.annotate({ "title": "Encrypted Index" }), + "title": Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(512)), Schema.Null]) + .annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result_location").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2048)) +}).annotate({ "title": "RequestWebSearchResultLocationCitation" }) +export type ResponseBashCodeExecutionOutputBlock = { + readonly "file_id": string + readonly "type": "bash_code_execution_output" +} +export const ResponseBashCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("bash_code_execution_output").annotate({ + "title": "Type", + "default": "bash_code_execution_output" + }) +}).annotate({ "title": "ResponseBashCodeExecutionOutputBlock" }) +export type ResponseCharLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "file_id": string | null + readonly "start_char_index": number + readonly "type": "char_location" +} +export const ResponseCharLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_char_index": Schema.Number.annotate({ "title": "End Char Index" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_char_index": Schema.Number.annotate({ "title": "Start Char Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("char_location").annotate({ "title": "Type", "default": "char_location" }) +}).annotate({ "title": "ResponseCharLocationCitation" }) +export type ResponseCitationsConfig = { readonly "enabled": boolean } +export const ResponseCitationsConfig = Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "title": "Enabled", "default": false }) +}).annotate({ "title": "ResponseCitationsConfig" }) +export type ResponseCodeExecutionOutputBlock = { readonly "file_id": string; readonly "type": "code_execution_output" } +export const ResponseCodeExecutionOutputBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("code_execution_output").annotate({ "title": "Type", "default": "code_execution_output" }) +}).annotate({ "title": "ResponseCodeExecutionOutputBlock" }) +export type ResponseContainerUploadBlock = { readonly "file_id": string; readonly "type": "container_upload" } +export const ResponseContainerUploadBlock = Schema.Struct({ + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("container_upload").annotate({ "title": "Type", "default": "container_upload" }) +}).annotate({ + "title": "ResponseContainerUploadBlock", + "description": "Response model for a file uploaded to the container." +}) +export type ResponseContentBlockLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "file_id": string | null + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const ResponseContentBlockLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "type": Schema.Literal("content_block_location").annotate({ "title": "Type", "default": "content_block_location" }) +}).annotate({ "title": "ResponseContentBlockLocationCitation" }) +export type ResponsePageLocationCitation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "file_id": string | null + readonly "start_page_number": number + readonly "type": "page_location" +} +export const ResponsePageLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "document_index": Schema.Number.annotate({ "title": "Document Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "document_title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Document Title" }), + "end_page_number": Schema.Number.annotate({ "title": "End Page Number" }).check(Schema.isInt()), + "file_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "File Id", "default": null }), + "start_page_number": Schema.Number.annotate({ "title": "Start Page Number" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(1) + ), + "type": Schema.Literal("page_location").annotate({ "title": "Type", "default": "page_location" }) +}).annotate({ "title": "ResponsePageLocationCitation" }) +export type ResponseRedactedThinkingBlock = { readonly "data": string; readonly "type": "redacted_thinking" } +export const ResponseRedactedThinkingBlock = Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "type": Schema.Literal("redacted_thinking").annotate({ "title": "Type", "default": "redacted_thinking" }) +}).annotate({ "title": "ResponseRedactedThinkingBlock" }) +export type ResponseSearchResultLocationCitation = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const ResponseSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "end_block_index": Schema.Number.annotate({ "title": "End Block Index" }).check(Schema.isInt()), + "search_result_index": Schema.Number.annotate({ "title": "Search Result Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "source": Schema.String.annotate({ "title": "Source" }), + "start_block_index": Schema.Number.annotate({ "title": "Start Block Index" }).check(Schema.isInt()).check( + Schema.isGreaterThanOrEqualTo(0) + ), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("search_result_location").annotate({ "title": "Type", "default": "search_result_location" }) +}).annotate({ "title": "ResponseSearchResultLocationCitation" }) +export type ResponseTextEditorCodeExecutionCreateResultBlock = { + readonly "is_file_update": boolean + readonly "type": "text_editor_code_execution_create_result" +} +export const ResponseTextEditorCodeExecutionCreateResultBlock = Schema.Struct({ + "is_file_update": Schema.Boolean.annotate({ "title": "Is File Update" }), + "type": Schema.Literal("text_editor_code_execution_create_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_create_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionCreateResultBlock" }) +export type ResponseTextEditorCodeExecutionStrReplaceResultBlock = { + readonly "lines": ReadonlyArray | null + readonly "new_lines": number | null + readonly "new_start": number | null + readonly "old_lines": number | null + readonly "old_start": number | null + readonly "type": "text_editor_code_execution_str_replace_result" +} +export const ResponseTextEditorCodeExecutionStrReplaceResultBlock = Schema.Struct({ + "lines": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Lines", "default": null }), + "new_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "New Lines", + "default": null + }), + "new_start": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "New Start", + "default": null + }), + "old_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Old Lines", + "default": null + }), + "old_start": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Old Start", + "default": null + }), + "type": Schema.Literal("text_editor_code_execution_str_replace_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_str_replace_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionStrReplaceResultBlock" }) +export type ResponseTextEditorCodeExecutionViewResultBlock = { + readonly "content": string + readonly "file_type": "text" | "image" | "pdf" + readonly "num_lines": number | null + readonly "start_line": number | null + readonly "total_lines": number | null + readonly "type": "text_editor_code_execution_view_result" +} +export const ResponseTextEditorCodeExecutionViewResultBlock = Schema.Struct({ + "content": Schema.String.annotate({ "title": "Content" }), + "file_type": Schema.Literals(["text", "image", "pdf"]).annotate({ "title": "File Type" }), + "num_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Num Lines", + "default": null + }), + "start_line": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Start Line", + "default": null + }), + "total_lines": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Total Lines", + "default": null + }), + "type": Schema.Literal("text_editor_code_execution_view_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_view_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionViewResultBlock" }) +export type ResponseThinkingBlock = { + readonly "signature": string + readonly "thinking": string + readonly "type": "thinking" +} +export const ResponseThinkingBlock = Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking").annotate({ "title": "Type", "default": "thinking" }) +}).annotate({ "title": "ResponseThinkingBlock" }) +export type ResponseToolReferenceBlock = { readonly "tool_name": string; readonly "type": "tool_reference" } +export const ResponseToolReferenceBlock = Schema.Struct({ + "tool_name": Schema.String.annotate({ "title": "Tool Name" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(256) + ).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,256}$"))), + "type": Schema.Literal("tool_reference").annotate({ "title": "Type", "default": "tool_reference" }) +}).annotate({ "title": "ResponseToolReferenceBlock" }) +export type ResponseWebSearchResultBlock = { + readonly "encrypted_content": string + readonly "page_age": string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string +} +export const ResponseWebSearchResultBlock = Schema.Struct({ + "encrypted_content": Schema.String.annotate({ "title": "Encrypted Content" }), + "page_age": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Page Age", "default": null }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result").annotate({ "title": "Type", "default": "web_search_result" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "ResponseWebSearchResultBlock" }) +export type ResponseWebSearchResultLocationCitation = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" + readonly "url": string +} +export const ResponseWebSearchResultLocationCitation = Schema.Struct({ + "cited_text": Schema.String.annotate({ "title": "Cited Text" }), + "encrypted_index": Schema.String.annotate({ "title": "Encrypted Index" }), + "title": Schema.Union([Schema.String.check(Schema.isMaxLength(512)), Schema.Null]).annotate({ "title": "Title" }), + "type": Schema.Literal("web_search_result_location").annotate({ + "title": "Type", + "default": "web_search_result_location" + }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "ResponseWebSearchResultLocationCitation" }) +export type ServerToolCaller = { readonly "tool_id": string; readonly "type": "code_execution_20250825" } +export const ServerToolCaller = Schema.Struct({ + "tool_id": Schema.String.annotate({ "title": "Tool Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_20250825").annotate({ "title": "Type" }) +}).annotate({ "title": "ServerToolCaller", "description": "Tool invocation generated by a server-side tool." }) +export type ServerToolCaller_20260120 = { readonly "tool_id": string; readonly "type": "code_execution_20260120" } +export const ServerToolCaller_20260120 = Schema.Struct({ + "tool_id": Schema.String.annotate({ "title": "Tool Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_20260120").annotate({ "title": "Type" }) +}).annotate({ "title": "ServerToolCaller_20260120" }) +export type ServerToolUsage = { readonly "web_fetch_requests": number; readonly "web_search_requests": number } +export const ServerToolUsage = Schema.Struct({ + "web_fetch_requests": Schema.Number.annotate({ + "title": "Web Fetch Requests", + "description": "The number of web fetch tool requests.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "web_search_requests": Schema.Number.annotate({ + "title": "Web Search Requests", + "description": "The number of web search tool requests.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ "title": "ServerToolUsage" }) +export type SignatureContentBlockDelta = { readonly "signature": string; readonly "type": "signature_delta" } +export const SignatureContentBlockDelta = Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "type": Schema.Literal("signature_delta").annotate({ "title": "Type", "default": "signature_delta" }) +}).annotate({ "title": "SignatureContentBlockDelta" }) +export type Skill = { + readonly "created_at": string + readonly "display_title": string | null + readonly "id": string + readonly "latest_version": string | null + readonly "source": string + readonly "type": string + readonly "updated_at": string +} +export const Skill = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill was created." + }), + "display_title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Display Title", + "description": + "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill.\n\nThe format and length of IDs may change over time." + }), + "latest_version": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Latest Version", + "description": + "The latest version identifier for the skill.\n\nThis represents the most recent version of the skill that has been created." + }), + "source": Schema.String.annotate({ + "title": "Source", + "description": + "Source of the skill.\n\nThis may be one of the following values:\n* `\"custom\"`: the skill was created by a user\n* `\"anthropic\"`: the skill was created by Anthropic" + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skills, this is always `\"skill\"`.", + "default": "skill" + }), + "updated_at": Schema.String.annotate({ + "title": "Updated At", + "description": "ISO 8601 timestamp of when the skill was last updated." + }) +}).annotate({ "title": "Skill" }) +export type SkillVersion = { + readonly "created_at": string + readonly "description": string + readonly "directory": string + readonly "id": string + readonly "name": string + readonly "skill_id": string + readonly "type": string + readonly "version": string +} +export const SkillVersion = Schema.Struct({ + "created_at": Schema.String.annotate({ + "title": "Created At", + "description": "ISO 8601 timestamp of when the skill version was created." + }), + "description": Schema.String.annotate({ + "title": "Description", + "description": "Description of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "directory": Schema.String.annotate({ + "title": "Directory", + "description": + "Directory name of the skill version.\n\nThis is the top-level directory name that was extracted from the uploaded files." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique identifier for the skill version.\n\nThe format and length of IDs may change over time." + }), + "name": Schema.String.annotate({ + "title": "Name", + "description": + "Human-readable name of the skill version.\n\nThis is extracted from the SKILL.md file in the skill upload." + }), + "skill_id": Schema.String.annotate({ + "title": "Skill Id", + "description": "Identifier for the skill that this version belongs to." + }), + "type": Schema.String.annotate({ + "title": "Type", + "description": "Object type.\n\nFor Skill Versions, this is always `\"skill_version\"`.", + "default": "skill_version" + }), + "version": Schema.String.annotate({ + "title": "Version", + "description": + "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\")." + }) +}).annotate({ "title": "SkillVersion" }) +export type TextContentBlockDelta = { readonly "text": string; readonly "type": "text_delta" } +export const TextContentBlockDelta = Schema.Struct({ + "text": Schema.String.annotate({ "title": "Text" }), + "type": Schema.Literal("text_delta").annotate({ "title": "Type", "default": "text_delta" }) +}).annotate({ "title": "TextContentBlockDelta" }) +export type TextEditorCodeExecutionToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "file_not_found" +export const TextEditorCodeExecutionToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "file_not_found" +]).annotate({ "title": "TextEditorCodeExecutionToolResultErrorCode" }) +export type ThinkingConfigAdaptive = { readonly "type": "adaptive" } +export const ThinkingConfigAdaptive = Schema.Struct({ + "type": Schema.Literal("adaptive").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigAdaptive" }) +export type ThinkingConfigDisabled = { readonly "type": "disabled" } +export const ThinkingConfigDisabled = Schema.Struct({ + "type": Schema.Literal("disabled").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigDisabled" }) +export type ThinkingConfigEnabled = { readonly "budget_tokens": number; readonly "type": "enabled" } +export const ThinkingConfigEnabled = Schema.Struct({ + "budget_tokens": Schema.Number.annotate({ + "title": "Budget Tokens", + "description": + "Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality.\n\nMust be ≥1024 and less than `max_tokens`.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1024)), + "type": Schema.Literal("enabled").annotate({ "title": "Type" }) +}).annotate({ "title": "ThinkingConfigEnabled" }) +export type ThinkingContentBlockDelta = { readonly "thinking": string; readonly "type": "thinking_delta" } +export const ThinkingContentBlockDelta = Schema.Struct({ + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking_delta").annotate({ "title": "Type", "default": "thinking_delta" }) +}).annotate({ "title": "ThinkingContentBlockDelta" }) +export type ToolChoiceAny = { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "any" } +export const ToolChoiceAny = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output exactly one tool use." + }) + ), + "type": Schema.Literal("any").annotate({ "title": "Type" }) +}).annotate({ "title": "ToolChoiceAny", "description": "The model will use any available tools." }) +export type ToolChoiceAuto = { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "auto" } +export const ToolChoiceAuto = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output at most one tool use." + }) + ), + "type": Schema.Literal("auto").annotate({ "title": "Type" }) +}).annotate({ "title": "ToolChoiceAuto", "description": "The model will automatically decide whether to use tools." }) +export type ToolChoiceNone = { readonly "type": "none" } +export const ToolChoiceNone = Schema.Struct({ "type": Schema.Literal("none").annotate({ "title": "Type" }) }).annotate({ + "title": "ToolChoiceNone", + "description": "The model will not be allowed to use tools." +}) +export type ToolChoiceTool = { + readonly "disable_parallel_tool_use"?: boolean + readonly "name": string + readonly "type": "tool" +} +export const ToolChoiceTool = Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Disable Parallel Tool Use", + "description": + "Whether to disable parallel tool use.\n\nDefaults to `false`. If set to `true`, the model will output exactly one tool use." + }) + ), + "name": Schema.String.annotate({ "title": "Name", "description": "The name of the tool to use." }), + "type": Schema.Literal("tool").annotate({ "title": "Type" }) +}).annotate({ + "title": "ToolChoiceTool", + "description": "The model will use the specified tool with `tool_choice.name`." +}) +export type ToolSearchToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" +export const ToolSearchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded" +]).annotate({ "title": "ToolSearchToolResultErrorCode" }) +export type URLImageSource = { readonly "type": "url"; readonly "url": string } +export const URLImageSource = Schema.Struct({ + "type": Schema.Literal("url").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "URLImageSource" }) +export type URLPDFSource = { readonly "type": "url"; readonly "url": string } +export const URLPDFSource = Schema.Struct({ + "type": Schema.Literal("url").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "URLPDFSource" }) +export type UserLocation = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type": "approximate" +} +export const UserLocation = Schema.Struct({ + "city": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "City", + "description": "The city of the user." + }) + ), + "country": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(2)).check(Schema.isMaxLength(2)), Schema.Null]).annotate({ + "title": "Country", + "description": "The two letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of the user." + }) + ), + "region": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "Region", + "description": "The region of the user." + }) + ), + "timezone": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), Schema.Null]).annotate({ + "title": "Timezone", + "description": "The [IANA timezone](https://nodatime.org/TimeZones) of the user." + }) + ), + "type": Schema.Literal("approximate").annotate({ "title": "Type" }) +}).annotate({ "title": "UserLocation" }) +export type WebFetchToolResultErrorCode = + | "invalid_tool_input" + | "url_too_long" + | "url_not_allowed" + | "url_not_accessible" + | "unsupported_content_type" + | "too_many_requests" + | "max_uses_exceeded" + | "unavailable" +export const WebFetchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "url_too_long", + "url_not_allowed", + "url_not_accessible", + "unsupported_content_type", + "too_many_requests", + "max_uses_exceeded", + "unavailable" +]).annotate({ "title": "WebFetchToolResultErrorCode" }) +export type WebSearchToolResultErrorCode = + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + | "request_too_large" +export const WebSearchToolResultErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long", + "request_too_large" +]).annotate({ "title": "WebSearchToolResultErrorCode" }) +export type StopReason = "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" +export const StopReason = Schema.Literals([ + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "refusal" +]) +export type BetaStopReason = + | "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use" + | "pause_turn" + | "compaction" + | "refusal" + | "model_context_window_exceeded" +export const BetaStopReason = Schema.Literals([ + "end_turn", + "max_tokens", + "stop_sequence", + "tool_use", + "pause_turn", + "compaction", + "refusal", + "model_context_window_exceeded" +]) +export type Model = + | string + | "claude-sonnet-5" + | "claude-fable-5" + | "claude-mythos-5" + | "claude-opus-4-8" + | "claude-opus-4-7" + | "claude-mythos-preview" + | "claude-opus-4-6" + | "claude-sonnet-4-6" + | "claude-haiku-4-5" + | "claude-haiku-4-5-20251001" + | "claude-opus-4-5" + | "claude-opus-4-5-20251101" + | "claude-sonnet-4-5" + | "claude-sonnet-4-5-20250929" + | "claude-opus-4-1" + | "claude-opus-4-1-20250805" +export const Model = Schema.Union([ + Schema.String, + Schema.Literals([ + "claude-sonnet-5", + "claude-fable-5", + "claude-mythos-5", + "claude-opus-4-8", + "claude-opus-4-7", + "claude-mythos-preview", + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-opus-4-5", + "claude-opus-4-5-20251101", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-opus-4-1", + "claude-opus-4-1-20250805" + ]) +]).annotate({ + "title": "Model", + "description": + "The model that will complete your prompt.\n\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options." +}) +export type BetaMemoryTool_20250818_ViewCommand = { + readonly "command": "view" + readonly "path": string + readonly "view_range"?: ReadonlyArray +} +export const BetaMemoryTool_20250818_ViewCommand = Schema.Struct({ + "command": Schema.Literal("view").annotate({ "description": "Command type identifier", "default": "view" }), + "path": Schema.String.annotate({ "description": "Path to directory or file to view" }), + "view_range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": "Optional line range for viewing specific lines" + }).check(Schema.isMinLength(2)).check(Schema.isMaxLength(2)) + ) +}) +export type BetaMemoryTool_20250818_CreateCommand = { + readonly "command": "create" + readonly "path": string + readonly "file_text": string +} +export const BetaMemoryTool_20250818_CreateCommand = Schema.Struct({ + "command": Schema.Literal("create").annotate({ "description": "Command type identifier", "default": "create" }), + "path": Schema.String.annotate({ "description": "Path where the file should be created" }), + "file_text": Schema.String.annotate({ "description": "Content to write to the file" }) +}) +export type BetaMemoryTool_20250818_StrReplaceCommand = { + readonly "command": "str_replace" + readonly "path": string + readonly "old_str": string + readonly "new_str": string +} +export const BetaMemoryTool_20250818_StrReplaceCommand = Schema.Struct({ + "command": Schema.Literal("str_replace").annotate({ + "description": "Command type identifier", + "default": "str_replace" + }), + "path": Schema.String.annotate({ "description": "Path to the file where text should be replaced" }), + "old_str": Schema.String.annotate({ "description": "Text to search for and replace" }), + "new_str": Schema.String.annotate({ "description": "Text to replace with" }) +}) +export type BetaMemoryTool_20250818_InsertCommand = { + readonly "command": "insert" + readonly "path": string + readonly "insert_line": number + readonly "insert_text": string +} +export const BetaMemoryTool_20250818_InsertCommand = Schema.Struct({ + "command": Schema.Literal("insert").annotate({ "description": "Command type identifier", "default": "insert" }), + "path": Schema.String.annotate({ "description": "Path to the file where text should be inserted" }), + "insert_line": Schema.Number.annotate({ "description": "Line number where text should be inserted" }).check( + Schema.isInt() + ).check(Schema.isGreaterThanOrEqualTo(1)), + "insert_text": Schema.String.annotate({ "description": "Text to insert at the specified line" }) +}) +export type BetaMemoryTool_20250818_DeleteCommand = { readonly "command": "delete"; readonly "path": string } +export const BetaMemoryTool_20250818_DeleteCommand = Schema.Struct({ + "command": Schema.Literal("delete").annotate({ "description": "Command type identifier", "default": "delete" }), + "path": Schema.String.annotate({ "description": "Path to the file or directory to delete" }) +}) +export type BetaMemoryTool_20250818_RenameCommand = { + readonly "command": "rename" + readonly "old_path": string + readonly "new_path": string +} +export const BetaMemoryTool_20250818_RenameCommand = Schema.Struct({ + "command": Schema.Literal("rename").annotate({ "description": "Command type identifier", "default": "rename" }), + "old_path": Schema.String.annotate({ "description": "Current path of the file or directory" }), + "new_path": Schema.String.annotate({ "description": "New path for the file or directory" }) +}) +export type RequestBashCodeExecutionToolResultError = { + readonly "error_code": BashCodeExecutionToolResultErrorCode + readonly "type": "bash_code_execution_tool_result_error" +} +export const RequestBashCodeExecutionToolResultError = Schema.Struct({ + "error_code": BashCodeExecutionToolResultErrorCode, + "type": Schema.Literal("bash_code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionToolResultError" }) +export type ResponseBashCodeExecutionToolResultError = { + readonly "error_code": BashCodeExecutionToolResultErrorCode + readonly "type": "bash_code_execution_tool_result_error" +} +export const ResponseBashCodeExecutionToolResultError = Schema.Struct({ + "error_code": BashCodeExecutionToolResultErrorCode, + "type": Schema.Literal("bash_code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "bash_code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseBashCodeExecutionToolResultError" }) +export type BetaRequestBashCodeExecutionToolResultError = { + readonly "error_code": BetaBashCodeExecutionToolResultErrorCode + readonly "type": "bash_code_execution_tool_result_error" +} +export const BetaRequestBashCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaBashCodeExecutionToolResultErrorCode, + "type": Schema.Literal("bash_code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionToolResultError" }) +export type BetaResponseBashCodeExecutionToolResultError = { + readonly "error_code": BetaBashCodeExecutionToolResultErrorCode + readonly "type": "bash_code_execution_tool_result_error" +} +export const BetaResponseBashCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaBashCodeExecutionToolResultErrorCode, + "type": Schema.Literal("bash_code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "bash_code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseBashCodeExecutionToolResultError" }) +export type BetaCodeExecutionTool_20250522 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20250522" +} +export const BetaCodeExecutionTool_20250522 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20250522").annotate({ "title": "Type" }) +}).annotate({ "title": "CodeExecutionTool_20250522" }) +export type BetaCodeExecutionTool_20250825 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20250825" +} +export const BetaCodeExecutionTool_20250825 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20250825").annotate({ "title": "Type" }) +}).annotate({ "title": "CodeExecutionTool_20250825" }) +export type BetaCodeExecutionTool_20260120 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20260120" +} +export const BetaCodeExecutionTool_20260120 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20260120").annotate({ "title": "Type" }) +}).annotate({ + "title": "CodeExecutionTool_20260120", + "description": "Code execution tool with REPL state persistence (daemon mode + gVisor checkpoint)." +}) +export type BetaRequestCompactionBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content": string | null + readonly "type": "compaction" +} +export const BetaRequestCompactionBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Content", + "description": "Summary of previously compacted content, or null if compaction failed" + }), + "type": Schema.Literal("compaction").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestCompactionBlock", + "description": + "A compaction block containing summary of previous context.\n\nUsers should round-trip these blocks from responses to subsequent requests\nto maintain context across compaction boundaries.\n\nWhen content is None, the block represents a failed compaction. The server\ntreats these as no-ops. Empty string content is not allowed." +}) +export type BetaRequestContainerUploadBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "file_id": string + readonly "type": "container_upload" +} +export const BetaRequestContainerUploadBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("container_upload").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestContainerUploadBlock", + "description": + "A content block that represents a file to be uploaded to the container\nFiles uploaded via this block will be available in the container's input directory." +}) +export type BetaRequestMCPToolUseBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "server_name": string + readonly "type": "mcp_tool_use" +} +export const BetaRequestMCPToolUseBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name" }), + "server_name": Schema.String.annotate({ "title": "Server Name", "description": "The name of the MCP server" }), + "type": Schema.Literal("mcp_tool_use").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestMCPToolUseBlock" }) +export type BetaRequestToolReferenceBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "tool_name": string + readonly "type": "tool_reference" +} +export const BetaRequestToolReferenceBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "tool_name": Schema.String.annotate({ "title": "Tool Name" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(256) + ).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,256}$"))), + "type": Schema.Literal("tool_reference").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestToolReferenceBlock", + "description": "Tool reference block that can be included in tool_result content." +}) +export type BetaToolSearchToolBM25_20251119 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_bm25" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_bm25_20251119" | "tool_search_tool_bm25" +} +export const BetaToolSearchToolBM25_20251119 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("tool_search_tool_bm25").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literals(["tool_search_tool_bm25_20251119", "tool_search_tool_bm25"]).annotate({ "title": "Type" }) +}).annotate({ "title": "ToolSearchToolBM25_20251119" }) +export type BetaToolSearchToolRegex_20251119 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_regex" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_regex_20251119" | "tool_search_tool_regex" +} +export const BetaToolSearchToolRegex_20251119 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("tool_search_tool_regex").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literals(["tool_search_tool_regex_20251119", "tool_search_tool_regex"]).annotate({ "title": "Type" }) +}).annotate({ "title": "ToolSearchToolRegex_20251119" }) +export type BetaCompactionIterationUsage = { + readonly "cache_creation": BetaCacheCreation | null + readonly "cache_creation_input_tokens": number + readonly "cache_read_input_tokens": number + readonly "input_tokens": number + readonly "output_tokens": number + readonly "type": "compaction" +} +export const BetaCompactionIterationUsage = Schema.Struct({ + "cache_creation": Schema.Union([BetaCacheCreation, Schema.Null]).annotate({ + "description": "Breakdown of cached tokens by TTL", + "default": null + }), + "cache_creation_input_tokens": Schema.Number.annotate({ + "title": "Cache Creation Input Tokens", + "description": "The number of input tokens used to create the cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "cache_read_input_tokens": Schema.Number.annotate({ + "title": "Cache Read Input Tokens", + "description": "The number of input tokens read from the cache.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The number of input tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The number of output tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("compaction").annotate({ + "title": "Type", + "description": "Usage for a compaction iteration", + "default": "compaction" + }) +}).annotate({ "title": "CompactionIterationUsage", "description": "Token usage for a compaction iteration." }) +export type BetaMessageIterationUsage = { + readonly "cache_creation": BetaCacheCreation | null + readonly "cache_creation_input_tokens": number + readonly "cache_read_input_tokens": number + readonly "input_tokens": number + readonly "output_tokens": number + readonly "type": "message" +} +export const BetaMessageIterationUsage = Schema.Struct({ + "cache_creation": Schema.Union([BetaCacheCreation, Schema.Null]).annotate({ + "description": "Breakdown of cached tokens by TTL", + "default": null + }), + "cache_creation_input_tokens": Schema.Number.annotate({ + "title": "Cache Creation Input Tokens", + "description": "The number of input tokens used to create the cache entry.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "cache_read_input_tokens": Schema.Number.annotate({ + "title": "Cache Read Input Tokens", + "description": "The number of input tokens read from the cache.", + "default": 0 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The number of input tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The number of output tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "type": Schema.Literal("message").annotate({ + "title": "Type", + "description": "Usage for a sampling iteration", + "default": "message" + }) +}).annotate({ "title": "MessageIterationUsage", "description": "Token usage for a sampling iteration." }) +export type BetaRequestCodeExecutionToolResultError = { + readonly "error_code": BetaCodeExecutionToolResultErrorCode + readonly "type": "code_execution_tool_result_error" +} +export const BetaRequestCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaCodeExecutionToolResultErrorCode, + "type": Schema.Literal("code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "Error" }) +export type BetaResponseCodeExecutionToolResultError = { + readonly "error_code": BetaCodeExecutionToolResultErrorCode + readonly "type": "code_execution_tool_result_error" +} +export const BetaResponseCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaCodeExecutionToolResultErrorCode, + "type": Schema.Literal("code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseCodeExecutionToolResultError" }) +export type BetaCountMessageTokensResponse = { + readonly "context_management": BetaContextManagementResponse | null + readonly "input_tokens": number +} +export const BetaCountMessageTokensResponse = Schema.Struct({ + "context_management": Schema.Union([BetaContextManagementResponse, Schema.Null]).annotate({ + "description": "Information about context management applied to the message." + }), + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The total number of tokens across the provided list of messages, system prompt, and tools." + }).check(Schema.isInt()) +}).annotate({ "title": "CountMessageTokensResponse" }) +export type BetaFileListResponse = { + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "has_more"?: boolean + readonly "last_id"?: string | null +} +export const BetaFileListResponse = Schema.Struct({ + "data": Schema.Array(BetaFileMetadataSchema).annotate({ + "title": "Data", + "description": "List of file metadata objects." + }), + "first_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "ID of the first file in this page of results." + }) + ), + "has_more": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Has More", + "description": "Whether there are more results available.", + "default": false + }) + ), + "last_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "ID of the last file in this page of results." + }) + ) +}).annotate({ "title": "FileListResponse" }) +export type BetaCompact20260112 = { + readonly "instructions"?: string | null + readonly "pause_after_compaction"?: boolean + readonly "trigger"?: BetaInputTokensTrigger | null + readonly "type": "compact_20260112" +} +export const BetaCompact20260112 = Schema.Struct({ + "instructions": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Instructions", + "description": "Additional instructions for summarization." + }) + ), + "pause_after_compaction": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Pause After Compaction", + "description": "Whether to pause after compaction and return the compaction block to the user." + }) + ), + "trigger": Schema.optionalKey( + Schema.Union([BetaInputTokensTrigger, Schema.Null]).annotate({ + "description": "When to trigger compaction. Defaults to 150000 input tokens." + }) + ), + "type": Schema.Literal("compact_20260112").annotate({ "title": "Type" }) +}).annotate({ + "title": "Compact20260112", + "description": "Automatically compact older context when reaching the configured trigger threshold." +}) +export type BetaBashTool_20241022 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "bash" + readonly "strict"?: boolean + readonly "type": "bash_20241022" +} +export const BetaBashTool_20241022 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("bash").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("bash_20241022").annotate({ "title": "Type" }) +}).annotate({ "title": "BashTool_20241022" }) +export type BetaBashTool_20250124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "bash" + readonly "strict"?: boolean + readonly "type": "bash_20250124" +} +export const BetaBashTool_20250124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("bash").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("bash_20250124").annotate({ "title": "Type" }) +}).annotate({ "title": "BashTool_20250124" }) +export type BetaComputerUseTool_20241022 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "display_height_px": number + readonly "display_number"?: number | null + readonly "display_width_px": number + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "computer" + readonly "strict"?: boolean + readonly "type": "computer_20241022" +} +export const BetaComputerUseTool_20241022 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "display_height_px": Schema.Number.annotate({ + "title": "Display Height Px", + "description": "The height of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "display_number": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), Schema.Null]).annotate({ + "title": "Display Number", + "description": "The X11 display number (e.g. 0, 1) for the display." + }) + ), + "display_width_px": Schema.Number.annotate({ + "title": "Display Width Px", + "description": "The width of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("computer").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("computer_20241022").annotate({ "title": "Type" }) +}).annotate({ "title": "ComputerUseTool_20241022" }) +export type BetaComputerUseTool_20250124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "display_height_px": number + readonly "display_number"?: number | null + readonly "display_width_px": number + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "computer" + readonly "strict"?: boolean + readonly "type": "computer_20250124" +} +export const BetaComputerUseTool_20250124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "display_height_px": Schema.Number.annotate({ + "title": "Display Height Px", + "description": "The height of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "display_number": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), Schema.Null]).annotate({ + "title": "Display Number", + "description": "The X11 display number (e.g. 0, 1) for the display." + }) + ), + "display_width_px": Schema.Number.annotate({ + "title": "Display Width Px", + "description": "The width of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("computer").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("computer_20250124").annotate({ "title": "Type" }) +}).annotate({ "title": "ComputerUseTool_20250124" }) +export type BetaComputerUseTool_20251124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "display_height_px": number + readonly "display_number"?: number | null + readonly "display_width_px": number + readonly "enable_zoom"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "computer" + readonly "strict"?: boolean + readonly "type": "computer_20251124" +} +export const BetaComputerUseTool_20251124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "display_height_px": Schema.Number.annotate({ + "title": "Display Height Px", + "description": "The height of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "display_number": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), Schema.Null]).annotate({ + "title": "Display Number", + "description": "The X11 display number (e.g. 0, 1) for the display." + }) + ), + "display_width_px": Schema.Number.annotate({ + "title": "Display Width Px", + "description": "The width of the display in pixels." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "enable_zoom": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Enable Zoom", + "description": "Whether to enable an action to take a zoomed-in screenshot of the screen." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("computer").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("computer_20251124").annotate({ "title": "Type" }) +}).annotate({ "title": "ComputerUseTool_20251124" }) +export type BetaMemoryTool_20250818 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "memory" + readonly "strict"?: boolean + readonly "type": "memory_20250818" +} +export const BetaMemoryTool_20250818 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("memory").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("memory_20250818").annotate({ "title": "Type" }) +}).annotate({ "title": "MemoryTool_20250818" }) +export type BetaTextEditor_20241022 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "str_replace_editor" + readonly "strict"?: boolean + readonly "type": "text_editor_20241022" +} +export const BetaTextEditor_20241022 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("str_replace_editor").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20241022").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20241022" }) +export type BetaTextEditor_20250124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "str_replace_editor" + readonly "strict"?: boolean + readonly "type": "text_editor_20250124" +} +export const BetaTextEditor_20250124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("str_replace_editor").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250124").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250124" }) +export type BetaTextEditor_20250429 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "name": "str_replace_based_edit_tool" + readonly "strict"?: boolean + readonly "type": "text_editor_20250429" +} +export const BetaTextEditor_20250429 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("str_replace_based_edit_tool").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250429").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250429" }) +export type BetaTextEditor_20250728 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> + readonly "max_characters"?: number | null + readonly "name": "str_replace_based_edit_tool" + readonly "strict"?: boolean + readonly "type": "text_editor_20250728" +} +export const BetaTextEditor_20250728 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ), + "max_characters": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]).annotate({ + "title": "Max Characters", + "description": + "Maximum number of characters to display when viewing a file. If not specified, defaults to displaying the full file." + }) + ), + "name": Schema.Literal("str_replace_based_edit_tool").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250728").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250728" }) +export type BetaTool = { + readonly "type"?: null | "custom" + readonly "description"?: string + readonly "name": string + readonly "input_schema": { + readonly "properties"?: { readonly [x: string]: Schema.Json } | null + readonly "required"?: ReadonlyArray | null + readonly "type": "object" + readonly [x: string]: Schema.Json + } + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "strict"?: boolean + readonly "eager_input_streaming"?: boolean | null + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: BetaJsonValue }> +} +export const BetaTool = Schema.Struct({ + "type": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Literal("custom")]).annotate({ "title": "Type" })), + "description": Schema.optionalKey(Schema.String.annotate({ + "title": "Description", + "description": + "Description of what this tool does.\n\nTool descriptions should be as detailed as possible. The more information that the model has about what the tool is and how to use it, the better it will perform. You can use natural language descriptions to reinforce important aspects of the tool input JSON schema." + })), + "name": Schema.String.annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(128)).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,128}$")) + ), + "input_schema": Schema.StructWithRest( + Schema.Struct({ + "properties": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json), Schema.Null]).annotate({ "title": "Properties" }) + ), + "required": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Required" }) + ), + "type": Schema.Literal("object").annotate({ "title": "Type" }) + }), + [Schema.Record(Schema.String, Schema.Json)] + ).annotate({ + "title": "InputSchema", + "description": + "[JSON schema](https://json-schema.org/draft/2020-12) for this tool's input.\n\nThis defines the shape of the `input` that your tool accepts and that the model will produce." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "eager_input_streaming": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "title": "Eager Input Streaming", + "description": + "Enable eager input streaming for this tool. When true, tool input parameters will be streamed incrementally as they are generated, and types will be inferred on-the-fly rather than buffering the full JSON output. When false, streaming is disabled for this tool even if the fine-grained-tool-streaming beta is active. When null (default), uses the default behavior based on beta headers." + }) + ), + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, BetaJsonValue)).annotate({ "title": "Input Examples" }) + ) +}).annotate({ "title": "Tool" }) +export type BetaMCPToolset = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "configs"?: { readonly [x: string]: BetaMCPToolConfig } | null + readonly "default_config"?: { readonly "defer_loading"?: boolean; readonly "enabled"?: boolean } + readonly "mcp_server_name": string + readonly "type": "mcp_toolset" +} +export const BetaMCPToolset = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "configs": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, BetaMCPToolConfig), Schema.Null]).annotate({ + "title": "Configs", + "description": "Configuration overrides for specific tools, keyed by tool name" + }) + ), + "default_config": Schema.optionalKey( + Schema.Struct({ + "defer_loading": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Defer Loading" })), + "enabled": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Enabled" })) + }).annotate({ + "title": "MCPToolDefaultConfig", + "description": "Default configuration applied to all tools from this server" + }) + ), + "mcp_server_name": Schema.String.annotate({ + "title": "Mcp Server Name", + "description": "Name of the MCP server to configure tools for" + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(255)), + "type": Schema.Literal("mcp_toolset").annotate({ "title": "Type" }) +}).annotate({ + "title": "MCPToolset", + "description": + "Configuration for a group of tools from an MCP server.\n\nAllows configuring enabled status and defer_loading for all tools\nfrom an MCP server, with optional per-tool overrides." +}) +export type BetaListResponse_MessageBatch_ = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "has_more": boolean + readonly "last_id": string | null +} +export const BetaListResponse_MessageBatch_ = Schema.Struct({ + "data": Schema.Array(BetaMessageBatch).annotate({ "title": "Data" }), + "first_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "First ID in the `data` list. Can be used as the `before_id` for the previous page." + }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "last_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "Last ID in the `data` list. Can be used as the `after_id` for the next page." + }) +}).annotate({ "title": "ListResponse[MessageBatch]" }) +export type BetaListResponse_ModelInfo_ = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "has_more": boolean + readonly "last_id": string | null +} +export const BetaListResponse_ModelInfo_ = Schema.Struct({ + "data": Schema.Array(BetaModelInfo).annotate({ "title": "Data" }), + "first_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "First ID in the `data` list. Can be used as the `before_id` for the previous page." + }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "last_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "Last ID in the `data` list. Can be used as the `after_id` for the next page." + }) +}).annotate({ "title": "ListResponse[ModelInfo]" }) +export type BetaErrorResponse = { + readonly "error": + | BetaInvalidRequestError + | BetaAuthenticationError + | BetaBillingError + | BetaPermissionError + | BetaNotFoundError + | BetaRateLimitError + | BetaGatewayTimeoutError + | BetaAPIError + | BetaOverloadedError + readonly "request_id": string | null + readonly "type": "error" +} +export const BetaErrorResponse = Schema.Struct({ + "error": Schema.Union([ + BetaInvalidRequestError, + BetaAuthenticationError, + BetaBillingError, + BetaPermissionError, + BetaNotFoundError, + BetaRateLimitError, + BetaGatewayTimeoutError, + BetaAPIError, + BetaOverloadedError + ], { mode: "oneOf" }).annotate({ "title": "Error" }), + "request_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Request Id", "default": null }), + "type": Schema.Literal("error").annotate({ "title": "Type", "default": "error" }) +}).annotate({ "title": "ErrorResponse" }) +export type BetaRequestBashCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "bash_code_execution_result" +} +export const BetaRequestBashCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaRequestBashCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("bash_code_execution_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionResultBlock" }) +export type BetaWebFetchTool_20250910 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig | null + readonly "defer_loading"?: boolean + readonly "max_content_tokens"?: number | null + readonly "max_uses"?: number | null + readonly "name": "web_fetch" + readonly "strict"?: boolean + readonly "type": "web_fetch_20250910" +} +export const BetaWebFetchTool_20250910 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": "List of domains to allow fetching from" + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": "List of domains to block fetching from" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([BetaRequestCitationsConfig, Schema.Null]).annotate({ + "description": "Citations configuration for fetched documents. Citations are disabled by default." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_content_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Content Tokens", + "description": + "Maximum number of tokens used by including web page text content in the context. The limit is approximate and does not apply to binary content such as PDFs." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_fetch").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_fetch_20250910").annotate({ "title": "Type" }) +}).annotate({ "title": "WebFetchTool_20250910" }) +export type BetaWebFetchTool_20260209 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig | null + readonly "defer_loading"?: boolean + readonly "max_content_tokens"?: number | null + readonly "max_uses"?: number | null + readonly "name": "web_fetch" + readonly "strict"?: boolean + readonly "type": "web_fetch_20260209" +} +export const BetaWebFetchTool_20260209 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": "List of domains to allow fetching from" + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": "List of domains to block fetching from" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([BetaRequestCitationsConfig, Schema.Null]).annotate({ + "description": "Citations configuration for fetched documents. Citations are disabled by default." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_content_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Content Tokens", + "description": + "Maximum number of tokens used by including web page text content in the context. The limit is approximate and does not apply to binary content such as PDFs." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_fetch").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_fetch_20260209").annotate({ "title": "Type" }) +}).annotate({ "title": "WebFetchTool_20260209" }) +export type BetaRequestCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "code_execution_result" +} +export const BetaRequestCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaRequestCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("code_execution_result").annotate({ "title": "Type" }) +}).annotate({ "title": "Result Block" }) +export type BetaRequestEncryptedCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "encrypted_stdout": string + readonly "return_code": number + readonly "stderr": string + readonly "type": "encrypted_code_execution_result" +} +export const BetaRequestEncryptedCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaRequestCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "encrypted_stdout": Schema.String.annotate({ "title": "Encrypted Stdout" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "type": Schema.Literal("encrypted_code_execution_result").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestEncryptedCodeExecutionResultBlock", + "description": "Code execution result with encrypted stdout for PFC + web_search results." +}) +export type BetaRequestMCPServerURLDefinition = { + readonly "authorization_token"?: string | null + readonly "name": string + readonly "tool_configuration"?: BetaRequestMCPServerToolConfiguration | null + readonly "type": "url" + readonly "url": string +} +export const BetaRequestMCPServerURLDefinition = Schema.Struct({ + "authorization_token": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Authorization Token" }) + ), + "name": Schema.String.annotate({ "title": "Name" }), + "tool_configuration": Schema.optionalKey(Schema.Union([BetaRequestMCPServerToolConfiguration, Schema.Null])), + "type": Schema.Literal("url").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url" }) +}).annotate({ "title": "RequestMCPServerURLDefinition" }) +export type BetaRequestMCPToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content"?: + | string + | ReadonlyArray< + { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: + | ReadonlyArray< + | BetaRequestCharLocationCitation + | BetaRequestPageLocationCitation + | BetaRequestContentBlockLocationCitation + | BetaRequestWebSearchResultLocationCitation + | BetaRequestSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" + } + > + readonly "is_error"?: boolean + readonly "tool_use_id": string + readonly "type": "mcp_tool_result" +} +export const BetaRequestMCPToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Array( + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + BetaRequestCharLocationCitation, + BetaRequestPageLocationCitation, + BetaRequestContentBlockLocationCitation, + BetaRequestWebSearchResultLocationCitation, + BetaRequestSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ "title": "Citations" }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("text").annotate({ "title": "Type" }) + }).annotate({ "title": "beta_mcp_tool_result_block_param_content_item" }) + ).annotate({ "title": "beta_mcp_tool_result_block_param_content" }) + ]).annotate({ "title": "Content" }) + ), + "is_error": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Is Error" })), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$")) + ), + "type": Schema.Literal("mcp_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestMCPToolResultBlock" }) +export type BetaRequestTextBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: + | ReadonlyArray< + | BetaRequestCharLocationCitation + | BetaRequestPageLocationCitation + | BetaRequestContentBlockLocationCitation + | BetaRequestWebSearchResultLocationCitation + | BetaRequestSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" +} +export const BetaRequestTextBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + BetaRequestCharLocationCitation, + BetaRequestPageLocationCitation, + BetaRequestContentBlockLocationCitation, + BetaRequestWebSearchResultLocationCitation, + BetaRequestSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ "title": "Citations" }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("text").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextBlock" }) +export type BetaResponseBashCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "bash_code_execution_result" +} +export const BetaResponseBashCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaResponseBashCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("bash_code_execution_result").annotate({ + "title": "Type", + "default": "bash_code_execution_result" + }) +}).annotate({ "title": "ResponseBashCodeExecutionResultBlock" }) +export type BetaResponseDocumentBlock = { + readonly "citations": BetaResponseCitationsConfig | null + readonly "source": BetaBase64PDFSource | BetaPlainTextSource + readonly "title": string | null + readonly "type": "document" +} +export const BetaResponseDocumentBlock = Schema.Struct({ + "citations": Schema.Union([BetaResponseCitationsConfig, Schema.Null]).annotate({ + "description": "Citation configuration for the document", + "default": null + }), + "source": Schema.Union([BetaBase64PDFSource, BetaPlainTextSource], { mode: "oneOf" }).annotate({ "title": "Source" }), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Title", + "description": "The title of the document", + "default": null + }), + "type": Schema.Literal("document").annotate({ "title": "Type", "default": "document" }) +}).annotate({ "title": "ResponseDocumentBlock" }) +export type BetaResponseContextManagement = { + readonly "applied_edits": ReadonlyArray +} +export const BetaResponseContextManagement = Schema.Struct({ + "applied_edits": Schema.Array( + Schema.Union([BetaResponseClearToolUses20250919Edit, BetaResponseClearThinking20251015Edit], { mode: "oneOf" }) + ).annotate({ "title": "Applied Edits", "description": "List of context management edits that were applied." }) +}).annotate({ "title": "ResponseContextManagement" }) +export type BetaResponseCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "code_execution_result" +} +export const BetaResponseCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaResponseCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("code_execution_result").annotate({ "title": "Type", "default": "code_execution_result" }) +}).annotate({ "title": "ResponseCodeExecutionResultBlock" }) +export type BetaResponseEncryptedCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "encrypted_stdout": string + readonly "return_code": number + readonly "stderr": string + readonly "type": "encrypted_code_execution_result" +} +export const BetaResponseEncryptedCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(BetaResponseCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "encrypted_stdout": Schema.String.annotate({ "title": "Encrypted Stdout" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "type": Schema.Literal("encrypted_code_execution_result").annotate({ + "title": "Type", + "default": "encrypted_code_execution_result" + }) +}).annotate({ + "title": "ResponseEncryptedCodeExecutionResultBlock", + "description": "Code execution result with encrypted stdout for PFC + web_search results." +}) +export type BetaResponseToolSearchToolSearchResultBlock = { + readonly "tool_references": ReadonlyArray + readonly "type": "tool_search_tool_search_result" +} +export const BetaResponseToolSearchToolSearchResultBlock = Schema.Struct({ + "tool_references": Schema.Array(BetaResponseToolReferenceBlock).annotate({ "title": "Tool References" }), + "type": Schema.Literal("tool_search_tool_search_result").annotate({ + "title": "Type", + "default": "tool_search_tool_search_result" + }) +}).annotate({ "title": "ResponseToolSearchToolSearchResultBlock" }) +export type BetaCitationsDelta = { + readonly "citation": + | BetaResponseCharLocationCitation + | BetaResponsePageLocationCitation + | BetaResponseContentBlockLocationCitation + | BetaResponseWebSearchResultLocationCitation + | BetaResponseSearchResultLocationCitation + readonly "type": "citations_delta" +} +export const BetaCitationsDelta = Schema.Struct({ + "citation": Schema.Union([ + BetaResponseCharLocationCitation, + BetaResponsePageLocationCitation, + BetaResponseContentBlockLocationCitation, + BetaResponseWebSearchResultLocationCitation, + BetaResponseSearchResultLocationCitation + ], { mode: "oneOf" }).annotate({ "title": "Citation" }), + "type": Schema.Literal("citations_delta").annotate({ "title": "Type", "default": "citations_delta" }) +}).annotate({ "title": "CitationsDelta" }) +export type BetaResponseMCPToolResultBlock = { + readonly "content": + | string + | ReadonlyArray< + { + readonly "citations": + | ReadonlyArray< + | BetaResponseCharLocationCitation + | BetaResponsePageLocationCitation + | BetaResponseContentBlockLocationCitation + | BetaResponseWebSearchResultLocationCitation + | BetaResponseSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" + } + > + readonly "is_error": boolean + readonly "tool_use_id": string + readonly "type": "mcp_tool_result" +} +export const BetaResponseMCPToolResultBlock = Schema.Struct({ + "content": Schema.Union([ + Schema.String, + Schema.Array( + Schema.Struct({ + "citations": Schema.Union([ + Schema.Array( + Schema.Union([ + BetaResponseCharLocationCitation, + BetaResponsePageLocationCitation, + BetaResponseContentBlockLocationCitation, + BetaResponseWebSearchResultLocationCitation, + BetaResponseSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ + "title": "Citations", + "description": + "Citations supporting the text block.\n\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.", + "default": null + }), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(0)).check( + Schema.isMaxLength(5000000) + ), + "type": Schema.Literal("text").annotate({ "title": "Type", "default": "text" }) + }).annotate({ "title": "beta_mcp_tool_result_block_content_item" }) + ).annotate({ "title": "beta_mcp_tool_result_block_content" }) + ]).annotate({ "title": "Content" }), + "is_error": Schema.Boolean.annotate({ "title": "Is Error", "default": false }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$")) + ), + "type": Schema.Literal("mcp_tool_result").annotate({ "title": "Type", "default": "mcp_tool_result" }) +}).annotate({ "title": "ResponseMCPToolResultBlock" }) +export type BetaResponseTextBlock = { + readonly "citations"?: + | ReadonlyArray< + | BetaResponseCharLocationCitation + | BetaResponsePageLocationCitation + | BetaResponseContentBlockLocationCitation + | BetaResponseWebSearchResultLocationCitation + | BetaResponseSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" +} +export const BetaResponseTextBlock = Schema.Struct({ + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + BetaResponseCharLocationCitation, + BetaResponsePageLocationCitation, + BetaResponseContentBlockLocationCitation, + BetaResponseWebSearchResultLocationCitation, + BetaResponseSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ + "title": "Citations", + "description": + "Citations supporting the text block.\n\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.", + "default": null + }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(0)).check(Schema.isMaxLength(5000000)), + "type": Schema.Literal("text").annotate({ "title": "Type", "default": "text" }) +}).annotate({ "title": "ResponseTextBlock" }) +export type BetaRequestServerToolUseBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": + | "web_search" + | "web_fetch" + | "code_execution" + | "bash_code_execution" + | "text_editor_code_execution" + | "tool_search_tool_regex" + | "tool_search_tool_bm25" + readonly "type": "server_tool_use" +} +export const BetaRequestServerToolUseBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.Literals([ + "web_search", + "web_fetch", + "code_execution", + "bash_code_execution", + "text_editor_code_execution", + "tool_search_tool_regex", + "tool_search_tool_bm25" + ]).annotate({ "title": "Name" }), + "type": Schema.Literal("server_tool_use").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestServerToolUseBlock" }) +export type BetaResponseServerToolUseBlock = { + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": + | "web_search" + | "web_fetch" + | "code_execution" + | "bash_code_execution" + | "text_editor_code_execution" + | "tool_search_tool_regex" + | "tool_search_tool_bm25" + readonly "type": "server_tool_use" +} +export const BetaResponseServerToolUseBlock = Schema.Struct({ + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.Literals([ + "web_search", + "web_fetch", + "code_execution", + "bash_code_execution", + "text_editor_code_execution", + "tool_search_tool_regex", + "tool_search_tool_bm25" + ]).annotate({ "title": "Name" }), + "type": Schema.Literal("server_tool_use").annotate({ "title": "Type", "default": "server_tool_use" }) +}).annotate({ "title": "ResponseServerToolUseBlock" }) +export type BetaResponseToolUseBlock = { + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "type": "tool_use" +} +export const BetaResponseToolUseBlock = Schema.Struct({ + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("tool_use").annotate({ "title": "Type", "default": "tool_use" }) +}).annotate({ "title": "ResponseToolUseBlock" }) +export type BetaContainer = { + readonly "expires_at": string + readonly "id": string + readonly "skills": ReadonlyArray | null +} +export const BetaContainer = Schema.Struct({ + "expires_at": Schema.String.annotate({ + "title": "Expires At", + "description": "The time at which the container will expire.", + "format": "date-time" + }), + "id": Schema.String.annotate({ "title": "Id", "description": "Identifier for the container used in this request" }), + "skills": Schema.Union([Schema.Array(BetaSkill), Schema.Null]).annotate({ + "title": "Skills", + "description": "Skills loaded in the container", + "default": null + }) +}).annotate({ + "title": "Container", + "description": "Information about the container used in the request (for the code execution tool)" +}) +export type BetaContainerParams = { + readonly "id"?: string | null + readonly "skills"?: ReadonlyArray | null +} +export const BetaContainerParams = Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Id", "description": "Container id" }) + ), + "skills": Schema.optionalKey( + Schema.Union([Schema.Array(BetaSkillParams).check(Schema.isMaxLength(8)), Schema.Null]).annotate({ + "title": "Skills", + "description": "List of skills to load in the container" + }) + ) +}).annotate({ "title": "ContainerParams", "description": "Container parameters with skills to be loaded." }) +export type BetaListSkillVersionsResponse = { + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const BetaListSkillVersionsResponse = Schema.Struct({ + "data": Schema.Array(BetaSkillVersion).annotate({ "title": "Data", "description": "List of skill versions." }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "next_page": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Next Page", + "description": "Token to provide in as `page` in the subsequent request to retrieve the next page of data." + }) +}).annotate({ "title": "ListSkillVersionsResponse" }) +export type BetaRequestTextEditorCodeExecutionToolResultError = { + readonly "error_code": BetaTextEditorCodeExecutionToolResultErrorCode + readonly "error_message"?: string | null + readonly "type": "text_editor_code_execution_tool_result_error" +} +export const BetaRequestTextEditorCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaTextEditorCodeExecutionToolResultErrorCode, + "error_message": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message" }) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionToolResultError" }) +export type BetaResponseTextEditorCodeExecutionToolResultError = { + readonly "error_code": BetaTextEditorCodeExecutionToolResultErrorCode + readonly "error_message": string | null + readonly "type": "text_editor_code_execution_tool_result_error" +} +export const BetaResponseTextEditorCodeExecutionToolResultError = Schema.Struct({ + "error_code": BetaTextEditorCodeExecutionToolResultErrorCode, + "error_message": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message", "default": null }), + "type": Schema.Literal("text_editor_code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "text_editor_code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionToolResultError" }) +export type BetaThinkingConfigParam = + | BetaThinkingConfigEnabled + | BetaThinkingConfigDisabled + | BetaThinkingConfigAdaptive +export const BetaThinkingConfigParam = Schema.Union([ + BetaThinkingConfigEnabled, + BetaThinkingConfigDisabled, + BetaThinkingConfigAdaptive +], { mode: "oneOf" }).annotate({ + "title": "Thinking", + "description": + "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details." +}) +export type BetaClearThinking20251015 = { + readonly "keep"?: BetaThinkingTurns | BetaAllThinkingTurns | "all" + readonly "type": "clear_thinking_20251015" +} +export const BetaClearThinking20251015 = Schema.Struct({ + "keep": Schema.optionalKey( + Schema.Union([Schema.Union([BetaThinkingTurns, BetaAllThinkingTurns], { mode: "oneOf" }), Schema.Literal("all")]) + .annotate({ + "title": "Keep", + "description": + "Number of most recent assistant turns to keep thinking blocks for. Older turns will have their thinking blocks removed." + }) + ), + "type": Schema.Literal("clear_thinking_20251015").annotate({ "title": "Type" }) +}).annotate({ "title": "ClearThinking20251015" }) +export type BetaToolChoice = BetaToolChoiceAuto | BetaToolChoiceAny | BetaToolChoiceTool | BetaToolChoiceNone +export const BetaToolChoice = Schema.Union([ + BetaToolChoiceAuto, + BetaToolChoiceAny, + BetaToolChoiceTool, + BetaToolChoiceNone +], { mode: "oneOf" }).annotate({ + "title": "Tool Choice", + "description": + "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all." +}) +export type BetaRequestToolSearchToolResultError = { + readonly "error_code": BetaToolSearchToolResultErrorCode + readonly "type": "tool_search_tool_result_error" +} +export const BetaRequestToolSearchToolResultError = Schema.Struct({ + "error_code": BetaToolSearchToolResultErrorCode, + "type": Schema.Literal("tool_search_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolResultError" }) +export type BetaResponseToolSearchToolResultError = { + readonly "error_code": BetaToolSearchToolResultErrorCode + readonly "error_message": string | null + readonly "type": "tool_search_tool_result_error" +} +export const BetaResponseToolSearchToolResultError = Schema.Struct({ + "error_code": BetaToolSearchToolResultErrorCode, + "error_message": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message", "default": null }), + "type": Schema.Literal("tool_search_tool_result_error").annotate({ + "title": "Type", + "default": "tool_search_tool_result_error" + }) +}).annotate({ "title": "ResponseToolSearchToolResultError" }) +export type BetaClearToolUses20250919 = { + readonly "clear_at_least"?: BetaInputTokensClearAtLeast | null + readonly "clear_tool_inputs"?: boolean | ReadonlyArray | null + readonly "exclude_tools"?: ReadonlyArray | null + readonly "keep"?: BetaToolUsesKeep + readonly "trigger"?: BetaInputTokensTrigger | BetaToolUsesTrigger + readonly "type": "clear_tool_uses_20250919" +} +export const BetaClearToolUses20250919 = Schema.Struct({ + "clear_at_least": Schema.optionalKey( + Schema.Union([BetaInputTokensClearAtLeast, Schema.Null]).annotate({ + "description": + "Minimum number of tokens that must be cleared when triggered. Context will only be modified if at least this many tokens can be removed." + }) + ), + "clear_tool_inputs": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Clear Tool Inputs", + "description": "Whether to clear all tool inputs (bool) or specific tool inputs to clear (list)" + }) + ), + "exclude_tools": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Exclude Tools", + "description": "Tool names whose uses are preserved from clearing" + }) + ), + "keep": Schema.optionalKey( + Schema.Union([BetaToolUsesKeep], { mode: "oneOf" }).annotate({ + "title": "Keep", + "description": "Number of tool uses to retain in the conversation" + }) + ), + "trigger": Schema.optionalKey( + Schema.Union([BetaInputTokensTrigger, BetaToolUsesTrigger], { mode: "oneOf" }).annotate({ + "title": "Trigger", + "description": "Condition that triggers the context management strategy" + }) + ), + "type": Schema.Literal("clear_tool_uses_20250919").annotate({ "title": "Type" }) +}).annotate({ "title": "ClearToolUses20250919" }) +export type BetaRequestImageBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "source": BetaBase64ImageSource | BetaURLImageSource | BetaFileImageSource + readonly "type": "image" +} +export const BetaRequestImageBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "source": Schema.Union([BetaBase64ImageSource, BetaURLImageSource, BetaFileImageSource], { mode: "oneOf" }).annotate({ + "title": "Source" + }), + "type": Schema.Literal("image").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestImageBlock" }) +export type BetaWebSearchTool_20250305 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "strict"?: boolean + readonly "type": "web_search_20250305" + readonly "user_location"?: BetaUserLocation | null +} +export const BetaWebSearchTool_20250305 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": + "If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`." + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": + "If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`." + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_search").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_search_20250305").annotate({ "title": "Type" }), + "user_location": Schema.optionalKey( + Schema.Union([BetaUserLocation, Schema.Null]).annotate({ + "description": "Parameters for the user's location. Used to provide more relevant search results." + }) + ) +}).annotate({ "title": "WebSearchTool_20250305" }) +export type BetaWebSearchTool_20260209 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "strict"?: boolean + readonly "type": "web_search_20260209" + readonly "user_location"?: BetaUserLocation | null +} +export const BetaWebSearchTool_20260209 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": + "If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`." + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": + "If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`." + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_search").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_search_20260209").annotate({ "title": "Type" }), + "user_location": Schema.optionalKey( + Schema.Union([BetaUserLocation, Schema.Null]).annotate({ + "description": "Parameters for the user's location. Used to provide more relevant search results." + }) + ) +}).annotate({ "title": "WebSearchTool_20260209" }) +export type BetaRequestWebFetchToolResultError = { + readonly "error_code": BetaWebFetchToolResultErrorCode + readonly "type": "web_fetch_tool_result_error" +} +export const BetaRequestWebFetchToolResultError = Schema.Struct({ + "error_code": BetaWebFetchToolResultErrorCode, + "type": Schema.Literal("web_fetch_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebFetchToolResultError" }) +export type BetaResponseWebFetchToolResultError = { + readonly "error_code": BetaWebFetchToolResultErrorCode + readonly "type": "web_fetch_tool_result_error" +} +export const BetaResponseWebFetchToolResultError = Schema.Struct({ + "error_code": BetaWebFetchToolResultErrorCode, + "type": Schema.Literal("web_fetch_tool_result_error").annotate({ + "title": "Type", + "default": "web_fetch_tool_result_error" + }) +}).annotate({ "title": "ResponseWebFetchToolResultError" }) +export type BetaRequestWebSearchToolResultError = { + readonly "error_code": BetaWebSearchToolResultErrorCode + readonly "type": "web_search_tool_result_error" +} +export const BetaRequestWebSearchToolResultError = Schema.Struct({ + "error_code": BetaWebSearchToolResultErrorCode, + "type": Schema.Literal("web_search_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "Error" }) +export type BetaResponseWebSearchToolResultError = { + readonly "error_code": BetaWebSearchToolResultErrorCode + readonly "type": "web_search_tool_result_error" +} +export const BetaResponseWebSearchToolResultError = Schema.Struct({ + "error_code": BetaWebSearchToolResultErrorCode, + "type": Schema.Literal("web_search_tool_result_error").annotate({ + "title": "Type", + "default": "web_search_tool_result_error" + }) +}).annotate({ "title": "ResponseWebSearchToolResultError" }) +export type BetaListSkillsResponse = { + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const BetaListSkillsResponse = Schema.Struct({ + "data": Schema.Array(Betaapi__schemas__skills__Skill).annotate({ "title": "Data", "description": "List of skills." }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": + "Whether there are more results available.\n\nIf `true`, there are additional results that can be fetched using the `next_page` token." + }), + "next_page": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Next Page", + "description": + "Token for fetching the next page of results.\n\nIf `null`, there are no more results available. Pass this value to the `page_token` parameter in the next request to get the next page." + }) +}).annotate({ "title": "ListSkillsResponse" }) +export type CodeExecutionTool_20250522 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20250522" +} +export const CodeExecutionTool_20250522 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20250522").annotate({ "title": "Type" }) +}).annotate({ "title": "CodeExecutionTool_20250522" }) +export type CodeExecutionTool_20250825 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20250825" +} +export const CodeExecutionTool_20250825 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20250825").annotate({ "title": "Type" }) +}).annotate({ "title": "CodeExecutionTool_20250825" }) +export type CodeExecutionTool_20260120 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "code_execution" + readonly "strict"?: boolean + readonly "type": "code_execution_20260120" +} +export const CodeExecutionTool_20260120 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("code_execution").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("code_execution_20260120").annotate({ "title": "Type" }) +}).annotate({ + "title": "CodeExecutionTool_20260120", + "description": "Code execution tool with REPL state persistence (daemon mode + gVisor checkpoint)." +}) +export type RequestContainerUploadBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "file_id": string + readonly "type": "container_upload" +} +export const RequestContainerUploadBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "file_id": Schema.String.annotate({ "title": "File Id" }), + "type": Schema.Literal("container_upload").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestContainerUploadBlock", + "description": + "A content block that represents a file to be uploaded to the container\nFiles uploaded via this block will be available in the container's input directory." +}) +export type RequestToolReferenceBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "tool_name": string + readonly "type": "tool_reference" +} +export const RequestToolReferenceBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "tool_name": Schema.String.annotate({ "title": "Tool Name" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(256) + ).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,256}$"))), + "type": Schema.Literal("tool_reference").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestToolReferenceBlock", + "description": "Tool reference block that can be included in tool_result content." +}) +export type ToolSearchToolBM25_20251119 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_bm25" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_bm25_20251119" | "tool_search_tool_bm25" +} +export const ToolSearchToolBM25_20251119 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("tool_search_tool_bm25").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literals(["tool_search_tool_bm25_20251119", "tool_search_tool_bm25"]).annotate({ "title": "Type" }) +}).annotate({ "title": "ToolSearchToolBM25_20251119" }) +export type ToolSearchToolRegex_20251119 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_regex" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_regex_20251119" | "tool_search_tool_regex" +} +export const ToolSearchToolRegex_20251119 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "name": Schema.Literal("tool_search_tool_regex").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literals(["tool_search_tool_regex_20251119", "tool_search_tool_regex"]).annotate({ "title": "Type" }) +}).annotate({ "title": "ToolSearchToolRegex_20251119" }) +export type RequestCodeExecutionToolResultError = { + readonly "error_code": CodeExecutionToolResultErrorCode + readonly "type": "code_execution_tool_result_error" +} +export const RequestCodeExecutionToolResultError = Schema.Struct({ + "error_code": CodeExecutionToolResultErrorCode, + "type": Schema.Literal("code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionToolResultError" }) +export type ResponseCodeExecutionToolResultError = { + readonly "error_code": CodeExecutionToolResultErrorCode + readonly "type": "code_execution_tool_result_error" +} +export const ResponseCodeExecutionToolResultError = Schema.Struct({ + "error_code": CodeExecutionToolResultErrorCode, + "type": Schema.Literal("code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseCodeExecutionToolResultError" }) +export type FileListResponse = { + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "has_more"?: boolean + readonly "last_id"?: string | null +} +export const FileListResponse = Schema.Struct({ + "data": Schema.Array(FileMetadataSchema).annotate({ + "title": "Data", + "description": "List of file metadata objects." + }), + "first_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "ID of the first file in this page of results." + }) + ), + "has_more": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Has More", + "description": "Whether there are more results available.", + "default": false + }) + ), + "last_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "ID of the last file in this page of results." + }) + ) +}).annotate({ "title": "FileListResponse" }) +export type BashTool_20250124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> + readonly "name": "bash" + readonly "strict"?: boolean + readonly "type": "bash_20250124" +} +export const BashTool_20250124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("bash").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("bash_20250124").annotate({ "title": "Type" }) +}).annotate({ "title": "BashTool_20250124" }) +export type MemoryTool_20250818 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> + readonly "name": "memory" + readonly "strict"?: boolean + readonly "type": "memory_20250818" +} +export const MemoryTool_20250818 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("memory").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("memory_20250818").annotate({ "title": "Type" }) +}).annotate({ "title": "MemoryTool_20250818" }) +export type TextEditor_20250124 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> + readonly "name": "str_replace_editor" + readonly "strict"?: boolean + readonly "type": "text_editor_20250124" +} +export const TextEditor_20250124 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("str_replace_editor").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250124").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250124" }) +export type TextEditor_20250429 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> + readonly "name": "str_replace_based_edit_tool" + readonly "strict"?: boolean + readonly "type": "text_editor_20250429" +} +export const TextEditor_20250429 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ), + "name": Schema.Literal("str_replace_based_edit_tool").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250429").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250429" }) +export type TextEditor_20250728 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> + readonly "max_characters"?: number | null + readonly "name": "str_replace_based_edit_tool" + readonly "strict"?: boolean + readonly "type": "text_editor_20250728" +} +export const TextEditor_20250728 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ), + "max_characters": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]).annotate({ + "title": "Max Characters", + "description": + "Maximum number of characters to display when viewing a file. If not specified, defaults to displaying the full file." + }) + ), + "name": Schema.Literal("str_replace_based_edit_tool").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("text_editor_20250728").annotate({ "title": "Type" }) +}).annotate({ "title": "TextEditor_20250728" }) +export type Tool = { + readonly "type"?: null | "custom" + readonly "description"?: string + readonly "name": string + readonly "input_schema": { + readonly "properties"?: { readonly [x: string]: Schema.Json } | null + readonly "required"?: ReadonlyArray | null + readonly "type": "object" + readonly [x: string]: Schema.Json + } + readonly "cache_control"?: CacheControlEphemeral | null + readonly "strict"?: boolean + readonly "eager_input_streaming"?: boolean | null + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "defer_loading"?: boolean + readonly "input_examples"?: ReadonlyArray<{ readonly [x: string]: JsonValue }> +} +export const Tool = Schema.Struct({ + "type": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Literal("custom")]).annotate({ "title": "Type" })), + "description": Schema.optionalKey(Schema.String.annotate({ + "title": "Description", + "description": + "Description of what this tool does.\n\nTool descriptions should be as detailed as possible. The more information that the model has about what the tool is and how to use it, the better it will perform. You can use natural language descriptions to reinforce important aspects of the tool input JSON schema." + })), + "name": Schema.String.annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(128)).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,128}$")) + ), + "input_schema": Schema.StructWithRest( + Schema.Struct({ + "properties": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json), Schema.Null]).annotate({ "title": "Properties" }) + ), + "required": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "title": "Required" }) + ), + "type": Schema.Literal("object").annotate({ "title": "Type" }) + }), + [Schema.Record(Schema.String, Schema.Json)] + ).annotate({ + "title": "InputSchema", + "description": + "[JSON schema](https://json-schema.org/draft/2020-12) for this tool's input.\n\nThis defines the shape of the `input` that your tool accepts and that the model will produce." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "eager_input_streaming": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "title": "Eager Input Streaming", + "description": + "Enable eager input streaming for this tool. When true, tool input parameters will be streamed incrementally as they are generated, and types will be inferred on-the-fly rather than buffering the full JSON output. When false, streaming is disabled for this tool even if the fine-grained-tool-streaming beta is active. When null (default), uses the default behavior based on beta headers." + }) + ), + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "input_examples": Schema.optionalKey( + Schema.Array(Schema.Record(Schema.String, JsonValue)).annotate({ "title": "Input Examples" }) + ) +}).annotate({ "title": "Tool" }) +export type ListResponse_MessageBatch_ = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "has_more": boolean + readonly "last_id": string | null +} +export const ListResponse_MessageBatch_ = Schema.Struct({ + "data": Schema.Array(MessageBatch).annotate({ "title": "Data" }), + "first_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "First ID in the `data` list. Can be used as the `before_id` for the previous page." + }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "last_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "Last ID in the `data` list. Can be used as the `after_id` for the next page." + }) +}).annotate({ "title": "ListResponse[MessageBatch]" }) +export type ListResponse_ModelInfo_ = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "has_more": boolean + readonly "last_id": string | null +} +export const ListResponse_ModelInfo_ = Schema.Struct({ + "data": Schema.Array(ModelInfo).annotate({ "title": "Data" }), + "first_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "First Id", + "description": "First ID in the `data` list. Can be used as the `before_id` for the previous page." + }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "last_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Last Id", + "description": "Last ID in the `data` list. Can be used as the `after_id` for the next page." + }) +}).annotate({ "title": "ListResponse[ModelInfo]" }) +export type ErrorResponse = { + readonly "error": + | InvalidRequestError + | AuthenticationError + | BillingError + | PermissionError + | NotFoundError + | RateLimitError + | GatewayTimeoutError + | APIError + | OverloadedError + readonly "request_id": string | null + readonly "type": "error" +} +export const ErrorResponse = Schema.Struct({ + "error": Schema.Union([ + InvalidRequestError, + AuthenticationError, + BillingError, + PermissionError, + NotFoundError, + RateLimitError, + GatewayTimeoutError, + APIError, + OverloadedError + ], { mode: "oneOf" }).annotate({ "title": "Error" }), + "request_id": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Request Id", "default": null }), + "type": Schema.Literal("error").annotate({ "title": "Type", "default": "error" }) +}).annotate({ "title": "ErrorResponse" }) +export type RequestBashCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "bash_code_execution_result" +} +export const RequestBashCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(RequestBashCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("bash_code_execution_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionResultBlock" }) +export type WebFetchTool_20250910 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig | null + readonly "defer_loading"?: boolean + readonly "max_content_tokens"?: number | null + readonly "max_uses"?: number | null + readonly "name": "web_fetch" + readonly "strict"?: boolean + readonly "type": "web_fetch_20250910" +} +export const WebFetchTool_20250910 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": "List of domains to allow fetching from" + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": "List of domains to block fetching from" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([RequestCitationsConfig, Schema.Null]).annotate({ + "description": "Citations configuration for fetched documents. Citations are disabled by default." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_content_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Content Tokens", + "description": + "Maximum number of tokens used by including web page text content in the context. The limit is approximate and does not apply to binary content such as PDFs." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_fetch").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_fetch_20250910").annotate({ "title": "Type" }) +}).annotate({ "title": "WebFetchTool_20250910" }) +export type WebFetchTool_20260209 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig | null + readonly "defer_loading"?: boolean + readonly "max_content_tokens"?: number | null + readonly "max_uses"?: number | null + readonly "name": "web_fetch" + readonly "strict"?: boolean + readonly "type": "web_fetch_20260209" +} +export const WebFetchTool_20260209 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": "List of domains to allow fetching from" + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": "List of domains to block fetching from" + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([RequestCitationsConfig, Schema.Null]).annotate({ + "description": "Citations configuration for fetched documents. Citations are disabled by default." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_content_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Content Tokens", + "description": + "Maximum number of tokens used by including web page text content in the context. The limit is approximate and does not apply to binary content such as PDFs." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_fetch").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_fetch_20260209").annotate({ "title": "Type" }) +}).annotate({ "title": "WebFetchTool_20260209" }) +export type RequestCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "code_execution_result" +} +export const RequestCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(RequestCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("code_execution_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionResultBlock" }) +export type RequestEncryptedCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "encrypted_stdout": string + readonly "return_code": number + readonly "stderr": string + readonly "type": "encrypted_code_execution_result" +} +export const RequestEncryptedCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(RequestCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "encrypted_stdout": Schema.String.annotate({ "title": "Encrypted Stdout" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "type": Schema.Literal("encrypted_code_execution_result").annotate({ "title": "Type" }) +}).annotate({ + "title": "RequestEncryptedCodeExecutionResultBlock", + "description": "Code execution result with encrypted stdout for PFC + web_search results." +}) +export type RequestTextBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: + | ReadonlyArray< + | RequestCharLocationCitation + | RequestPageLocationCitation + | RequestContentBlockLocationCitation + | RequestWebSearchResultLocationCitation + | RequestSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" +} +export const RequestTextBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + RequestCharLocationCitation, + RequestPageLocationCitation, + RequestContentBlockLocationCitation, + RequestWebSearchResultLocationCitation, + RequestSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ "title": "Citations" }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("text").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextBlock" }) +export type ResponseBashCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "bash_code_execution_result" +} +export const ResponseBashCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(ResponseBashCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("bash_code_execution_result").annotate({ + "title": "Type", + "default": "bash_code_execution_result" + }) +}).annotate({ "title": "ResponseBashCodeExecutionResultBlock" }) +export type ResponseDocumentBlock = { + readonly "citations": ResponseCitationsConfig | null + readonly "source": Base64PDFSource | PlainTextSource + readonly "title": string | null + readonly "type": "document" +} +export const ResponseDocumentBlock = Schema.Struct({ + "citations": Schema.Union([ResponseCitationsConfig, Schema.Null]).annotate({ + "description": "Citation configuration for the document", + "default": null + }), + "source": Schema.Union([Base64PDFSource, PlainTextSource], { mode: "oneOf" }).annotate({ "title": "Source" }), + "title": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Title", + "description": "The title of the document", + "default": null + }), + "type": Schema.Literal("document").annotate({ "title": "Type", "default": "document" }) +}).annotate({ "title": "ResponseDocumentBlock" }) +export type ResponseCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "code_execution_result" +} +export const ResponseCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(ResponseCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "stdout": Schema.String.annotate({ "title": "Stdout" }), + "type": Schema.Literal("code_execution_result").annotate({ "title": "Type", "default": "code_execution_result" }) +}).annotate({ "title": "ResponseCodeExecutionResultBlock" }) +export type ResponseEncryptedCodeExecutionResultBlock = { + readonly "content": ReadonlyArray + readonly "encrypted_stdout": string + readonly "return_code": number + readonly "stderr": string + readonly "type": "encrypted_code_execution_result" +} +export const ResponseEncryptedCodeExecutionResultBlock = Schema.Struct({ + "content": Schema.Array(ResponseCodeExecutionOutputBlock).annotate({ "title": "Content" }), + "encrypted_stdout": Schema.String.annotate({ "title": "Encrypted Stdout" }), + "return_code": Schema.Number.annotate({ "title": "Return Code" }).check(Schema.isInt()), + "stderr": Schema.String.annotate({ "title": "Stderr" }), + "type": Schema.Literal("encrypted_code_execution_result").annotate({ + "title": "Type", + "default": "encrypted_code_execution_result" + }) +}).annotate({ + "title": "ResponseEncryptedCodeExecutionResultBlock", + "description": "Code execution result with encrypted stdout for PFC + web_search results." +}) +export type ResponseToolSearchToolSearchResultBlock = { + readonly "tool_references": ReadonlyArray + readonly "type": "tool_search_tool_search_result" +} +export const ResponseToolSearchToolSearchResultBlock = Schema.Struct({ + "tool_references": Schema.Array(ResponseToolReferenceBlock).annotate({ "title": "Tool References" }), + "type": Schema.Literal("tool_search_tool_search_result").annotate({ + "title": "Type", + "default": "tool_search_tool_search_result" + }) +}).annotate({ "title": "ResponseToolSearchToolSearchResultBlock" }) +export type CitationsDelta = { + readonly "citation": + | ResponseCharLocationCitation + | ResponsePageLocationCitation + | ResponseContentBlockLocationCitation + | ResponseWebSearchResultLocationCitation + | ResponseSearchResultLocationCitation + readonly "type": "citations_delta" +} +export const CitationsDelta = Schema.Struct({ + "citation": Schema.Union([ + ResponseCharLocationCitation, + ResponsePageLocationCitation, + ResponseContentBlockLocationCitation, + ResponseWebSearchResultLocationCitation, + ResponseSearchResultLocationCitation + ], { mode: "oneOf" }).annotate({ "title": "Citation" }), + "type": Schema.Literal("citations_delta").annotate({ "title": "Type", "default": "citations_delta" }) +}).annotate({ "title": "CitationsDelta" }) +export type ResponseTextBlock = { + readonly "citations"?: + | ReadonlyArray< + | ResponseCharLocationCitation + | ResponsePageLocationCitation + | ResponseContentBlockLocationCitation + | ResponseWebSearchResultLocationCitation + | ResponseSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" +} +export const ResponseTextBlock = Schema.Struct({ + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + ResponseCharLocationCitation, + ResponsePageLocationCitation, + ResponseContentBlockLocationCitation, + ResponseWebSearchResultLocationCitation, + ResponseSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ + "title": "Citations", + "description": + "Citations supporting the text block.\n\nThe type of citation returned will depend on the type of document being cited. Citing a PDF results in `page_location`, plain text results in `char_location`, and content document results in `content_block_location`.", + "default": null + }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(0)).check(Schema.isMaxLength(5000000)), + "type": Schema.Literal("text").annotate({ "title": "Type", "default": "text" }) +}).annotate({ "title": "ResponseTextBlock" }) +export type RequestServerToolUseBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "caller"?: DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": + | "web_search" + | "web_fetch" + | "code_execution" + | "bash_code_execution" + | "text_editor_code_execution" + | "tool_search_tool_regex" + | "tool_search_tool_bm25" + readonly "type": "server_tool_use" +} +export const RequestServerToolUseBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.Literals([ + "web_search", + "web_fetch", + "code_execution", + "bash_code_execution", + "text_editor_code_execution", + "tool_search_tool_regex", + "tool_search_tool_bm25" + ]).annotate({ "title": "Name" }), + "type": Schema.Literal("server_tool_use").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestServerToolUseBlock" }) +export type ResponseServerToolUseBlock = { + readonly "caller": DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": + | "web_search" + | "web_fetch" + | "code_execution" + | "bash_code_execution" + | "text_editor_code_execution" + | "tool_search_tool_regex" + | "tool_search_tool_bm25" + readonly "type": "server_tool_use" +} +export const ResponseServerToolUseBlock = Schema.Struct({ + "caller": Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller", + "default": { "type": "direct" } + }), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.Literals([ + "web_search", + "web_fetch", + "code_execution", + "bash_code_execution", + "text_editor_code_execution", + "tool_search_tool_regex", + "tool_search_tool_bm25" + ]).annotate({ "title": "Name" }), + "type": Schema.Literal("server_tool_use").annotate({ "title": "Type", "default": "server_tool_use" }) +}).annotate({ "title": "ResponseServerToolUseBlock" }) +export type ResponseToolUseBlock = { + readonly "caller": DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "type": "tool_use" +} +export const ResponseToolUseBlock = Schema.Struct({ + "caller": Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller", + "default": { "type": "direct" } + }), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("tool_use").annotate({ "title": "Type", "default": "tool_use" }) +}).annotate({ "title": "ResponseToolUseBlock" }) +export type ListSkillsResponse = { + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const ListSkillsResponse = Schema.Struct({ + "data": Schema.Array(Skill).annotate({ "title": "Data", "description": "List of skills." }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": + "Whether there are more results available.\n\nIf `true`, there are additional results that can be fetched using the `next_page` token." + }), + "next_page": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Next Page", + "description": + "Token for fetching the next page of results.\n\nIf `null`, there are no more results available. Pass this value to the `page_token` parameter in the next request to get the next page." + }) +}).annotate({ "title": "ListSkillsResponse" }) +export type ListSkillVersionsResponse = { + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const ListSkillVersionsResponse = Schema.Struct({ + "data": Schema.Array(SkillVersion).annotate({ "title": "Data", "description": "List of skill versions." }), + "has_more": Schema.Boolean.annotate({ + "title": "Has More", + "description": "Indicates if there are more results in the requested page direction." + }), + "next_page": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Next Page", + "description": "Token to provide in as `page` in the subsequent request to retrieve the next page of data." + }) +}).annotate({ "title": "ListSkillVersionsResponse" }) +export type RequestTextEditorCodeExecutionToolResultError = { + readonly "error_code": TextEditorCodeExecutionToolResultErrorCode + readonly "error_message"?: string | null + readonly "type": "text_editor_code_execution_tool_result_error" +} +export const RequestTextEditorCodeExecutionToolResultError = Schema.Struct({ + "error_code": TextEditorCodeExecutionToolResultErrorCode, + "error_message": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message" }) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionToolResultError" }) +export type ResponseTextEditorCodeExecutionToolResultError = { + readonly "error_code": TextEditorCodeExecutionToolResultErrorCode + readonly "error_message": string | null + readonly "type": "text_editor_code_execution_tool_result_error" +} +export const ResponseTextEditorCodeExecutionToolResultError = Schema.Struct({ + "error_code": TextEditorCodeExecutionToolResultErrorCode, + "error_message": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message", "default": null }), + "type": Schema.Literal("text_editor_code_execution_tool_result_error").annotate({ + "title": "Type", + "default": "text_editor_code_execution_tool_result_error" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionToolResultError" }) +export type ThinkingConfigParam = ThinkingConfigEnabled | ThinkingConfigDisabled | ThinkingConfigAdaptive +export const ThinkingConfigParam = Schema.Union( + [ThinkingConfigEnabled, ThinkingConfigDisabled, ThinkingConfigAdaptive], + { mode: "oneOf" } +).annotate({ + "title": "Thinking", + "description": + "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details." +}) +export type ToolChoice = ToolChoiceAuto | ToolChoiceAny | ToolChoiceTool | ToolChoiceNone +export const ToolChoice = Schema.Union([ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, ToolChoiceNone], { + mode: "oneOf" +}).annotate({ + "title": "Tool Choice", + "description": + "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all." +}) +export type RequestToolSearchToolResultError = { + readonly "error_code": ToolSearchToolResultErrorCode + readonly "type": "tool_search_tool_result_error" +} +export const RequestToolSearchToolResultError = Schema.Struct({ + "error_code": ToolSearchToolResultErrorCode, + "type": Schema.Literal("tool_search_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolResultError" }) +export type ResponseToolSearchToolResultError = { + readonly "error_code": ToolSearchToolResultErrorCode + readonly "error_message": string | null + readonly "type": "tool_search_tool_result_error" +} +export const ResponseToolSearchToolResultError = Schema.Struct({ + "error_code": ToolSearchToolResultErrorCode, + "error_message": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Error Message", "default": null }), + "type": Schema.Literal("tool_search_tool_result_error").annotate({ + "title": "Type", + "default": "tool_search_tool_result_error" + }) +}).annotate({ "title": "ResponseToolSearchToolResultError" }) +export type RequestImageBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "source": Base64ImageSource | URLImageSource + readonly "type": "image" +} +export const RequestImageBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "source": Schema.Union([Base64ImageSource, URLImageSource], { mode: "oneOf" }).annotate({ "title": "Source" }), + "type": Schema.Literal("image").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestImageBlock" }) +export type WebSearchTool_20250305 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "strict"?: boolean + readonly "type": "web_search_20250305" + readonly "user_location"?: UserLocation | null +} +export const WebSearchTool_20250305 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": + "If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`." + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": + "If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`." + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_search").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_search_20250305").annotate({ "title": "Type" }), + "user_location": Schema.optionalKey( + Schema.Union([UserLocation, Schema.Null]).annotate({ + "description": "Parameters for the user's location. Used to provide more relevant search results." + }) + ) +}).annotate({ "title": "WebSearchTool_20250305" }) +export type WebSearchTool_20260209 = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: CacheControlEphemeral | null + readonly "defer_loading"?: boolean + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "strict"?: boolean + readonly "type": "web_search_20260209" + readonly "user_location"?: UserLocation | null +} +export const WebSearchTool_20260209 = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Array(Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"])).annotate({ + "title": "Allowed Callers" + }) + ), + "allowed_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Allowed Domains", + "description": + "If provided, only these domains will be included in results. Cannot be used alongside `blocked_domains`." + }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "title": "Blocked Domains", + "description": + "If provided, these domains will never appear in results. Cannot be used alongside `allowed_domains`." + }) + ), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Defer Loading", + "description": + "If true, tool will not be included in initial system prompt. Only loaded when returned via tool_reference from tool search." + }) + ), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)), Schema.Null]).annotate({ + "title": "Max Uses", + "description": "Maximum number of times the tool can be used in the API request." + }) + ), + "name": Schema.Literal("web_search").annotate({ + "title": "Name", + "description": "Name of the tool.\n\nThis is how the tool will be called by the model and in `tool_use` blocks." + }), + "strict": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Strict", + "description": "When true, guarantees schema validation on tool names and inputs" + }) + ), + "type": Schema.Literal("web_search_20260209").annotate({ "title": "Type" }), + "user_location": Schema.optionalKey( + Schema.Union([UserLocation, Schema.Null]).annotate({ + "description": "Parameters for the user's location. Used to provide more relevant search results." + }) + ) +}).annotate({ "title": "WebSearchTool_20260209" }) +export type RequestWebFetchToolResultError = { + readonly "error_code": WebFetchToolResultErrorCode + readonly "type": "web_fetch_tool_result_error" +} +export const RequestWebFetchToolResultError = Schema.Struct({ + "error_code": WebFetchToolResultErrorCode, + "type": Schema.Literal("web_fetch_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebFetchToolResultError" }) +export type ResponseWebFetchToolResultError = { + readonly "error_code": WebFetchToolResultErrorCode + readonly "type": "web_fetch_tool_result_error" +} +export const ResponseWebFetchToolResultError = Schema.Struct({ + "error_code": WebFetchToolResultErrorCode, + "type": Schema.Literal("web_fetch_tool_result_error").annotate({ + "title": "Type", + "default": "web_fetch_tool_result_error" + }) +}).annotate({ "title": "ResponseWebFetchToolResultError" }) +export type RequestWebSearchToolResultError = { + readonly "error_code": WebSearchToolResultErrorCode + readonly "type": "web_search_tool_result_error" +} +export const RequestWebSearchToolResultError = Schema.Struct({ + "error_code": WebSearchToolResultErrorCode, + "type": Schema.Literal("web_search_tool_result_error").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebSearchToolResultError" }) +export type ResponseWebSearchToolResultError = { + readonly "error_code": WebSearchToolResultErrorCode + readonly "type": "web_search_tool_result_error" +} +export const ResponseWebSearchToolResultError = Schema.Struct({ + "error_code": WebSearchToolResultErrorCode, + "type": Schema.Literal("web_search_tool_result_error").annotate({ + "title": "Type", + "default": "web_search_tool_result_error" + }) +}).annotate({ "title": "ResponseWebSearchToolResultError" }) +export type MessageDelta = { + readonly "container": Container | null + readonly "stop_reason": StopReason | null + readonly "stop_sequence": string | null +} +export const MessageDelta = Schema.Struct({ + "container": Schema.Union([Container, Schema.Null]).annotate({ + "description": + "Information about the container used in this request.\n\nThis will be non-null if a container tool (e.g. code execution) was used.", + "default": null + }), + "stop_reason": Schema.Union([StopReason, Schema.Null]).annotate({ "title": "Stop Reason", "default": null }), + "stop_sequence": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Stop Sequence", "default": null }) +}).annotate({ "title": "MessageDelta" }) +export type CompletionRequest = { + readonly "model": Model + readonly "prompt": string + readonly "max_tokens_to_sample": number + readonly "stop_sequences"?: ReadonlyArray + readonly "temperature"?: number + readonly "top_p"?: number + readonly "top_k"?: number + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "stream"?: boolean +} +export const CompletionRequest = Schema.Struct({ + "model": Model, + "prompt": Schema.String.annotate({ + "title": "Prompt", + "description": + "The prompt that you want Claude to complete.\n\nFor proper response generation you will need to format your prompt using alternating `\\n\\nHuman:` and `\\n\\nAssistant:` conversational turns. For example:\n\n```\n\"\\n\\nHuman: {userQuestion}\\n\\nAssistant:\"\n```\n\nSee [prompt validation](https://docs.claude.com/en/api/prompt-validation) and our guide to [prompt design](https://docs.claude.com/en/docs/intro-to-prompting) for more details." + }).check(Schema.isMinLength(1)), + "max_tokens_to_sample": Schema.Number.annotate({ + "title": "Max Tokens To Sample", + "description": + "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "stop_sequences": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "title": "Stop Sequences", + "description": + "Sequences that will cause the model to stop generating.\n\nOur models stop on `\"\\n\\nHuman:\"`, and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Temperature", + "description": + "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top P", + "description": + "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "top_k": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top K", + "description": + "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ), + "metadata": Schema.optionalKey( + Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMaxLength(256)), Schema.Null]).annotate({ + "title": "User Id", + "description": + "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number." + }) + ) + }).annotate({ "title": "Metadata", "description": "An object describing metadata about the request." }) + ), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Stream", + "description": + "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/streaming) for details." + }) + ) +}).annotate({ "title": "CompletionRequest" }) +export type CompletionResponse = { + readonly "completion": string + readonly "id": string + readonly "model": Model + readonly "stop_reason": string | null + readonly "type": "completion" +} +export const CompletionResponse = Schema.Struct({ + "completion": Schema.String.annotate({ + "title": "Completion", + "description": "The resulting completion up to and excluding the stop sequences." + }), + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "model": Model, + "stop_reason": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Stop Reason", + "description": + "The reason that we stopped.\n\nThis may be one the following values:\n* `\"stop_sequence\"`: we reached a stop sequence — either provided by you via the `stop_sequences` parameter, or a stop sequence built into the model\n* `\"max_tokens\"`: we exceeded `max_tokens_to_sample` or the model's maximum" + }), + "type": Schema.Literal("completion").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Text Completions, this is always `\"completion\"`.", + "default": "completion" + }) +}).annotate({ "title": "CompletionResponse" }) +export type BetaRequestToolSearchToolSearchResultBlock = { + readonly "tool_references": ReadonlyArray + readonly "type": "tool_search_tool_search_result" +} +export const BetaRequestToolSearchToolSearchResultBlock = Schema.Struct({ + "tool_references": Schema.Array(BetaRequestToolReferenceBlock).annotate({ "title": "Tool References" }), + "type": Schema.Literal("tool_search_tool_search_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolSearchResultBlock" }) +export type BetaIterationsUsage = ReadonlyArray | null +export const BetaIterationsUsage = Schema.Union([ + Schema.Array(Schema.Union([BetaMessageIterationUsage, BetaCompactionIterationUsage])), + Schema.Null +]).annotate({ + "title": "Iterations", + "description": + "Per-iteration token usage breakdown.\n\nEach entry represents one sampling iteration, with its own input/output token counts and cache statistics. This allows you to:\n- Determine which iterations exceeded long context thresholds (>=200k tokens)\n- Calculate the true context window size from the last iteration\n- Understand token accumulation across server-side tool use loops", + "default": null +}) +export type BetaErroredResult = { readonly "error": BetaErrorResponse; readonly "type": "errored" } +export const BetaErroredResult = Schema.Struct({ + "error": BetaErrorResponse, + "type": Schema.Literal("errored").annotate({ "title": "Type", "default": "errored" }) +}).annotate({ "title": "ErroredResult" }) +export type BetaRequestBashCodeExecutionToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content": BetaRequestBashCodeExecutionToolResultError | BetaRequestBashCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "bash_code_execution_tool_result" +} +export const BetaRequestBashCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([BetaRequestBashCodeExecutionToolResultError, BetaRequestBashCodeExecutionResultBlock]) + .annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("bash_code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionToolResultBlock" }) +export type BetaRequestCodeExecutionToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content": + | BetaRequestCodeExecutionToolResultError + | BetaRequestCodeExecutionResultBlock + | BetaRequestEncryptedCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "code_execution_tool_result" +} +export const BetaRequestCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([ + BetaRequestCodeExecutionToolResultError, + BetaRequestCodeExecutionResultBlock, + BetaRequestEncryptedCodeExecutionResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionToolResultBlock" }) +export type BetaRequestSearchResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig + readonly "content": ReadonlyArray + readonly "source": string + readonly "title": string + readonly "type": "search_result" +} +export const BetaRequestSearchResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(BetaRequestCitationsConfig), + "content": Schema.Array(BetaRequestTextBlock).annotate({ "title": "Content" }), + "source": Schema.String.annotate({ "title": "Source" }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("search_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestSearchResultBlock" }) +export type BetaResponseBashCodeExecutionToolResultBlock = { + readonly "content": BetaResponseBashCodeExecutionToolResultError | BetaResponseBashCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "bash_code_execution_tool_result" +} +export const BetaResponseBashCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([BetaResponseBashCodeExecutionToolResultError, BetaResponseBashCodeExecutionResultBlock]) + .annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("bash_code_execution_tool_result").annotate({ + "title": "Type", + "default": "bash_code_execution_tool_result" + }) +}).annotate({ "title": "ResponseBashCodeExecutionToolResultBlock" }) +export type BetaResponseWebFetchResultBlock = { + readonly "content": BetaResponseDocumentBlock + readonly "retrieved_at": string | null + readonly "type": "web_fetch_result" + readonly "url": string +} +export const BetaResponseWebFetchResultBlock = Schema.Struct({ + "content": BetaResponseDocumentBlock, + "retrieved_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Retrieved At", + "description": "ISO 8601 timestamp when the content was retrieved", + "default": null + }), + "type": Schema.Literal("web_fetch_result").annotate({ "title": "Type", "default": "web_fetch_result" }), + "url": Schema.String.annotate({ "title": "Url", "description": "Fetched content URL" }) +}).annotate({ "title": "ResponseWebFetchResultBlock" }) +export type BetaResponseCodeExecutionToolResultBlock = { + readonly "content": + | BetaResponseCodeExecutionToolResultError + | BetaResponseCodeExecutionResultBlock + | BetaResponseEncryptedCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "code_execution_tool_result" +} +export const BetaResponseCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([ + BetaResponseCodeExecutionToolResultError, + BetaResponseCodeExecutionResultBlock, + BetaResponseEncryptedCodeExecutionResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_tool_result").annotate({ + "title": "Type", + "default": "code_execution_tool_result" + }) +}).annotate({ "title": "ResponseCodeExecutionToolResultBlock" }) +export type BetaContentBlockDeltaEvent = { + readonly "delta": + | BetaTextContentBlockDelta + | BetaInputJsonContentBlockDelta + | BetaCitationsDelta + | BetaThinkingContentBlockDelta + | BetaSignatureContentBlockDelta + | BetaCompactionContentBlockDelta + readonly "index": number + readonly "type": "content_block_delta" +} +export const BetaContentBlockDeltaEvent = Schema.Struct({ + "delta": Schema.Union([ + BetaTextContentBlockDelta, + BetaInputJsonContentBlockDelta, + BetaCitationsDelta, + BetaThinkingContentBlockDelta, + BetaSignatureContentBlockDelta, + BetaCompactionContentBlockDelta + ], { mode: "oneOf" }).annotate({ "title": "Delta" }), + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_delta").annotate({ "title": "Type", "default": "content_block_delta" }) +}).annotate({ "title": "ContentBlockDeltaEvent" }) +export type BetaMessageDelta = { + readonly "container"?: BetaContainer | null + readonly "stop_reason": BetaStopReason | null + readonly "stop_sequence": string | null +} +export const BetaMessageDelta = Schema.Struct({ + "container": Schema.optionalKey( + Schema.Union([BetaContainer, Schema.Null]).annotate({ + "description": + "Information about the container used in this request.\n\nThis will be non-null if a container tool (e.g. code execution) was used.", + "default": null + }) + ), + "stop_reason": Schema.Union([BetaStopReason, Schema.Null]).annotate({ "title": "Stop Reason", "default": null }), + "stop_sequence": Schema.Union([Schema.String, Schema.Null]).annotate({ "title": "Stop Sequence", "default": null }) +}).annotate({ "title": "MessageDelta" }) +export type BetaRequestTextEditorCodeExecutionToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content": + | BetaRequestTextEditorCodeExecutionToolResultError + | BetaRequestTextEditorCodeExecutionViewResultBlock + | BetaRequestTextEditorCodeExecutionCreateResultBlock + | BetaRequestTextEditorCodeExecutionStrReplaceResultBlock + readonly "tool_use_id": string + readonly "type": "text_editor_code_execution_tool_result" +} +export const BetaRequestTextEditorCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([ + BetaRequestTextEditorCodeExecutionToolResultError, + BetaRequestTextEditorCodeExecutionViewResultBlock, + BetaRequestTextEditorCodeExecutionCreateResultBlock, + BetaRequestTextEditorCodeExecutionStrReplaceResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionToolResultBlock" }) +export type BetaResponseTextEditorCodeExecutionToolResultBlock = { + readonly "content": + | BetaResponseTextEditorCodeExecutionToolResultError + | BetaResponseTextEditorCodeExecutionViewResultBlock + | BetaResponseTextEditorCodeExecutionCreateResultBlock + | BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + readonly "tool_use_id": string + readonly "type": "text_editor_code_execution_tool_result" +} +export const BetaResponseTextEditorCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([ + BetaResponseTextEditorCodeExecutionToolResultError, + BetaResponseTextEditorCodeExecutionViewResultBlock, + BetaResponseTextEditorCodeExecutionCreateResultBlock, + BetaResponseTextEditorCodeExecutionStrReplaceResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_tool_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionToolResultBlock" }) +export type BetaResponseToolSearchToolResultBlock = { + readonly "content": BetaResponseToolSearchToolResultError | BetaResponseToolSearchToolSearchResultBlock + readonly "tool_use_id": string + readonly "type": "tool_search_tool_result" +} +export const BetaResponseToolSearchToolResultBlock = Schema.Struct({ + "content": Schema.Union([BetaResponseToolSearchToolResultError, BetaResponseToolSearchToolSearchResultBlock]) + .annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("tool_search_tool_result").annotate({ "title": "Type", "default": "tool_search_tool_result" }) +}).annotate({ "title": "ResponseToolSearchToolResultBlock" }) +export type BetaContextManagementConfig = { + readonly "edits"?: ReadonlyArray +} +export const BetaContextManagementConfig = Schema.Struct({ + "edits": Schema.optionalKey( + Schema.Array( + Schema.Union([BetaClearToolUses20250919, BetaClearThinking20251015, BetaCompact20260112], { mode: "oneOf" }) + ).annotate({ "title": "Edits", "description": "List of context management edits to apply" }).check( + Schema.isMinLength(0) + ) + ) +}).annotate({ "title": "ContextManagementConfig" }) +export type BetaContentBlockSource = { + readonly "content": string | ReadonlyArray + readonly "type": "content" +} +export const BetaContentBlockSource = Schema.Struct({ + "content": Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([BetaRequestTextBlock, BetaRequestImageBlock], { mode: "oneOf" }).annotate({ + "title": "beta_content_block_source_content_item" + }) + ).annotate({ "title": "beta_content_block_source_content" }) + ]).annotate({ "title": "Content" }), + "type": Schema.Literal("content").annotate({ "title": "Type" }) +}).annotate({ "title": "ContentBlockSource" }) +export type BetaRequestWebSearchToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "content": ReadonlyArray | BetaRequestWebSearchToolResultError + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" +} +export const BetaRequestWebSearchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([ + Schema.Array(BetaRequestWebSearchResultBlock).annotate({ "title": "Result Block" }), + BetaRequestWebSearchToolResultError + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_search_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebSearchToolResultBlock" }) +export type BetaResponseWebSearchToolResultBlock = { + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "content": BetaResponseWebSearchToolResultError | ReadonlyArray + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" +} +export const BetaResponseWebSearchToolResultBlock = Schema.Struct({ + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([BetaResponseWebSearchToolResultError, Schema.Array(BetaResponseWebSearchResultBlock)]) + .annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_search_tool_result").annotate({ "title": "Type", "default": "web_search_tool_result" }) +}).annotate({ "title": "ResponseWebSearchToolResultBlock" }) +export type RequestToolSearchToolSearchResultBlock = { + readonly "tool_references": ReadonlyArray + readonly "type": "tool_search_tool_search_result" +} +export const RequestToolSearchToolSearchResultBlock = Schema.Struct({ + "tool_references": Schema.Array(RequestToolReferenceBlock).annotate({ "title": "Tool References" }), + "type": Schema.Literal("tool_search_tool_search_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolSearchResultBlock" }) +export type ErroredResult = { readonly "error": ErrorResponse; readonly "type": "errored" } +export const ErroredResult = Schema.Struct({ + "error": ErrorResponse, + "type": Schema.Literal("errored").annotate({ "title": "Type", "default": "errored" }) +}).annotate({ "title": "ErroredResult" }) +export type RequestBashCodeExecutionToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "content": RequestBashCodeExecutionToolResultError | RequestBashCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "bash_code_execution_tool_result" +} +export const RequestBashCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([RequestBashCodeExecutionToolResultError, RequestBashCodeExecutionResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("bash_code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestBashCodeExecutionToolResultBlock" }) +export type RequestCodeExecutionToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "content": + | RequestCodeExecutionToolResultError + | RequestCodeExecutionResultBlock + | RequestEncryptedCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "code_execution_tool_result" +} +export const RequestCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([ + RequestCodeExecutionToolResultError, + RequestCodeExecutionResultBlock, + RequestEncryptedCodeExecutionResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestCodeExecutionToolResultBlock" }) +export type RequestSearchResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig + readonly "content": ReadonlyArray + readonly "source": string + readonly "title": string + readonly "type": "search_result" +} +export const RequestSearchResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(RequestCitationsConfig), + "content": Schema.Array(RequestTextBlock).annotate({ "title": "Content" }), + "source": Schema.String.annotate({ "title": "Source" }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("search_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestSearchResultBlock" }) +export type ResponseBashCodeExecutionToolResultBlock = { + readonly "content": ResponseBashCodeExecutionToolResultError | ResponseBashCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "bash_code_execution_tool_result" +} +export const ResponseBashCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([ResponseBashCodeExecutionToolResultError, ResponseBashCodeExecutionResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("bash_code_execution_tool_result").annotate({ + "title": "Type", + "default": "bash_code_execution_tool_result" + }) +}).annotate({ "title": "ResponseBashCodeExecutionToolResultBlock" }) +export type ResponseWebFetchResultBlock = { + readonly "content": ResponseDocumentBlock + readonly "retrieved_at": string | null + readonly "type": "web_fetch_result" + readonly "url": string +} +export const ResponseWebFetchResultBlock = Schema.Struct({ + "content": ResponseDocumentBlock, + "retrieved_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Retrieved At", + "description": "ISO 8601 timestamp when the content was retrieved", + "default": null + }), + "type": Schema.Literal("web_fetch_result").annotate({ "title": "Type", "default": "web_fetch_result" }), + "url": Schema.String.annotate({ "title": "Url", "description": "Fetched content URL" }) +}).annotate({ "title": "ResponseWebFetchResultBlock" }) +export type ResponseCodeExecutionToolResultBlock = { + readonly "content": + | ResponseCodeExecutionToolResultError + | ResponseCodeExecutionResultBlock + | ResponseEncryptedCodeExecutionResultBlock + readonly "tool_use_id": string + readonly "type": "code_execution_tool_result" +} +export const ResponseCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([ + ResponseCodeExecutionToolResultError, + ResponseCodeExecutionResultBlock, + ResponseEncryptedCodeExecutionResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("code_execution_tool_result").annotate({ + "title": "Type", + "default": "code_execution_tool_result" + }) +}).annotate({ "title": "ResponseCodeExecutionToolResultBlock" }) +export type ContentBlockDeltaEvent = { + readonly "delta": + | TextContentBlockDelta + | InputJsonContentBlockDelta + | CitationsDelta + | ThinkingContentBlockDelta + | SignatureContentBlockDelta + readonly "index": number + readonly "type": "content_block_delta" +} +export const ContentBlockDeltaEvent = Schema.Struct({ + "delta": Schema.Union([ + TextContentBlockDelta, + InputJsonContentBlockDelta, + CitationsDelta, + ThinkingContentBlockDelta, + SignatureContentBlockDelta + ], { mode: "oneOf" }).annotate({ "title": "Delta" }), + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_delta").annotate({ "title": "Type", "default": "content_block_delta" }) +}).annotate({ "title": "ContentBlockDeltaEvent" }) +export type RequestTextEditorCodeExecutionToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "content": + | RequestTextEditorCodeExecutionToolResultError + | RequestTextEditorCodeExecutionViewResultBlock + | RequestTextEditorCodeExecutionCreateResultBlock + | RequestTextEditorCodeExecutionStrReplaceResultBlock + readonly "tool_use_id": string + readonly "type": "text_editor_code_execution_tool_result" +} +export const RequestTextEditorCodeExecutionToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([ + RequestTextEditorCodeExecutionToolResultError, + RequestTextEditorCodeExecutionViewResultBlock, + RequestTextEditorCodeExecutionCreateResultBlock, + RequestTextEditorCodeExecutionStrReplaceResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestTextEditorCodeExecutionToolResultBlock" }) +export type ResponseTextEditorCodeExecutionToolResultBlock = { + readonly "content": + | ResponseTextEditorCodeExecutionToolResultError + | ResponseTextEditorCodeExecutionViewResultBlock + | ResponseTextEditorCodeExecutionCreateResultBlock + | ResponseTextEditorCodeExecutionStrReplaceResultBlock + readonly "tool_use_id": string + readonly "type": "text_editor_code_execution_tool_result" +} +export const ResponseTextEditorCodeExecutionToolResultBlock = Schema.Struct({ + "content": Schema.Union([ + ResponseTextEditorCodeExecutionToolResultError, + ResponseTextEditorCodeExecutionViewResultBlock, + ResponseTextEditorCodeExecutionCreateResultBlock, + ResponseTextEditorCodeExecutionStrReplaceResultBlock + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("text_editor_code_execution_tool_result").annotate({ + "title": "Type", + "default": "text_editor_code_execution_tool_result" + }) +}).annotate({ "title": "ResponseTextEditorCodeExecutionToolResultBlock" }) +export type ResponseToolSearchToolResultBlock = { + readonly "content": ResponseToolSearchToolResultError | ResponseToolSearchToolSearchResultBlock + readonly "tool_use_id": string + readonly "type": "tool_search_tool_result" +} +export const ResponseToolSearchToolResultBlock = Schema.Struct({ + "content": Schema.Union([ResponseToolSearchToolResultError, ResponseToolSearchToolSearchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("tool_search_tool_result").annotate({ "title": "Type", "default": "tool_search_tool_result" }) +}).annotate({ "title": "ResponseToolSearchToolResultBlock" }) +export type ContentBlockSource = { + readonly "content": string | ReadonlyArray + readonly "type": "content" +} +export const ContentBlockSource = Schema.Struct({ + "content": Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([RequestTextBlock, RequestImageBlock], { mode: "oneOf" }).annotate({ + "title": "content_block_source_content_item" + }) + ).annotate({ "title": "content_block_source_content" }) + ]).annotate({ "title": "Content" }), + "type": Schema.Literal("content").annotate({ "title": "Type" }) +}).annotate({ "title": "ContentBlockSource" }) +export type RequestWebSearchToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "caller"?: DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "content": ReadonlyArray | RequestWebSearchToolResultError + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" +} +export const RequestWebSearchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([ + Schema.Array(RequestWebSearchResultBlock).annotate({ "title": "web_search_tool_result_block_item" }), + RequestWebSearchToolResultError + ]).annotate({ "title": "Content" }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_search_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebSearchToolResultBlock" }) +export type ResponseWebSearchToolResultBlock = { + readonly "caller": DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "content": ResponseWebSearchToolResultError | ReadonlyArray + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" +} +export const ResponseWebSearchToolResultBlock = Schema.Struct({ + "caller": Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller", + "default": { "type": "direct" } + }), + "content": Schema.Union([ResponseWebSearchToolResultError, Schema.Array(ResponseWebSearchResultBlock)]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_search_tool_result").annotate({ "title": "Type", "default": "web_search_tool_result" }) +}).annotate({ "title": "ResponseWebSearchToolResultBlock" }) +export type MessageDeltaEvent = { + readonly "delta": MessageDelta + readonly "type": "message_delta" + readonly "usage": { + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "input_tokens": number | null + readonly "output_tokens": number + readonly "server_tool_use"?: ServerToolUsage | null + } +} +export const MessageDeltaEvent = Schema.Struct({ + "delta": MessageDelta, + "type": Schema.Literal("message_delta").annotate({ "title": "Type", "default": "message_delta" }), + "usage": Schema.Struct({ + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Creation Input Tokens", + "description": "The cumulative number of input tokens used to create the cache entry.", + "default": null + }), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Read Input Tokens", + "description": "The cumulative number of input tokens read from the cache.", + "default": null + }), + "input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Input Tokens", + "description": "The cumulative number of input tokens which were used.", + "default": null + }), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The cumulative number of output tokens which were used." + }).check(Schema.isInt()), + "server_tool_use": Schema.optionalKey( + Schema.Union([ServerToolUsage, Schema.Null]).annotate({ + "description": "The number of server tool requests.", + "default": null + }) + ) + }).annotate({ + "title": "MessageDeltaUsage", + "description": + "Billing and rate-limit usage.\n\nAnthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems.\n\nUnder the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response.\n\nFor example, `output_tokens` will be non-zero, even for an empty string response from Claude.\n\nTotal input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`." + }) +}).annotate({ "title": "MessageDeltaEvent" }) +export type BetaRequestToolSearchToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content": BetaRequestToolSearchToolResultError | BetaRequestToolSearchToolSearchResultBlock + readonly "tool_use_id": string + readonly "type": "tool_search_tool_result" +} +export const BetaRequestToolSearchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([BetaRequestToolSearchToolResultError, BetaRequestToolSearchToolSearchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("tool_search_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolResultBlock" }) +export type BetaResponseWebFetchToolResultBlock = { + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "content": BetaResponseWebFetchToolResultError | BetaResponseWebFetchResultBlock + readonly "tool_use_id": string + readonly "type": "web_fetch_tool_result" +} +export const BetaResponseWebFetchToolResultBlock = Schema.Struct({ + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([BetaResponseWebFetchToolResultError, BetaResponseWebFetchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_fetch_tool_result").annotate({ "title": "Type", "default": "web_fetch_tool_result" }) +}).annotate({ "title": "ResponseWebFetchToolResultBlock" }) +export type BetaMessageDeltaEvent = { + readonly "context_management"?: BetaResponseContextManagement | null + readonly "delta": BetaMessageDelta + readonly "type": "message_delta" + readonly "usage": { + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "input_tokens": number | null + readonly "iterations"?: BetaIterationsUsage + readonly "output_tokens": number + readonly "server_tool_use"?: BetaServerToolUsage | null + } +} +export const BetaMessageDeltaEvent = Schema.Struct({ + "context_management": Schema.optionalKey( + Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ + "description": "Information about context management strategies applied during the request", + "default": null + }) + ), + "delta": BetaMessageDelta, + "type": Schema.Literal("message_delta").annotate({ "title": "Type", "default": "message_delta" }), + "usage": Schema.Struct({ + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Creation Input Tokens", + "description": "The cumulative number of input tokens used to create the cache entry.", + "default": null + }), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Read Input Tokens", + "description": "The cumulative number of input tokens read from the cache.", + "default": null + }), + "input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Input Tokens", + "description": "The cumulative number of input tokens which were used.", + "default": null + }), + "iterations": Schema.optionalKey(BetaIterationsUsage), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The cumulative number of output tokens which were used." + }).check(Schema.isInt()), + "server_tool_use": Schema.optionalKey( + Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ + "description": "The number of server tool requests.", + "default": null + }) + ) + }).annotate({ + "title": "MessageDeltaUsage", + "description": + "Billing and rate-limit usage.\n\nAnthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems.\n\nUnder the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response.\n\nFor example, `output_tokens` will be non-zero, even for an empty string response from Claude.\n\nTotal input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`." + }) +}).annotate({ "title": "MessageDeltaEvent" }) +export type BetaRequestDocumentBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig | null + readonly "context"?: string | null + readonly "source": + | BetaBase64PDFSource + | BetaPlainTextSource + | BetaContentBlockSource + | BetaURLPDFSource + | BetaFileDocumentSource + readonly "title"?: string | null + readonly "type": "document" +} +export const BetaRequestDocumentBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(Schema.Union([BetaRequestCitationsConfig, Schema.Null])), + "context": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)), Schema.Null]).annotate({ "title": "Context" }) + ), + "source": Schema.Union([ + BetaBase64PDFSource, + BetaPlainTextSource, + BetaContentBlockSource, + BetaURLPDFSource, + BetaFileDocumentSource + ], { mode: "oneOf" }).annotate({ "title": "Source" }), + "title": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), Schema.Null]).annotate({ + "title": "Title" + }) + ), + "type": Schema.Literal("document").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestDocumentBlock" }) +export type RequestToolSearchToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "content": RequestToolSearchToolResultError | RequestToolSearchToolSearchResultBlock + readonly "tool_use_id": string + readonly "type": "tool_search_tool_result" +} +export const RequestToolSearchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.Union([RequestToolSearchToolResultError, RequestToolSearchToolSearchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("tool_search_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestToolSearchToolResultBlock" }) +export type ResponseWebFetchToolResultBlock = { + readonly "caller": DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "content": ResponseWebFetchToolResultError | ResponseWebFetchResultBlock + readonly "tool_use_id": string + readonly "type": "web_fetch_tool_result" +} +export const ResponseWebFetchToolResultBlock = Schema.Struct({ + "caller": Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller", + "default": { "type": "direct" } + }), + "content": Schema.Union([ResponseWebFetchToolResultError, ResponseWebFetchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_fetch_tool_result").annotate({ "title": "Type", "default": "web_fetch_tool_result" }) +}).annotate({ "title": "ResponseWebFetchToolResultBlock" }) +export type RequestDocumentBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig | null + readonly "context"?: string | null + readonly "source": Base64PDFSource | PlainTextSource | ContentBlockSource | URLPDFSource + readonly "title"?: string | null + readonly "type": "document" +} +export const RequestDocumentBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(Schema.Union([RequestCitationsConfig, Schema.Null])), + "context": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)), Schema.Null]).annotate({ "title": "Context" }) + ), + "source": Schema.Union([Base64PDFSource, PlainTextSource, ContentBlockSource, URLPDFSource], { mode: "oneOf" }) + .annotate({ "title": "Source" }), + "title": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), Schema.Null]).annotate({ + "title": "Title" + }) + ), + "type": Schema.Literal("document").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestDocumentBlock" }) +export type BetaContentBlockStartEvent = { + readonly "content_block": + | BetaResponseTextBlock + | BetaResponseThinkingBlock + | BetaResponseRedactedThinkingBlock + | BetaResponseToolUseBlock + | BetaResponseServerToolUseBlock + | BetaResponseWebSearchToolResultBlock + | BetaResponseWebFetchToolResultBlock + | BetaResponseCodeExecutionToolResultBlock + | BetaResponseBashCodeExecutionToolResultBlock + | BetaResponseTextEditorCodeExecutionToolResultBlock + | BetaResponseToolSearchToolResultBlock + | BetaResponseMCPToolUseBlock + | BetaResponseMCPToolResultBlock + | BetaResponseContainerUploadBlock + | BetaResponseCompactionBlock + readonly "index": number + readonly "type": "content_block_start" +} +export const BetaContentBlockStartEvent = Schema.Struct({ + "content_block": Schema.Union([ + BetaResponseTextBlock, + BetaResponseThinkingBlock, + BetaResponseRedactedThinkingBlock, + BetaResponseToolUseBlock, + BetaResponseServerToolUseBlock, + BetaResponseWebSearchToolResultBlock, + BetaResponseWebFetchToolResultBlock, + BetaResponseCodeExecutionToolResultBlock, + BetaResponseBashCodeExecutionToolResultBlock, + BetaResponseTextEditorCodeExecutionToolResultBlock, + BetaResponseToolSearchToolResultBlock, + BetaResponseMCPToolUseBlock, + BetaResponseMCPToolResultBlock, + BetaResponseContainerUploadBlock, + BetaResponseCompactionBlock + ], { mode: "oneOf" }).annotate({ "title": "Content Block" }), + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_start").annotate({ "title": "Type", "default": "content_block_start" }) +}).annotate({ "title": "ContentBlockStartEvent" }) +export type BetaContentBlock = + | BetaResponseTextBlock + | BetaResponseThinkingBlock + | BetaResponseRedactedThinkingBlock + | BetaResponseToolUseBlock + | BetaResponseServerToolUseBlock + | BetaResponseWebSearchToolResultBlock + | BetaResponseWebFetchToolResultBlock + | BetaResponseCodeExecutionToolResultBlock + | BetaResponseBashCodeExecutionToolResultBlock + | BetaResponseTextEditorCodeExecutionToolResultBlock + | BetaResponseToolSearchToolResultBlock + | BetaResponseMCPToolUseBlock + | BetaResponseMCPToolResultBlock + | BetaResponseContainerUploadBlock + | BetaResponseCompactionBlock +export const BetaContentBlock = Schema.Union([ + BetaResponseTextBlock, + BetaResponseThinkingBlock, + BetaResponseRedactedThinkingBlock, + BetaResponseToolUseBlock, + BetaResponseServerToolUseBlock, + BetaResponseWebSearchToolResultBlock, + BetaResponseWebFetchToolResultBlock, + BetaResponseCodeExecutionToolResultBlock, + BetaResponseBashCodeExecutionToolResultBlock, + BetaResponseTextEditorCodeExecutionToolResultBlock, + BetaResponseToolSearchToolResultBlock, + BetaResponseMCPToolUseBlock, + BetaResponseMCPToolResultBlock, + BetaResponseContainerUploadBlock, + BetaResponseCompactionBlock +], { mode: "oneOf" }) +export type BetaRequestWebFetchResultBlock = { + readonly "content": BetaRequestDocumentBlock + readonly "retrieved_at"?: string | null + readonly "type": "web_fetch_result" + readonly "url": string +} +export const BetaRequestWebFetchResultBlock = Schema.Struct({ + "content": BetaRequestDocumentBlock, + "retrieved_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Retrieved At", + "description": "ISO 8601 timestamp when the content was retrieved" + }) + ), + "type": Schema.Literal("web_fetch_result").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url", "description": "Fetched content URL" }) +}).annotate({ "title": "RequestWebFetchResultBlock" }) +export type ContentBlockStartEvent = { + readonly "content_block": + | ResponseTextBlock + | ResponseThinkingBlock + | ResponseRedactedThinkingBlock + | ResponseToolUseBlock + | ResponseServerToolUseBlock + | ResponseWebSearchToolResultBlock + | ResponseWebFetchToolResultBlock + | ResponseCodeExecutionToolResultBlock + | ResponseBashCodeExecutionToolResultBlock + | ResponseTextEditorCodeExecutionToolResultBlock + | ResponseToolSearchToolResultBlock + | ResponseContainerUploadBlock + readonly "index": number + readonly "type": "content_block_start" +} +export const ContentBlockStartEvent = Schema.Struct({ + "content_block": Schema.Union([ + ResponseTextBlock, + ResponseThinkingBlock, + ResponseRedactedThinkingBlock, + ResponseToolUseBlock, + ResponseServerToolUseBlock, + ResponseWebSearchToolResultBlock, + ResponseWebFetchToolResultBlock, + ResponseCodeExecutionToolResultBlock, + ResponseBashCodeExecutionToolResultBlock, + ResponseTextEditorCodeExecutionToolResultBlock, + ResponseToolSearchToolResultBlock, + ResponseContainerUploadBlock + ], { mode: "oneOf" }).annotate({ "title": "Content Block" }), + "index": Schema.Number.annotate({ "title": "Index" }).check(Schema.isInt()), + "type": Schema.Literal("content_block_start").annotate({ "title": "Type", "default": "content_block_start" }) +}).annotate({ "title": "ContentBlockStartEvent" }) +export type ContentBlock = + | ResponseTextBlock + | ResponseThinkingBlock + | ResponseRedactedThinkingBlock + | ResponseToolUseBlock + | ResponseServerToolUseBlock + | ResponseWebSearchToolResultBlock + | ResponseWebFetchToolResultBlock + | ResponseCodeExecutionToolResultBlock + | ResponseBashCodeExecutionToolResultBlock + | ResponseTextEditorCodeExecutionToolResultBlock + | ResponseToolSearchToolResultBlock + | ResponseContainerUploadBlock +export const ContentBlock = Schema.Union([ + ResponseTextBlock, + ResponseThinkingBlock, + ResponseRedactedThinkingBlock, + ResponseToolUseBlock, + ResponseServerToolUseBlock, + ResponseWebSearchToolResultBlock, + ResponseWebFetchToolResultBlock, + ResponseCodeExecutionToolResultBlock, + ResponseBashCodeExecutionToolResultBlock, + ResponseTextEditorCodeExecutionToolResultBlock, + ResponseToolSearchToolResultBlock, + ResponseContainerUploadBlock +], { mode: "oneOf" }) +export type RequestWebFetchResultBlock = { + readonly "content": RequestDocumentBlock + readonly "retrieved_at"?: string | null + readonly "type": "web_fetch_result" + readonly "url": string +} +export const RequestWebFetchResultBlock = Schema.Struct({ + "content": RequestDocumentBlock, + "retrieved_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Retrieved At", + "description": "ISO 8601 timestamp when the content was retrieved" + }) + ), + "type": Schema.Literal("web_fetch_result").annotate({ "title": "Type" }), + "url": Schema.String.annotate({ "title": "Url", "description": "Fetched content URL" }) +}).annotate({ "title": "RequestWebFetchResultBlock" }) +export type BetaMessage = { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray + readonly "model": Model + readonly "stop_reason": BetaStopReason | null + readonly "stop_sequence": string | null + readonly "usage": { + readonly "cache_creation": BetaCacheCreation | null + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "iterations"?: BetaIterationsUsage + readonly "output_tokens": number + readonly "server_tool_use"?: BetaServerToolUsage | null + readonly "service_tier": "standard" | "priority" | "batch" | null + readonly "speed"?: BetaSpeed | null + } + readonly "context_management"?: BetaResponseContextManagement | null + readonly "container"?: BetaContainer | null +} +export const BetaMessage = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "type": Schema.Literal("message").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Messages, this is always `\"message\"`.", + "default": "message" + }), + "role": Schema.Literal("assistant").annotate({ + "title": "Role", + "description": "Conversational role of the generated message.\n\nThis will always be `\"assistant\"`.", + "default": "assistant" + }), + "content": Schema.Array(BetaContentBlock).annotate({ + "title": "Content", + "description": + "Content generated by the model.\n\nThis is an array of content blocks, each of which has a `type` that determines its shape.\n\nExample:\n\n```json\n[{\"type\": \"text\", \"text\": \"Hi, I'm Claude.\"}]\n```\n\nIf the request input `messages` ended with an `assistant` turn, then the response `content` will continue directly from that last turn. You can use this to constrain the model's output.\n\nFor example, if the input `messages` were:\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"}\n]\n```\n\nThen the response `content` might be:\n\n```json\n[{\"type\": \"text\", \"text\": \"B)\"}]\n```" + }), + "model": Model, + "stop_reason": Schema.Union([BetaStopReason, Schema.Null]).annotate({ + "title": "Stop Reason", + "description": + "The reason that we stopped.\n\nThis may be one the following values:\n* `\"end_turn\"`: the model reached a natural stopping point\n* `\"max_tokens\"`: we exceeded the requested `max_tokens` or the model's maximum\n* `\"stop_sequence\"`: one of your provided custom `stop_sequences` was generated\n* `\"tool_use\"`: the model invoked one or more tools\n* `\"pause_turn\"`: we paused a long-running turn. You may provide the response back as-is in a subsequent request to let the model continue.\n* `\"refusal\"`: when streaming classifiers intervene to handle potential policy violations\n\nIn non-streaming mode this value is always non-null. In streaming mode, it is null in the `message_start` event and non-null otherwise." + }), + "stop_sequence": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Stop Sequence", + "description": + "Which custom stop sequence was generated, if any.\n\nThis value will be a non-null string if one of your custom stop sequences was generated.", + "default": null + }), + "usage": Schema.Struct({ + "cache_creation": Schema.Union([BetaCacheCreation, Schema.Null]).annotate({ + "description": "Breakdown of cached tokens by TTL", + "default": null + }), + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Creation Input Tokens", + "description": "The number of input tokens used to create the cache entry.", + "default": null + }), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Read Input Tokens", + "description": "The number of input tokens read from the cache.", + "default": null + }), + "inference_geo": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": "The geographic region where inference was performed for this request.", + "default": null + }), + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The number of input tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "iterations": Schema.optionalKey(BetaIterationsUsage), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The number of output tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "server_tool_use": Schema.optionalKey( + Schema.Union([BetaServerToolUsage, Schema.Null]).annotate({ + "description": "The number of server tool requests.", + "default": null + }) + ), + "service_tier": Schema.Union([Schema.Literals(["standard", "priority", "batch"]), Schema.Null]).annotate({ + "title": "Service Tier", + "description": "If the request used the priority, standard, or batch tier.", + "default": null + }), + "speed": Schema.optionalKey( + Schema.Union([BetaSpeed, Schema.Null]).annotate({ + "description": "The inference speed mode used for this request.", + "default": null + }) + ) + }).annotate({ + "title": "Usage", + "description": + "Billing and rate-limit usage.\n\nAnthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems.\n\nUnder the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response.\n\nFor example, `output_tokens` will be non-zero, even for an empty string response from Claude.\n\nTotal input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`." + }), + "context_management": Schema.optionalKey( + Schema.Union([BetaResponseContextManagement, Schema.Null]).annotate({ + "description": + "Context management response.\n\nInformation about context management strategies applied during the request.", + "default": null + }) + ), + "container": Schema.optionalKey( + Schema.Union([BetaContainer, Schema.Null]).annotate({ + "description": + "Information about the container used in this request.\n\nThis will be non-null if a container tool (e.g. code execution) was used.", + "default": null + }) + ) +}).annotate({ "title": "Message" }) +export type BetaRequestWebFetchToolResultBlock = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "content": BetaRequestWebFetchToolResultError | BetaRequestWebFetchResultBlock + readonly "tool_use_id": string + readonly "type": "web_fetch_tool_result" +} +export const BetaRequestWebFetchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([BetaRequestWebFetchToolResultError, BetaRequestWebFetchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_fetch_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebFetchToolResultBlock" }) +export type Message = { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray + readonly "model": Model + readonly "stop_reason": StopReason | null + readonly "stop_sequence": string | null + readonly "usage": { + readonly "cache_creation": CacheCreation | null + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "output_tokens": number + readonly "server_tool_use"?: ServerToolUsage | null + readonly "service_tier": "standard" | "priority" | "batch" | null + } + readonly "container": Container | null +} +export const Message = Schema.Struct({ + "id": Schema.String.annotate({ + "title": "Id", + "description": "Unique object identifier.\n\nThe format and length of IDs may change over time." + }), + "type": Schema.Literal("message").annotate({ + "title": "Type", + "description": "Object type.\n\nFor Messages, this is always `\"message\"`.", + "default": "message" + }), + "role": Schema.Literal("assistant").annotate({ + "title": "Role", + "description": "Conversational role of the generated message.\n\nThis will always be `\"assistant\"`.", + "default": "assistant" + }), + "content": Schema.Array(ContentBlock).annotate({ + "title": "Content", + "description": + "Content generated by the model.\n\nThis is an array of content blocks, each of which has a `type` that determines its shape.\n\nExample:\n\n```json\n[{\"type\": \"text\", \"text\": \"Hi, I'm Claude.\"}]\n```\n\nIf the request input `messages` ended with an `assistant` turn, then the response `content` will continue directly from that last turn. You can use this to constrain the model's output.\n\nFor example, if the input `messages` were:\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"}\n]\n```\n\nThen the response `content` might be:\n\n```json\n[{\"type\": \"text\", \"text\": \"B)\"}]\n```" + }), + "model": Model, + "stop_reason": Schema.Union([StopReason, Schema.Null]).annotate({ + "title": "Stop Reason", + "description": + "The reason that we stopped.\n\nThis may be one the following values:\n* `\"end_turn\"`: the model reached a natural stopping point\n* `\"max_tokens\"`: we exceeded the requested `max_tokens` or the model's maximum\n* `\"stop_sequence\"`: one of your provided custom `stop_sequences` was generated\n* `\"tool_use\"`: the model invoked one or more tools\n* `\"pause_turn\"`: we paused a long-running turn. You may provide the response back as-is in a subsequent request to let the model continue.\n* `\"refusal\"`: when streaming classifiers intervene to handle potential policy violations\n\nIn non-streaming mode this value is always non-null. In streaming mode, it is null in the `message_start` event and non-null otherwise." + }), + "stop_sequence": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Stop Sequence", + "description": + "Which custom stop sequence was generated, if any.\n\nThis value will be a non-null string if one of your custom stop sequences was generated.", + "default": null + }), + "usage": Schema.Struct({ + "cache_creation": Schema.Union([CacheCreation, Schema.Null]).annotate({ + "description": "Breakdown of cached tokens by TTL", + "default": null + }), + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Creation Input Tokens", + "description": "The number of input tokens used to create the cache entry.", + "default": null + }), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]).annotate({ + "title": "Cache Read Input Tokens", + "description": "The number of input tokens read from the cache.", + "default": null + }), + "inference_geo": Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": "The geographic region where inference was performed for this request.", + "default": null + }), + "input_tokens": Schema.Number.annotate({ + "title": "Input Tokens", + "description": "The number of input tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "output_tokens": Schema.Number.annotate({ + "title": "Output Tokens", + "description": "The number of output tokens which were used." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "server_tool_use": Schema.optionalKey( + Schema.Union([ServerToolUsage, Schema.Null]).annotate({ + "description": "The number of server tool requests.", + "default": null + }) + ), + "service_tier": Schema.Union([Schema.Literals(["standard", "priority", "batch"]), Schema.Null]).annotate({ + "title": "Service Tier", + "description": "If the request used the priority, standard, or batch tier.", + "default": null + }) + }).annotate({ + "title": "Usage", + "description": + "Billing and rate-limit usage.\n\nAnthropic's API bills and rate-limits by token counts, as tokens represent the underlying cost to our systems.\n\nUnder the hood, the API transforms requests into a format suitable for the model. The model's output then goes through a parsing stage before becoming an API response. As a result, the token counts in `usage` will not match one-to-one with the exact visible content of an API request or response.\n\nFor example, `output_tokens` will be non-zero, even for an empty string response from Claude.\n\nTotal input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`." + }), + "container": Schema.Union([Container, Schema.Null]).annotate({ + "description": + "Information about the container used in this request.\n\nThis will be non-null if a container tool (e.g. code execution) was used.", + "default": null + }) +}).annotate({ "title": "Message" }) +export type RequestWebFetchToolResultBlock = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "caller"?: DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "content": RequestWebFetchToolResultError | RequestWebFetchResultBlock + readonly "tool_use_id": string + readonly "type": "web_fetch_tool_result" +} +export const RequestWebFetchToolResultBlock = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "content": Schema.Union([RequestWebFetchToolResultError, RequestWebFetchResultBlock]).annotate({ + "title": "Content" + }), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^srvtoolu_[a-zA-Z0-9_]+$")) + ), + "type": Schema.Literal("web_fetch_tool_result").annotate({ "title": "Type" }) +}).annotate({ "title": "RequestWebFetchToolResultBlock" }) +export type BetaMessageStartEvent = { readonly "message": BetaMessage; readonly "type": "message_start" } +export const BetaMessageStartEvent = Schema.Struct({ + "message": BetaMessage, + "type": Schema.Literal("message_start").annotate({ "title": "Type", "default": "message_start" }) +}).annotate({ "title": "MessageStartEvent" }) +export type BetaSucceededResult = { readonly "message": BetaMessage; readonly "type": "succeeded" } +export const BetaSucceededResult = Schema.Struct({ + "message": BetaMessage, + "type": Schema.Literal("succeeded").annotate({ "title": "Type", "default": "succeeded" }) +}).annotate({ "title": "SucceededResult" }) +export type BetaInputContentBlock = + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: + | ReadonlyArray< + | BetaRequestCharLocationCitation + | BetaRequestPageLocationCitation + | BetaRequestContentBlockLocationCitation + | BetaRequestWebSearchResultLocationCitation + | BetaRequestSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" + } + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "source": BetaBase64ImageSource | BetaURLImageSource | BetaFileImageSource + readonly "type": "image" + } + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig | null + readonly "context"?: string | null + readonly "source": + | BetaBase64PDFSource + | BetaPlainTextSource + | BetaContentBlockSource + | BetaURLPDFSource + | BetaFileDocumentSource + readonly "title"?: string | null + readonly "type": "document" + } + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "citations"?: BetaRequestCitationsConfig + readonly "content": ReadonlyArray + readonly "source": string + readonly "title": string + readonly "type": "search_result" + } + | { readonly "signature": string; readonly "thinking": string; readonly "type": "thinking" } + | { readonly "data": string; readonly "type": "redacted_thinking" } + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "caller"?: BetaDirectCaller | BetaServerToolCaller | BetaServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "type": "tool_use" + } + | { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "content"?: + | string + | ReadonlyArray< + | BetaRequestTextBlock + | BetaRequestImageBlock + | BetaRequestSearchResultBlock + | BetaRequestDocumentBlock + | BetaRequestToolReferenceBlock + > + readonly "is_error"?: boolean + readonly "tool_use_id": string + readonly "type": "tool_result" + } + | BetaRequestServerToolUseBlock + | BetaRequestWebSearchToolResultBlock + | BetaRequestWebFetchToolResultBlock + | BetaRequestCodeExecutionToolResultBlock + | BetaRequestBashCodeExecutionToolResultBlock + | BetaRequestTextEditorCodeExecutionToolResultBlock + | BetaRequestToolSearchToolResultBlock + | BetaRequestMCPToolUseBlock + | BetaRequestMCPToolResultBlock + | BetaRequestContainerUploadBlock + | BetaRequestCompactionBlock +export const BetaInputContentBlock = Schema.Union([ + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + BetaRequestCharLocationCitation, + BetaRequestPageLocationCitation, + BetaRequestContentBlockLocationCitation, + BetaRequestWebSearchResultLocationCitation, + BetaRequestSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ "title": "Citations" }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("text").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestTextBlock", "description": "Regular text content." }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "source": Schema.Union([BetaBase64ImageSource, BetaURLImageSource, BetaFileImageSource], { mode: "oneOf" }) + .annotate({ "title": "Source" }), + "type": Schema.Literal("image").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestImageBlock", + "description": "Image content specified directly as base64 data or as a reference via a URL." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(Schema.Union([BetaRequestCitationsConfig, Schema.Null])), + "context": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)), Schema.Null]).annotate({ "title": "Context" }) + ), + "source": Schema.Union([ + BetaBase64PDFSource, + BetaPlainTextSource, + BetaContentBlockSource, + BetaURLPDFSource, + BetaFileDocumentSource + ], { mode: "oneOf" }).annotate({ "title": "Source" }), + "title": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), Schema.Null]).annotate({ + "title": "Title" + }) + ), + "type": Schema.Literal("document").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestDocumentBlock", + "description": "Document content, either specified directly as base64 data, as text, or as a reference via a URL." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(BetaRequestCitationsConfig), + "content": Schema.Array(BetaRequestTextBlock).annotate({ "title": "Content" }), + "source": Schema.String.annotate({ "title": "Source" }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("search_result").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestSearchResultBlock", + "description": "A search result block containing source, title, and content from search operations." + }), + Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestThinkingBlock", "description": "A block specifying internal thinking by the model." }), + Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "type": Schema.Literal("redacted_thinking").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestRedactedThinkingBlock", + "description": "A block specifying internal, redacted thinking by the model." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([BetaDirectCaller, BetaServerToolCaller, BetaServerToolCaller_20260120], { mode: "oneOf" }).annotate( + { "title": "Caller" } + ) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name" }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(200)), + "type": Schema.Literal("tool_use").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestToolUseBlock", "description": "A block indicating a tool use by the model." }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([ + BetaRequestTextBlock, + BetaRequestImageBlock, + BetaRequestSearchResultBlock, + BetaRequestDocumentBlock, + BetaRequestToolReferenceBlock + ], { mode: "oneOf" }).annotate({ "title": "Block" }) + ) + ]).annotate({ "title": "Content" }) + ), + "is_error": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Is Error" })), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$")) + ), + "type": Schema.Literal("tool_result").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestToolResultBlock", + "description": "A block specifying the results of a tool use by the model." + }), + BetaRequestServerToolUseBlock, + BetaRequestWebSearchToolResultBlock, + BetaRequestWebFetchToolResultBlock, + BetaRequestCodeExecutionToolResultBlock, + BetaRequestBashCodeExecutionToolResultBlock, + BetaRequestTextEditorCodeExecutionToolResultBlock, + BetaRequestToolSearchToolResultBlock, + BetaRequestMCPToolUseBlock, + BetaRequestMCPToolResultBlock, + BetaRequestContainerUploadBlock, + BetaRequestCompactionBlock +], { mode: "oneOf" }) +export type MessageStartEvent = { readonly "message": Message; readonly "type": "message_start" } +export const MessageStartEvent = Schema.Struct({ + "message": Message, + "type": Schema.Literal("message_start").annotate({ "title": "Type", "default": "message_start" }) +}).annotate({ "title": "MessageStartEvent" }) +export type SucceededResult = { readonly "message": Message; readonly "type": "succeeded" } +export const SucceededResult = Schema.Struct({ + "message": Message, + "type": Schema.Literal("succeeded").annotate({ "title": "Type", "default": "succeeded" }) +}).annotate({ "title": "SucceededResult" }) +export type InputContentBlock = + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: + | ReadonlyArray< + | RequestCharLocationCitation + | RequestPageLocationCitation + | RequestContentBlockLocationCitation + | RequestWebSearchResultLocationCitation + | RequestSearchResultLocationCitation + > + | null + readonly "text": string + readonly "type": "text" + } + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "source": Base64ImageSource | URLImageSource + readonly "type": "image" + } + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig | null + readonly "context"?: string | null + readonly "source": Base64PDFSource | PlainTextSource | ContentBlockSource | URLPDFSource + readonly "title"?: string | null + readonly "type": "document" + } + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "citations"?: RequestCitationsConfig + readonly "content": ReadonlyArray + readonly "source": string + readonly "title": string + readonly "type": "search_result" + } + | { readonly "signature": string; readonly "thinking": string; readonly "type": "thinking" } + | { readonly "data": string; readonly "type": "redacted_thinking" } + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "caller"?: DirectCaller | ServerToolCaller | ServerToolCaller_20260120 + readonly "id": string + readonly "input": { readonly [x: string]: Schema.Json } + readonly "name": string + readonly "type": "tool_use" + } + | { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "content"?: + | string + | ReadonlyArray< + | RequestTextBlock + | RequestImageBlock + | RequestSearchResultBlock + | RequestDocumentBlock + | RequestToolReferenceBlock + > + readonly "is_error"?: boolean + readonly "tool_use_id": string + readonly "type": "tool_result" + } + | RequestServerToolUseBlock + | RequestWebSearchToolResultBlock + | RequestWebFetchToolResultBlock + | RequestCodeExecutionToolResultBlock + | RequestBashCodeExecutionToolResultBlock + | RequestTextEditorCodeExecutionToolResultBlock + | RequestToolSearchToolResultBlock + | RequestContainerUploadBlock +export const InputContentBlock = Schema.Union([ + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + RequestCharLocationCitation, + RequestPageLocationCitation, + RequestContentBlockLocationCitation, + RequestWebSearchResultLocationCitation, + RequestSearchResultLocationCitation + ], { mode: "oneOf" }) + ), + Schema.Null + ]).annotate({ "title": "Citations" }) + ), + "text": Schema.String.annotate({ "title": "Text" }).check(Schema.isMinLength(1)), + "type": Schema.Literal("text").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestTextBlock", "description": "Regular text content." }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "source": Schema.Union([Base64ImageSource, URLImageSource], { mode: "oneOf" }).annotate({ "title": "Source" }), + "type": Schema.Literal("image").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestImageBlock", + "description": "Image content specified directly as base64 data or as a reference via a URL." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(Schema.Union([RequestCitationsConfig, Schema.Null])), + "context": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)), Schema.Null]).annotate({ "title": "Context" }) + ), + "source": Schema.Union([Base64PDFSource, PlainTextSource, ContentBlockSource, URLPDFSource], { mode: "oneOf" }) + .annotate({ "title": "Source" }), + "title": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(500)), Schema.Null]).annotate({ + "title": "Title" + }) + ), + "type": Schema.Literal("document").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestDocumentBlock", + "description": "Document content, either specified directly as base64 data, as text, or as a reference via a URL." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "citations": Schema.optionalKey(RequestCitationsConfig), + "content": Schema.Array(RequestTextBlock).annotate({ "title": "Content" }), + "source": Schema.String.annotate({ "title": "Source" }), + "title": Schema.String.annotate({ "title": "Title" }), + "type": Schema.Literal("search_result").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestSearchResultBlock", + "description": "A search result block containing source, title, and content from search operations." + }), + Schema.Struct({ + "signature": Schema.String.annotate({ "title": "Signature" }), + "thinking": Schema.String.annotate({ "title": "Thinking" }), + "type": Schema.Literal("thinking").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestThinkingBlock", "description": "A block specifying internal thinking by the model." }), + Schema.Struct({ + "data": Schema.String.annotate({ "title": "Data" }), + "type": Schema.Literal("redacted_thinking").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestRedactedThinkingBlock", + "description": "A block specifying internal, redacted thinking by the model." + }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "caller": Schema.optionalKey( + Schema.Union([DirectCaller, ServerToolCaller, ServerToolCaller_20260120], { mode: "oneOf" }).annotate({ + "title": "Caller" + }) + ), + "id": Schema.String.annotate({ "title": "Id" }).check(Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$"))), + "input": Schema.Record(Schema.String, Schema.Json).annotate({ "title": "Input" }), + "name": Schema.String.annotate({ "title": "Name" }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(200)), + "type": Schema.Literal("tool_use").annotate({ "title": "Type" }) + }).annotate({ "title": "RequestToolUseBlock", "description": "A block indicating a tool use by the model." }), + Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": "Create a cache control breakpoint at this content block." + }) + ), + "content": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([ + RequestTextBlock, + RequestImageBlock, + RequestSearchResultBlock, + RequestDocumentBlock, + RequestToolReferenceBlock + ], { mode: "oneOf" }).annotate({ "title": "Block" }) + ) + ]).annotate({ "title": "Content" }) + ), + "is_error": Schema.optionalKey(Schema.Boolean.annotate({ "title": "Is Error" })), + "tool_use_id": Schema.String.annotate({ "title": "Tool Use Id" }).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$")) + ), + "type": Schema.Literal("tool_result").annotate({ "title": "Type" }) + }).annotate({ + "title": "RequestToolResultBlock", + "description": "A block specifying the results of a tool use by the model." + }), + RequestServerToolUseBlock, + RequestWebSearchToolResultBlock, + RequestWebFetchToolResultBlock, + RequestCodeExecutionToolResultBlock, + RequestBashCodeExecutionToolResultBlock, + RequestTextEditorCodeExecutionToolResultBlock, + RequestToolSearchToolResultBlock, + RequestContainerUploadBlock +], { mode: "oneOf" }) +export type BetaInputMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "user" | "assistant" +} +export const BetaInputMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(BetaInputContentBlock)]).annotate({ "title": "Content" }), + "role": Schema.Literals(["user", "assistant"]).annotate({ "title": "Role" }) +}).annotate({ "title": "InputMessage" }) +export type InputMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "user" | "assistant" +} +export const InputMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(InputContentBlock)]).annotate({ "title": "Content" }), + "role": Schema.Literals(["user", "assistant"]).annotate({ "title": "Role" }) +}).annotate({ "title": "InputMessage" }) +export type BetaCountMessageTokensParams = { + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "context_management"?: BetaContextManagementConfig | null + readonly "mcp_servers"?: ReadonlyArray + readonly "messages": ReadonlyArray + readonly "model": Model + readonly "output_config"?: { + readonly "effort"?: BetaEffortLevel | null + readonly "format"?: BetaJsonOutputFormat | null + } + readonly "output_format"?: BetaJsonOutputFormat | null + readonly "speed"?: BetaSpeed | null + readonly "system"?: string | ReadonlyArray + readonly "thinking"?: BetaThinkingConfigParam + readonly "tool_choice"?: BetaToolChoice + readonly "tools"?: ReadonlyArray< + | BetaTool + | BetaBashTool_20241022 + | BetaBashTool_20250124 + | BetaCodeExecutionTool_20250522 + | BetaCodeExecutionTool_20250825 + | BetaCodeExecutionTool_20260120 + | BetaComputerUseTool_20241022 + | BetaMemoryTool_20250818 + | BetaComputerUseTool_20250124 + | BetaTextEditor_20241022 + | BetaComputerUseTool_20251124 + | BetaTextEditor_20250124 + | BetaTextEditor_20250429 + | BetaTextEditor_20250728 + | BetaWebSearchTool_20250305 + | BetaWebFetchTool_20250910 + | BetaWebSearchTool_20260209 + | BetaWebFetchTool_20260209 + | BetaToolSearchToolBM25_20251119 + | BetaToolSearchToolRegex_20251119 + | BetaMCPToolset + > +} +export const BetaCountMessageTokensParams = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "context_management": Schema.optionalKey( + Schema.Union([BetaContextManagementConfig, Schema.Null]).annotate({ + "description": + "Context management configuration.\n\nThis allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not." + }) + ), + "mcp_servers": Schema.optionalKey( + Schema.Array(BetaRequestMCPServerURLDefinition).annotate({ + "title": "Mcp Servers", + "description": "MCP servers to be utilized in this request" + }).check(Schema.isMaxLength(20)) + ), + "messages": Schema.Array(BetaInputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "model": Model, + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([BetaEffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "output_format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "Deprecated: Use `output_config.format` instead. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)\n\nA schema to specify Claude's output format in responses. This parameter will be removed in a future release." + }) + ), + "speed": Schema.optionalKey( + Schema.Union([BetaSpeed, Schema.Null]).annotate({ + "description": + "The inference speed mode for this request. `\"fast\"` enables high output-tokens-per-second inference." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(BetaRequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "thinking": Schema.optionalKey(BetaThinkingConfigParam), + "tool_choice": Schema.optionalKey(BetaToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + BetaTool, + BetaBashTool_20241022, + BetaBashTool_20250124, + BetaCodeExecutionTool_20250522, + BetaCodeExecutionTool_20250825, + BetaCodeExecutionTool_20260120, + BetaComputerUseTool_20241022, + BetaMemoryTool_20250818, + BetaComputerUseTool_20250124, + BetaTextEditor_20241022, + BetaComputerUseTool_20251124, + BetaTextEditor_20250124, + BetaTextEditor_20250429, + BetaTextEditor_20250728, + BetaWebSearchTool_20250305, + BetaWebFetchTool_20250910, + BetaWebSearchTool_20260209, + BetaWebFetchTool_20260209, + BetaToolSearchToolBM25_20251119, + BetaToolSearchToolRegex_20251119, + BetaMCPToolset + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ) +}).annotate({ "title": "CountMessageTokensParams" }) +export type BetaCreateMessageParams = { + readonly "model": Model + readonly "messages": ReadonlyArray + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "container"?: BetaContainerParams | string | null + readonly "context_management"?: BetaContextManagementConfig | null + readonly "inference_geo"?: string | null + readonly "max_tokens": number + readonly "mcp_servers"?: ReadonlyArray + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "output_config"?: { + readonly "effort"?: BetaEffortLevel | null + readonly "format"?: BetaJsonOutputFormat | null + } + readonly "output_format"?: BetaJsonOutputFormat | null + readonly "service_tier"?: "auto" | "standard_only" + readonly "speed"?: BetaSpeed | null + readonly "stop_sequences"?: ReadonlyArray + readonly "stream"?: boolean + readonly "system"?: string | ReadonlyArray + readonly "temperature"?: number + readonly "thinking"?: BetaThinkingConfigParam + readonly "tool_choice"?: BetaToolChoice + readonly "tools"?: ReadonlyArray< + | BetaTool + | BetaBashTool_20241022 + | BetaBashTool_20250124 + | BetaCodeExecutionTool_20250522 + | BetaCodeExecutionTool_20250825 + | BetaCodeExecutionTool_20260120 + | BetaComputerUseTool_20241022 + | BetaMemoryTool_20250818 + | BetaComputerUseTool_20250124 + | BetaTextEditor_20241022 + | BetaComputerUseTool_20251124 + | BetaTextEditor_20250124 + | BetaTextEditor_20250429 + | BetaTextEditor_20250728 + | BetaWebSearchTool_20250305 + | BetaWebFetchTool_20250910 + | BetaWebSearchTool_20260209 + | BetaWebFetchTool_20260209 + | BetaToolSearchToolBM25_20251119 + | BetaToolSearchToolRegex_20251119 + | BetaMCPToolset + > + readonly "top_k"?: number + readonly "top_p"?: number +} +export const BetaCreateMessageParams = Schema.Struct({ + "model": Model, + "messages": Schema.Array(BetaInputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "container": Schema.optionalKey( + Schema.Union([BetaContainerParams, Schema.String, Schema.Null]).annotate({ + "title": "Container", + "description": "Container identifier for reuse across requests." + }) + ), + "context_management": Schema.optionalKey( + Schema.Union([BetaContextManagementConfig, Schema.Null]).annotate({ + "description": + "Context management configuration.\n\nThis allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not." + }) + ), + "inference_geo": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": + "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used." + }) + ), + "max_tokens": Schema.Number.annotate({ + "title": "Max Tokens", + "description": + "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "mcp_servers": Schema.optionalKey( + Schema.Array(BetaRequestMCPServerURLDefinition).annotate({ + "title": "Mcp Servers", + "description": "MCP servers to be utilized in this request" + }).check(Schema.isMaxLength(20)) + ), + "metadata": Schema.optionalKey( + Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMaxLength(256)), Schema.Null]).annotate({ + "title": "User Id", + "description": + "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number." + }) + ) + }).annotate({ "title": "Metadata", "description": "An object describing metadata about the request." }) + ), + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([BetaEffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "output_format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "Deprecated: Use `output_config.format` instead. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)\n\nA schema to specify Claude's output format in responses. This parameter will be removed in a future release." + }) + ), + "service_tier": Schema.optionalKey( + Schema.Literals(["auto", "standard_only"]).annotate({ + "title": "Service Tier", + "description": + "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details." + }) + ), + "speed": Schema.optionalKey( + Schema.Union([BetaSpeed, Schema.Null]).annotate({ + "description": + "The inference speed mode for this request. `\"fast\"` enables high output-tokens-per-second inference." + }) + ), + "stop_sequences": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "title": "Stop Sequences", + "description": + "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence." + }) + ), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Stream", + "description": + "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(BetaRequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Temperature", + "description": + "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "thinking": Schema.optionalKey(BetaThinkingConfigParam), + "tool_choice": Schema.optionalKey(BetaToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + BetaTool, + BetaBashTool_20241022, + BetaBashTool_20250124, + BetaCodeExecutionTool_20250522, + BetaCodeExecutionTool_20250825, + BetaCodeExecutionTool_20260120, + BetaComputerUseTool_20241022, + BetaMemoryTool_20250818, + BetaComputerUseTool_20250124, + BetaTextEditor_20241022, + BetaComputerUseTool_20251124, + BetaTextEditor_20250124, + BetaTextEditor_20250429, + BetaTextEditor_20250728, + BetaWebSearchTool_20250305, + BetaWebFetchTool_20250910, + BetaWebSearchTool_20260209, + BetaWebFetchTool_20260209, + BetaToolSearchToolBM25_20251119, + BetaToolSearchToolRegex_20251119, + BetaMCPToolset + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ), + "top_k": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top K", + "description": + "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top P", + "description": + "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ) +}).annotate({ "title": "CreateMessageParams" }) +export type BetaMessageBatchIndividualRequestParams = { + readonly "custom_id": string + readonly "params": { + readonly "model": Model + readonly "messages": ReadonlyArray + readonly "cache_control"?: BetaCacheControlEphemeral | null + readonly "container"?: BetaContainerParams | string | null + readonly "context_management"?: BetaContextManagementConfig | null + readonly "inference_geo"?: string | null + readonly "max_tokens": number + readonly "mcp_servers"?: ReadonlyArray + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "output_config"?: { + readonly "effort"?: BetaEffortLevel | null + readonly "format"?: BetaJsonOutputFormat | null + } + readonly "output_format"?: BetaJsonOutputFormat | null + readonly "service_tier"?: "auto" | "standard_only" + readonly "speed"?: BetaSpeed | null + readonly "stop_sequences"?: ReadonlyArray + readonly "stream"?: boolean + readonly "system"?: string | ReadonlyArray + readonly "temperature"?: number + readonly "thinking"?: BetaThinkingConfigParam + readonly "tool_choice"?: BetaToolChoice + readonly "tools"?: ReadonlyArray< + | BetaTool + | BetaBashTool_20241022 + | BetaBashTool_20250124 + | BetaCodeExecutionTool_20250522 + | BetaCodeExecutionTool_20250825 + | BetaCodeExecutionTool_20260120 + | BetaComputerUseTool_20241022 + | BetaMemoryTool_20250818 + | BetaComputerUseTool_20250124 + | BetaTextEditor_20241022 + | BetaComputerUseTool_20251124 + | BetaTextEditor_20250124 + | BetaTextEditor_20250429 + | BetaTextEditor_20250728 + | BetaWebSearchTool_20250305 + | BetaWebFetchTool_20250910 + | BetaWebSearchTool_20260209 + | BetaWebFetchTool_20260209 + | BetaToolSearchToolBM25_20251119 + | BetaToolSearchToolRegex_20251119 + | BetaMCPToolset + > + readonly "top_k"?: number + readonly "top_p"?: number + } +} +export const BetaMessageBatchIndividualRequestParams = Schema.Struct({ + "custom_id": Schema.String.annotate({ + "title": "Custom Id", + "description": + "Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order.\n\nMust be unique for each request within the Message Batch." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,64}$")) + ), + "params": Schema.Struct({ + "model": Model, + "messages": Schema.Array(BetaInputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([BetaCacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "container": Schema.optionalKey( + Schema.Union([BetaContainerParams, Schema.String, Schema.Null]).annotate({ + "title": "Container", + "description": "Container identifier for reuse across requests." + }) + ), + "context_management": Schema.optionalKey( + Schema.Union([BetaContextManagementConfig, Schema.Null]).annotate({ + "description": + "Context management configuration.\n\nThis allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not." + }) + ), + "inference_geo": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": + "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used." + }) + ), + "max_tokens": Schema.Number.annotate({ + "title": "Max Tokens", + "description": + "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "mcp_servers": Schema.optionalKey( + Schema.Array(BetaRequestMCPServerURLDefinition).annotate({ + "title": "Mcp Servers", + "description": "MCP servers to be utilized in this request" + }).check(Schema.isMaxLength(20)) + ), + "metadata": Schema.optionalKey( + Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMaxLength(256)), Schema.Null]).annotate({ + "title": "User Id", + "description": + "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number." + }) + ) + }).annotate({ "title": "Metadata", "description": "An object describing metadata about the request." }) + ), + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([BetaEffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "output_format": Schema.optionalKey( + Schema.Union([BetaJsonOutputFormat, Schema.Null]).annotate({ + "description": + "Deprecated: Use `output_config.format` instead. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)\n\nA schema to specify Claude's output format in responses. This parameter will be removed in a future release." + }) + ), + "service_tier": Schema.optionalKey( + Schema.Literals(["auto", "standard_only"]).annotate({ + "title": "Service Tier", + "description": + "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details." + }) + ), + "speed": Schema.optionalKey( + Schema.Union([BetaSpeed, Schema.Null]).annotate({ + "description": + "The inference speed mode for this request. `\"fast\"` enables high output-tokens-per-second inference." + }) + ), + "stop_sequences": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "title": "Stop Sequences", + "description": + "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence." + }) + ), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Stream", + "description": + "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(BetaRequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Temperature", + "description": + "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "thinking": Schema.optionalKey(BetaThinkingConfigParam), + "tool_choice": Schema.optionalKey(BetaToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + BetaTool, + BetaBashTool_20241022, + BetaBashTool_20250124, + BetaCodeExecutionTool_20250522, + BetaCodeExecutionTool_20250825, + BetaCodeExecutionTool_20260120, + BetaComputerUseTool_20241022, + BetaMemoryTool_20250818, + BetaComputerUseTool_20250124, + BetaTextEditor_20241022, + BetaComputerUseTool_20251124, + BetaTextEditor_20250124, + BetaTextEditor_20250429, + BetaTextEditor_20250728, + BetaWebSearchTool_20250305, + BetaWebFetchTool_20250910, + BetaWebSearchTool_20260209, + BetaWebFetchTool_20260209, + BetaToolSearchToolBM25_20251119, + BetaToolSearchToolRegex_20251119, + BetaMCPToolset + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ), + "top_k": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top K", + "description": + "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top P", + "description": + "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ) + }).annotate({ + "title": "CreateMessageParams", + "description": + "Messages API creation parameters for the individual request.\n\nSee the [Messages API reference](https://docs.claude.com/en/api/messages) for full documentation on available parameters." + }) +}).annotate({ "title": "MessageBatchIndividualRequestParams" }) +export type CountMessageTokensParams = { + readonly "cache_control"?: CacheControlEphemeral | null + readonly "messages": ReadonlyArray + readonly "model": Model + readonly "output_config"?: { readonly "effort"?: EffortLevel | null; readonly "format"?: JsonOutputFormat | null } + readonly "system"?: string | ReadonlyArray + readonly "thinking"?: ThinkingConfigParam + readonly "tool_choice"?: ToolChoice + readonly "tools"?: ReadonlyArray< + | Tool + | BashTool_20250124 + | CodeExecutionTool_20250522 + | CodeExecutionTool_20250825 + | CodeExecutionTool_20260120 + | MemoryTool_20250818 + | TextEditor_20250124 + | TextEditor_20250429 + | TextEditor_20250728 + | WebSearchTool_20250305 + | WebFetchTool_20250910 + | WebSearchTool_20260209 + | WebFetchTool_20260209 + | ToolSearchToolBM25_20251119 + | ToolSearchToolRegex_20251119 + > +} +export const CountMessageTokensParams = Schema.Struct({ + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "messages": Schema.Array(InputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "model": Model, + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([EffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([JsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(RequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "thinking": Schema.optionalKey(ThinkingConfigParam), + "tool_choice": Schema.optionalKey(ToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Tool, + BashTool_20250124, + CodeExecutionTool_20250522, + CodeExecutionTool_20250825, + CodeExecutionTool_20260120, + MemoryTool_20250818, + TextEditor_20250124, + TextEditor_20250429, + TextEditor_20250728, + WebSearchTool_20250305, + WebFetchTool_20250910, + WebSearchTool_20260209, + WebFetchTool_20260209, + ToolSearchToolBM25_20251119, + ToolSearchToolRegex_20251119 + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ) +}).annotate({ "title": "CountMessageTokensParams" }) +export type CreateMessageParams = { + readonly "model": Model + readonly "messages": ReadonlyArray + readonly "cache_control"?: CacheControlEphemeral | null + readonly "container"?: string | null + readonly "inference_geo"?: string | null + readonly "max_tokens": number + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "output_config"?: { readonly "effort"?: EffortLevel | null; readonly "format"?: JsonOutputFormat | null } + readonly "service_tier"?: "auto" | "standard_only" + readonly "stop_sequences"?: ReadonlyArray + readonly "stream"?: boolean + readonly "system"?: string | ReadonlyArray + readonly "temperature"?: number + readonly "thinking"?: ThinkingConfigParam + readonly "tool_choice"?: ToolChoice + readonly "tools"?: ReadonlyArray< + | Tool + | BashTool_20250124 + | CodeExecutionTool_20250522 + | CodeExecutionTool_20250825 + | CodeExecutionTool_20260120 + | MemoryTool_20250818 + | TextEditor_20250124 + | TextEditor_20250429 + | TextEditor_20250728 + | WebSearchTool_20250305 + | WebFetchTool_20250910 + | WebSearchTool_20260209 + | WebFetchTool_20260209 + | ToolSearchToolBM25_20251119 + | ToolSearchToolRegex_20251119 + > + readonly "top_k"?: number + readonly "top_p"?: number +} +export const CreateMessageParams = Schema.Struct({ + "model": Model, + "messages": Schema.Array(InputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "container": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Container", + "description": "Container identifier for reuse across requests." + }) + ), + "inference_geo": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": + "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used." + }) + ), + "max_tokens": Schema.Number.annotate({ + "title": "Max Tokens", + "description": + "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "metadata": Schema.optionalKey( + Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMaxLength(256)), Schema.Null]).annotate({ + "title": "User Id", + "description": + "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number." + }) + ) + }).annotate({ "title": "Metadata", "description": "An object describing metadata about the request." }) + ), + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([EffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([JsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "service_tier": Schema.optionalKey( + Schema.Literals(["auto", "standard_only"]).annotate({ + "title": "Service Tier", + "description": + "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details." + }) + ), + "stop_sequences": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "title": "Stop Sequences", + "description": + "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence." + }) + ), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Stream", + "description": + "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(RequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Temperature", + "description": + "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "thinking": Schema.optionalKey(ThinkingConfigParam), + "tool_choice": Schema.optionalKey(ToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Tool, + BashTool_20250124, + CodeExecutionTool_20250522, + CodeExecutionTool_20250825, + CodeExecutionTool_20260120, + MemoryTool_20250818, + TextEditor_20250124, + TextEditor_20250429, + TextEditor_20250728, + WebSearchTool_20250305, + WebFetchTool_20250910, + WebSearchTool_20260209, + WebFetchTool_20260209, + ToolSearchToolBM25_20251119, + ToolSearchToolRegex_20251119 + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ), + "top_k": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top K", + "description": + "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top P", + "description": + "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ) +}).annotate({ "title": "CreateMessageParams" }) +export type MessageBatchIndividualRequestParams = { + readonly "custom_id": string + readonly "params": { + readonly "model": Model + readonly "messages": ReadonlyArray + readonly "cache_control"?: CacheControlEphemeral | null + readonly "container"?: string | null + readonly "inference_geo"?: string | null + readonly "max_tokens": number + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "output_config"?: { readonly "effort"?: EffortLevel | null; readonly "format"?: JsonOutputFormat | null } + readonly "service_tier"?: "auto" | "standard_only" + readonly "stop_sequences"?: ReadonlyArray + readonly "stream"?: boolean + readonly "system"?: string | ReadonlyArray + readonly "temperature"?: number + readonly "thinking"?: ThinkingConfigParam + readonly "tool_choice"?: ToolChoice + readonly "tools"?: ReadonlyArray< + | Tool + | BashTool_20250124 + | CodeExecutionTool_20250522 + | CodeExecutionTool_20250825 + | CodeExecutionTool_20260120 + | MemoryTool_20250818 + | TextEditor_20250124 + | TextEditor_20250429 + | TextEditor_20250728 + | WebSearchTool_20250305 + | WebFetchTool_20250910 + | WebSearchTool_20260209 + | WebFetchTool_20260209 + | ToolSearchToolBM25_20251119 + | ToolSearchToolRegex_20251119 + > + readonly "top_k"?: number + readonly "top_p"?: number + } +} +export const MessageBatchIndividualRequestParams = Schema.Struct({ + "custom_id": Schema.String.annotate({ + "title": "Custom Id", + "description": + "Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order.\n\nMust be unique for each request within the Message Batch." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]{1,64}$")) + ), + "params": Schema.Struct({ + "model": Model, + "messages": Schema.Array(InputMessage).annotate({ + "title": "Messages", + "description": + "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request." + }), + "cache_control": Schema.optionalKey( + Schema.Union([Schema.Union([CacheControlEphemeral], { mode: "oneOf" }), Schema.Null]).annotate({ + "title": "Cache Control", + "description": + "Top-level cache control automatically applies a cache_control marker to the last cacheable block in the request." + }) + ), + "container": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Container", + "description": "Container identifier for reuse across requests." + }) + ), + "inference_geo": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Inference Geo", + "description": + "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used." + }) + ), + "max_tokens": Schema.Number.annotate({ + "title": "Max Tokens", + "description": + "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + "metadata": Schema.optionalKey( + Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String.check(Schema.isMaxLength(256)), Schema.Null]).annotate({ + "title": "User Id", + "description": + "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number." + }) + ) + }).annotate({ "title": "Metadata", "description": "An object describing metadata about the request." }) + ), + "output_config": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([EffortLevel, Schema.Null]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer.\n\nValid values are `low`, `medium`, `high`, or `max`." + }) + ), + "format": Schema.optionalKey( + Schema.Union([JsonOutputFormat, Schema.Null]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)" + }) + ) + }).annotate({ + "title": "OutputConfig", + "description": "Configuration options for the model's output, such as the output format." + }) + ), + "service_tier": Schema.optionalKey( + Schema.Literals(["auto", "standard_only"]).annotate({ + "title": "Service Tier", + "description": + "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details." + }) + ), + "stop_sequences": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "title": "Stop Sequences", + "description": + "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence." + }) + ), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "title": "Stream", + "description": + "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details." + }) + ), + "system": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(RequestTextBlock)]).annotate({ + "title": "System", + "description": + "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts)." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Temperature", + "description": + "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "thinking": Schema.optionalKey(ThinkingConfigParam), + "tool_choice": Schema.optionalKey(ToolChoice), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Tool, + BashTool_20250124, + CodeExecutionTool_20250522, + CodeExecutionTool_20250825, + CodeExecutionTool_20260120, + MemoryTool_20250818, + TextEditor_20250124, + TextEditor_20250429, + TextEditor_20250728, + WebSearchTool_20250305, + WebFetchTool_20250910, + WebSearchTool_20260209, + WebFetchTool_20260209, + ToolSearchToolBM25_20251119, + ToolSearchToolRegex_20251119 + ], { mode: "oneOf" }) + ).annotate({ + "title": "Tools", + "description": + "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details." + }) + ), + "top_k": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top K", + "description": + "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Top P", + "description": + "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ) + }).annotate({ + "title": "CreateMessageParams", + "description": + "Messages API creation parameters for the individual request.\n\nSee the [Messages API reference](https://docs.claude.com/en/api/messages) for full documentation on available parameters." + }) +}).annotate({ "title": "MessageBatchIndividualRequestParams" }) +export type BetaCreateMessageBatchParams = { + readonly "requests": ReadonlyArray +} +export const BetaCreateMessageBatchParams = Schema.Struct({ + "requests": Schema.Array(BetaMessageBatchIndividualRequestParams).annotate({ + "title": "Requests", + "description": "List of requests for prompt completion. Each is an individual request to create a Message." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(100000)) +}).annotate({ "title": "CreateMessageBatchParams" }) +export type CreateMessageBatchParams = { readonly "requests": ReadonlyArray } +export const CreateMessageBatchParams = Schema.Struct({ + "requests": Schema.Array(MessageBatchIndividualRequestParams).annotate({ + "title": "Requests", + "description": "List of requests for prompt completion. Each is an individual request to create a Message." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(100000)) +}).annotate({ "title": "CreateMessageBatchParams" }) +// schemas +export type MessagesPostParams = { readonly "anthropic-version"?: string } +export const MessagesPostParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type MessagesPostRequestJson = CreateMessageParams +export const MessagesPostRequestJson = CreateMessageParams +export type MessagesPost200 = Message +export const MessagesPost200 = Message +export type MessagesPost4XX = ErrorResponse +export const MessagesPost4XX = ErrorResponse +export type CompletePostParams = { readonly "anthropic-version"?: string; readonly "anthropic-beta"?: string } +export const CompletePostParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ) +}) +export type CompletePostRequestJson = CompletionRequest +export const CompletePostRequestJson = CompletionRequest +export type CompletePost200 = CompletionResponse +export const CompletePost200 = CompletionResponse +export type CompletePost4XX = ErrorResponse +export const CompletePost4XX = ErrorResponse +export type ModelsListParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-version"?: string + readonly "x-api-key"?: string + readonly "anthropic-beta"?: string +} +export const ModelsListParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ) +}) +export type ModelsList200 = ListResponse_ModelInfo_ +export const ModelsList200 = ListResponse_ModelInfo_ +export type ModelsList4XX = ErrorResponse +export const ModelsList4XX = ErrorResponse +export type ModelsGetParams = { + readonly "anthropic-version"?: string + readonly "x-api-key"?: string + readonly "anthropic-beta"?: string +} +export const ModelsGetParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ) +}) +export type ModelsGet200 = ModelInfo +export const ModelsGet200 = ModelInfo +export type ModelsGet4XX = ErrorResponse +export const ModelsGet4XX = ErrorResponse +export type MessageBatchesListParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const MessageBatchesListParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type MessageBatchesList200 = ListResponse_MessageBatch_ +export const MessageBatchesList200 = ListResponse_MessageBatch_ +export type MessageBatchesList4XX = ErrorResponse +export const MessageBatchesList4XX = ErrorResponse +export type MessageBatchesPostParams = { readonly "anthropic-version"?: string } +export const MessageBatchesPostParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type MessageBatchesPostRequestJson = CreateMessageBatchParams +export const MessageBatchesPostRequestJson = CreateMessageBatchParams +export type MessageBatchesPost200 = MessageBatch +export const MessageBatchesPost200 = MessageBatch +export type MessageBatchesPost4XX = ErrorResponse +export const MessageBatchesPost4XX = ErrorResponse +export type MessageBatchesRetrieveParams = { readonly "anthropic-version"?: string; readonly "x-api-key"?: string } +export const MessageBatchesRetrieveParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type MessageBatchesRetrieve200 = MessageBatch +export const MessageBatchesRetrieve200 = MessageBatch +export type MessageBatchesRetrieve4XX = ErrorResponse +export const MessageBatchesRetrieve4XX = ErrorResponse +export type MessageBatchesDeleteParams = { readonly "anthropic-version"?: string; readonly "x-api-key"?: string } +export const MessageBatchesDeleteParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type MessageBatchesDelete200 = DeleteMessageBatchResponse +export const MessageBatchesDelete200 = DeleteMessageBatchResponse +export type MessageBatchesDelete4XX = ErrorResponse +export const MessageBatchesDelete4XX = ErrorResponse +export type MessageBatchesCancelParams = { readonly "anthropic-version"?: string } +export const MessageBatchesCancelParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type MessageBatchesCancel200 = MessageBatch +export const MessageBatchesCancel200 = MessageBatch +export type MessageBatchesCancel4XX = ErrorResponse +export const MessageBatchesCancel4XX = ErrorResponse +export type MessageBatchesResultsParams = { readonly "anthropic-version"?: string; readonly "x-api-key"?: string } +export const MessageBatchesResultsParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type MessageBatchesResults4XX = ErrorResponse +export const MessageBatchesResults4XX = ErrorResponse +export type MessagesCountTokensPostParams = { readonly "anthropic-version"?: string } +export const MessagesCountTokensPostParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type MessagesCountTokensPostRequestJson = CountMessageTokensParams +export const MessagesCountTokensPostRequestJson = CountMessageTokensParams +export type MessagesCountTokensPost200 = CountMessageTokensResponse +export const MessagesCountTokensPost200 = CountMessageTokensResponse +export type MessagesCountTokensPost4XX = ErrorResponse +export const MessagesCountTokensPost4XX = ErrorResponse +export type ListFilesV1FilesGetParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const ListFilesV1FilesGetParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type ListFilesV1FilesGet200 = FileListResponse +export const ListFilesV1FilesGet200 = FileListResponse +export type ListFilesV1FilesGet4XX = ErrorResponse +export const ListFilesV1FilesGet4XX = ErrorResponse +export type UploadFileV1FilesPostParams = { readonly "anthropic-beta"?: string; readonly "anthropic-version"?: string } +export const UploadFileV1FilesPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type UploadFileV1FilesPostRequestFormData = { readonly "file": string } +export const UploadFileV1FilesPostRequestFormData = Schema.Struct({ + "file": Schema.String.annotate({ "description": "The file to upload", "format": "binary" }) +}) +export type UploadFileV1FilesPost200 = FileMetadataSchema +export const UploadFileV1FilesPost200 = FileMetadataSchema +export type UploadFileV1FilesPost4XX = ErrorResponse +export const UploadFileV1FilesPost4XX = ErrorResponse +export type GetFileMetadataV1FilesFileIdGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const GetFileMetadataV1FilesFileIdGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type GetFileMetadataV1FilesFileIdGet200 = FileMetadataSchema +export const GetFileMetadataV1FilesFileIdGet200 = FileMetadataSchema +export type GetFileMetadataV1FilesFileIdGet4XX = ErrorResponse +export const GetFileMetadataV1FilesFileIdGet4XX = ErrorResponse +export type DeleteFileV1FilesFileIdDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const DeleteFileV1FilesFileIdDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type DeleteFileV1FilesFileIdDelete200 = FileDeleteResponse +export const DeleteFileV1FilesFileIdDelete200 = FileDeleteResponse +export type DeleteFileV1FilesFileIdDelete4XX = ErrorResponse +export const DeleteFileV1FilesFileIdDelete4XX = ErrorResponse +export type DownloadFileV1FilesFileIdContentGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const DownloadFileV1FilesFileIdContentGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type ListSkillsV1SkillsGetParams = { + readonly "page"?: string | null + readonly "limit"?: number + readonly "source"?: string | null + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const ListSkillsV1SkillsGetParams = Schema.Struct({ + "page": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Page", + "description": + "Pagination token for fetching a specific page of results.\n\nPass the value from a previous response's `next_page` field to get the next page of results." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of results to return per page.\n\nMaximum value is 100. Defaults to 20.", + "default": 20 + }).check(Schema.isInt()) + ), + "source": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Source", + "description": + "Filter skills by source.\n\nIf provided, only skills from the specified source will be returned:\n* `\"custom\"`: only return user-created skills\n* `\"anthropic\"`: only return Anthropic-created skills" + }) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type ListSkillsV1SkillsGet200 = ListSkillsResponse +export const ListSkillsV1SkillsGet200 = ListSkillsResponse +export type ListSkillsV1SkillsGet4XX = ErrorResponse +export const ListSkillsV1SkillsGet4XX = ErrorResponse +export type CreateSkillV1SkillsPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const CreateSkillV1SkillsPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type CreateSkillV1SkillsPostRequestFormData = Body_create_skill_v1_skills_post +export const CreateSkillV1SkillsPostRequestFormData = Body_create_skill_v1_skills_post +export type CreateSkillV1SkillsPost200 = CreateSkillResponse +export const CreateSkillV1SkillsPost200 = CreateSkillResponse +export type CreateSkillV1SkillsPost4XX = ErrorResponse +export const CreateSkillV1SkillsPost4XX = ErrorResponse +export type GetSkillV1SkillsSkillIdGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const GetSkillV1SkillsSkillIdGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type GetSkillV1SkillsSkillIdGet200 = GetSkillResponse +export const GetSkillV1SkillsSkillIdGet200 = GetSkillResponse +export type GetSkillV1SkillsSkillIdGet4XX = ErrorResponse +export const GetSkillV1SkillsSkillIdGet4XX = ErrorResponse +export type DeleteSkillV1SkillsSkillIdDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const DeleteSkillV1SkillsSkillIdDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type DeleteSkillV1SkillsSkillIdDelete200 = DeleteSkillResponse +export const DeleteSkillV1SkillsSkillIdDelete200 = DeleteSkillResponse +export type DeleteSkillV1SkillsSkillIdDelete4XX = ErrorResponse +export const DeleteSkillV1SkillsSkillIdDelete4XX = ErrorResponse +export type ListSkillVersionsV1SkillsSkillIdVersionsGetParams = { + readonly "page"?: string | null + readonly "limit"?: number | null + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const ListSkillVersionsV1SkillsSkillIdVersionsGetParams = Schema.Struct({ + "page": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Page", + "description": "Optionally set to the `next_page` token from the previous response." + }) + ), + "limit": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`." + }) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type ListSkillVersionsV1SkillsSkillIdVersionsGet200 = ListSkillVersionsResponse +export const ListSkillVersionsV1SkillsSkillIdVersionsGet200 = ListSkillVersionsResponse +export type ListSkillVersionsV1SkillsSkillIdVersionsGet4XX = ErrorResponse +export const ListSkillVersionsV1SkillsSkillIdVersionsGet4XX = ErrorResponse +export type CreateSkillVersionV1SkillsSkillIdVersionsPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const CreateSkillVersionV1SkillsSkillIdVersionsPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type CreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData = + Body_create_skill_version_v1_skills__skill_id__versions_post +export const CreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData = + Body_create_skill_version_v1_skills__skill_id__versions_post +export type CreateSkillVersionV1SkillsSkillIdVersionsPost200 = CreateSkillVersionResponse +export const CreateSkillVersionV1SkillsSkillIdVersionsPost200 = CreateSkillVersionResponse +export type CreateSkillVersionV1SkillsSkillIdVersionsPost4XX = ErrorResponse +export const CreateSkillVersionV1SkillsSkillIdVersionsPost4XX = ErrorResponse +export type GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type GetSkillVersionV1SkillsSkillIdVersionsVersionGet200 = GetSkillVersionResponse +export const GetSkillVersionV1SkillsSkillIdVersionsVersionGet200 = GetSkillVersionResponse +export type GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX = ErrorResponse +export const GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX = ErrorResponse +export type DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200 = DeleteSkillVersionResponse +export const DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200 = DeleteSkillVersionResponse +export type DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX = ErrorResponse +export const DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX = ErrorResponse +export type BetaMessagesPostParams = { readonly "anthropic-beta"?: string; readonly "anthropic-version"?: string } +export const BetaMessagesPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaMessagesPostRequestJson = BetaCreateMessageParams +export const BetaMessagesPostRequestJson = BetaCreateMessageParams +export type BetaMessagesPost200 = BetaMessage +export const BetaMessagesPost200 = BetaMessage +export type BetaMessagesPost4XX = BetaErrorResponse +export const BetaMessagesPost4XX = BetaErrorResponse +export type BetaModelsListParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-version"?: string + readonly "x-api-key"?: string + readonly "anthropic-beta"?: string +} +export const BetaModelsListParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ) +}) +export type BetaModelsList200 = BetaListResponse_ModelInfo_ +export const BetaModelsList200 = BetaListResponse_ModelInfo_ +export type BetaModelsList4XX = BetaErrorResponse +export const BetaModelsList4XX = BetaErrorResponse +export type BetaModelsGetParams = { + readonly "anthropic-version"?: string + readonly "x-api-key"?: string + readonly "anthropic-beta"?: string +} +export const BetaModelsGetParams = Schema.Struct({ + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ) +}) +export type BetaModelsGet200 = BetaModelInfo +export const BetaModelsGet200 = BetaModelInfo +export type BetaModelsGet4XX = BetaErrorResponse +export const BetaModelsGet4XX = BetaErrorResponse +export type BetaMessageBatchesListParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaMessageBatchesListParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaMessageBatchesList200 = BetaListResponse_MessageBatch_ +export const BetaMessageBatchesList200 = BetaListResponse_MessageBatch_ +export type BetaMessageBatchesList4XX = BetaErrorResponse +export const BetaMessageBatchesList4XX = BetaErrorResponse +export type BetaMessageBatchesPostParams = { readonly "anthropic-beta"?: string; readonly "anthropic-version"?: string } +export const BetaMessageBatchesPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaMessageBatchesPostRequestJson = BetaCreateMessageBatchParams +export const BetaMessageBatchesPostRequestJson = BetaCreateMessageBatchParams +export type BetaMessageBatchesPost200 = BetaMessageBatch +export const BetaMessageBatchesPost200 = BetaMessageBatch +export type BetaMessageBatchesPost4XX = BetaErrorResponse +export const BetaMessageBatchesPost4XX = BetaErrorResponse +export type BetaMessageBatchesRetrieveParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaMessageBatchesRetrieveParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaMessageBatchesRetrieve200 = BetaMessageBatch +export const BetaMessageBatchesRetrieve200 = BetaMessageBatch +export type BetaMessageBatchesRetrieve4XX = BetaErrorResponse +export const BetaMessageBatchesRetrieve4XX = BetaErrorResponse +export type BetaMessageBatchesDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaMessageBatchesDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaMessageBatchesDelete200 = BetaDeleteMessageBatchResponse +export const BetaMessageBatchesDelete200 = BetaDeleteMessageBatchResponse +export type BetaMessageBatchesDelete4XX = BetaErrorResponse +export const BetaMessageBatchesDelete4XX = BetaErrorResponse +export type BetaMessageBatchesCancelParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const BetaMessageBatchesCancelParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaMessageBatchesCancel200 = BetaMessageBatch +export const BetaMessageBatchesCancel200 = BetaMessageBatch +export type BetaMessageBatchesCancel4XX = BetaErrorResponse +export const BetaMessageBatchesCancel4XX = BetaErrorResponse +export type BetaMessageBatchesResultsParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaMessageBatchesResultsParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaMessageBatchesResults4XX = BetaErrorResponse +export const BetaMessageBatchesResults4XX = BetaErrorResponse +export type BetaMessagesCountTokensPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const BetaMessagesCountTokensPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaMessagesCountTokensPostRequestJson = BetaCountMessageTokensParams +export const BetaMessagesCountTokensPostRequestJson = BetaCountMessageTokensParams +export type BetaMessagesCountTokensPost200 = BetaCountMessageTokensResponse +export const BetaMessagesCountTokensPost200 = BetaCountMessageTokensResponse +export type BetaMessagesCountTokensPost4XX = BetaErrorResponse +export const BetaMessagesCountTokensPost4XX = BetaErrorResponse +export type BetaListFilesV1FilesGetParams = { + readonly "before_id"?: string + readonly "after_id"?: string + readonly "limit"?: number + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaListFilesV1FilesGetParams = Schema.Struct({ + "before_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "Before Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object." + }) + ), + "after_id": Schema.optionalKey( + Schema.String.annotate({ + "title": "After Id", + "description": + "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + "default": 20 + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaListFilesV1FilesGet200 = BetaFileListResponse +export const BetaListFilesV1FilesGet200 = BetaFileListResponse +export type BetaListFilesV1FilesGet4XX = BetaErrorResponse +export const BetaListFilesV1FilesGet4XX = BetaErrorResponse +export type BetaUploadFileV1FilesPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const BetaUploadFileV1FilesPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaUploadFileV1FilesPostRequestFormData = { readonly "file": string } +export const BetaUploadFileV1FilesPostRequestFormData = Schema.Struct({ + "file": Schema.String.annotate({ "description": "The file to upload", "format": "binary" }) +}) +export type BetaUploadFileV1FilesPost200 = BetaFileMetadataSchema +export const BetaUploadFileV1FilesPost200 = BetaFileMetadataSchema +export type BetaUploadFileV1FilesPost4XX = BetaErrorResponse +export const BetaUploadFileV1FilesPost4XX = BetaErrorResponse +export type BetaGetFileMetadataV1FilesFileIdGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaGetFileMetadataV1FilesFileIdGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaGetFileMetadataV1FilesFileIdGet200 = BetaFileMetadataSchema +export const BetaGetFileMetadataV1FilesFileIdGet200 = BetaFileMetadataSchema +export type BetaGetFileMetadataV1FilesFileIdGet4XX = BetaErrorResponse +export const BetaGetFileMetadataV1FilesFileIdGet4XX = BetaErrorResponse +export type BetaDeleteFileV1FilesFileIdDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaDeleteFileV1FilesFileIdDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaDeleteFileV1FilesFileIdDelete200 = BetaFileDeleteResponse +export const BetaDeleteFileV1FilesFileIdDelete200 = BetaFileDeleteResponse +export type BetaDeleteFileV1FilesFileIdDelete4XX = BetaErrorResponse +export const BetaDeleteFileV1FilesFileIdDelete4XX = BetaErrorResponse +export type BetaDownloadFileV1FilesFileIdContentGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaDownloadFileV1FilesFileIdContentGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaListSkillsV1SkillsGetParams = { + readonly "page"?: string | null + readonly "limit"?: number + readonly "source"?: string | null + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaListSkillsV1SkillsGetParams = Schema.Struct({ + "page": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Page", + "description": + "Pagination token for fetching a specific page of results.\n\nPass the value from a previous response's `next_page` field to get the next page of results." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "title": "Limit", + "description": "Number of results to return per page.\n\nMaximum value is 100. Defaults to 20.", + "default": 20 + }).check(Schema.isInt()) + ), + "source": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Source", + "description": + "Filter skills by source.\n\nIf provided, only skills from the specified source will be returned:\n* `\"custom\"`: only return user-created skills\n* `\"anthropic\"`: only return Anthropic-created skills" + }) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaListSkillsV1SkillsGet200 = BetaListSkillsResponse +export const BetaListSkillsV1SkillsGet200 = BetaListSkillsResponse +export type BetaListSkillsV1SkillsGet4XX = BetaErrorResponse +export const BetaListSkillsV1SkillsGet4XX = BetaErrorResponse +export type BetaCreateSkillV1SkillsPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const BetaCreateSkillV1SkillsPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaCreateSkillV1SkillsPostRequestFormData = BetaBody_create_skill_v1_skills_post +export const BetaCreateSkillV1SkillsPostRequestFormData = BetaBody_create_skill_v1_skills_post +export type BetaCreateSkillV1SkillsPost200 = BetaCreateSkillResponse +export const BetaCreateSkillV1SkillsPost200 = BetaCreateSkillResponse +export type BetaCreateSkillV1SkillsPost4XX = BetaErrorResponse +export const BetaCreateSkillV1SkillsPost4XX = BetaErrorResponse +export type BetaGetSkillV1SkillsSkillIdGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaGetSkillV1SkillsSkillIdGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaGetSkillV1SkillsSkillIdGet200 = BetaGetSkillResponse +export const BetaGetSkillV1SkillsSkillIdGet200 = BetaGetSkillResponse +export type BetaGetSkillV1SkillsSkillIdGet4XX = BetaErrorResponse +export const BetaGetSkillV1SkillsSkillIdGet4XX = BetaErrorResponse +export type BetaDeleteSkillV1SkillsSkillIdDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaDeleteSkillV1SkillsSkillIdDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaDeleteSkillV1SkillsSkillIdDelete200 = BetaDeleteSkillResponse +export const BetaDeleteSkillV1SkillsSkillIdDelete200 = BetaDeleteSkillResponse +export type BetaDeleteSkillV1SkillsSkillIdDelete4XX = BetaErrorResponse +export const BetaDeleteSkillV1SkillsSkillIdDelete4XX = BetaErrorResponse +export type BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams = { + readonly "page"?: string | null + readonly "limit"?: number | null + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams = Schema.Struct({ + "page": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "title": "Page", + "description": "Optionally set to the `next_page` token from the previous response." + }) + ), + "limit": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "title": "Limit", + "description": "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`." + }) + ), + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaListSkillVersionsV1SkillsSkillIdVersionsGet200 = BetaListSkillVersionsResponse +export const BetaListSkillVersionsV1SkillsSkillIdVersionsGet200 = BetaListSkillVersionsResponse +export type BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX = BetaErrorResponse +export const BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX = BetaErrorResponse +export type BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string +} +export const BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ) +}) +export type BetaCreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData = + BetaBody_create_skill_version_v1_skills__skill_id__versions_post +export const BetaCreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData = + BetaBody_create_skill_version_v1_skills__skill_id__versions_post +export type BetaCreateSkillVersionV1SkillsSkillIdVersionsPost200 = BetaCreateSkillVersionResponse +export const BetaCreateSkillVersionV1SkillsSkillIdVersionsPost200 = BetaCreateSkillVersionResponse +export type BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX = BetaErrorResponse +export const BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX = BetaErrorResponse +export type BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet200 = BetaGetSkillVersionResponse +export const BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet200 = BetaGetSkillVersionResponse +export type BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX = BetaErrorResponse +export const BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX = BetaErrorResponse +export type BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams = { + readonly "anthropic-beta"?: string + readonly "anthropic-version"?: string + readonly "x-api-key"?: string +} +export const BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams = Schema.Struct({ + "anthropic-beta": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Beta", + "description": + "Optional header to specify the beta version(s) you want to use.\n\nTo use multiple betas, use a comma separated list like `beta1,beta2` or specify the header multiple times for each beta." + }) + ), + "anthropic-version": Schema.optionalKey( + Schema.String.annotate({ + "title": "Anthropic-Version", + "description": + "The version of the Claude API you want to use.\n\nRead more about versioning and our version history [here](https://docs.claude.com/en/api/versioning)." + }) + ), + "x-api-key": Schema.optionalKey(Schema.String.annotate({ + "title": "X-Api-Key", + "description": + "Your unique API key for authentication.\n\nThis key is required in the header of all API requests, to authenticate your account and access Anthropic's services. Get your API key through the [Console](https://console.anthropic.com/settings/keys). Each key is scoped to a Workspace." + })) +}) +export type BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200 = BetaDeleteSkillVersionResponse +export const BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200 = BetaDeleteSkillVersionResponse +export type BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX = BetaErrorResponse +export const BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX = BetaErrorResponse + +export interface OperationConfig { + /** + * Whether or not the response should be included in the value returned from + * an operation. + * + * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, + * where `A` is the success type of the operation. + * + * If set to `false`, only the success type of the operation will be returned. + */ + readonly includeResponse?: boolean | undefined +} + +/** + * A utility type which optionally includes the response in the return result + * of an operation based upon the value of the `includeResponse` configuration + * option. + */ +export type WithOptionalResponse = Config extends { + readonly includeResponse: true +} ? [A, HttpClientResponse.HttpClientResponse] : + A + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): AnthropicClient => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description) + }) + }) + ) + ) + const withResponse = (config: Config | undefined) => + ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { + const withOptionalResponse = ( + config?.includeResponse + ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) + : (response: HttpClientResponse.HttpClientResponse) => f(response) + ) as any + return options?.transformClient + ? (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + withOptionalResponse + ) + : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + } + const binaryRequest = ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap + ) + const decodeSuccess = + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(AnthropicClientError(tag, cause, response)) + ) + return { + httpClient, + "messagesPost": (options) => + HttpClientRequest.post(`/v1/messages`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessagesPost200), + "4xx": decodeError("MessagesPost4XX", MessagesPost4XX), + orElse: unexpectedStatus + })) + ), + "completePost": (options) => + HttpClientRequest.post(`/v1/complete`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options.params?.["anthropic-version"] ?? undefined, + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CompletePost200), + "4xx": decodeError("CompletePost4XX", CompletePost4XX), + orElse: unexpectedStatus + })) + ), + "modelsList": (options) => + HttpClientRequest.get(`/v1/models`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsList200), + "4xx": decodeError("ModelsList4XX", ModelsList4XX), + orElse: unexpectedStatus + })) + ), + "modelsGet": (modelId, options) => + HttpClientRequest.get(`/v1/models/${modelId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModelsGet200), + "4xx": decodeError("ModelsGet4XX", ModelsGet4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesList": (options) => + HttpClientRequest.get(`/v1/messages/batches`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatchesList200), + "4xx": decodeError("MessageBatchesList4XX", MessageBatchesList4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesPost": (options) => + HttpClientRequest.post(`/v1/messages/batches`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatchesPost200), + "4xx": decodeError("MessageBatchesPost4XX", MessageBatchesPost4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesRetrieve": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatchesRetrieve200), + "4xx": decodeError("MessageBatchesRetrieve4XX", MessageBatchesRetrieve4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesDelete": (messageBatchId, options) => + HttpClientRequest.delete(`/v1/messages/batches/${messageBatchId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatchesDelete200), + "4xx": decodeError("MessageBatchesDelete4XX", MessageBatchesDelete4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesCancel": (messageBatchId, options) => + HttpClientRequest.post(`/v1/messages/batches/${messageBatchId}/cancel`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options?.params?.["anthropic-version"] ?? undefined }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessageBatchesCancel200), + "4xx": decodeError("MessageBatchesCancel4XX", MessageBatchesCancel4XX), + orElse: unexpectedStatus + })) + ), + "messageBatchesResults": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}/results`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "4xx": decodeError("MessageBatchesResults4XX", MessageBatchesResults4XX), + orElse: unexpectedStatus + })) + ), + "messagesCountTokensPost": (options) => + HttpClientRequest.post(`/v1/messages/count_tokens`).pipe( + HttpClientRequest.setHeaders({ "anthropic-version": options.params?.["anthropic-version"] ?? undefined }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(MessagesCountTokensPost200), + "4xx": decodeError("MessagesCountTokensPost4XX", MessagesCountTokensPost4XX), + orElse: unexpectedStatus + })) + ), + "listFilesV1FilesGet": (options) => + HttpClientRequest.get(`/v1/files`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFilesV1FilesGet200), + "4xx": decodeError("ListFilesV1FilesGet4XX", ListFilesV1FilesGet4XX), + orElse: unexpectedStatus + })) + ), + "uploadFileV1FilesPost": (options) => + HttpClientRequest.post(`/v1/files`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UploadFileV1FilesPost200), + "4xx": decodeError("UploadFileV1FilesPost4XX", UploadFileV1FilesPost4XX), + orElse: unexpectedStatus + })) + ), + "getFileMetadataV1FilesFileIdGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetFileMetadataV1FilesFileIdGet200), + "4xx": decodeError("GetFileMetadataV1FilesFileIdGet4XX", GetFileMetadataV1FilesFileIdGet4XX), + orElse: unexpectedStatus + })) + ), + "deleteFileV1FilesFileIdDelete": (fileId, options) => + HttpClientRequest.delete(`/v1/files/${fileId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFileV1FilesFileIdDelete200), + "4xx": decodeError("DeleteFileV1FilesFileIdDelete4XX", DeleteFileV1FilesFileIdDelete4XX), + orElse: unexpectedStatus + })) + ), + "downloadFileV1FilesFileIdContentGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "downloadFileV1FilesFileIdContentGetStream": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + binaryRequest + ), + "listSkillsV1SkillsGet": (options) => + HttpClientRequest.get(`/v1/skills`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.params?.["page"] as any, + "limit": options?.params?.["limit"] as any, + "source": options?.params?.["source"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkillsV1SkillsGet200), + "4xx": decodeError("ListSkillsV1SkillsGet4XX", ListSkillsV1SkillsGet4XX), + orElse: unexpectedStatus + })) + ), + "createSkillV1SkillsPost": (options) => + HttpClientRequest.post(`/v1/skills`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkillV1SkillsPost200), + "4xx": decodeError("CreateSkillV1SkillsPost4XX", CreateSkillV1SkillsPost4XX), + orElse: unexpectedStatus + })) + ), + "getSkillV1SkillsSkillIdGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillV1SkillsSkillIdGet200), + "4xx": decodeError("GetSkillV1SkillsSkillIdGet4XX", GetSkillV1SkillsSkillIdGet4XX), + orElse: unexpectedStatus + })) + ), + "deleteSkillV1SkillsSkillIdDelete": (skillId, options) => + HttpClientRequest.delete(`/v1/skills/${skillId}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkillV1SkillsSkillIdDelete200), + "4xx": decodeError("DeleteSkillV1SkillsSkillIdDelete4XX", DeleteSkillV1SkillsSkillIdDelete4XX), + orElse: unexpectedStatus + })) + ), + "listSkillVersionsV1SkillsSkillIdVersionsGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.params?.["page"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkillVersionsV1SkillsSkillIdVersionsGet200), + "4xx": decodeError( + "ListSkillVersionsV1SkillsSkillIdVersionsGet4XX", + ListSkillVersionsV1SkillsSkillIdVersionsGet4XX + ), + orElse: unexpectedStatus + })) + ), + "createSkillVersionV1SkillsSkillIdVersionsPost": (skillId, options) => + HttpClientRequest.post(`/v1/skills/${skillId}/versions`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkillVersionV1SkillsSkillIdVersionsPost200), + "4xx": decodeError( + "CreateSkillVersionV1SkillsSkillIdVersionsPost4XX", + CreateSkillVersionV1SkillsSkillIdVersionsPost4XX + ), + orElse: unexpectedStatus + })) + ), + "getSkillVersionV1SkillsSkillIdVersionsVersionGet": (skillId, version, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions/${version}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillVersionV1SkillsSkillIdVersionsVersionGet200), + "4xx": decodeError( + "GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX", + GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX + ), + orElse: unexpectedStatus + })) + ), + "deleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": (skillId, version, options) => + HttpClientRequest.delete(`/v1/skills/${skillId}/versions/${version}`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200), + "4xx": decodeError( + "DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX", + DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX + ), + orElse: unexpectedStatus + })) + ), + "betaMessagesPost": (options) => + HttpClientRequest.post(`/v1/messages?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessagesPost200), + "4xx": decodeError("BetaMessagesPost4XX", BetaMessagesPost4XX), + orElse: unexpectedStatus + })) + ), + "betaModelsList": (options) => + HttpClientRequest.get(`/v1/models?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaModelsList200), + "4xx": decodeError("BetaModelsList4XX", BetaModelsList4XX), + orElse: unexpectedStatus + })) + ), + "betaModelsGet": (modelId, options) => + HttpClientRequest.get(`/v1/models/${modelId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined, + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaModelsGet200), + "4xx": decodeError("BetaModelsGet4XX", BetaModelsGet4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesList": (options) => + HttpClientRequest.get(`/v1/messages/batches?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatchesList200), + "4xx": decodeError("BetaMessageBatchesList4XX", BetaMessageBatchesList4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesPost": (options) => + HttpClientRequest.post(`/v1/messages/batches?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatchesPost200), + "4xx": decodeError("BetaMessageBatchesPost4XX", BetaMessageBatchesPost4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesRetrieve": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatchesRetrieve200), + "4xx": decodeError("BetaMessageBatchesRetrieve4XX", BetaMessageBatchesRetrieve4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesDelete": (messageBatchId, options) => + HttpClientRequest.delete(`/v1/messages/batches/${messageBatchId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatchesDelete200), + "4xx": decodeError("BetaMessageBatchesDelete4XX", BetaMessageBatchesDelete4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesCancel": (messageBatchId, options) => + HttpClientRequest.post(`/v1/messages/batches/${messageBatchId}/cancel?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessageBatchesCancel200), + "4xx": decodeError("BetaMessageBatchesCancel4XX", BetaMessageBatchesCancel4XX), + orElse: unexpectedStatus + })) + ), + "betaMessageBatchesResults": (messageBatchId, options) => + HttpClientRequest.get(`/v1/messages/batches/${messageBatchId}/results?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "4xx": decodeError("BetaMessageBatchesResults4XX", BetaMessageBatchesResults4XX), + orElse: unexpectedStatus + })) + ), + "betaMessagesCountTokensPost": (options) => + HttpClientRequest.post(`/v1/messages/count_tokens?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaMessagesCountTokensPost200), + "4xx": decodeError("BetaMessagesCountTokensPost4XX", BetaMessagesCountTokensPost4XX), + orElse: unexpectedStatus + })) + ), + "betaListFilesV1FilesGet": (options) => + HttpClientRequest.get(`/v1/files?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "before_id": options?.params?.["before_id"] as any, + "after_id": options?.params?.["after_id"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListFilesV1FilesGet200), + "4xx": decodeError("BetaListFilesV1FilesGet4XX", BetaListFilesV1FilesGet4XX), + orElse: unexpectedStatus + })) + ), + "betaUploadFileV1FilesPost": (options) => + HttpClientRequest.post(`/v1/files?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaUploadFileV1FilesPost200), + "4xx": decodeError("BetaUploadFileV1FilesPost4XX", BetaUploadFileV1FilesPost4XX), + orElse: unexpectedStatus + })) + ), + "betaGetFileMetadataV1FilesFileIdGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaGetFileMetadataV1FilesFileIdGet200), + "4xx": decodeError("BetaGetFileMetadataV1FilesFileIdGet4XX", BetaGetFileMetadataV1FilesFileIdGet4XX), + orElse: unexpectedStatus + })) + ), + "betaDeleteFileV1FilesFileIdDelete": (fileId, options) => + HttpClientRequest.delete(`/v1/files/${fileId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteFileV1FilesFileIdDelete200), + "4xx": decodeError("BetaDeleteFileV1FilesFileIdDelete4XX", BetaDeleteFileV1FilesFileIdDelete4XX), + orElse: unexpectedStatus + })) + ), + "betaDownloadFileV1FilesFileIdContentGet": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "betaDownloadFileV1FilesFileIdContentGetStream": (fileId, options) => + HttpClientRequest.get(`/v1/files/${fileId}/content?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + binaryRequest + ), + "betaListSkillsV1SkillsGet": (options) => + HttpClientRequest.get(`/v1/skills?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.params?.["page"] as any, + "limit": options?.params?.["limit"] as any, + "source": options?.params?.["source"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListSkillsV1SkillsGet200), + "4xx": decodeError("BetaListSkillsV1SkillsGet4XX", BetaListSkillsV1SkillsGet4XX), + orElse: unexpectedStatus + })) + ), + "betaCreateSkillV1SkillsPost": (options) => + HttpClientRequest.post(`/v1/skills?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaCreateSkillV1SkillsPost200), + "4xx": decodeError("BetaCreateSkillV1SkillsPost4XX", BetaCreateSkillV1SkillsPost4XX), + orElse: unexpectedStatus + })) + ), + "betaGetSkillV1SkillsSkillIdGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaGetSkillV1SkillsSkillIdGet200), + "4xx": decodeError("BetaGetSkillV1SkillsSkillIdGet4XX", BetaGetSkillV1SkillsSkillIdGet4XX), + orElse: unexpectedStatus + })) + ), + "betaDeleteSkillV1SkillsSkillIdDelete": (skillId, options) => + HttpClientRequest.delete(`/v1/skills/${skillId}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteSkillV1SkillsSkillIdDelete200), + "4xx": decodeError("BetaDeleteSkillV1SkillsSkillIdDelete4XX", BetaDeleteSkillV1SkillsSkillIdDelete4XX), + orElse: unexpectedStatus + })) + ), + "betaListSkillVersionsV1SkillsSkillIdVersionsGet": (skillId, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions?beta=true`).pipe( + HttpClientRequest.setUrlParams({ + "page": options?.params?.["page"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaListSkillVersionsV1SkillsSkillIdVersionsGet200), + "4xx": decodeError( + "BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX", + BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX + ), + orElse: unexpectedStatus + })) + ), + "betaCreateSkillVersionV1SkillsSkillIdVersionsPost": (skillId, options) => + HttpClientRequest.post(`/v1/skills/${skillId}/versions?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options.params?.["anthropic-version"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaCreateSkillVersionV1SkillsSkillIdVersionsPost200), + "4xx": decodeError( + "BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX", + BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX + ), + orElse: unexpectedStatus + })) + ), + "betaGetSkillVersionV1SkillsSkillIdVersionsVersionGet": (skillId, version, options) => + HttpClientRequest.get(`/v1/skills/${skillId}/versions/${version}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet200), + "4xx": decodeError( + "BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX", + BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX + ), + orElse: unexpectedStatus + })) + ), + "betaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": (skillId, version, options) => + HttpClientRequest.delete(`/v1/skills/${skillId}/versions/${version}?beta=true`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": options?.params?.["anthropic-beta"] ?? undefined, + "anthropic-version": options?.params?.["anthropic-version"] ?? undefined, + "x-api-key": options?.params?.["x-api-key"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete200), + "4xx": decodeError( + "BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX", + BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX + ), + orElse: unexpectedStatus + })) + ) + } +} + +export interface AnthropicClient { + readonly httpClient: HttpClient.HttpClient + /** + * Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. + * + * The Messages API can be used for either single queries or stateless multi-turn conversations. + * + * Learn more about the Messages API in our [user guide](https://docs.claude.com/en/docs/initial-setup) + */ + readonly "messagesPost": ( + options: { + readonly params?: typeof MessagesPostParams.Encoded | undefined + readonly payload: typeof MessagesPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | AnthropicClientError<"MessagesPost4XX", typeof MessagesPost4XX.Type> + > + /** + * [Legacy] Create a Text Completion. + * + * The Text Completions API is a legacy API. We recommend using the [Messages API](https://docs.claude.com/en/api/messages) going forward. + * + * Future models and features will not be compatible with Text Completions. See our [migration guide](https://docs.claude.com/en/api/migrating-from-text-completions-to-messages) for guidance in migrating from Text Completions to Messages. + */ + readonly "completePost": ( + options: { + readonly params?: typeof CompletePostParams.Encoded | undefined + readonly payload: typeof CompletePostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | AnthropicClientError<"CompletePost4XX", typeof CompletePost4XX.Type> + > + /** + * List available models. + * + * The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first. + */ + readonly "modelsList": ( + options: + | { readonly params?: typeof ModelsListParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | AnthropicClientError<"ModelsList4XX", typeof ModelsList4XX.Type> + > + /** + * Get a specific model. + * + * The Models API response can be used to determine information about a specific model or resolve a model alias to a model ID. + */ + readonly "modelsGet": ( + modelId: string, + options: + | { readonly params?: typeof ModelsGetParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | AnthropicClientError<"ModelsGet4XX", typeof ModelsGet4XX.Type> + > + /** + * List all Message Batches within a Workspace. Most recently created batches are returned first. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesList": ( + options: { + readonly params?: typeof MessageBatchesListParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesList4XX", typeof MessageBatchesList4XX.Type> + > + /** + * Send a batch of Message creation requests. + * + * The Message Batches API can be used to process multiple Messages API requests at once. Once a Message Batch is created, it begins processing immediately. Batches can take up to 24 hours to complete. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesPost": ( + options: { + readonly params?: typeof MessageBatchesPostParams.Encoded | undefined + readonly payload: typeof MessageBatchesPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesPost4XX", typeof MessageBatchesPost4XX.Type> + > + /** + * This endpoint is idempotent and can be used to poll for Message Batch completion. To access the results of a Message Batch, make a request to the `results_url` field in the response. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesRetrieve": ( + messageBatchId: string, + options: { + readonly params?: typeof MessageBatchesRetrieveParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesRetrieve4XX", typeof MessageBatchesRetrieve4XX.Type> + > + /** + * Delete a Message Batch. + * + * Message Batches can only be deleted once they've finished processing. If you'd like to delete an in-progress batch, you must first cancel it. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesDelete": ( + messageBatchId: string, + options: { + readonly params?: typeof MessageBatchesDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesDelete4XX", typeof MessageBatchesDelete4XX.Type> + > + /** + * Batches may be canceled any time before processing ends. Once cancellation is initiated, the batch enters a `canceling` state, at which time the system may complete any in-progress, non-interruptible requests before finalizing cancellation. + * + * The number of canceled requests is specified in `request_counts`. To determine which requests were canceled, check the individual results within the batch. Note that cancellation may not result in any canceled requests if they were non-interruptible. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesCancel": ( + messageBatchId: string, + options: { + readonly params?: typeof MessageBatchesCancelParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesCancel4XX", typeof MessageBatchesCancel4XX.Type> + > + /** + * Streams the results of a Message Batch as a `.jsonl` file. + * + * Each line in the file is a JSON object containing the result of a single request in the Message Batch. Results are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "messageBatchesResults": ( + messageBatchId: string, + options: { + readonly params?: typeof MessageBatchesResultsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessageBatchesResults4XX", typeof MessageBatchesResults4XX.Type> + > + /** + * Count the number of tokens in a Message. + * + * The Token Count API can be used to count the number of tokens in a Message, including tools, images, and documents, without creating it. + * + * Learn more about token counting in our [user guide](https://docs.claude.com/en/docs/build-with-claude/token-counting) + */ + readonly "messagesCountTokensPost": ( + options: { + readonly params?: typeof MessagesCountTokensPostParams.Encoded | undefined + readonly payload: typeof MessagesCountTokensPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"MessagesCountTokensPost4XX", typeof MessagesCountTokensPost4XX.Type> + > + /** + * List Files + */ + readonly "listFilesV1FilesGet": ( + options: { + readonly params?: typeof ListFilesV1FilesGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"ListFilesV1FilesGet4XX", typeof ListFilesV1FilesGet4XX.Type> + > + /** + * Upload File + */ + readonly "uploadFileV1FilesPost": ( + options: { + readonly params?: typeof UploadFileV1FilesPostParams.Encoded | undefined + readonly payload: typeof UploadFileV1FilesPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"UploadFileV1FilesPost4XX", typeof UploadFileV1FilesPost4XX.Type> + > + /** + * Get File Metadata + */ + readonly "getFileMetadataV1FilesFileIdGet": ( + fileId: string, + options: { + readonly params?: typeof GetFileMetadataV1FilesFileIdGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"GetFileMetadataV1FilesFileIdGet4XX", typeof GetFileMetadataV1FilesFileIdGet4XX.Type> + > + /** + * Delete File + */ + readonly "deleteFileV1FilesFileIdDelete": ( + fileId: string, + options: { + readonly params?: typeof DeleteFileV1FilesFileIdDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"DeleteFileV1FilesFileIdDelete4XX", typeof DeleteFileV1FilesFileIdDelete4XX.Type> + > + /** + * Download File + */ + readonly "downloadFileV1FilesFileIdContentGet": ( + fileId: string, + options: { + readonly params?: typeof DownloadFileV1FilesFileIdContentGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Download File + */ + readonly "downloadFileV1FilesFileIdContentGetStream": ( + fileId: string, + options: { readonly params?: typeof DownloadFileV1FilesFileIdContentGetParams.Encoded | undefined } | undefined + ) => Stream.Stream + /** + * List Skills + */ + readonly "listSkillsV1SkillsGet": ( + options: { + readonly params?: typeof ListSkillsV1SkillsGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"ListSkillsV1SkillsGet4XX", typeof ListSkillsV1SkillsGet4XX.Type> + > + /** + * Create Skill + */ + readonly "createSkillV1SkillsPost": ( + options: { + readonly params?: typeof CreateSkillV1SkillsPostParams.Encoded | undefined + readonly payload: typeof CreateSkillV1SkillsPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"CreateSkillV1SkillsPost4XX", typeof CreateSkillV1SkillsPost4XX.Type> + > + /** + * Get Skill + */ + readonly "getSkillV1SkillsSkillIdGet": ( + skillId: string, + options: { + readonly params?: typeof GetSkillV1SkillsSkillIdGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"GetSkillV1SkillsSkillIdGet4XX", typeof GetSkillV1SkillsSkillIdGet4XX.Type> + > + /** + * Delete Skill + */ + readonly "deleteSkillV1SkillsSkillIdDelete": ( + skillId: string, + options: { + readonly params?: typeof DeleteSkillV1SkillsSkillIdDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"DeleteSkillV1SkillsSkillIdDelete4XX", typeof DeleteSkillV1SkillsSkillIdDelete4XX.Type> + > + /** + * List Skill Versions + */ + readonly "listSkillVersionsV1SkillsSkillIdVersionsGet": ( + skillId: string, + options: { + readonly params?: typeof ListSkillVersionsV1SkillsSkillIdVersionsGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "ListSkillVersionsV1SkillsSkillIdVersionsGet4XX", + typeof ListSkillVersionsV1SkillsSkillIdVersionsGet4XX.Type + > + > + /** + * Create Skill Version + */ + readonly "createSkillVersionV1SkillsSkillIdVersionsPost": ( + skillId: string, + options: { + readonly params?: typeof CreateSkillVersionV1SkillsSkillIdVersionsPostParams.Encoded | undefined + readonly payload: typeof CreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "CreateSkillVersionV1SkillsSkillIdVersionsPost4XX", + typeof CreateSkillVersionV1SkillsSkillIdVersionsPost4XX.Type + > + > + /** + * Get Skill Version + */ + readonly "getSkillVersionV1SkillsSkillIdVersionsVersionGet": ( + skillId: string, + version: string, + options: { + readonly params?: typeof GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX", + typeof GetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX.Type + > + > + /** + * Delete Skill Version + */ + readonly "deleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": ( + skillId: string, + version: string, + options: { + readonly params?: typeof DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX", + typeof DeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX.Type + > + > + /** + * Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. + * + * The Messages API can be used for either single queries or stateless multi-turn conversations. + * + * Learn more about the Messages API in our [user guide](https://docs.claude.com/en/docs/initial-setup) + */ + readonly "betaMessagesPost": ( + options: { + readonly params?: typeof BetaMessagesPostParams.Encoded | undefined + readonly payload: typeof BetaMessagesPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessagesPost4XX", typeof BetaMessagesPost4XX.Type> + > + /** + * List available models. + * + * The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first. + */ + readonly "betaModelsList": ( + options: + | { readonly params?: typeof BetaModelsListParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaModelsList4XX", typeof BetaModelsList4XX.Type> + > + /** + * Get a specific model. + * + * The Models API response can be used to determine information about a specific model or resolve a model alias to a model ID. + */ + readonly "betaModelsGet": ( + modelId: string, + options: + | { readonly params?: typeof BetaModelsGetParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaModelsGet4XX", typeof BetaModelsGet4XX.Type> + > + /** + * List all Message Batches within a Workspace. Most recently created batches are returned first. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesList": ( + options: { + readonly params?: typeof BetaMessageBatchesListParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesList4XX", typeof BetaMessageBatchesList4XX.Type> + > + /** + * Send a batch of Message creation requests. + * + * The Message Batches API can be used to process multiple Messages API requests at once. Once a Message Batch is created, it begins processing immediately. Batches can take up to 24 hours to complete. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesPost": ( + options: { + readonly params?: typeof BetaMessageBatchesPostParams.Encoded | undefined + readonly payload: typeof BetaMessageBatchesPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesPost4XX", typeof BetaMessageBatchesPost4XX.Type> + > + /** + * This endpoint is idempotent and can be used to poll for Message Batch completion. To access the results of a Message Batch, make a request to the `results_url` field in the response. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesRetrieve": ( + messageBatchId: string, + options: { + readonly params?: typeof BetaMessageBatchesRetrieveParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesRetrieve4XX", typeof BetaMessageBatchesRetrieve4XX.Type> + > + /** + * Delete a Message Batch. + * + * Message Batches can only be deleted once they've finished processing. If you'd like to delete an in-progress batch, you must first cancel it. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesDelete": ( + messageBatchId: string, + options: { + readonly params?: typeof BetaMessageBatchesDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesDelete4XX", typeof BetaMessageBatchesDelete4XX.Type> + > + /** + * Batches may be canceled any time before processing ends. Once cancellation is initiated, the batch enters a `canceling` state, at which time the system may complete any in-progress, non-interruptible requests before finalizing cancellation. + * + * The number of canceled requests is specified in `request_counts`. To determine which requests were canceled, check the individual results within the batch. Note that cancellation may not result in any canceled requests if they were non-interruptible. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesCancel": ( + messageBatchId: string, + options: { + readonly params?: typeof BetaMessageBatchesCancelParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesCancel4XX", typeof BetaMessageBatchesCancel4XX.Type> + > + /** + * Streams the results of a Message Batch as a `.jsonl` file. + * + * Each line in the file is a JSON object containing the result of a single request in the Message Batch. Results are not guaranteed to be in the same order as requests. Use the `custom_id` field to match results to requests. + * + * Learn more about the Message Batches API in our [user guide](https://docs.claude.com/en/docs/build-with-claude/batch-processing) + */ + readonly "betaMessageBatchesResults": ( + messageBatchId: string, + options: { + readonly params?: typeof BetaMessageBatchesResultsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessageBatchesResults4XX", typeof BetaMessageBatchesResults4XX.Type> + > + /** + * Count the number of tokens in a Message. + * + * The Token Count API can be used to count the number of tokens in a Message, including tools, images, and documents, without creating it. + * + * Learn more about token counting in our [user guide](https://docs.claude.com/en/docs/build-with-claude/token-counting) + */ + readonly "betaMessagesCountTokensPost": ( + options: { + readonly params?: typeof BetaMessagesCountTokensPostParams.Encoded | undefined + readonly payload: typeof BetaMessagesCountTokensPostRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaMessagesCountTokensPost4XX", typeof BetaMessagesCountTokensPost4XX.Type> + > + /** + * List Files + */ + readonly "betaListFilesV1FilesGet": ( + options: { + readonly params?: typeof BetaListFilesV1FilesGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaListFilesV1FilesGet4XX", typeof BetaListFilesV1FilesGet4XX.Type> + > + /** + * Upload File + */ + readonly "betaUploadFileV1FilesPost": ( + options: { + readonly params?: typeof BetaUploadFileV1FilesPostParams.Encoded | undefined + readonly payload: typeof BetaUploadFileV1FilesPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaUploadFileV1FilesPost4XX", typeof BetaUploadFileV1FilesPost4XX.Type> + > + /** + * Get File Metadata + */ + readonly "betaGetFileMetadataV1FilesFileIdGet": ( + fileId: string, + options: { + readonly params?: typeof BetaGetFileMetadataV1FilesFileIdGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaGetFileMetadataV1FilesFileIdGet4XX", typeof BetaGetFileMetadataV1FilesFileIdGet4XX.Type> + > + /** + * Delete File + */ + readonly "betaDeleteFileV1FilesFileIdDelete": ( + fileId: string, + options: { + readonly params?: typeof BetaDeleteFileV1FilesFileIdDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaDeleteFileV1FilesFileIdDelete4XX", typeof BetaDeleteFileV1FilesFileIdDelete4XX.Type> + > + /** + * Download File + */ + readonly "betaDownloadFileV1FilesFileIdContentGet": ( + fileId: string, + options: { + readonly params?: typeof BetaDownloadFileV1FilesFileIdContentGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Download File + */ + readonly "betaDownloadFileV1FilesFileIdContentGetStream": ( + fileId: string, + options: { readonly params?: typeof BetaDownloadFileV1FilesFileIdContentGetParams.Encoded | undefined } | undefined + ) => Stream.Stream + /** + * List Skills + */ + readonly "betaListSkillsV1SkillsGet": ( + options: { + readonly params?: typeof BetaListSkillsV1SkillsGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaListSkillsV1SkillsGet4XX", typeof BetaListSkillsV1SkillsGet4XX.Type> + > + /** + * Create Skill + */ + readonly "betaCreateSkillV1SkillsPost": ( + options: { + readonly params?: typeof BetaCreateSkillV1SkillsPostParams.Encoded | undefined + readonly payload: typeof BetaCreateSkillV1SkillsPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaCreateSkillV1SkillsPost4XX", typeof BetaCreateSkillV1SkillsPost4XX.Type> + > + /** + * Get Skill + */ + readonly "betaGetSkillV1SkillsSkillIdGet": ( + skillId: string, + options: { + readonly params?: typeof BetaGetSkillV1SkillsSkillIdGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError<"BetaGetSkillV1SkillsSkillIdGet4XX", typeof BetaGetSkillV1SkillsSkillIdGet4XX.Type> + > + /** + * Delete Skill + */ + readonly "betaDeleteSkillV1SkillsSkillIdDelete": ( + skillId: string, + options: { + readonly params?: typeof BetaDeleteSkillV1SkillsSkillIdDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "BetaDeleteSkillV1SkillsSkillIdDelete4XX", + typeof BetaDeleteSkillV1SkillsSkillIdDelete4XX.Type + > + > + /** + * List Skill Versions + */ + readonly "betaListSkillVersionsV1SkillsSkillIdVersionsGet": ( + skillId: string, + options: { + readonly params?: typeof BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX", + typeof BetaListSkillVersionsV1SkillsSkillIdVersionsGet4XX.Type + > + > + /** + * Create Skill Version + */ + readonly "betaCreateSkillVersionV1SkillsSkillIdVersionsPost": ( + skillId: string, + options: { + readonly params?: typeof BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams.Encoded | undefined + readonly payload: typeof BetaCreateSkillVersionV1SkillsSkillIdVersionsPostRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX", + typeof BetaCreateSkillVersionV1SkillsSkillIdVersionsPost4XX.Type + > + > + /** + * Get Skill Version + */ + readonly "betaGetSkillVersionV1SkillsSkillIdVersionsVersionGet": ( + skillId: string, + version: string, + options: { + readonly params?: typeof BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX", + typeof BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGet4XX.Type + > + > + /** + * Delete Skill Version + */ + readonly "betaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete": ( + skillId: string, + version: string, + options: { + readonly params?: typeof BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | AnthropicClientError< + "BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX", + typeof BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDelete4XX.Type + > + > +} + +export interface AnthropicClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class AnthropicClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const AnthropicClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): AnthropicClientError => + new AnthropicClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/anthropic/src/index.ts b/repos/effect/packages/ai/anthropic/src/index.ts new file mode 100644 index 0000000..48af730 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/index.ts @@ -0,0 +1,40 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as AnthropicClient from "./AnthropicClient.ts" + +/** + * @since 4.0.0 + */ +export * as AnthropicConfig from "./AnthropicConfig.ts" + +/** + * @since 4.0.0 + */ +export * as AnthropicError from "./AnthropicError.ts" + +/** + * @since 4.0.0 + */ +export * as AnthropicLanguageModel from "./AnthropicLanguageModel.ts" + +/** + * @since 4.0.0 + */ +export * as AnthropicTelemetry from "./AnthropicTelemetry.ts" + +/** + * @since 4.0.0 + */ +export * as AnthropicTool from "./AnthropicTool.ts" + +/** + * @since 4.0.0 + */ +export * as Generated from "./Generated.ts" diff --git a/repos/effect/packages/ai/anthropic/src/internal/errors.ts b/repos/effect/packages/ai/anthropic/src/internal/errors.ts new file mode 100644 index 0000000..3745bcc --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/internal/errors.ts @@ -0,0 +1,370 @@ +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import type * as Response from "effect/unstable/ai/Response" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type { AnthropicErrorMetadata } from "../AnthropicError.ts" +import type * as Generated from "../Generated.ts" + +// ============================================================================= +// Anthropic Error Body Schema +// ============================================================================= + +/** @internal */ +export const AnthropicErrorBody = Schema.Struct({ + type: Schema.Literal("error"), + error: Schema.Struct({ + type: Schema.String, + message: Schema.String + }) +}) + +/** @internal */ +export type AnthropicClientErrorBody = { + readonly type: "error" + readonly error: { + readonly type: string + readonly message: string + } + readonly request_id: string | null +} + +// ============================================================================= +// Error Mappers +// ============================================================================= + +/** @internal */ +export const mapSchemaError = dual< + (method: string) => (error: Schema.SchemaError) => AiError.AiError, + (error: Schema.SchemaError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "AnthropicClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + })) + +/** @internal */ +export const mapClientError = dual< + (method: string) => (error: Generated.AnthropicClientError) => AiError.AiError, + (error: Generated.AnthropicClientError, method: string) => AiError.AiError +>(2, (error, method) => { + const { request, response, cause } = error + const status = response.status + const headers = response.headers as Record + const metadata: AnthropicErrorMetadata = { + errorType: cause.error.type, + requestId: cause.request_id + } + const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) + const reason = mapStatusCodeToReason({ + status, + headers, + message: cause.error.message, + metadata, + http + }) + return AiError.make({ module: "AnthropicClient", method, reason }) +}) + +/** @internal */ +export const mapHttpClientError = dual< + (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, + (error: HttpClientError.HttpClientError, method: string) => Effect.Effect +>(2, (error, method) => { + const reason = error.reason + switch (reason._tag) { + case "TransportError": { + return Effect.fail(AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.NetworkError({ + reason: "TransportError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "EncodeError": { + return Effect.fail(AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.NetworkError({ + reason: "EncodeError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "InvalidUrlError": { + return Effect.fail(AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.NetworkError({ + reason: "InvalidUrlError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "StatusCodeError": { + return mapStatusCodeError(reason, method) + } + case "DecodeError": { + return Effect.fail(AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Failed to decode response" + }) + })) + } + case "EmptyBodyError": { + return Effect.fail(AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Response body was empty" + }) + })) + } + } +}) + +/** @internal */ +const mapStatusCodeError = Effect.fnUntraced(function*( + error: HttpClientError.StatusCodeError, + method: string +) { + const { request, response, description } = error + const status = response.status + const headers = response.headers as Record + const requestId = headers["request-id"] + + let body: string | undefined = description + if (!description || !description.startsWith("{")) { + const responseBody = yield* Effect.option(response.text) + if (Option.isSome(responseBody) && responseBody.value) { + body = responseBody.value + } + } + + let json: unknown = undefined + // @effect-diagnostics effect/tryCatchInEffectGen:off + try { + json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined + } catch { + json = undefined + } + const decoded = Schema.decodeUnknownOption(AnthropicErrorBody)(json) + + const reason = mapStatusCodeToReason({ + status, + headers, + message: Option.isSome(decoded) ? decoded.value.error.message : undefined, + http: buildHttpContext({ request, response, body }), + metadata: { + errorType: Option.isSome(decoded) ? decoded.value.error.type : null, + requestId: requestId ?? null + } + }) + + return yield* AiError.make({ module: "AnthropicClient", method, reason }) +}) + +// ============================================================================= +// Rate Limits +// ============================================================================= + +/** @internal */ +export const parseRateLimitHeaders = (headers: Record) => { + const retryAfterRaw = headers["retry-after"] + let retryAfter: Duration.Duration | undefined + if (Predicate.isNotUndefined(retryAfterRaw)) { + const parsed = Number.parse(retryAfterRaw) + if (Option.isSome(parsed)) { + retryAfter = Duration.seconds(parsed.value) + } + } + const requestsLimitRaw = headers["anthropic-ratelimit-requests-limit"] + const requestsRemainingRaw = headers["anthropic-ratelimit-requests-remaining"] + const tokensLimitRaw = headers["anthropic-ratelimit-tokens-limit"] + const tokensRemainingRaw = headers["anthropic-ratelimit-tokens-remaining"] + return { + retryAfter, + requestsLimit: Predicate.isNotUndefined(requestsLimitRaw) ? Option.getOrNull(Number.parse(requestsLimitRaw)) : null, + requestsRemaining: Predicate.isNotUndefined(requestsRemainingRaw) + ? Option.getOrNull(Number.parse(requestsRemainingRaw)) + : null, + requestsReset: headers["anthropic-ratelimit-requests-reset"] ?? null, + tokensLimit: Predicate.isNotUndefined(tokensLimitRaw) ? Option.getOrNull(Number.parse(tokensLimitRaw)) : null, + tokensRemaining: Predicate.isNotUndefined(tokensRemainingRaw) + ? Option.getOrNull(Number.parse(tokensRemainingRaw)) + : null, + tokensReset: headers["anthropic-ratelimit-tokens-reset"] ?? null + } +} + +// ============================================================================= +// HTTP Context +// ============================================================================= + +/** @internal */ +export const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +/** @internal */ +export const buildHttpContext = (params: { + readonly request: HttpClientRequest.HttpClientRequest + readonly response?: HttpClientResponse.HttpClientResponse + readonly body?: string | undefined +}): typeof AiError.HttpContext.Type => ({ + request: buildHttpRequestDetails(params.request), + response: Predicate.isNotUndefined(params.response) + ? { + status: params.response.status, + headers: Redactable.redact(params.response.headers) as Record + } + : undefined, + body: params.body +}) + +// ============================================================================= +// HTTP Status Code +// ============================================================================= + +const buildInvalidRequestDescription = (params: { + readonly status: number + readonly message: string | undefined + readonly method: string + readonly url: string + readonly errorType: string | null + readonly requestId: string | null + readonly body: string | undefined +}): string => { + const parts: Array = [] + + if (params.message) { + parts.push(params.message) + } else { + parts.push(`HTTP ${params.status}`) + } + + parts.push(`(${params.method} ${params.url})`) + + if (params.errorType) { + parts.push(`[type: ${params.errorType}]`) + } + + if (params.requestId) { + parts.push(`[requestId: ${params.requestId}]`) + } + + if (!params.message && params.body) { + const truncated = params.body.length > 200 + ? params.body.slice(0, 200) + "..." + : params.body + parts.push(`Response: ${truncated}`) + } + + return parts.join(" ") +} + +/** @internal */ +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { + readonly status: number + readonly headers: Record + readonly message: string | undefined + readonly metadata: AnthropicErrorMetadata + readonly http: typeof AiError.HttpContext.Type +}): AiError.AiErrorReason => { + const invalidRequestDescription = buildInvalidRequestDescription({ + status, + message, + method: http.request.method, + url: http.request.url, + errorType: metadata.errorType, + requestId: metadata.requestId, + body: http.body + }) + + switch (status) { + case 400: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { anthropic: metadata }, + http + }) + case 401: + return new AiError.AuthenticationError({ + kind: "InvalidKey", + metadata: { anthropic: metadata }, + http + }) + case 403: + return new AiError.AuthenticationError({ + kind: "InsufficientPermissions", + metadata: { anthropic: metadata }, + http + }) + case 404: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { anthropic: metadata }, + http + }) + case 422: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { anthropic: metadata }, + http + }) + case 429: { + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + return new AiError.RateLimitError({ + retryAfter, + metadata: { + anthropic: { + ...metadata, + ...rateLimitMetadata + } + }, + http + }) + } + case 529: + return new AiError.InternalProviderError({ + description: message ?? "Anthropic API is overloaded", + metadata: { anthropic: metadata }, + http + }) + default: + if (status >= 500) { + return new AiError.InternalProviderError({ + description: message ?? "Server error", + metadata: { anthropic: metadata }, + http + }) + } + return new AiError.UnknownError({ + description: message, + metadata: { anthropic: metadata }, + http + }) + } +} diff --git a/repos/effect/packages/ai/anthropic/src/internal/utilities.ts b/repos/effect/packages/ai/anthropic/src/internal/utilities.ts new file mode 100644 index 0000000..320754c --- /dev/null +++ b/repos/effect/packages/ai/anthropic/src/internal/utilities.ts @@ -0,0 +1,26 @@ +import * as Predicate from "effect/Predicate" +import type * as Response from "effect/unstable/ai/Response" + +const finishReasonMap: Record = { + end_turn: "stop", + max_tokens: "length", + pause_turn: "pause", + refusal: "content-filter", + stop_sequence: "stop", + tool_use: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string, + isJsonResponse: boolean = false +): Response.FinishReason => { + const reason = finishReasonMap[finishReason] + if (Predicate.isUndefined(reason)) { + return "unknown" + } + if (isJsonResponse && reason === "tool-calls") { + return "stop" + } + return reason +} diff --git a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts new file mode 100644 index 0000000..f5d2848 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -0,0 +1,526 @@ +import { AnthropicClient, AnthropicLanguageModel, AnthropicTool } from "@effect/ai-anthropic" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Schema, Stream } from "effect" +import { AnthropicStructuredOutput, LanguageModel, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("AnthropicLanguageModel", () => { + describe("streamText", () => { + it.effect("decodes tool call params in content_block_stop", () => + Effect.gen(function*() { + const toolParams = { pattern: "*.ts" } + + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: "toolu_test_1", + name: "GlobTool", + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify(toolParams) + } + }, + { + type: "content_block_stop", + index: 0 + }, + { + type: "message_delta", + delta: { + stop_reason: "tool_use", + stop_sequence: null + }, + usage: { + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + input_tokens: null, + output_tokens: 5 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const GlobTool = Tool.make("GlobTool", { + description: "Search for files", + parameters: Schema.Struct({ pattern: Schema.String }), + success: Schema.String + }) + + const toolkit = Toolkit.make(GlobTool) + const toolkitLayer = toolkit.toLayer({ + GlobTool: () => Effect.succeed("found.ts") + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "find ts files", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.name, "GlobTool") + assert.deepStrictEqual(toolCall.params, toolParams) + })) + + // `Model` is an open enum in Anthropic's spec (`anyOf: [{ type: string }, ...consts]`), and it is + // $ref'd by response schemas. Responses must therefore decode for model ids that are newer than the + // generated literals, and the id must survive decoding unchanged. + it.effect("decodes responses for a model id that is not a known literal", () => + Effect.gen(function*() { + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-not-a-known-model-id", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" } + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "Hello" } + }, + { + type: "content_block_stop", + index: 0 + }, + { + type: "message_delta", + delta: { + stop_reason: "end_turn", + stop_sequence: null + }, + usage: { + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + input_tokens: null, + output_tokens: 5 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "say hello" + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-not-a-known-model-id")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const metadata = parts.find((part) => part.type === "response-metadata") + assert.isDefined(metadata) + if (metadata?.type !== "response-metadata") { + return + } + + assert.strictEqual(metadata.modelId, "claude-not-a-known-model-id") + + const text = parts.find((part) => part.type === "text-delta") + assert.isDefined(text) + if (text?.type !== "text-delta") { + return + } + + assert.strictEqual(text.delta, "Hello") + })) + }) + + describe("generateText", () => { + it.effect("encodes dynamic tools", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "Done" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + })) + }) + )) + ) + + const inputSchema = { + type: "object", + properties: { + query: { type: "string" }, + limit: { type: "number" } + }, + required: ["query"], + additionalProperties: false + } as const + + const DynamicTool = Tool.dynamic("DynamicTool", { + description: "A dynamic tool", + parameters: inputSchema + }) + + yield* LanguageModel.generateText({ + prompt: "Use the dynamic tool", + toolkit: Toolkit.make(DynamicTool), + disableToolCallResolution: true + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + const dynamicTool = body.tools.find((tool: any) => tool.name === "DynamicTool") + + assert.isDefined(dynamicTool) + if (dynamicTool === undefined) { + return + } + + assert.strictEqual(dynamicTool.description, "A dynamic tool") + assert.deepStrictEqual(dynamicTool.input_schema, inputSchema) + })) + }) + + describe("generateObject", () => { + // A model that supports native structured output requests it via `output_config.format` (json_schema) + // rather than falling back to a forced JSON tool. + const assertNativeStructuredOutput = (model: string) => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model, + content: [{ type: "text", text: JSON.stringify({ name: "John", age: 30 }) }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + })) + }) + )) + ) + + // Assert the request shape; the response outcome is irrelevant here. + yield* LanguageModel.generateObject({ + prompt: "Give me a person", + schema: Schema.Struct({ name: Schema.String, age: Schema.Number }) + }).pipe( + Effect.provide(AnthropicLanguageModel.model(model)), + Effect.provide(layer), + Effect.ignore + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.output_config?.format?.type, "json_schema") + }) + + it.effect("uses native json_schema output for claude-opus-4-6", () => + assertNativeStructuredOutput("claude-opus-4-6")) + + it.effect("uses native json_schema output for claude-sonnet-4-6", () => + assertNativeStructuredOutput("claude-sonnet-4-6")) + }) + + // The packaged `Memory_20250818` tool ships `customName: "AnthropicMemory"` / + // `providerName: "memory"`, and is a client-executed provider tool. These + // tests cover the round-trip that was broken on beta.98 (see #2615): + // - the provider wire name ("memory") must resolve to the toolkit's custom + // name ("AnthropicMemory"), otherwise `makeResponse` raises ToolNotFound + // - `view_range` uses `Schema.optionalKey`, otherwise the Anthropic codec + // rejects the tool schema with "Unsupported AST Undefined" + // - `create` must carry `file_text`, otherwise the file body is dropped + describe("Memory tool", () => { + const memoryResponse = (request: HttpClientRequest.HttpClientRequest, input: unknown) => + jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "tool_use", id: "toolu_mem_1", name: "memory", input }], + stop_reason: "tool_use", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + }) + + it.effect("resolves the provider wire name to the tool's custom name (view)", () => + Effect.gen(function*() { + let receivedParams: unknown = undefined + const toolkit = Toolkit.make(AnthropicTool.Memory_20250818({})) + const toolkitLayer = toolkit.toLayer({ + AnthropicMemory: (params) => { + receivedParams = params + return Effect.succeed("memory listing") + } + }) + + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => Effect.succeed(memoryResponse(request, { command: "view", path: "/memories" }))) + )) + ) + + const response = yield* LanguageModel.generateText({ + prompt: "check memory", + toolkit + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + // Handler was resolved under the custom name and received the decoded command + assert.deepStrictEqual(receivedParams, { command: "view", path: "/memories" }) + + const toolResult = response.toolResults[0] + assert.isDefined(toolResult) + assert.strictEqual(toolResult.name, "AnthropicMemory") + assert.strictEqual(toolResult.result, "memory listing") + })) + + it.effect("decodes the create command including file_text", () => + Effect.gen(function*() { + let receivedParams: unknown = undefined + const toolkit = Toolkit.make(AnthropicTool.Memory_20250818({})) + const toolkitLayer = toolkit.toLayer({ + AnthropicMemory: (params) => { + receivedParams = params + return Effect.succeed("File created successfully at: /memories/notes.txt") + } + }) + + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(memoryResponse(request, { + command: "create", + path: "/memories/notes.txt", + file_text: "hello world" + })) + ) + )) + ) + + const response = yield* LanguageModel.generateText({ + prompt: "save a note", + toolkit + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + assert.deepStrictEqual(receivedParams, { + command: "create", + path: "/memories/notes.txt", + file_text: "hello world" + }) + assert.strictEqual(response.toolResults[0]?.name, "AnthropicMemory") + })) + }) + + // Client-executed (`requiresHandler`) provider tools have their `parameters` + // decoded via `toCodecAnthropic` when the model calls them. Optional + // parameters must use `Schema.optionalKey` (not `Schema.optional`), otherwise + // the codec rejects the schema with "Unsupported AST Undefined" (see #2615). + describe("client provider tool parameters compile with the Anthropic codec", () => { + const displayArgs = { displayWidthPx: 800, displayHeightPx: 600 } + const clientTools: ReadonlyArray = [ + ["Bash_20241022", AnthropicTool.Bash_20241022({})], + ["Bash_20250124", AnthropicTool.Bash_20250124({})], + ["ComputerUse_20241022", AnthropicTool.ComputerUse_20241022(displayArgs)], + ["ComputerUse_20250124", AnthropicTool.ComputerUse_20250124(displayArgs)], + ["ComputerUse_20251124", AnthropicTool.ComputerUse_20251124(displayArgs)], + ["Memory_20250818", AnthropicTool.Memory_20250818({})], + ["TextEditor_20241022", AnthropicTool.TextEditor_20241022({})], + ["TextEditor_20250124", AnthropicTool.TextEditor_20250124({})], + ["TextEditor_20250429", AnthropicTool.TextEditor_20250429({})], + ["TextEditor_20250728", AnthropicTool.TextEditor_20250728({})] + ] + + for (const [name, tool] of clientTools) { + it(name, () => { + const codec = AnthropicStructuredOutput.toCodecAnthropic(tool.parametersSchema) + assert.isDefined(codec) + }) + } + }) +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const sseResponse = ( + request: HttpClientRequest.HttpClientRequest, + events: ReadonlyArray +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(toSseBody(events), { + status: 200, + headers: { + "content-type": "text/event-stream" + } + }) + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag !== "Uint8Array") { + return yield* Effect.die(new Error("Expected Uint8Array body")) + } + return JSON.parse(new TextDecoder().decode(body.body)) + }) + +const toSseBody = (events: ReadonlyArray): string => + events.map((event) => `event: message_stream\ndata: ${JSON.stringify(event)}\n\n`).join("") diff --git a/repos/effect/packages/ai/anthropic/tsconfig.json b/repos/effect/packages/ai/anthropic/tsconfig.json new file mode 100644 index 0000000..19a2f5d --- /dev/null +++ b/repos/effect/packages/ai/anthropic/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts b/repos/effect/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts new file mode 100644 index 0000000..79d86a5 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts @@ -0,0 +1,14 @@ +import { AnthropicLanguageModel } from "@effect/ai-anthropic" +import { describe, expect, it } from "tstyche" + +declare const acceptsKnownModel: (model: AnthropicLanguageModel.Model) => void + +describe("AnthropicLanguageModel", () => { + describe("Model", () => { + it("keeps the known model ids as literals, while the constructors still accept custom ids", () => { + expect(acceptsKnownModel).type.toBeCallableWith("claude-opus-4-8") + expect(acceptsKnownModel).type.not.toBeCallableWith("not-a-real-model") + expect(AnthropicLanguageModel.model).type.toBeCallableWith("not-a-real-model") + }) + }) +}) diff --git a/repos/effect/packages/ai/anthropic/vitest.config.ts b/repos/effect/packages/ai/anthropic/vitest.config.ts new file mode 100644 index 0000000..c8a52c1 --- /dev/null +++ b/repos/effect/packages/ai/anthropic/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/openai-compat/CHANGELOG.md b/repos/effect/packages/ai/openai-compat/CHANGELOG.md new file mode 100644 index 0000000..61ed36d --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/CHANGELOG.md @@ -0,0 +1,742 @@ +# @effect/ai-openai-compat + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- [#2608](https://github.com/Effect-TS/effect-smol/pull/2608) [`5f142a2`](https://github.com/Effect-TS/effect-smol/commit/5f142a2136e62274ec121273fecb5d248c19f159) Thanks @tim-smart! - Preserve autocomplete for known OpenAI-compatible model config properties while allowing provider-specific properties. + +- [#2576](https://github.com/Effect-TS/effect-smol/pull/2576) [`0c2ed5e`](https://github.com/Effect-TS/effect-smol/commit/0c2ed5ee0ef5a437c5e6a2de456a5ddf214c281f) Thanks @tim-smart! - Normalize empty assistant message content to an empty string for OpenAI-compatible providers that reject null content values. + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- [#2380](https://github.com/Effect-TS/effect-smol/pull/2380) [`b5f67a0`](https://github.com/Effect-TS/effect-smol/commit/b5f67a0ce9c110b98731ac485e5894df9da07e3a) Thanks @walln! - Surface reasoning tokens from OpenAI-compatible chat completion responses. + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- [#2338](https://github.com/Effect-TS/effect-smol/pull/2338) [`b12991f`](https://github.com/Effect-TS/effect-smol/commit/b12991f2346eda47fec72df5a89034394ad6c8fb) Thanks @boozedog! - Fix dropped streamed tool-call arguments when a provider sends `function.name: null` on continuation fragments. + + OpenAI-compatible providers such as Fireworks send the tool name only on the first streamed `tool_calls` fragment and `function.name: null` on the continuation fragments that carry the argument deltas. `ChatCompletionToolFunctionDelta.name` was `Schema.optionalKey(Schema.String)` (non-nullable), so chunk validation rejected every continuation and silently discarded its argument delta, leaving the assembled tool call with empty or partial params. `name` is now nullable (`Schema.NullOr(Schema.String)`). + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- [#2133](https://github.com/Effect-TS/effect-smol/pull/2133) [`5be0aaa`](https://github.com/Effect-TS/effect-smol/commit/5be0aaad694c9eb943a710eb4f896bc4c3fcae99) Thanks @Zelys-DFKH! - Fix `OpenAiLanguageModel` leaking library-only config fields (`fileIdPrefixes`, `strictJsonSchema`) into request body, causing OpenAI 400 errors. + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- [#1859](https://github.com/Effect-TS/effect-smol/pull/1859) [`a4809db`](https://github.com/Effect-TS/effect-smol/commit/a4809db80f65601d37ccafc13d773e55909c4e98) Thanks @lloydrichards! - add dynamic tooling for openai and openai-compact language models + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- [#1764](https://github.com/Effect-TS/effect-smol/pull/1764) [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f) Thanks @tim-smart! - Add unstable EmbeddingModel support across core and OpenAI providers. + - Add the unstable EmbeddingModel module API surface in `effect`, including service, request, response, and provider types. + - Implement the unstable EmbeddingModel runtime constructor in `effect`, with `RequestResolver` batching, `embed` / `embedMany` spans, provider error propagation, deterministic ordering, and empty-input `embedMany` fast-path behavior. + - Add and align EmbeddingModel behavior tests in `effect` for embedding usage, batching, ordering, and error handling. + - Add `OpenAiEmbeddingModel` in `@effect/ai-openai`, including model / make / layer constructors, config overrides, and provider output index validation with deterministic reordering. + - Add OpenAI-compatible EmbeddingModel provider support in `@effect/ai-openai-compat`, including config overrides, layer constructors, and output index validation. + +- [#1771](https://github.com/Effect-TS/effect-smol/pull/1771) [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e) Thanks @tim-smart! - Add `EmbeddingModel.ModelDimensions` and require dimensions in embedding provider `model` constructors. + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- [#1692](https://github.com/Effect-TS/effect-smol/pull/1692) [`00caf4f`](https://github.com/Effect-TS/effect-smol/commit/00caf4fbe209ff56426e9bcd51a205bdb84b978e) Thanks @tim-smart! - Preserve streamed OpenAI compat tool call ids and names across fragmented chat completion chunks. + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- [#1634](https://github.com/Effect-TS/effect-smol/pull/1634) [`621d3a1`](https://github.com/Effect-TS/effect-smol/commit/621d3a1248ef50e83c35d71333c69bf01bc33a11) Thanks @tim-smart! - Allow custom request properties in openai-compat model config and chat request types, and forward model-level custom properties to chat-completions payloads. + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- [#1623](https://github.com/Effect-TS/effect-smol/pull/1623) [`2cf00c1`](https://github.com/Effect-TS/effect-smol/commit/2cf00c184ccc994b7977f33996321923fa3534c4) Thanks @tim-smart! - Forward `OpenAiLanguageModel` `reasoning` config into chat-completions requests. + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- [#1529](https://github.com/Effect-TS/effect-smol/pull/1529) [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8) Thanks @tim-smart! - Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. + +- [#1528](https://github.com/Effect-TS/effect-smol/pull/1528) [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34) Thanks @tim-smart! - Add `Model.ModelName` and provide it from AI model constructors. + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- [#1502](https://github.com/Effect-TS/effect-smol/pull/1502) [`285b7e6`](https://github.com/Effect-TS/effect-smol/commit/285b7e667167566d5788367d5155b19c79f1bf22) Thanks @tim-smart! - allow undefined for ai config + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/ai/openai-compat/docgen.json b/repos/effect/packages/ai/openai-compat/docgen.json new file mode 100644 index 0000000..5b265ff --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/docgen.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openai-compat/src/", + "exclude": ["src/internal/**"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/ai/openai-compat/package.json b/repos/effect/packages/ai/openai-compat/package.json new file mode 100644 index 0000000..72d52ab --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/package.json @@ -0,0 +1,68 @@ +{ + "name": "@effect/ai-openai-compat", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "An OpenAI compat integration for Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/openai-compat" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "ai", + "openai", + "compat" + ], + "keywords": [ + "typescript", + "ai", + "openai", + "compat" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiClient.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiClient.ts new file mode 100644 index 0000000..c1fe575 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiClient.ts @@ -0,0 +1,1221 @@ +/** + * The `OpenAiClient` module provides an Effect service for OpenAI-compatible + * chat completions and embeddings APIs. It builds on the Effect HTTP client, + * adds authentication and OpenAI organization or project headers, and exposes + * typed helpers for non-streaming chat completions, streaming chat completions, + * and embedding requests. + * + * @since 4.0.0 + */ +import * as Array from "effect/Array" +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity, pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import type * as AiError from "effect/unstable/ai/AiError" +import * as Sse from "effect/unstable/encoding/Sse" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Errors from "./internal/errors.ts" +import { OpenAiConfig } from "./OpenAiConfig.ts" + +/** + * Effect service interface for OpenAI-compatible chat completions and embeddings. + * + * **Details** + * + * Exposes the configured HTTP client plus helpers for non-streaming chat + * completions, streaming chat completions, and embeddings. Transport and + * schema decoding failures are mapped to `AiError`. + * + * @category models + * @since 4.0.0 + */ +export interface Service { + readonly client: HttpClient.HttpClient + readonly createResponse: ( + options: CreateResponseRequestJson + ) => Effect.Effect< + [body: CreateResponse200, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > + readonly createResponseStream: ( + options: Omit + ) => Effect.Effect< + [ + response: HttpClientResponse.HttpClientResponse, + stream: Stream.Stream + ], + AiError.AiError + > + readonly createEmbedding: ( + options: CreateEmbeddingRequestJson + ) => Effect.Effect +} + +/** + * Service tag for the OpenAI-compatible chat completions and embeddings client. + * + * **When to use** + * + * Use when building effects that depend on the low-level OpenAI-compatible + * client through context rather than receiving the client as a value. + * + * **Details** + * + * The tagged service is the `Service` interface produced by `make` and provided + * by `layer` or `layerConfig`. + * + * @see {@link Service} for the operations provided by the service + * @see {@link make} for constructing the service from explicit options + * @see {@link layer} for providing the service from explicit options + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category services + * @since 4.0.0 + */ +export class OpenAiClient extends Context.Service()( + "@effect/ai-openai-compat/OpenAiClient" +) {} + +/** + * Configuration options used to construct an OpenAI-compatible client. + * + * @category options + * @since 4.0.0 + */ +export type Options = { + readonly apiKey?: Redacted.Redacted | undefined + readonly apiUrl?: string | undefined + readonly organizationId?: Redacted.Redacted | undefined + readonly projectId?: Redacted.Redacted | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +const RedactedOpenAiHeaders = { + OpenAiOrganization: "OpenAI-Organization", + OpenAiProject: "OpenAI-Project" +} + +/** + * Constructs an OpenAI-compatible client service from explicit options. + * + * **When to use** + * + * Use when you need the OpenAI-compatible client service value inside an effect. + * + * **Details** + * + * The returned service uses the current `HttpClient`, prepends `apiUrl` or + * `https://api.openai.com/v1`, adds authentication and OpenAI + * organization/project headers, accepts JSON responses, and applies + * `transformClient` when provided. + * + * **Gotchas** + * + * A scoped `OpenAiConfig.withClientTransform` is applied when request helpers + * run, after the `transformClient` option supplied to `make`. + * + * @see {@link layer} for providing this client from explicit options + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*(options: Options): Effect.fn.Return { + const baseClient = yield* HttpClient.HttpClient + + const httpClient = baseClient.pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.openai.com/v1"), + options.apiKey !== undefined + ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey)) + : identity, + options.organizationId !== undefined + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiOrganization, + Redacted.value(options.organizationId) + ) + : identity, + options.projectId !== undefined + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiProject, + Redacted.value(options.projectId) + ) + : identity, + HttpClientRequest.acceptJson + ) + ), + options.transformClient !== undefined + ? options.transformClient + : identity + ) + + const resolveHttpClient = Effect.map( + OpenAiConfig.getOrUndefined, + (config) => + config?.transformClient !== undefined + ? config.transformClient(httpClient) + : httpClient + ) + + const decodeResponse = HttpClientResponse.schemaBodyJson(ChatCompletionResponse) + + const createResponse = ( + payload: CreateResponseRequestJson + ): Effect.Effect< + [body: CreateResponse200, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > => + Effect.flatMap(resolveHttpClient, (client) => + pipe( + HttpClientRequest.post("/chat/completions"), + HttpClientRequest.bodyJsonUnsafe(payload), + HttpClient.filterStatusOk(client).execute, + Effect.flatMap((response) => + Effect.map(decodeResponse(response), ( + body + ): [CreateResponse200, HttpClientResponse.HttpClientResponse] => [ + body, + response + ]) + ), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) + }) + )) + + const buildResponseStream = ( + response: HttpClientResponse.HttpClientResponse + ): [ + HttpClientResponse.HttpClientResponse, + Stream.Stream + ] => { + const stream = response.stream.pipe( + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decode()), + Stream.flatMap((event) => { + const data = decodeChatCompletionSseData(event.data) + return Stream.fromIterable(data !== undefined ? [data] : []) + }), + Stream.takeUntil((event) => event === "[DONE]"), + Stream.catchTags({ + Retry: (error) => Stream.die(error), + HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")) + }) + ) as any + return [response, stream] + } + + const createResponseStream: Service["createResponseStream"] = (payload) => + Effect.flatMap(resolveHttpClient, (client) => + pipe( + HttpClientRequest.post("/chat/completions"), + HttpClientRequest.bodyJsonUnsafe({ + ...payload, + stream: true, + stream_options: { + include_usage: true + } + }), + HttpClient.filterStatusOk(client).execute, + Effect.map(buildResponseStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createResponseStream") + ) + )) + + const decodeEmbedding = HttpClientResponse.schemaBodyJson(CreateEmbeddingResponseSchema) + + const createEmbedding = ( + payload: CreateEmbeddingRequestJson + ): Effect.Effect => + Effect.flatMap(resolveHttpClient, (client) => + pipe( + HttpClientRequest.post("/embeddings"), + HttpClientRequest.bodyJsonUnsafe(payload), + HttpClient.filterStatusOk(client).execute, + Effect.flatMap(decodeEmbedding), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) + }) + )) + + return OpenAiClient.of({ + client: httpClient, + createResponse, + createResponseStream, + createEmbedding + }) + }, + Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) + ) +) + +/** + * Creates a layer that provides an OpenAI-compatible client from explicit options. + * + * **When to use** + * + * Use to install `OpenAiClient` in an application layer when the client options + * are already available as values rather than loaded from `Config`. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(OpenAiClient, make(options)) + +/** + * Creates a layer that loads OpenAI-compatible client settings from `Config` + * values before constructing the service. + * + * **When to use** + * + * Use when you need client settings for OpenAI-compatible APIs to be read from + * Effect `Config` values while providing `OpenAiClient` as a layer. + * + * **Details** + * + * Only config values supplied in `options` are loaded. Omitted fields are + * passed to `make` as `undefined`, and `transformClient` is forwarded as a + * plain option. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layer} for providing the client from already-resolved options + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = (options?: { + readonly apiKey?: Config.Config | undefined> | undefined + readonly apiUrl?: Config.Config | undefined + readonly organizationId?: Config.Config | undefined> | undefined + readonly projectId?: Config.Config | undefined> | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + OpenAiClient, + Effect.gen(function*() { + const apiKey = options?.apiKey !== undefined + ? yield* options.apiKey : + undefined + const apiUrl = options?.apiUrl !== undefined + ? yield* options.apiUrl : + undefined + const organizationId = options?.organizationId !== undefined + ? yield* options.organizationId + : undefined + const projectId = options?.projectId !== undefined + ? yield* options.projectId : + undefined + return yield* make({ + apiKey, + apiUrl, + organizationId, + projectId, + transformClient: options?.transformClient + }) + }) + ) + +type JsonObject = { readonly [x: string]: Schema.Json } + +/** + * Optional response fields that can be requested with the `include` parameter. + * + * @category response + * @since 4.0.0 + */ +export type IncludeEnum = + | "message.input_image.image_url" + | "reasoning.encrypted_content" + | "message.output_text.logprobs" + +/** + * Lifecycle status shared by message, reasoning, and tool-call items. + * + * @category models + * @since 4.0.0 + */ +export type MessageStatus = "in_progress" | "completed" | "incomplete" + +type InputTextContent = { + readonly type: "input_text" + readonly text: string +} + +type InputImageContent = { + readonly type: "input_image" + readonly image_url?: string | null | undefined + readonly file_id?: string | null | undefined + readonly detail?: "low" | "high" | "auto" | null | undefined +} + +type InputFileContent = { + readonly type: "input_file" + readonly file_id?: string | null | undefined + readonly filename?: string | undefined + readonly file_url?: string | undefined + readonly file_data?: string | undefined +} + +/** + * Content blocks accepted in input messages. + * + * @category request + * @since 4.0.0 + */ +export type InputContent = InputTextContent | InputImageContent | InputFileContent + +/** + * Text content block used for model-provided reasoning summaries. + * + * @category response + * @since 4.0.0 + */ +export type SummaryTextContent = { + readonly type: "summary_text" + readonly text: string +} + +type ReasoningTextContent = { + readonly type: "reasoning_text" + readonly text: string +} + +type RefusalContent = { + readonly type: "refusal" + readonly refusal: string +} + +type TextContent = { + readonly type: "text" + readonly text: string +} + +type ComputerScreenshotContent = { + readonly type: "computer_screenshot" + readonly image_url: string | null + readonly file_id: string | null +} + +type FileCitationAnnotation = { + readonly type: "file_citation" + readonly file_id: string + readonly index: number + readonly filename: string +} + +type UrlCitationAnnotation = { + readonly type: "url_citation" + readonly url: string + readonly start_index: number + readonly end_index: number + readonly title: string +} + +type ContainerFileCitationAnnotation = { + readonly type: "container_file_citation" + readonly container_id: string + readonly file_id: string + readonly start_index: number + readonly end_index: number + readonly filename: string +} + +type FilePathAnnotation = { + readonly type: "file_path" + readonly file_id: string + readonly index: number +} + +/** + * Citation and file-path annotations attached to output text content. + * + * @category response + * @since 4.0.0 + */ +export type Annotation = + | FileCitationAnnotation + | UrlCitationAnnotation + | ContainerFileCitationAnnotation + | FilePathAnnotation + +type OutputTextContent = { + readonly type: "output_text" + readonly text: string + readonly annotations?: ReadonlyArray | undefined + readonly logprobs?: ReadonlyArray | undefined +} + +type OutputMessageContent = + | InputTextContent + | OutputTextContent + | TextContent + | SummaryTextContent + | ReasoningTextContent + | RefusalContent + | InputImageContent + | ComputerScreenshotContent + | InputFileContent + +type OutputMessage = { + readonly id: string + readonly type: "message" + readonly role: "assistant" + readonly content: ReadonlyArray + readonly status: MessageStatus +} + +/** + * Reasoning output item containing encrypted reasoning content, summaries, and + * optional reasoning text. + * + * @category response + * @since 4.0.0 + */ +export type ReasoningItem = { + readonly type: "reasoning" + readonly id: string + readonly encrypted_content?: string | null | undefined + readonly summary: ReadonlyArray + readonly content?: ReadonlyArray | undefined + readonly status?: MessageStatus | undefined +} + +type FunctionCall = { + readonly id?: string | undefined + readonly type: "function_call" + readonly call_id: string + readonly name: string + readonly arguments: string + readonly status?: MessageStatus | undefined +} + +type FunctionCallOutput = { + readonly id?: string | null | undefined + readonly call_id: string + readonly type: "function_call_output" + readonly output: string | ReadonlyArray + readonly status?: MessageStatus | null | undefined +} + +type CustomToolCall = { + readonly type: "custom_tool_call" + readonly id?: string | undefined + readonly call_id: string + readonly name: string + readonly input: string +} + +type CustomToolCallOutput = { + readonly type: "custom_tool_call_output" + readonly id?: string | undefined + readonly call_id: string + readonly output: string | ReadonlyArray +} + +type ItemReference = { + readonly type?: "item_reference" | null | undefined + readonly id: string +} + +/** + * Item shapes accepted by a Responses-style `input` field. + * + * **Details** + * + * Supports input messages, output messages, tool calls, tool outputs, reasoning + * items, custom tool interactions, and item references. + * + * @category request + * @since 4.0.0 + */ +export type InputItem = + | { + readonly role: "user" | "assistant" | "system" | "developer" + readonly content: string | ReadonlyArray + readonly type?: "message" | undefined + } + | { + readonly type?: "message" | undefined + readonly role: "user" | "system" | "developer" + readonly status?: MessageStatus | undefined + readonly content: ReadonlyArray + } + | OutputMessage + | FunctionCall + | FunctionCallOutput + | ReasoningItem + | CustomToolCallOutput + | CustomToolCall + | ItemReference + +type FunctionTool = { + readonly type: "function" + readonly name: string + readonly description?: string | null | undefined + readonly parameters?: JsonObject | null | undefined + readonly strict?: boolean | null | undefined +} + +type CustomToolParam = { + readonly type: "custom" + readonly name: string + readonly description?: string | undefined + readonly format?: unknown +} + +/** + * Tool definitions that can be supplied to a Responses-style request. + * + * @category request + * @since 4.0.0 + */ +export type Tool = + | FunctionTool + | CustomToolParam + +type ToolChoice = + | "none" + | "auto" + | "required" + | { + readonly type: "allowed_tools" + readonly mode: "auto" | "required" + readonly tools: ReadonlyArray + } + | { + readonly type: "function" + readonly name: string + } + | { + readonly type: "custom" + readonly name: string + } + +/** + * Text output format configuration for plain text, JSON object, or JSON Schema + * responses. + * + * @category configuration + * @since 4.0.0 + */ +export type TextResponseFormatConfiguration = + | { + readonly type: "text" + } + | { + readonly type: "json_schema" + readonly description?: string | undefined + readonly name: string + readonly schema: JsonObject + readonly strict?: boolean | null | undefined + } + | { + readonly type: "json_object" + } + +/** + * Request options for creating a Responses-style response with an + * OpenAI-compatible provider. + * + * @category request + * @since 4.0.0 + */ +export type CreateResponse = { + readonly metadata?: Readonly> | null | undefined + readonly top_logprobs?: number | undefined + readonly temperature?: number | null | undefined + readonly top_p?: number | null | undefined + readonly user?: string | null | undefined + readonly safety_identifier?: string | null | undefined + readonly prompt_cache_key?: string | null | undefined + readonly service_tier?: string | undefined + readonly prompt_cache_retention?: "in-memory" | "24h" | null | undefined + readonly previous_response_id?: string | null | undefined + readonly model?: string | undefined + readonly reasoning?: unknown + readonly background?: boolean | null | undefined + readonly max_output_tokens?: number | null | undefined + readonly max_tool_calls?: number | null | undefined + readonly text?: { + readonly format?: TextResponseFormatConfiguration | undefined + readonly verbosity?: "low" | "medium" | "high" | null | undefined + } | undefined + readonly tools?: ReadonlyArray | undefined + readonly tool_choice?: ToolChoice | undefined + readonly truncation?: "auto" | "disabled" | null | undefined + readonly input?: string | ReadonlyArray | undefined + readonly include?: ReadonlyArray | null | undefined + readonly parallel_tool_calls?: boolean | null | undefined + readonly store?: boolean | null | undefined + readonly instructions?: string | null | undefined + readonly stream?: boolean | null | undefined + readonly conversation?: string | null | undefined + readonly modalities?: ReadonlyArray<"text" | "audio"> | undefined + readonly seed?: number | undefined +} + +/** + * Token accounting reported on Responses-style response objects. + * + * @category response + * @since 4.0.0 + */ +export type ResponseUsage = { + readonly input_tokens: number + readonly output_tokens: number + readonly total_tokens: number + readonly input_tokens_details?: unknown + readonly output_tokens_details?: unknown +} + +type OutputItem = + | OutputMessage + | FunctionCall + | ReasoningItem + | CustomToolCall + +/** + * Responses-style response object returned by compatible providers or embedded + * in response stream lifecycle events. + * + * @category response + * @since 4.0.0 + */ +export type Response = { + readonly id: string + readonly object?: "response" | undefined + readonly model: string + readonly status?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" | undefined + readonly created_at: number + readonly output: ReadonlyArray + readonly usage?: ResponseUsage | null | undefined + readonly incomplete_details?: + | { + readonly reason?: "max_output_tokens" | "content_filter" | undefined + } + | null + | undefined + readonly service_tier?: string | undefined +} + +type ResponseCreatedEvent = { + readonly type: "response.created" + readonly response: Response + readonly sequence_number: number +} + +type ResponseCompletedEvent = { + readonly type: "response.completed" + readonly response: Response + readonly sequence_number: number +} + +type ResponseIncompleteEvent = { + readonly type: "response.incomplete" + readonly response: Response + readonly sequence_number: number +} + +type ResponseFailedEvent = { + readonly type: "response.failed" + readonly response: Response + readonly sequence_number: number +} + +type ResponseOutputItemAddedEvent = { + readonly type: "response.output_item.added" + readonly output_index: number + readonly sequence_number: number + readonly item: OutputItem +} + +type ResponseOutputItemDoneEvent = { + readonly type: "response.output_item.done" + readonly output_index: number + readonly sequence_number: number + readonly item: OutputItem +} + +type ResponseTextDeltaEvent = { + readonly type: "response.output_text.delta" + readonly item_id: string + readonly output_index: number + readonly content_index: number + readonly delta: string + readonly sequence_number: number + readonly logprobs?: ReadonlyArray | undefined +} + +type ResponseOutputTextAnnotationAddedEvent = { + readonly type: "response.output_text.annotation.added" + readonly item_id: string + readonly output_index: number + readonly content_index: number + readonly annotation_index: number + readonly sequence_number: number + readonly annotation: Annotation +} + +type ResponseFunctionCallArgumentsDeltaEvent = { + readonly type: "response.function_call_arguments.delta" + readonly item_id: string + readonly output_index: number + readonly sequence_number: number + readonly delta: string +} + +type ResponseReasoningSummaryPartAddedEvent = { + readonly type: "response.reasoning_summary_part.added" + readonly item_id: string + readonly output_index: number + readonly summary_index: number + readonly sequence_number: number + readonly part: SummaryTextContent +} + +type ResponseReasoningSummaryPartDoneEvent = { + readonly type: "response.reasoning_summary_part.done" + readonly item_id: string + readonly output_index: number + readonly summary_index: number + readonly sequence_number: number + readonly part: SummaryTextContent +} + +type ResponseReasoningSummaryTextDeltaEvent = { + readonly type: "response.reasoning_summary_text.delta" + readonly item_id: string + readonly output_index: number + readonly summary_index: number + readonly delta: string + readonly sequence_number: number +} + +type ResponseErrorEvent = { + readonly type: "error" + readonly code: string | null + readonly message: string + readonly param: string | null + readonly sequence_number: number +} + +type UnknownResponseStreamEvent = { + readonly type: string + readonly [key: string]: unknown +} + +/** + * Server-sent event shapes emitted by Responses-style response streams. + * + * @category streaming + * @since 4.0.0 + */ +export type ResponseStreamEvent = + | ResponseCreatedEvent + | ResponseCompletedEvent + | ResponseIncompleteEvent + | ResponseFailedEvent + | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent + | ResponseTextDeltaEvent + | ResponseOutputTextAnnotationAddedEvent + | ResponseFunctionCallArgumentsDeltaEvent + | ResponseReasoningSummaryPartAddedEvent + | ResponseReasoningSummaryPartDoneEvent + | ResponseReasoningSummaryTextDeltaEvent + | ResponseErrorEvent + | UnknownResponseStreamEvent + +/** + * Represents one embedding item returned by an OpenAI-compatible embeddings API. + * + * **Details** + * + * The embedding can be returned either as a numeric vector or as a base64-encoded + * string. The `index` field identifies the input item that produced this + * embedding. + * + * @category response + * @since 4.0.0 + */ +export type Embedding = { + readonly embedding: ReadonlyArray | string + readonly index: number + readonly object?: string | undefined +} + +/** + * Request payload for the embeddings endpoint. + * + * @category request + * @since 4.0.0 + */ +export type CreateEmbeddingRequest = { + readonly input: string | ReadonlyArray | ReadonlyArray | ReadonlyArray> + readonly model: string + readonly encoding_format?: "float" | "base64" | undefined + readonly dimensions?: number | undefined + readonly user?: string | undefined +} + +/** + * Successful response payload returned by the embeddings endpoint. + * + * @category response + * @since 4.0.0 + */ +export type CreateEmbeddingResponse = { + readonly data: ReadonlyArray + readonly model: string + readonly object?: "list" | undefined + readonly usage?: { + readonly prompt_tokens: number + readonly total_tokens: number + } | undefined +} + +/** + * JSON request body accepted by the embeddings endpoint. + * + * @category request + * @since 4.0.0 + */ +export type CreateEmbeddingRequestJson = CreateEmbeddingRequest +/** + * Decoded successful embeddings response body. + * + * @category response + * @since 4.0.0 + */ +export type CreateEmbedding200 = CreateEmbeddingResponse +/** + * Structured content parts accepted in chat completion messages. + * + * @category request + * @since 4.0.0 + */ +export type ChatCompletionContentPart = + | { + readonly type: "text" + readonly text: string + } + | { + readonly type: "image_url" + readonly image_url: { + readonly url: string + readonly detail?: "low" | "high" | "auto" | undefined + } + } +/** + * Tool call data attached to an assistant chat completion message. + * + * @category request + * @since 4.0.0 + */ +export type ChatCompletionRequestToolCall = { + readonly id: string + readonly type: "function" + readonly function: { + readonly name: string + readonly arguments: string + } +} +/** + * Message shapes accepted by the chat completions endpoint. + * + * @category request + * @since 4.0.0 + */ +export type ChatCompletionRequestMessage = + | { + readonly role: "system" | "developer" | "user" | "assistant" + readonly content: string | ReadonlyArray | null + readonly tool_calls?: ReadonlyArray | undefined + } + | { + readonly role: "tool" + readonly tool_call_id: string + readonly content: string + } +/** + * Function tool definition accepted by the chat completions endpoint. + * + * @category request + * @since 4.0.0 + */ +export type ChatCompletionTool = { + readonly type: "function" + readonly function: { + readonly name: string + readonly description?: string | null | undefined + readonly parameters?: JsonObject | undefined + readonly strict?: boolean | undefined + } +} +/** + * Controls whether the model may call tools and can force a specific function. + * + * @category configuration + * @since 4.0.0 + */ +export type ChatCompletionToolChoice = + | "none" + | "auto" + | "required" + | { + readonly type: "function" + readonly function: { + readonly name: string + } + } +/** + * JSON response format configuration for chat completion requests. + * + * @category configuration + * @since 4.0.0 + */ +export type ChatCompletionResponseFormat = + | { + readonly type: "json_object" + } + | { + readonly type: "json_schema" + readonly json_schema: { + readonly name: string + readonly schema: JsonObject + readonly description?: string | undefined + readonly strict?: boolean | undefined + } + } +/** + * Request payload for the OpenAI-compatible chat completions endpoint. + * + * @category request + * @since 4.0.0 + */ +export type ChatCompletionRequest = { + readonly model: string + readonly messages: ReadonlyArray + readonly temperature?: number | null | undefined + readonly top_p?: number | null | undefined + readonly max_tokens?: number | null | undefined + readonly user?: string | null | undefined + readonly seed?: number | undefined + readonly parallel_tool_calls?: boolean | null | undefined + readonly response_format?: ChatCompletionResponseFormat | undefined + readonly tools?: ReadonlyArray | undefined + readonly tool_choice?: ChatCompletionToolChoice | undefined + readonly service_tier?: string | undefined + readonly reasoning?: unknown + readonly stream?: boolean | undefined + readonly stream_options?: { + readonly include_usage?: boolean | undefined + } | undefined + readonly [x: string]: unknown +} +/** + * JSON request body used by this client when creating a chat completion response. + * + * @category request + * @since 4.0.0 + */ +export type CreateResponseRequestJson = ChatCompletionRequest +/** + * Decoded successful chat completion response body returned by `createResponse`. + * + * @category response + * @since 4.0.0 + */ +export type CreateResponse200 = ChatCompletionResponse +/** + * Decoded server-sent event payload emitted by `createResponseStream`. + * + * @category streaming + * @since 4.0.0 + */ +export type CreateResponse200Sse = ChatCompletionStreamEvent + +const EmbeddingSchema = Schema.Struct({ + embedding: Schema.Union([Schema.Array(Schema.Number), Schema.String]), + index: Schema.Number, + object: Schema.optionalKey(Schema.String) +}) + +const CreateEmbeddingResponseSchema = Schema.Struct({ + data: Schema.Array(EmbeddingSchema), + model: Schema.String, + object: Schema.optionalKey(Schema.Literal("list")), + usage: Schema.optionalKey(Schema.Struct({ + prompt_tokens: Schema.Number, + total_tokens: Schema.Number + })) +}) + +const ChatCompletionToolFunction = Schema.Struct({ + name: Schema.String, + arguments: Schema.optionalKey(Schema.String) +}) + +const ChatCompletionToolFunctionDelta = Schema.Struct({ + // Some OpenAI-compatible providers (e.g. Fireworks) send `name: null` on + // streamed tool-call continuation fragments. `name` must be nullable, else + // the whole chunk fails validation and its argument delta is dropped. + name: Schema.optionalKey(Schema.NullOr(Schema.String)), + arguments: Schema.optionalKey(Schema.String) +}) + +const ChatCompletionToolCall = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + index: Schema.optionalKey(Schema.Number), + type: Schema.optionalKey(Schema.String), + function: Schema.optionalKey(ChatCompletionToolFunction) +}) + +const ChatCompletionToolCallDelta = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + index: Schema.optionalKey(Schema.Number), + type: Schema.optionalKey(Schema.String), + function: Schema.optionalKey(ChatCompletionToolFunctionDelta) +}) + +const ChatCompletionMessage = Schema.Struct({ + role: Schema.optionalKey(Schema.String), + content: Schema.optionalKey(Schema.NullOr(Schema.String)), + reasoning: Schema.optionalKey(Schema.NullOr(Schema.String)), + reasoning_content: Schema.optionalKey(Schema.NullOr(Schema.String)), + tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCall)) +}) + +const ChatCompletionDelta = Schema.Struct({ + role: Schema.optionalKey(Schema.String), + content: Schema.optionalKey(Schema.NullOr(Schema.String)), + reasoning: Schema.optionalKey(Schema.NullOr(Schema.String)), + reasoning_content: Schema.optionalKey(Schema.NullOr(Schema.String)), + tool_calls: Schema.optionalKey(Schema.Array(ChatCompletionToolCallDelta)) +}) + +const ChatCompletionChoice = Schema.Struct({ + index: Schema.Number, + finish_reason: Schema.optionalKey(Schema.NullOr(Schema.String)), + message: Schema.optionalKey(ChatCompletionMessage), + delta: Schema.optionalKey(ChatCompletionDelta) +}) + +const ChatCompletionUsage = Schema.Struct({ + prompt_tokens: Schema.Number, + completion_tokens: Schema.Number, + total_tokens: Schema.Number, + prompt_tokens_details: Schema.optionalKey(Schema.Any), + completion_tokens_details: Schema.optionalKey(Schema.Any) +}) + +const ChatCompletionResponse = Schema.Struct({ + id: Schema.String, + model: Schema.String, + created: Schema.Number, + choices: Schema.Array(ChatCompletionChoice), + usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)), + service_tier: Schema.optionalKey(Schema.String) +}) + +const ChatCompletionChunk = Schema.Struct({ + id: Schema.String, + model: Schema.String, + created: Schema.Number, + choices: Schema.Array(ChatCompletionChoice), + usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)), + service_tier: Schema.optionalKey(Schema.String) +}) + +/** + * Decoded tool-call object from a chat completion response or streaming chunk. + * + * @category response + * @since 4.0.0 + */ +export type ChatCompletionToolCall = typeof ChatCompletionToolCall.Type +/** + * Decoded message object from a non-streaming chat completion choice. + * + * @category response + * @since 4.0.0 + */ +export type ChatCompletionMessage = typeof ChatCompletionMessage.Type +/** + * Decoded choice object returned by chat completion responses and chunks. + * + * @category response + * @since 4.0.0 + */ +export type ChatCompletionChoice = typeof ChatCompletionChoice.Type +/** + * Decoded token usage summary returned by chat completions. + * + * @category response + * @since 4.0.0 + */ +export type ChatCompletionUsage = typeof ChatCompletionUsage.Type +/** + * Decoded successful response from the chat completions endpoint. + * + * @category response + * @since 4.0.0 + */ +export type ChatCompletionResponse = typeof ChatCompletionResponse.Type +/** + * Decoded streaming chunk emitted by the chat completions endpoint. + * + * @category streaming + * @since 4.0.0 + */ +export type ChatCompletionChunk = typeof ChatCompletionChunk.Type +/** + * Streaming chat completion event, including decoded chunks and the `[DONE]` + * sentinel. + * + * @category streaming + * @since 4.0.0 + */ +export type ChatCompletionStreamEvent = ChatCompletionChunk | "[DONE]" + +const parseJson = (value: string): unknown => { + try { + return JSON.parse(value) + } catch { + return undefined + } +} + +const isChatCompletionChunk = Schema.is(ChatCompletionChunk) + +const decodeChatCompletionSseData = ( + data: string +): ChatCompletionStreamEvent | undefined => { + if (data === "[DONE]") { + return data + } + const parsed = parseJson(data) + return isChatCompletionChunk(parsed) + ? parsed + : undefined +} diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiConfig.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiConfig.ts new file mode 100644 index 0000000..452144a --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiConfig.ts @@ -0,0 +1,87 @@ +/** + * The `OpenAiConfig` module lets a workflow temporarily customize the HTTP + * client used by OpenAI-compatible request helpers. Model, embedding, and + * tool-calling code can use this scoped configuration to add middleware, + * instrumentation, or routing without rebuilding the client layer. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * Context service for OpenAI-compatible client configuration in the current + * Effect scope. + * + * **When to use** + * + * Use as the context service for scoped OpenAI-compatible client configuration + * and HTTP client transforms. + * + * @see {@link withClientTransform} for scoping an HTTP client transformation + * + * @category services + * @since 4.0.0 + */ +export class OpenAiConfig extends Context.Service< + OpenAiConfig, + OpenAiConfig.Service +>()("@effect/ai-openai-compat/OpenAiConfig") { + /** + * Gets the configured OpenAI-compatible service from the current context when present. + * + * @since 4.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.mapUnsafe.get(OpenAiConfig.key) + ) +} + +/** + * Types associated with the `OpenAiConfig` context service. + * + * @since 4.0.0 + */ +export declare namespace OpenAiConfig { + /** + * Configuration consumed by OpenAI-compatible clients when they build or + * resolve the underlying HTTP client. + * + * @category models + * @since 4.0.0 + */ + export interface Service { + readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined + } +} + +/** + * Provides an HTTP client transform for the supplied effect. + * + * **When to use** + * + * Use to add provider-specific OpenAI-compatible HTTP behavior, such as + * headers, retries, instrumentation, or proxy routing. + * + * **Details** + * + * OpenAI-compatible provider services read the transform from the + * `OpenAiConfig` context. + * + * @category configuration + * @since 4.0.0 + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + transformClient: (client: HttpClient) => HttpClient +) => + Effect.flatMap( + OpenAiConfig.getOrUndefined, + (config) => Effect.provideService(self, OpenAiConfig, { ...config, transformClient }) + )) diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts new file mode 100644 index 0000000..ca6de8d --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts @@ -0,0 +1,244 @@ +/** + * The `OpenAiEmbeddingModel` module adapts OpenAI-compatible embeddings + * endpoints to Effect's embedding model service. It sends embedding requests + * through {@link OpenAiClient}, exposes constructors for layers and `AiModel` + * values, supports scoped request configuration overrides, and checks that the + * provider returns one numeric vector for each requested input. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import type { Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel" +import * as AiModel from "effect/unstable/ai/Model" +import type { CreateEmbedding200, CreateEmbeddingRequestJson } from "./OpenAiClient.ts" +import { OpenAiClient } from "./OpenAiClient.ts" + +/** + * A model identifier accepted by an OpenAI-compatible embeddings endpoint. + * + * @category models + * @since 4.0.0 + */ +export type Model = string + +type ConfigOptions = Simplify>> +type ModelConfig = Omit & { readonly [x: string]: unknown } + +/** + * Context service for OpenAI embedding model configuration. + * + * **When to use** + * + * Use when you need to provide shared default request options for + * OpenAI-compatible embedding operations through the Effect context, such as + * `dimensions`, `encoding_format`, or `user`. + * + * **Details** + * + * The service stores the embedding request payload without `input`. Requests + * combine the selected model, layer or constructor config, and scoped context + * config, with scoped context config taking precedence. + * + * @see {@link withConfigOverride} for scoping embedding request overrides + * + * @category context + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + ConfigOptions & { readonly [x: string]: unknown } +>()("@effect/ai-openai-compat/OpenAiEmbeddingModel/Config") {} + +/** + * Creates an `AiModel` for an OpenAI-compatible embedding model with its configured vector dimensions. + * + * **When to use** + * + * Use to provide an OpenAI-compatible `EmbeddingModel` and its `Dimensions` + * service to an Effect program. + * + * @see {@link layer} for providing only the embedding model service + * @see {@link withConfigOverride} for scoped request configuration overrides + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: string, + options: Omit & { + readonly dimensions: number + readonly [x: string]: unknown + } +): AiModel.Model<"openai", EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient> => + AiModel.make( + "openai", + model, + Layer.merge( + layer({ + model, + config: options + }), + Layer.succeed(EmbeddingModel.Dimensions, options.dimensions) + ) + ) + +/** + * Creates an OpenAI-compatible embedding model service backed by `OpenAiClient`. + * + * **When to use** + * + * Use when you need to build or provide an `EmbeddingModel` service directly + * from an existing `OpenAiClient`. + * + * **Details** + * + * The service sends embedding requests through `OpenAiClient.createEmbedding`. + * Request config is merged as the selected model, constructor config, then + * scoped `Config`, so scoped overrides take precedence. Provider usage + * `prompt_tokens` is exposed as `usage.inputTokens`. + * + * **Gotchas** + * + * Provider responses must contain one numeric vector for every requested input + * with unique, in-range `index` values; otherwise embedding operations fail with + * `AiError.InvalidOutputError`. + * + * @see {@link model} for the higher-level `AiModel` descriptor that also provides `EmbeddingModel.Dimensions` + * @see {@link layer} for providing the service as a `Layer` + * @see {@link withConfigOverride} for scoping embedding request overrides + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: string + readonly config?: ModelConfig | undefined +}): Effect.fn.Return { + const client = yield* OpenAiClient + + const makeConfig = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + return yield* EmbeddingModel.make({ + embedMany: Effect.fnUntraced(function*({ inputs }) { + const config = yield* makeConfig + const response = yield* client.createEmbedding({ ...config, input: inputs }) + return yield* mapProviderResponse(inputs.length, response) + }) + }) +}) + +/** + * Creates a layer for an OpenAI-compatible embedding model service. + * + * **When to use** + * + * Use when composing application layers and you want an OpenAI-compatible + * embeddings endpoint to satisfy `EmbeddingModel.EmbeddingModel` while + * supplying `OpenAiClient` from another layer. + * + * @see {@link make} for constructing the embedding model service effectfully + * @see {@link model} for creating an `AiModel` with configured dimensions + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: string + readonly config?: ModelConfig | undefined +}): Layer.Layer => + Layer.effect(EmbeddingModel.EmbeddingModel, make(options)) + +/** + * Provides scoped request config overrides for OpenAI-compatible embedding model operations. + * + * **When to use** + * + * Use to apply embedding request options to one effect without changing the + * model's default configuration. + * + * **Details** + * + * The overrides are merged with any existing `Config` service for the duration + * of the supplied effect. Fields in `overrides` take precedence over existing + * config, and the helper supports both `effect.pipe(withConfigOverride(overrides))` + * and `withConfigOverride(effect, overrides)`. + * + * @see {@link Config} for available OpenAI-compatible embedding request configuration fields + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +const mapProviderResponse = ( + inputLength: number, + response: CreateEmbedding200 +): Effect.Effect => { + if (response.data.length !== inputLength) { + return Effect.fail( + invalidOutput(`Provider returned ${response.data.length} embeddings but expected ${inputLength}`) + ) + } + + const results = new Array>(inputLength) + const seen = new Set() + + for (const entry of response.data) { + if (!Number.isInteger(entry.index) || entry.index < 0 || entry.index >= inputLength) { + return Effect.fail(invalidOutput(`Provider returned invalid embedding index: ${entry.index}`)) + } + if (seen.has(entry.index)) { + return Effect.fail(invalidOutput(`Provider returned duplicate embedding index: ${entry.index}`)) + } + if (!Array.isArray(entry.embedding)) { + return Effect.fail(invalidOutput(`Provider returned non-vector embedding at index ${entry.index}`)) + } + + seen.add(entry.index) + results[entry.index] = [...entry.embedding] + } + + if (seen.size !== inputLength) { + return Effect.fail( + invalidOutput(`Provider returned embeddings for ${seen.size} inputs but expected ${inputLength}`) + ) + } + + return Effect.succeed({ + results, + usage: { + inputTokens: response.usage?.prompt_tokens + } + }) +} + +const invalidOutput = (description: string): AiError.AiError => + AiError.make({ + module: "OpenAiEmbeddingModel", + method: "embedMany", + reason: new AiError.InvalidOutputError({ description }) + }) diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiError.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiError.ts new file mode 100644 index 0000000..88ba2dd --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiError.ts @@ -0,0 +1,178 @@ +/** + * The `OpenAiError` module defines OpenAI-specific metadata that can be + * attached to the shared `AiError` error types used by the AI packages. It is + * primarily used by OpenAI-compatible clients to preserve provider details + * such as error codes, error types, request IDs, and rate limit headers while + * still exposing errors through the provider-neutral Effect AI error model. + * + * Use this module when mapping OpenAI API failures into `AiError` values and + * when consumers need enough structured metadata to debug failed requests, + * inspect quota or rate limit responses, or correlate an error with OpenAI + * support. The exported types are metadata shapes only; the module augmentation + * makes those shapes available on the corresponding shared AI error metadata + * interfaces without defining new runtime error classes. + * + * @since 4.0.0 + */ + +/** + * OpenAI-specific error metadata fields. + * + * @category models + * @since 4.0.0 + */ +export type OpenAiErrorMetadata = { + /** + * The OpenAI error code returned by the API. + */ + readonly errorCode: string | null + /** + * The OpenAI error type returned by the API. + */ + readonly errorType: string | null + /** + * The unique request ID for debugging with OpenAI support. + */ + readonly requestId: string | null +} + +/** + * OpenAI-specific rate limit metadata fields. + * + * **Details** + * + * Extends base error metadata with rate limit specific information from + * OpenAI's rate limit headers. + * + * @category models + * @since 4.0.0 + */ +export type OpenAiRateLimitMetadata = OpenAiErrorMetadata & { + /** + * The rate limit type (e.g. "requests", "tokens"). + */ + readonly limit: string | null + /** + * Number of remaining requests in the current window. + */ + readonly remaining: number | null + /** + * Time until the request rate limit resets. + */ + readonly resetRequests: string | null + /** + * Time until the token rate limit resets. + */ + readonly resetTokens: string | null +} + +declare module "effect/unstable/ai/AiError" { + /** + * Metadata attached to rate limit errors returned by OpenAI-compatible APIs. + * + * @category models + * @since 4.0.0 + */ + export interface RateLimitErrorMetadata { + readonly openai?: OpenAiRateLimitMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible provider reports that quota or + * billing limits have been exhausted. + * + * @category models + * @since 4.0.0 + */ + export interface QuotaExhaustedErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached to authentication failures from OpenAI-compatible APIs, + * such as invalid, missing, or unauthorized API credentials. + * + * @category models + * @since 4.0.0 + */ + export interface AuthenticationErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible provider rejects content because + * it violates a safety or usage policy. + * + * @category models + * @since 4.0.0 + */ + export interface ContentPolicyErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached to malformed or unsupported requests rejected by an + * OpenAI-compatible API before model execution. + * + * @category models + * @since 4.0.0 + */ + export interface InvalidRequestErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached to unexpected server-side failures reported by an + * OpenAI-compatible provider. + * + * @category models + * @since 4.0.0 + */ + export interface InternalProviderErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible response cannot be converted + * into the expected AI package output shape. + * + * @category models + * @since 4.0.0 + */ + export interface InvalidOutputErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible structured output response does + * not satisfy the requested schema or parsing constraints. + * + * @category models + * @since 4.0.0 + */ + export interface StructuredOutputErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible provider cannot support the + * schema supplied for structured output or tool definitions. + * + * @category models + * @since 4.0.0 + */ + export interface UnsupportedSchemaErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * Metadata attached when an OpenAI-compatible error response cannot be mapped + * to a more specific shared AI error category. + * + * @category models + * @since 4.0.0 + */ + export interface UnknownErrorMetadata { + readonly openai?: OpenAiErrorMetadata | null + } +} diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts new file mode 100644 index 0000000..0803e53 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -0,0 +1,1948 @@ +/** + * The `OpenAiLanguageModel` module adapts OpenAI-compatible chat completions + * providers to Effect AI's `LanguageModel` service. It builds a model service + * from a model id, translates prompts, files, tools, structured output schemas, + * and provider-specific options into `OpenAiClient` requests, and maps normal + * or streaming chat completion results back into Effect AI response content and + * metadata. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import type * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { DeepMutable, Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import * as LanguageModel from "effect/unstable/ai/LanguageModel" +import * as AiModel from "effect/unstable/ai/Model" +import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" +import type * as Prompt from "effect/unstable/ai/Prompt" +import type * as Response from "effect/unstable/ai/Response" +import * as Tool from "effect/unstable/ai/Tool" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as InternalUtilities from "./internal/utilities.ts" +import { + type Annotation, + type ChatCompletionContentPart, + type CreateResponse, + type CreateResponse200, + type CreateResponse200Sse, + type CreateResponseRequestJson, + type IncludeEnum, + type InputContent, + type InputItem, + type MessageStatus, + OpenAiClient, + type ReasoningItem, + type SummaryTextContent, + type TextResponseFormatConfiguration, + type Tool as OpenAiClientTool +} from "./OpenAiClient.ts" +import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" + +/** + * Image detail level for vision requests. + */ +type ImageDetail = "auto" | "low" | "high" + +// ============================================================================= +// Configuration +// ============================================================================= + +type ConfigOptions = Simplify< + & Partial< + Omit + > + & { + /** + * File ID prefixes used to identify file IDs in Responses API. + * When undefined, all file data is treated as base64 content. + * + * Examples: + * - OpenAI: ['file-'] for IDs like 'file-abc123' + * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123' + */ + readonly fileIdPrefixes?: ReadonlyArray | undefined + /** + * Configuration options for a text response from the model. + */ + readonly text?: { + /** + * Constrains the verbosity of the model's response. Lower values will + * result in more concise responses, while higher values will result in + * more verbose responses. + * + * Defaults to `"medium"`. + */ + readonly verbosity?: "low" | "medium" | "high" | undefined + } | undefined + /** + * Whether to use strict JSON schema validation. + * + * Defaults to `true`. + */ + readonly strictJsonSchema?: boolean | undefined + } +> +type ModelConfig = Omit & { readonly [x: string]: unknown } + +/** + * Context service for OpenAI language model configuration. + * + * **When to use** + * + * Use as the context service for OpenAI-compatible language model request + * configuration, especially when a scoped operation should override the defaults + * supplied to `model`, `make`, or `layer`. + * + * @see {@link withConfigOverride} for scoping language model request overrides + * + * @category context + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + ConfigOptions & { readonly [x: string]: unknown } +>()("@effect/ai-openai-compat/OpenAiLanguageModel/Config") {} + +// ============================================================================= +// Provider Options / Metadata +// ============================================================================= + +declare module "effect/unstable/ai/Prompt" { + /** + * OpenAI-compatible options for file prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface FilePartOptions extends ProviderOptions { + /** + * Provider-specific file options for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`. + */ + readonly imageDetail?: ImageDetail | null + } | null + } + + /** + * OpenAI-compatible options for reasoning prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ReasoningPartOptions extends ProviderOptions { + /** + * Provider-specific reasoning options for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The encrypted content of the reasoning item - populated when a response + * is generated with `reasoning.encrypted_content` in the `include` + * parameter. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI-compatible options for assistant tool-call prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ToolCallPartOptions extends ProviderOptions { + /** + * Provider-specific tool-call options for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status to send for the tool-call item. + */ + readonly status?: MessageStatus | null + } | null + } + + /** + * OpenAI-compatible options for tool-result prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ToolResultPartOptions extends ProviderOptions { + /** + * Provider-specific tool-result options for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status to send for the tool-result item. + */ + readonly status?: MessageStatus | null + } | null + } + + /** + * OpenAI-compatible options for text prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface TextPartOptions extends ProviderOptions { + /** + * Provider-specific text options for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status to send for the text item. + */ + readonly status?: MessageStatus | null + /** + * A list of annotations that apply to the output text. + */ + readonly annotations?: ReadonlyArray | null + } | null + } +} + +declare module "effect/unstable/ai/Response" { + /** + * OpenAI-compatible metadata attached to a complete text response part. + * + * @category response + * @since 4.0.0 + */ + export interface TextPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the text part. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the text part. + */ + readonly itemId?: string | null + /** + * If the model emits a refusal content part, the refusal explanation + * from the model will be contained in the metadata of an empty text + * part. + */ + readonly refusal?: string | null + /** + * The status returned for the text item. + */ + readonly status?: MessageStatus | null + /** + * The text content part annotations. + */ + readonly annotations?: ReadonlyArray | null + } + } + + /** + * OpenAI-compatible metadata emitted when a streamed text part starts. + * + * @category response + * @since 4.0.0 + */ + export interface TextStartPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed text start. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the streamed text part. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI-compatible metadata emitted when a streamed text part ends. + * + * @category response + * @since 4.0.0 + */ + export interface TextEndPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed text end. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the streamed text part. + */ + readonly itemId?: string | null + /** + * The annotations collected for the completed streamed text part. + */ + readonly annotations?: ReadonlyArray | null + } | null + } + + /** + * OpenAI-compatible metadata attached to a complete reasoning response part. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the reasoning part. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI-compatible metadata emitted when a streamed reasoning part starts. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningStartPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning start. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI-compatible metadata emitted for a streamed reasoning delta. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning delta. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI-compatible metadata emitted when a streamed reasoning part ends. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningEndPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning end. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string + } | null + } + + /** + * OpenAI-compatible metadata attached to tool-call response parts. + * + * @category response + * @since 4.0.0 + */ + export interface ToolCallPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the tool call. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the tool call. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI-compatible metadata attached to document source citations. + * + * @category response + * @since 4.0.0 + */ + export interface DocumentSourcePartMetadata extends ProviderMetadata { + /** + * Provider-specific citation metadata for OpenAI-compatible APIs. + */ + readonly openai?: + | { + /** + * Identifies a citation to an uploaded file. + */ + readonly type: "file_citation" + /** + * The index of the file in the list of files. + */ + readonly index: number + /** + * The ID of the file. + */ + readonly fileId: string + } + | { + /** + * Identifies a citation to a generated file path. + */ + readonly type: "file_path" + /** + * The index of the file in the list of files. + */ + readonly index: number + /** + * The ID of the file. + */ + readonly fileId: string + } + | { + /** + * Identifies a citation to a file inside a container. + */ + readonly type: "container_file_citation" + /** + * The ID of the file. + */ + readonly fileId: string + /** + * The ID of the container file. + */ + readonly containerId: string + } + | null + } + + /** + * OpenAI-compatible metadata attached to URL source citations. + * + * @category response + * @since 4.0.0 + */ + export interface UrlSourcePartMetadata extends ProviderMetadata { + /** + * Provider-specific URL citation metadata for OpenAI-compatible APIs. + */ + readonly openai?: { + /** + * Identifies a citation to a URL. + */ + readonly type: "url_citation" + /** + * The index of the first character of the URL citation in the message. + */ + readonly startIndex: number + /** + * The index of the last character of the URL citation in the message. + */ + readonly endIndex: number + } | null + } + + /** + * OpenAI-compatible metadata attached to finish response parts. + * + * @category response + * @since 4.0.0 + */ + export interface FinishPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned when generation finishes. + */ + readonly openai?: { + /** + * The service tier reported by the OpenAI-compatible provider. + */ + readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | null + } | null + } +} + +// ============================================================================= +// Language Model +// ============================================================================= + +/** + * Creates an OpenAI-compatible model descriptor that can be provided with `Effect.provide`. + * + * **When to use** + * + * Use when you want an OpenAI-compatible language model value that carries + * provider and model metadata and can be supplied directly to an Effect program. + * + * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly + * @see {@link make} for constructing the language model service effectfully + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: string, + config?: ModelConfig +): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> => + AiModel.make("openai", model, layer({ model, config })) + +// TODO +// /** +// * @since 4.0.0 +// * @category constructors +// */ +// export const modelWithTokenizer = ( +// model: string, +// config?: Omit +// ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> => +// AiModel.make("openai", model, layerWithTokenizer({ model, config })) + +/** + * Creates an OpenAI-compatible `LanguageModel` service from a model identifier and optional request defaults. + * + * **When to use** + * + * Use to construct an OpenAI-compatible chat-completions language model service + * backed by `OpenAiClient`. + * + * **Details** + * + * The returned effect requires `OpenAiClient`. Request defaults from the + * `config` option are merged with any `Config` service in the context, with + * context values taking precedence. The service supports both `generateText` and + * `streamText`. + * + * @see {@link layer} for providing the service as a `Layer` + * @see {@link model} for creating a model descriptor for `AiModel.provide` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: string + readonly config?: ModelConfig | undefined +}): Effect.fn.Return { + const client = yield* OpenAiClient + + const makeConfig = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + const makeRequest = Effect.fnUntraced( + function*>({ config, options, toolNameMapper }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return { + const include = new Set() + const capabilities = getModelCapabilities(config.model!) + const messages = yield* prepareMessages({ + config, + options, + capabilities, + include, + toolNameMapper + }) + const { toolChoice, tools } = yield* prepareTools({ + config, + options, + toolNameMapper + }) + const responseFormat = yield* prepareResponseFormat({ + config, + options + }) + const { fileIdPrefixes: _fip, strictJsonSchema: _sjs, ...apiConfig } = config + const request: CreateResponse = { + ...apiConfig, + input: messages, + include: include.size > 0 ? Array.from(include) : null, + text: { + verbosity: config.text?.verbosity ?? null, + format: responseFormat + }, + ...(tools !== undefined ? { tools } : undefined), + ...(toolChoice !== undefined ? { tool_choice: toolChoice } : undefined) + } + return toChatCompletionsRequest(request) + } + ) + + return yield* LanguageModel.make({ + codecTransformer: toCodecOpenAI, + generateText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request) + const [rawResponse, response] = yield* client.createResponse(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse({ + rawResponse, + response, + toolNameMapper + }) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request) + const [response, stream] = yield* client.createResponseStream(request) + return yield* makeStreamResponse({ + stream, + response, + toolNameMapper + }) + }, + (effect, options) => + effect.pipe( + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * Creates a layer for the OpenAI-compatible language model. + * + * **When to use** + * + * Use when composing application layers and you want OpenAI-compatible APIs to + * satisfy `LanguageModel.LanguageModel` while supplying `OpenAiClient` from + * another layer. + * + * @see {@link make} for constructing the language model service effectfully + * @see {@link model} for creating an AI model descriptor + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: string + readonly config?: ModelConfig | undefined +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make(options)) + +/** + * Provides scoped config overrides for OpenAI-compatible language model operations. + * + * **When to use** + * + * Use to override request configuration for a single language model effect + * without changing the defaults supplied to `model`, `make`, or `layer`. + * + * **Details** + * + * Existing `Config` values from the Effect context are merged with `overrides`, + * and the override values take precedence. + * + * @see {@link Config} for the configuration shape + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const getSystemMessageMode = (model: string): "system" | "developer" => + model.startsWith("o") || + model.startsWith("gpt-5") || + model.startsWith("codex-") || + model.startsWith("computer-use") + ? "developer" + : "system" + +const prepareMessages = Effect.fnUntraced( + function*>({ + config, + options, + capabilities, + include, + toolNameMapper + }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly include: Set + readonly capabilities: ModelCapabilities + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return, AiError.AiError> { + const hasConversation = Predicate.isNotNullish(config.conversation) + + // Handle Included Features + if (config.top_logprobs !== undefined) { + include.add("message.output_text.logprobs") + } + if (config.store === false && capabilities.isReasoningModel) { + include.add("reasoning.encrypted_content") + } + + const messages: Array = [] + + for (const message of options.prompt.content) { + switch (message.role) { + case "system": { + messages.push({ + role: getSystemMessageMode(config.model!), + content: message.content + }) + break + } + + case "user": { + const content: Array = [] + + for (let index = 0; index < message.content.length; index++) { + const part = message.content[index] + + switch (part.type) { + case "text": { + content.push({ type: "input_text", text: part.text }) + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const detail = getImageDetail(part) + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_image", file_id: part.data, detail }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_image", image_url: part.data.toString(), detail }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const imageUrl = `data:${mediaType};base64,${base64}` + content.push({ type: "input_image", image_url: imageUrl, detail }) + } + } else if (part.mediaType === "application/pdf") { + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_file", file_id: part.data }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_file", file_url: part.data.toString() }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const fileName = part.fileName ?? `part-${index}.pdf` + const fileData = `data:application/pdf;base64,${base64}` + content.push({ type: "input_file", filename: fileName, file_data: fileData }) + } + } else { + return yield* AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidRequestError({ + description: `Detected unsupported media type for file: '${part.mediaType}'` + }) + }) + } + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const reasoningMessages: Record> = {} + + for (const part of message.content) { + switch (part.type) { + case "text": { + const id = getItemId(part) + + // When in conversation mode, skip items that already exist in the + // conversation context to avoid "Duplicate item found" errors + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (config.store === true && Predicate.isNotNull(id)) { + messages.push({ type: "item_reference", id }) + break + } + + messages.push({ + id: id!, + type: "message", + role: "assistant", + status: part.options.openai?.status ?? "completed", + content: [{ + type: "output_text", + text: part.text, + annotations: part.options.openai?.annotations ?? [], + logprobs: [] + }] + }) + + break + } + + case "reasoning": { + const id = getItemId(part) + const encryptedContent = getEncryptedContent(part) + + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (Predicate.isNotNull(id)) { + const message = reasoningMessages[id] + + if (config.store === true) { + // Use item references to refer to reasoning (single reference) + // when the first part is encountered + if (Predicate.isUndefined(message)) { + messages.push({ type: "item_reference", id }) + + // Store unused reasoning message to mark its id as used + reasoningMessages[id] = { + type: "reasoning", + id, + summary: [] + } + } + } else { + const summaryParts: Array = [] + + if (part.text.length > 0) { + summaryParts.push({ type: "summary_text", text: part.text }) + } + + if (Predicate.isUndefined(message)) { + reasoningMessages[id] = { + type: "reasoning", + id, + summary: summaryParts, + encrypted_content: encryptedContent ?? null + } + + messages.push(reasoningMessages[id]) + } else { + message.summary.push(...summaryParts) + + // Update encrypted content to enable setting it in the + // last summary part + if (Predicate.isNotNull(encryptedContent)) { + message.encrypted_content = encryptedContent + } + } + } + } + + break + } + + case "tool-call": { + const id = getItemId(part) + const status = getStatus(part) + + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (config.store && Predicate.isNotNull(id)) { + messages.push({ type: "item_reference", id }) + break + } + + if (part.providerExecuted) { + break + } + + const toolName = toolNameMapper.getProviderName(part.name) + + messages.push({ + type: "function_call", + name: toolName, + call_id: part.id, + arguments: JSON.stringify(part.params), + ...(Predicate.isNotNull(id) ? { id } : {}), + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + + break + } + + // Assistant tool-result parts are always provider executed + case "tool-result": { + // Skip execution denied results - these have no corresponding + // item in OpenAI's store + if ( + Predicate.hasProperty(part.result, "type") && + part.result.type === "execution-denied" + ) { + break + } + + if (hasConversation) { + break + } + + if (config.store === true) { + const id = getItemId(part) ?? part.id + messages.push({ type: "item_reference", id }) + } + } + } + } + + break + } + + case "tool": { + for (const part of message.content) { + if (part.type === "tool-approval-response") { + continue + } + + const status = getStatus(part) + + messages.push({ + type: "function_call_output", + call_id: part.id, + output: typeof part.result === "string" ? part.result : JSON.stringify(part.result), + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + } + + break + } + } + } + + return messages + } +) + +// ============================================================================= +// HTTP Details +// ============================================================================= + +const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +const buildHttpResponseDetails = ( + response: HttpClientResponse.HttpClientResponse +): typeof Response.HttpResponseDetails.Type => ({ + status: response.status, + headers: Redactable.redact(response.headers) as Record +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +type ResponseStreamEvent = CreateResponse200Sse + +type ActiveToolCall = { + readonly id: string + name: string + arguments: string +} + +const makeResponse = Effect.fnUntraced( + function*>({ + rawResponse, + response, + toolNameMapper + }: { + readonly rawResponse: CreateResponse200 + readonly response: HttpClientResponse.HttpClientResponse + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Array, + AiError.AiError + > { + let hasToolCalls = false + const parts: Array = [] + + const createdAt = new Date(rawResponse.created * 1000) + parts.push({ + type: "response-metadata", + id: rawResponse.id, + modelId: rawResponse.model as string, + timestamp: DateTime.formatIso(DateTime.fromDateUnsafe(createdAt)), + request: buildHttpRequestDetails(response.request) + }) + + const choice = rawResponse.choices[0] + const message = choice?.message + + if (message !== undefined) { + const reasoning = message.reasoning ?? message.reasoning_content + if (Predicate.isNotNullish(reasoning) && reasoning.length > 0) { + parts.push({ type: "reasoning", text: reasoning }) + } + + if ( + message.content !== undefined && Predicate.isNotNull(message.content) && message.content.length > 0 + ) { + parts.push({ type: "text", text: message.content }) + } + + if (message.tool_calls !== undefined) { + for (const [index, toolCall] of message.tool_calls.entries()) { + const toolId = toolCall.id ?? `${rawResponse.id}_tool_${index}` + const toolName = toolNameMapper.getCustomName(toolCall.function?.name ?? "unknown_tool") + const toolParams = toolCall.function?.arguments ?? "{}" + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams: {}, + description: `Failed to securely JSON parse tool parameters: ${cause}` + }) + }) + }) + hasToolCalls = true + parts.push({ + type: "tool-call", + id: toolId, + name: toolName, + params, + metadata: { openai: { ...makeItemIdMetadata(toolCall.id) } } + }) + } + } + } + + const finishReason = InternalUtilities.resolveFinishReason( + choice?.finish_reason, + hasToolCalls + ) + const serviceTier = normalizeServiceTier(rawResponse.service_tier) + + parts.push({ + type: "finish", + reason: finishReason, + usage: getUsage(rawResponse.usage), + response: buildHttpResponseDetails(response), + ...(serviceTier !== undefined && { metadata: { openai: { serviceTier } } }) + }) + + return parts + } +) + +const makeStreamResponse = Effect.fnUntraced( + function*>({ + stream, + response, + toolNameMapper + }: { + readonly stream: Stream.Stream + readonly response: HttpClientResponse.HttpClientResponse + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Stream.Stream, + AiError.AiError + > { + let serviceTier: string | undefined = undefined + let usage: CreateResponse200["usage"] = undefined + let finishReason: string | null | undefined = undefined + let metadataEmitted = false + let textStarted = false + let textId = "" + let reasoningStarted = false + let reasoningId = "" + let hasToolCalls = false + const activeToolCalls: Record = {} + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if (event === "[DONE]") { + if (reasoningStarted) { + parts.push({ + type: "reasoning-end", + id: reasoningId, + metadata: { openai: { ...makeItemIdMetadata(reasoningId) } } + }) + } + + if (textStarted) { + parts.push({ + type: "text-end", + id: textId, + metadata: { openai: { ...makeItemIdMetadata(textId) } } + }) + } + + for (const toolCall of Object.values(activeToolCalls)) { + const toolParams = toolCall.arguments.length > 0 ? toolCall.arguments : "{}" + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeStreamResponse", + reason: new AiError.ToolParameterValidationError({ + toolName: toolCall.name, + toolParams: {}, + description: `Failed to securely JSON parse tool parameters: ${cause}` + }) + }) + }) + parts.push({ type: "tool-params-end", id: toolCall.id }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolCall.name, + params, + metadata: { openai: { ...makeItemIdMetadata(toolCall.id) } } + }) + hasToolCalls = true + } + + const normalizedServiceTier = normalizeServiceTier(serviceTier) + parts.push({ + type: "finish", + reason: InternalUtilities.resolveFinishReason(finishReason, hasToolCalls), + usage: getUsage(usage), + response: buildHttpResponseDetails(response), + ...(normalizedServiceTier !== undefined + ? { metadata: { openai: { serviceTier: normalizedServiceTier } } } + : undefined) + }) + return parts + } + + if (event.service_tier !== undefined) { + serviceTier = event.service_tier + } + if (event.usage !== undefined && Predicate.isNotNull(event.usage)) { + usage = event.usage + } + + if (!metadataEmitted) { + metadataEmitted = true + textId = `${event.id}_message` + reasoningId = `${event.id}_reasoning` + parts.push({ + type: "response-metadata", + id: event.id, + modelId: event.model, + timestamp: DateTime.formatIso(DateTime.fromDateUnsafe(new Date(event.created * 1000))), + request: buildHttpRequestDetails(response.request) + }) + } + + const choice = event.choices[0] + if (Predicate.isUndefined(choice)) { + return parts + } + + const reasoningDelta = choice.delta?.reasoning ?? choice.delta?.reasoning_content + if (Predicate.isNotNullish(reasoningDelta) && reasoningDelta.length > 0) { + if (!reasoningStarted) { + reasoningStarted = true + parts.push({ + type: "reasoning-start", + id: reasoningId, + metadata: { openai: { ...makeItemIdMetadata(reasoningId) } } + }) + } + parts.push({ type: "reasoning-delta", id: reasoningId, delta: reasoningDelta }) + } + + if (choice.delta?.content !== undefined && Predicate.isNotNull(choice.delta.content)) { + if (reasoningStarted) { + reasoningStarted = false + parts.push({ + type: "reasoning-end", + id: reasoningId, + metadata: { openai: { ...makeItemIdMetadata(reasoningId) } } + }) + } + + if (!textStarted) { + textStarted = true + parts.push({ + type: "text-start", + id: textId, + metadata: { openai: { ...makeItemIdMetadata(textId) } } + }) + } + parts.push({ type: "text-delta", id: textId, delta: choice.delta.content }) + } + + if (choice.delta?.tool_calls !== undefined) { + hasToolCalls = hasToolCalls || choice.delta.tool_calls.length > 0 + choice.delta.tool_calls.forEach((deltaTool, indexInChunk) => { + const toolIndex = deltaTool.index ?? indexInChunk + const activeToolCall = activeToolCalls[toolIndex] + const toolId = activeToolCall?.id ?? deltaTool.id ?? `${event.id}_tool_${toolIndex}` + const providerToolName = deltaTool.function?.name + const toolName = Predicate.isNotNullish(providerToolName) + ? toolNameMapper.getCustomName(providerToolName) + : activeToolCall?.name ?? toolNameMapper.getCustomName("unknown_tool") + const argumentsDelta = deltaTool.function?.arguments ?? "" + + if (Predicate.isUndefined(activeToolCall)) { + activeToolCalls[toolIndex] = { + id: toolId, + name: toolName, + arguments: argumentsDelta + } + parts.push({ type: "tool-params-start", id: toolId, name: toolName }) + } else { + activeToolCall.name = toolName + activeToolCall.arguments = `${activeToolCall.arguments}${argumentsDelta}` + } + + if (argumentsDelta.length > 0) { + parts.push({ type: "tool-params-delta", id: toolId, delta: argumentsDelta }) + } + }) + } + + if (choice.finish_reason !== undefined && Predicate.isNotNull(choice.finish_reason)) { + finishReason = choice.finish_reason + } + + return parts + })), + Stream.flattenIterable + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: CreateResponseRequestJson +): void => { + addGenAIAnnotations(span, { + system: "openai", + operation: { name: "chat" }, + request: { + model: request.model as string, + temperature: request.temperature as number | undefined, + topP: request.top_p as number | undefined, + maxTokens: request.max_tokens as number | undefined + }, + openai: { + request: { + responseFormat: request.response_format?.type, + serviceTier: request.service_tier as string | undefined + } + } + }) +} + +const annotateResponse = (span: Span, response: CreateResponse200): void => { + const finishReason = response.choices[0]?.finish_reason ?? undefined + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model as string, + finishReasons: finishReason !== undefined ? [finishReason] : undefined + }, + usage: { + inputTokens: response.usage?.prompt_tokens, + outputTokens: response.usage?.completion_tokens + }, + openai: { + response: { + serviceTier: response.service_tier as string | undefined + } + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + const serviceTier = (part.metadata as any)?.openai?.serviceTier as string | undefined + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens.total, + outputTokens: part.usage.outputTokens.total + }, + openai: { + response: { serviceTier } + } + }) + } +} + +// ============================================================================= +// Tool Conversion +// ============================================================================= + +type OpenAiToolChoice = CreateResponse["tool_choice"] + +const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError => + AiError.make({ + module: "OpenAiLanguageModel", + method, + reason: new AiError.UnsupportedSchemaError({ + description: error instanceof Error ? error.message : String(error) + }) + }) + +const tryJsonSchema = (schema: S, method: string) => + Effect.try({ + try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const tryToolJsonSchema = (tool: T, method: string) => + Effect.try({ + try: () => Tool.getJsonSchema(tool, { transformer: toCodecOpenAI }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const prepareTools = Effect.fnUntraced(function*>({ + config, + options, + toolNameMapper +}: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper +}): Effect.fn.Return<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: OpenAiToolChoice | undefined +}, AiError.AiError> { + // Return immediately if no tools are in the toolkit + if (options.tools.length === 0) { + return { tools: undefined, toolChoice: undefined } + } + + const tools: Array = [] + let toolChoice: OpenAiToolChoice | undefined = undefined + + // Filter the incoming tools down to the set of allowed tools as indicated by + // the tool choice. This must be done here given that there is no tool name + // in OpenAI's provider-defined tools, so there would be no way to perform + // this filter otherwise + let allowedTools = options.tools + if (typeof options.toolChoice === "object" && "oneOf" in options.toolChoice) { + const allowedToolNames = new Set(options.toolChoice.oneOf) + allowedTools = options.tools.filter((tool) => allowedToolNames.has(tool.name)) + toolChoice = options.toolChoice.mode === "required" ? "required" : "auto" + } + + // Convert the tools in the toolkit to the provider-defined format + for (const tool of allowedTools) { + if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) { + const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true + const parameters = yield* tryToolJsonSchema(tool, "prepareTools") + tools.push({ + type: "function", + name: tool.name, + description: Tool.getDescription(tool) ?? null, + parameters: parameters as { readonly [x: string]: Schema.Json }, + strict + }) + } + + if (Tool.isProviderDefined(tool)) { + tools.push({ + type: "function", + name: tool.providerName, + description: Tool.getDescription(tool) ?? null, + parameters: Tool.getJsonSchema(tool) as { readonly [x: string]: Schema.Json }, + strict: config.strictJsonSchema ?? true + }) + } + } + + if (options.toolChoice === "auto" || options.toolChoice === "none" || options.toolChoice === "required") { + toolChoice = options.toolChoice + } + + if (typeof options.toolChoice === "object" && "tool" in options.toolChoice) { + const toolName = toolNameMapper.getProviderName(options.toolChoice.tool) + const providerNames = toolNameMapper.providerNames + if (providerNames.includes(toolName)) { + toolChoice = { type: "function", name: toolName } + } else { + toolChoice = { type: "function", name: options.toolChoice.tool } + } + } + + return { tools, toolChoice } +}) + +const toChatCompletionsRequest = (payload: CreateResponse): CreateResponseRequestJson => { + const messages = toChatMessages(payload.input) + const responseFormat = toChatResponseFormat(payload.text?.format) + const tools = payload.tools !== undefined + ? payload.tools.map(toChatTool).filter((tool): tool is NonNullable> => + tool !== undefined + ) + : [] + const toolChoice = toChatToolChoice(payload.tool_choice) + + return { + ...extractCustomRequestProperties(payload), + model: payload.model ?? "", + messages: messages.length > 0 ? messages : [{ role: "user", content: "" }], + ...(payload.temperature !== undefined ? { temperature: payload.temperature } : undefined), + ...(payload.top_p !== undefined ? { top_p: payload.top_p } : undefined), + ...(payload.max_output_tokens !== undefined ? { max_tokens: payload.max_output_tokens } : undefined), + ...(payload.user !== undefined ? { user: payload.user } : undefined), + ...(payload.seed !== undefined ? { seed: payload.seed } : undefined), + ...(payload.parallel_tool_calls !== undefined + ? { parallel_tool_calls: payload.parallel_tool_calls } + : undefined), + ...(payload.service_tier !== undefined ? { service_tier: payload.service_tier } : undefined), + ...(payload.reasoning !== undefined ? { reasoning: payload.reasoning } : undefined), + ...(responseFormat !== undefined ? { response_format: responseFormat } : undefined), + ...(tools.length > 0 ? { tools } : undefined), + ...(toolChoice !== undefined ? { tool_choice: toolChoice } : undefined) + } +} + +const createResponseKnownProperties = new Set([ + "metadata", + "top_logprobs", + "temperature", + "top_p", + "user", + "safety_identifier", + "prompt_cache_key", + "service_tier", + "prompt_cache_retention", + "previous_response_id", + "model", + "reasoning", + "background", + "max_output_tokens", + "max_tool_calls", + "text", + "tools", + "tool_choice", + "truncation", + "input", + "include", + "parallel_tool_calls", + "store", + "instructions", + "stream", + "conversation", + "modalities", + "seed" +]) + +const extractCustomRequestProperties = (payload: CreateResponse): Record => { + const customProperties: Record = {} + for (const [key, value] of Object.entries(payload)) { + if (!createResponseKnownProperties.has(key)) { + customProperties[key] = value + } + } + return customProperties +} + +const toChatResponseFormat = ( + format: TextResponseFormatConfiguration | undefined +): CreateResponseRequestJson["response_format"] | undefined => { + if (Predicate.isUndefined(format) || Predicate.isNull(format)) { + return undefined + } + + switch (format.type) { + case "json_object": { + return { type: "json_object" } + } + case "json_schema": { + return { + type: "json_schema", + json_schema: { + name: format.name, + schema: format.schema, + ...(format.description !== undefined ? { description: format.description } : undefined), + ...(Predicate.isNotNullish(format.strict) ? { strict: format.strict } : undefined) + } + } + } + default: { + return undefined + } + } +} + +const toChatToolChoice = ( + toolChoice: OpenAiToolChoice +): CreateResponseRequestJson["tool_choice"] | undefined => { + if (Predicate.isUndefined(toolChoice)) { + return undefined + } + + if (typeof toolChoice === "string") { + return toolChoice + } + + if (toolChoice.type === "allowed_tools") { + return toolChoice.mode + } + + if (toolChoice.type === "function") { + return { + type: "function", + function: { + name: toolChoice.name + } + } + } + + const functionName = Predicate.hasProperty(toolChoice, "name") && typeof toolChoice.name === "string" + ? toolChoice.name + : toolChoice.type + + return { + type: "function", + function: { + name: functionName + } + } +} + +const toChatTool = ( + tool: OpenAiClientTool +): NonNullable[number] | undefined => { + if (tool.type === "function") { + return { + type: "function", + function: { + name: tool.name, + ...(tool.description !== undefined ? { description: tool.description } : undefined), + ...(Predicate.isNotNullish(tool.parameters) ? { parameters: tool.parameters } : undefined), + ...(Predicate.isNotNullish(tool.strict) ? { strict: tool.strict } : undefined) + } + } + } + + if (tool.type === "custom") { + return { + type: "function", + function: { + name: tool.name, + parameters: { type: "object", additionalProperties: true } + } + } + } + + return undefined +} + +const toChatMessages = ( + input: CreateResponse["input"] +): Array => { + if (Predicate.isUndefined(input)) { + return [] + } + + if (typeof input === "string") { + return [{ role: "user", content: input }] + } + + const messages: Array = [] + + for (const item of input) { + messages.push(...toChatMessagesFromItem(item)) + } + + return messages +} + +const toChatMessagesFromItem = ( + item: InputItem +): Array => { + if (Predicate.hasProperty(item, "type") && item.type === "message") { + return [{ + role: item.role, + content: toAssistantChatMessageContent(item.content) + }] + } + + if (Predicate.hasProperty(item, "role")) { + return [{ + role: item.role, + content: toChatMessageContent(item.content) + }] + } + + switch (item.type) { + case "function_call": { + return [{ + role: "assistant", + content: null, + tool_calls: [{ + id: item.call_id, + type: "function", + function: { + name: item.name, + arguments: item.arguments + } + }] + }] + } + + case "function_call_output": { + return [{ + role: "tool", + tool_call_id: item.call_id, + content: stringifyJson(item.output) + }] + } + + default: { + return [] + } + } +} + +const toAssistantChatMessageContent = ( + content: ReadonlyArray<{ + readonly type: string + readonly [x: string]: unknown + }> +): string => { + let text = "" + for (const part of content) { + if (part.type === "output_text" && typeof part.text === "string") { + text += part.text + } + if (part.type === "refusal" && typeof part.refusal === "string") { + text += part.refusal + } + } + return text +} + +const toChatMessageContent = ( + content: string | ReadonlyArray +): string | ReadonlyArray => { + if (typeof content === "string") { + return content + } + + const parts: Array = [] + + for (const part of content) { + switch (part.type) { + case "input_text": { + parts.push({ type: "text", text: part.text }) + break + } + case "input_image": { + const imageUrl = part.image_url !== undefined + ? part.image_url + : part.file_id !== undefined + ? `openai://file/${part.file_id}` + : undefined + + if (imageUrl !== undefined && Predicate.isNotNull(imageUrl)) { + parts.push({ + type: "image_url", + image_url: { + url: imageUrl, + ...(Predicate.isNotNullish(part.detail) ? { detail: part.detail } : undefined) + } + }) + } + break + } + case "input_file": { + if (part.file_url !== undefined) { + parts.push({ type: "text", text: part.file_url }) + } else if (part.file_data !== undefined) { + parts.push({ type: "text", text: part.file_data }) + } else if (part.file_id !== undefined) { + parts.push({ type: "text", text: `openai://file/${part.file_id}` }) + } + break + } + } + } + + if (parts.length === 0) { + return "" + } + + if (parts.every((part) => part.type === "text")) { + return parts.map((part) => part.text).join("\n") + } + + return parts +} + +const stringifyJson = (value: unknown): string => + typeof value === "string" + ? value + : JSON.stringify(value) + +// ============================================================================= +// Utilities +// ============================================================================= + +const isFileId = (data: string, config: typeof Config.Service): boolean => + config.fileIdPrefixes != null && config.fileIdPrefixes.some((prefix) => data.startsWith(prefix)) + +const getItemId = ( + part: + | Prompt.TextPart + | Prompt.ReasoningPart + | Prompt.ToolCallPart + | Prompt.ToolResultPart +): string | null => part.options.openai?.itemId ?? null +const getStatus = ( + part: + | Prompt.TextPart + | Prompt.ToolCallPart + | Prompt.ToolResultPart +): MessageStatus | null => part.options.openai?.status ?? null +const getEncryptedContent = ( + part: Prompt.ReasoningPart +): string | null => part.options.openai?.encryptedContent ?? null + +const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto" + +const makeItemIdMetadata = (itemId: string | undefined) => itemId !== undefined ? { itemId } : undefined + +const normalizeServiceTier = ( + serviceTier: string | undefined +): "default" | "auto" | "flex" | "scale" | "priority" | null | undefined => { + switch (serviceTier) { + case undefined: + return undefined + case "default": + case "auto": + case "flex": + case "scale": + case "priority": + return serviceTier + default: + return null + } +} + +const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions +}): Effect.fn.Return { + if (options.responseFormat.type === "json") { + const name = options.responseFormat.objectName + const schema = options.responseFormat.schema + const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat") + return { + type: "json_schema", + name, + description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object", + schema: jsonSchema as any, + strict: config.strictJsonSchema ?? true + } + } + return { type: "text" } +}) + +interface ModelCapabilities { + readonly isReasoningModel: boolean + readonly systemMessageMode: "remove" | "system" | "developer" + readonly supportsFlexProcessing: boolean + readonly supportsPriorityProcessing: boolean + /** + * Allow temperature, topP, logProbs when reasoningEffort is none. + */ + readonly supportsNonReasoningParameters: boolean +} + +const getModelCapabilities = (modelId: string): ModelCapabilities => { + const supportsFlexProcessing = modelId.startsWith("o3") || + modelId.startsWith("o4-mini") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) + + const supportsPriorityProcessing = modelId.startsWith("gpt-4") || + modelId.startsWith("gpt-5-mini") || + (modelId.startsWith("gpt-5") && + !modelId.startsWith("gpt-5-nano") && + !modelId.startsWith("gpt-5-chat")) || + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") + + // Use allowlist approach: only known reasoning models should use 'developer' role + // This prevents issues with fine-tuned models, third-party models, and custom models + const isReasoningModel = modelId.startsWith("o1") || + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") || + modelId.startsWith("codex-mini") || + modelId.startsWith("computer-use-preview") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) + + // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility + // GPT-5.1 and GPT-5.2 support temperature, topP, logProbs when reasoningEffort is none + const supportsNonReasoningParameters = modelId.startsWith("gpt-5.1") || modelId.startsWith("gpt-5.2") + + const systemMessageMode = isReasoningModel ? "developer" : "system" + + return { + supportsFlexProcessing, + supportsPriorityProcessing, + isReasoningModel, + systemMessageMode, + supportsNonReasoningParameters + } +} + +const getUsage = (usage: CreateResponse200["usage"]): Response.Usage => { + if (Predicate.isNullish(usage)) { + return { + inputTokens: { + uncached: undefined, + total: undefined, + cacheRead: undefined, + cacheWrite: undefined + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined + } + } + } + + const inputTokens = usage.prompt_tokens + const outputTokens = usage.completion_tokens + const cachedTokens = getUsageDetailNumber(usage.prompt_tokens_details, "cached_tokens") ?? 0 + const reasoningTokens = getUsageDetailNumber(usage.completion_tokens_details, "reasoning_tokens") ?? 0 + + return { + inputTokens: { + uncached: inputTokens - cachedTokens, + total: inputTokens, + cacheRead: cachedTokens, + cacheWrite: undefined + }, + outputTokens: { + total: outputTokens, + text: outputTokens - reasoningTokens, + reasoning: reasoningTokens + } + } +} + +const getUsageDetailNumber = ( + details: unknown, + field: string +): number | undefined => { + if (typeof details !== "object" || details === null) { + return undefined + } + + const value = (details as Record)[field] + return typeof value === "number" ? value : undefined +} diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts b/repos/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts new file mode 100644 index 0000000..349a139 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts @@ -0,0 +1,164 @@ +/** + * The `OpenAiTelemetry` module defines OpenAI-compatible telemetry attributes + * and a helper for adding them to a tracing span. It keeps the standard GenAI + * telemetry attributes and adds request and response metadata under the + * `gen_ai.openai.*` OpenTelemetry namespaces. + * + * @since 4.0.0 + */ +import { dual } from "effect/Function" +import * as String from "effect/String" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import * as Telemetry from "effect/unstable/ai/Telemetry" + +/** + * The attributes used to describe telemetry in the context of Generative + * Artificial Intelligence (GenAI) Models requests and responses. + * + * **Details** + * + * These attributes follow the OpenTelemetry generative AI semantic + * conventions: + * https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/ + * + * @category models + * @since 4.0.0 + */ +export type OpenAiTelemetryAttributes = Simplify< + & Telemetry.GenAITelemetryAttributes + & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix +> + +/** + * All telemetry attributes which are part of the GenAI specification, + * including the OpenAI-specific attributes. + * + * @category models + * @since 4.0.0 + */ +export type AllAttributes = Telemetry.AllAttributes & RequestAttributes & ResponseAttributes + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.request`. + * + * @category models + * @since 4.0.0 + */ +export interface RequestAttributes { + /** + * The response format that is requested. + */ + readonly responseFormat?: (string & {}) | WellKnownResponseFormat | null | undefined + /** + * The service tier requested. May be a specific tier, `default`, or `auto`. + */ + readonly serviceTier?: (string & {}) | WellKnownServiceTier | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.response`. + * + * @category models + * @since 4.0.0 + */ +export interface ResponseAttributes { + /** + * The service tier used for the response. + */ + readonly serviceTier?: string | null | undefined + /** + * A fingerprint to track any eventual change in the Generative AI + * environment. + */ + readonly systemFingerprint?: string | null | undefined +} + +/** + * The `gen_ai.openai.request.response_format` attribute has a list of + * well-known values. + * + * **Details** + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @category models + * @since 4.0.0 + */ +export type WellKnownResponseFormat = "json_object" | "json_schema" | "text" + +/** + * The `gen_ai.openai.request.service_tier` attribute has a list of + * well-known values. + * + * **Details** + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @category models + * @since 4.0.0 + */ +export type WellKnownServiceTier = "auto" | "default" + +/** + * Options accepted by `addGenAIAnnotations`, combining standard GenAI telemetry + * attributes with optional OpenAI-compatible request and response attributes. + * + * @category options + * @since 4.0.0 + */ +export type OpenAiTelemetryAttributeOptions = Telemetry.GenAITelemetryAttributeOptions & { + openai?: { + request?: RequestAttributes | undefined + response?: ResponseAttributes | undefined + } | undefined +} + +const addOpenAiRequestAttributes = Telemetry.addSpanAttributes("gen_ai.openai.request", String.camelToSnake)< + RequestAttributes +> +const addOpenAiResponseAttributes = Telemetry.addSpanAttributes("gen_ai.openai.response", String.camelToSnake)< + ResponseAttributes +> + +/** + * Applies the specified OpenAI GenAI telemetry attributes to the provided + * `Span`. + * + * **When to use** + * + * Use to annotate an OpenAI-compatible model span with standard GenAI telemetry + * attributes and OpenAI-specific request or response metadata. + * + * **Details** + * + * Standard GenAI attributes are applied first. When OpenAI request or response + * metadata is present, it is written under `gen_ai.openai.request.*` and + * `gen_ai.openai.response.*` attributes. + * + * **Gotchas** + * + * Mutates the supplied `Span` in place. + * + * @category tracing + * @since 4.0.0 + */ +export const addGenAIAnnotations: { + (options: OpenAiTelemetryAttributeOptions): (span: Span) => void + (span: Span, options: OpenAiTelemetryAttributeOptions): void +} = dual(2, (span: Span, options: OpenAiTelemetryAttributeOptions) => { + Telemetry.addGenAIAnnotations(span, options) + if (options.openai != null) { + if (options.openai.request != null) { + addOpenAiRequestAttributes(span, options.openai.request) + } + if (options.openai.response != null) { + addOpenAiResponseAttributes(span, options.openai.response) + } + } +}) diff --git a/repos/effect/packages/ai/openai-compat/src/index.ts b/repos/effect/packages/ai/openai-compat/src/index.ts new file mode 100644 index 0000000..7a5cd7f --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/index.ts @@ -0,0 +1,35 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as OpenAiClient from "./OpenAiClient.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiConfig from "./OpenAiConfig.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiError from "./OpenAiError.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiLanguageModel from "./OpenAiLanguageModel.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiTelemetry from "./OpenAiTelemetry.ts" diff --git a/repos/effect/packages/ai/openai-compat/src/internal/errors.ts b/repos/effect/packages/ai/openai-compat/src/internal/errors.ts new file mode 100644 index 0000000..2636d00 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/internal/errors.ts @@ -0,0 +1,327 @@ +import * as Arr from "effect/Array" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as SchemaTransformation from "effect/SchemaTransformation" +import * as String from "effect/String" +import * as AiError from "effect/unstable/ai/AiError" +import type * as Response from "effect/unstable/ai/Response" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type { OpenAiErrorMetadata } from "../OpenAiError.ts" + +/** @internal */ +export const OpenAiErrorBody = Schema.Struct({ + error: Schema.Struct({ + message: Schema.String, + type: Schema.optional(Schema.NullOr(Schema.String)), + status: Schema.optional(Schema.NullOr(Schema.String)), + param: Schema.optional(Schema.NullOr(Schema.String)), + code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Number]))) + }) +}) +const OpenAiErrorBodyJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Union([ + OpenAiErrorBody, + Schema.NonEmptyArray(OpenAiErrorBody).pipe( + Schema.decodeTo( + Schema.toType(OpenAiErrorBody), + SchemaTransformation.transform({ + decode: Arr.headNonEmpty, + encode: (item) => [item] + }) + ) + ) +]))) + +/** @internal */ +export const mapSchemaError = dual< + (method: string) => (error: Schema.SchemaError) => AiError.AiError, + (error: Schema.SchemaError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenAiClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + })) + +/** @internal */ +export const mapHttpClientError = dual< + (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, + (error: HttpClientError.HttpClientError, method: string) => Effect.Effect +>(2, (error, method) => { + const reason = error.reason + switch (reason._tag) { + case "TransportError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "TransportError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "EncodeError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "EncodeError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "InvalidUrlError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "InvalidUrlError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "StatusCodeError": { + return mapStatusCodeError(reason, method) + } + case "DecodeError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Failed to decode response" + }) + })) + } + case "EmptyBodyError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Response body was empty" + }) + })) + } + } +}) + +/** @internal */ +const mapStatusCodeError = Effect.fnUntraced(function*( + error: HttpClientError.StatusCodeError, + method: string +) { + const { request, response, description } = error + const status = response.status + const headers = response.headers as Record + const requestId = headers["x-request-id"] + + let body = yield* response.text.pipe( + Effect.catchCause(() => Effect.succeed(description?.startsWith("{") ? description : undefined)) + ) + const decoded = OpenAiErrorBodyJson(body) + + const reason = mapStatusCodeToReason({ + status, + headers, + message: Option.isSome(decoded) ? decoded.value.error.message : undefined, + http: buildHttpContext({ request, response, body }), + metadata: { + errorCode: Option.isSome(decoded) ? decoded.value.error.code?.toString() ?? null : null, + errorType: decoded.pipe( + Option.flatMapNullishOr((d) => d.error.type ?? d.error.status), + Option.map(String.toLowerCase), + Option.getOrNull + ), + requestId: requestId ?? null + } + }) + + return yield* AiError.make({ module: "OpenAiClient", method, reason }) +}) + +/** @internal */ +export const parseRateLimitHeaders = (headers: Record) => { + const retryAfterRaw = headers["retry-after"] + let retryAfter: Duration.Duration | undefined + if (retryAfterRaw !== undefined) { + const parsed = Number.parse(retryAfterRaw) + if (Option.isSome(parsed)) { + retryAfter = Duration.seconds(parsed.value) + } + } + const remainingRaw = headers["x-ratelimit-remaining-requests"] + const remaining = remainingRaw !== undefined ? Option.getOrNull(Number.parse(remainingRaw)) : null + return { + retryAfter, + limit: headers["x-ratelimit-limit-requests"] ?? null, + remaining, + resetRequests: headers["x-ratelimit-reset-requests"] ?? null, + resetTokens: headers["x-ratelimit-reset-tokens"] ?? null + } +} + +/** @internal */ +export const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +/** @internal */ +export const buildHttpContext = (params: { + readonly request: HttpClientRequest.HttpClientRequest + readonly response?: HttpClientResponse.HttpClientResponse + readonly body?: string | undefined +}): typeof AiError.HttpContext.Type => ({ + request: buildHttpRequestDetails(params.request), + response: params.response !== undefined + ? { + status: params.response.status, + headers: Redactable.redact(params.response.headers) as Record + } + : undefined, + body: params.body +}) + +const buildInvalidRequestDescription = (params: { + readonly status: number + readonly message: string | undefined + readonly method: string + readonly url: string + readonly errorCode: string | null + readonly errorType: string | null + readonly requestId: string | null + readonly body: string | undefined +}): string => { + const parts: Array = [] + + if (params.message) { + parts.push(params.message) + } else { + parts.push(`HTTP ${params.status}`) + } + + parts.push(`(${params.method} ${params.url})`) + + if (params.errorCode) { + parts.push(`[code: ${params.errorCode}]`) + } else if (params.errorType) { + parts.push(`[type: ${params.errorType}]`) + } + + if (params.requestId) { + parts.push(`[requestId: ${params.requestId}]`) + } + + if (!params.message && params.body) { + const truncated = params.body.length > 200 + ? params.body.slice(0, 200) + "..." + : params.body + parts.push(`Response: ${truncated}`) + } + + return parts.join(" ") +} + +/** @internal */ +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { + readonly status: number + readonly headers: Record + readonly message: string | undefined + readonly metadata: OpenAiErrorMetadata + readonly http: typeof AiError.HttpContext.Type +}): AiError.AiErrorReason => { + const invalidRequestDescription = buildInvalidRequestDescription({ + status, + message, + method: http.request.method, + url: http.request.url, + errorCode: metadata.errorCode, + errorType: metadata.errorType, + requestId: metadata.requestId, + body: http.body + }) + + switch (status) { + case 400: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 401: + return new AiError.AuthenticationError({ + kind: "InvalidKey", + metadata, + http + }) + case 403: + return new AiError.AuthenticationError({ + kind: "InsufficientPermissions", + metadata, + http + }) + case 404: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 409: + case 422: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 429: { + if ( + metadata.errorCode === "insufficient_quota" || + metadata.errorType === "insufficient_quota" || + metadata.errorType?.includes("quota") || + metadata.errorType?.includes("exhausted") + ) { + return new AiError.QuotaExhaustedError({ + metadata: { openai: metadata }, + http + }) + } + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + return new AiError.RateLimitError({ + retryAfter, + metadata: { + openai: { + ...metadata, + ...rateLimitMetadata + } + }, + http + }) + } + default: + if (status >= 500) { + return new AiError.InternalProviderError({ + description: message ?? "Server error", + metadata, + http + }) + } + return new AiError.UnknownError({ + description: message, + metadata, + http + }) + } +} diff --git a/repos/effect/packages/ai/openai-compat/src/internal/utilities.ts b/repos/effect/packages/ai/openai-compat/src/internal/utilities.ts new file mode 100644 index 0000000..a67365d --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/src/internal/utilities.ts @@ -0,0 +1,24 @@ +import type * as Response from "effect/unstable/ai/Response" + +const finishReasonMap: Record = { + content_filter: "content-filter", + function_call: "tool-calls", + length: "length", + stop: "stop", + tool_calls: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string | null | undefined, + hasToolCalls: boolean +): Response.FinishReason => { + if (finishReason == null) { + return hasToolCalls ? "tool-calls" : "stop" + } + const reason = finishReasonMap[finishReason] + if (reason == null) { + return hasToolCalls ? "tool-calls" : "unknown" + } + return reason +} diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts b/repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts new file mode 100644 index 0000000..2d89ffb --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts @@ -0,0 +1,474 @@ +import * as OpenAiClient from "@effect/ai-openai-compat/OpenAiClient" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Stream } from "effect" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenAiClient", () => { + describe("request behavior", () => { + it.effect("sets auth and OpenAI headers on /chat/completions requests", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key"), + apiUrl: "https://compat.example.test/v1", + organizationId: Redacted.make("org_123"), + projectId: Redacted.make("proj_456") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) + }) + )) + ) + + yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + assert.isTrue(capturedRequest.url.endsWith("/chat/completions")) + assert.isTrue(capturedRequest.url.startsWith("https://compat.example.test/v1")) + assert.strictEqual(capturedRequest.headers["authorization"], "Bearer sk-test-key") + assert.strictEqual(capturedRequest.headers["openai-organization"], "org_123") + assert.strictEqual(capturedRequest.headers["openai-project"], "proj_456") + + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.messages[0]?.role, "user") + assert.strictEqual(body.messages[0]?.content, "hello") + })) + + it.effect("passes custom chat-completions request properties through", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) + }) + )) + ) + + yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }], + provider_feature: { + enabled: true + } + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(body.provider_feature, { + enabled: true + }) + })) + + it.effect("uses /embeddings path and decodes permissive embedding payloads", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key"), + apiUrl: "https://compat.example.test/v1" + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, 200, { + data: [{ + embedding: "YmFzZTY0LWRhdGE=", + index: 0, + object: "embedding", + vendor_payload: { future_field: true } + }], + model: "my-custom-embedding-model", + object: "list", + usage: { + prompt_tokens: 5, + total_tokens: 5 + }, + unknown_top_level: true + })) + }) + )) + ) + + const embedding = yield* client.createEmbedding({ + model: "my-custom-embedding-model", + input: "embed this" + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + assert.isTrue(capturedRequest.url.endsWith("/embeddings")) + assert.strictEqual(embedding.model, "my-custom-embedding-model") + assert.strictEqual(embedding.data[0]?.index, 0) + assert.strictEqual(typeof embedding.data[0]?.embedding, "string") + })) + + it.effect("sets stream=true for createResponseStream and returns chat chunks", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_test_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + future_provider_field: { accepted: true }, + choices: [{ + index: 0, + delta: { content: "Hello" }, + finish_reason: null + }] + }, + { + id: "chatcmpl_test_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + usage: { + prompt_tokens: 4, + completion_tokens: 2, + total_tokens: 6, + prompt_tokens_details: { cached_tokens: 1 }, + completion_tokens_details: { reasoning_tokens: 1 } + }, + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }] + }, + "[DONE]" + ])) + }) + )) + ) + + const eventsChunk = yield* client.createResponseStream({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }).pipe( + Effect.flatMap(([_, stream]) => Stream.runCollect(stream)) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + assert.strictEqual(body.stream, true) + assert.strictEqual(body.stream_options.include_usage, true) + assert.isTrue(capturedRequest.url.endsWith("/chat/completions")) + + const events = globalThis.Array.from(eventsChunk) + const firstEvent = events[0] + const secondEvent = events[1] + assert.isTrue(typeof firstEvent === "object") + assert.isTrue(typeof secondEvent === "object") + if ( + typeof firstEvent !== "object" || firstEvent === null || typeof secondEvent !== "object" || + secondEvent === null + ) { + return + } + assert.strictEqual(firstEvent.id, "chatcmpl_test_1") + assert.strictEqual(secondEvent.id, "chatcmpl_test_1") + assert.strictEqual(events[2], "[DONE]") + })) + + it.effect("passes chat-completions tool_choice payload through unchanged", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) + }) + )) + ) + + yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }], + tool_choice: { + type: "function", + function: { + name: "TestTool" + } + }, + tools: [{ + type: "function", + function: { + name: "TestTool", + parameters: { + type: "object", + additionalProperties: false, + properties: { + input: { type: "string" } + }, + required: ["input"] + } + } + }] + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(body.tool_choice, { type: "function", function: { name: "TestTool" } }) + })) + + it.effect("accepts assistant tool-call and tool result chat history", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) + }) + )) + ) + + yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [ + { + role: "assistant", + content: null, + tool_calls: [{ + id: "patch_call_1", + type: "function", + function: { + name: "apply_patch", + arguments: JSON.stringify({ + call_id: "patch_call_1", + operation: { + type: "delete_file", + path: "src/obsolete.ts" + } + }) + } + }] + }, + { + role: "tool", + tool_call_id: "patch_call_1", + content: "deleted" + } + ] + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + const assistantMessages = body.messages.filter((message: any) => message.role === "assistant") + const patchMessage = assistantMessages.find((message: any) => + message.tool_calls?.[0]?.function?.name === "apply_patch" + ) + + assert.isDefined(patchMessage) + + assert.strictEqual(patchMessage.tool_calls[0].id, "patch_call_1") + assert.deepStrictEqual(JSON.parse(patchMessage.tool_calls[0].function.arguments), { + call_id: "patch_call_1", + operation: { + type: "delete_file", + path: "src/obsolete.ts" + } + }) + + const toolMessages = body.messages.filter((message: any) => message.role === "tool") + const patchOutput = toolMessages.find((message: any) => message.tool_call_id === "patch_call_1") + assert.isDefined(patchOutput) + assert.strictEqual(patchOutput.content, "deleted") + })) + }) + + describe("error mapping", () => { + it.effect("maps 400 responses to InvalidRequestError", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, 400, { + error: { + message: "Bad request", + type: "invalid_request_error", + code: null + } + })) + ) + )) + ) + + const error = yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }).pipe(Effect.flip) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.method, "createResponse") + assert.strictEqual(error.reason._tag, "InvalidRequestError") + })) + + it.effect("maps insufficient quota errors to QuotaExhaustedError", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-key") + }).pipe( + Effect.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, 429, { + error: { + message: "You exceeded your current quota", + type: "insufficient_quota", + code: "insufficient_quota" + } + })) + ) + )) + ) + + const error = yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }).pipe(Effect.flip) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.method, "createResponse") + assert.strictEqual(error.reason._tag, "QuotaExhaustedError") + })) + }) +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const makeChatCompletion = () => ({ + id: "chatcmpl_test_1", + object: "chat.completion", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Hello" + } + }], + usage: { + prompt_tokens: 1, + completion_tokens: 1, + total_tokens: 2 + } +}) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + status: number, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status, + headers: { + "content-type": "application/json" + } + }) + ) + +const sseResponse = ( + request: HttpClientRequest.HttpClientRequest, + events: ReadonlyArray +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(toSseBody(events), { + status: 200, + headers: { + "content-type": "text/event-stream" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const toSseBody = (events: ReadonlyArray): string => + events.map((event) => { + if (typeof event === "string") { + return `data: ${event}\n\n` + } + return `data: ${JSON.stringify(event)}\n\n` + }).join("") diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts b/repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts new file mode 100644 index 0000000..b80e664 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts @@ -0,0 +1,216 @@ +import { OpenAiClient, OpenAiEmbeddingModel } from "@effect/ai-openai-compat" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted } from "effect" +import { EmbeddingModel } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenAiEmbeddingModel", () => { + it.effect("model provides dimensions service", () => + Effect.gen(function*() { + const dimensions = yield* EmbeddingModel.Dimensions + assert.strictEqual(dimensions, 1536) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.model("text-embedding-3-small", { dimensions: 1536 })), + Effect.provideService(OpenAiClient.OpenAiClient, noopOpenAiClient) + )) + + it.effect("reorders embeddings by provider index", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + data: [ + { index: 1, embedding: [20, 21] }, + { index: 0, embedding: [10, 11] } + ], + model: "text-embedding-3-small", + usage: { + prompt_tokens: 7, + total_tokens: 7 + } + })) + }) + )) + ) + + const response = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["first", "second"]) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.deepStrictEqual(response.embeddings.map((embedding) => embedding.vector), [[10, 11], [20, 21]]) + assert.strictEqual(response.usage.inputTokens, 7) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.model, "text-embedding-3-small") + assert.deepStrictEqual(requestBody.input, ["first", "second"]) + })) + + it.effect("merges config and applies withConfigOverride precedence", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + data: [{ index: 0, embedding: [1, 2, 3] }], + model: "override-model" + })) + }) + )) + ) + + yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + yield* model.embed("hello") + }).pipe( + OpenAiEmbeddingModel.withConfigOverride({ + model: "override-model", + dimensions: 1024, + user: "request-user" + }), + Effect.provide(OpenAiEmbeddingModel.layer({ + model: "base-model", + config: { + dimensions: 256, + user: "provider-user" + } + })), + Effect.provide(clientLayer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.model, "override-model") + assert.strictEqual(requestBody.dimensions, 1024) + assert.strictEqual(requestBody.user, "request-user") + assert.deepStrictEqual(requestBody.input, ["hello"]) + })) + + it.effect("fails with InvalidOutputError when provider returns duplicate indices", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [ + { index: 0, embedding: [1] }, + { index: 0, embedding: [2] } + ], + model: "text-embedding-3-small", + usage: { + prompt_tokens: 2, + total_tokens: 2 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["a", "b"]).pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when provider returns non-vector embedding payload", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [{ index: 0, embedding: "AQID" }], + model: "text-embedding-3-small", + usage: { + prompt_tokens: 1, + total_tokens: 1 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embed("a").pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const noopOpenAiClient: OpenAiClient.Service = { + client: undefined as unknown as OpenAiClient.Service["client"], + createResponse: () => Effect.die(new Error("noop")), + createResponseStream: () => Effect.die(new Error("noop")), + createEmbedding: () => Effect.die(new Error("noop")) +} diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts new file mode 100644 index 0000000..e6c26fb --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -0,0 +1,1558 @@ +import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai-compat" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" +import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenAiLanguageModel", () => { + describe("generateText", () => { + it.effect("sends model in request and decodes text output", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Hello, compat!" + } + }] + }) + )) + }) + )) + ) + + const result = yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.strictEqual(result.text, "Hello, compat!") + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.model, "gpt-4o-mini") + assert.strictEqual(requestBody.messages[0]?.content, "hello") + })) + + it.effect("forwards reasoning config to chat completions request", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Done" + } + }] + }) + )) + }) + )) + ) + + yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-5", { + reasoning: { + effort: "medium", + summary: "auto" + } + })), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(requestBody.reasoning, { + effort: "medium", + summary: "auto" + }) + })) + + it.effect("forwards custom model config properties to chat completions request", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Done" + } + }] + }) + )) + }) + )) + ) + + yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { + vendor_setting: { + mode: "strict" + } + })), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(requestBody.vendor_setting, { + mode: "strict" + }) + })) + + it.effect("preserves multimodal user content order in chat payload", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "done" + } + }] + }) + )) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.textPart({ text: "first text" }), + Prompt.filePart({ + mediaType: "image/png", + data: new URL("https://example.com/image.png") + }), + Prompt.textPart({ text: "second text" }) + ] + }]) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + const content = requestBody.messages[0]?.content + assert.isTrue(Array.isArray(content)) + assert.deepStrictEqual(content, [ + { + type: "text", + text: "first text" + }, + { + type: "image_url", + image_url: { + url: "https://example.com/image.png", + detail: "auto" + } + }, + { + type: "text", + text: "second text" + } + ]) + })) + + it.effect("normalizes empty assistant message content to an empty string", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "done" + } + }] + }) + )) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "hello" }, + { + role: "assistant", + content: [Prompt.textPart({ text: "" })] + }, + { role: "user", content: "continue" } + ]) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + const assistantMessage = requestBody.messages.find((message: any) => message.role === "assistant") + assert.isDefined(assistantMessage) + assert.strictEqual(assistantMessage.content, "") + assert.isUndefined(assistantMessage.tool_calls) + })) + + it.effect("maps function_call output to tool-call part and sends function tool schema", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "tool_calls", + message: { + role: "assistant", + content: null, + tool_calls: [{ + id: "call_1", + type: "function", + function: { + name: "TestTool", + arguments: JSON.stringify({ input: "hello" }) + } + }] + } + }] + }) + )) + }) + )) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "use the tool", + toolkit: TestToolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(TestToolkitLayer), + Effect.provide(layer) + ) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.name, "TestTool") + assert.deepStrictEqual(toolCall.params, { input: "hello" }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + const functionTool = requestBody.tools.find((tool: any) => tool.type === "function") + assert.isDefined(functionTool) + assert.strictEqual(functionTool.function.name, "TestTool") + assert.strictEqual(functionTool.function.strict, true) + })) + + it.effect("converts dynamic tools to function type", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Done" + } + }] + }) + )) + }) + )) + ) + + const inputSchema = { + type: "object", + properties: { + query: { type: "string" }, + limit: { type: "number" } + }, + required: ["query"], + additionalProperties: false + } as const + + const DynamicTool = Tool.dynamic("DynamicTool", { + description: "A dynamic tool", + parameters: inputSchema + }) + + yield* LanguageModel.generateText({ + prompt: "use dynamic tool", + toolkit: Toolkit.make(DynamicTool), + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + const functionTool = requestBody.tools?.find((tool: any) => + tool.type === "function" && tool.function?.name === "DynamicTool" + ) + assert.isDefined(functionTool) + assert.strictEqual(functionTool.function.description, "A dynamic tool") + assert.deepStrictEqual(functionTool.function.parameters, inputSchema) + })) + + it.effect("maps provider apply_patch function call back to custom provider-defined tool", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "tool_calls", + message: { + role: "assistant", + content: null, + tool_calls: [{ + id: "call_1", + type: "function", + function: { + name: "apply_patch", + arguments: JSON.stringify({ + call_id: "call_1", + operation: { + type: "delete_file", + path: "src/obsolete.ts" + } + }) + } + }] + } + }] + }) + )) + }) + )) + ) + + const toolkit = Toolkit.make(CompatApplyPatchTool({})) + const toolkitLayer = toolkit.toLayer({ + CompatApplyPatch: () => + Effect.succeed({ + status: "completed", + output: "deleted" + }) + }) + + const result = yield* LanguageModel.generateText({ + prompt: "delete src/obsolete.ts", + toolkit, + toolChoice: { tool: "CompatApplyPatch" }, + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.name, "CompatApplyPatch") + assert.deepStrictEqual(toolCall.params, { + call_id: "call_1", + operation: { + type: "delete_file", + path: "src/obsolete.ts" + } + }) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + + const functionTool = requestBody.tools.find((tool: any) => tool.type === "function") + assert.isDefined(functionTool) + assert.strictEqual(functionTool.function.name, "apply_patch") + assert.deepStrictEqual(requestBody.tool_choice, { + type: "function", + function: { + name: "apply_patch" + } + }) + })) + + it.effect("decodes usage when token detail fields are absent", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "Hello" + } + }], + usage: { + prompt_tokens: 4, + completion_tokens: 5, + total_tokens: 9, + provider_future_field: true + } + }) + )) + ) + )) + ) + + const result = yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const finish = result.content.find((part) => part.type === "finish") + assert.isDefined(finish) + if (finish?.type !== "finish") { + return + } + + assert.deepStrictEqual(finish.usage.inputTokens, { + uncached: 4, + total: 4, + cacheRead: 0, + cacheWrite: undefined + }) + assert.deepStrictEqual(finish.usage.outputTokens, { + total: 5, + text: 5, + reasoning: 0 + }) + })) + + it.effect("surfaces reasoning from non-streaming responses", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + reasoning: "I should greet the user.", + content: "Hello!" + } + }] + }) + )) + ) + )) + ) + + const result = yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const reasoning = result.content.find((part) => part.type === "reasoning") + assert.isDefined(reasoning) + if (reasoning?.type !== "reasoning") { + return + } + assert.strictEqual(reasoning.text, "I should greet the user.") + assert.strictEqual(result.text, "Hello!") + })) + + it.effect("surfaces reasoning_content from non-streaming responses", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + reasoning_content: "I should greet the user.", + content: "Hello!" + } + }] + }) + )) + ) + )) + ) + + const result = yield* LanguageModel.generateText({ prompt: "hello" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const reasoning = result.content.find((part) => part.type === "reasoning") + assert.isDefined(reasoning) + if (reasoning?.type !== "reasoning") { + return + } + assert.strictEqual(reasoning.text, "I should greet the user.") + assert.strictEqual(result.text, "Hello!") + })) + }) + + describe("generateObject", () => { + it.effect("uses json_schema format for structured output", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: JSON.stringify({ name: "Ada", age: 37 }) + } + }] + }) + )) + }) + )) + ) + + const person = yield* LanguageModel.generateObject({ + prompt: "Return a person", + schema: Schema.Struct({ + name: Schema.String, + age: Schema.Number + }) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.strictEqual(person.value.name, "Ada") + assert.strictEqual(person.value.age, 37) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.response_format.type, "json_schema") + assert.strictEqual(requestBody.response_format.json_schema.strict, true) + })) + + it.effect("uses OpenAI codec transformer for optional structured fields", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: JSON.stringify({ name: "Ada", nickname: null }) + } + }] + }) + )) + }) + )) + ) + + const person = yield* LanguageModel.generateObject({ + prompt: "Return a person", + schema: Schema.Struct({ + name: Schema.String, + nickname: Schema.optionalKey(Schema.String) + }) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + assert.strictEqual(person.value.name, "Ada") + assert.isUndefined(person.value.nickname) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(requestBody.response_format.json_schema.schema.required, ["name", "nickname"]) + assert.deepStrictEqual(requestBody.response_format.json_schema.schema.properties.nickname, { + anyOf: [{ type: "string" }, { type: "null" }] + }) + })) + }) + + describe("streamText", () => { + it.effect("handles chat completion stream chunks", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_test123", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }] + }, + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ prompt: "test" }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const parts = Array.from(partsChunk) + + assert.isTrue(parts.some((part) => part.type === "response-metadata")) + + const finish = parts.find((part) => part.type === "finish") + assert.isDefined(finish) + if (finish?.type === "finish") { + assert.strictEqual(finish.reason, "stop") + } + })) + + it.effect("maps local shell stream tool calls to local_shell call outputs", () => + Effect.gen(function*() { + const capturedRequests = yield* Ref.make>([]) + const requestCount = yield* Ref.make(0) + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(capturedRequests, (requests) => [...requests, request]) + const index = yield* Ref.getAndUpdate(requestCount, (value) => value + 1) + + if (index === 0) { + return sseResponse(request, [makeLocalShellChunk(), "[DONE]"]) + } + + return jsonResponse(request, makeChatCompletion()) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, httpClient)) + ) + + const toolkit = Toolkit.make(CompatLocalShellTool({})) + const toolkitLayer = toolkit.toLayer({ + CompatLocalShell: () => Effect.succeed("done") + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Run pwd", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const toolCall = globalThis.Array.from(partsChunk).find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.name, "CompatLocalShell") + assert.deepStrictEqual(toolCall.params, { action: localShellAction }) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Run pwd" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: toolCall.id, + name: toolCall.name, + params: { action: localShellAction }, + providerExecuted: false, + options: { + openai: { + itemId: "ls_call_1" + } + } + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: toolCall.id, + name: toolCall.name, + isFailure: false, + result: "done" + })] + } + ]), + toolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const requests = yield* Ref.get(capturedRequests) + const followUpRequest = requests[1] + assert.isDefined(followUpRequest) + if (followUpRequest === undefined) { + return + } + + const followUpBody = yield* getRequestBody(followUpRequest) + + const localShellCall = followUpBody.messages.find((item: any) => + item.role === "assistant" && item.tool_calls?.[0]?.function?.name === "local_shell" + ) + assert.isDefined(localShellCall) + assert.strictEqual(localShellCall.content, null) + assert.strictEqual(localShellCall.tool_calls[0].id, toolCall.id) + + const localShellOutput = followUpBody.messages.find((item: any) => item.role === "tool") + assert.isDefined(localShellOutput) + assert.strictEqual(localShellOutput.tool_call_id, toolCall.id) + assert.strictEqual(localShellOutput.content, "done") + })) + + it.effect("maps apply_patch stream tool calls to custom provider-defined tool", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_apply_patch_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + id: "patch_call_1", + type: "function", + function: { + name: "apply_patch", + arguments: JSON.stringify({ + call_id: "patch_call_1", + operation: { + type: "delete_file", + path: "src/legacy.ts" + } + }) + } + }] + }, + finish_reason: "tool_calls" + }] + }, + "[DONE]" + ])) + ) + )) + ) + + const toolkit = Toolkit.make(CompatApplyPatchTool({})) + const toolkitLayer = toolkit.toLayer({ + CompatApplyPatch: () => + Effect.succeed({ + status: "completed", + output: "deleted" + }) + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Delete src/legacy.ts", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const toolCall = globalThis.Array.from(partsChunk).find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.name, "CompatApplyPatch") + assert.deepStrictEqual(toolCall.params, { + call_id: "patch_call_1", + operation: { + type: "delete_file", + path: "src/legacy.ts" + } + }) + })) + + it.effect("preserves fragmented stream tool call ids and names", () => + Effect.gen(function*() { + const expectedParams = { + call_id: "patch_call_2", + operation: { + type: "delete_file", + path: "src/fragmented.ts" + } + } + const toolArguments = JSON.stringify(expectedParams) + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_apply_patch_fragmented_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + id: "patch_call_2", + type: "function", + function: { + name: "apply_patch", + arguments: toolArguments.slice(0, 24) + } + }] + }, + finish_reason: null + }] + }, + { + id: "chatcmpl_apply_patch_fragmented_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + function: { + arguments: toolArguments.slice(24, 48) + } + }] + }, + finish_reason: null + }] + }, + { + id: "chatcmpl_apply_patch_fragmented_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + function: { + arguments: toolArguments.slice(48) + } + }] + }, + finish_reason: "tool_calls" + }] + }, + "[DONE]" + ])) + ) + )) + ) + + const toolkit = Toolkit.make(CompatApplyPatchTool({})) + const toolkitLayer = toolkit.toLayer({ + CompatApplyPatch: () => + Effect.succeed({ + status: "completed", + output: "deleted" + }) + }) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Delete src/fragmented.ts", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + + const start = parts.find((part) => part.type === "tool-params-start" && part.id === "patch_call_2") + assert.isDefined(start) + if (start?.type !== "tool-params-start") { + return + } + assert.strictEqual(start.name, "CompatApplyPatch") + + assert.deepStrictEqual(decodeToolParamsFromStream(parts, "patch_call_2"), expectedParams) + + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + + assert.strictEqual(toolCall.id, "patch_call_2") + assert.strictEqual(toolCall.name, "CompatApplyPatch") + assert.deepStrictEqual(toolCall.params, expectedParams) + })) + + it.effect("streams known events and ignores unknown ones", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const events = [ + { + id: "chatcmpl_stream_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { content: "Hello" }, + finish_reason: null + }] + }, + { + id: "chatcmpl_stream_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }], + usage: { + prompt_tokens: 10, + completion_tokens: 7, + total_tokens: 17, + prompt_tokens_details: { + cached_tokens: 3 + }, + completion_tokens_details: { + reasoning_tokens: 2 + } + }, + provider_future_field: { accepted: true } + }, + "[DONE]" + ] + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(sseResponse(request, events)) + }) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ prompt: "hello" }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const metadata = parts.find((part) => part.type === "response-metadata") + const finish = parts.find((part) => part.type === "finish") + const deltas = parts.filter((part) => part.type === "text-delta") + + assert.isDefined(metadata) + assert.isDefined(finish) + assert.strictEqual(deltas.length, 1) + assert.strictEqual(deltas[0]?.delta, "Hello") + if (finish?.type === "finish") { + assert.strictEqual(finish.reason, "stop") + assert.deepStrictEqual(finish.usage.inputTokens, { + uncached: 7, + total: 10, + cacheRead: 3, + cacheWrite: undefined + }) + assert.deepStrictEqual(finish.usage.outputTokens, { + total: 7, + text: 5, + reasoning: 2 + }) + } + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.stream, true) + assert.isTrue(capturedRequest.url.endsWith("/chat/completions")) + })) + + it.effect("assembles streamed tool args when continuation fragments have function.name: null", () => + Effect.gen(function*() { + // Some OpenAI-compatible providers (e.g. Fireworks) only send the tool + // name on the first fragment and `function.name: null` on every + // continuation. The argument fragments live on those continuations, so + // they must not be dropped during chunk validation. + const chunk = (fnDelta: Record) => ({ + id: "chatcmpl_null_name_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { tool_calls: [{ index: 0, id: "call_1", type: "function", function: fnDelta }] } + }] + }) + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + chunk({ name: "TestTool", arguments: "" }), + chunk({ name: null, arguments: "{\"in" }), + chunk({ name: null, arguments: "put\":\"hel" }), + chunk({ name: null, arguments: "lo\"}" }), + { + id: "chatcmpl_null_name_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] + }, + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "use the tool", + toolkit: TestToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(TestToolkitLayer), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + const paramsDeltas = parts.filter((part) => part.type === "tool-params-delta") + assert.isAbove(paramsDeltas.length, 0) + + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + assert.strictEqual(toolCall.name, "TestTool") + assert.deepStrictEqual(toolCall.params, { input: "hello" }) + })) + + it.effect("emits reasoning lifecycle parts for delta.reasoning", () => + Effect.gen(function*() { + const chunk = (delta: Record, finishReason: string | null = null) => ({ + id: "chatcmpl_reasoning_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ index: 0, delta, finish_reason: finishReason }] + }) + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + chunk({ reasoning: "Let me think" }), + chunk({ reasoning: " about this." }), + chunk({ content: "Hello" }), + chunk({ content: " there" }, "stop"), + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ prompt: "test" }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + assert.deepStrictEqual(parts.map((part) => part.type), [ + "response-metadata", + "reasoning-start", + "reasoning-delta", + "reasoning-delta", + "reasoning-end", + "text-start", + "text-delta", + "text-delta", + "text-end", + "finish" + ]) + + const reasoningText = parts + .flatMap((part) => part.type === "reasoning-delta" ? [part.delta] : []) + .join("") + assert.strictEqual(reasoningText, "Let me think about this.") + + const reasoningIds = parts.flatMap((part) => + part.type === "reasoning-start" || part.type === "reasoning-delta" || part.type === "reasoning-end" + ? [part.id] + : [] + ) + assert.strictEqual(new Set(reasoningIds).size, 1) + + const textIds = parts.flatMap((part) => part.type === "text-start" ? [part.id] : []) + assert.notStrictEqual(reasoningIds[0], textIds[0]) + })) + + it.effect("emits reasoning lifecycle parts for delta.reasoning_content", () => + Effect.gen(function*() { + const chunk = (delta: Record, finishReason: string | null = null) => ({ + id: "chatcmpl_reasoning_2", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ index: 0, delta, finish_reason: finishReason }] + }) + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + chunk({ reasoning_content: "Thinking..." }), + chunk({ content: "Answer." }, "stop"), + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ prompt: "test" }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + assert.deepStrictEqual(parts.map((part) => part.type), [ + "response-metadata", + "reasoning-start", + "reasoning-delta", + "reasoning-end", + "text-start", + "text-delta", + "text-end", + "finish" + ]) + + const reasoningText = parts + .flatMap((part) => part.type === "reasoning-delta" ? [part.delta] : []) + .join("") + assert.strictEqual(reasoningText, "Thinking...") + })) + + it.effect("closes an open reasoning part at stream end", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_reasoning_3", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ index: 0, delta: { reasoning: "Only thoughts." }, finish_reason: "stop" }] + }, + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ prompt: "test" }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + assert.deepStrictEqual(parts.map((part) => part.type), [ + "response-metadata", + "reasoning-start", + "reasoning-delta", + "reasoning-end", + "finish" + ]) + })) + }) + + describe("config", () => { + it.effect("does not leak library-only fields into request body", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, makeChatCompletion())) + }) + )) + ) + + yield* LanguageModel.generateText({ prompt: "test" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { + fileIdPrefixes: ["file-"], + strictJsonSchema: false, + temperature: 0.5 + })), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) return + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.fileIdPrefixes, undefined) + assert.strictEqual(requestBody.strictJsonSchema, undefined) + assert.strictEqual(requestBody.temperature, 0.5) + })) + }) +}) + +const TestTool = Tool.make("TestTool", { + description: "A test tool", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }) +}) + +const TestToolkit = Toolkit.make(TestTool) + +const TestToolkitLayer = TestToolkit.toLayer({ + TestTool: ({ input }) => Effect.succeed({ output: input }) +}) + +const CompatApplyPatchTool = Tool.providerDefined({ + id: "compat.apply_patch", + customName: "CompatApplyPatch", + providerName: "apply_patch", + requiresHandler: true, + parameters: Schema.Struct({ + call_id: Schema.String, + operation: Schema.Any + }), + success: Schema.Struct({ + status: Schema.Literals(["completed", "failed"]), + output: Schema.optionalKey(Schema.NullOr(Schema.String)) + }) +}) + +const localShellAction = { + type: "exec", + command: ["pwd"], + env: {} +} + +const CompatLocalShellTool = Tool.providerDefined({ + id: "compat.local_shell", + customName: "CompatLocalShell", + providerName: "local_shell", + requiresHandler: true, + parameters: Schema.Struct({ + action: Schema.Any + }), + success: Schema.String +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const makeChatCompletion = (overrides: Record = {}) => ({ + id: "chatcmpl_test_1", + object: "chat.completion", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: "" + } + }], + ...overrides +}) + +const makeLocalShellChunk = () => ({ + id: "chatcmpl_local_shell_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + id: "local_shell_call_1", + type: "function", + function: { + name: "local_shell", + arguments: JSON.stringify({ action: localShellAction }) + } + }] + }, + finish_reason: "tool_calls" + }] +}) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const sseResponse = ( + request: HttpClientRequest.HttpClientRequest, + events: ReadonlyArray +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(toSseBody(events), { + status: 200, + headers: { + "content-type": "text/event-stream" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const decodeToolParamsFromStream = ( + parts: ReadonlyArray, + toolCallId: string +): Record => { + const start = parts.find((part) => part.type === "tool-params-start" && part.id === toolCallId) + const end = parts.find((part) => part.type === "tool-params-end" && part.id === toolCallId) + assert.isDefined(start) + assert.isDefined(end) + + const deltas = parts + .filter((part) => part.type === "tool-params-delta" && part.id === toolCallId) + .map((part) => part.delta) + .join("") + + return JSON.parse(deltas) as Record +} + +const toSseBody = (events: ReadonlyArray): string => + events.map((event) => { + if (typeof event === "string") { + return `data: ${event}\n\n` + } + return `data: ${JSON.stringify(event)}\n\n` + }).join("") diff --git a/repos/effect/packages/ai/openai-compat/tsconfig.json b/repos/effect/packages/ai/openai-compat/tsconfig.json new file mode 100644 index 0000000..19a2f5d --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts b/repos/effect/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts new file mode 100644 index 0000000..b87ba69 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts @@ -0,0 +1,26 @@ +import { OpenAiEmbeddingModel, OpenAiLanguageModel } from "@effect/ai-openai-compat" +import { describe, expect, it } from "tstyche" + +describe("OpenAI-compatible config", () => { + it("accepts custom language model properties while preserving known property types", () => { + expect(OpenAiLanguageModel.model).type.toBeCallableWith("model", { + temperature: 0.5, + vendor_setting: true + }) + expect(OpenAiLanguageModel.model).type.not.toBeCallableWith("model", { + temperature: "high" + }) + }) + + it("accepts custom embedding properties while preserving known property types", () => { + expect(OpenAiEmbeddingModel.model).type.toBeCallableWith("model", { + dimensions: 3, + user: "test-user", + vendor_setting: true + }) + expect(OpenAiEmbeddingModel.model).type.not.toBeCallableWith("model", { + dimensions: 3, + user: 1 + }) + }) +}) diff --git a/repos/effect/packages/ai/openai-compat/vitest.config.ts b/repos/effect/packages/ai/openai-compat/vitest.config.ts new file mode 100644 index 0000000..c8a52c1 --- /dev/null +++ b/repos/effect/packages/ai/openai-compat/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/openai/CHANGELOG.md b/repos/effect/packages/ai/openai/CHANGELOG.md new file mode 100644 index 0000000..74a06a5 --- /dev/null +++ b/repos/effect/packages/ai/openai/CHANGELOG.md @@ -0,0 +1,780 @@ +# @effect/ai-openai + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- [#2312](https://github.com/Effect-TS/effect-smol/pull/2312) [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e) Thanks @gcanti! - Validate `Schema.StructWithRest` fixed fields against rest index signatures at the type level so schemas cannot be constructed with incompatible decoded, encoded, or make shapes. This keeps `StructWithRest` types sound and updates the generated OpenAI conversation-items request schema to keep accepting arbitrary additional fields under the stricter validation. + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- [#2280](https://github.com/Effect-TS/effect-smol/pull/2280) [`fa07f9e`](https://github.com/Effect-TS/effect-smol/commit/fa07f9e7138bde3e9404aa91c22b41e273aba091) Thanks @tim-smart! - improve openai websocket error status + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- [#2279](https://github.com/Effect-TS/effect-smol/pull/2279) [`1fab4ac`](https://github.com/Effect-TS/effect-smol/commit/1fab4acc63818dece3a0e732b1a71843f0759f21) Thanks @tim-smart! - handle missing output array in openai responses + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- [#2224](https://github.com/Effect-TS/effect-smol/pull/2224) [`74d8f37`](https://github.com/Effect-TS/effect-smol/commit/74d8f37a05ce755ff09cff48f992cb80ce9e4769) Thanks @aniravi24! - Fix `OpenAIFile` schema decode failure on responses where `expires_at` and `status_details` are returned as literal `null`. The OpenAI files endpoint returns `null` (not omitted) for these fields when no expiration / status detail applies (e.g. uploads with `purpose: "user_data"`), but the upstream OpenAPI spec marks them only as optional. Codegen patches widen both fields to allow `null`, which now decodes cleanly via `OpenAiClient.createFile`, `retrieveFile`, `listFiles`, and any other endpoint returning the `OpenAIFile` shape. + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- [#2133](https://github.com/Effect-TS/effect-smol/pull/2133) [`5be0aaa`](https://github.com/Effect-TS/effect-smol/commit/5be0aaad694c9eb943a710eb4f896bc4c3fcae99) Thanks @Zelys-DFKH! - Fix `OpenAiLanguageModel` leaking library-only config fields (`fileIdPrefixes`, `strictJsonSchema`) into request body, causing OpenAI 400 errors. + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- [#2101](https://github.com/Effect-TS/effect-smol/pull/2101) [`953edef`](https://github.com/Effect-TS/effect-smol/commit/953edef2e1ade369e530017d64391281ef22f28f) Thanks @tim-smart! - don't omit reasoning from openai config + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- [#1933](https://github.com/Effect-TS/effect-smol/pull/1933) [`c5a7327`](https://github.com/Effect-TS/effect-smol/commit/c5a732746690185b8bb7935d0cf26a5ef21658e4) Thanks @IMax153! - Accept both `in-memory` and `in_memory` for OpenAI `prompt_cache_retention` schema fields. + +- [#1933](https://github.com/Effect-TS/effect-smol/pull/1933) [`c5a7327`](https://github.com/Effect-TS/effect-smol/commit/c5a732746690185b8bb7935d0cf26a5ef21658e4) Thanks @IMax153! - Fix OpenAI MCP tool call handling to keep the canonical `OpenAiMcp` tool name across response and stream paths, including approval flows. + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- [#2060](https://github.com/Effect-TS/effect-smol/pull/2060) [`aef2b1c`](https://github.com/Effect-TS/effect-smol/commit/aef2b1c0130a61a430f116465e4c200c51bcd9e2) Thanks @tim-smart! - add back openai reasoning types + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Minor Changes + +- [#2059](https://github.com/Effect-TS/effect-smol/pull/2059) [`6f73f92`](https://github.com/Effect-TS/effect-smol/commit/6f73f92291733dc1e970e222e63aba865183a072) Thanks @tim-smart! - Add a new public `OpenAiSchema` module with minimal local schemas for responses, streaming SSE events (including unknown-event fallback), and embeddings. + +### Patch Changes + +- [#2059](https://github.com/Effect-TS/effect-smol/pull/2059) [`6f73f92`](https://github.com/Effect-TS/effect-smol/commit/6f73f92291733dc1e970e222e63aba865183a072) Thanks @tim-smart! - Add a new `OpenAiClientGenerated` module that exposes the generated OpenAI client as a dedicated context service with `make`, `layer`, and `layerConfig` constructors. This provides a compatibility path for direct generated-client access while preserving existing auth, base URL, header, and `OpenAiConfig.transformClient` wiring. + +- [#2059](https://github.com/Effect-TS/effect-smol/pull/2059) [`6f73f92`](https://github.com/Effect-TS/effect-smol/commit/6f73f92291733dc1e970e222e63aba865183a072) Thanks @tim-smart! - Refactor `OpenAiClient` to the handwritten minimal-schema path so `client` now exposes the configured `HttpClient`, `createResponse` / `createResponseStream` / `createEmbedding` use `OpenAiSchema` request-response types, and websocket mode no longer depends on generated-client internals. + + Also migrate OpenAI language and embedding model request-response typing to `OpenAiSchema` and make embedding decoding explicitly reject non-vector (string/base64) payloads with `InvalidOutputError`. + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- [#1905](https://github.com/Effect-TS/effect-smol/pull/1905) [`fc1444d`](https://github.com/Effect-TS/effect-smol/commit/fc1444df6fb7223ed228650c5c927b1df54a8757) Thanks @tim-smart! - openai ws tweaks + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- [#1859](https://github.com/Effect-TS/effect-smol/pull/1859) [`a4809db`](https://github.com/Effect-TS/effect-smol/commit/a4809db80f65601d37ccafc13d773e55909c4e98) Thanks @lloydrichards! - add dynamic tooling for openai and openai-compact language models + +- [#1890](https://github.com/Effect-TS/effect-smol/pull/1890) [`4549175`](https://github.com/Effect-TS/effect-smol/commit/454917547e8c305707f0777f4efac7adaa924f00) Thanks @tim-smart! - reset openai websocket on error + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- [#1887](https://github.com/Effect-TS/effect-smol/pull/1887) [`f291344`](https://github.com/Effect-TS/effect-smol/commit/f2913443d99146a6b36d8437e50b94f7c03c1284) Thanks @tim-smart! - make defects a retryable network error in websocket mode + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- [#1807](https://github.com/Effect-TS/effect-smol/pull/1807) [`1bbd4d9`](https://github.com/Effect-TS/effect-smol/commit/1bbd4d9d617e683a7f338ddd40d11f5838c722bd) Thanks @tim-smart! - Handle streamed OpenAI function calls from `response.function_call_arguments.done` so tool calls are emitted even when `response.output_item.done` is missing. + +- [#1809](https://github.com/Effect-TS/effect-smol/pull/1809) [`126db2e`](https://github.com/Effect-TS/effect-smol/commit/126db2e38774c8ef4934321a70d39428ed4491ed) Thanks @tim-smart! - Fix OpenAI reasoning stream state handling so out-of-order reasoning summary events do not crash when prior reasoning item state is missing. + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- [#1776](https://github.com/Effect-TS/effect-smol/pull/1776) [`f5e553d`](https://github.com/Effect-TS/effect-smol/commit/f5e553d133b8e0a303f6c82624d2fec4ab8043da) Thanks @tim-smart! - improve openai socket errors + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- [#1759](https://github.com/Effect-TS/effect-smol/pull/1759) [`8feecd2`](https://github.com/Effect-TS/effect-smol/commit/8feecd24158f254ca0571a1ddb554b560ed3177d) Thanks @tim-smart! - Ensure OpenAiSocket sends a `{"type":"response.cancel"}` websocket event when a response stream is interrupted. + +- [#1764](https://github.com/Effect-TS/effect-smol/pull/1764) [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f) Thanks @tim-smart! - Add unstable EmbeddingModel support across core and OpenAI providers. + - Add the unstable EmbeddingModel module API surface in `effect`, including service, request, response, and provider types. + - Implement the unstable EmbeddingModel runtime constructor in `effect`, with `RequestResolver` batching, `embed` / `embedMany` spans, provider error propagation, deterministic ordering, and empty-input `embedMany` fast-path behavior. + - Add and align EmbeddingModel behavior tests in `effect` for embedding usage, batching, ordering, and error handling. + - Add `OpenAiEmbeddingModel` in `@effect/ai-openai`, including model / make / layer constructors, config overrides, and provider output index validation with deterministic reordering. + - Add OpenAI-compatible EmbeddingModel provider support in `@effect/ai-openai-compat`, including config overrides, layer constructors, and output index validation. + +- [#1771](https://github.com/Effect-TS/effect-smol/pull/1771) [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e) Thanks @tim-smart! - Add `EmbeddingModel.ModelDimensions` and require dimensions in embedding provider `model` constructors. + +- [#1765](https://github.com/Effect-TS/effect-smol/pull/1765) [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2) Thanks @tim-smart! - retry incremental prompt on invalid request + +- [#1760](https://github.com/Effect-TS/effect-smol/pull/1760) [`273f4c6`](https://github.com/Effect-TS/effect-smol/commit/273f4c6517f5adc4994734a46698c52015890d1a) Thanks @tim-smart! - handle openai ws error events + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- [#1638](https://github.com/Effect-TS/effect-smol/pull/1638) [`e198fea`](https://github.com/Effect-TS/effect-smol/commit/e198fea76c89a36c6f05ea7b7d11d222cf5a240c) Thanks @tim-smart! - add OpenAiClient.withWebSocketMode + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- [#1679](https://github.com/Effect-TS/effect-smol/pull/1679) [`d440fd4`](https://github.com/Effect-TS/effect-smol/commit/d440fd4e79f5943998249f5e67fb42505bd20228) Thanks @tim-smart! - Add support for OpenAI `keepalive` response stream events. + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- [#1529](https://github.com/Effect-TS/effect-smol/pull/1529) [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8) Thanks @tim-smart! - Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. + +- [#1528](https://github.com/Effect-TS/effect-smol/pull/1528) [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34) Thanks @tim-smart! - Add `Model.ModelName` and provide it from AI model constructors. + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- [#1522](https://github.com/Effect-TS/effect-smol/pull/1522) [`19984c2`](https://github.com/Effect-TS/effect-smol/commit/19984c2c1b23ef06d69f9bbd4c4ad992a03c6860) Thanks @IMax153! - Fix missing `yield*` in `OpenAiLanguageModel.prepareResponseFormat` + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- [#1520](https://github.com/Effect-TS/effect-smol/pull/1520) [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d) Thanks @IMax153! - Ensure that OpenAI JSON schemas for tool calls and structured outputs are properly transformed + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- [#1502](https://github.com/Effect-TS/effect-smol/pull/1502) [`285b7e6`](https://github.com/Effect-TS/effect-smol/commit/285b7e667167566d5788367d5155b19c79f1bf22) Thanks @tim-smart! - allow undefined for ai config + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- [#1354](https://github.com/Effect-TS/effect-smol/pull/1354) [`b94962c`](https://github.com/Effect-TS/effect-smol/commit/b94962c249d46cf96cdf2e41188dc9feda41536a) Thanks @IMax153! - Fix the generated schemas for ai providers + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/ai/openai/codegen.yaml b/repos/effect/packages/ai/openai/codegen.yaml new file mode 100644 index 0000000..19d06da --- /dev/null +++ b/repos/effect/packages/ai/openai/codegen.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=../../tools/ai-codegen/codegen.schema.json +spec: https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml +output: src/Generated.ts +name: OpenAiClient +header: | + /** + * @since 1.0.0 + */ +patches: + - '[{"op":"replace","path":"/openapi","value":"3.0.0"}]' + - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/user/nullable","value":true}]' + - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/safety_identifier/nullable","value":true}]' + - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/prompt_cache_key/nullable","value":true}]' + - '[{"op":"add","path":"/components/schemas/Response/allOf/2/properties/usage/nullable","value":true}]' + - '[{"op":"remove","path":"/components/schemas/ResponseFunctionCallArgumentsDoneEvent/required/2"}]' + - '[{"op":"remove","path":"/components/schemas/WebSearchActionSearch/required/1"}]' + - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/prompt_cache_retention/anyOf/0/enum/1","value":"in_memory"}]' + - '[{"op":"add","path":"/components/schemas/PromptCacheRetentionEnum/enum/1","value":"in-memory"}]' + - '[{"op":"replace","path":"/components/schemas/OpenAIFile/properties/expires_at","value":{"anyOf":[{"type":"integer","format":"unixtime","description":"The Unix timestamp (in seconds) for when the file will expire."},{"type":"null"}]}}]' + - '[{"op":"add","path":"/components/schemas/OpenAIFile/properties/status_details/nullable","value":true}]' + # Add missing keepalive stream event support + - '[{"op":"add","path":"/components/schemas/ResponseKeepAliveEvent","value":{"type":"object","title":"Keep alive","description":"A keepalive event emitted during long-running response streams.","properties":{"type":{"type":"string","enum":["keepalive"],"description":"The type of the keepalive event. Always `keepalive`.","default":"keepalive","x-stainless-const":true},"sequence_number":{"type":"integer","description":"The sequence number of this keepalive event."}},"required":["type","sequence_number"]}}]' + - '[{"op":"add","path":"/components/schemas/ResponseStreamEvent/anyOf/-","value":{"$ref":"#/components/schemas/ResponseKeepAliveEvent"}}]' + # Add missing ResponseApplyPatchCallOperationDiffDeltaEvent schema + - '[{"op":"add","path":"/components/schemas/ResponseApplyPatchCallOperationDiffDeltaEvent","value":{"title":"ResponseApplyPatchCallOperationDiffDelta","type":"object","description":"Event representing a delta for an apply_patch tool call operation diff.","properties":{"type":{"type":"string","enum":["response.apply_patch_call_operation_diff.delta"],"description":"The event type identifier.","x-stainless-const":true},"sequence_number":{"type":"integer","description":"The sequence number of this event."},"output_index":{"type":"integer","description":"The index of the output this delta applies to."},"item_id":{"type":"string","description":"Unique identifier for the API item associated with this event."},"delta":{"type":"string","description":"The incremental diff data for the apply_patch tool call."}},"required":["type","output_index","item_id","delta","sequence_number"]}}]' + # Add missing ResponseApplyPatchCallOperationDiffDoneEvent schema (delta is optional - not always sent by OpenAI) + - '[{"op":"add","path":"/components/schemas/ResponseApplyPatchCallOperationDiffDoneEvent","value":{"title":"ResponseApplyPatchCallOperationDiffDone","type":"object","description":"Event indicating that the operation diff for an apply_patch tool call is complete.","properties":{"type":{"type":"string","enum":["response.apply_patch_call_operation_diff.done"],"description":"The event type identifier.","x-stainless-const":true},"sequence_number":{"type":"integer","description":"The sequence number of this event."},"output_index":{"type":"integer","description":"The index of the output this event applies to."},"item_id":{"type":"string","description":"Unique identifier for the API item associated with this event."},"delta":{"type":"string","description":"The final diff data for the apply_patch tool call."}},"required":["type","output_index","item_id","sequence_number"]}}]' + # Add both events to ResponseStreamEvent anyOf array + - '[{"op":"add","path":"/components/schemas/ResponseStreamEvent/anyOf/-","value":{"$ref":"#/components/schemas/ResponseApplyPatchCallOperationDiffDeltaEvent"}}]' + - '[{"op":"add","path":"/components/schemas/ResponseStreamEvent/anyOf/-","value":{"$ref":"#/components/schemas/ResponseApplyPatchCallOperationDiffDoneEvent"}}]' +excludeAnnotations: + - examples + - default +disableAdditionalProperties: true +replacements: + # Schema.Unknown doesn't work with Schema.toCodecJson (used by HttpClientResponse.schemaBodyJson) + # Replace with Schema.Json which properly handles arbitrary JSON values + - from: "Schema.Record(Schema.String, Schema.Unknown)" + to: "Schema.Record(Schema.String, Schema.Json)" + - from: "{ readonly [x: string]: unknown }" + to: "{ readonly [x: string]: Schema.Json }" diff --git a/repos/effect/packages/ai/openai/docgen.json b/repos/effect/packages/ai/openai/docgen.json new file mode 100644 index 0000000..0cdaf8e --- /dev/null +++ b/repos/effect/packages/ai/openai/docgen.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openai/src/", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/ai/openai/package.json b/repos/effect/packages/ai/openai/package.json new file mode 100644 index 0000000..06646c4 --- /dev/null +++ b/repos/effect/packages/ai/openai/package.json @@ -0,0 +1,66 @@ +{ + "name": "@effect/ai-openai", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "An OpenAI provider integration for Effect AI SDK", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/openai" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "ai", + "openai" + ], + "keywords": [ + "typescript", + "ai", + "openai" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/openai/src/Generated.ts b/repos/effect/packages/ai/openai/src/Generated.ts new file mode 100644 index 0000000..d7189bf --- /dev/null +++ b/repos/effect/packages/ai/openai/src/Generated.ts @@ -0,0 +1,35774 @@ +/** + * @since 1.0.0 + */ + +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { SchemaError } from "effect/Schema" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as Sse from "effect/unstable/encoding/Sse" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +// non-recursive definitions +export type AddUploadPartRequest = { readonly "data": string } +export const AddUploadPartRequest = Schema.Struct({ + "data": Schema.String.annotate({ "description": "The chunk of bytes for this Part.\n", "format": "binary" }) +}) +export type AdminApiKey = { + readonly "object": "organization.admin_api_key" + readonly "id": string + readonly "name"?: string | null + readonly "redacted_value": string + readonly "created_at": number + readonly "last_used_at"?: number | null + readonly "owner": { + readonly "type"?: string + readonly "object"?: string + readonly "id"?: string + readonly "name"?: string + readonly "created_at"?: number + readonly "role"?: string + } +} +export const AdminApiKey = Schema.Struct({ + "object": Schema.Literal("organization.admin_api_key").annotate({ + "description": "The object type, which is always `organization.admin_api_key`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the API key" }) + ), + "redacted_value": Schema.String.annotate({ "description": "The redacted value of the API key" }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the API key was created", + "format": "unixtime" + }).check(Schema.isInt()), + "last_used_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the API key was last used", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "owner": Schema.Struct({ + "type": Schema.optionalKey(Schema.String.annotate({ "description": "Always `user`" })), + "object": Schema.optionalKey( + Schema.String.annotate({ "description": "The object type, which is always organization.user" }) + ), + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the user" })), + "created_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the user was created", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "role": Schema.optionalKey(Schema.String.annotate({ "description": "Always `owner`" })) + }) +}).annotate({ "description": "Represents an individual Admin API key in an org." }) +export type AdminApiKeyCreateResponse = { + readonly "object": "organization.admin_api_key" + readonly "id": string + readonly "name"?: string | null + readonly "redacted_value": string + readonly "created_at": number + readonly "last_used_at"?: number | null + readonly "owner": { + readonly "type"?: string + readonly "object"?: string + readonly "id"?: string + readonly "name"?: string + readonly "created_at"?: number + readonly "role"?: string + } + readonly "value": string +} +export const AdminApiKeyCreateResponse = Schema.Struct({ + "object": Schema.Literal("organization.admin_api_key").annotate({ + "description": "The object type, which is always `organization.admin_api_key`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the API key" }) + ), + "redacted_value": Schema.String.annotate({ "description": "The redacted value of the API key" }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the API key was created", + "format": "unixtime" + }).check(Schema.isInt()), + "last_used_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the API key was last used", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "owner": Schema.Struct({ + "type": Schema.optionalKey(Schema.String.annotate({ "description": "Always `user`" })), + "object": Schema.optionalKey( + Schema.String.annotate({ "description": "The object type, which is always organization.user" }) + ), + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the user" })), + "created_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the user was created", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "role": Schema.optionalKey(Schema.String.annotate({ "description": "Always `owner`" })) + }), + "value": Schema.String.annotate({ "description": "The value of the API key. Only shown on create." }) +}).annotate({ + "description": "The newly created admin API key. The `value` field is only returned once, when the key is created." +}) +export type AssignedRoleDetails = { + readonly "id": string + readonly "name": string + readonly "permissions": ReadonlyArray + readonly "resource_type": string + readonly "predefined_role": boolean + readonly "description": string | null + readonly "created_at": number | null + readonly "updated_at": number | null + readonly "created_by": string | null + readonly "created_by_user_obj": {} | null + readonly "metadata": {} | null +} +export const AssignedRoleDetails = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier for the role." }), + "name": Schema.String.annotate({ "description": "Name of the role." }), + "permissions": Schema.Array(Schema.String).annotate({ "description": "Permissions associated with the role." }), + "resource_type": Schema.String.annotate({ "description": "Resource type the role applies to." }), + "predefined_role": Schema.Boolean.annotate({ "description": "Whether the role is predefined by OpenAI." }), + "description": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Description of the role." }), + "created_at": Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]) + .annotate({ "description": "When the role was created." }), + "updated_at": Schema.Union([Schema.Number.annotate({ "format": "int64" }).check(Schema.isInt()), Schema.Null]) + .annotate({ "description": "When the role was last updated." }), + "created_by": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Identifier of the actor who created the role." + }), + "created_by_user_obj": Schema.Union([Schema.Struct({}), Schema.Null]).annotate({ + "description": "User details for the actor that created the role, when available." + }), + "metadata": Schema.Union([Schema.Struct({}), Schema.Null]).annotate({ + "description": "Arbitrary metadata stored on the role." + }) +}).annotate({ "description": "Detailed information about a role assignment entry returned when listing assignments." }) +export type AssistantSupportedModels = + | "gpt-5" + | "gpt-5-mini" + | "gpt-5-nano" + | "gpt-5-2025-08-07" + | "gpt-5-mini-2025-08-07" + | "gpt-5-nano-2025-08-07" + | "gpt-4.1" + | "gpt-4.1-mini" + | "gpt-4.1-nano" + | "gpt-4.1-2025-04-14" + | "gpt-4.1-mini-2025-04-14" + | "gpt-4.1-nano-2025-04-14" + | "o3-mini" + | "o3-mini-2025-01-31" + | "o1" + | "o1-2024-12-17" + | "gpt-4o" + | "gpt-4o-2024-11-20" + | "gpt-4o-2024-08-06" + | "gpt-4o-2024-05-13" + | "gpt-4o-mini" + | "gpt-4o-mini-2024-07-18" + | "gpt-4.5-preview" + | "gpt-4.5-preview-2025-02-27" + | "gpt-4-turbo" + | "gpt-4-turbo-2024-04-09" + | "gpt-4-0125-preview" + | "gpt-4-turbo-preview" + | "gpt-4-1106-preview" + | "gpt-4-vision-preview" + | "gpt-4" + | "gpt-4-0314" + | "gpt-4-0613" + | "gpt-4-32k" + | "gpt-4-32k-0314" + | "gpt-4-32k-0613" + | "gpt-3.5-turbo" + | "gpt-3.5-turbo-16k" + | "gpt-3.5-turbo-0613" + | "gpt-3.5-turbo-1106" + | "gpt-3.5-turbo-0125" + | "gpt-3.5-turbo-16k-0613" +export const AssistantSupportedModels = Schema.Literals([ + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-2025-08-07", + "gpt-5-mini-2025-08-07", + "gpt-5-nano-2025-08-07", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano-2025-04-14", + "o3-mini", + "o3-mini-2025-01-31", + "o1", + "o1-2024-12-17", + "gpt-4o", + "gpt-4o-2024-11-20", + "gpt-4o-2024-08-06", + "gpt-4o-2024-05-13", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4.5-preview", + "gpt-4.5-preview-2025-02-27", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613" +]) +export type AssistantToolsCode = { readonly "type": "code_interpreter" } +export const AssistantToolsCode = Schema.Struct({ + "type": Schema.Literal("code_interpreter").annotate({ + "description": "The type of tool being defined: `code_interpreter`" + }) +}).annotate({ "title": "Code interpreter tool" }) +export type AssistantToolsFileSearchTypeOnly = { readonly "type": "file_search" } +export const AssistantToolsFileSearchTypeOnly = Schema.Struct({ + "type": Schema.Literal("file_search").annotate({ "description": "The type of tool being defined: `file_search`" }) +}).annotate({ "title": "FileSearch tool" }) +export type AssistantsNamedToolChoice = { + readonly "type": "function" | "code_interpreter" | "file_search" + readonly "function"?: { readonly "name": string } +} +export const AssistantsNamedToolChoice = Schema.Struct({ + "type": Schema.Literals(["function", "code_interpreter", "file_search"]).annotate({ + "description": "The type of the tool. If type is `function`, the function name must be set" + }), + "function": Schema.optionalKey( + Schema.Struct({ "name": Schema.String.annotate({ "description": "The name of the function to call." }) }) + ) +}).annotate({ "description": "Specifies a tool the model should use. Use to force the model to call a specific tool." }) +export type AudioResponseFormat = "json" | "text" | "srt" | "verbose_json" | "vtt" | "diarized_json" +export const AudioResponseFormat = Schema.Literals(["json", "text", "srt", "verbose_json", "vtt", "diarized_json"]) + .annotate({ + "description": + "The format of the output, in one of these options: `json`, `text`, `srt`, `verbose_json`, `vtt`, or `diarized_json`. For `gpt-4o-transcribe` and `gpt-4o-mini-transcribe`, the only supported format is `json`. For `gpt-4o-transcribe-diarize`, the supported formats are `json`, `text`, and `diarized_json`, with `diarized_json` required to receive speaker annotations.\n" + }) +export type AuditLogActorServiceAccount = { readonly "id"?: string } +export const AuditLogActorServiceAccount = Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The service account id." })) +}).annotate({ "description": "The service account that performed the audit logged action." }) +export type AuditLogActorUser = { readonly "id"?: string; readonly "email"?: string } +export const AuditLogActorUser = Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The user id." })), + "email": Schema.optionalKey(Schema.String.annotate({ "description": "The user email." })) +}).annotate({ "description": "The user who performed the audit logged action." }) +export type AuditLogEventType = + | "api_key.created" + | "api_key.updated" + | "api_key.deleted" + | "certificate.created" + | "certificate.updated" + | "certificate.deleted" + | "certificates.activated" + | "certificates.deactivated" + | "checkpoint.permission.created" + | "checkpoint.permission.deleted" + | "external_key.registered" + | "external_key.removed" + | "group.created" + | "group.updated" + | "group.deleted" + | "invite.sent" + | "invite.accepted" + | "invite.deleted" + | "ip_allowlist.created" + | "ip_allowlist.updated" + | "ip_allowlist.deleted" + | "ip_allowlist.config.activated" + | "ip_allowlist.config.deactivated" + | "login.succeeded" + | "login.failed" + | "logout.succeeded" + | "logout.failed" + | "organization.updated" + | "project.created" + | "project.updated" + | "project.archived" + | "project.deleted" + | "rate_limit.updated" + | "rate_limit.deleted" + | "resource.deleted" + | "tunnel.created" + | "tunnel.updated" + | "tunnel.deleted" + | "role.created" + | "role.updated" + | "role.deleted" + | "role.assignment.created" + | "role.assignment.deleted" + | "scim.enabled" + | "scim.disabled" + | "service_account.created" + | "service_account.updated" + | "service_account.deleted" + | "user.added" + | "user.updated" + | "user.deleted" +export const AuditLogEventType = Schema.Literals([ + "api_key.created", + "api_key.updated", + "api_key.deleted", + "certificate.created", + "certificate.updated", + "certificate.deleted", + "certificates.activated", + "certificates.deactivated", + "checkpoint.permission.created", + "checkpoint.permission.deleted", + "external_key.registered", + "external_key.removed", + "group.created", + "group.updated", + "group.deleted", + "invite.sent", + "invite.accepted", + "invite.deleted", + "ip_allowlist.created", + "ip_allowlist.updated", + "ip_allowlist.deleted", + "ip_allowlist.config.activated", + "ip_allowlist.config.deactivated", + "login.succeeded", + "login.failed", + "logout.succeeded", + "logout.failed", + "organization.updated", + "project.created", + "project.updated", + "project.archived", + "project.deleted", + "rate_limit.updated", + "rate_limit.deleted", + "resource.deleted", + "tunnel.created", + "tunnel.updated", + "tunnel.deleted", + "role.created", + "role.updated", + "role.deleted", + "role.assignment.created", + "role.assignment.deleted", + "scim.enabled", + "scim.disabled", + "service_account.created", + "service_account.updated", + "service_account.deleted", + "user.added", + "user.updated", + "user.deleted" +]).annotate({ "description": "The event type." }) +export type BatchFileExpirationAfter = { readonly "anchor": "created_at"; readonly "seconds": number } +export const BatchFileExpirationAfter = Schema.Struct({ + "anchor": Schema.Literal("created_at").annotate({ + "description": + "Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`. Note that the anchor is the file creation time, not the time the batch is created." + }), + "seconds": Schema.Number.annotate({ + "description": + "The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days).", + "format": "int64" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(3600)).check(Schema.isLessThanOrEqualTo(2592000)) +}).annotate({ + "title": "File expiration policy", + "description": "The expiration policy for the output and/or error file that are generated for a batch." +}) +export type Certificate = { + readonly "object": "certificate" | "organization.certificate" | "organization.project.certificate" + readonly "id": string + readonly "name": string | null + readonly "created_at": number + readonly "certificate_details": { + readonly "valid_at"?: number + readonly "expires_at"?: number + readonly "content"?: string + } + readonly "active"?: boolean +} +export const Certificate = Schema.Struct({ + "object": Schema.Literals(["certificate", "organization.certificate", "organization.project.certificate"]).annotate({ + "description": + "The object type.\n\n- If creating, updating, or getting a specific certificate, the object type is `certificate`.\n- If listing, activating, or deactivating certificates for the organization, the object type is `organization.certificate`.\n- If listing, activating, or deactivating certificates for a project, the object type is `organization.project.certificate`.\n" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the certificate." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate was uploaded.", + "format": "unixtime" + }).check(Schema.isInt()), + "certificate_details": Schema.Struct({ + "valid_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate becomes valid.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate expires.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "content": Schema.optionalKey( + Schema.String.annotate({ "description": "The content of the certificate in PEM format." }) + ) + }), + "active": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the certificate is currently active at the specified scope. Not returned when getting details for a specific certificate." + }) + ) +}).annotate({ "description": "Represents an individual `certificate` uploaded to the organization." }) +export type ChatCompletionAllowedTools = { readonly "mode": "auto" | "required"; readonly "tools": ReadonlyArray<{}> } +export const ChatCompletionAllowedTools = Schema.Struct({ + "mode": Schema.Literals(["auto", "required"]).annotate({ + "description": + "Constrains the tools available to the model to a pre-defined set.\n\n`auto` allows the model to pick from among the allowed tools and generate a\nmessage.\n\n`required` requires the model to call one or more of the allowed tools.\n" + }), + "tools": Schema.Array( + Schema.Struct({}).annotate({ "description": "A tool definition that the model should be allowed to call.\n" }) + ).annotate({ + "description": + "A list of tool definitions that the model should be allowed to call.\n\nFor the Chat Completions API, the list of tool definitions might look like:\n```json\n[\n { \"type\": \"function\", \"function\": { \"name\": \"get_weather\" } },\n { \"type\": \"function\", \"function\": { \"name\": \"get_time\" } }\n]\n```\n" + }) +}).annotate({ + "title": "Allowed tools", + "description": "Constrains the tools available to the model to a pre-defined set.\n" +}) +export type ChatCompletionDeleted = { + readonly "object": "chat.completion.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const ChatCompletionDeleted = Schema.Struct({ + "object": Schema.Literal("chat.completion.deleted").annotate({ "description": "The type of object being deleted." }), + "id": Schema.String.annotate({ "description": "The ID of the chat completion that was deleted." }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the chat completion was deleted." }) +}) +export type ChatCompletionFunctionCallOption = { readonly "name": string } +export const ChatCompletionFunctionCallOption = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the function to call." }) +}).annotate({ + "description": + "Specifying a particular function via `{\"name\": \"my_function\"}` forces the model to call that function.\n" +}) +export type ChatCompletionMessageCustomToolCall = { + readonly "id": string + readonly "type": "custom" + readonly "custom": { readonly "name": string; readonly "input": string } +} +export const ChatCompletionMessageCustomToolCall = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the tool call." }), + "type": Schema.Literal("custom").annotate({ "description": "The type of the tool. Always `custom`." }), + "custom": Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the custom tool to call." }), + "input": Schema.String.annotate({ "description": "The input for the custom tool call generated by the model." }) + }).annotate({ "description": "The custom tool that the model called." }) +}).annotate({ "title": "Custom tool call", "description": "A call to a custom tool created by the model.\n" }) +export type ChatCompletionMessageToolCall = { + readonly "id": string + readonly "type": "function" + readonly "function": { readonly "name": string; readonly "arguments": string } +} +export const ChatCompletionMessageToolCall = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the tool call." }), + "type": Schema.Literal("function").annotate({ + "description": "The type of the tool. Currently, only `function` is supported." + }), + "function": Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the function to call." }), + "arguments": Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + }) + }).annotate({ "description": "The function that the model called." }) +}).annotate({ "title": "Function tool call", "description": "A call to a function tool created by the model.\n" }) +export type ChatCompletionMessageToolCallChunk = { + readonly "index": number + readonly "id"?: string + readonly "type"?: "function" + readonly "function"?: { readonly "name"?: string; readonly "arguments"?: string } +} +export const ChatCompletionMessageToolCallChunk = Schema.Struct({ + "index": Schema.Number.check(Schema.isInt()), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the tool call." })), + "type": Schema.optionalKey( + Schema.Literal("function").annotate({ + "description": "The type of the tool. Currently, only `function` is supported." + }) + ), + "function": Schema.optionalKey(Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function to call." })), + "arguments": Schema.optionalKey(Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + })) + })) +}) +export type ChatCompletionNamedToolChoice = { + readonly "type": "function" + readonly "function": { readonly "name": string } +} +export const ChatCompletionNamedToolChoice = Schema.Struct({ + "type": Schema.Literal("function").annotate({ + "description": "For function calling, the type is always `function`." + }), + "function": Schema.Struct({ "name": Schema.String.annotate({ "description": "The name of the function to call." }) }) +}).annotate({ + "title": "Function tool choice", + "description": "Specifies a tool the model should use. Use to force the model to call a specific function." +}) +export type ChatCompletionNamedToolChoiceCustom = { + readonly "type": "custom" + readonly "custom": { readonly "name": string } +} +export const ChatCompletionNamedToolChoiceCustom = Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "For custom tool calling, the type is always `custom`." }), + "custom": Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the custom tool to call." }) + }) +}).annotate({ + "title": "Custom tool choice", + "description": "Specifies a tool the model should use. Use to force the model to call a specific custom tool." +}) +export type ChatCompletionRequestFunctionMessage = { + readonly "role": "function" + readonly "content": string | null + readonly "name": string +} +export const ChatCompletionRequestFunctionMessage = Schema.Struct({ + "role": Schema.Literal("function").annotate({ + "description": "The role of the messages author, in this case `function`." + }), + "content": Schema.Union([ + Schema.String.annotate({ "description": "The contents of the function message." }), + Schema.Null + ]), + "name": Schema.String.annotate({ "description": "The name of the function to call." }) +}).annotate({ "title": "Function message" }) +export type ChatCompletionRequestMessageContentPartAudio = { + readonly "type": "input_audio" + readonly "input_audio": { readonly "data": string; readonly "format": "wav" | "mp3" } +} +export const ChatCompletionRequestMessageContentPartAudio = Schema.Struct({ + "type": Schema.Literal("input_audio").annotate({ + "description": "The type of the content part. Always `input_audio`." + }), + "input_audio": Schema.Struct({ + "data": Schema.String.annotate({ "description": "Base64 encoded audio data." }), + "format": Schema.Literals(["wav", "mp3"]).annotate({ + "description": "The format of the encoded audio data. Currently supports \"wav\" and \"mp3\".\n" + }) + }) +}).annotate({ "title": "Audio content part", "description": "Learn about [audio inputs](/docs/guides/audio).\n" }) +export type ChatCompletionRequestMessageContentPartFile = { + readonly "type": "file" + readonly "file": { readonly "filename"?: string; readonly "file_data"?: string; readonly "file_id"?: string } +} +export const ChatCompletionRequestMessageContentPartFile = Schema.Struct({ + "type": Schema.Literal("file").annotate({ "description": "The type of the content part. Always `file`." }), + "file": Schema.Struct({ + "filename": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name of the file, used when passing the file to the model as a \nstring.\n" + }) + ), + "file_data": Schema.optionalKey( + Schema.String.annotate({ + "description": "The base64 encoded file data, used when passing the file to the model \nas a string.\n" + }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of an uploaded file to use as input.\n" }) + ) + }) +}).annotate({ + "title": "File content part", + "description": "Learn about [file inputs](/docs/guides/text) for text generation.\n" +}) +export type ChatCompletionRequestMessageContentPartImage = { + readonly "type": "image_url" + readonly "image_url": { readonly "url": string; readonly "detail"?: "auto" | "low" | "high" } +} +export const ChatCompletionRequestMessageContentPartImage = Schema.Struct({ + "type": Schema.Literal("image_url").annotate({ "description": "The type of the content part." }), + "image_url": Schema.Struct({ + "url": Schema.String.annotate({ + "description": "Either a URL of the image or the base64 encoded image data.", + "format": "uri" + }), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": + "Specifies the detail level of the image. Learn more in the [Vision guide](/docs/guides/vision#low-or-high-fidelity-image-understanding)." + }) + ) + }) +}).annotate({ "title": "Image content part", "description": "Learn about [image inputs](/docs/guides/vision).\n" }) +export type ChatCompletionRequestMessageContentPartRefusal = { readonly "type": "refusal"; readonly "refusal": string } +export const ChatCompletionRequestMessageContentPartRefusal = Schema.Struct({ + "type": Schema.Literal("refusal").annotate({ "description": "The type of the content part." }), + "refusal": Schema.String.annotate({ "description": "The refusal message generated by the model." }) +}).annotate({ "title": "Refusal content part" }) +export type ChatCompletionRequestMessageContentPartText = { readonly "type": "text"; readonly "text": string } +export const ChatCompletionRequestMessageContentPartText = Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "The type of the content part." }), + "text": Schema.String.annotate({ "description": "The text content." }) +}).annotate({ + "title": "Text content part", + "description": "Learn about [text inputs](/docs/guides/text-generation).\n" +}) +export type ChatCompletionStreamOptions = { + readonly "include_usage"?: boolean + readonly "include_obfuscation"?: boolean +} | null +export const ChatCompletionStreamOptions = Schema.Union([ + Schema.Struct({ + "include_usage": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "If set, an additional chunk will be streamed before the `data: [DONE]`\nmessage. The `usage` field on this chunk shows the token usage statistics\nfor the entire request, and the `choices` field will always be an empty\narray.\n\nAll other chunks will also include a `usage` field, but with a null\nvalue. **NOTE:** If the stream is interrupted, you may not receive the\nfinal usage chunk which contains the total token usage for the request.\n" + })), + "include_obfuscation": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "When true, stream obfuscation will be enabled. Stream obfuscation adds\nrandom characters to an `obfuscation` field on streaming delta events to\nnormalize payload sizes as a mitigation to certain side-channel attacks.\nThese obfuscation fields are included by default, but add a small amount\nof overhead to the data stream. You can set `include_obfuscation` to\nfalse to optimize for bandwidth if you trust the network links between\nyour application and the OpenAI API.\n" + })) + }).annotate({ "description": "Options for streaming response. Only set this when you set `stream: true`.\n" }), + Schema.Null +]) +export type ChatCompletionTokenLogprob = { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray | null + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "logprob": number; readonly "bytes": ReadonlyArray | null } + > +} +export const ChatCompletionTokenLogprob = Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]), + "top_logprobs": Schema.Array(Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]) + })).annotate({ + "description": + "List of the most likely tokens and their log probability, at this token position. The number of entries may be fewer than the requested `top_logprobs`." + }) +}) +export type ComparisonFilter = { + readonly "type": "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" + readonly "key": string + readonly "value": string | number | boolean | ReadonlyArray +} +export const ComparisonFilter = Schema.Struct({ + "type": Schema.Literals(["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"]).annotate({ + "description": + "Specifies the comparison operator: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`.\n- `eq`: equals\n- `ne`: not equal\n- `gt`: greater than\n- `gte`: greater than or equal\n- `lt`: less than\n- `lte`: less than or equal\n- `in`: in\n- `nin`: not in\n" + }), + "key": Schema.String.annotate({ "description": "The key to compare against the value." }), + "value": Schema.Union([ + Schema.String, + Schema.Number.check(Schema.isFinite()), + Schema.Boolean, + Schema.Array(Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())], { mode: "oneOf" })) + ], { mode: "oneOf" }).annotate({ + "description": "The value to compare against the attribute key; supports string, number, or boolean types." + }) +}).annotate({ + "title": "Comparison Filter", + "description": + "A filter used to compare a specified attribute key to a given value using a defined comparison operation.\n" +}) +export type CompleteUploadRequest = { readonly "part_ids": ReadonlyArray; readonly "md5"?: string } +export const CompleteUploadRequest = Schema.Struct({ + "part_ids": Schema.Array(Schema.String).annotate({ "description": "The ordered list of Part IDs.\n" }), + "md5": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The optional md5 checksum for the file contents to verify if the bytes uploaded matches what you expect.\n" + }) + ) +}) +export type CompletionUsage = { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "total_tokens": number + readonly "completion_tokens_details"?: { + readonly "accepted_prediction_tokens"?: number + readonly "audio_tokens"?: number + readonly "reasoning_tokens"?: number + readonly "rejected_prediction_tokens"?: number + } + readonly "prompt_tokens_details"?: { readonly "audio_tokens"?: number; readonly "cached_tokens"?: number } +} +export const CompletionUsage = Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ "description": "Number of tokens in the generated completion." }).check( + Schema.isInt() + ), + "prompt_tokens": Schema.Number.annotate({ "description": "Number of tokens in the prompt." }).check(Schema.isInt()), + "total_tokens": Schema.Number.annotate({ + "description": "Total number of tokens used in the request (prompt + completion)." + }).check(Schema.isInt()), + "completion_tokens_details": Schema.optionalKey( + Schema.Struct({ + "accepted_prediction_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "When using Predicted Outputs, the number of tokens in the\nprediction that appeared in the completion.\n" + }).check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio input tokens generated by the model." }).check(Schema.isInt()) + ), + "reasoning_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Tokens generated by the model for reasoning." }).check(Schema.isInt()) + ), + "rejected_prediction_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "When using Predicted Outputs, the number of tokens in the\nprediction that did not appear in the completion. However, like\nreasoning tokens, these tokens are still counted in the total\ncompletion tokens for purposes of billing, output, and context window\nlimits.\n" + }).check(Schema.isInt()) + ) + }).annotate({ "description": "Breakdown of tokens used in a completion." }) + ), + "prompt_tokens_details": Schema.optionalKey( + Schema.Struct({ + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio input tokens present in the prompt." }).check(Schema.isInt()) + ), + "cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cached tokens present in the prompt." }).check(Schema.isInt()) + ) + }).annotate({ "description": "Breakdown of tokens used in the prompt." }) + ) +}).annotate({ "description": "Usage statistics for the completion request." }) +export type ComputerScreenshotImage = { + readonly "type": "computer_screenshot" + readonly "image_url"?: string + readonly "file_id"?: string +} +export const ComputerScreenshotImage = Schema.Struct({ + "type": Schema.Literal("computer_screenshot").annotate({ + "description": + "Specifies the event type. For a computer screenshot, this property is \nalways set to `computer_screenshot`.\n" + }), + "image_url": Schema.optionalKey( + Schema.String.annotate({ "description": "The URL of the screenshot image.", "format": "uri" }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of an uploaded file that contains the screenshot." }) + ) +}).annotate({ "description": "A computer screenshot image used with the computer use tool.\n" }) +export type ContainerFileResource = { + readonly "id": string + readonly "object": string + readonly "container_id": string + readonly "created_at": number + readonly "bytes": number + readonly "path": string + readonly "source": string +} +export const ContainerFileResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the file." }), + "object": Schema.String.annotate({ "description": "The type of this object (`container.file`)." }), + "container_id": Schema.String.annotate({ "description": "The container this file belongs to." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the file was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "bytes": Schema.Number.annotate({ "description": "Size of the file in bytes." }).check(Schema.isInt()), + "path": Schema.String.annotate({ "description": "Path of the file in the container." }), + "source": Schema.String.annotate({ "description": "Source of the file (e.g., `user`, `assistant`)." }) +}).annotate({ "title": "The container file object" }) +export type ContainerResource = { + readonly "id": string + readonly "object": string + readonly "name": string + readonly "created_at": number + readonly "status": string + readonly "last_active_at"?: number + readonly "expires_after"?: { readonly "anchor"?: "last_active_at"; readonly "minutes"?: number } + readonly "memory_limit"?: "1g" | "4g" | "16g" | "64g" + readonly "network_policy"?: { + readonly "type": "allowlist" | "disabled" + readonly "allowed_domains"?: ReadonlyArray + } +} +export const ContainerResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the container." }), + "object": Schema.String.annotate({ "description": "The type of this object." }), + "name": Schema.String.annotate({ "description": "Name of the container." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the container was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "status": Schema.String.annotate({ "description": "Status of the container (e.g., active, deleted)." }), + "last_active_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the container was last active.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expires_after": Schema.optionalKey( + Schema.Struct({ + "anchor": Schema.optionalKey( + Schema.Literal("last_active_at").annotate({ "description": "The reference point for the expiration." }) + ), + "minutes": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of minutes after the anchor before the container expires." + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "The container will expire after this time period.\nThe anchor is the reference point for the expiration.\nThe minutes is the number of minutes after the anchor before the container expires.\n" + }) + ), + "memory_limit": Schema.optionalKey( + Schema.Literals(["1g", "4g", "16g", "64g"]).annotate({ + "description": "The memory limit configured for the container." + }) + ), + "network_policy": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literals(["allowlist", "disabled"]).annotate({ "description": "The network policy mode." }), + "allowed_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Allowed outbound domains when `type` is `allowlist`." }) + ) + }).annotate({ "description": "Network access policy for the container." }) + ) +}).annotate({ "title": "The container object" }) +export type CostsResult = { + readonly "object": "organization.costs.result" + readonly "amount"?: { readonly "value"?: number; readonly "currency"?: string } + readonly "line_item"?: string | null + readonly "project_id"?: string | null + readonly "api_key_id"?: string | null + readonly "quantity"?: number | null +} +export const CostsResult = Schema.Struct({ + "object": Schema.Literal("organization.costs.result"), + "amount": Schema.optionalKey( + Schema.Struct({ + "value": Schema.optionalKey( + Schema.Number.annotate({ "description": "The numeric value of the cost." }).check(Schema.isFinite()) + ), + "currency": Schema.optionalKey( + Schema.String.annotate({ "description": "Lowercase ISO-4217 currency e.g. \"usd\"" }) + ) + }).annotate({ "description": "The monetary value in its associated currency." }) + ), + "line_item": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=line_item`, this field provides the line item of the grouped costs result." + }), + Schema.Null + ]) + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped costs result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API Key ID of the grouped costs result." + }), + Schema.Null + ]) + ), + "quantity": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "When `group_by=line_item`, this field provides the quantity of the grouped costs result." + }).check(Schema.isFinite()), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated costs details of the specific time bucket." }) +export type CreateContainerFileBody = { readonly "file_id"?: string; readonly "file"?: string } +export const CreateContainerFileBody = Schema.Struct({ + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "Name of the file to create." })), + "file": Schema.optionalKey( + Schema.String.annotate({ "description": "The File object (not file name) to be uploaded.\n", "format": "binary" }) + ) +}) +export type CreateEmbeddingRequest = { + readonly "input": string | ReadonlyArray | ReadonlyArray | ReadonlyArray> + readonly "model": string | "text-embedding-ada-002" | "text-embedding-3-small" | "text-embedding-3-large" + readonly "encoding_format"?: "float" | "base64" + readonly "dimensions"?: number + readonly "user"?: string +} +export const CreateEmbeddingRequest = Schema.Struct({ + "input": Schema.Union([ + Schema.String.annotate({ "title": "string", "description": "The string that will be turned into an embedding." }), + Schema.Array(Schema.String).annotate({ + "title": "array", + "description": "The array of strings that will be turned into an embedding." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2048)), + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "title": "array", + "description": "The array of integers that will be turned into an embedding." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2048)), + Schema.Array(Schema.Array(Schema.Number.check(Schema.isInt())).check(Schema.isMinLength(1))).annotate({ + "title": "array", + "description": "The array of arrays containing integers that will be turned into an embedding." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2048)) + ], { mode: "oneOf" }).annotate({ + "description": + "Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays. The input must not exceed the max input tokens for the model (8192 tokens for all embedding models), cannot be an empty string, and any array must be 2048 dimensions or less. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. In addition to the per-input token limit, all embedding models enforce a maximum of 300,000 tokens summed across all inputs in a single request.\n" + }), + "model": Schema.Union([ + Schema.String, + Schema.Literals(["text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large"]) + ]).annotate({ + "description": + "ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n" + }), + "encoding_format": Schema.optionalKey( + Schema.Literals(["float", "base64"]).annotate({ + "description": + "The format to return the embeddings in. Can be either `float` or [`base64`](https://pypi.org/project/pybase64/)." + }) + ), + "dimensions": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The number of dimensions the resulting output embeddings should have. Only supported in `text-embedding-3` and later models.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).\n" + }) + ) +}) +export type CreateFineTuningCheckpointPermissionRequest = { readonly "project_ids": ReadonlyArray } +export const CreateFineTuningCheckpointPermissionRequest = Schema.Struct({ + "project_ids": Schema.Array(Schema.String).annotate({ "description": "The project identifiers to grant access to." }) +}) +export type CreateGroupBody = { readonly "name": string } +export const CreateGroupBody = Schema.Struct({ + "name": Schema.String.annotate({ "description": "Human readable name for the group." }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(255)) +}).annotate({ "description": "Request payload for creating a new group in the organization." }) +export type CreateGroupUserBody = { readonly "user_id": string } +export const CreateGroupUserBody = Schema.Struct({ + "user_id": Schema.String.annotate({ "description": "Identifier of the user to add to the group." }) +}).annotate({ "description": "Request payload for adding a user to a group." }) +export type CreateImageVariationRequest = { + readonly "image": string + readonly "model"?: string | "dall-e-2" | null + readonly "n"?: number + readonly "response_format"?: "url" | "b64_json" | null + readonly "size"?: "256x256" | "512x512" | "1024x1024" | null + readonly "user"?: string +} +export const CreateImageVariationRequest = Schema.Struct({ + "image": Schema.String.annotate({ + "description": + "The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square.", + "format": "binary" + }), + "model": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.String, Schema.Literal("dall-e-2")]).annotate({ + "description": "The model to use for image generation. Only `dall-e-2` is supported at this time." + }), + Schema.Null + ]) + ), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(10)], { + "description": "The number of images to generate. Must be between 1 and 10." + }) + ) + ]) + ), + "response_format": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["url", "b64_json"]).annotate({ + "description": + "The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated." + }) + ]) + ), + "size": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["256x256", "512x512", "1024x1024"]).annotate({ + "description": "The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`." + }), + Schema.Union([Schema.Null]).annotate({ + "description": "The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`." + }) + ]) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).\n" + }) + ) +}) +export type CreateModerationRequest = { + readonly "input": + | string + | ReadonlyArray + | ReadonlyArray< + { readonly "type": "image_url"; readonly "image_url": { readonly "url": string } } | { + readonly "type": "text" + readonly "text": string + } + > + readonly "model"?: + | string + | "omni-moderation-latest" + | "omni-moderation-2024-09-26" + | "text-moderation-latest" + | "text-moderation-stable" +} +export const CreateModerationRequest = Schema.Struct({ + "input": Schema.Union([ + Schema.String.annotate({ "description": "A string of text to classify for moderation." }), + Schema.Array(Schema.String).annotate({ "description": "An array of strings to classify for moderation." }), + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("image_url").annotate({ "description": "Always `image_url`." }), + "image_url": Schema.Struct({ + "url": Schema.String.annotate({ + "description": "Either a URL of the image or the base64 encoded image data.", + "format": "uri" + }) + }).annotate({ "description": "Contains either an image URL or a data URL for a base64 encoded image." }) + }).annotate({ "description": "An object describing an image to classify." }), + Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "Always `text`." }), + "text": Schema.String.annotate({ "description": "A string of text to classify." }) + }).annotate({ "description": "An object describing text to classify." }) + ], { mode: "oneOf" })).annotate({ "description": "An array of multi-modal inputs to the moderation model." }) + ], { mode: "oneOf" }).annotate({ + "description": + "Input (or inputs) to classify. Can be a single string, an array of strings, or\nan array of multi-modal input objects similar to other models.\n" + }), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "omni-moderation-latest", + "omni-moderation-2024-09-26", + "text-moderation-latest", + "text-moderation-stable" + ]) + ]).annotate({ + "description": + "The content moderation model you would like to use. Learn more in\n[the moderation guide](/docs/guides/moderation), and learn about\navailable models [here](/docs/models#moderation).\n" + }) + ) +}) +export type CreateModerationResponse = { + readonly "id": string + readonly "model": string + readonly "results": ReadonlyArray< + { + readonly "flagged": boolean + readonly "categories": { + readonly "hate": boolean + readonly "hate/threatening": boolean + readonly "harassment": boolean + readonly "harassment/threatening": boolean + readonly "illicit": boolean | null + readonly "illicit/violent": boolean | null + readonly "self-harm": boolean + readonly "self-harm/intent": boolean + readonly "self-harm/instructions": boolean + readonly "sexual": boolean + readonly "sexual/minors": boolean + readonly "violence": boolean + readonly "violence/graphic": boolean + } + readonly "category_scores": { + readonly "hate": number + readonly "hate/threatening": number + readonly "harassment": number + readonly "harassment/threatening": number + readonly "illicit": number + readonly "illicit/violent": number + readonly "self-harm": number + readonly "self-harm/intent": number + readonly "self-harm/instructions": number + readonly "sexual": number + readonly "sexual/minors": number + readonly "violence": number + readonly "violence/graphic": number + } + readonly "category_applied_input_types": { + readonly "hate": ReadonlyArray<"text"> + readonly "hate/threatening": ReadonlyArray<"text"> + readonly "harassment": ReadonlyArray<"text"> + readonly "harassment/threatening": ReadonlyArray<"text"> + readonly "illicit": ReadonlyArray<"text"> + readonly "illicit/violent": ReadonlyArray<"text"> + readonly "self-harm": ReadonlyArray<"text" | "image"> + readonly "self-harm/intent": ReadonlyArray<"text" | "image"> + readonly "self-harm/instructions": ReadonlyArray<"text" | "image"> + readonly "sexual": ReadonlyArray<"text" | "image"> + readonly "sexual/minors": ReadonlyArray<"text"> + readonly "violence": ReadonlyArray<"text" | "image"> + readonly "violence/graphic": ReadonlyArray<"text" | "image"> + } + } + > +} +export const CreateModerationResponse = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique identifier for the moderation request." }), + "model": Schema.String.annotate({ "description": "The model used to generate the moderation results." }), + "results": Schema.Array(Schema.Struct({ + "flagged": Schema.Boolean.annotate({ "description": "Whether any of the below categories are flagged." }), + "categories": Schema.Struct({ + "hate": Schema.Boolean.annotate({ + "description": + "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment." + }), + "hate/threatening": Schema.Boolean.annotate({ + "description": + "Hateful content that also includes violence or serious harm towards the targeted group based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste." + }), + "harassment": Schema.Boolean.annotate({ + "description": "Content that expresses, incites, or promotes harassing language towards any target." + }), + "harassment/threatening": Schema.Boolean.annotate({ + "description": "Harassment content that also includes violence or serious harm towards any target." + }), + "illicit": Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Content that includes instructions or advice that facilitate the planning or execution of wrongdoing, or that gives advice or instruction on how to commit illicit acts. For example, \"how to shoplift\" would fit this category." + }), + Schema.Null + ]), + "illicit/violent": Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Content that includes instructions or advice that facilitate the planning or execution of wrongdoing that also includes violence, or that gives advice or instruction on the procurement of any weapon." + }), + Schema.Null + ]), + "self-harm": Schema.Boolean.annotate({ + "description": + "Content that promotes, encourages, or depicts acts of self-harm, such as suicide, cutting, and eating disorders." + }), + "self-harm/intent": Schema.Boolean.annotate({ + "description": + "Content where the speaker expresses that they are engaging or intend to engage in acts of self-harm, such as suicide, cutting, and eating disorders." + }), + "self-harm/instructions": Schema.Boolean.annotate({ + "description": + "Content that encourages performing acts of self-harm, such as suicide, cutting, and eating disorders, or that gives instructions or advice on how to commit such acts." + }), + "sexual": Schema.Boolean.annotate({ + "description": + "Content meant to arouse sexual excitement, such as the description of sexual activity, or that promotes sexual services (excluding sex education and wellness)." + }), + "sexual/minors": Schema.Boolean.annotate({ + "description": "Sexual content that includes an individual who is under 18 years old." + }), + "violence": Schema.Boolean.annotate({ + "description": "Content that depicts death, violence, or physical injury." + }), + "violence/graphic": Schema.Boolean.annotate({ + "description": "Content that depicts death, violence, or physical injury in graphic detail." + }) + }).annotate({ "description": "A list of the categories, and whether they are flagged or not." }), + "category_scores": Schema.Struct({ + "hate": Schema.Number.annotate({ "description": "The score for the category 'hate'." }).check(Schema.isFinite()), + "hate/threatening": Schema.Number.annotate({ "description": "The score for the category 'hate/threatening'." }) + .check(Schema.isFinite()), + "harassment": Schema.Number.annotate({ "description": "The score for the category 'harassment'." }).check( + Schema.isFinite() + ), + "harassment/threatening": Schema.Number.annotate({ + "description": "The score for the category 'harassment/threatening'." + }).check(Schema.isFinite()), + "illicit": Schema.Number.annotate({ "description": "The score for the category 'illicit'." }).check( + Schema.isFinite() + ), + "illicit/violent": Schema.Number.annotate({ "description": "The score for the category 'illicit/violent'." }) + .check(Schema.isFinite()), + "self-harm": Schema.Number.annotate({ "description": "The score for the category 'self-harm'." }).check( + Schema.isFinite() + ), + "self-harm/intent": Schema.Number.annotate({ "description": "The score for the category 'self-harm/intent'." }) + .check(Schema.isFinite()), + "self-harm/instructions": Schema.Number.annotate({ + "description": "The score for the category 'self-harm/instructions'." + }).check(Schema.isFinite()), + "sexual": Schema.Number.annotate({ "description": "The score for the category 'sexual'." }).check( + Schema.isFinite() + ), + "sexual/minors": Schema.Number.annotate({ "description": "The score for the category 'sexual/minors'." }).check( + Schema.isFinite() + ), + "violence": Schema.Number.annotate({ "description": "The score for the category 'violence'." }).check( + Schema.isFinite() + ), + "violence/graphic": Schema.Number.annotate({ "description": "The score for the category 'violence/graphic'." }) + .check(Schema.isFinite()) + }).annotate({ "description": "A list of the categories along with their scores as predicted by model." }), + "category_applied_input_types": Schema.Struct({ + "hate": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'hate'." + }), + "hate/threatening": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'hate/threatening'." + }), + "harassment": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'harassment'." + }), + "harassment/threatening": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'harassment/threatening'." + }), + "illicit": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'illicit'." + }), + "illicit/violent": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'illicit/violent'." + }), + "self-harm": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'self-harm'." + }), + "self-harm/intent": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'self-harm/intent'." + }), + "self-harm/instructions": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'self-harm/instructions'." + }), + "sexual": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'sexual'." + }), + "sexual/minors": Schema.Array(Schema.Literal("text")).annotate({ + "description": "The applied input type(s) for the category 'sexual/minors'." + }), + "violence": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'violence'." + }), + "violence/graphic": Schema.Array(Schema.Literals(["text", "image"])).annotate({ + "description": "The applied input type(s) for the category 'violence/graphic'." + }) + }).annotate({ "description": "A list of the categories along with the input type(s) that the score applies to." }) + })).annotate({ "description": "A list of moderation objects." }) +}).annotate({ "description": "Represents if a given text input is potentially harmful." }) +export type CreateTranscriptionResponseJson = { + readonly "text": string + readonly "logprobs"?: ReadonlyArray< + { readonly "token"?: string; readonly "logprob"?: number; readonly "bytes"?: ReadonlyArray } + > + readonly "usage"?: { + readonly "type": "tokens" + readonly "input_tokens": number + readonly "input_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + readonly "output_tokens": number + readonly "total_tokens": number + } | { readonly "type": "duration"; readonly "seconds": number } +} +export const CreateTranscriptionResponseJson = Schema.Struct({ + "text": Schema.String.annotate({ "description": "The transcribed text." }), + "logprobs": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "token": Schema.optionalKey(Schema.String.annotate({ "description": "The token in the transcription." })), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "description": "The log probability of the token." }).check(Schema.isFinite()) + ), + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ "description": "The bytes of the token." }) + ) + })).annotate({ + "description": + "The log probabilities of the tokens in the transcription. Only returned with the models `gpt-4o-transcribe` and `gpt-4o-mini-transcribe` if `logprobs` is added to the `include` array.\n" + }) + ), + "usage": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("tokens").annotate({ + "description": "The type of the usage object. Always `tokens` for this variant." + }), + "input_tokens": Schema.Number.annotate({ "description": "Number of input tokens billed for this request." }) + .check(Schema.isInt()), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of text tokens billed for this request." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio tokens billed for this request." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the input tokens billed for this request." }) + ), + "output_tokens": Schema.Number.annotate({ "description": "Number of output tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (input + output)." }).check( + Schema.isInt() + ) + }).annotate({ "title": "Token Usage", "description": "Token usage statistics for the request." }), + Schema.Struct({ + "type": Schema.Literal("duration").annotate({ + "description": "The type of the usage object. Always `duration` for this variant." + }), + "seconds": Schema.Number.annotate({ + "description": "Duration of the input audio in seconds.", + "format": "double" + }).check(Schema.isFinite()) + }).annotate({ "title": "Duration Usage", "description": "Token usage statistics for the request." }) + ], { mode: "oneOf" })) +}).annotate({ "description": "Represents a transcription response returned by model, based on the provided input." }) +export type CreateTranslationRequest = { + readonly "file": string + readonly "model": string | "whisper-1" + readonly "prompt"?: string + readonly "response_format"?: "json" | "text" | "srt" | "verbose_json" | "vtt" + readonly "temperature"?: number +} +export const CreateTranslationRequest = Schema.Struct({ + "file": Schema.String.annotate({ + "description": + "The audio file object (not file name) translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.\n", + "format": "binary" + }), + "model": Schema.Union([Schema.String, Schema.Literal("whisper-1")]).annotate({ + "description": + "ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2 model) is currently available.\n" + }), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio segment. The [prompt](/docs/guides/speech-to-text#prompting) should be in English.\n" + }) + ), + "response_format": Schema.optionalKey( + Schema.Literals(["json", "text", "srt", "verbose_json", "vtt"]).annotate({ + "description": + "The format of the output, in one of these options: `json`, `text`, `srt`, `verbose_json`, or `vtt`.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use [log probability](https://en.wikipedia.org/wiki/Log_probability) to automatically increase the temperature until certain thresholds are hit.\n" + }).check(Schema.isFinite()) + ) +}) +export type CreateTranslationResponseJson = { readonly "text": string } +export const CreateTranslationResponseJson = Schema.Struct({ "text": Schema.String }) +export type CreateVoiceConsentRequest = { + readonly "name": string + readonly "recording": string + readonly "language": string +} +export const CreateVoiceConsentRequest = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The label to use for this consent recording." }), + "recording": Schema.String.annotate({ + "description": + "The consent audio recording file. Maximum size is 10 MiB.\n\nSupported MIME types:\n`audio/mpeg`, `audio/wav`, `audio/x-wav`, `audio/ogg`, `audio/aac`, `audio/flac`, `audio/webm`, `audio/mp4`.\n", + "format": "binary" + }), + "language": Schema.String.annotate({ + "description": "The BCP 47 language tag for the consent phrase (for example, `en-US`)." + }) +}) +export type CreateVoiceRequest = { + readonly "name": string + readonly "audio_sample": string + readonly "consent": string +} +export const CreateVoiceRequest = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the new voice." }), + "audio_sample": Schema.String.annotate({ + "description": + "The sample audio recording file. Maximum size is 10 MiB.\n\nSupported MIME types:\n`audio/mpeg`, `audio/wav`, `audio/x-wav`, `audio/ogg`, `audio/aac`, `audio/flac`, `audio/webm`, `audio/mp4`.\n", + "format": "binary" + }), + "consent": Schema.String.annotate({ "description": "The consent recording ID (for example, `cons_1234`)." }) +}) +export type CustomToolCall = { + readonly "type": "custom_tool_call" + readonly "id"?: string + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "input": string +} +export const CustomToolCall = Schema.Struct({ + "type": Schema.Literal("custom_tool_call").annotate({ + "description": "The type of the custom tool call. Always `custom_tool_call`.\n" + }), + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The unique ID of the custom tool call in the OpenAI platform.\n" }) + ), + "call_id": Schema.String.annotate({ + "description": "An identifier used to map this custom tool call to a tool call output.\n" + }), + "namespace": Schema.optionalKey( + Schema.String.annotate({ "description": "The namespace of the custom tool being called.\n" }) + ), + "name": Schema.String.annotate({ "description": "The name of the custom tool being called.\n" }), + "input": Schema.String.annotate({ "description": "The input for the custom tool call generated by the model.\n" }) +}).annotate({ "title": "Custom tool call", "description": "A call to a custom tool created by the model.\n" }) +export type CustomToolCallResource = { + readonly "type": "custom_tool_call" + readonly "id": string + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "input": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const CustomToolCallResource = Schema.Struct({ + "type": Schema.Literal("custom_tool_call").annotate({ + "description": "The type of the custom tool call. Always `custom_tool_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the custom tool call item.\n" }), + "call_id": Schema.String.annotate({ + "description": "An identifier used to map this custom tool call to a tool call output.\n" + }), + "namespace": Schema.optionalKey( + Schema.String.annotate({ "description": "The namespace of the custom tool being called.\n" }) + ), + "name": Schema.String.annotate({ "description": "The name of the custom tool being called.\n" }), + "input": Schema.String.annotate({ "description": "The input for the custom tool call generated by the model.\n" }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item.\n" }) + ) +}).annotate({ "title": "Custom tool call", "description": "A call to a custom tool created by the model.\n" }) +export type CustomToolChatCompletions = { + readonly "type": "custom" + readonly "custom": { + readonly "name": string + readonly "description"?: string + readonly "format"?: { readonly "type": "text" } | { + readonly "type": "grammar" + readonly "grammar": { readonly "definition": string; readonly "syntax": "lark" | "regex" } + } + } +} +export const CustomToolChatCompletions = Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "The type of the custom tool. Always `custom`." }), + "custom": Schema.Struct({ + "name": Schema.String.annotate({ + "description": "The name of the custom tool, used to identify it in tool calls." + }), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional description of the custom tool, used to provide more context.\n" + }) + ), + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "Unconstrained text format. Always `text`." }) + }).annotate({ "title": "Text format", "description": "Unconstrained free-form text." }), + Schema.Struct({ + "type": Schema.Literal("grammar").annotate({ "description": "Grammar format. Always `grammar`." }), + "grammar": Schema.Struct({ + "definition": Schema.String.annotate({ "description": "The grammar definition." }), + "syntax": Schema.Literals(["lark", "regex"]).annotate({ + "description": "The syntax of the grammar definition. One of `lark` or `regex`." + }) + }).annotate({ "title": "Grammar format", "description": "Your chosen grammar." }) + }).annotate({ "title": "Grammar format", "description": "A grammar defined by the user." }) + ], { mode: "oneOf" }).annotate({ + "description": "The input format for the custom tool. Default is unconstrained text.\n" + }) + ) + }).annotate({ "title": "Custom tool properties", "description": "Properties of the custom tool.\n" }) +}).annotate({ "title": "Custom tool", "description": "A custom tool that processes input using a specified format.\n" }) +export type DeleteAssistantResponse = { + readonly "id": string + readonly "deleted": boolean + readonly "object": "assistant.deleted" +} +export const DeleteAssistantResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.Literal("assistant.deleted") +}) +export type DeleteCertificateResponse = { readonly "object": "certificate.deleted"; readonly "id": string } +export const DeleteCertificateResponse = Schema.Struct({ + "object": Schema.Literal("certificate.deleted").annotate({ + "description": "The object type, must be `certificate.deleted`." + }), + "id": Schema.String.annotate({ "description": "The ID of the certificate that was deleted." }) +}) +export type DeleteFileResponse = { readonly "id": string; readonly "object": "file"; readonly "deleted": boolean } +export const DeleteFileResponse = Schema.Struct({ + "id": Schema.String, + "object": Schema.Literal("file"), + "deleted": Schema.Boolean +}) +export type DeleteFineTuningCheckpointPermissionResponse = { + readonly "id": string + readonly "object": "checkpoint.permission" + readonly "deleted": boolean +} +export const DeleteFineTuningCheckpointPermissionResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The ID of the fine-tuned model checkpoint permission that was deleted." + }), + "object": Schema.Literal("checkpoint.permission").annotate({ + "description": "The object type, which is always \"checkpoint.permission\"." + }), + "deleted": Schema.Boolean.annotate({ + "description": "Whether the fine-tuned model checkpoint permission was successfully deleted." + }) +}) +export type DeleteMessageResponse = { + readonly "id": string + readonly "deleted": boolean + readonly "object": "thread.message.deleted" +} +export const DeleteMessageResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.Literal("thread.message.deleted") +}) +export type DeleteModelResponse = { readonly "id": string; readonly "deleted": boolean; readonly "object": string } +export const DeleteModelResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.String +}) +export type DeleteThreadResponse = { + readonly "id": string + readonly "deleted": boolean + readonly "object": "thread.deleted" +} +export const DeleteThreadResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.Literal("thread.deleted") +}) +export type DeleteVectorStoreFileResponse = { + readonly "id": string + readonly "deleted": boolean + readonly "object": "vector_store.file.deleted" +} +export const DeleteVectorStoreFileResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.Literal("vector_store.file.deleted") +}) +export type DeleteVectorStoreResponse = { + readonly "id": string + readonly "deleted": boolean + readonly "object": "vector_store.deleted" +} +export const DeleteVectorStoreResponse = Schema.Struct({ + "id": Schema.String, + "deleted": Schema.Boolean, + "object": Schema.Literal("vector_store.deleted") +}) +export type DeletedRoleAssignmentResource = { readonly "object": string; readonly "deleted": boolean } +export const DeletedRoleAssignmentResource = Schema.Struct({ + "object": Schema.String.annotate({ + "description": "Identifier for the deleted assignment, such as `group.role.deleted` or `user.role.deleted`." + }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the assignment was removed." }) +}).annotate({ "description": "Confirmation payload returned after unassigning a role." }) +export type DoneEvent = { readonly "event": "done"; readonly "data": "[DONE]" } +export const DoneEvent = Schema.Struct({ "event": Schema.Literal("done"), "data": Schema.Literal("[DONE]") }).annotate({ + "description": "Occurs when a stream ends." +}) +export type Embedding = { + readonly "index": number + readonly "embedding": ReadonlyArray + readonly "object": "embedding" +} +export const Embedding = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the embedding in the list of embeddings." }).check( + Schema.isInt() + ), + "embedding": Schema.Array(Schema.Number.annotate({ "format": "float" }).check(Schema.isFinite())).annotate({ + "description": + "The embedding vector, which is a list of floats. The length of vector depends on the model as listed in the [embedding guide](/docs/guides/embeddings).\n" + }), + "object": Schema.Literal("embedding").annotate({ "description": "The object type, which is always \"embedding\"." }) +}).annotate({ "description": "Represents an embedding vector returned by embedding endpoint.\n" }) +export type Error = { + readonly "code": string | null + readonly "message": string + readonly "param": string | null + readonly "type": string +} +export const Error = Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "message": Schema.String, + "param": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.String +}) +export type EvalApiError = { readonly "code": string; readonly "message": string } +export const EvalApiError = Schema.Struct({ + "code": Schema.String.annotate({ "description": "The error code." }), + "message": Schema.String.annotate({ "description": "The error message." }) +}).annotate({ "title": "EvalApiError", "description": "An object representing an error response from the Eval API.\n" }) +export type EvalGraderPython = { + readonly "type": "python" + readonly "name": string + readonly "source": string + readonly "image_tag"?: string + readonly "pass_threshold"?: number +} +export const EvalGraderPython = Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ), + "pass_threshold": Schema.optionalKey( + Schema.Number.annotate({ "description": "The threshold for the score." }).check(Schema.isFinite()) + ) +}).annotate({ + "title": "PythonGrader", + "description": "A PythonGrader object that runs a python script on the input.\n" +}) +export type EvalGraderStringCheck = { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" +} +export const EvalGraderStringCheck = Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) +}).annotate({ + "title": "StringCheckGrader", + "description": + "A StringCheckGrader object that performs a string comparison between input and reference using a specified operation.\n" +}) +export type EvalGraderTextSimilarity = { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" + readonly "pass_threshold": number +} +export const EvalGraderTextSimilarity = Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }), + "pass_threshold": Schema.Number.annotate({ "description": "The threshold for the score." }).check(Schema.isFinite()) +}).annotate({ + "title": "TextSimilarityGrader", + "description": "A TextSimilarityGrader object which grades text based on similarity metrics.\n" +}) +export type EvalItemContentOutputText = { readonly "type": "output_text"; readonly "text": string } +export const EvalItemContentOutputText = Schema.Struct({ + "type": Schema.Literal("output_text").annotate({ + "description": "The type of the output text. Always `output_text`.\n" + }), + "text": Schema.String.annotate({ "description": "The text output from the model.\n" }) +}).annotate({ "title": "Output text", "description": "A text output from the model.\n" }) +export type EvalItemContentText = string +export const EvalItemContentText = Schema.String.annotate({ + "title": "Text input", + "description": "A text input to the model.\n" +}) +export type EvalItemInputImage = { + readonly "type": "input_image" + readonly "image_url": string + readonly "detail"?: string +} +export const EvalItemInputImage = Schema.Struct({ + "type": Schema.Literal("input_image").annotate({ + "description": "The type of the image input. Always `input_image`.\n" + }), + "image_url": Schema.String.annotate({ "description": "The URL of the image input.\n", "format": "uri" }), + "detail": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.\n" + }) + ) +}).annotate({ "title": "Input image", "description": "An image input block used within EvalItem content arrays." }) +export type EvalJsonlFileContentSource = { + readonly "type": "file_content" + readonly "content": ReadonlyArray<{ readonly "item": {}; readonly "sample"?: {} }> +} +export const EvalJsonlFileContentSource = Schema.Struct({ + "type": Schema.Literal("file_content").annotate({ + "description": "The type of jsonl source. Always `file_content`." + }), + "content": Schema.Array(Schema.Struct({ "item": Schema.Struct({}), "sample": Schema.optionalKey(Schema.Struct({})) })) + .annotate({ "description": "The content of the jsonl file." }) +}).annotate({ "title": "EvalJsonlFileContentSource" }) +export type EvalJsonlFileIdSource = { readonly "type": "file_id"; readonly "id": string } +export const EvalJsonlFileIdSource = Schema.Struct({ + "type": Schema.Literal("file_id").annotate({ "description": "The type of jsonl source. Always `file_id`." }), + "id": Schema.String.annotate({ "description": "The identifier of the file." }) +}).annotate({ "title": "EvalJsonlFileIdSource" }) +export type EvalResponsesSource = { + readonly "type": "responses" + readonly "metadata"?: {} | null + readonly "model"?: string | null + readonly "instructions_search"?: string | null + readonly "created_after"?: number | null + readonly "created_before"?: number | null + readonly "reasoning_effort"?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | null | null + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "users"?: ReadonlyArray | null + readonly "tools"?: ReadonlyArray | null +} +export const EvalResponsesSource = Schema.Struct({ + "type": Schema.Literal("responses").annotate({ "description": "The type of run data source. Always `responses`." }), + "metadata": Schema.optionalKey( + Schema.Union([ + Schema.Struct({}).annotate({ + "description": "Metadata filter for the responses. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The name of the model to find responses for. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ), + "instructions_search": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "Optional string to search the 'instructions' field. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ), + "created_after": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Only include items created after this timestamp (inclusive). This is a query parameter used to select responses." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]) + ), + "created_before": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Only include items created before this timestamp (inclusive). This is a query parameter used to select responses." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literals(["none", "minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Constrains effort on reasoning for\n[reasoning models](https://platform.openai.com/docs/guides/reasoning).\nCurrently supported values are `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Reducing\nreasoning effort can result in faster responses and fewer tokens used\non reasoning in a response.\n\n- `gpt-5.1` defaults to `none`, which does not perform reasoning. The supported reasoning values for `gpt-5.1` are `none`, `low`, `medium`, and `high`. Tool calls are supported for all reasoning values in gpt-5.1.\n- All models before `gpt-5.1` default to `medium` reasoning effort, and do not support `none`.\n- The `gpt-5-pro` model defaults to (and only supports) `high` reasoning effort.\n- `xhigh` is supported for all models after `gpt-5.1-codex-max`.\n" + }), + Schema.Null + ]).annotate({ + "description": "Optional reasoning effort parameter. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "Sampling temperature. This is a query parameter used to select responses." + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "Nucleus sampling parameter. This is a query parameter used to select responses." + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "users": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ + "description": "List of user identifiers. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ + "description": "List of tool names. This is a query parameter used to select responses." + }), + Schema.Null + ]) + ) +}).annotate({ + "title": "EvalResponsesSource", + "description": "A EvalResponsesSource object describing a run data source configuration.\n" +}) +export type EvalRunOutputItemResult = { + readonly "name": string + readonly "type"?: string + readonly "score": number + readonly "passed": boolean + readonly "sample"?: {} | null +} +export const EvalRunOutputItemResult = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "type": Schema.optionalKey( + Schema.String.annotate({ "description": "The grader type (for example, \"string-check-grader\")." }) + ), + "score": Schema.Number.annotate({ "description": "The numeric score produced by the grader." }).check( + Schema.isFinite() + ), + "passed": Schema.Boolean.annotate({ "description": "Whether the grader considered the output a pass." }), + "sample": Schema.optionalKey( + Schema.Union([Schema.Struct({}), Schema.Null]).annotate({ + "description": "Optional sample or intermediate data produced by the grader." + }) + ) +}).annotate({ + "title": "EvalRunOutputItemResult", + "description": "A single grader result for an evaluation run output item.\n" +}) +export type FileExpirationAfter = { readonly "anchor": "created_at"; readonly "seconds": number } +export const FileExpirationAfter = Schema.Struct({ + "anchor": Schema.Literal("created_at").annotate({ + "description": "Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`." + }), + "seconds": Schema.Number.annotate({ + "description": + "The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days).", + "format": "int64" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(3600)).check(Schema.isLessThanOrEqualTo(2592000)) +}).annotate({ + "title": "File expiration policy", + "description": + "The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted." +}) +export type FilePath = { readonly "type": "file_path"; readonly "file_id": string; readonly "index": number } +export const FilePath = Schema.Struct({ + "type": Schema.Literal("file_path").annotate({ "description": "The type of the file path. Always `file_path`.\n" }), + "file_id": Schema.String.annotate({ "description": "The ID of the file.\n" }), + "index": Schema.Number.annotate({ "description": "The index of the file in the list of files.\n" }).check( + Schema.isInt() + ) +}).annotate({ "title": "File path", "description": "A path to a file.\n" }) +export type FileSearchRanker = "auto" | "default_2024_08_21" +export const FileSearchRanker = Schema.Literals(["auto", "default_2024_08_21"]).annotate({ + "description": "The ranker to use for the file search. If not specified will use the `auto` ranker." +}) +export type FineTuneDPOHyperparameters = { + readonly "beta"?: "auto" | number + readonly "batch_size"?: "auto" | number + readonly "learning_rate_multiplier"?: "auto" | number + readonly "n_epochs"?: "auto" | number +} +export const FineTuneDPOHyperparameters = Schema.Struct({ + "beta": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isFinite()).check(Schema.isLessThanOrEqualTo(2)).check(Schema.isGreaterThan(0)) + ], { mode: "oneOf" }).annotate({ + "description": + "The beta value for the DPO method. A higher beta value will increase the weight of the penalty between the policy and reference model.\n" + }) + ), + "batch_size": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(256)) + ], { mode: "oneOf" }).annotate({ + "description": + "Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance.\n" + }) + ), + "learning_rate_multiplier": Schema.optionalKey( + Schema.Union([Schema.Literal("auto"), Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(0))], { + mode: "oneOf" + }).annotate({ + "description": + "Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting.\n" + }) + ), + "n_epochs": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(50)) + ], { mode: "oneOf" }).annotate({ + "description": + "The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset.\n" + }) + ) +}).annotate({ "description": "The hyperparameters used for the DPO fine-tuning job." }) +export type FineTuneReinforcementHyperparameters = { + readonly "batch_size"?: "auto" | number + readonly "learning_rate_multiplier"?: "auto" | number + readonly "n_epochs"?: "auto" | number + readonly "reasoning_effort"?: "default" | "low" | "medium" | "high" + readonly "compute_multiplier"?: "auto" | number + readonly "eval_interval"?: "auto" | number + readonly "eval_samples"?: "auto" | number +} +export const FineTuneReinforcementHyperparameters = Schema.Struct({ + "batch_size": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(256)) + ], { mode: "oneOf" }).annotate({ + "description": + "Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance.\n" + }) + ), + "learning_rate_multiplier": Schema.optionalKey( + Schema.Union([Schema.Literal("auto"), Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(0))], { + mode: "oneOf" + }).annotate({ + "description": + "Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting.\n" + }) + ), + "n_epochs": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(50)) + ], { mode: "oneOf" }).annotate({ + "description": + "The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset.\n" + }) + ), + "reasoning_effort": Schema.optionalKey( + Schema.Literals(["default", "low", "medium", "high"]).annotate({ "description": "Level of reasoning effort.\n" }) + ), + "compute_multiplier": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isFinite()).check(Schema.isLessThanOrEqualTo(10)).check(Schema.isGreaterThan(0.00001)) + ], { mode: "oneOf" }).annotate({ + "description": "Multiplier on amount of compute used for exploring search space during training.\n" + }) + ), + "eval_interval": Schema.optionalKey( + Schema.Union( + [Schema.Literal("auto"), Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1))], + { mode: "oneOf" } + ).annotate({ "description": "The number of training steps between evaluation runs.\n" }) + ), + "eval_samples": Schema.optionalKey( + Schema.Union( + [Schema.Literal("auto"), Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1))], + { mode: "oneOf" } + ).annotate({ "description": "Number of evaluation samples to generate per training step.\n" }) + ) +}).annotate({ "description": "The hyperparameters used for the reinforcement fine-tuning job." }) +export type FineTuneSupervisedHyperparameters = { + readonly "batch_size"?: "auto" | number + readonly "learning_rate_multiplier"?: "auto" | number + readonly "n_epochs"?: "auto" | number +} +export const FineTuneSupervisedHyperparameters = Schema.Struct({ + "batch_size": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(256)) + ], { mode: "oneOf" }).annotate({ + "description": + "Number of examples in each batch. A larger batch size means that model parameters are updated less frequently, but with lower variance.\n" + }) + ), + "learning_rate_multiplier": Schema.optionalKey( + Schema.Union([Schema.Literal("auto"), Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(0))], { + mode: "oneOf" + }).annotate({ + "description": + "Scaling factor for the learning rate. A smaller learning rate may be useful to avoid overfitting.\n" + }) + ), + "n_epochs": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(50)) + ], { mode: "oneOf" }).annotate({ + "description": + "The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset.\n" + }) + ) +}).annotate({ "description": "The hyperparameters used for the fine-tuning job." }) +export type FineTuningCheckpointPermission = { + readonly "id": string + readonly "created_at": number + readonly "project_id": string + readonly "object": "checkpoint.permission" +} +export const FineTuningCheckpointPermission = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The permission identifier, which can be referenced in the API endpoints." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the permission was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "project_id": Schema.String.annotate({ "description": "The project identifier that the permission is for." }), + "object": Schema.Literal("checkpoint.permission").annotate({ + "description": "The object type, which is always \"checkpoint.permission\"." + }) +}).annotate({ + "title": "FineTuningCheckpointPermission", + "description": "The `checkpoint.permission` object represents a permission for a fine-tuned model checkpoint.\n" +}) +export type FineTuningIntegration = { + readonly "type": "wandb" + readonly "wandb": { + readonly "project": string + readonly "name"?: string | null + readonly "entity"?: string | null + readonly "tags"?: ReadonlyArray + } +} +export const FineTuningIntegration = Schema.Struct({ + "type": Schema.Literal("wandb").annotate({ + "description": "The type of the integration being enabled for the fine-tuning job" + }), + "wandb": Schema.Struct({ + "project": Schema.String.annotate({ + "description": "The name of the project that the new run will be created under.\n" + }), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "A display name to set for the run. If not set, we will use the Job ID as the name.\n" + }), + Schema.Null + ]) + ), + "entity": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The entity to use for the run. This allows you to set the team or username of the WandB user that you would\nlike associated with the run. If not set, the default entity for the registered WandB API key is used.\n" + }), + Schema.Null + ]) + ), + "tags": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some\ndefault tags are generated by OpenAI: \"openai/finetune\", \"openai/{base-model}\", \"openai/{ftjob-abcdef}\".\n" + }) + ) + }).annotate({ + "description": + "The settings for your integration with Weights and Biases. This payload specifies the project that\nmetrics will be sent to. Optionally, you can set an explicit display name for your run, add tags\nto your run, and set a default entity (team, username, etc) to be associated with your run.\n" + }) +}).annotate({ "title": "Fine-Tuning Job Integration" }) +export type FineTuningJobCheckpoint = { + readonly "id": string + readonly "created_at": number + readonly "fine_tuned_model_checkpoint": string + readonly "step_number": number + readonly "metrics": { + readonly "step"?: number + readonly "train_loss"?: number + readonly "train_mean_token_accuracy"?: number + readonly "valid_loss"?: number + readonly "valid_mean_token_accuracy"?: number + readonly "full_valid_loss"?: number + readonly "full_valid_mean_token_accuracy"?: number + } + readonly "fine_tuning_job_id": string + readonly "object": "fine_tuning.job.checkpoint" +} +export const FineTuningJobCheckpoint = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The checkpoint identifier, which can be referenced in the API endpoints." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the checkpoint was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "fine_tuned_model_checkpoint": Schema.String.annotate({ + "description": "The name of the fine-tuned checkpoint model that is created." + }), + "step_number": Schema.Number.annotate({ "description": "The step number that the checkpoint was created at." }).check( + Schema.isInt() + ), + "metrics": Schema.Struct({ + "step": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "train_loss": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "train_mean_token_accuracy": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "valid_loss": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "valid_mean_token_accuracy": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "full_valid_loss": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "full_valid_mean_token_accuracy": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }).annotate({ "description": "Metrics at the step number during the fine-tuning job." }), + "fine_tuning_job_id": Schema.String.annotate({ + "description": "The name of the fine-tuning job that this checkpoint was created from." + }), + "object": Schema.Literal("fine_tuning.job.checkpoint").annotate({ + "description": "The object type, which is always \"fine_tuning.job.checkpoint\"." + }) +}).annotate({ + "title": "FineTuningJobCheckpoint", + "description": + "The `fine_tuning.job.checkpoint` object represents a model checkpoint for a fine-tuning job that is ready to use.\n" +}) +export type FineTuningJobEvent = { + readonly "object": "fine_tuning.job.event" + readonly "id": string + readonly "created_at": number + readonly "level": "info" | "warn" | "error" + readonly "message": string + readonly "type"?: "message" | "metrics" + readonly "data"?: {} +} +export const FineTuningJobEvent = Schema.Struct({ + "object": Schema.Literal("fine_tuning.job.event").annotate({ + "description": "The object type, which is always \"fine_tuning.job.event\"." + }), + "id": Schema.String.annotate({ "description": "The object identifier." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the fine-tuning job was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "level": Schema.Literals(["info", "warn", "error"]).annotate({ "description": "The log level of the event." }), + "message": Schema.String.annotate({ "description": "The message of the event." }), + "type": Schema.optionalKey(Schema.Literals(["message", "metrics"]).annotate({ "description": "The type of event." })), + "data": Schema.optionalKey(Schema.Struct({}).annotate({ "description": "The data associated with the event." })) +}).annotate({ "description": "Fine-tuning job event object" }) +export type FunctionParameters = {} +export const FunctionParameters = Schema.Struct({}).annotate({ + "description": + "The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. \n\nOmitting `parameters` defines a function with an empty parameter list." +}) +export type FunctionToolCall = { + readonly "id"?: string + readonly "type": "function_call" + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "arguments": string + readonly "status"?: "in_progress" | "completed" | "incomplete" +} +export const FunctionToolCall = Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the function tool call.\n" })), + "type": Schema.Literal("function_call").annotate({ + "description": "The type of the function tool call. Always `function_call`.\n" + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model.\n" + }), + "namespace": Schema.optionalKey(Schema.String.annotate({ "description": "The namespace of the function to run.\n" })), + "name": Schema.String.annotate({ "description": "The name of the function to run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of the arguments to pass to the function.\n" }), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ) +}).annotate({ + "title": "Function tool call", + "description": + "A tool call to run a function. See the \n[function calling guide](/docs/guides/function-calling) for more information.\n" +}) +export type FunctionToolCallResource = { + readonly "id": string + readonly "type": "function_call" + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "arguments": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const FunctionToolCallResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the function tool call.\n" }), + "type": Schema.Literal("function_call").annotate({ + "description": "The type of the function tool call. Always `function_call`.\n" + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model.\n" + }), + "namespace": Schema.optionalKey(Schema.String.annotate({ "description": "The namespace of the function to run.\n" })), + "name": Schema.String.annotate({ "description": "The name of the function to run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of the arguments to pass to the function.\n" }), + "status": Schema.Union([ + Schema.Literal("in_progress").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + Schema.Literal("completed").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + Schema.Literal("incomplete").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item.\n" }) + ) +}).annotate({ + "title": "Function tool call", + "description": + "A tool call to run a function. See the \n[function calling guide](/docs/guides/function-calling) for more information.\n" +}) +export type GraderPython = { + readonly "type": "python" + readonly "name": string + readonly "source": string + readonly "image_tag"?: string +} +export const GraderPython = Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ) +}).annotate({ + "title": "PythonGrader", + "description": "A PythonGrader object that runs a python script on the input.\n" +}) +export type GraderStringCheck = { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" +} +export const GraderStringCheck = Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) +}).annotate({ + "title": "StringCheckGrader", + "description": + "A StringCheckGrader object that performs a string comparison between input and reference using a specified operation.\n" +}) +export type GraderTextSimilarity = { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" +} +export const GraderTextSimilarity = Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }) +}).annotate({ + "title": "TextSimilarityGrader", + "description": "A TextSimilarityGrader object which grades text based on similarity metrics.\n" +}) +export type Group = { + readonly "object": "group" + readonly "id": string + readonly "name": string + readonly "created_at": number + readonly "scim_managed": boolean +} +export const Group = Schema.Struct({ + "object": Schema.Literal("group").annotate({ "description": "Always `group`." }), + "id": Schema.String.annotate({ "description": "Identifier for the group." }), + "name": Schema.String.annotate({ "description": "Display name of the group." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the group was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "scim_managed": Schema.Boolean.annotate({ "description": "Whether the group is managed through SCIM." }) +}).annotate({ "description": "Summary information about a group returned in role assignment responses." }) +export type GroupDeletedResource = { + readonly "object": "group.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const GroupDeletedResource = Schema.Struct({ + "object": Schema.Literal("group.deleted").annotate({ "description": "Always `group.deleted`." }), + "id": Schema.String.annotate({ "description": "Identifier of the deleted group." }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the group was deleted." }) +}).annotate({ "description": "Confirmation payload returned after deleting a group." }) +export type GroupResourceWithSuccess = { + readonly "id": string + readonly "name": string + readonly "created_at": number + readonly "is_scim_managed": boolean +} +export const GroupResourceWithSuccess = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier for the group." }), + "name": Schema.String.annotate({ "description": "Updated display name for the group." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the group was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "is_scim_managed": Schema.Boolean.annotate({ + "description": "Whether the group is managed through SCIM and controlled by your identity provider." + }) +}).annotate({ "description": "Response returned after updating a group." }) +export type GroupResponse = { + readonly "id": string + readonly "name": string + readonly "created_at": number + readonly "is_scim_managed": boolean + readonly "group_type": string +} +export const GroupResponse = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier for the group." }), + "name": Schema.String.annotate({ "description": "Display name of the group." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the group was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "is_scim_managed": Schema.Boolean.annotate({ + "description": "Whether the group is managed through SCIM and controlled by your identity provider." + }), + "group_type": Schema.String.annotate({ "description": "The type of the group." }) +}).annotate({ "description": "Details about an organization group." }) +export type GroupUser = { readonly "id": string; readonly "name": string; readonly "email": string | null } +export const GroupUser = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.String.annotate({ "description": "The name of the user." }), + "email": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The email address of the user." }) +}).annotate({ "description": "Represents an individual user returned when inspecting group membership." }) +export type GroupUserAssignment = { + readonly "object": "group.user" + readonly "user_id": string + readonly "group_id": string +} +export const GroupUserAssignment = Schema.Struct({ + "object": Schema.Literal("group.user").annotate({ "description": "Always `group.user`." }), + "user_id": Schema.String.annotate({ "description": "Identifier of the user that was added." }), + "group_id": Schema.String.annotate({ "description": "Identifier of the group the user was added to." }) +}).annotate({ "description": "Confirmation payload returned after adding a user to a group." }) +export type GroupUserDeletedResource = { readonly "object": "group.user.deleted"; readonly "deleted": boolean } +export const GroupUserDeletedResource = Schema.Struct({ + "object": Schema.Literal("group.user.deleted").annotate({ "description": "Always `group.user.deleted`." }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the group membership was removed." }) +}).annotate({ "description": "Confirmation payload returned after removing a user from a group." }) +export type HostedToolPermission = { readonly "enabled": boolean } +export const HostedToolPermission = Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Whether the hosted tool is enabled for the project." }) +}).annotate({ "description": "Permission state for a single hosted tool on a project." }) +export type HostedToolPermissionUpdate = { readonly "enabled": boolean } +export const HostedToolPermissionUpdate = Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Whether to enable the hosted tool for the project." }) +}) +export type Image = { readonly "b64_json"?: string; readonly "url"?: string; readonly "revised_prompt"?: string } +export const Image = Schema.Struct({ + "b64_json": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The base64-encoded JSON of the generated image. Returned by default for the GPT image models, and only present if `response_format` is set to `b64_json` for `dall-e-2` and `dall-e-3`." + }) + ), + "url": Schema.optionalKey( + Schema.String.annotate({ + "description": + "When using `dall-e-2` or `dall-e-3`, the URL of the generated image if `response_format` is set to `url` (default value). Unsupported for the GPT image models.", + "format": "uri" + }) + ), + "revised_prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "For `dall-e-3` only, the revised prompt that was used to generate the image." + }) + ) +}).annotate({ "description": "Represents the content or the URL of an image generated by the OpenAI API." }) +export type ImageEditPartialImageEvent = { + readonly "type": "image_edit.partial_image" + readonly "b64_json": string + readonly "created_at": number + readonly "size": "1024x1024" | "1024x1536" | "1536x1024" | "auto" + readonly "quality": "low" | "medium" | "high" | "auto" + readonly "background": "transparent" | "opaque" | "auto" + readonly "output_format": "png" | "webp" | "jpeg" + readonly "partial_image_index": number +} +export const ImageEditPartialImageEvent = Schema.Struct({ + "type": Schema.Literal("image_edit.partial_image").annotate({ + "description": "The type of the event. Always `image_edit.partial_image`.\n" + }), + "b64_json": Schema.String.annotate({ + "description": "Base64-encoded partial image data, suitable for rendering as an image.\n" + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp when the event was created.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "size": Schema.Literals(["1024x1024", "1024x1536", "1536x1024", "auto"]).annotate({ + "description": "The size of the requested edited image.\n" + }), + "quality": Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": "The quality setting for the requested edited image.\n" + }), + "background": Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": "The background setting for the requested edited image.\n" + }), + "output_format": Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format for the requested edited image.\n" + }), + "partial_image_index": Schema.Number.annotate({ "description": "0-based index for the partial image (streaming).\n" }) + .check(Schema.isInt()) +}).annotate({ "description": "Emitted when a partial image is available during image editing streaming.\n" }) +export type ImageGenPartialImageEvent = { + readonly "type": "image_generation.partial_image" + readonly "b64_json": string + readonly "created_at": number + readonly "size": "1024x1024" | "1024x1536" | "1536x1024" | "auto" + readonly "quality": "low" | "medium" | "high" | "auto" + readonly "background": "transparent" | "opaque" | "auto" + readonly "output_format": "png" | "webp" | "jpeg" + readonly "partial_image_index": number +} +export const ImageGenPartialImageEvent = Schema.Struct({ + "type": Schema.Literal("image_generation.partial_image").annotate({ + "description": "The type of the event. Always `image_generation.partial_image`.\n" + }), + "b64_json": Schema.String.annotate({ + "description": "Base64-encoded partial image data, suitable for rendering as an image.\n" + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp when the event was created.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "size": Schema.Literals(["1024x1024", "1024x1536", "1536x1024", "auto"]).annotate({ + "description": "The size of the requested image.\n" + }), + "quality": Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": "The quality setting for the requested image.\n" + }), + "background": Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": "The background setting for the requested image.\n" + }), + "output_format": Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format for the requested image.\n" + }), + "partial_image_index": Schema.Number.annotate({ "description": "0-based index for the partial image (streaming).\n" }) + .check(Schema.isInt()) +}).annotate({ "description": "Emitted when a partial image is available during image generation streaming.\n" }) +export type ImageGenToolCall = { + readonly "type": "image_generation_call" + readonly "id": string + readonly "status": "in_progress" | "completed" | "generating" | "failed" + readonly "result": string | null +} +export const ImageGenToolCall = Schema.Struct({ + "type": Schema.Literal("image_generation_call").annotate({ + "description": "The type of the image generation call. Always `image_generation_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the image generation call.\n" }), + "status": Schema.Literals(["in_progress", "completed", "generating", "failed"]).annotate({ + "description": "The status of the image generation call.\n" + }), + "result": Schema.Union([ + Schema.String.annotate({ "description": "The generated image encoded in base64.\n" }), + Schema.Null + ]) +}).annotate({ "title": "Image generation call", "description": "An image generation request made by the model.\n" }) +export type ImageRefParam = { readonly "image_url": string; readonly "file_id"?: string } | { + readonly "file_id": string + readonly "image_url"?: string +} +export const ImageRefParam = Schema.Union([ + Schema.Struct({ + "image_url": Schema.String.annotate({ + "description": "A fully qualified URL or base64-encoded data URL.", + "format": "uri" + }).check(Schema.isMaxLength(20971520)), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The File API ID of an uploaded image to use as input." }) + ) + }).annotate({ + "description": + "Reference an input image by either URL or uploaded file ID.\nProvide exactly one of `image_url` or `file_id`.\n" + }), + Schema.Struct({ + "file_id": Schema.String.annotate({ "description": "The File API ID of an uploaded image to use as input." }), + "image_url": Schema.optionalKey( + Schema.String.annotate({ "description": "A fully qualified URL or base64-encoded data URL.", "format": "uri" }) + .check(Schema.isMaxLength(20971520)) + ) + }).annotate({ + "description": + "Reference an input image by either URL or uploaded file ID.\nProvide exactly one of `image_url` or `file_id`.\n" + }) +]) +export type ImagesUsage = { + readonly "total_tokens": number + readonly "input_tokens": number + readonly "output_tokens": number + readonly "input_tokens_details": { readonly "text_tokens": number; readonly "image_tokens": number } +} +export const ImagesUsage = Schema.Struct({ + "total_tokens": Schema.Number.annotate({ + "description": "The total number of tokens (images and text) used for the image generation.\n" + }).check(Schema.isInt()), + "input_tokens": Schema.Number.annotate({ + "description": "The number of tokens (images and text) in the input prompt." + }).check(Schema.isInt()), + "output_tokens": Schema.Number.annotate({ "description": "The number of image tokens in the output image." }).check( + Schema.isInt() + ), + "input_tokens_details": Schema.Struct({ + "text_tokens": Schema.Number.annotate({ "description": "The number of text tokens in the input prompt." }).check( + Schema.isInt() + ), + "image_tokens": Schema.Number.annotate({ "description": "The number of image tokens in the input prompt." }).check( + Schema.isInt() + ) + }).annotate({ "description": "The input tokens detailed information for the image generation." }) +}).annotate({ "description": "For the GPT image models only, the token usage information for the image generation.\n" }) +export type InputAudio = { + readonly "type": "input_audio" + readonly "input_audio": { readonly "data": string; readonly "format": "mp3" | "wav" } +} +export const InputAudio = Schema.Struct({ + "type": Schema.Literal("input_audio").annotate({ + "description": "The type of the input item. Always `input_audio`.\n" + }), + "input_audio": Schema.Struct({ + "data": Schema.String.annotate({ "description": "Base64-encoded audio data.\n" }), + "format": Schema.Literals(["mp3", "wav"]).annotate({ + "description": "The format of the audio data. Currently supported formats are `mp3` and\n`wav`.\n" + }) + }) +}).annotate({ "title": "Input audio", "description": "An audio input to the model.\n" }) +export type Invite = { + readonly "object": "organization.invite" + readonly "id": string + readonly "email": string + readonly "role": "owner" | "reader" + readonly "status": "accepted" | "expired" | "pending" + readonly "created_at": number + readonly "expires_at"?: number | null + readonly "accepted_at"?: number | null + readonly "projects": ReadonlyArray<{ readonly "id": string; readonly "role": "member" | "owner" }> +} +export const Invite = Schema.Struct({ + "object": Schema.Literal("organization.invite").annotate({ + "description": "The object type, which is always `organization.invite`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "email": Schema.String.annotate({ "description": "The email address of the individual to whom the invite was sent" }), + "role": Schema.Literals(["owner", "reader"]).annotate({ "description": "`owner` or `reader`" }), + "status": Schema.Literals(["accepted", "expired", "pending"]).annotate({ + "description": "`accepted`,`expired`, or `pending`" + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the invite was sent.", + "format": "unixtime" + }).check(Schema.isInt()), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]).annotate({ + "description": "The Unix timestamp (in seconds) of when the invite expires." + }) + ), + "accepted_at": Schema.optionalKey( + Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]).annotate({ + "description": "The Unix timestamp (in seconds) of when the invite was accepted." + }) + ), + "projects": Schema.Array( + Schema.Struct({ + "id": Schema.String.annotate({ "description": "Project's public ID" }), + "role": Schema.Literals(["member", "owner"]).annotate({ "description": "Project membership role" }) + }) + ).annotate({ "description": "The projects that were granted membership upon acceptance of the invite." }) +}).annotate({ "description": "Represents an individual `invite` to the organization." }) +export type InviteDeleteResponse = { + readonly "object": "organization.invite.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const InviteDeleteResponse = Schema.Struct({ + "object": Schema.Literal("organization.invite.deleted").annotate({ + "description": "The object type, which is always `organization.invite.deleted`" + }), + "id": Schema.String, + "deleted": Schema.Boolean +}) +export type InviteProjectGroupBody = { readonly "group_id": string; readonly "role": string } +export const InviteProjectGroupBody = Schema.Struct({ + "group_id": Schema.String.annotate({ "description": "Identifier of the group to add to the project." }), + "role": Schema.String.annotate({ "description": "Identifier of the project role to grant to the group." }) +}).annotate({ "description": "Request payload for granting a group access to a project." }) +export type InviteRequest = { + readonly "email": string + readonly "role": "reader" | "owner" + readonly "projects"?: ReadonlyArray<{ readonly "id": string; readonly "role": "member" | "owner" }> +} +export const InviteRequest = Schema.Struct({ + "email": Schema.String.annotate({ "description": "Send an email to this address" }), + "role": Schema.Literals(["reader", "owner"]).annotate({ "description": "`owner` or `reader`" }), + "projects": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "id": Schema.String.annotate({ "description": "Project's public ID" }), + "role": Schema.Literals(["member", "owner"]).annotate({ "description": "Project membership role" }) + }) + ).annotate({ + "description": + "An array of projects to which membership is granted at the same time the org invite is accepted. If omitted, the user will be invited to the default project for compatibility with legacy behavior. If empty list is passed, the user will not be invited to any projects, including the default one." + }) + ) +}) +export type LocalShellToolCallOutput = { + readonly "type": "local_shell_call_output" + readonly "id": string + readonly "output": string + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + readonly "call_id": unknown +} +export const LocalShellToolCallOutput = Schema.Struct({ + "type": Schema.Literal("local_shell_call_output").annotate({ + "description": "The type of the local shell tool call output. Always `local_shell_call_output`.\n" + }), + "id": Schema.String.annotate({ + "description": "The unique ID of the local shell tool call generated by the model.\n" + }), + "output": Schema.String.annotate({ "description": "A JSON string of the output of the local shell tool call.\n" }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the item. One of `in_progress`, `completed`, or `incomplete`.\n" + }), + Schema.Null + ]) + ), + "call_id": Schema.Unknown +}).annotate({ "title": "Local shell call output", "description": "The output of a local shell tool call.\n" }) +export type LogProbProperties = { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray +} +export const LogProbProperties = Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token that was used to generate the log probability.\n" }), + "logprob": Schema.Number.annotate({ "description": "The log probability of the token.\n" }).check(Schema.isFinite()), + "bytes": Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": "The bytes that were used to generate the log probability.\n" + }) +}).annotate({ "description": "A log probability object.\n" }) +export type MCPApprovalRequest = { + readonly "type": "mcp_approval_request" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string +} +export const MCPApprovalRequest = Schema.Struct({ + "type": Schema.Literal("mcp_approval_request").annotate({ + "description": "The type of the item. Always `mcp_approval_request`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the approval request.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server making the request.\n" }), + "name": Schema.String.annotate({ "description": "The name of the tool to run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of arguments for the tool.\n" }) +}).annotate({ "title": "MCP approval request", "description": "A request for human approval of a tool invocation.\n" }) +export type MCPApprovalResponseResource = { + readonly "type": "mcp_approval_response" + readonly "id": string + readonly "approval_request_id": string + readonly "approve": boolean + readonly "reason"?: string | null + readonly "request_id": unknown +} +export const MCPApprovalResponseResource = Schema.Struct({ + "type": Schema.Literal("mcp_approval_response").annotate({ + "description": "The type of the item. Always `mcp_approval_response`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the approval response\n" }), + "approval_request_id": Schema.String.annotate({ "description": "The ID of the approval request being answered.\n" }), + "approve": Schema.Boolean.annotate({ "description": "Whether the request was approved.\n" }), + "reason": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Optional reason for the decision.\n" }), Schema.Null]) + ), + "request_id": Schema.Unknown +}).annotate({ "title": "MCP approval response", "description": "A response to an MCP approval request.\n" }) +export type MCPListToolsTool = { + readonly "name": string + readonly "description"?: string | null + readonly "input_schema": {} + readonly "annotations"?: {} | null +} +export const MCPListToolsTool = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the tool.\n" }), + "description": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The description of the tool.\n" }), Schema.Null]) + ), + "input_schema": Schema.Struct({}).annotate({ "description": "The JSON schema describing the tool's input.\n" }), + "annotations": Schema.optionalKey( + Schema.Union([ + Schema.Struct({}).annotate({ "description": "Additional annotations about the tool.\n" }), + Schema.Null + ]) + ) +}).annotate({ "title": "MCP list tools tool", "description": "A tool available on an MCP server.\n" }) +export type MCPToolCall = { + readonly "type": "mcp_call" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string + readonly "output"?: string | null + readonly "error"?: string | null + readonly "status"?: "in_progress" | "completed" | "incomplete" | "calling" | "failed" + readonly "approval_request_id"?: string | null +} +export const MCPToolCall = Schema.Struct({ + "type": Schema.Literal("mcp_call").annotate({ "description": "The type of the item. Always `mcp_call`.\n" }), + "id": Schema.String.annotate({ "description": "The unique ID of the tool call.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server running the tool.\n" }), + "name": Schema.String.annotate({ "description": "The name of the tool that was run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of the arguments passed to the tool.\n" }), + "output": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The output from the tool call.\n" }), Schema.Null]) + ), + "error": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The error from the tool call, if any.\n" }), Schema.Null]) + ), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete", "calling", "failed"]).annotate({ + "description": + "The status of the tool call. One of `in_progress`, `completed`, `incomplete`, `calling`, or `failed`.\n" + }) + ), + "approval_request_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "Unique identifier for the MCP tool call approval request.\nInclude this value in a subsequent `mcp_approval_response` input to approve or reject the corresponding tool call.\n" + }), + Schema.Null + ]) + ) +}).annotate({ "title": "MCP tool call", "description": "An invocation of a tool on an MCP server.\n" }) +export type MCPToolFilter = { readonly "tool_names"?: ReadonlyArray; readonly "read_only"?: boolean } +export const MCPToolFilter = Schema.Struct({ + "tool_names": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "title": "MCP allowed tools", "description": "List of allowed tool names." }) + ), + "read_only": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Indicates whether or not a tool modifies data or is read-only. If an\nMCP server is [annotated with `readOnlyHint`](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-readonlyhint),\nit will match this filter.\n" + }) + ) +}).annotate({ "title": "MCP tool filter", "description": "A filter object to specify which tools are allowed.\n" }) +export type MessageContentImageFileObject = { + readonly "type": "image_file" + readonly "image_file": { readonly "file_id": string; readonly "detail"?: "auto" | "low" | "high" } +} +export const MessageContentImageFileObject = Schema.Struct({ + "type": Schema.Literal("image_file").annotate({ "description": "Always `image_file`." }), + "image_file": Schema.Struct({ + "file_id": Schema.String.annotate({ + "description": + "The [File](/docs/api-reference/files) ID of the image in the message content. Set `purpose=\"vision\"` when uploading the File if you need to later display the file content." + }), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": + "Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`." + }) + ) + }) +}).annotate({ + "title": "Image file", + "description": "References an image [File](/docs/api-reference/files) in the content of a message." +}) +export type MessageContentImageUrlObject = { + readonly "type": "image_url" + readonly "image_url": { readonly "url": string; readonly "detail"?: "auto" | "low" | "high" } +} +export const MessageContentImageUrlObject = Schema.Struct({ + "type": Schema.Literal("image_url").annotate({ "description": "The type of the content part." }), + "image_url": Schema.Struct({ + "url": Schema.String.annotate({ + "description": "The external URL of the image, must be a supported image types: jpeg, jpg, png, gif, webp.", + "format": "uri" + }), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": + "Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`. Default value is `auto`" + }) + ) + }) +}).annotate({ "title": "Image URL", "description": "References an image URL in the content of a message." }) +export type MessageContentRefusalObject = { readonly "type": "refusal"; readonly "refusal": string } +export const MessageContentRefusalObject = Schema.Struct({ + "type": Schema.Literal("refusal").annotate({ "description": "Always `refusal`." }), + "refusal": Schema.String +}).annotate({ "title": "Refusal", "description": "The refusal content generated by the assistant." }) +export type MessageContentTextAnnotationsFileCitationObject = { + readonly "type": "file_citation" + readonly "text": string + readonly "file_citation": { readonly "file_id": string } + readonly "start_index": number + readonly "end_index": number +} +export const MessageContentTextAnnotationsFileCitationObject = Schema.Struct({ + "type": Schema.Literal("file_citation").annotate({ "description": "Always `file_citation`." }), + "text": Schema.String.annotate({ "description": "The text in the message content that needs to be replaced." }), + "file_citation": Schema.Struct({ + "file_id": Schema.String.annotate({ "description": "The ID of the specific File the citation is from." }) + }), + "start_index": Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "end_index": Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ + "title": "File citation", + "description": + "A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the \"file_search\" tool to search files." +}) +export type MessageContentTextAnnotationsFilePathObject = { + readonly "type": "file_path" + readonly "text": string + readonly "file_path": { readonly "file_id": string } + readonly "start_index": number + readonly "end_index": number +} +export const MessageContentTextAnnotationsFilePathObject = Schema.Struct({ + "type": Schema.Literal("file_path").annotate({ "description": "Always `file_path`." }), + "text": Schema.String.annotate({ "description": "The text in the message content that needs to be replaced." }), + "file_path": Schema.Struct({ + "file_id": Schema.String.annotate({ "description": "The ID of the file that was generated." }) + }), + "start_index": Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), + "end_index": Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +}).annotate({ + "title": "File path", + "description": + "A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file." +}) +export type MessageDeltaContentImageFileObject = { + readonly "index": number + readonly "type": "image_file" + readonly "image_file"?: { readonly "file_id"?: string; readonly "detail"?: "auto" | "low" | "high" } +} +export const MessageDeltaContentImageFileObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the content part in the message." }).check( + Schema.isInt() + ), + "type": Schema.Literal("image_file").annotate({ "description": "Always `image_file`." }), + "image_file": Schema.optionalKey(Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The [File](/docs/api-reference/files) ID of the image in the message content. Set `purpose=\"vision\"` when uploading the File if you need to later display the file content." + }) + ), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": + "Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`." + }) + ) + })) +}).annotate({ + "title": "Image file", + "description": "References an image [File](/docs/api-reference/files) in the content of a message." +}) +export type MessageDeltaContentImageUrlObject = { + readonly "index": number + readonly "type": "image_url" + readonly "image_url"?: { readonly "url"?: string; readonly "detail"?: "auto" | "low" | "high" } +} +export const MessageDeltaContentImageUrlObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the content part in the message." }).check( + Schema.isInt() + ), + "type": Schema.Literal("image_url").annotate({ "description": "Always `image_url`." }), + "image_url": Schema.optionalKey(Schema.Struct({ + "url": Schema.optionalKey( + Schema.String.annotate({ + "description": "The URL of the image, must be a supported image types: jpeg, jpg, png, gif, webp.", + "format": "uri" + }) + ), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": + "Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`." + }) + ) + })) +}).annotate({ "title": "Image URL", "description": "References an image URL in the content of a message." }) +export type MessageDeltaContentRefusalObject = { + readonly "index": number + readonly "type": "refusal" + readonly "refusal"?: string +} +export const MessageDeltaContentRefusalObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the refusal part in the message." }).check( + Schema.isInt() + ), + "type": Schema.Literal("refusal").annotate({ "description": "Always `refusal`." }), + "refusal": Schema.optionalKey(Schema.String) +}).annotate({ "title": "Refusal", "description": "The refusal content that is part of a message." }) +export type MessageDeltaContentTextAnnotationsFileCitationObject = { + readonly "index": number + readonly "type": "file_citation" + readonly "text"?: string + readonly "file_citation"?: { readonly "file_id"?: string; readonly "quote"?: string } + readonly "start_index"?: number + readonly "end_index"?: number +} +export const MessageDeltaContentTextAnnotationsFileCitationObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the annotation in the text content part." }).check( + Schema.isInt() + ), + "type": Schema.Literal("file_citation").annotate({ "description": "Always `file_citation`." }), + "text": Schema.optionalKey( + Schema.String.annotate({ "description": "The text in the message content that needs to be replaced." }) + ), + "file_citation": Schema.optionalKey( + Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the specific File the citation is from." }) + ), + "quote": Schema.optionalKey(Schema.String.annotate({ "description": "The specific quote in the file." })) + }) + ), + "start_index": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), + "end_index": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) +}).annotate({ + "title": "File citation", + "description": + "A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the \"file_search\" tool to search files." +}) +export type MessageDeltaContentTextAnnotationsFilePathObject = { + readonly "index": number + readonly "type": "file_path" + readonly "text"?: string + readonly "file_path"?: { readonly "file_id"?: string } + readonly "start_index"?: number + readonly "end_index"?: number +} +export const MessageDeltaContentTextAnnotationsFilePathObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the annotation in the text content part." }).check( + Schema.isInt() + ), + "type": Schema.Literal("file_path").annotate({ "description": "Always `file_path`." }), + "text": Schema.optionalKey( + Schema.String.annotate({ "description": "The text in the message content that needs to be replaced." }) + ), + "file_path": Schema.optionalKey( + Schema.Struct({ + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the file that was generated." })) + }) + ), + "start_index": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), + "end_index": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) +}).annotate({ + "title": "File path", + "description": + "A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file." +}) +export type MessagePhase = "commentary" | "final_answer" +export const MessagePhase = Schema.Literals(["commentary", "final_answer"]).annotate({ + "description": + "Labels an `assistant` message as intermediate commentary (`commentary`) or the final answer (`final_answer`).\nFor models like `gpt-5.3-codex` and beyond, when sending follow-up requests, preserve and resend\nphase on all assistant messages — dropping it can degrade performance. Not used for user messages.\n" +}) +export type MessageRequestContentTextObject = { readonly "type": "text"; readonly "text": string } +export const MessageRequestContentTextObject = Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "Always `text`." }), + "text": Schema.String.annotate({ "description": "Text content to be sent to the model" }) +}).annotate({ "title": "Text", "description": "The text content that is part of a message." }) +export type Metadata = {} | null +export const Metadata = Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null +]) +export type Model = { + readonly "id": string + readonly "created": number + readonly "object": "model" + readonly "owned_by": string + readonly [x: string]: unknown +} +export const Model = Schema.StructWithRest( + Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The model identifier, which can be referenced in the API endpoints." + }), + "created": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) when the model was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "object": Schema.Literal("model").annotate({ "description": "The object type, which is always \"model\"." }), + "owned_by": Schema.String.annotate({ "description": "The organization that owns the model." }) + }), + [Schema.Record(Schema.String, Schema.Json)] +).annotate({ "title": "Model", "description": "Describes an OpenAI model offering that can be used with the API." }) +export type ModelIdsShared = + | string + | "gpt-5.4" + | "gpt-5.4-mini" + | "gpt-5.4-nano" + | "gpt-5.4-mini-2026-03-17" + | "gpt-5.4-nano-2026-03-17" + | "gpt-5.3-chat-latest" + | "gpt-5.2" + | "gpt-5.2-2025-12-11" + | "gpt-5.2-chat-latest" + | "gpt-5.2-pro" + | "gpt-5.2-pro-2025-12-11" + | "gpt-5.1" + | "gpt-5.1-2025-11-13" + | "gpt-5.1-codex" + | "gpt-5.1-mini" + | "gpt-5.1-chat-latest" + | "gpt-5" + | "gpt-5-mini" + | "gpt-5-nano" + | "gpt-5-2025-08-07" + | "gpt-5-mini-2025-08-07" + | "gpt-5-nano-2025-08-07" + | "gpt-5-chat-latest" + | "gpt-4.1" + | "gpt-4.1-mini" + | "gpt-4.1-nano" + | "gpt-4.1-2025-04-14" + | "gpt-4.1-mini-2025-04-14" + | "gpt-4.1-nano-2025-04-14" + | "o4-mini" + | "o4-mini-2025-04-16" + | "o3" + | "o3-2025-04-16" + | "o3-mini" + | "o3-mini-2025-01-31" + | "o1" + | "o1-2024-12-17" + | "o1-preview" + | "o1-preview-2024-09-12" + | "o1-mini" + | "o1-mini-2024-09-12" + | "gpt-4o" + | "gpt-4o-2024-11-20" + | "gpt-4o-2024-08-06" + | "gpt-4o-2024-05-13" + | "gpt-4o-audio-preview" + | "gpt-4o-audio-preview-2024-10-01" + | "gpt-4o-audio-preview-2024-12-17" + | "gpt-4o-audio-preview-2025-06-03" + | "gpt-4o-mini-audio-preview" + | "gpt-4o-mini-audio-preview-2024-12-17" + | "gpt-4o-search-preview" + | "gpt-4o-mini-search-preview" + | "gpt-4o-search-preview-2025-03-11" + | "gpt-4o-mini-search-preview-2025-03-11" + | "chatgpt-4o-latest" + | "codex-mini-latest" + | "gpt-4o-mini" + | "gpt-4o-mini-2024-07-18" + | "gpt-4-turbo" + | "gpt-4-turbo-2024-04-09" + | "gpt-4-0125-preview" + | "gpt-4-turbo-preview" + | "gpt-4-1106-preview" + | "gpt-4-vision-preview" + | "gpt-4" + | "gpt-4-0314" + | "gpt-4-0613" + | "gpt-4-32k" + | "gpt-4-32k-0314" + | "gpt-4-32k-0613" + | "gpt-3.5-turbo" + | "gpt-3.5-turbo-16k" + | "gpt-3.5-turbo-0301" + | "gpt-3.5-turbo-0613" + | "gpt-3.5-turbo-1106" + | "gpt-3.5-turbo-0125" + | "gpt-3.5-turbo-16k-0613" +export const ModelIdsShared = Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5.4-nano", + "gpt-5.4-mini-2026-03-17", + "gpt-5.4-nano-2026-03-17", + "gpt-5.3-chat-latest", + "gpt-5.2", + "gpt-5.2-2025-12-11", + "gpt-5.2-chat-latest", + "gpt-5.2-pro", + "gpt-5.2-pro-2025-12-11", + "gpt-5.1", + "gpt-5.1-2025-11-13", + "gpt-5.1-codex", + "gpt-5.1-mini", + "gpt-5.1-chat-latest", + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-2025-08-07", + "gpt-5-mini-2025-08-07", + "gpt-5-nano-2025-08-07", + "gpt-5-chat-latest", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano-2025-04-14", + "o4-mini", + "o4-mini-2025-04-16", + "o3", + "o3-2025-04-16", + "o3-mini", + "o3-mini-2025-01-31", + "o1", + "o1-2024-12-17", + "o1-preview", + "o1-preview-2024-09-12", + "o1-mini", + "o1-mini-2024-09-12", + "gpt-4o", + "gpt-4o-2024-11-20", + "gpt-4o-2024-08-06", + "gpt-4o-2024-05-13", + "gpt-4o-audio-preview", + "gpt-4o-audio-preview-2024-10-01", + "gpt-4o-audio-preview-2024-12-17", + "gpt-4o-audio-preview-2025-06-03", + "gpt-4o-mini-audio-preview", + "gpt-4o-mini-audio-preview-2024-12-17", + "gpt-4o-search-preview", + "gpt-4o-mini-search-preview", + "gpt-4o-search-preview-2025-03-11", + "gpt-4o-mini-search-preview-2025-03-11", + "chatgpt-4o-latest", + "codex-mini-latest", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613" + ]) +]) +export type ModifyCertificateRequest = { readonly "name"?: string } +export const ModifyCertificateRequest = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The updated name for the certificate" })) +}) +export type NoiseReductionType = "near_field" | "far_field" +export const NoiseReductionType = Schema.Literals(["near_field", "far_field"]).annotate({ + "description": + "Type of noise reduction. `near_field` is for close-talking microphones such as headphones, `far_field` is for far-field microphones such as laptop or conference room microphones.\n" +}) +export type OpenAIFile = { + readonly "id": string + readonly "bytes": number + readonly "created_at": number + readonly "expires_at"?: number | null + readonly "filename": string + readonly "object": "file" + readonly "purpose": + | "assistants" + | "assistants_output" + | "batch" + | "batch_output" + | "fine-tune" + | "fine-tune-results" + | "vision" + | "user_data" + readonly "status": "uploaded" | "processed" | "error" + readonly "status_details"?: string | null + readonly [x: string]: unknown +} +export const OpenAIFile = Schema.StructWithRest( + Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The file identifier, which can be referenced in the API endpoints." + }), + "bytes": Schema.Number.annotate({ "description": "The size of the file, in bytes." }).check(Schema.isInt()), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the file was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "expires_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the file will expire.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "filename": Schema.String.annotate({ "description": "The name of the file." }), + "object": Schema.Literal("file").annotate({ "description": "The object type, which is always `file`." }), + "purpose": Schema.Literals([ + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" + ]).annotate({ + "description": + "The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`." + }), + "status": Schema.Literals(["uploaded", "processed", "error"]).annotate({ + "description": + "Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`." + }), + "status_details": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`." + }) + ) + }), + [Schema.Record(Schema.String, Schema.Json)] +).annotate({ + "title": "OpenAIFile", + "description": "The `File` object represents a document that has been uploaded to OpenAI." +}) +export type OrganizationCertificate = { + readonly "object": "organization.certificate" + readonly "id": string + readonly "name": string | null + readonly "created_at": number + readonly "certificate_details": { readonly "valid_at"?: number; readonly "expires_at"?: number } + readonly "active": boolean +} +export const OrganizationCertificate = Schema.Struct({ + "object": Schema.Literal("organization.certificate").annotate({ + "description": "The object type, which is always `organization.certificate`." + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the certificate." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate was uploaded.", + "format": "unixtime" + }).check(Schema.isInt()), + "certificate_details": Schema.Struct({ + "valid_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate becomes valid.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate expires.", + "format": "unixtime" + }).check(Schema.isInt()) + ) + }), + "active": Schema.Boolean.annotate({ + "description": "Whether the certificate is currently active at the organization level." + }) +}).annotate({ "description": "Represents an individual certificate configured at the organization level." }) +export type OrganizationProjectCertificate = { + readonly "object": "organization.project.certificate" + readonly "id": string + readonly "name": string | null + readonly "created_at": number + readonly "certificate_details": { readonly "valid_at"?: number; readonly "expires_at"?: number } + readonly "active": boolean +} +export const OrganizationProjectCertificate = Schema.Struct({ + "object": Schema.Literal("organization.project.certificate").annotate({ + "description": "The object type, which is always `organization.project.certificate`." + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the certificate." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate was uploaded.", + "format": "unixtime" + }).check(Schema.isInt()), + "certificate_details": Schema.Struct({ + "valid_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate becomes valid.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the certificate expires.", + "format": "unixtime" + }).check(Schema.isInt()) + ) + }), + "active": Schema.Boolean.annotate({ + "description": "Whether the certificate is currently active at the project level." + }) +}).annotate({ "description": "Represents an individual certificate configured at the project level." }) +export type ParallelToolCalls = boolean +export const ParallelToolCalls = Schema.Boolean.annotate({ + "description": + "Whether to enable [parallel function calling](/docs/guides/function-calling#configuring-parallel-function-calling) during tool use." +}) +export type PartialImages = number | null +export const PartialImages = Schema.Union([ + Schema.Number.annotate({ + "description": + "The number of partial images to generate. This parameter is used for\nstreaming responses that return partial images. Value must be between 0 and 3.\nWhen set to 0, the response will be a single image sent in one streaming event.\n\nNote that the final image may be sent before the full number of partial images\nare generated if the full image is generated more quickly.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(3)), + Schema.Null +]) +export type Project = { + readonly "id": string + readonly "object": "organization.project" + readonly "name"?: string | null + readonly "created_at": number + readonly "archived_at"?: number | null + readonly "status"?: string | null + readonly "external_key_id"?: string | null +} +export const Project = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "object": Schema.Literal("organization.project").annotate({ + "description": "The object type, which is always `organization.project`" + }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The name of the project. This appears in reporting." + }) + ), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the project was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "archived_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the project was archived or `null`.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "status": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "`active` or `archived`" }) + ), + "external_key_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The external key associated with the project." + }) + ) +}).annotate({ "description": "Represents an individual project." }) +export type ProjectApiKeyDeleteResponse = { + readonly "object": "organization.project.api_key.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const ProjectApiKeyDeleteResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.api_key.deleted"), + "id": Schema.String, + "deleted": Schema.Boolean +}) +export type ProjectApiKeyOwnerServiceAccount = { + readonly "id": string + readonly "name": string + readonly "created_at": number + readonly "role": string +} +export const ProjectApiKeyOwnerServiceAccount = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.String.annotate({ "description": "The name of the service account." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the service account was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "role": Schema.String.annotate({ "description": "The service account's project role." }) +}).annotate({ "description": "The service account that owns a project API key." }) +export type ProjectApiKeyOwnerUser = { + readonly "id": string + readonly "email": string + readonly "name": string + readonly "created_at": number + readonly "role": string +} +export const ProjectApiKeyOwnerUser = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "email": Schema.String.annotate({ "description": "The email address of the user." }), + "name": Schema.String.annotate({ "description": "The name of the user." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the user was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "role": Schema.String.annotate({ "description": "The user's project role." }) +}).annotate({ "description": "The user that owns a project API key." }) +export type ProjectCreateRequest = { + readonly "name": string + readonly "geography"?: string | null + readonly "external_key_id"?: string | null +} +export const ProjectCreateRequest = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The friendly name of the project, this name appears in reports." }), + "geography": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Create the project with the specified data residency region. Your organization must have access to Data residency functionality in order to use. See [data residency controls](/docs/guides/your-data#data-residency-controls) to review the functionality and limitations of setting this field." + }) + ), + "external_key_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "External key ID to associate with the project." + }) + ) +}) +export type ProjectGroup = { + readonly "object": "project.group" + readonly "project_id": string + readonly "group_id": string + readonly "group_name": string + readonly "group_type": string + readonly "created_at": number +} +export const ProjectGroup = Schema.Struct({ + "object": Schema.Literal("project.group").annotate({ "description": "Always `project.group`." }), + "project_id": Schema.String.annotate({ "description": "Identifier of the project." }), + "group_id": Schema.String.annotate({ "description": "Identifier of the group that has access to the project." }), + "group_name": Schema.String.annotate({ "description": "Display name of the group." }), + "group_type": Schema.String.annotate({ "description": "The type of the group." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the group was granted project access.", + "format": "unixtime" + }).check(Schema.isInt()) +}).annotate({ "description": "Details about a group's membership in a project." }) +export type ProjectGroupDeletedResource = { readonly "object": "project.group.deleted"; readonly "deleted": boolean } +export const ProjectGroupDeletedResource = Schema.Struct({ + "object": Schema.Literal("project.group.deleted").annotate({ "description": "Always `project.group.deleted`." }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the group membership in the project was removed." }) +}).annotate({ "description": "Confirmation payload returned after removing a group from a project." }) +export type ProjectModelPermissions = { + readonly "object": "project.model_permissions" + readonly "mode": "allow_list" | "deny_list" + readonly "model_ids": ReadonlyArray +} +export const ProjectModelPermissions = Schema.Struct({ + "object": Schema.Literal("project.model_permissions").annotate({ + "description": "The object type, which is always `project.model_permissions`." + }), + "mode": Schema.Literals(["allow_list", "deny_list"]).annotate({ + "description": "Whether the project uses an allowlist or a denylist." + }), + "model_ids": Schema.Array(Schema.String).annotate({ + "description": "The model IDs included in the model permissions policy." + }) +}).annotate({ "description": "Represents the model allowlist or denylist policy for a project." }) +export type ProjectModelPermissionsDeleteResponse = { + readonly "object": "project.model_permissions.deleted" + readonly "deleted": boolean +} +export const ProjectModelPermissionsDeleteResponse = Schema.Struct({ + "object": Schema.Literal("project.model_permissions.deleted").annotate({ + "description": "The object type, which is always `project.model_permissions.deleted`." + }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the project model permissions were deleted." }) +}).annotate({ "description": "Confirmation payload returned after deleting project model permissions." }) +export type ProjectModelPermissionsUpdateRequest = { + readonly "mode": "allow_list" | "deny_list" + readonly "model_ids": ReadonlyArray +} +export const ProjectModelPermissionsUpdateRequest = Schema.Struct({ + "mode": Schema.Literals(["allow_list", "deny_list"]).annotate({ + "description": "The model permissions mode to apply." + }), + "model_ids": Schema.Array(Schema.String).annotate({ + "description": "The model IDs included in this permissions policy." + }) +}) +export type ProjectRateLimit = { + readonly "object": "project.rate_limit" + readonly "id": string + readonly "model": string + readonly "max_requests_per_1_minute": number + readonly "max_tokens_per_1_minute": number + readonly "max_images_per_1_minute"?: number + readonly "max_audio_megabytes_per_1_minute"?: number + readonly "max_requests_per_1_day"?: number + readonly "batch_1_day_max_input_tokens"?: number +} +export const ProjectRateLimit = Schema.Struct({ + "object": Schema.Literal("project.rate_limit").annotate({ + "description": "The object type, which is always `project.rate_limit`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "model": Schema.String.annotate({ "description": "The model this rate limit applies to." }), + "max_requests_per_1_minute": Schema.Number.annotate({ "description": "The maximum requests per minute." }).check( + Schema.isInt() + ), + "max_tokens_per_1_minute": Schema.Number.annotate({ "description": "The maximum tokens per minute." }).check( + Schema.isInt() + ), + "max_images_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum images per minute. Only present for relevant models." }).check( + Schema.isInt() + ) + ), + "max_audio_megabytes_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum audio megabytes per minute. Only present for relevant models." + }).check(Schema.isInt()) + ), + "max_requests_per_1_day": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum requests per day. Only present for relevant models." }).check( + Schema.isInt() + ) + ), + "batch_1_day_max_input_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum batch input tokens per day. Only present for relevant models." + }).check(Schema.isInt()) + ) +}).annotate({ "description": "Represents a project rate limit config." }) +export type ProjectRateLimitUpdateRequest = { + readonly "max_requests_per_1_minute"?: number + readonly "max_tokens_per_1_minute"?: number + readonly "max_images_per_1_minute"?: number + readonly "max_audio_megabytes_per_1_minute"?: number + readonly "max_requests_per_1_day"?: number + readonly "batch_1_day_max_input_tokens"?: number +} +export const ProjectRateLimitUpdateRequest = Schema.Struct({ + "max_requests_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum requests per minute." }).check(Schema.isInt()) + ), + "max_tokens_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum tokens per minute." }).check(Schema.isInt()) + ), + "max_images_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum images per minute. Only relevant for certain models." }).check( + Schema.isInt() + ) + ), + "max_audio_megabytes_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum audio megabytes per minute. Only relevant for certain models." + }).check(Schema.isInt()) + ), + "max_requests_per_1_day": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum requests per day. Only relevant for certain models." }).check( + Schema.isInt() + ) + ), + "batch_1_day_max_input_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum batch input tokens per day. Only relevant for certain models." + }).check(Schema.isInt()) + ) +}) +export type ProjectServiceAccount = { + readonly "object": "organization.project.service_account" + readonly "id": string + readonly "name": string + readonly "role": "owner" | "member" + readonly "created_at": number +} +export const ProjectServiceAccount = Schema.Struct({ + "object": Schema.Literal("organization.project.service_account").annotate({ + "description": "The object type, which is always `organization.project.service_account`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.String.annotate({ "description": "The name of the service account" }), + "role": Schema.Literals(["owner", "member"]).annotate({ "description": "`owner` or `member`" }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the service account was created", + "format": "unixtime" + }).check(Schema.isInt()) +}).annotate({ "description": "Represents an individual service account in a project." }) +export type ProjectServiceAccountApiKey = { + readonly "object": "organization.project.service_account.api_key" + readonly "value": string + readonly "name": string + readonly "created_at": number + readonly "id": string +} +export const ProjectServiceAccountApiKey = Schema.Struct({ + "object": Schema.Literal("organization.project.service_account.api_key").annotate({ + "description": "The object type, which is always `organization.project.service_account.api_key`" + }), + "value": Schema.String, + "name": Schema.String, + "created_at": Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), + "id": Schema.String +}) +export type ProjectServiceAccountCreateRequest = { readonly "name": string } +export const ProjectServiceAccountCreateRequest = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the service account being created." }) +}) +export type ProjectServiceAccountDeleteResponse = { + readonly "object": "organization.project.service_account.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const ProjectServiceAccountDeleteResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.service_account.deleted"), + "id": Schema.String, + "deleted": Schema.Boolean +}) +export type ProjectUpdateRequest = { + readonly "name"?: string | null + readonly "external_key_id"?: string | null + readonly "geography"?: string | null +} +export const ProjectUpdateRequest = Schema.Struct({ + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The updated name of the project, this name appears in reports." + }) + ), + "external_key_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "External key ID to associate with the project." + }) + ), + "geography": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Geography for the project." }) + ) +}) +export type ProjectUser = { + readonly "object": "organization.project.user" + readonly "id": string + readonly "name"?: string | null + readonly "email"?: string | null + readonly "role": string + readonly "added_at": number +} +export const ProjectUser = Schema.Struct({ + "object": Schema.Literal("organization.project.user").annotate({ + "description": "The object type, which is always `organization.project.user`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the user" }) + ), + "email": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The email address of the user" }) + ), + "role": Schema.String.annotate({ "description": "`owner` or `member`" }), + "added_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the project was added.", + "format": "unixtime" + }).check(Schema.isInt()) +}).annotate({ "description": "Represents an individual user in a project." }) +export type ProjectUserCreateRequest = { + readonly "user_id"?: string | null + readonly "email"?: string | null + readonly "role": string +} +export const ProjectUserCreateRequest = Schema.Struct({ + "user_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The ID of the user." }) + ), + "email": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Email of the user to add." }) + ), + "role": Schema.String.annotate({ "description": "`owner` or `member`" }) +}) +export type ProjectUserDeleteResponse = { + readonly "object": "organization.project.user.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const ProjectUserDeleteResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.user.deleted"), + "id": Schema.String, + "deleted": Schema.Boolean +}) +export type ProjectUserUpdateRequest = { readonly "role"?: string | null } +export const ProjectUserUpdateRequest = Schema.Struct({ + "role": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "`owner` or `member`" }) + ) +}) +export type PublicAssignOrganizationGroupRoleBody = { readonly "role_id": string } +export const PublicAssignOrganizationGroupRoleBody = Schema.Struct({ + "role_id": Schema.String.annotate({ "description": "Identifier of the role to assign." }) +}).annotate({ "description": "Request payload for assigning a role to a group or user." }) +export type PublicCreateOrganizationRoleBody = { + readonly "role_name": string + readonly "permissions": ReadonlyArray + readonly "description"?: string | null +} +export const PublicCreateOrganizationRoleBody = Schema.Struct({ + "role_name": Schema.String.annotate({ "description": "Unique name for the role." }), + "permissions": Schema.Array(Schema.String).annotate({ "description": "Permissions to grant to the role." }), + "description": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Optional description of the role." }) + ) +}).annotate({ "description": "Request payload for creating a custom role." }) +export type PublicUpdateOrganizationRoleBody = { + readonly "permissions"?: ReadonlyArray | null + readonly "description"?: string | null + readonly "role_name"?: string | null +} +export const PublicUpdateOrganizationRoleBody = Schema.Struct({ + "permissions": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": "Updated set of permissions for the role." + }) + ), + "description": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "New description for the role." }) + ), + "role_name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "New name for the role." }) + ) +}).annotate({ "description": "Request payload for updating an existing role." }) +export type RealtimeAudioFormats = { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" +} | { readonly "type"?: "audio/pcma" } +export const RealtimeAudioFormats = Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) +]) +export type RealtimeCallReferRequest = { readonly "target_uri": string } +export const RealtimeCallReferRequest = Schema.Struct({ + "target_uri": Schema.String.annotate({ + "description": + "URI that should appear in the SIP Refer-To header. Supports values like\n`tel:+14155550123` or `sip:agent@example.com`." + }) +}).annotate({ + "title": "Realtime call refer request", + "description": "Parameters required to transfer a SIP call to a new destination using the\nRealtime API." +}) +export type RealtimeCallRejectRequest = { readonly "status_code"?: number } +export const RealtimeCallRejectRequest = Schema.Struct({ + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "SIP response code to send back to the caller. Defaults to `603` (Decline)\nwhen omitted." + }).check(Schema.isInt()) + ) +}).annotate({ + "title": "Realtime call reject request", + "description": "Parameters used to decline an incoming SIP call handled by the Realtime API." +}) +export type RealtimeClientEventConversationItemDelete = { + readonly "event_id"?: string + readonly "type": "conversation.item.delete" + readonly "item_id": string +} +export const RealtimeClientEventConversationItemDelete = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("conversation.item.delete").annotate({ + "description": "The event type, must be `conversation.item.delete`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item to delete." }) +}).annotate({ + "description": + "Send this event when you want to remove any item from the conversation \nhistory. The server will respond with a `conversation.item.deleted` event, \nunless the item does not exist in the conversation history, in which case the \nserver will respond with an error.\n" +}) +export type RealtimeClientEventConversationItemRetrieve = { + readonly "event_id"?: string + readonly "type": "conversation.item.retrieve" + readonly "item_id": string +} +export const RealtimeClientEventConversationItemRetrieve = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("conversation.item.retrieve").annotate({ + "description": "The event type, must be `conversation.item.retrieve`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item to retrieve." }) +}).annotate({ + "description": + "Send this event when you want to retrieve the server's representation of a specific item in the conversation history. This is useful, for example, to inspect user audio after noise cancellation and VAD.\nThe server will respond with a `conversation.item.retrieved` event, \nunless the item does not exist in the conversation history, in which case the \nserver will respond with an error.\n" +}) +export type RealtimeClientEventConversationItemTruncate = { + readonly "event_id"?: string + readonly "type": "conversation.item.truncate" + readonly "item_id": string + readonly "content_index": number + readonly "audio_end_ms": number +} +export const RealtimeClientEventConversationItemTruncate = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("conversation.item.truncate").annotate({ + "description": "The event type, must be `conversation.item.truncate`." + }), + "item_id": Schema.String.annotate({ + "description": + "The ID of the assistant message item to truncate. Only assistant message \nitems can be truncated.\n" + }), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part to truncate. Set this to `0`." + }).check(Schema.isInt()), + "audio_end_ms": Schema.Number.annotate({ + "description": + "Inclusive duration up to which audio is truncated, in milliseconds. If \nthe audio_end_ms is greater than the actual audio duration, the server \nwill respond with an error.\n" + }).check(Schema.isInt()) +}).annotate({ + "description": + "Send this event to truncate a previous assistant message’s audio. The server \nwill produce audio faster than realtime, so this event is useful when the user \ninterrupts to truncate audio that has already been sent to the client but not \nyet played. This will synchronize the server's understanding of the audio with \nthe client's playback.\n\nTruncating audio will delete the server-side text transcript to ensure there \nis not text in the context that hasn't been heard by the user.\n\nIf successful, the server will respond with a `conversation.item.truncated` \nevent. \n" +}) +export type RealtimeClientEventInputAudioBufferAppend = { + readonly "event_id"?: string + readonly "type": "input_audio_buffer.append" + readonly "audio": string +} +export const RealtimeClientEventInputAudioBufferAppend = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("input_audio_buffer.append").annotate({ + "description": "The event type, must be `input_audio_buffer.append`." + }), + "audio": Schema.String.annotate({ + "description": + "Base64-encoded audio bytes. This must be in the format specified by the \n`input_audio_format` field in the session configuration.\n" + }) +}).annotate({ + "description": + "Send this event to append audio bytes to the input audio buffer. The audio \nbuffer is temporary storage you can write to and later commit. A \"commit\" will create a new\nuser message item in the conversation history from the buffer content and clear the buffer.\nInput audio transcription (if enabled) will be generated when the buffer is committed.\n\nIf VAD is enabled the audio buffer is used to detect speech and the server will decide \nwhen to commit. When Server VAD is disabled, you must commit the audio buffer\nmanually. Input audio noise reduction operates on writes to the audio buffer.\n\nThe client may choose how much audio to place in each event up to a maximum \nof 15 MiB, for example streaming smaller chunks from the client may allow the \nVAD to be more responsive. Unlike most other client events, the server will \nnot send a confirmation response to this event.\n" +}) +export type RealtimeClientEventInputAudioBufferClear = { + readonly "event_id"?: string + readonly "type": "input_audio_buffer.clear" +} +export const RealtimeClientEventInputAudioBufferClear = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("input_audio_buffer.clear").annotate({ + "description": "The event type, must be `input_audio_buffer.clear`." + }) +}).annotate({ + "description": + "Send this event to clear the audio bytes in the buffer. The server will \nrespond with an `input_audio_buffer.cleared` event.\n" +}) +export type RealtimeClientEventInputAudioBufferCommit = { + readonly "event_id"?: string + readonly "type": "input_audio_buffer.commit" +} +export const RealtimeClientEventInputAudioBufferCommit = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("input_audio_buffer.commit").annotate({ + "description": "The event type, must be `input_audio_buffer.commit`." + }) +}).annotate({ + "description": + "Send this event to commit the user input audio buffer, which will create a new user message item in the conversation. This event will produce an error if the input audio buffer is empty. When in Server VAD mode, the client does not need to send this event, the server will commit the audio buffer automatically.\n\nCommitting the input audio buffer will trigger input audio transcription (if enabled in session configuration), but it will not create a response from the model. The server will respond with an `input_audio_buffer.committed` event.\n" +}) +export type RealtimeClientEventOutputAudioBufferClear = { + readonly "event_id"?: string + readonly "type": "output_audio_buffer.clear" +} +export const RealtimeClientEventOutputAudioBufferClear = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The unique ID of the client event used for error handling." }) + ), + "type": Schema.Literal("output_audio_buffer.clear").annotate({ + "description": "The event type, must be `output_audio_buffer.clear`." + }) +}).annotate({ + "description": + "**WebRTC/SIP Only:** Emit to cut off the current audio response. This will trigger the server to\nstop generating audio and emit a `output_audio_buffer.cleared` event. This\nevent should be preceded by a `response.cancel` client event to stop the\ngeneration of the current response.\n[Learn more](/docs/guides/realtime-conversations#client-and-server-events-for-audio-in-webrtc).\n" +}) +export type RealtimeClientEventResponseCancel = { + readonly "event_id"?: string + readonly "type": "response.cancel" + readonly "response_id"?: string +} +export const RealtimeClientEventResponseCancel = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("response.cancel").annotate({ "description": "The event type, must be `response.cancel`." }), + "response_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A specific response ID to cancel - if not provided, will cancel an \nin-progress response in the default conversation.\n" + }) + ) +}).annotate({ + "description": + "Send this event to cancel an in-progress response. The server will respond \nwith a `response.done` event with a status of `response.status=cancelled`. If \nthere is no response to cancel, the server will respond with an error. It's safe\nto call `response.cancel` even if no response is in progress, an error will be\nreturned the session will remain unaffected.\n" +}) +export type RealtimeConversationItemFunctionCall = { + readonly "id"?: string + readonly "object"?: "realtime.item" + readonly "type": "function_call" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "call_id"?: string + readonly "name": string + readonly "arguments": string +} +export const RealtimeConversationItemFunctionCall = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the item. This may be provided by the client or generated by the server." + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.item").annotate({ + "description": + "Identifier for the API object being returned - always `realtime.item`. Optional when creating a new item." + }) + ), + "type": Schema.Literal("function_call").annotate({ "description": "The type of the item. Always `function_call`." }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "incomplete", "in_progress"]).annotate({ + "description": "The status of the item. Has no effect on the conversation." + }) + ), + "call_id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the function call." })), + "name": Schema.String.annotate({ "description": "The name of the function being called." }), + "arguments": Schema.String.annotate({ + "description": + "The arguments of the function call. This is a JSON-encoded string representing the arguments passed to the function, for example `{\"arg1\": \"value1\", \"arg2\": 42}`." + }) +}).annotate({ + "title": "Realtime function call item", + "description": "A function call item in a Realtime conversation." +}) +export type RealtimeConversationItemFunctionCallOutput = { + readonly "id"?: string + readonly "object"?: "realtime.item" + readonly "type": "function_call_output" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "call_id": string + readonly "output": string +} +export const RealtimeConversationItemFunctionCallOutput = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the item. This may be provided by the client or generated by the server." + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.item").annotate({ + "description": + "Identifier for the API object being returned - always `realtime.item`. Optional when creating a new item." + }) + ), + "type": Schema.Literal("function_call_output").annotate({ + "description": "The type of the item. Always `function_call_output`." + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "incomplete", "in_progress"]).annotate({ + "description": "The status of the item. Has no effect on the conversation." + }) + ), + "call_id": Schema.String.annotate({ "description": "The ID of the function call this output is for." }), + "output": Schema.String.annotate({ + "description": + "The output of the function call, this is free text and can contain any information or simply be empty." + }) +}).annotate({ + "title": "Realtime function call output item", + "description": "A function call output item in a Realtime conversation." +}) +export type RealtimeConversationItemMessageAssistant = { + readonly "id"?: string + readonly "object"?: "realtime.item" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "role": "assistant" + readonly "content": ReadonlyArray< + { + readonly "type"?: "output_text" | "output_audio" + readonly "text"?: string + readonly "audio"?: string + readonly "transcript"?: string + } + > +} +export const RealtimeConversationItemMessageAssistant = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the item. This may be provided by the client or generated by the server." + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.item").annotate({ + "description": + "Identifier for the API object being returned - always `realtime.item`. Optional when creating a new item." + }) + ), + "type": Schema.Literal("message").annotate({ "description": "The type of the item. Always `message`." }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "incomplete", "in_progress"]).annotate({ + "description": "The status of the item. Has no effect on the conversation." + }) + ), + "role": Schema.Literal("assistant").annotate({ + "description": "The role of the message sender. Always `assistant`." + }), + "content": Schema.Array(Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["output_text", "output_audio"]).annotate({ + "description": + "The content type, `output_text` or `output_audio` depending on the session `output_modalities` configuration." + }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content." })), + "audio": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Base64-encoded audio bytes, these will be parsed as the format specified in the session output audio type configuration. This defaults to PCM 16-bit 24kHz mono if not specified." + }) + ), + "transcript": Schema.optionalKey( + Schema.String.annotate({ + "description": "The transcript of the audio content, this will always be present if the output type is `audio`." + }) + ) + })).annotate({ "description": "The content of the message." }) +}).annotate({ + "title": "Realtime assistant message item", + "description": "An assistant message item in a Realtime conversation." +}) +export type RealtimeConversationItemMessageSystem = { + readonly "id"?: string + readonly "object"?: "realtime.item" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "role": "system" + readonly "content": ReadonlyArray<{ readonly "type"?: "input_text"; readonly "text"?: string }> +} +export const RealtimeConversationItemMessageSystem = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the item. This may be provided by the client or generated by the server." + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.item").annotate({ + "description": + "Identifier for the API object being returned - always `realtime.item`. Optional when creating a new item." + }) + ), + "type": Schema.Literal("message").annotate({ "description": "The type of the item. Always `message`." }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "incomplete", "in_progress"]).annotate({ + "description": "The status of the item. Has no effect on the conversation." + }) + ), + "role": Schema.Literal("system").annotate({ "description": "The role of the message sender. Always `system`." }), + "content": Schema.Array( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("input_text").annotate({ + "description": "The content type. Always `input_text` for system messages." + }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content." })) + }) + ).annotate({ "description": "The content of the message." }) +}).annotate({ + "title": "Realtime system message item", + "description": + "A system message in a Realtime conversation can be used to provide additional context or instructions to the model. This is similar but distinct from the instruction prompt provided at the start of a conversation, as system messages can be added at any point in the conversation. For major changes to the conversation's behavior, use instructions, but for smaller updates (e.g. \"the user is now asking about a different topic\"), use system messages." +}) +export type RealtimeConversationItemMessageUser = { + readonly "id"?: string + readonly "object"?: "realtime.item" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "role": "user" + readonly "content": ReadonlyArray< + { + readonly "type"?: "input_text" | "input_audio" | "input_image" + readonly "text"?: string + readonly "audio"?: string + readonly "image_url"?: string + readonly "detail"?: "auto" | "low" | "high" + readonly "transcript"?: string + } + > +} +export const RealtimeConversationItemMessageUser = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the item. This may be provided by the client or generated by the server." + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.item").annotate({ + "description": + "Identifier for the API object being returned - always `realtime.item`. Optional when creating a new item." + }) + ), + "type": Schema.Literal("message").annotate({ "description": "The type of the item. Always `message`." }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "incomplete", "in_progress"]).annotate({ + "description": "The status of the item. Has no effect on the conversation." + }) + ), + "role": Schema.Literal("user").annotate({ "description": "The role of the message sender. Always `user`." }), + "content": Schema.Array(Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["input_text", "input_audio", "input_image"]).annotate({ + "description": "The content type (`input_text`, `input_audio`, or `input_image`)." + }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content (for `input_text`)." })), + "audio": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Base64-encoded audio bytes (for `input_audio`), these will be parsed as the format specified in the session input audio type configuration. This defaults to PCM 16-bit 24kHz mono if not specified." + }) + ), + "image_url": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Base64-encoded image bytes (for `input_image`) as a data URI. For example `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...`. Supported formats are PNG and JPEG.", + "format": "uri" + }) + ), + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high"]).annotate({ + "description": "The detail level of the image (for `input_image`). `auto` will default to `high`." + }) + ), + "transcript": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Transcript of the audio (for `input_audio`). This is not sent to the model, but will be attached to the message item for reference." + }) + ) + })).annotate({ "description": "The content of the message." }) +}).annotate({ "title": "Realtime user message item", "description": "A user message item in a Realtime conversation." }) +export type RealtimeFunctionTool = { + readonly "type"?: "function" + readonly "name"?: string + readonly "description"?: string + readonly "parameters"?: {} +} +export const RealtimeFunctionTool = Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("function").annotate({ "description": "The type of the tool, i.e. `function`." }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function." })), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The description of the function, including guidance on when and how\nto call it, and guidance about what to tell the user when calling\n(if anything).\n" + }) + ), + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Parameters of the function in JSON Schema." }) + ) +}).annotate({ "title": "Function tool" }) +export type RealtimeMCPApprovalRequest = { + readonly "type": "mcp_approval_request" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string +} +export const RealtimeMCPApprovalRequest = Schema.Struct({ + "type": Schema.Literal("mcp_approval_request").annotate({ + "description": "The type of the item. Always `mcp_approval_request`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the approval request." }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server making the request." }), + "name": Schema.String.annotate({ "description": "The name of the tool to run." }), + "arguments": Schema.String.annotate({ "description": "A JSON string of arguments for the tool." }) +}).annotate({ + "title": "Realtime MCP approval request", + "description": "A Realtime item requesting human approval of a tool invocation.\n" +}) +export type RealtimeMCPApprovalResponse = { + readonly "type": "mcp_approval_response" + readonly "id": string + readonly "approval_request_id": string + readonly "approve": boolean + readonly "reason"?: string | null +} +export const RealtimeMCPApprovalResponse = Schema.Struct({ + "type": Schema.Literal("mcp_approval_response").annotate({ + "description": "The type of the item. Always `mcp_approval_response`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the approval response." }), + "approval_request_id": Schema.String.annotate({ "description": "The ID of the approval request being answered." }), + "approve": Schema.Boolean.annotate({ "description": "Whether the request was approved." }), + "reason": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Optional reason for the decision." }), Schema.Null]) + ) +}).annotate({ + "title": "Realtime MCP approval response", + "description": "A Realtime item responding to an MCP approval request.\n" +}) +export type RealtimeMCPHTTPError = { + readonly "type": "http_error" + readonly "code": number + readonly "message": string +} +export const RealtimeMCPHTTPError = Schema.Struct({ + "type": Schema.Literal("http_error"), + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String +}).annotate({ "title": "Realtime MCP HTTP error" }) +export type RealtimeMCPProtocolError = { + readonly "type": "protocol_error" + readonly "code": number + readonly "message": string +} +export const RealtimeMCPProtocolError = Schema.Struct({ + "type": Schema.Literal("protocol_error"), + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String +}).annotate({ "title": "Realtime MCP protocol error" }) +export type RealtimeMCPToolExecutionError = { readonly "type": "tool_execution_error"; readonly "message": string } +export const RealtimeMCPToolExecutionError = Schema.Struct({ + "type": Schema.Literal("tool_execution_error"), + "message": Schema.String +}).annotate({ "title": "Realtime MCP tool execution error" }) +export type RealtimeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" +export const RealtimeReasoningEffort = Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": "Constrains effort on reasoning for reasoning-capable Realtime models such as\n`gpt-realtime-2`.\n" +}) +export type RealtimeServerEventConversationCreated = { + readonly "event_id": string + readonly "type": "conversation.created" + readonly "conversation": { readonly "id"?: string; readonly "object"?: string } +} +export const RealtimeServerEventConversationCreated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.created").annotate({ + "description": "The event type, must be `conversation.created`." + }), + "conversation": Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the conversation." })), + "object": Schema.optionalKey( + Schema.String.annotate({ "description": "The object type, must be `realtime.conversation`." }) + ) + }).annotate({ "description": "The conversation resource." }) +}).annotate({ "description": "Returned when a conversation is created. Emitted right after session creation.\n" }) +export type RealtimeServerEventConversationItemDeleted = { + readonly "event_id": string + readonly "type": "conversation.item.deleted" + readonly "item_id": string +} +export const RealtimeServerEventConversationItemDeleted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.deleted").annotate({ + "description": "The event type, must be `conversation.item.deleted`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item that was deleted." }) +}).annotate({ + "description": + "Returned when an item in the conversation is deleted by the client with a \n`conversation.item.delete` event. This event is used to synchronize the \nserver's understanding of the conversation history with the client's view.\n" +}) +export type RealtimeServerEventConversationItemInputAudioTranscriptionFailed = { + readonly "event_id": string + readonly "type": "conversation.item.input_audio_transcription.failed" + readonly "item_id": string + readonly "content_index": number + readonly "error": { + readonly "type"?: string + readonly "code"?: string + readonly "message"?: string + readonly "param"?: string + } +} +export const RealtimeServerEventConversationItemInputAudioTranscriptionFailed = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.input_audio_transcription.failed").annotate({ + "description": "The event type, must be\n`conversation.item.input_audio_transcription.failed`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the user message item." }), + "content_index": Schema.Number.annotate({ "description": "The index of the content part containing the audio." }) + .check(Schema.isInt()), + "error": Schema.Struct({ + "type": Schema.optionalKey(Schema.String.annotate({ "description": "The type of error." })), + "code": Schema.optionalKey(Schema.String.annotate({ "description": "Error code, if any." })), + "message": Schema.optionalKey(Schema.String.annotate({ "description": "A human-readable error message." })), + "param": Schema.optionalKey(Schema.String.annotate({ "description": "Parameter related to the error, if any." })) + }).annotate({ "description": "Details of the transcription error." }) +}).annotate({ + "description": + "Returned when input audio transcription is configured, and a transcription \nrequest for a user message failed. These events are separate from other \n`error` events so that the client can identify the related Item.\n" +}) +export type RealtimeServerEventConversationItemInputAudioTranscriptionSegment = { + readonly "event_id": string + readonly "type": "conversation.item.input_audio_transcription.segment" + readonly "item_id": string + readonly "content_index": number + readonly "text": string + readonly "id": string + readonly "speaker": string + readonly "start": number + readonly "end": number +} +export const RealtimeServerEventConversationItemInputAudioTranscriptionSegment = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.input_audio_transcription.segment").annotate({ + "description": "The event type, must be `conversation.item.input_audio_transcription.segment`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item containing the input audio content." }), + "content_index": Schema.Number.annotate({ + "description": "The index of the input audio content part within the item." + }).check(Schema.isInt()), + "text": Schema.String.annotate({ "description": "The text for this segment." }), + "id": Schema.String.annotate({ "description": "The segment identifier." }), + "speaker": Schema.String.annotate({ "description": "The detected speaker label for this segment." }), + "start": Schema.Number.annotate({ "description": "Start time of the segment in seconds.", "format": "double" }).check( + Schema.isFinite() + ), + "end": Schema.Number.annotate({ "description": "End time of the segment in seconds.", "format": "double" }).check( + Schema.isFinite() + ) +}).annotate({ "description": "Returned when an input audio transcription segment is identified for an item." }) +export type RealtimeServerEventConversationItemTruncated = { + readonly "event_id": string + readonly "type": "conversation.item.truncated" + readonly "item_id": string + readonly "content_index": number + readonly "audio_end_ms": number +} +export const RealtimeServerEventConversationItemTruncated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.truncated").annotate({ + "description": "The event type, must be `conversation.item.truncated`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the assistant message item that was truncated." }), + "content_index": Schema.Number.annotate({ "description": "The index of the content part that was truncated." }).check( + Schema.isInt() + ), + "audio_end_ms": Schema.Number.annotate({ + "description": "The duration up to which the audio was truncated, in milliseconds.\n" + }).check(Schema.isInt()) +}).annotate({ + "description": + "Returned when an earlier assistant audio message item is truncated by the \nclient with a `conversation.item.truncate` event. This event is used to \nsynchronize the server's understanding of the audio with the client's playback.\n\nThis action will truncate the audio and remove the server-side text transcript \nto ensure there is no text in the context that hasn't been heard by the user.\n" +}) +export type RealtimeServerEventError = { + readonly "event_id": string + readonly "type": "error" + readonly "error": { + readonly "type": string + readonly "code"?: string | null + readonly "message": string + readonly "param"?: string | null + readonly "event_id"?: string | null + } +} +export const RealtimeServerEventError = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("error").annotate({ "description": "The event type, must be `error`." }), + "error": Schema.Struct({ + "type": Schema.String.annotate({ + "description": "The type of error (e.g., \"invalid_request_error\", \"server_error\").\n" + }), + "code": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Error code, if any." }), Schema.Null]) + ), + "message": Schema.String.annotate({ "description": "A human-readable error message." }), + "param": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Parameter related to the error, if any." }), Schema.Null]) + ), + "event_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The event_id of the client event that caused the error, if applicable.\n" + }), + Schema.Null + ]) + ) + }).annotate({ "description": "Details of the error." }) +}).annotate({ + "description": + "Returned when an error occurs, which could be a client problem or a server\nproblem. Most errors are recoverable and the session will stay open, we\nrecommend to implementors to monitor and log error messages by default.\n" +}) +export type RealtimeServerEventInputAudioBufferCleared = { + readonly "event_id": string + readonly "type": "input_audio_buffer.cleared" +} +export const RealtimeServerEventInputAudioBufferCleared = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("input_audio_buffer.cleared").annotate({ + "description": "The event type, must be `input_audio_buffer.cleared`." + }) +}).annotate({ + "description": + "Returned when the input audio buffer is cleared by the client with a \n`input_audio_buffer.clear` event.\n" +}) +export type RealtimeServerEventInputAudioBufferCommitted = { + readonly "event_id": string + readonly "type": "input_audio_buffer.committed" + readonly "previous_item_id"?: string | null + readonly "item_id": string +} +export const RealtimeServerEventInputAudioBufferCommitted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("input_audio_buffer.committed").annotate({ + "description": "The event type, must be `input_audio_buffer.committed`." + }), + "previous_item_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The ID of the preceding item after which the new item will be inserted.\nCan be `null` if the item has no predecessor.\n" + }), + Schema.Null + ]) + ), + "item_id": Schema.String.annotate({ "description": "The ID of the user message item that will be created." }) +}).annotate({ + "description": + "Returned when an input audio buffer is committed, either by the client or\nautomatically in server VAD mode. The `item_id` property is the ID of the user\nmessage item that will be created, thus a `conversation.item.created` event\nwill also be sent to the client.\n" +}) +export type RealtimeServerEventInputAudioBufferDtmfEventReceived = { + readonly "type": "input_audio_buffer.dtmf_event_received" + readonly "event": string + readonly "received_at": number +} +export const RealtimeServerEventInputAudioBufferDtmfEventReceived = Schema.Struct({ + "type": Schema.Literal("input_audio_buffer.dtmf_event_received").annotate({ + "description": "The event type, must be `input_audio_buffer.dtmf_event_received`." + }), + "event": Schema.String.annotate({ "description": "The telephone keypad that was pressed by the user." }), + "received_at": Schema.Number.annotate({ + "description": "UTC Unix Timestamp when DTMF Event was received by server.\n" + }).check(Schema.isInt()) +}).annotate({ + "description": + "**SIP Only:** Returned when an DTMF event is received. A DTMF event is a message that\nrepresents a telephone keypad press (0–9, *, #, A–D). The `event` property\nis the keypad that the user press. The `received_at` is the UTC Unix Timestamp\nthat the server received the event.\n" +}) +export type RealtimeServerEventInputAudioBufferSpeechStarted = { + readonly "event_id": string + readonly "type": "input_audio_buffer.speech_started" + readonly "audio_start_ms": number + readonly "item_id": string +} +export const RealtimeServerEventInputAudioBufferSpeechStarted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("input_audio_buffer.speech_started").annotate({ + "description": "The event type, must be `input_audio_buffer.speech_started`." + }), + "audio_start_ms": Schema.Number.annotate({ + "description": + "Milliseconds from the start of all audio written to the buffer during the \nsession when speech was first detected. This will correspond to the \nbeginning of audio sent to the model, and thus includes the \n`prefix_padding_ms` configured in the Session.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The ID of the user message item that will be created when speech stops.\n" + }) +}).annotate({ + "description": + "Sent by the server when in `server_vad` mode to indicate that speech has been \ndetected in the audio buffer. This can happen any time audio is added to the \nbuffer (unless speech is already detected). The client may want to use this \nevent to interrupt audio playback or provide visual feedback to the user. \n\nThe client should expect to receive a `input_audio_buffer.speech_stopped` event \nwhen speech stops. The `item_id` property is the ID of the user message item \nthat will be created when speech stops and will also be included in the \n`input_audio_buffer.speech_stopped` event (unless the client manually commits \nthe audio buffer during VAD activation).\n" +}) +export type RealtimeServerEventInputAudioBufferSpeechStopped = { + readonly "event_id": string + readonly "type": "input_audio_buffer.speech_stopped" + readonly "audio_end_ms": number + readonly "item_id": string +} +export const RealtimeServerEventInputAudioBufferSpeechStopped = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("input_audio_buffer.speech_stopped").annotate({ + "description": "The event type, must be `input_audio_buffer.speech_stopped`." + }), + "audio_end_ms": Schema.Number.annotate({ + "description": + "Milliseconds since the session started when speech stopped. This will \ncorrespond to the end of audio sent to the model, and thus includes the \n`min_silence_duration_ms` configured in the Session.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The ID of the user message item that will be created." }) +}).annotate({ + "description": + "Returned in `server_vad` mode when the server detects the end of speech in \nthe audio buffer. The server will also send an `conversation.item.created` \nevent with the user message item that is created from the audio buffer.\n" +}) +export type RealtimeServerEventInputAudioBufferTimeoutTriggered = { + readonly "event_id": string + readonly "type": "input_audio_buffer.timeout_triggered" + readonly "audio_start_ms": number + readonly "audio_end_ms": number + readonly "item_id": string +} +export const RealtimeServerEventInputAudioBufferTimeoutTriggered = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("input_audio_buffer.timeout_triggered").annotate({ + "description": "The event type, must be `input_audio_buffer.timeout_triggered`." + }), + "audio_start_ms": Schema.Number.annotate({ + "description": + "Millisecond offset of audio written to the input audio buffer that was after the playback time of the last model response." + }).check(Schema.isInt()), + "audio_end_ms": Schema.Number.annotate({ + "description": + "Millisecond offset of audio written to the input audio buffer at the time the timeout was triggered." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The ID of the item associated with this segment." }) +}).annotate({ + "description": + "Returned when the Server VAD timeout is triggered for the input audio buffer. This is configured\nwith `idle_timeout_ms` in the `turn_detection` settings of the session, and it indicates that\nthere hasn't been any speech detected for the configured duration.\n\nThe `audio_start_ms` and `audio_end_ms` fields indicate the segment of audio after the last\nmodel response up to the triggering time, as an offset from the beginning of audio written\nto the input audio buffer. This means it demarcates the segment of audio that was silent and\nthe difference between the start and end values will roughly match the configured timeout.\n\nThe empty audio will be committed to the conversation as an `input_audio` item (there will be a\n`input_audio_buffer.committed` event) and a model response will be generated. There may be speech\nthat didn't trigger VAD but is still detected by the model, so the model may respond with\nsomething relevant to the conversation or a prompt to continue speaking.\n" +}) +export type RealtimeServerEventMCPListToolsCompleted = { + readonly "event_id": string + readonly "type": "mcp_list_tools.completed" + readonly "item_id": string +} +export const RealtimeServerEventMCPListToolsCompleted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("mcp_list_tools.completed").annotate({ + "description": "The event type, must be `mcp_list_tools.completed`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP list tools item." }) +}).annotate({ "description": "Returned when listing MCP tools has completed for an item." }) +export type RealtimeServerEventMCPListToolsFailed = { + readonly "event_id": string + readonly "type": "mcp_list_tools.failed" + readonly "item_id": string +} +export const RealtimeServerEventMCPListToolsFailed = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("mcp_list_tools.failed").annotate({ + "description": "The event type, must be `mcp_list_tools.failed`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP list tools item." }) +}).annotate({ "description": "Returned when listing MCP tools has failed for an item." }) +export type RealtimeServerEventMCPListToolsInProgress = { + readonly "event_id": string + readonly "type": "mcp_list_tools.in_progress" + readonly "item_id": string +} +export const RealtimeServerEventMCPListToolsInProgress = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("mcp_list_tools.in_progress").annotate({ + "description": "The event type, must be `mcp_list_tools.in_progress`." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP list tools item." }) +}).annotate({ "description": "Returned when listing MCP tools is in progress for an item." }) +export type RealtimeServerEventOutputAudioBufferCleared = { + readonly "event_id": string + readonly "type": "output_audio_buffer.cleared" + readonly "response_id": string +} +export const RealtimeServerEventOutputAudioBufferCleared = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("output_audio_buffer.cleared").annotate({ + "description": "The event type, must be `output_audio_buffer.cleared`." + }), + "response_id": Schema.String.annotate({ "description": "The unique ID of the response that produced the audio." }) +}).annotate({ + "description": + "**WebRTC/SIP Only:** Emitted when the output audio buffer is cleared. This happens either in VAD\nmode when the user has interrupted (`input_audio_buffer.speech_started`),\nor when the client has emitted the `output_audio_buffer.clear` event to manually\ncut off the current audio response.\n[Learn more](/docs/guides/realtime-conversations#client-and-server-events-for-audio-in-webrtc).\n" +}) +export type RealtimeServerEventOutputAudioBufferStarted = { + readonly "event_id": string + readonly "type": "output_audio_buffer.started" + readonly "response_id": string +} +export const RealtimeServerEventOutputAudioBufferStarted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("output_audio_buffer.started").annotate({ + "description": "The event type, must be `output_audio_buffer.started`." + }), + "response_id": Schema.String.annotate({ "description": "The unique ID of the response that produced the audio." }) +}).annotate({ + "description": + "**WebRTC/SIP Only:** Emitted when the server begins streaming audio to the client. This event is\nemitted after an audio content part has been added (`response.content_part.added`)\nto the response.\n[Learn more](/docs/guides/realtime-conversations#client-and-server-events-for-audio-in-webrtc).\n" +}) +export type RealtimeServerEventOutputAudioBufferStopped = { + readonly "event_id": string + readonly "type": "output_audio_buffer.stopped" + readonly "response_id": string +} +export const RealtimeServerEventOutputAudioBufferStopped = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("output_audio_buffer.stopped").annotate({ + "description": "The event type, must be `output_audio_buffer.stopped`." + }), + "response_id": Schema.String.annotate({ "description": "The unique ID of the response that produced the audio." }) +}).annotate({ + "description": + "**WebRTC/SIP Only:** Emitted when the output audio buffer has been completely drained on the server,\nand no more audio is forthcoming. This event is emitted after the full response\ndata has been sent to the client (`response.done`).\n[Learn more](/docs/guides/realtime-conversations#client-and-server-events-for-audio-in-webrtc).\n" +}) +export type RealtimeServerEventRateLimitsUpdated = { + readonly "event_id": string + readonly "type": "rate_limits.updated" + readonly "rate_limits": ReadonlyArray< + { + readonly "name"?: "requests" | "tokens" + readonly "limit"?: number + readonly "remaining"?: number + readonly "reset_seconds"?: number + } + > +} +export const RealtimeServerEventRateLimitsUpdated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("rate_limits.updated").annotate({ + "description": "The event type, must be `rate_limits.updated`." + }), + "rate_limits": Schema.Array(Schema.Struct({ + "name": Schema.optionalKey( + Schema.Literals(["requests", "tokens"]).annotate({ + "description": "The name of the rate limit (`requests`, `tokens`).\n" + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum allowed value for the rate limit." }).check(Schema.isInt()) + ), + "remaining": Schema.optionalKey( + Schema.Number.annotate({ "description": "The remaining value before the limit is reached." }).check( + Schema.isInt() + ) + ), + "reset_seconds": Schema.optionalKey( + Schema.Number.annotate({ "description": "Seconds until the rate limit resets." }).check(Schema.isFinite()) + ) + })).annotate({ "description": "List of rate limit information." }) +}).annotate({ + "description": + "Emitted at the beginning of a Response to indicate the updated rate limits. \nWhen a Response is created some tokens will be \"reserved\" for the output \ntokens, the rate limits shown here reflect that reservation, which is then \nadjusted accordingly once the Response is completed.\n" +}) +export type RealtimeServerEventResponseAudioDelta = { + readonly "event_id": string + readonly "type": "response.output_audio.delta" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string +} +export const RealtimeServerEventResponseAudioDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_audio.delta").annotate({ + "description": "The event type, must be `response.output_audio.delta`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "Base64-encoded audio data delta." }) +}).annotate({ "description": "Returned when the model-generated audio is updated." }) +export type RealtimeServerEventResponseAudioDone = { + readonly "event_id": string + readonly "type": "response.output_audio.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number +} +export const RealtimeServerEventResponseAudioDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_audio.done").annotate({ + "description": "The event type, must be `response.output_audio.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()) +}).annotate({ + "description": + "Returned when the model-generated audio is done. Also emitted when a Response\nis interrupted, incomplete, or cancelled.\n" +}) +export type RealtimeServerEventResponseAudioTranscriptDelta = { + readonly "event_id": string + readonly "type": "response.output_audio_transcript.delta" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string +} +export const RealtimeServerEventResponseAudioTranscriptDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_audio_transcript.delta").annotate({ + "description": "The event type, must be `response.output_audio_transcript.delta`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The transcript delta." }) +}).annotate({ "description": "Returned when the model-generated transcription of audio output is updated.\n" }) +export type RealtimeServerEventResponseAudioTranscriptDone = { + readonly "event_id": string + readonly "type": "response.output_audio_transcript.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "transcript": string +} +export const RealtimeServerEventResponseAudioTranscriptDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_audio_transcript.done").annotate({ + "description": "The event type, must be `response.output_audio_transcript.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "transcript": Schema.String.annotate({ "description": "The final transcript of the audio." }) +}).annotate({ + "description": + "Returned when the model-generated transcription of audio output is done\nstreaming. Also emitted when a Response is interrupted, incomplete, or\ncancelled.\n" +}) +export type RealtimeServerEventResponseContentPartAdded = { + readonly "event_id": string + readonly "type": "response.content_part.added" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "part": { + readonly "type"?: "audio" | "text" + readonly "text"?: string + readonly "audio"?: string + readonly "transcript"?: string + } +} +export const RealtimeServerEventResponseContentPartAdded = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.content_part.added").annotate({ + "description": "The event type, must be `response.content_part.added`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item to which the content part was added." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "part": Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["audio", "text"]).annotate({ "description": "The content type (\"text\", \"audio\")." }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content (if type is \"text\")." })), + "audio": Schema.optionalKey( + Schema.String.annotate({ "description": "Base64-encoded audio data (if type is \"audio\")." }) + ), + "transcript": Schema.optionalKey( + Schema.String.annotate({ "description": "The transcript of the audio (if type is \"audio\")." }) + ) + }).annotate({ "description": "The content part that was added." }) +}).annotate({ + "description": "Returned when a new content part is added to an assistant message item during\nresponse generation.\n" +}) +export type RealtimeServerEventResponseContentPartDone = { + readonly "event_id": string + readonly "type": "response.content_part.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "part": { + readonly "type"?: "audio" | "text" + readonly "text"?: string + readonly "audio"?: string + readonly "transcript"?: string + } +} +export const RealtimeServerEventResponseContentPartDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.content_part.done").annotate({ + "description": "The event type, must be `response.content_part.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "part": Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["audio", "text"]).annotate({ "description": "The content type (\"text\", \"audio\")." }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content (if type is \"text\")." })), + "audio": Schema.optionalKey( + Schema.String.annotate({ "description": "Base64-encoded audio data (if type is \"audio\")." }) + ), + "transcript": Schema.optionalKey( + Schema.String.annotate({ "description": "The transcript of the audio (if type is \"audio\")." }) + ) + }).annotate({ "description": "The content part that is done." }) +}).annotate({ + "description": + "Returned when a content part is done streaming in an assistant message item.\nAlso emitted when a Response is interrupted, incomplete, or cancelled.\n" +}) +export type RealtimeServerEventResponseFunctionCallArgumentsDelta = { + readonly "event_id": string + readonly "type": "response.function_call_arguments.delta" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "call_id": string + readonly "delta": string +} +export const RealtimeServerEventResponseFunctionCallArgumentsDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.function_call_arguments.delta").annotate({ + "description": "The event type, must be `response.function_call_arguments.delta`.\n" + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the function call item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "call_id": Schema.String.annotate({ "description": "The ID of the function call." }), + "delta": Schema.String.annotate({ "description": "The arguments delta as a JSON string." }) +}).annotate({ "description": "Returned when the model-generated function call arguments are updated.\n" }) +export type RealtimeServerEventResponseFunctionCallArgumentsDone = { + readonly "event_id": string + readonly "type": "response.function_call_arguments.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "call_id": string + readonly "name": string + readonly "arguments": string +} +export const RealtimeServerEventResponseFunctionCallArgumentsDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.function_call_arguments.done").annotate({ + "description": "The event type, must be `response.function_call_arguments.done`.\n" + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the function call item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "call_id": Schema.String.annotate({ "description": "The ID of the function call." }), + "name": Schema.String.annotate({ "description": "The name of the function that was called." }), + "arguments": Schema.String.annotate({ "description": "The final arguments as a JSON string." }) +}).annotate({ + "description": + "Returned when the model-generated function call arguments are done streaming.\nAlso emitted when a Response is interrupted, incomplete, or cancelled.\n" +}) +export type RealtimeServerEventResponseMCPCallArgumentsDelta = { + readonly "event_id": string + readonly "type": "response.mcp_call_arguments.delta" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "delta": string + readonly "obfuscation"?: string | null +} +export const RealtimeServerEventResponseMCPCallArgumentsDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.mcp_call_arguments.delta").annotate({ + "description": "The event type, must be `response.mcp_call_arguments.delta`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "delta": Schema.String.annotate({ "description": "The JSON-encoded arguments delta." }), + "obfuscation": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "If present, indicates the delta text was obfuscated." }), + Schema.Null + ]) + ) +}).annotate({ "description": "Returned when MCP tool call arguments are updated during response generation." }) +export type RealtimeServerEventResponseMCPCallArgumentsDone = { + readonly "event_id": string + readonly "type": "response.mcp_call_arguments.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "arguments": string +} +export const RealtimeServerEventResponseMCPCallArgumentsDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.mcp_call_arguments.done").annotate({ + "description": "The event type, must be `response.mcp_call_arguments.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "arguments": Schema.String.annotate({ "description": "The final JSON-encoded arguments string." }) +}).annotate({ "description": "Returned when MCP tool call arguments are finalized during response generation." }) +export type RealtimeServerEventResponseMCPCallCompleted = { + readonly "event_id": string + readonly "type": "response.mcp_call.completed" + readonly "output_index": number + readonly "item_id": string +} +export const RealtimeServerEventResponseMCPCallCompleted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.mcp_call.completed").annotate({ + "description": "The event type, must be `response.mcp_call.completed`." + }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item." }) +}).annotate({ "description": "Returned when an MCP tool call has completed successfully." }) +export type RealtimeServerEventResponseMCPCallFailed = { + readonly "event_id": string + readonly "type": "response.mcp_call.failed" + readonly "output_index": number + readonly "item_id": string +} +export const RealtimeServerEventResponseMCPCallFailed = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.mcp_call.failed").annotate({ + "description": "The event type, must be `response.mcp_call.failed`." + }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item." }) +}).annotate({ "description": "Returned when an MCP tool call has failed." }) +export type RealtimeServerEventResponseMCPCallInProgress = { + readonly "event_id": string + readonly "type": "response.mcp_call.in_progress" + readonly "output_index": number + readonly "item_id": string +} +export const RealtimeServerEventResponseMCPCallInProgress = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.mcp_call.in_progress").annotate({ + "description": "The event type, must be `response.mcp_call.in_progress`." + }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item." }) +}).annotate({ "description": "Returned when an MCP tool call has started and is in progress." }) +export type RealtimeServerEventResponseTextDelta = { + readonly "event_id": string + readonly "type": "response.output_text.delta" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string +} +export const RealtimeServerEventResponseTextDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_text.delta").annotate({ + "description": "The event type, must be `response.output_text.delta`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The text delta." }) +}).annotate({ "description": "Returned when the text value of an \"output_text\" content part is updated." }) +export type RealtimeServerEventResponseTextDone = { + readonly "event_id": string + readonly "type": "response.output_text.done" + readonly "response_id": string + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "text": string +} +export const RealtimeServerEventResponseTextDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_text.done").annotate({ + "description": "The event type, must be `response.output_text.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the response." }), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the response." }).check( + Schema.isInt() + ), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part in the item's content array." + }).check(Schema.isInt()), + "text": Schema.String.annotate({ "description": "The final text content." }) +}).annotate({ + "description": + "Returned when the text value of an \"output_text\" content part is done streaming. Also\nemitted when a Response is interrupted, incomplete, or cancelled.\n" +}) +export type RealtimeTranscriptionSessionCreateResponse = { + readonly "client_secret": { readonly "value": string; readonly "expires_at": number } + readonly "modalities"?: ReadonlyArray<"text" | "audio"> + readonly "input_audio_format"?: string + readonly "input_audio_transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + } + readonly "turn_detection"?: { + readonly "type"?: string + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } +} +export const RealtimeTranscriptionSessionCreateResponse = Schema.Struct({ + "client_secret": Schema.Struct({ + "value": Schema.String.annotate({ + "description": + "Ephemeral key usable in client environments to authenticate connections\nto the Realtime API. Use this in client-side environments rather than\na standard API token, which should only be used server-side.\n" + }), + "expires_at": Schema.Number.annotate({ + "description": "Timestamp for when the token expires. Currently, all tokens expire\nafter one minute.\n", + "format": "unixtime" + }).check(Schema.isInt()) + }).annotate({ + "description": + "Ephemeral key returned by the API. Only present when the session is\ncreated on the server via REST API.\n" + }), + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": "The set of modalities the model can respond with. To disable audio,\nset this to [\"text\"].\n" + }) + ), + "input_audio_format": Schema.optionalKey( + Schema.String.annotate({ + "description": "The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\n" + }) + ), + "input_audio_transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model used for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`.\n" + }) + ), + "language": Schema.optionalKey(Schema.String.annotate({ "description": "The language of the input audio.\n" })), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt configured for input audio transcription, when present.\n" + }) + ) + }).annotate({ "description": "Configuration of the transcription model.\n" }) + ), + "turn_detection": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ "description": "Type of turn detection, only `server_vad` is currently supported.\n" }) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "Configuration for turn detection. Can be set to `null` to turn off. Server\nVAD means that the model will detect the start and end of speech based on\naudio volume and respond at the end of user speech.\n" + }) + ) +}).annotate({ + "description": + "A new Realtime transcription session configuration.\n\nWhen a session is created on the server via REST API, the session object\nalso contains an ephemeral key. Default TTL for keys is 10 minutes. This\nproperty is not present when a session is updated via the WebSocket API.\n" +}) +export type RealtimeTranslationClientEventInputAudioBufferAppend = { + readonly "event_id"?: string + readonly "type": "session.input_audio_buffer.append" + readonly "audio": string +} +export const RealtimeTranslationClientEventInputAudioBufferAppend = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("session.input_audio_buffer.append").annotate({ + "description": "The event type, must be `session.input_audio_buffer.append`." + }), + "audio": Schema.String.annotate({ "description": "Base64-encoded 24 kHz PCM16 mono audio bytes." }) +}).annotate({ + "description": + "Send this event to append audio bytes to the translation session input audio buffer.\n\nWebSocket translation sessions accept base64-encoded 24 kHz PCM16 mono\nlittle-endian raw audio bytes. Unsupported websocket audio formats return a\nvalidation error because lower-quality audio materially degrades translation\nquality.\n\nTranslation consumes 200 ms engine frames. For best realtime behavior, append\naudio in 200 ms chunks. If a chunk is shorter, the server buffers it until it\nhas enough audio for one frame. If a chunk is longer, the server splits it into\n200 ms frames and enqueues them back-to-back.\n\nKeep appending silence while the session is active. If a client stops sending\naudio and later resumes, model time treats the resumed audio as contiguous with\nthe previous audio rather than as a real-world pause.\n" +}) +export type RealtimeTranslationClientEventSessionClose = { + readonly "event_id"?: string + readonly "type": "session.close" +} +export const RealtimeTranslationClientEventSessionClose = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("session.close").annotate({ "description": "The event type, must be `session.close`." }) +}).annotate({ + "description": + "Gracefully close the realtime translation session. The server flushes pending\ninput audio and emits any remaining translated output before closing the\nsession.\n" +}) +export type RealtimeTranslationServerEventSessionClosed = { + readonly "event_id": string + readonly "type": "session.closed" +} +export const RealtimeTranslationServerEventSessionClosed = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.closed").annotate({ "description": "The event type, must be `session.closed`." }) +}).annotate({ "description": "Returned when a realtime translation session is closed.\n" }) +export type RealtimeTranslationServerEventSessionInputTranscriptDelta = { + readonly "event_id": string + readonly "type": "session.input_transcript.delta" + readonly "delta": string + readonly "elapsed_ms"?: number | null +} +export const RealtimeTranslationServerEventSessionInputTranscriptDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.input_transcript.delta").annotate({ + "description": "The event type, must be `session.input_transcript.delta`." + }), + "delta": Schema.String.annotate({ "description": "Append-only source-language transcript text." }), + "elapsed_ms": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "Timing metadata for stream alignment, derived from the translation frame\nwhen available. It advances in 200 ms increments, but multiple transcript\ndeltas may share the same `elapsed_ms`. Treat it as alignment metadata,\nnot a unique transcript-delta identifier.\n" + }).check(Schema.isInt()), + Schema.Null + ])) +}).annotate({ + "description": + "Returned when optional source-language transcript text is available. This event\nis emitted only when `audio.input.transcription` is configured.\n\nTranscript deltas are append-only text fragments. Clients should not insert\nunconditional spaces between deltas.\n" +}) +export type RealtimeTranslationServerEventSessionOutputAudioDelta = { + readonly "event_id": string + readonly "type": "session.output_audio.delta" + readonly "delta": string + readonly "sample_rate"?: number + readonly "channels"?: number + readonly "format"?: "pcm16" + readonly "elapsed_ms"?: number | null +} +export const RealtimeTranslationServerEventSessionOutputAudioDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.output_audio.delta").annotate({ + "description": "The event type, must be `session.output_audio.delta`." + }), + "delta": Schema.String.annotate({ "description": "Base64-encoded translated audio data." }), + "sample_rate": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sample rate of the audio delta." }).check(Schema.isInt()) + ), + "channels": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio channels." }).check(Schema.isInt()) + ), + "format": Schema.optionalKey(Schema.Literal("pcm16").annotate({ "description": "Audio encoding for `delta`." })), + "elapsed_ms": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Timing metadata for stream alignment, derived from the translation frame\nwhen available. Treat `elapsed_ms` as alignment metadata, not a unique\nevent identifier.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) +}).annotate({ + "description": + "Returned when translated output audio is available. Output audio deltas are\n200 ms frames of PCM16 audio.\n" +}) +export type RealtimeTranslationServerEventSessionOutputTranscriptDelta = { + readonly "event_id": string + readonly "type": "session.output_transcript.delta" + readonly "delta": string + readonly "elapsed_ms"?: number | null +} +export const RealtimeTranslationServerEventSessionOutputTranscriptDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.output_transcript.delta").annotate({ + "description": "The event type, must be `session.output_transcript.delta`." + }), + "delta": Schema.String.annotate({ "description": "Append-only transcript text for the translated output audio." }), + "elapsed_ms": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "Timing metadata for stream alignment, derived from the translation frame\nwhen available. It advances in 200 ms increments, but multiple transcript\ndeltas may share the same `elapsed_ms`. Treat it as alignment metadata,\nnot a unique transcript-delta identifier.\n" + }).check(Schema.isInt()), + Schema.Null + ])) +}).annotate({ + "description": + "Returned when translated transcript text is available.\n\nTranscript deltas are append-only text fragments. Clients should not insert\nunconditional spaces between deltas.\n" +}) +export type RealtimeTruncation = "auto" | "disabled" | { + readonly "type": "retention_ratio" + readonly "retention_ratio": number + readonly "token_limits"?: { readonly "post_instructions"?: number } +} +export const RealtimeTruncation = Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the session. `auto` is the default truncation strategy. `disabled` will disable truncation and emit errors when the conversation exceeds the input token limit." + }), + Schema.Struct({ + "type": Schema.Literal("retention_ratio").annotate({ "description": "Use retention ratio truncation." }), + "retention_ratio": Schema.Number.annotate({ + "description": + "Fraction of post-instruction conversation tokens to retain (`0.0` - `1.0`) when the conversation exceeds the input token limit. Setting this to `0.8` means that messages will be dropped until 80% of the maximum allowed tokens are used. This helps reduce the frequency of truncations and improve cache rates.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + "token_limits": Schema.optionalKey( + Schema.Struct({ + "post_instructions": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum tokens allowed in the conversation after instructions (which including tool definitions). For example, setting this to 5,000 would mean that truncation would occur when the conversation exceeds 5,000 tokens after instructions. This cannot be higher than the model's context window size minus the maximum output tokens." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + ) + }).annotate({ + "description": + "Optional custom token limits for this truncation strategy. If not provided, the model's default token limits will be used." + }) + ) + }).annotate({ + "title": "Retention ratio truncation", + "description": + "Retain a fraction of the conversation tokens when the conversation exceeds the input token limit. This allows you to amortize truncations across multiple turns, which can help improve cached token usage." + }) +], { mode: "oneOf" }).annotate({ + "title": "Realtime Truncation Controls", + "description": + "When the number of tokens in a conversation exceeds the model's input token limit, the conversation be truncated, meaning messages (starting from the oldest) will not be included in the model's context. A 32k context model with 4,096 max output tokens can only include 28,224 tokens in the context before truncation occurs.\n\nClients can configure truncation behavior to truncate with a lower max token limit, which is an effective way to control token usage and cost.\n\nTruncation will reduce the number of cached tokens on the next turn (busting the cache), since messages are dropped from the beginning of the context. However, clients can also configure truncation to retain messages up to a fraction of the maximum context size, which will reduce the need for future truncations and thus improve the cache rate.\n\nTruncation can be disabled entirely, which means the server will never truncate but would instead return an error if the conversation exceeds the model's input token limit.\n" +}) +export type RealtimeTurnDetection = { + readonly "type": "server_vad" + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + readonly "create_response"?: boolean + readonly "interrupt_response"?: boolean + readonly "idle_timeout_ms"?: number | null +} | { + readonly "type": "semantic_vad" + readonly "eagerness"?: "low" | "medium" | "high" | "auto" + readonly "create_response"?: boolean + readonly "interrupt_response"?: boolean +} | null +export const RealtimeTurnDetection = Schema.Union([ + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("server_vad").annotate({ + "description": "Type of turn detection, `server_vad` to turn on simple Server VAD.\n" + }), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Used only for `server_vad` mode. Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Used only for `server_vad` mode. Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Used only for `server_vad` mode. Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ), + "create_response": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "Whether or not to automatically generate a response when a VAD stop event occurs. If `interrupt_response` is set to `false` this may fail to create a response if the model is already responding.\n\nIf both `create_response` and `interrupt_response` are set to `false`, the model will never respond automatically but VAD events will still be emitted.\n" + })), + "interrupt_response": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "Whether or not to automatically interrupt (cancel) any ongoing response with output to the default\nconversation (i.e. `conversation` of `auto`) when a VAD start event occurs. If `true` then the response will be cancelled, otherwise it will continue until complete.\n\nIf both `create_response` and `interrupt_response` are set to `false`, the model will never respond automatically but VAD events will still be emitted.\n" + })), + "idle_timeout_ms": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "Optional timeout after which a model response will be triggered automatically. This is\nuseful for situations in which a long pause from the user is unexpected, such as a phone\ncall. The model will effectively prompt the user to continue the conversation based\non the current context.\n\nThe timeout value will be applied after the last model response's audio has finished playing,\ni.e. it's set to the `response.done` time plus audio playback duration.\n\nAn `input_audio_buffer.timeout_triggered` event (plus events\nassociated with the Response) will be emitted when the timeout is reached.\nIdle timeout is currently only supported for `server_vad` mode.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(5000)).check(Schema.isLessThanOrEqualTo(30000)), + Schema.Null + ])) + }).annotate({ + "title": "Server VAD", + "description": + "Server-side voice activity detection (VAD) which flips on when user speech is detected and off after a period of silence." + }), + Schema.Struct({ + "type": Schema.Literal("semantic_vad").annotate({ + "description": "Type of turn detection, `semantic_vad` to turn on Semantic VAD.\n" + }), + "eagerness": Schema.optionalKey( + Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": + "Used only for `semantic_vad` mode. The eagerness of the model to respond. `low` will wait longer for the user to continue speaking, `high` will respond more quickly. `auto` is the default and is equivalent to `medium`. `low`, `medium`, and `high` have max timeouts of 8s, 4s, and 2s respectively.\n" + }) + ), + "create_response": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Whether or not to automatically generate a response when a VAD stop event occurs.\n" + }) + ), + "interrupt_response": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether or not to automatically interrupt any ongoing response with output to the default\nconversation (i.e. `conversation` of `auto`) when a VAD start event occurs.\n" + }) + ) + }).annotate({ + "title": "Semantic VAD", + "description": + "Server-side semantic turn detection which uses a model to determine when the user has finished speaking." + }) + ], { mode: "oneOf" }).annotate({ + "title": "Realtime Turn Detection", + "description": + "Configuration for turn detection, ether Server VAD or Semantic VAD. This can be set to `null` to turn off, in which case the client must manually trigger model response.\n\nServer VAD means that the model will detect the start and end of speech based on audio volume and respond at the end of user speech.\n\nSemantic VAD is more advanced and uses a turn detection model (in conjunction with VAD) to semantically estimate whether the user has finished speaking, then dynamically sets a timeout based on this probability. For example, if user audio trails off with \"uhhm\", the model will score a low probability of turn end and wait longer for the user to continue speaking. This can be useful for more natural conversations, but may have a higher latency.\n\nFor `gpt-realtime-whisper` transcription sessions, turn detection must be\nset to `null`; VAD is not supported.\n" + }), + Schema.Null +]) +export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | null +export const ReasoningEffort = Schema.Union([ + Schema.Literals(["none", "minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Constrains effort on reasoning for\n[reasoning models](https://platform.openai.com/docs/guides/reasoning).\nCurrently supported values are `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Reducing\nreasoning effort can result in faster responses and fewer tokens used\non reasoning in a response.\n\n- `gpt-5.1` defaults to `none`, which does not perform reasoning. The supported reasoning values for `gpt-5.1` are `none`, `low`, `medium`, and `high`. Tool calls are supported for all reasoning values in gpt-5.1.\n- All models before `gpt-5.1` default to `medium` reasoning effort, and do not support `none`.\n- The `gpt-5-pro` model defaults to (and only supports) `high` reasoning effort.\n- `xhigh` is supported for all models after `gpt-5.1-codex-max`.\n" + }), + Schema.Null +]) +export type ResponseAudioDeltaEvent = { + readonly "type": "response.audio.delta" + readonly "sequence_number": number + readonly "delta": string +} +export const ResponseAudioDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.audio.delta").annotate({ + "description": "The type of the event. Always `response.audio.delta`.\n" + }), + "sequence_number": Schema.Number.annotate({ + "description": "A sequence number for this chunk of the stream response.\n" + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "A chunk of Base64 encoded response audio bytes.\n" }) +}).annotate({ "description": "Emitted when there is a partial audio response." }) +export type ResponseAudioDoneEvent = { + readonly "type": "response.audio.done" + readonly "sequence_number": number + readonly "response_id": unknown +} +export const ResponseAudioDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.audio.done").annotate({ + "description": "The type of the event. Always `response.audio.done`.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of the delta.\n" }).check( + Schema.isInt() + ), + "response_id": Schema.Unknown +}).annotate({ "description": "Emitted when the audio response is complete." }) +export type ResponseAudioTranscriptDeltaEvent = { + readonly "type": "response.audio.transcript.delta" + readonly "delta": string + readonly "sequence_number": number + readonly "response_id": unknown +} +export const ResponseAudioTranscriptDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.audio.transcript.delta").annotate({ + "description": "The type of the event. Always `response.audio.transcript.delta`.\n" + }), + "delta": Schema.String.annotate({ "description": "The partial transcript of the audio response.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "response_id": Schema.Unknown +}).annotate({ "description": "Emitted when there is a partial transcript of audio." }) +export type ResponseAudioTranscriptDoneEvent = { + readonly "type": "response.audio.transcript.done" + readonly "sequence_number": number + readonly "response_id": unknown +} +export const ResponseAudioTranscriptDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.audio.transcript.done").annotate({ + "description": "The type of the event. Always `response.audio.transcript.done`.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "response_id": Schema.Unknown +}).annotate({ "description": "Emitted when the full audio transcript is completed." }) +export type ResponseCodeInterpreterCallCodeDeltaEvent = { + readonly "type": "response.code_interpreter_call_code.delta" + readonly "output_index": number + readonly "item_id": string + readonly "delta": string + readonly "sequence_number": number +} +export const ResponseCodeInterpreterCallCodeDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.code_interpreter_call_code.delta").annotate({ + "description": "The type of the event. Always `response.code_interpreter_call_code.delta`." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response for which the code is being streamed." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The unique identifier of the code interpreter tool call item." }), + "delta": Schema.String.annotate({ + "description": "The partial code snippet being streamed by the code interpreter." + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of this event, used to order streaming events." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when a partial code snippet is streamed by the code interpreter." }) +export type ResponseCodeInterpreterCallCodeDoneEvent = { + readonly "type": "response.code_interpreter_call_code.done" + readonly "output_index": number + readonly "item_id": string + readonly "code": string + readonly "sequence_number": number +} +export const ResponseCodeInterpreterCallCodeDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.code_interpreter_call_code.done").annotate({ + "description": "The type of the event. Always `response.code_interpreter_call_code.done`." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response for which the code is finalized." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The unique identifier of the code interpreter tool call item." }), + "code": Schema.String.annotate({ "description": "The final code snippet output by the code interpreter." }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of this event, used to order streaming events." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when the code snippet is finalized by the code interpreter." }) +export type ResponseCodeInterpreterCallCompletedEvent = { + readonly "type": "response.code_interpreter_call.completed" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseCodeInterpreterCallCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.code_interpreter_call.completed").annotate({ + "description": "The type of the event. Always `response.code_interpreter_call.completed`." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response for which the code interpreter call is completed." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The unique identifier of the code interpreter tool call item." }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of this event, used to order streaming events." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when the code interpreter call is completed." }) +export type ResponseCodeInterpreterCallInProgressEvent = { + readonly "type": "response.code_interpreter_call.in_progress" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseCodeInterpreterCallInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.code_interpreter_call.in_progress").annotate({ + "description": "The type of the event. Always `response.code_interpreter_call.in_progress`." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response for which the code interpreter call is in progress." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The unique identifier of the code interpreter tool call item." }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of this event, used to order streaming events." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when a code interpreter call is in progress." }) +export type ResponseCodeInterpreterCallInterpretingEvent = { + readonly "type": "response.code_interpreter_call.interpreting" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseCodeInterpreterCallInterpretingEvent = Schema.Struct({ + "type": Schema.Literal("response.code_interpreter_call.interpreting").annotate({ + "description": "The type of the event. Always `response.code_interpreter_call.interpreting`." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response for which the code interpreter is interpreting code." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ "description": "The unique identifier of the code interpreter tool call item." }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of this event, used to order streaming events." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when the code interpreter is actively interpreting the code snippet." }) +export type ResponseCustomToolCallInputDeltaEvent = { + readonly "type": "response.custom_tool_call_input.delta" + readonly "sequence_number": number + readonly "output_index": number + readonly "item_id": string + readonly "delta": string +} +export const ResponseCustomToolCallInputDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.custom_tool_call_input.delta").annotate({ + "description": "The event type identifier." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "output_index": Schema.Number.annotate({ "description": "The index of the output this delta applies to." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ + "description": "Unique identifier for the API item associated with this event." + }), + "delta": Schema.String.annotate({ "description": "The incremental input data (delta) for the custom tool call." }) +}).annotate({ + "title": "ResponseCustomToolCallInputDelta", + "description": "Event representing a delta (partial update) to the input of a custom tool call.\n" +}) +export type ResponseCustomToolCallInputDoneEvent = { + readonly "type": "response.custom_tool_call_input.done" + readonly "sequence_number": number + readonly "output_index": number + readonly "item_id": string + readonly "input": string +} +export const ResponseCustomToolCallInputDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.custom_tool_call_input.done").annotate({ + "description": "The event type identifier." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "output_index": Schema.Number.annotate({ "description": "The index of the output this event applies to." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ + "description": "Unique identifier for the API item associated with this event." + }), + "input": Schema.String.annotate({ "description": "The complete input data for the custom tool call." }) +}).annotate({ + "title": "ResponseCustomToolCallInputDone", + "description": "Event indicating that input for a custom tool call is complete.\n" +}) +export type ResponseErrorCode = + | "server_error" + | "rate_limit_exceeded" + | "invalid_prompt" + | "vector_store_timeout" + | "invalid_image" + | "invalid_image_format" + | "invalid_base64_image" + | "invalid_image_url" + | "image_too_large" + | "image_too_small" + | "image_parse_error" + | "image_content_policy_violation" + | "invalid_image_mode" + | "image_file_too_large" + | "unsupported_image_media_type" + | "empty_image_file" + | "failed_to_download_image" + | "image_file_not_found" +export const ResponseErrorCode = Schema.Literals([ + "server_error", + "rate_limit_exceeded", + "invalid_prompt", + "vector_store_timeout", + "invalid_image", + "invalid_image_format", + "invalid_base64_image", + "invalid_image_url", + "image_too_large", + "image_too_small", + "image_parse_error", + "image_content_policy_violation", + "invalid_image_mode", + "image_file_too_large", + "unsupported_image_media_type", + "empty_image_file", + "failed_to_download_image", + "image_file_not_found" +]).annotate({ "description": "The error code for the response.\n" }) +export type ResponseErrorEvent = { + readonly "type": "error" + readonly "code": string | null + readonly "message": string + readonly "param": string | null + readonly "sequence_number": number +} +export const ResponseErrorEvent = Schema.Struct({ + "type": Schema.Literal("error").annotate({ "description": "The type of the event. Always `error`.\n" }), + "code": Schema.Union([Schema.String.annotate({ "description": "The error code.\n" }), Schema.Null]), + "message": Schema.String.annotate({ "description": "The error message.\n" }), + "param": Schema.Union([Schema.String.annotate({ "description": "The error parameter.\n" }), Schema.Null]), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when an error occurs." }) +export type ResponseFileSearchCallCompletedEvent = { + readonly "type": "response.file_search_call.completed" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseFileSearchCallCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.file_search_call.completed").annotate({ + "description": "The type of the event. Always `response.file_search_call.completed`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the file search call is initiated.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the file search call is initiated.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a file search call is completed (results found)." }) +export type ResponseFileSearchCallInProgressEvent = { + readonly "type": "response.file_search_call.in_progress" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseFileSearchCallInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.file_search_call.in_progress").annotate({ + "description": "The type of the event. Always `response.file_search_call.in_progress`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the file search call is initiated.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the file search call is initiated.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a file search call is initiated." }) +export type ResponseFileSearchCallSearchingEvent = { + readonly "type": "response.file_search_call.searching" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseFileSearchCallSearchingEvent = Schema.Struct({ + "type": Schema.Literal("response.file_search_call.searching").annotate({ + "description": "The type of the event. Always `response.file_search_call.searching`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the file search call is searching.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the file search call is initiated.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a file search is currently searching." }) +export type ResponseFormatJsonObject = { readonly "type": "json_object" } +export const ResponseFormatJsonObject = Schema.Struct({ + "type": Schema.Literal("json_object").annotate({ + "description": "The type of response format being defined. Always `json_object`." + }) +}).annotate({ + "title": "JSON object", + "description": + "JSON object response format. An older method of generating JSON responses.\nUsing `json_schema` is recommended for models that support it. Note that the\nmodel will not generate JSON without a system or user message instructing it\nto do so.\n" +}) +export type ResponseFormatJsonSchemaSchema = {} +export const ResponseFormatJsonSchemaSchema = Schema.Struct({}).annotate({ + "title": "JSON schema", + "description": + "The schema for the response format, described as a JSON Schema object.\nLearn how to build JSON schemas [here](https://json-schema.org/).\n" +}) +export type ResponseFormatText = { readonly "type": "text" } +export const ResponseFormatText = Schema.Struct({ + "type": Schema.Literal("text").annotate({ + "description": "The type of response format being defined. Always `text`." + }) +}).annotate({ "title": "Text", "description": "Default response format. Used to generate text responses.\n" }) +export type ResponseFunctionCallArgumentsDeltaEvent = { + readonly "type": "response.function_call_arguments.delta" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "delta": string +} +export const ResponseFunctionCallArgumentsDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.function_call_arguments.delta").annotate({ + "description": "The type of the event. Always `response.function_call_arguments.delta`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the function-call arguments delta is added to.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the function-call arguments delta is added to.\n" + }).check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "delta": Schema.String.annotate({ "description": "The function-call arguments delta that is added.\n" }) +}).annotate({ "description": "Emitted when there is a partial function-call arguments delta." }) +export type ResponseFunctionCallArgumentsDoneEvent = { + readonly "type": "response.function_call_arguments.done" + readonly "item_id": string + readonly "name"?: string + readonly "output_index": number + readonly "sequence_number": number + readonly "arguments": string +} +export const ResponseFunctionCallArgumentsDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.function_call_arguments.done"), + "item_id": Schema.String.annotate({ "description": "The ID of the item." }), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function that was called." })), + "output_index": Schema.Number.annotate({ "description": "The index of the output item." }).check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "arguments": Schema.String.annotate({ "description": "The function-call arguments." }) +}).annotate({ "description": "Emitted when function-call arguments are finalized." }) +export type ResponseImageGenCallCompletedEvent = { + readonly "type": "response.image_generation_call.completed" + readonly "output_index": number + readonly "sequence_number": number + readonly "item_id": string +} +export const ResponseImageGenCallCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.completed").annotate({ + "description": "The type of the event. Always 'response.image_generation_call.completed'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the image generation item being processed." + }) +}).annotate({ + "title": "ResponseImageGenCallCompletedEvent", + "description": "Emitted when an image generation tool call has completed and the final image is available.\n" +}) +export type ResponseImageGenCallGeneratingEvent = { + readonly "type": "response.image_generation_call.generating" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseImageGenCallGeneratingEvent = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.generating").annotate({ + "description": "The type of the event. Always 'response.image_generation_call.generating'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the image generation item being processed." + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the image generation item being processed." + }).check(Schema.isInt()) +}).annotate({ + "title": "ResponseImageGenCallGeneratingEvent", + "description": "Emitted when an image generation tool call is actively generating an image (intermediate state).\n" +}) +export type ResponseImageGenCallInProgressEvent = { + readonly "type": "response.image_generation_call.in_progress" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseImageGenCallInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.in_progress").annotate({ + "description": "The type of the event. Always 'response.image_generation_call.in_progress'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the image generation item being processed." + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the image generation item being processed." + }).check(Schema.isInt()) +}).annotate({ + "title": "ResponseImageGenCallInProgressEvent", + "description": "Emitted when an image generation tool call is in progress.\n" +}) +export type ResponseImageGenCallPartialImageEvent = { + readonly "type": "response.image_generation_call.partial_image" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number + readonly "partial_image_index": number + readonly "partial_image_b64": string +} +export const ResponseImageGenCallPartialImageEvent = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.partial_image").annotate({ + "description": "The type of the event. Always 'response.image_generation_call.partial_image'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the image generation item being processed." + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the image generation item being processed." + }).check(Schema.isInt()), + "partial_image_index": Schema.Number.annotate({ + "description": "0-based index for the partial image (backend is 1-based, but this is 0-based for the user)." + }).check(Schema.isInt()), + "partial_image_b64": Schema.String.annotate({ + "description": "Base64-encoded partial image data, suitable for rendering as an image." + }) +}).annotate({ + "title": "ResponseImageGenCallPartialImageEvent", + "description": "Emitted when a partial image is available during image generation streaming.\n" +}) +export type ResponseLogProb = { + readonly "token": string + readonly "logprob": number + readonly "top_logprobs"?: ReadonlyArray<{ readonly "token"?: string; readonly "logprob"?: number }> +} +export const ResponseLogProb = Schema.Struct({ + "token": Schema.String.annotate({ "description": "A possible text token." }), + "logprob": Schema.Number.annotate({ "description": "The log probability of this token.\n" }).check(Schema.isFinite()), + "top_logprobs": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "token": Schema.optionalKey(Schema.String.annotate({ "description": "A possible text token." })), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "description": "The log probability of this token." }).check(Schema.isFinite()) + ) + }) + ).annotate({ "description": "The log probabilities of up to 20 of the most likely tokens.\n" }) + ) +}).annotate({ + "description": + "A logprob is the logarithmic probability that the model assigns to producing \na particular token at a given position in the sequence. Less-negative (higher) \nlogprob values indicate greater model confidence in that token choice.\n" +}) +export type ResponseMCPCallArgumentsDeltaEvent = { + readonly "type": "response.mcp_call_arguments.delta" + readonly "output_index": number + readonly "item_id": string + readonly "delta": string + readonly "sequence_number": number +} +export const ResponseMCPCallArgumentsDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_call_arguments.delta").annotate({ + "description": "The type of the event. Always 'response.mcp_call_arguments.delta'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the MCP tool call item being processed." + }), + "delta": Schema.String.annotate({ + "description": "A JSON string containing the partial update to the arguments for the MCP tool call.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPCallArgumentsDeltaEvent", + "description": "Emitted when there is a delta (partial update) to the arguments of an MCP tool call.\n" +}) +export type ResponseMCPCallArgumentsDoneEvent = { + readonly "type": "response.mcp_call_arguments.done" + readonly "output_index": number + readonly "item_id": string + readonly "arguments": string + readonly "sequence_number": number +} +export const ResponseMCPCallArgumentsDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_call_arguments.done").annotate({ + "description": "The type of the event. Always 'response.mcp_call_arguments.done'." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the MCP tool call item being processed." + }), + "arguments": Schema.String.annotate({ + "description": "A JSON string containing the finalized arguments for the MCP tool call.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPCallArgumentsDoneEvent", + "description": "Emitted when the arguments for an MCP tool call are finalized.\n" +}) +export type ResponseMCPCallCompletedEvent = { + readonly "type": "response.mcp_call.completed" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const ResponseMCPCallCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_call.completed").annotate({ + "description": "The type of the event. Always 'response.mcp_call.completed'." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item that completed." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that completed." }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPCallCompletedEvent", + "description": "Emitted when an MCP tool call has completed successfully.\n" +}) +export type ResponseMCPCallFailedEvent = { + readonly "type": "response.mcp_call.failed" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const ResponseMCPCallFailedEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_call.failed").annotate({ + "description": "The type of the event. Always 'response.mcp_call.failed'." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item that failed." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that failed." }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "title": "ResponseMCPCallFailedEvent", "description": "Emitted when an MCP tool call has failed.\n" }) +export type ResponseMCPCallInProgressEvent = { + readonly "type": "response.mcp_call.in_progress" + readonly "sequence_number": number + readonly "output_index": number + readonly "item_id": string +} +export const ResponseMCPCallInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_call.in_progress").annotate({ + "description": "The type of the event. Always 'response.mcp_call.in_progress'." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the MCP tool call item being processed." + }) +}).annotate({ + "title": "ResponseMCPCallInProgressEvent", + "description": "Emitted when an MCP tool call is in progress.\n" +}) +export type ResponseMCPListToolsCompletedEvent = { + readonly "type": "response.mcp_list_tools.completed" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const ResponseMCPListToolsCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_list_tools.completed").annotate({ + "description": "The type of the event. Always 'response.mcp_list_tools.completed'." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item that produced this output." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that was processed." }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPListToolsCompletedEvent", + "description": "Emitted when the list of available MCP tools has been successfully retrieved.\n" +}) +export type ResponseMCPListToolsFailedEvent = { + readonly "type": "response.mcp_list_tools.failed" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const ResponseMCPListToolsFailedEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_list_tools.failed").annotate({ + "description": "The type of the event. Always 'response.mcp_list_tools.failed'." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item that failed." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that failed." }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPListToolsFailedEvent", + "description": "Emitted when the attempt to list available MCP tools has failed.\n" +}) +export type ResponseMCPListToolsInProgressEvent = { + readonly "type": "response.mcp_list_tools.in_progress" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const ResponseMCPListToolsInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.mcp_list_tools.in_progress").annotate({ + "description": "The type of the event. Always 'response.mcp_list_tools.in_progress'." + }), + "item_id": Schema.String.annotate({ "description": "The ID of the MCP tool call item that is being processed." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that is being processed." }) + .check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseMCPListToolsInProgressEvent", + "description": "Emitted when the system is in the process of retrieving the list of available MCP tools.\n" +}) +export type ResponseModalities = ReadonlyArray<"text" | "audio"> | null +export const ResponseModalities = Schema.Union([ + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "Output types that you would like the model to generate.\nMost models are capable of generating text, which is the default:\n\n`[\"text\"]`\n\nThe `gpt-4o-audio-preview` model can also be used to\n[generate audio](/docs/guides/audio). To request that this model generate\nboth text and audio responses, you can use:\n\n`[\"text\", \"audio\"]`\n" + }), + Schema.Null +]) +export type ResponseOutputTextAnnotationAddedEvent = { + readonly "type": "response.output_text.annotation.added" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "annotation_index": number + readonly "sequence_number": number + readonly "annotation": {} +} +export const ResponseOutputTextAnnotationAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.annotation.added").annotate({ + "description": "The type of the event. Always 'response.output_text.annotation.added'." + }), + "item_id": Schema.String.annotate({ + "description": "The unique identifier of the item to which the annotation is being added." + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item in the response's output array." + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ "description": "The index of the content part within the output item." }) + .check(Schema.isInt()), + "annotation_index": Schema.Number.annotate({ "description": "The index of the annotation within the content part." }) + .check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "annotation": Schema.Struct({}).annotate({ + "description": "The annotation object being added. (See annotation schema for details.)" + }) +}).annotate({ + "title": "ResponseOutputTextAnnotationAddedEvent", + "description": "Emitted when an annotation is added to output text content.\n" +}) +export type ResponsePromptVariables = {} | null +export const ResponsePromptVariables = Schema.Union([ + Schema.Struct({}).annotate({ + "title": "Prompt Variables", + "description": + "Optional map of values to substitute in for variables in your\nprompt. The substitution values can either be strings, or other\nResponse input types like images or files.\n" + }), + Schema.Null +]) +export type ResponseReasoningSummaryPartAddedEvent = { + readonly "type": "response.reasoning_summary_part.added" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "sequence_number": number + readonly "part": { readonly "type": "summary_text"; readonly "text": string } +} +export const ResponseReasoningSummaryPartAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_part.added").annotate({ + "description": "The type of the event. Always `response.reasoning_summary_part.added`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item this summary part is associated with.\n" }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this summary part is associated with.\n" + }).check(Schema.isInt()), + "summary_index": Schema.Number.annotate({ + "description": "The index of the summary part within the reasoning summary.\n" + }).check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ), + "part": Schema.Struct({ + "type": Schema.Literal("summary_text").annotate({ + "description": "The type of the summary part. Always `summary_text`." + }), + "text": Schema.String.annotate({ "description": "The text of the summary part." }) + }).annotate({ "description": "The summary part that was added.\n" }) +}).annotate({ "description": "Emitted when a new reasoning summary part is added." }) +export type ResponseReasoningSummaryPartDoneEvent = { + readonly "type": "response.reasoning_summary_part.done" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "sequence_number": number + readonly "part": { readonly "type": "summary_text"; readonly "text": string } +} +export const ResponseReasoningSummaryPartDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_part.done").annotate({ + "description": "The type of the event. Always `response.reasoning_summary_part.done`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item this summary part is associated with.\n" }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this summary part is associated with.\n" + }).check(Schema.isInt()), + "summary_index": Schema.Number.annotate({ + "description": "The index of the summary part within the reasoning summary.\n" + }).check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ), + "part": Schema.Struct({ + "type": Schema.Literal("summary_text").annotate({ + "description": "The type of the summary part. Always `summary_text`." + }), + "text": Schema.String.annotate({ "description": "The text of the summary part." }) + }).annotate({ "description": "The completed summary part.\n" }) +}).annotate({ "description": "Emitted when a reasoning summary part is completed." }) +export type ResponseReasoningSummaryTextDeltaEvent = { + readonly "type": "response.reasoning_summary_text.delta" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const ResponseReasoningSummaryTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_text.delta").annotate({ + "description": "The type of the event. Always `response.reasoning_summary_text.delta`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the item this summary text delta is associated with.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this summary text delta is associated with.\n" + }).check(Schema.isInt()), + "summary_index": Schema.Number.annotate({ + "description": "The index of the summary part within the reasoning summary.\n" + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The text delta that was added to the summary.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a delta is added to a reasoning summary text." }) +export type ResponseReasoningSummaryTextDoneEvent = { + readonly "type": "response.reasoning_summary_text.done" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "text": string + readonly "sequence_number": number +} +export const ResponseReasoningSummaryTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_text.done").annotate({ + "description": "The type of the event. Always `response.reasoning_summary_text.done`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item this summary text is associated with.\n" }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this summary text is associated with.\n" + }).check(Schema.isInt()), + "summary_index": Schema.Number.annotate({ + "description": "The index of the summary part within the reasoning summary.\n" + }).check(Schema.isInt()), + "text": Schema.String.annotate({ "description": "The full text of the completed reasoning summary.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a reasoning summary text is completed." }) +export type ResponseReasoningTextDeltaEvent = { + readonly "type": "response.reasoning_text.delta" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const ResponseReasoningTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_text.delta").annotate({ + "description": "The type of the event. Always `response.reasoning_text.delta`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the item this reasoning text delta is associated with.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this reasoning text delta is associated with.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ + "description": "The index of the reasoning content part this delta is associated with.\n" + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The text delta that was added to the reasoning content.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a delta is added to a reasoning text." }) +export type ResponseReasoningTextDoneEvent = { + readonly "type": "response.reasoning_text.done" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "text": string + readonly "sequence_number": number +} +export const ResponseReasoningTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_text.done").annotate({ + "description": "The type of the event. Always `response.reasoning_text.done`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the item this reasoning text is associated with.\n" }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item this reasoning text is associated with.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ "description": "The index of the reasoning content part.\n" }).check( + Schema.isInt() + ), + "text": Schema.String.annotate({ "description": "The full text of the completed reasoning content.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a reasoning text is completed." }) +export type ResponseRefusalDeltaEvent = { + readonly "type": "response.refusal.delta" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const ResponseRefusalDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.refusal.delta").annotate({ + "description": "The type of the event. Always `response.refusal.delta`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the refusal text is added to.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the refusal text is added to.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part that the refusal text is added to.\n" + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The refusal text that is added.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when there is a partial refusal text." }) +export type ResponseRefusalDoneEvent = { + readonly "type": "response.refusal.done" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "refusal": string + readonly "sequence_number": number +} +export const ResponseRefusalDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.refusal.done").annotate({ + "description": "The type of the event. Always `response.refusal.done`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the refusal text is finalized.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the refusal text is finalized.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part that the refusal text is finalized.\n" + }).check(Schema.isInt()), + "refusal": Schema.String.annotate({ "description": "The refusal text that is finalized.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when refusal text is finalized." }) +export type ResponseStreamOptions = { readonly "include_obfuscation"?: boolean } | null +export const ResponseStreamOptions = Schema.Union([ + Schema.Struct({ + "include_obfuscation": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "When true, stream obfuscation will be enabled. Stream obfuscation adds\nrandom characters to an `obfuscation` field on streaming delta events to\nnormalize payload sizes as a mitigation to certain side-channel attacks.\nThese obfuscation fields are included by default, but add a small amount\nof overhead to the data stream. You can set `include_obfuscation` to\nfalse to optimize for bandwidth if you trust the network links between\nyour application and the OpenAI API.\n" + })) + }).annotate({ "description": "Options for streaming responses. Only set this when you set `stream: true`.\n" }), + Schema.Null +]) +export type ResponseUsage = { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number +} +export const ResponseUsage = Schema.Struct({ + "input_tokens": Schema.Number.annotate({ "description": "The number of input tokens." }).check(Schema.isInt()), + "input_tokens_details": Schema.Struct({ + "cached_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that were retrieved from the cache. \n[More on prompt caching](/docs/guides/prompt-caching).\n" + }).check(Schema.isInt()) + }).annotate({ "description": "A detailed breakdown of the input tokens." }), + "output_tokens": Schema.Number.annotate({ "description": "The number of output tokens." }).check(Schema.isInt()), + "output_tokens_details": Schema.Struct({ + "reasoning_tokens": Schema.Number.annotate({ "description": "The number of reasoning tokens." }).check( + Schema.isInt() + ) + }).annotate({ "description": "A detailed breakdown of the output tokens." }), + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used." }).check(Schema.isInt()) +}).annotate({ + "description": + "Represents token usage details including input tokens, output tokens,\na breakdown of output tokens, and the total tokens used.\n" +}) +export type ResponseWebSearchCallCompletedEvent = { + readonly "type": "response.web_search_call.completed" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseWebSearchCallCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.web_search_call.completed").annotate({ + "description": "The type of the event. Always `response.web_search_call.completed`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the web search call is associated with.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "Unique ID for the output item associated with the web search call.\n" + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the web search call being processed." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when a web search call is completed." }) +export type ResponseWebSearchCallInProgressEvent = { + readonly "type": "response.web_search_call.in_progress" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseWebSearchCallInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.web_search_call.in_progress").annotate({ + "description": "The type of the event. Always `response.web_search_call.in_progress`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the web search call is associated with.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "Unique ID for the output item associated with the web search call.\n" + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the web search call being processed." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when a web search call is initiated." }) +export type ResponseWebSearchCallSearchingEvent = { + readonly "type": "response.web_search_call.searching" + readonly "output_index": number + readonly "item_id": string + readonly "sequence_number": number +} +export const ResponseWebSearchCallSearchingEvent = Schema.Struct({ + "type": Schema.Literal("response.web_search_call.searching").annotate({ + "description": "The type of the event. Always `response.web_search_call.searching`.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the web search call is associated with.\n" + }).check(Schema.isInt()), + "item_id": Schema.String.annotate({ + "description": "Unique ID for the output item associated with the web search call.\n" + }), + "sequence_number": Schema.Number.annotate({ + "description": "The sequence number of the web search call being processed." + }).check(Schema.isInt()) +}).annotate({ "description": "Emitted when a web search call is executing." }) +export type Role = { + readonly "object": "role" + readonly "id": string + readonly "name": string + readonly "description": string | null + readonly "permissions": ReadonlyArray + readonly "resource_type": string + readonly "predefined_role": boolean +} +export const Role = Schema.Struct({ + "object": Schema.Literal("role").annotate({ "description": "Always `role`." }), + "id": Schema.String.annotate({ "description": "Identifier for the role." }), + "name": Schema.String.annotate({ "description": "Unique name for the role." }), + "description": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Optional description of the role." + }), + "permissions": Schema.Array(Schema.String).annotate({ "description": "Permissions granted by the role." }), + "resource_type": Schema.String.annotate({ + "description": "Resource type the role is bound to (for example `api.organization` or `api.project`)." + }), + "predefined_role": Schema.Boolean.annotate({ "description": "Whether the role is predefined and managed by OpenAI." }) +}).annotate({ "description": "Details about a role that can be assigned through the public Roles API." }) +export type RoleDeletedResource = { + readonly "object": "role.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const RoleDeletedResource = Schema.Struct({ + "object": Schema.Literal("role.deleted").annotate({ "description": "Always `role.deleted`." }), + "id": Schema.String.annotate({ "description": "Identifier of the deleted role." }), + "deleted": Schema.Boolean.annotate({ "description": "Whether the role was deleted." }) +}).annotate({ "description": "Confirmation payload returned after deleting a role." }) +export type RunCompletionUsage = { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "total_tokens": number +} | null +export const RunCompletionUsage = Schema.Union([ + Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ + "description": "Number of completion tokens used over the course of the run." + }).check(Schema.isInt()), + "prompt_tokens": Schema.Number.annotate({ + "description": "Number of prompt tokens used over the course of the run." + }).check(Schema.isInt()), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (prompt + completion)." }) + .check(Schema.isInt()) + }).annotate({ + "description": + "Usage statistics related to the run. This value will be `null` if the run is not in a terminal state (i.e. `in_progress`, `queued`, etc.)." + }), + Schema.Null +]) +export type RunGraderResponse = { + readonly "reward": number + readonly "metadata": { + readonly "name": string + readonly "type": string + readonly "errors": { + readonly "formula_parse_error": boolean + readonly "sample_parse_error": boolean + readonly "truncated_observation_error": boolean + readonly "unresponsive_reward_error": boolean + readonly "invalid_variable_error": boolean + readonly "other_error": boolean + readonly "python_grader_server_error": boolean + readonly "python_grader_server_error_type": string | null + readonly "python_grader_runtime_error": boolean + readonly "python_grader_runtime_error_details": string | null + readonly "model_grader_server_error": boolean + readonly "model_grader_refusal_error": boolean + readonly "model_grader_parse_error": boolean + readonly "model_grader_server_error_details": string | null + } + readonly "execution_time": number + readonly "scores": {} + readonly "token_usage": number | null + readonly "sampled_model_name": string | null + } + readonly "sub_rewards": {} + readonly "model_grader_token_usage_per_model": {} +} +export const RunGraderResponse = Schema.Struct({ + "reward": Schema.Number.check(Schema.isFinite()), + "metadata": Schema.Struct({ + "name": Schema.String, + "type": Schema.String, + "errors": Schema.Struct({ + "formula_parse_error": Schema.Boolean, + "sample_parse_error": Schema.Boolean, + "truncated_observation_error": Schema.Boolean, + "unresponsive_reward_error": Schema.Boolean, + "invalid_variable_error": Schema.Boolean, + "other_error": Schema.Boolean, + "python_grader_server_error": Schema.Boolean, + "python_grader_server_error_type": Schema.Union([Schema.String, Schema.Null]), + "python_grader_runtime_error": Schema.Boolean, + "python_grader_runtime_error_details": Schema.Union([Schema.String, Schema.Null]), + "model_grader_server_error": Schema.Boolean, + "model_grader_refusal_error": Schema.Boolean, + "model_grader_parse_error": Schema.Boolean, + "model_grader_server_error_details": Schema.Union([Schema.String, Schema.Null]) + }), + "execution_time": Schema.Number.check(Schema.isFinite()), + "scores": Schema.Struct({}), + "token_usage": Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]), + "sampled_model_name": Schema.Union([Schema.String, Schema.Null]) + }), + "sub_rewards": Schema.Struct({}), + "model_grader_token_usage_per_model": Schema.Struct({}) +}) +export type RunStepCompletionUsage = { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "total_tokens": number +} | null +export const RunStepCompletionUsage = Schema.Union([ + Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ + "description": "Number of completion tokens used over the course of the run step." + }).check(Schema.isInt()), + "prompt_tokens": Schema.Number.annotate({ + "description": "Number of prompt tokens used over the course of the run step." + }).check(Schema.isInt()), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (prompt + completion)." }) + .check(Schema.isInt()) + }).annotate({ + "description": + "Usage statistics related to the run step. This value will be `null` while the run step's status is `in_progress`." + }), + Schema.Null +]) +export type RunStepDeltaStepDetailsToolCallsCodeObject = { + readonly "index": number + readonly "id"?: string + readonly "type": "code_interpreter" + readonly "code_interpreter"?: { + readonly "input"?: string + readonly "outputs"?: ReadonlyArray< + { readonly "index": number; readonly "type": "logs"; readonly "logs"?: string } | { + readonly "index": number + readonly "type": "image" + readonly "image"?: { readonly "file_id"?: string } + } + > + } +} +export const RunStepDeltaStepDetailsToolCallsCodeObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the tool call in the tool calls array." }).check( + Schema.isInt() + ), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the tool call." })), + "type": Schema.Literal("code_interpreter").annotate({ + "description": "The type of tool call. This is always going to be `code_interpreter` for this type of tool call." + }), + "code_interpreter": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey( + Schema.String.annotate({ "description": "The input to the Code Interpreter tool call." }) + ), + "outputs": Schema.optionalKey( + Schema.Array(Schema.Union([ + Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the output in the outputs array." }).check( + Schema.isInt() + ), + "type": Schema.Literal("logs").annotate({ "description": "Always `logs`." }), + "logs": Schema.optionalKey( + Schema.String.annotate({ "description": "The text output from the Code Interpreter tool call." }) + ) + }).annotate({ + "title": "Code interpreter log output", + "description": "Text output from the Code Interpreter tool call as part of a run step." + }), + Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the output in the outputs array." }).check( + Schema.isInt() + ), + "type": Schema.Literal("image").annotate({ "description": "Always `image`." }), + "image": Schema.optionalKey( + Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The [file](/docs/api-reference/files) ID of the image." }) + ) + }) + ) + }).annotate({ "title": "Code interpreter image output" }) + ], { mode: "oneOf" })).annotate({ + "description": + "The outputs from the Code Interpreter tool call. Code Interpreter can output one or more items, including text (`logs`) or images (`image`). Each of these are represented by a different object type." + }) + ) + }).annotate({ "description": "The Code Interpreter tool call definition." }) + ) +}).annotate({ + "title": "Code interpreter tool call", + "description": "Details of the Code Interpreter tool call the run step was involved in." +}) +export type RunStepDeltaStepDetailsToolCallsFileSearchObject = { + readonly "index": number + readonly "id"?: string + readonly "type": "file_search" + readonly "file_search": {} +} +export const RunStepDeltaStepDetailsToolCallsFileSearchObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the tool call in the tool calls array." }).check( + Schema.isInt() + ), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the tool call object." })), + "type": Schema.Literal("file_search").annotate({ + "description": "The type of tool call. This is always going to be `file_search` for this type of tool call." + }), + "file_search": Schema.Struct({}).annotate({ "description": "For now, this is always going to be an empty object." }) +}).annotate({ "title": "File search tool call" }) +export type RunStepDeltaStepDetailsToolCallsFunctionObject = { + readonly "index": number + readonly "id"?: string + readonly "type": "function" + readonly "function"?: { readonly "name"?: string; readonly "arguments"?: string; readonly "output"?: string | null } +} +export const RunStepDeltaStepDetailsToolCallsFunctionObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the tool call in the tool calls array." }).check( + Schema.isInt() + ), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the tool call object." })), + "type": Schema.Literal("function").annotate({ + "description": "The type of tool call. This is always going to be `function` for this type of tool call." + }), + "function": Schema.optionalKey( + Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function." })), + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The arguments passed to the function." }) + ), + "output": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The output of the function. This will be `null` if the outputs have not been [submitted](/docs/api-reference/runs/submitToolOutputs) yet." + }), + Schema.Null + ]) + ) + }).annotate({ "description": "The definition of the function that was called." }) + ) +}).annotate({ "title": "Function tool call" }) +export type RunStepDetailsToolCallsCodeObject = { + readonly "id": string + readonly "type": "code_interpreter" + readonly "code_interpreter": { + readonly "input": string + readonly "outputs": ReadonlyArray< + { readonly "type": "logs"; readonly "logs": string } | { + readonly "type": "image" + readonly "image": { readonly "file_id": string } + } + > + } +} +export const RunStepDetailsToolCallsCodeObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the tool call." }), + "type": Schema.Literal("code_interpreter").annotate({ + "description": "The type of tool call. This is always going to be `code_interpreter` for this type of tool call." + }), + "code_interpreter": Schema.Struct({ + "input": Schema.String.annotate({ "description": "The input to the Code Interpreter tool call." }), + "outputs": Schema.Array( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("logs").annotate({ "description": "Always `logs`." }), + "logs": Schema.String.annotate({ "description": "The text output from the Code Interpreter tool call." }) + }).annotate({ + "title": "Code Interpreter log output", + "description": "Text output from the Code Interpreter tool call as part of a run step." + }), + Schema.Struct({ + "type": Schema.Literal("image").annotate({ "description": "Always `image`." }), + "image": Schema.Struct({ + "file_id": Schema.String.annotate({ + "description": "The [file](/docs/api-reference/files) ID of the image." + }) + }) + }).annotate({ "title": "Code Interpreter image output" }) + ], { mode: "oneOf" }) + ).annotate({ + "description": + "The outputs from the Code Interpreter tool call. Code Interpreter can output one or more items, including text (`logs`) or images (`image`). Each of these are represented by a different object type." + }) + }).annotate({ "description": "The Code Interpreter tool call definition." }) +}).annotate({ + "title": "Code Interpreter tool call", + "description": "Details of the Code Interpreter tool call the run step was involved in." +}) +export type RunStepDetailsToolCallsFileSearchResultObject = { + readonly "file_id": string + readonly "file_name": string + readonly "score": number + readonly "content"?: ReadonlyArray<{ readonly "type"?: "text"; readonly "text"?: string }> +} +export const RunStepDetailsToolCallsFileSearchResultObject = Schema.Struct({ + "file_id": Schema.String.annotate({ "description": "The ID of the file that result was found in." }), + "file_name": Schema.String.annotate({ "description": "The name of the file that result was found in." }), + "score": Schema.Number.annotate({ + "description": "The score of the result. All values must be a floating point number between 0 and 1." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + "content": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "type": Schema.optionalKey(Schema.Literal("text").annotate({ "description": "The type of the content." })), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content of the file." })) + }) + ).annotate({ + "description": + "The content of the result that was found. The content is only included if requested via the include query parameter." + }) + ) +}).annotate({ "title": "File search tool call result", "description": "A result instance of the file search." }) +export type RunStepDetailsToolCallsFunctionObject = { + readonly "id": string + readonly "type": "function" + readonly "function": { readonly "name": string; readonly "arguments": string; readonly "output": string | null } +} +export const RunStepDetailsToolCallsFunctionObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the tool call object." }), + "type": Schema.Literal("function").annotate({ + "description": "The type of tool call. This is always going to be `function` for this type of tool call." + }), + "function": Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the function." }), + "arguments": Schema.String.annotate({ "description": "The arguments passed to the function." }), + "output": Schema.Union([ + Schema.String.annotate({ + "description": + "The output of the function. This will be `null` if the outputs have not been [submitted](/docs/api-reference/runs/submitToolOutputs) yet." + }), + Schema.Null + ]) + }).annotate({ "description": "The definition of the function that was called." }) +}).annotate({ "title": "Function tool call" }) +export type RunToolCallObject = { + readonly "id": string + readonly "type": "function" + readonly "function": { readonly "name": string; readonly "arguments": string } +} +export const RunToolCallObject = Schema.Struct({ + "id": Schema.String.annotate({ + "description": + "The ID of the tool call. This ID must be referenced when you submit the tool outputs in using the [Submit tool outputs to run](/docs/api-reference/runs/submitToolOutputs) endpoint." + }), + "type": Schema.Literal("function").annotate({ + "description": "The type of tool call the output is required for. For now, this is always `function`." + }), + "function": Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the function." }), + "arguments": Schema.String.annotate({ + "description": "The arguments that the model expects you to pass to the function." + }) + }).annotate({ "description": "The function definition." }) +}).annotate({ "description": "Tool call objects" }) +export type ServiceTier = "auto" | "default" | "flex" | "scale" | "priority" | null +export const ServiceTier = Schema.Union([ + Schema.Literals(["auto", "default", "flex", "scale", "priority"]).annotate({ + "description": + "Specifies the processing type used for serving the request.\n - If set to 'auto', then the request will be processed with the service tier configured in the Project settings. Unless otherwise configured, the Project will use 'default'.\n - If set to 'default', then the request will be processed with the standard pricing and performance for the selected model.\n - If set to '[flex](/docs/guides/flex-processing)' or '[priority](https://openai.com/api-priority-processing/)', then the request will be processed with the corresponding service tier.\n - When not set, the default behavior is 'auto'.\n\n When the `service_tier` parameter is set, the response body will include the `service_tier` value based on the processing mode actually used to serve the request. This response value may be different from the value set in the parameter.\n" + }), + Schema.Null +]) +export type SpeechAudioDeltaEvent = { readonly "type": "speech.audio.delta"; readonly "audio": string } +export const SpeechAudioDeltaEvent = Schema.Struct({ + "type": Schema.Literal("speech.audio.delta").annotate({ + "description": "The type of the event. Always `speech.audio.delta`.\n" + }), + "audio": Schema.String.annotate({ "description": "A chunk of Base64-encoded audio data.\n" }) +}).annotate({ "description": "Emitted for each chunk of audio data generated during speech synthesis." }) +export type SpeechAudioDoneEvent = { + readonly "type": "speech.audio.done" + readonly "usage": { + readonly "input_tokens": number + readonly "output_tokens": number + readonly "total_tokens": number + } +} +export const SpeechAudioDoneEvent = Schema.Struct({ + "type": Schema.Literal("speech.audio.done").annotate({ + "description": "The type of the event. Always `speech.audio.done`.\n" + }), + "usage": Schema.Struct({ + "input_tokens": Schema.Number.annotate({ "description": "Number of input tokens in the prompt." }).check( + Schema.isInt() + ), + "output_tokens": Schema.Number.annotate({ "description": "Number of output tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (input + output)." }).check( + Schema.isInt() + ) + }).annotate({ "description": "Token usage statistics for the request.\n" }) +}).annotate({ "description": "Emitted when the speech synthesis is complete and all audio has been streamed." }) +export type StaticChunkingStrategy = { + readonly "max_chunk_size_tokens": number + readonly "chunk_overlap_tokens": number +} +export const StaticChunkingStrategy = Schema.Struct({ + "max_chunk_size_tokens": Schema.Number.annotate({ + "description": + "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(100)).check(Schema.isLessThanOrEqualTo(4096)), + "chunk_overlap_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + }).check(Schema.isInt()) +}) +export type StopConfiguration = string | null | ReadonlyArray | null +export const StopConfiguration = Schema.Union([ + Schema.Union([ + Schema.Union([Schema.String, Schema.Null]), + Schema.Array(Schema.String).check(Schema.isMinLength(1)).check(Schema.isMaxLength(4)) + ], { mode: "oneOf" }).annotate({ + "description": + "Not supported with latest reasoning models `o3` and `o4-mini`.\n\nUp to 4 sequences where the API will stop generating further tokens. The\nreturned text will not contain the stop sequence.\n" + }), + Schema.Null +]) +export type SubmitToolOutputsRunRequest = { + readonly "tool_outputs": ReadonlyArray<{ readonly "tool_call_id"?: string; readonly "output"?: string }> + readonly "stream"?: boolean | null +} +export const SubmitToolOutputsRunRequest = Schema.Struct({ + "tool_outputs": Schema.Array(Schema.Struct({ + "tool_call_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The ID of the tool call in the `required_action` object within the run object the output is being submitted for." + }) + ), + "output": Schema.optionalKey( + Schema.String.annotate({ "description": "The output of the tool call to be submitted to continue the run." }) + ) + })).annotate({ "description": "A list of tools for which the outputs are being submitted." }), + "stream": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message.\n" + }), + Schema.Null + ]) + ) +}) +export type ToggleCertificatesRequest = { readonly "certificate_ids": ReadonlyArray } +export const ToggleCertificatesRequest = Schema.Struct({ + "certificate_ids": Schema.Array(Schema.String).check(Schema.isMinLength(1)).check(Schema.isMaxLength(10)) +}) +export type ToolChoiceAllowed = { + readonly "type": "allowed_tools" + readonly "mode": "auto" | "required" + readonly "tools": ReadonlyArray<{}> +} +export const ToolChoiceAllowed = Schema.Struct({ + "type": Schema.Literal("allowed_tools").annotate({ + "description": "Allowed tool configuration type. Always `allowed_tools`." + }), + "mode": Schema.Literals(["auto", "required"]).annotate({ + "description": + "Constrains the tools available to the model to a pre-defined set.\n\n`auto` allows the model to pick from among the allowed tools and generate a\nmessage.\n\n`required` requires the model to call one or more of the allowed tools.\n" + }), + "tools": Schema.Array( + Schema.Struct({}).annotate({ "description": "A tool definition that the model should be allowed to call.\n" }) + ).annotate({ + "description": + "A list of tool definitions that the model should be allowed to call.\n\nFor the Responses API, the list of tool definitions might look like:\n```json\n[\n { \"type\": \"function\", \"name\": \"get_weather\" },\n { \"type\": \"mcp\", \"server_label\": \"deepwiki\" },\n { \"type\": \"image_generation\" }\n]\n```\n" + }) +}).annotate({ + "title": "Allowed tools", + "description": "Constrains the tools available to the model to a pre-defined set.\n" +}) +export type ToolChoiceCustom = { readonly "type": "custom"; readonly "name": string } +export const ToolChoiceCustom = Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "For custom tool calling, the type is always `custom`." }), + "name": Schema.String.annotate({ "description": "The name of the custom tool to call." }) +}).annotate({ + "title": "Custom tool", + "description": "Use this option to force the model to call a specific custom tool.\n" +}) +export type ToolChoiceFunction = { readonly "type": "function"; readonly "name": string } +export const ToolChoiceFunction = Schema.Struct({ + "type": Schema.Literal("function").annotate({ + "description": "For function calling, the type is always `function`." + }), + "name": Schema.String.annotate({ "description": "The name of the function to call." }) +}).annotate({ + "title": "Function tool", + "description": "Use this option to force the model to call a specific function.\n" +}) +export type ToolChoiceMCP = { readonly "type": "mcp"; readonly "server_label": string; readonly "name"?: string | null } +export const ToolChoiceMCP = Schema.Struct({ + "type": Schema.Literal("mcp").annotate({ "description": "For MCP tools, the type is always `mcp`." }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server to use.\n" }), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The name of the tool to call on the server.\n" }), + Schema.Null + ]) + ) +}).annotate({ + "title": "MCP tool", + "description": "Use this option to force the model to call a specific tool on a remote MCP server.\n" +}) +export type ToolChoiceOptions = "none" | "auto" | "required" +export const ToolChoiceOptions = Schema.Literals(["none", "auto", "required"]).annotate({ + "title": "Tool choice mode", + "description": + "Controls which (if any) tool is called by the model.\n\n`none` means the model will not call any tool and instead generates a message.\n\n`auto` means the model can pick between generating a message or calling one or\nmore tools.\n\n`required` means the model must call one or more tools.\n" +}) +export type ToolChoiceTypes = { + readonly "type": + | "file_search" + | "web_search_preview" + | "computer" + | "computer_use_preview" + | "computer_use" + | "web_search_preview_2025_03_11" + | "image_generation" + | "code_interpreter" +} +export const ToolChoiceTypes = Schema.Struct({ + "type": Schema.Literals([ + "file_search", + "web_search_preview", + "computer", + "computer_use_preview", + "computer_use", + "web_search_preview_2025_03_11", + "image_generation", + "code_interpreter" + ]).annotate({ + "description": + "The type of hosted tool the model should to use. Learn more about\n[built-in tools](/docs/guides/tools).\n\nAllowed values are:\n- `file_search`\n- `web_search_preview`\n- `computer`\n- `computer_use_preview`\n- `computer_use`\n- `code_interpreter`\n- `image_generation`\n" + }) +}).annotate({ + "title": "Hosted tool", + "description": + "Indicates that the model should use a built-in tool to generate a response.\n[Learn more about built-in tools](/docs/guides/tools).\n" +}) +export type TranscriptTextDeltaEvent = { + readonly "type": "transcript.text.delta" + readonly "delta": string + readonly "logprobs"?: ReadonlyArray< + { readonly "token"?: string; readonly "logprob"?: number; readonly "bytes"?: ReadonlyArray } + > + readonly "segment_id"?: string +} +export const TranscriptTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("transcript.text.delta").annotate({ + "description": "The type of the event. Always `transcript.text.delta`.\n" + }), + "delta": Schema.String.annotate({ "description": "The text delta that was additionally transcribed.\n" }), + "logprobs": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "token": Schema.optionalKey( + Schema.String.annotate({ "description": "The token that was used to generate the log probability.\n" }) + ), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "description": "The log probability of the token.\n" }).check(Schema.isFinite()) + ), + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": "The bytes that were used to generate the log probability.\n" + }) + ) + })).annotate({ + "description": + "The log probabilities of the delta. Only included if you [create a transcription](/docs/api-reference/audio/create-transcription) with the `include[]` parameter set to `logprobs`.\n" + }) + ), + "segment_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Identifier of the diarized segment that this delta belongs to. Only present when using `gpt-4o-transcribe-diarize`.\n" + }) + ) +}).annotate({ + "description": + "Emitted when there is an additional text delta. This is also the first event emitted when the transcription starts. Only emitted when you [create a transcription](/docs/api-reference/audio/create-transcription) with the `Stream` parameter set to `true`." +}) +export type TranscriptTextSegmentEvent = { + readonly "type": "transcript.text.segment" + readonly "id": string + readonly "start": number + readonly "end": number + readonly "text": string + readonly "speaker": string +} +export const TranscriptTextSegmentEvent = Schema.Struct({ + "type": Schema.Literal("transcript.text.segment").annotate({ + "description": "The type of the event. Always `transcript.text.segment`." + }), + "id": Schema.String.annotate({ "description": "Unique identifier for the segment." }), + "start": Schema.Number.annotate({ "description": "Start timestamp of the segment in seconds.", "format": "double" }) + .check(Schema.isFinite()), + "end": Schema.Number.annotate({ "description": "End timestamp of the segment in seconds.", "format": "double" }) + .check(Schema.isFinite()), + "text": Schema.String.annotate({ "description": "Transcript text for this segment." }), + "speaker": Schema.String.annotate({ "description": "Speaker label for this segment." }) +}).annotate({ + "description": + "Emitted when a diarized transcription returns a completed segment with speaker information. Only emitted when you [create a transcription](/docs/api-reference/audio/create-transcription) with `stream` set to `true` and `response_format` set to `diarized_json`.\n" +}) +export type TranscriptTextUsageDuration = { readonly "type": "duration"; readonly "seconds": number } +export const TranscriptTextUsageDuration = Schema.Struct({ + "type": Schema.Literal("duration").annotate({ + "description": "The type of the usage object. Always `duration` for this variant." + }), + "seconds": Schema.Number.annotate({ "description": "Duration of the input audio in seconds.", "format": "double" }) + .check(Schema.isFinite()) +}).annotate({ "title": "Duration Usage", "description": "Usage statistics for models billed by audio input duration." }) +export type TranscriptTextUsageTokens = { + readonly "type": "tokens" + readonly "input_tokens": number + readonly "input_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + readonly "output_tokens": number + readonly "total_tokens": number +} +export const TranscriptTextUsageTokens = Schema.Struct({ + "type": Schema.Literal("tokens").annotate({ + "description": "The type of the usage object. Always `tokens` for this variant." + }), + "input_tokens": Schema.Number.annotate({ "description": "Number of input tokens billed for this request." }).check( + Schema.isInt() + ), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of text tokens billed for this request." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio tokens billed for this request." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the input tokens billed for this request." }) + ), + "output_tokens": Schema.Number.annotate({ "description": "Number of output tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (input + output)." }).check( + Schema.isInt() + ) +}).annotate({ "title": "Token Usage", "description": "Usage statistics for models billed by token usage." }) +export type TranscriptionDiarizedSegment = { + readonly "type": "transcript.text.segment" + readonly "id": string + readonly "start": number + readonly "end": number + readonly "text": string + readonly "speaker": string +} +export const TranscriptionDiarizedSegment = Schema.Struct({ + "type": Schema.Literal("transcript.text.segment").annotate({ + "description": "The type of the segment. Always `transcript.text.segment`.\n" + }), + "id": Schema.String.annotate({ "description": "Unique identifier for the segment." }), + "start": Schema.Number.annotate({ "description": "Start timestamp of the segment in seconds.", "format": "double" }) + .check(Schema.isFinite()), + "end": Schema.Number.annotate({ "description": "End timestamp of the segment in seconds.", "format": "double" }) + .check(Schema.isFinite()), + "text": Schema.String.annotate({ "description": "Transcript text for this segment." }), + "speaker": Schema.String.annotate({ + "description": + "Speaker label for this segment. When known speakers are provided, the label matches `known_speaker_names[]`. Otherwise speakers are labeled sequentially using capital letters (`A`, `B`, ...).\n" + }) +}).annotate({ "description": "A segment of diarized transcript text with speaker metadata." }) +export type TranscriptionInclude = "logprobs" +export const TranscriptionInclude = Schema.Literal("logprobs") +export type TranscriptionSegment = { + readonly "id": number + readonly "seek": number + readonly "start": number + readonly "end": number + readonly "text": string + readonly "tokens": ReadonlyArray + readonly "temperature": number + readonly "avg_logprob": number + readonly "compression_ratio": number + readonly "no_speech_prob": number +} +export const TranscriptionSegment = Schema.Struct({ + "id": Schema.Number.annotate({ "description": "Unique identifier of the segment." }).check(Schema.isInt()), + "seek": Schema.Number.annotate({ "description": "Seek offset of the segment." }).check(Schema.isInt()), + "start": Schema.Number.annotate({ "description": "Start time of the segment in seconds.", "format": "double" }).check( + Schema.isFinite() + ), + "end": Schema.Number.annotate({ "description": "End time of the segment in seconds.", "format": "double" }).check( + Schema.isFinite() + ), + "text": Schema.String.annotate({ "description": "Text content of the segment." }), + "tokens": Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": "Array of token IDs for the text content." + }), + "temperature": Schema.Number.annotate({ + "description": "Temperature parameter used for generating the segment.", + "format": "float" + }).check(Schema.isFinite()), + "avg_logprob": Schema.Number.annotate({ + "description": "Average logprob of the segment. If the value is lower than -1, consider the logprobs failed.", + "format": "float" + }).check(Schema.isFinite()), + "compression_ratio": Schema.Number.annotate({ + "description": + "Compression ratio of the segment. If the value is greater than 2.4, consider the compression failed.", + "format": "float" + }).check(Schema.isFinite()), + "no_speech_prob": Schema.Number.annotate({ + "description": + "Probability of no speech in the segment. If the value is higher than 1.0 and the `avg_logprob` is below -1, consider this segment silent.", + "format": "float" + }).check(Schema.isFinite()) +}) +export type TranscriptionWord = { readonly "word": string; readonly "start": number; readonly "end": number } +export const TranscriptionWord = Schema.Struct({ + "word": Schema.String.annotate({ "description": "The text content of the word." }), + "start": Schema.Number.annotate({ "description": "Start time of the word in seconds.", "format": "double" }).check( + Schema.isFinite() + ), + "end": Schema.Number.annotate({ "description": "End time of the word in seconds.", "format": "double" }).check( + Schema.isFinite() + ) +}) +export type UpdateGroupBody = { readonly "name": string } +export const UpdateGroupBody = Schema.Struct({ + "name": Schema.String.annotate({ "description": "New display name for the group." }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(255)) +}).annotate({ "description": "Request payload for updating the details of an existing group." }) +export type UpdateVoiceConsentRequest = { readonly "name": string } +export const UpdateVoiceConsentRequest = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The updated label for this consent recording." }) +}) +export type Upload = { + readonly "id": string + readonly "created_at": number + readonly "filename": string + readonly "bytes": number + readonly "purpose": string + readonly "status": "pending" | "completed" | "cancelled" | "expired" + readonly "expires_at": number + readonly "object"?: "upload" + readonly "file"?: { + readonly "id": string + readonly "bytes": number + readonly "created_at": number + readonly "expires_at"?: number | null + readonly "filename": string + readonly "object": "file" + readonly "purpose": + | "assistants" + | "assistants_output" + | "batch" + | "batch_output" + | "fine-tune" + | "fine-tune-results" + | "vision" + | "user_data" + readonly "status": "uploaded" | "processed" | "error" + readonly "status_details"?: string | null + readonly [x: string]: unknown + } +} +export const Upload = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The Upload unique identifier, which can be referenced in API endpoints." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the Upload was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "filename": Schema.String.annotate({ "description": "The name of the file to be uploaded." }), + "bytes": Schema.Number.annotate({ "description": "The intended number of bytes to be uploaded." }).check( + Schema.isInt() + ), + "purpose": Schema.String.annotate({ + "description": + "The intended purpose of the file. [Please refer here](/docs/api-reference/files/object#files/object-purpose) for acceptable values." + }), + "status": Schema.Literals(["pending", "completed", "cancelled", "expired"]).annotate({ + "description": "The status of the Upload." + }), + "expires_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the Upload will expire.", + "format": "unixtime" + }).check(Schema.isInt()), + "object": Schema.optionalKey( + Schema.Literal("upload").annotate({ "description": "The object type, which is always \"upload\"." }) + ), + "file": Schema.optionalKey(Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The file identifier, which can be referenced in the API endpoints." + }), + "bytes": Schema.Number.annotate({ "description": "The size of the file, in bytes." }).check(Schema.isInt()), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the file was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "expires_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the file will expire.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "filename": Schema.String.annotate({ "description": "The name of the file." }), + "object": Schema.Literal("file").annotate({ "description": "The object type, which is always `file`." }), + "purpose": Schema.Literals([ + "assistants", + "assistants_output", + "batch", + "batch_output", + "fine-tune", + "fine-tune-results", + "vision", + "user_data" + ]).annotate({ + "description": + "The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results`, `vision`, and `user_data`." + }), + "status": Schema.Literals(["uploaded", "processed", "error"]).annotate({ + "description": + "Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`." + }), + "status_details": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`." + }) + ) + }), + [Schema.Record(Schema.String, Schema.Json)] + ).annotate({ + "description": "The `File` object represents a document that has been uploaded to OpenAI.", + "title": "OpenAIFile" + }) + ])) +}).annotate({ "title": "Upload", "description": "The Upload object can accept byte chunks in the form of Parts.\n" }) +export type UploadCertificateRequest = { readonly "name"?: string; readonly "certificate": string } +export const UploadCertificateRequest = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "An optional name for the certificate" })), + "certificate": Schema.String.annotate({ "description": "The certificate content in PEM format" }) +}) +export type UploadPart = { + readonly "id": string + readonly "created_at": number + readonly "upload_id": string + readonly "object": "upload.part" +} +export const UploadPart = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The upload Part unique identifier, which can be referenced in API endpoints." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the Part was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "upload_id": Schema.String.annotate({ "description": "The ID of the Upload object that this Part was added to." }), + "object": Schema.Literal("upload.part").annotate({ "description": "The object type, which is always `upload.part`." }) +}).annotate({ + "title": "UploadPart", + "description": "The upload Part represents a chunk of bytes we can add to an Upload object.\n" +}) +export type UsageAudioSpeechesResult = { + readonly "object": "organization.usage.audio_speeches.result" + readonly "characters": number + readonly "num_model_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null +} +export const UsageAudioSpeechesResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.audio_speeches.result"), + "characters": Schema.Number.annotate({ "description": "The number of characters processed." }).check(Schema.isInt()), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated audio speeches usage details of the specific time bucket." }) +export type UsageAudioTranscriptionsResult = { + readonly "object": "organization.usage.audio_transcriptions.result" + readonly "seconds": number + readonly "num_model_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null +} +export const UsageAudioTranscriptionsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.audio_transcriptions.result"), + "seconds": Schema.Number.annotate({ "description": "The number of seconds processed.", "format": "int64" }).check( + Schema.isInt() + ), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated audio transcriptions usage details of the specific time bucket." }) +export type UsageCodeInterpreterSessionsResult = { + readonly "object": "organization.usage.code_interpreter_sessions.result" + readonly "num_sessions": number + readonly "project_id"?: string | null +} +export const UsageCodeInterpreterSessionsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.code_interpreter_sessions.result"), + "num_sessions": Schema.Number.annotate({ "description": "The number of code interpreter sessions." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated code interpreter sessions usage details of the specific time bucket." }) +export type UsageCompletionsResult = { + readonly "object": "organization.usage.completions.result" + readonly "input_tokens": number + readonly "input_cached_tokens"?: number + readonly "output_tokens": number + readonly "input_audio_tokens"?: number + readonly "output_audio_tokens"?: number + readonly "num_model_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null + readonly "batch"?: boolean | null + readonly "service_tier"?: string | null +} +export const UsageCompletionsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.completions.result"), + "input_tokens": Schema.Number.annotate({ + "description": + "The aggregated number of text input tokens used, including cached tokens. For customers subscribe to scale tier, this includes scale tier tokens." + }).check(Schema.isInt()), + "input_cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The aggregated number of text input tokens that has been cached from previous requests. For customers subscribe to scale tier, this includes scale tier tokens." + }).check(Schema.isInt()) + ), + "output_tokens": Schema.Number.annotate({ + "description": + "The aggregated number of text output tokens used. For customers subscribe to scale tier, this includes scale tier tokens." + }).check(Schema.isInt()), + "input_audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The aggregated number of audio input tokens used, including cached tokens." + }).check(Schema.isInt()) + ), + "output_audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The aggregated number of audio output tokens used." }).check( + Schema.isInt() + ) + ), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ), + "batch": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "When `group_by=batch`, this field tells whether the grouped usage result is batch or not." + }), + Schema.Null + ]) + ), + "service_tier": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=service_tier`, this field provides the service tier of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated completions usage details of the specific time bucket." }) +export type UsageEmbeddingsResult = { + readonly "object": "organization.usage.embeddings.result" + readonly "input_tokens": number + readonly "num_model_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null +} +export const UsageEmbeddingsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.embeddings.result"), + "input_tokens": Schema.Number.annotate({ "description": "The aggregated number of input tokens used." }).check( + Schema.isInt() + ), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated embeddings usage details of the specific time bucket." }) +export type UsageFileSearchCallsResult = { + readonly "object": "organization.usage.file_searches.result" + readonly "num_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "vector_store_id"?: string | null +} +export const UsageFileSearchCallsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.file_searches.result"), + "num_requests": Schema.Number.annotate({ "description": "The count of file search calls." }).check(Schema.isInt()), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "vector_store_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "When `group_by=vector_store_id`, this field provides the vector store ID of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated file search calls usage details of the specific time bucket." }) +export type UsageImagesResult = { + readonly "object": "organization.usage.images.result" + readonly "images": number + readonly "num_model_requests": number + readonly "source"?: string | null + readonly "size"?: string | null + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null +} +export const UsageImagesResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.images.result"), + "images": Schema.Number.annotate({ "description": "The number of images processed." }).check(Schema.isInt()), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "source": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "When `group_by=source`, this field provides the source of the grouped usage result, possible values are `image.generation`, `image.edit`, `image.variation`." + }), + Schema.Null + ]) + ), + "size": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=size`, this field provides the image size of the grouped usage result." + }), + Schema.Null + ]) + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated images usage details of the specific time bucket." }) +export type UsageModerationsResult = { + readonly "object": "organization.usage.moderations.result" + readonly "input_tokens": number + readonly "num_model_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null +} +export const UsageModerationsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.moderations.result"), + "input_tokens": Schema.Number.annotate({ "description": "The aggregated number of input tokens used." }).check( + Schema.isInt() + ), + "num_model_requests": Schema.Number.annotate({ "description": "The count of requests made to the model." }).check( + Schema.isInt() + ), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated moderations usage details of the specific time bucket." }) +export type UsageVectorStoresResult = { + readonly "object": "organization.usage.vector_stores.result" + readonly "usage_bytes": number + readonly "project_id"?: string | null +} +export const UsageVectorStoresResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.vector_stores.result"), + "usage_bytes": Schema.Number.annotate({ "description": "The vector stores usage in bytes." }).check(Schema.isInt()), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated vector stores usage details of the specific time bucket." }) +export type UsageWebSearchCallsResult = { + readonly "object": "organization.usage.web_searches.result" + readonly "num_model_requests": number + readonly "num_requests": number + readonly "project_id"?: string | null + readonly "user_id"?: string | null + readonly "api_key_id"?: string | null + readonly "model"?: string | null + readonly "context_level"?: string | null +} +export const UsageWebSearchCallsResult = Schema.Struct({ + "object": Schema.Literal("organization.usage.web_searches.result"), + "num_model_requests": Schema.Number.annotate({ "description": "The count of model requests." }).check(Schema.isInt()), + "num_requests": Schema.Number.annotate({ "description": "The count of web search calls." }).check(Schema.isInt()), + "project_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=project_id`, this field provides the project ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=user_id`, this field provides the user ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "api_key_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=api_key_id`, this field provides the API key ID of the grouped usage result." + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "When `group_by=model`, this field provides the model name of the grouped usage result." + }), + Schema.Null + ]) + ), + "context_level": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "When `group_by=context_level`, this field provides the search context size of the grouped usage result." + }), + Schema.Null + ]) + ) +}).annotate({ "description": "The aggregated web search calls usage details of the specific time bucket." }) +export type User = { + readonly "object": "organization.user" + readonly "id": string + readonly "name"?: string | null + readonly "email"?: string | null + readonly "role"?: string | null + readonly "added_at": number + readonly "is_default"?: boolean + readonly "created"?: number + readonly "user"?: { + readonly "object": "user" + readonly "id": string + readonly "email"?: string | null + readonly "name"?: string | null + readonly "picture"?: string | null + readonly "enabled"?: boolean | null + readonly "banned"?: boolean | null + readonly "banned_at"?: number | null + } + readonly "is_service_account"?: boolean + readonly "is_scale_tier_authorized_purchaser"?: boolean | null + readonly "is_scim_managed"?: boolean + readonly "api_key_last_used_at"?: number | null + readonly "technical_level"?: string | null + readonly "developer_persona"?: string | null + readonly "projects"?: { + readonly "object": "list" + readonly "data": ReadonlyArray< + { readonly "id"?: string | null; readonly "name"?: string | null; readonly "role"?: string | null } + > + } | null +} +export const User = Schema.Struct({ + "object": Schema.Literal("organization.user").annotate({ + "description": "The object type, which is always `organization.user`" + }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the user" }) + ), + "email": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The email address of the user" }) + ), + "role": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "`owner` or `reader`" }) + ), + "added_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the user was added.", + "format": "unixtime" + }).check(Schema.isInt()), + "is_default": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this is the organization's default user." }) + ), + "created": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the user was created.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "user": Schema.optionalKey( + Schema.Struct({ + "object": Schema.Literal("user"), + "id": Schema.String, + "email": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "picture": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "enabled": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "banned": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "banned_at": Schema.optionalKey( + Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]) + ) + }).annotate({ "description": "Nested user details." }) + ), + "is_service_account": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether the user is a service account." }) + ), + "is_scale_tier_authorized_purchaser": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Whether the user is an authorized purchaser for Scale Tier." + }) + ), + "is_scim_managed": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether the user is managed through SCIM." }) + ), + "api_key_last_used_at": Schema.optionalKey( + Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]).annotate({ + "description": "The Unix timestamp (in seconds) of the user's last API key usage." + }) + ), + "technical_level": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The technical level metadata for the user." }) + ), + "developer_persona": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The developer persona metadata for the user." + }) + ), + "projects": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array( + Schema.Struct({ + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "role": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }) + ) + }), + Schema.Null + ]).annotate({ "description": "Projects associated with the user, if included." }) + ) +}).annotate({ "description": "Represents an individual `user` within an organization." }) +export type UserDeleteResponse = { + readonly "object": "organization.user.deleted" + readonly "id": string + readonly "deleted": boolean +} +export const UserDeleteResponse = Schema.Struct({ + "object": Schema.Literal("organization.user.deleted"), + "id": Schema.String, + "deleted": Schema.Boolean +}) +export type UserRoleUpdateRequest = { + readonly "role"?: string | null + readonly "role_id"?: string | null + readonly "technical_level"?: string | null + readonly "developer_persona"?: string | null +} +export const UserRoleUpdateRequest = Schema.Struct({ + "role": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "`owner` or `reader`" }) + ), + "role_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Role ID to assign to the user." }) + ), + "technical_level": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Technical level metadata." }) + ), + "developer_persona": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Developer persona metadata." }) + ) +}) +export type VadConfig = { + readonly "type": "server_vad" + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + readonly "threshold"?: number +} +export const VadConfig = Schema.Struct({ + "type": Schema.Literal("server_vad").annotate({ + "description": "Must be set to `server_vad` to enable manual chunking using server side VAD." + }), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Amount of audio to include before the VAD detected speech (in \nmilliseconds).\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds).\nWith shorter values the model will respond more quickly, \nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Sensitivity threshold (0.0 to 1.0) for voice activity detection. A \nhigher threshold will require louder audio to activate the model, and \nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ) +}) +export type VectorStoreExpirationAfter = { readonly "anchor": "last_active_at"; readonly "days": number } +export const VectorStoreExpirationAfter = Schema.Struct({ + "anchor": Schema.Literal("last_active_at").annotate({ + "description": "Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`." + }), + "days": Schema.Number.annotate({ + "description": "The number of days after the anchor time that the vector store will expire." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(365)) +}).annotate({ "title": "Vector store expiration policy", "description": "The expiration policy for a vector store." }) +export type VectorStoreFileAttributes = {} | null +export const VectorStoreFileAttributes = Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard. Keys are strings\nwith a maximum length of 64 characters. Values are strings with a maximum\nlength of 512 characters, booleans, or numbers.\n" + }).check(Schema.isMaxProperties(16)).check(Schema.isPropertyNames(Schema.String.check(Schema.isMaxLength(64)))), + Schema.Null +]) +export type VectorStoreFileBatchObject = { + readonly "id": string + readonly "object": "vector_store.files_batch" + readonly "created_at": number + readonly "vector_store_id": string + readonly "status": "in_progress" | "completed" | "cancelled" | "failed" + readonly "file_counts": { + readonly "in_progress": number + readonly "completed": number + readonly "failed": number + readonly "cancelled": number + readonly "total": number + } +} +export const VectorStoreFileBatchObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("vector_store.files_batch").annotate({ + "description": "The object type, which is always `vector_store.file_batch`." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the vector store files batch was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "vector_store_id": Schema.String.annotate({ + "description": + "The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) is attached to." + }), + "status": Schema.Literals(["in_progress", "completed", "cancelled", "failed"]).annotate({ + "description": + "The status of the vector store files batch, which can be either `in_progress`, `completed`, `cancelled` or `failed`." + }), + "file_counts": Schema.Struct({ + "in_progress": Schema.Number.annotate({ "description": "The number of files that are currently being processed." }) + .check(Schema.isInt()), + "completed": Schema.Number.annotate({ "description": "The number of files that have been processed." }).check( + Schema.isInt() + ), + "failed": Schema.Number.annotate({ "description": "The number of files that have failed to process." }).check( + Schema.isInt() + ), + "cancelled": Schema.Number.annotate({ "description": "The number of files that where cancelled." }).check( + Schema.isInt() + ), + "total": Schema.Number.annotate({ "description": "The total number of files." }).check(Schema.isInt()) + }) +}).annotate({ "title": "Vector store file batch", "description": "A batch of files attached to a vector store." }) +export type VectorStoreFileContentResponse = { + readonly "object": "vector_store.file_content.page" + readonly "data": ReadonlyArray<{ readonly "type"?: string; readonly "text"?: string }> + readonly "has_more": boolean + readonly "next_page": string | null +} +export const VectorStoreFileContentResponse = Schema.Struct({ + "object": Schema.Literal("vector_store.file_content.page").annotate({ + "description": "The object type, which is always `vector_store.file_content.page`" + }), + "data": Schema.Array( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ "description": "The content type (currently only `\"text\"`)" }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The text content" })) + }) + ).annotate({ "description": "Parsed content of the file." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates if there are more content pages to fetch." }), + "next_page": Schema.Union([ + Schema.String.annotate({ "description": "The token for the next page, if any." }), + Schema.Null + ]) +}).annotate({ "description": "Represents the parsed content of a vector store file." }) +export type VectorStoreSearchResultContentObject = { readonly "type": "text"; readonly "text": string } +export const VectorStoreSearchResultContentObject = Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "The type of content." }), + "text": Schema.String.annotate({ "description": "The text content returned from search." }) +}) +export type Verbosity = "low" | "medium" | "high" | null +export const Verbosity = Schema.Union([ + Schema.Literals(["low", "medium", "high"]).annotate({ + "description": + "Constrains the verbosity of the model's response. Lower values will result in\nmore concise responses, while higher values will result in more verbose responses.\nCurrently supported values are `low`, `medium`, and `high`.\n" + }), + Schema.Null +]) +export type VoiceConsentDeletedResource = { + readonly "id": string + readonly "object": "audio.voice_consent" + readonly "deleted": boolean +} +export const VoiceConsentDeletedResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The consent recording identifier." }), + "object": Schema.Literal("audio.voice_consent"), + "deleted": Schema.Boolean +}) +export type VoiceConsentResource = { + readonly "object": "audio.voice_consent" + readonly "id": string + readonly "name": string + readonly "language": string + readonly "created_at": number +} +export const VoiceConsentResource = Schema.Struct({ + "object": Schema.Literal("audio.voice_consent").annotate({ + "description": "The object type, which is always `audio.voice_consent`." + }), + "id": Schema.String.annotate({ "description": "The consent recording identifier." }), + "name": Schema.String.annotate({ "description": "The label provided when the consent recording was uploaded." }), + "language": Schema.String.annotate({ + "description": "The BCP 47 language tag for the consent phrase (for example, `en-US`)." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the consent recording was created.", + "format": "unixtime" + }).check(Schema.isInt()) +}).annotate({ + "title": "Voice consent", + "description": "A consent recording used to authorize creation of a custom voice." +}) +export type VoiceIdsShared = + | string + | "alloy" + | "ash" + | "ballad" + | "coral" + | "echo" + | "sage" + | "shimmer" + | "verse" + | "marin" + | "cedar" +export const VoiceIdsShared = Schema.Union([ + Schema.String, + Schema.Literals(["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]) +]) +export type VoiceResource = { + readonly "object": "audio.voice" + readonly "id": string + readonly "name": string + readonly "created_at": number +} +export const VoiceResource = Schema.Struct({ + "object": Schema.Literal("audio.voice").annotate({ + "description": "The object type, which is always `audio.voice`." + }), + "id": Schema.String.annotate({ "description": "The voice identifier, which can be referenced in API endpoints." }), + "name": Schema.String.annotate({ "description": "The name of the voice." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the voice was created.", + "format": "unixtime" + }).check(Schema.isInt()) +}).annotate({ "title": "Voice", "description": "A custom voice that can be used for audio output." }) +export type WebSearchApproximateLocation = { + readonly "type"?: "approximate" + readonly "country"?: string | null + readonly "region"?: string | null + readonly "city"?: string | null + readonly "timezone"?: string | null +} | null +export const WebSearchApproximateLocation = Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("approximate").annotate({ + "description": "The type of location approximation. Always `approximate`." + }) + ), + "country": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The two-letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of the user, e.g. `US`." + }), + Schema.Null + ]) + ), + "region": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Free text input for the region of the user, e.g. `California`." }), + Schema.Null + ]) + ), + "city": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Free text input for the city of the user, e.g. `San Francisco`." }), + Schema.Null + ]) + ), + "timezone": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The [IANA timezone](https://timeapi.io/documentation/iana-timezones) of the user, e.g. `America/Los_Angeles`." + }), + Schema.Null + ]) + ) + }).annotate({ "title": "Web search approximate location", "description": "The approximate location of the user.\n" }), + Schema.Null +]) +export type WebSearchContextSize = "low" | "medium" | "high" +export const WebSearchContextSize = Schema.Literals(["low", "medium", "high"]).annotate({ + "description": + "High level guidance for the amount of context window space to use for the \nsearch. One of `low`, `medium`, or `high`. `medium` is the default.\n" +}) +export type WebSearchLocation = { + readonly "country"?: string + readonly "region"?: string + readonly "city"?: string + readonly "timezone"?: string +} +export const WebSearchLocation = Schema.Struct({ + "country": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The two-letter \n[ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of the user,\ne.g. `US`.\n" + }) + ), + "region": Schema.optionalKey( + Schema.String.annotate({ "description": "Free text input for the region of the user, e.g. `California`.\n" }) + ), + "city": Schema.optionalKey( + Schema.String.annotate({ "description": "Free text input for the city of the user, e.g. `San Francisco`.\n" }) + ), + "timezone": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The [IANA timezone](https://timeapi.io/documentation/iana-timezones) \nof the user, e.g. `America/Los_Angeles`.\n" + }) + ) +}).annotate({ "title": "Web search location", "description": "Approximate location parameters for the search." }) +export type WebSearchToolCall = { + readonly "id": string + readonly "type": "web_search_call" + readonly "status": "in_progress" | "searching" | "completed" | "failed" + readonly "action": + | { + readonly "type": "search" + readonly "query"?: string + readonly "queries"?: ReadonlyArray + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "type": "find_in_page"; readonly "url": string; readonly "pattern": string } +} +export const WebSearchToolCall = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the web search tool call.\n" }), + "type": Schema.Literal("web_search_call").annotate({ + "description": "The type of the web search tool call. Always `web_search_call`.\n" + }), + "status": Schema.Literals(["in_progress", "searching", "completed", "failed"]).annotate({ + "description": "The status of the web search tool call.\n" + }), + "action": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("search").annotate({ "description": "The action type.\n" }), + "query": Schema.optionalKey(Schema.String.annotate({ "description": "[DEPRECATED] The search query.\n" })), + "queries": Schema.optionalKey( + Schema.Array(Schema.String.annotate({ "description": "A search query.\n" })).annotate({ + "title": "Search queries", + "description": "The search queries.\n" + }) + ), + "sources": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), + "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) + }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }) + ).annotate({ "title": "Web search sources", "description": "The sources used in the search.\n" }) + ) + }).annotate({ + "title": "Search action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }), + Schema.Struct({ + "type": Schema.Literal("open_page").annotate({ "description": "The action type. Always `open_page`.\n" }), + "url": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "format": "uri" }), Schema.Null]).annotate({ + "description": "The URL opened by the model.\n" + }) + ) + }).annotate({ + "title": "Open page action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }), + Schema.Struct({ + "type": Schema.Literal("find_in_page").annotate({ "description": "The action type.\n" }), + "url": Schema.String.annotate({ + "description": "The URL of the page searched for the pattern.\n", + "format": "uri" + }), + "pattern": Schema.String.annotate({ "description": "The pattern or text to search for within the page.\n" }) + }).annotate({ + "title": "Find action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }) + ], { mode: "oneOf" }) +}).annotate({ + "title": "Web search tool call", + "description": + "The results of a web search tool call. See the\n[web search guide](/docs/guides/tools-web-search) for more information.\n" +}) +export type SkillReferenceParam = { + readonly "type": "skill_reference" + readonly "skill_id": string + readonly "version"?: string +} +export const SkillReferenceParam = Schema.Struct({ + "type": Schema.Literal("skill_reference").annotate({ + "description": "References a skill created with the /v1/skills endpoint." + }), + "skill_id": Schema.String.annotate({ "description": "The ID of the referenced skill." }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)), + "version": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional skill version. Use a positive integer or 'latest'. Omit for default." + }) + ) +}) +export type InlineSkillParam = { + readonly "type": "inline" + readonly "name": string + readonly "description": string + readonly "source": { readonly "type": "base64"; readonly "media_type": "application/zip"; readonly "data": string } +} +export const InlineSkillParam = Schema.Struct({ + "type": Schema.Literal("inline").annotate({ "description": "Defines an inline skill for this request." }), + "name": Schema.String.annotate({ "description": "The name of the skill." }), + "description": Schema.String.annotate({ "description": "The description of the skill." }), + "source": Schema.Struct({ + "type": Schema.Literal("base64").annotate({ + "description": "The type of the inline skill source. Must be `base64`." + }), + "media_type": Schema.Literal("application/zip").annotate({ + "description": "The media type of the inline skill payload. Must be `application/zip`." + }), + "data": Schema.String.annotate({ "description": "Base64-encoded skill zip bundle." }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(70254592)) + }).annotate({ "description": "Inline skill payload" }) +}) +export type ContainerNetworkPolicyDisabledParam = { readonly "type": "disabled" } +export const ContainerNetworkPolicyDisabledParam = Schema.Struct({ + "type": Schema.Literal("disabled").annotate({ "description": "Disable outbound network access. Always `disabled`." }) +}) +export type ContainerNetworkPolicyDomainSecretParam = { + readonly "domain": string + readonly "name": string + readonly "value": string +} +export const ContainerNetworkPolicyDomainSecretParam = Schema.Struct({ + "domain": Schema.String.annotate({ "description": "The domain associated with the secret." }).check( + Schema.isMinLength(1) + ), + "name": Schema.String.annotate({ "description": "The name of the secret to inject for the domain." }).check( + Schema.isMinLength(1) + ), + "value": Schema.String.annotate({ "description": "The secret value to inject for the domain." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(10485760)) +}) +export type IncludeEnum = + | "file_search_call.results" + | "web_search_call.results" + | "web_search_call.action.sources" + | "message.input_image.image_url" + | "computer_call_output.output.image_url" + | "code_interpreter_call.outputs" + | "reasoning.encrypted_content" + | "message.output_text.logprobs" +export const IncludeEnum = Schema.Literals([ + "file_search_call.results", + "web_search_call.results", + "web_search_call.action.sources", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "code_interpreter_call.outputs", + "reasoning.encrypted_content", + "message.output_text.logprobs" +]).annotate({ + "description": + "Specify additional output data to include in the model response. Currently supported values are:\n- `web_search_call.results`: Include the search results of the web search tool call.\n- `web_search_call.action.sources`: Include the sources of the web search tool call.\n- `code_interpreter_call.outputs`: Includes the outputs of python code execution in code interpreter tool call items.\n- `computer_call_output.output.image_url`: Include image urls from the computer call output.\n- `file_search_call.results`: Include the search results of the file search tool call.\n- `message.input_image.image_url`: Include image urls from the input message.\n- `message.output_text.logprobs`: Include logprobs with assistant messages.\n- `reasoning.encrypted_content`: Includes an encrypted version of reasoning tokens in reasoning item outputs. This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly (like when the `store` parameter is set to `false`, or when an organization is enrolled in the zero data retention program)." +}) +export type InputTextContent = { readonly "type": "input_text"; readonly "text": string } +export const InputTextContent = Schema.Struct({ + "type": Schema.Literal("input_text").annotate({ "description": "The type of the input item. Always `input_text`." }), + "text": Schema.String.annotate({ "description": "The text input to the model." }) +}).annotate({ "title": "Input text", "description": "A text input to the model." }) +export type FileCitationBody = { + readonly "type": "file_citation" + readonly "file_id": string + readonly "index": number + readonly "filename": string +} +export const FileCitationBody = Schema.Struct({ + "type": Schema.Literal("file_citation").annotate({ + "description": "The type of the file citation. Always `file_citation`." + }), + "file_id": Schema.String.annotate({ "description": "The ID of the file." }), + "index": Schema.Number.annotate({ "description": "The index of the file in the list of files." }).check( + Schema.isInt() + ), + "filename": Schema.String.annotate({ "description": "The filename of the file cited." }) +}).annotate({ "title": "File citation", "description": "A citation to a file." }) +export type UrlCitationBody = { + readonly "type": "url_citation" + readonly "url": string + readonly "start_index": number + readonly "end_index": number + readonly "title": string +} +export const UrlCitationBody = Schema.Struct({ + "type": Schema.Literal("url_citation").annotate({ + "description": "The type of the URL citation. Always `url_citation`." + }), + "url": Schema.String.annotate({ "description": "The URL of the web resource.", "format": "uri" }), + "start_index": Schema.Number.annotate({ + "description": "The index of the first character of the URL citation in the message." + }).check(Schema.isInt()), + "end_index": Schema.Number.annotate({ + "description": "The index of the last character of the URL citation in the message." + }).check(Schema.isInt()), + "title": Schema.String.annotate({ "description": "The title of the web resource." }) +}).annotate({ + "title": "URL citation", + "description": "A citation for a web resource used to generate a model response." +}) +export type ContainerFileCitationBody = { + readonly "type": "container_file_citation" + readonly "container_id": string + readonly "file_id": string + readonly "start_index": number + readonly "end_index": number + readonly "filename": string +} +export const ContainerFileCitationBody = Schema.Struct({ + "type": Schema.Literal("container_file_citation").annotate({ + "description": "The type of the container file citation. Always `container_file_citation`." + }), + "container_id": Schema.String.annotate({ "description": "The ID of the container file." }), + "file_id": Schema.String.annotate({ "description": "The ID of the file." }), + "start_index": Schema.Number.annotate({ + "description": "The index of the first character of the container file citation in the message." + }).check(Schema.isInt()), + "end_index": Schema.Number.annotate({ + "description": "The index of the last character of the container file citation in the message." + }).check(Schema.isInt()), + "filename": Schema.String.annotate({ "description": "The filename of the container file cited." }) +}).annotate({ + "title": "Container file citation", + "description": "A citation for a container file used to generate a model response." +}) +export type TopLogProb = { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray +} +export const TopLogProb = Schema.Struct({ + "token": Schema.String, + "logprob": Schema.Number.check(Schema.isFinite()), + "bytes": Schema.Array(Schema.Number.check(Schema.isInt())) +}).annotate({ "title": "Top log probability", "description": "The top log probability of a token." }) +export type TextContent = { readonly "type": "text"; readonly "text": string } +export const TextContent = Schema.Struct({ "type": Schema.Literal("text"), "text": Schema.String }).annotate({ + "title": "Text Content", + "description": "A text content." +}) +export type SummaryTextContent = { readonly "type": "summary_text"; readonly "text": string } +export const SummaryTextContent = Schema.Struct({ + "type": Schema.Literal("summary_text").annotate({ "description": "The type of the object. Always `summary_text`." }), + "text": Schema.String.annotate({ "description": "A summary of the reasoning output from the model so far." }) +}).annotate({ "title": "Summary text", "description": "A summary text from the model." }) +export type ReasoningTextContent = { readonly "type": "reasoning_text"; readonly "text": string } +export const ReasoningTextContent = Schema.Struct({ + "type": Schema.Literal("reasoning_text").annotate({ + "description": "The type of the reasoning text. Always `reasoning_text`." + }), + "text": Schema.String.annotate({ "description": "The reasoning text from the model." }) +}).annotate({ "title": "Reasoning text", "description": "Reasoning text from the model." }) +export type RefusalContent = { readonly "type": "refusal"; readonly "refusal": string } +export const RefusalContent = Schema.Struct({ + "type": Schema.Literal("refusal").annotate({ "description": "The type of the refusal. Always `refusal`." }), + "refusal": Schema.String.annotate({ "description": "The refusal explanation from the model." }) +}).annotate({ "title": "Refusal", "description": "A refusal from the model." }) +export type InputImageContent = { + readonly "type": "input_image" + readonly "image_url"?: string | null + readonly "file_id"?: string | null + readonly "detail": "low" | "high" | "auto" | "original" +} +export const InputImageContent = Schema.Struct({ + "type": Schema.Literal("input_image").annotate({ + "description": "The type of the input item. Always `input_image`." + }), + "image_url": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL.", + "format": "uri" + }), + Schema.Null + ]) + ), + "file_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of the file to be sent to the model." }), + Schema.Null + ]) + ), + "detail": Schema.Literals(["low", "high", "auto", "original"]).annotate({ + "description": + "The detail level of the image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`." + }) +}).annotate({ + "title": "Input image", + "description": "An image input to the model. Learn about [image inputs](/docs/guides/vision)." +}) +export type ComputerScreenshotContent = { + readonly "type": "computer_screenshot" + readonly "image_url": string | null + readonly "file_id": string | null + readonly "detail": "low" | "high" | "auto" | "original" +} +export const ComputerScreenshotContent = Schema.Struct({ + "type": Schema.Literal("computer_screenshot").annotate({ + "description": + "Specifies the event type. For a computer screenshot, this property is always set to `computer_screenshot`." + }), + "image_url": Schema.Union([ + Schema.String.annotate({ "description": "The URL of the screenshot image.", "format": "uri" }), + Schema.Null + ]), + "file_id": Schema.Union([ + Schema.String.annotate({ "description": "The identifier of an uploaded file that contains the screenshot." }), + Schema.Null + ]), + "detail": Schema.Literals(["low", "high", "auto", "original"]).annotate({ + "description": + "The detail level of the screenshot image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`." + }) +}).annotate({ "title": "Computer screenshot", "description": "A screenshot of a computer." }) +export type InputFileContent = { + readonly "type": "input_file" + readonly "file_id"?: string | null + readonly "filename"?: string + readonly "file_data"?: string + readonly "file_url"?: string + readonly "detail"?: "low" | "high" +} +export const InputFileContent = Schema.Struct({ + "type": Schema.Literal("input_file").annotate({ "description": "The type of the input item. Always `input_file`." }), + "file_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of the file to be sent to the model." }), + Schema.Null + ]) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The name of the file to be sent to the model." }) + ), + "file_data": Schema.optionalKey( + Schema.String.annotate({ "description": "The content of the file to be sent to the model.\n" }) + ), + "file_url": Schema.optionalKey( + Schema.String.annotate({ "description": "The URL of the file to be sent to the model.", "format": "uri" }) + ), + "detail": Schema.optionalKey( + Schema.Literals(["low", "high"]).annotate({ + "description": + "The detail level of the file to be sent to the model. Use `low` for the default rendering behavior, or `high` to render the file at higher quality. Defaults to `low`." + }) + ) +}).annotate({ "title": "Input file", "description": "A file input to the model." }) +export type ClickParam = { + readonly "type": "click" + readonly "button": "left" | "right" | "wheel" | "back" | "forward" + readonly "x": number + readonly "y": number + readonly "keys"?: ReadonlyArray | null +} +export const ClickParam = Schema.Struct({ + "type": Schema.Literal("click").annotate({ + "description": "Specifies the event type. For a click action, this property is always `click`." + }), + "button": Schema.Literals(["left", "right", "wheel", "back", "forward"]).annotate({ + "description": + "Indicates which mouse button was pressed during the click. One of `left`, `right`, `wheel`, `back`, or `forward`." + }), + "x": Schema.Number.annotate({ "description": "The x-coordinate where the click occurred." }).check(Schema.isInt()), + "y": Schema.Number.annotate({ "description": "The y-coordinate where the click occurred." }).check(Schema.isInt()), + "keys": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ "description": "The keys being held while clicking." }), + Schema.Null + ]) + ) +}).annotate({ "title": "Click", "description": "A click action." }) +export type DoubleClickAction = { + readonly "type": "double_click" + readonly "x": number + readonly "y": number + readonly "keys": ReadonlyArray | null +} +export const DoubleClickAction = Schema.Struct({ + "type": Schema.Literal("double_click").annotate({ + "description": "Specifies the event type. For a double click action, this property is always set to `double_click`." + }), + "x": Schema.Number.annotate({ "description": "The x-coordinate where the double click occurred." }).check( + Schema.isInt() + ), + "y": Schema.Number.annotate({ "description": "The y-coordinate where the double click occurred." }).check( + Schema.isInt() + ), + "keys": Schema.Union([ + Schema.Array(Schema.String).annotate({ "description": "The keys being held while double-clicking." }), + Schema.Null + ]) +}).annotate({ "title": "DoubleClick", "description": "A double click action." }) +export type CoordParam = { readonly "x": number; readonly "y": number } +export const CoordParam = Schema.Struct({ + "x": Schema.Number.annotate({ "description": "The x-coordinate." }).check(Schema.isInt()), + "y": Schema.Number.annotate({ "description": "The y-coordinate." }).check(Schema.isInt()) +}).annotate({ "title": "Coordinate", "description": "An x/y coordinate pair, e.g. `{ x: 100, y: 200 }`." }) +export type KeyPressAction = { readonly "type": "keypress"; readonly "keys": ReadonlyArray } +export const KeyPressAction = Schema.Struct({ + "type": Schema.Literal("keypress").annotate({ + "description": "Specifies the event type. For a keypress action, this property is always set to `keypress`." + }), + "keys": Schema.Array( + Schema.String.annotate({ "description": "One of the keys the model is requesting to be pressed." }) + ).annotate({ + "description": + "The combination of keys the model is requesting to be pressed. This is an array of strings, each representing a key." + }) +}).annotate({ "title": "KeyPress", "description": "A collection of keypresses the model would like to perform." }) +export type MoveParam = { + readonly "type": "move" + readonly "x": number + readonly "y": number + readonly "keys"?: ReadonlyArray | null +} +export const MoveParam = Schema.Struct({ + "type": Schema.Literal("move").annotate({ + "description": "Specifies the event type. For a move action, this property is always set to `move`." + }), + "x": Schema.Number.annotate({ "description": "The x-coordinate to move to." }).check(Schema.isInt()), + "y": Schema.Number.annotate({ "description": "The y-coordinate to move to." }).check(Schema.isInt()), + "keys": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ "description": "The keys being held while moving the mouse." }), + Schema.Null + ]) + ) +}).annotate({ "title": "Move", "description": "A mouse move action." }) +export type ScreenshotParam = { readonly "type": "screenshot" } +export const ScreenshotParam = Schema.Struct({ + "type": Schema.Literal("screenshot").annotate({ + "description": "Specifies the event type. For a screenshot action, this property is always set to `screenshot`." + }) +}).annotate({ "title": "Screenshot", "description": "A screenshot action." }) +export type ScrollParam = { + readonly "type": "scroll" + readonly "x": number + readonly "y": number + readonly "scroll_x": number + readonly "scroll_y": number + readonly "keys"?: ReadonlyArray | null +} +export const ScrollParam = Schema.Struct({ + "type": Schema.Literal("scroll").annotate({ + "description": "Specifies the event type. For a scroll action, this property is always set to `scroll`." + }), + "x": Schema.Number.annotate({ "description": "The x-coordinate where the scroll occurred." }).check(Schema.isInt()), + "y": Schema.Number.annotate({ "description": "The y-coordinate where the scroll occurred." }).check(Schema.isInt()), + "scroll_x": Schema.Number.annotate({ "description": "The horizontal scroll distance." }).check(Schema.isInt()), + "scroll_y": Schema.Number.annotate({ "description": "The vertical scroll distance." }).check(Schema.isInt()), + "keys": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ "description": "The keys being held while scrolling." }), + Schema.Null + ]) + ) +}).annotate({ "title": "Scroll", "description": "A scroll action." }) +export type TypeParam = { readonly "type": "type"; readonly "text": string } +export const TypeParam = Schema.Struct({ + "type": Schema.Literal("type").annotate({ + "description": "Specifies the event type. For a type action, this property is always set to `type`." + }), + "text": Schema.String.annotate({ "description": "The text to type." }) +}).annotate({ "title": "Type", "description": "An action to type in text." }) +export type WaitParam = { readonly "type": "wait" } +export const WaitParam = Schema.Struct({ + "type": Schema.Literal("wait").annotate({ + "description": "Specifies the event type. For a wait action, this property is always set to `wait`." + }) +}).annotate({ "title": "Wait", "description": "A wait action." }) +export type ComputerCallSafetyCheckParam = { + readonly "id": string + readonly "code"?: string | null + readonly "message"?: string | null +} +export const ComputerCallSafetyCheckParam = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the pending safety check." }), + "code": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The type of the pending safety check." }), Schema.Null]) + ), + "message": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Details about the pending safety check." }), Schema.Null]) + ) +}).annotate({ "description": "A pending safety check for the computer call." }) +export type ToolSearchCall = { + readonly "type": "tool_search_call" + readonly "id": string + readonly "call_id": string | null + readonly "execution": "server" | "client" + readonly "arguments": unknown + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const ToolSearchCall = Schema.Struct({ + "type": Schema.Literal("tool_search_call").annotate({ + "description": "The type of the item. Always `tool_search_call`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the tool search call item." }), + "call_id": Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of the tool search call generated by the model." }), + Schema.Null + ]), + "execution": Schema.Literals(["server", "client"]).annotate({ + "description": "Whether tool search was executed by the server or by the client." + }), + "arguments": Schema.Unknown.annotate({ "description": "Arguments used for the tool search call." }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the tool search call item that was recorded." + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item." }) + ) +}) +export type FunctionTool = { + readonly "type": "function" + readonly "name": string + readonly "description"?: string | null + readonly "parameters": {} | null + readonly "strict": boolean | null + readonly "defer_loading"?: boolean +} +export const FunctionTool = Schema.Struct({ + "type": Schema.Literal("function").annotate({ "description": "The type of the function tool. Always `function`." }), + "name": Schema.String.annotate({ "description": "The name of the function to call." }), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "A description of the function. Used by the model to determine whether or not to call the function." + }), + Schema.Null + ]) + ), + "parameters": Schema.Union([ + Schema.Struct({}).annotate({ "description": "A JSON schema object describing the parameters of the function." }), + Schema.Null + ]), + "strict": Schema.Union([ + Schema.Boolean.annotate({ "description": "Whether to enforce strict parameter validation. Default `true`." }), + Schema.Null + ]), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this function is deferred and loaded via tool search." }) + ) +}).annotate({ + "title": "Function", + "description": + "Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling)." +}) +export type ComputerTool = { readonly "type": "computer" } +export const ComputerTool = Schema.Struct({ + "type": Schema.Literal("computer").annotate({ "description": "The type of the computer tool. Always `computer`." }) +}).annotate({ + "title": "Computer", + "description": + "A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use)." +}) +export type ComputerUsePreviewTool = { + readonly "type": "computer_use_preview" + readonly "environment": "windows" | "mac" | "linux" | "ubuntu" | "browser" + readonly "display_width": number + readonly "display_height": number +} +export const ComputerUsePreviewTool = Schema.Struct({ + "type": Schema.Literal("computer_use_preview").annotate({ + "description": "The type of the computer use tool. Always `computer_use_preview`." + }), + "environment": Schema.Literals(["windows", "mac", "linux", "ubuntu", "browser"]).annotate({ + "description": "The type of computer environment to control." + }), + "display_width": Schema.Number.annotate({ "description": "The width of the computer display." }).check( + Schema.isInt() + ), + "display_height": Schema.Number.annotate({ "description": "The height of the computer display." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "Computer use preview", + "description": + "A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use)." +}) +export type InputFidelity = "high" | "low" +export const InputFidelity = Schema.Literals(["high", "low"]).annotate({ + "description": + "Control how much effort the model will exert to match the style and features, especially facial features, of input images. This parameter is only supported for `gpt-image-1` and `gpt-image-1.5` and later models, unsupported for `gpt-image-1-mini`. Supports `high` and `low`. Defaults to `low`." +}) +export type LocalShellToolParam = { readonly "type": "local_shell" } +export const LocalShellToolParam = Schema.Struct({ + "type": Schema.Literal("local_shell").annotate({ + "description": "The type of the local shell tool. Always `local_shell`." + }) +}).annotate({ + "title": "Local shell tool", + "description": "A tool that allows the model to execute shell commands in a local environment." +}) +export type LocalSkillParam = { readonly "name": string; readonly "description": string; readonly "path": string } +export const LocalSkillParam = Schema.Struct({ + "name": Schema.String.annotate({ "description": "The name of the skill." }), + "description": Schema.String.annotate({ "description": "The description of the skill." }), + "path": Schema.String.annotate({ "description": "The path to the directory containing the skill." }) +}) +export type ContainerReferenceParam = { readonly "type": "container_reference"; readonly "container_id": string } +export const ContainerReferenceParam = Schema.Struct({ + "type": Schema.Literal("container_reference").annotate({ + "description": "References a container created with the /v1/containers endpoint" + }), + "container_id": Schema.String.annotate({ "description": "The ID of the referenced container." }) +}) +export type CustomTextFormatParam = { readonly "type": "text" } +export const CustomTextFormatParam = Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "Unconstrained text format. Always `text`." }) +}).annotate({ "title": "Text format", "description": "Unconstrained free-form text." }) +export type CustomGrammarFormatParam = { + readonly "type": "grammar" + readonly "syntax": "lark" | "regex" + readonly "definition": string +} +export const CustomGrammarFormatParam = Schema.Struct({ + "type": Schema.Literal("grammar").annotate({ "description": "Grammar format. Always `grammar`." }), + "syntax": Schema.Literals(["lark", "regex"]).annotate({ + "description": "The syntax of the grammar definition. One of `lark` or `regex`." + }), + "definition": Schema.String.annotate({ "description": "The grammar definition." }) +}).annotate({ "title": "Grammar format", "description": "A grammar defined by the user." }) +export type EmptyModelParam = {} +export const EmptyModelParam = Schema.Struct({}) +export type ToolSearchToolParam = { + readonly "type": "tool_search" + readonly "execution"?: "server" | "client" + readonly "description"?: string | null + readonly "parameters"?: {} | null +} +export const ToolSearchToolParam = Schema.Struct({ + "type": Schema.Literal("tool_search").annotate({ "description": "The type of the tool. Always `tool_search`." }), + "execution": Schema.optionalKey( + Schema.Literals(["server", "client"]).annotate({ + "description": "Whether tool search is executed by the server or by the client." + }) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "Description shown to the model for a client-executed tool search tool." + }), + Schema.Null + ]) + ), + "parameters": Schema.optionalKey( + Schema.Union([ + Schema.Struct({}).annotate({ "description": "Parameter schema for a client-executed tool search tool." }), + Schema.Null + ]) + ) +}).annotate({ + "title": "Tool search tool", + "description": "Hosted or BYOT tool search configuration for deferred tools." +}) +export type SearchContentType = "text" | "image" +export const SearchContentType = Schema.Literals(["text", "image"]) +export type ApplyPatchToolParam = { readonly "type": "apply_patch" } +export const ApplyPatchToolParam = Schema.Struct({ + "type": Schema.Literal("apply_patch").annotate({ "description": "The type of the tool. Always `apply_patch`." }) +}).annotate({ + "title": "Apply patch tool", + "description": "Allows the assistant to create, delete, or update files using unified diffs." +}) +export type CompactionBody = { + readonly "type": "compaction" + readonly "id": string + readonly "encrypted_content": string + readonly "created_by"?: string +} +export const CompactionBody = Schema.Struct({ + "type": Schema.Literal("compaction").annotate({ "description": "The type of the item. Always `compaction`." }), + "id": Schema.String.annotate({ "description": "The unique ID of the compaction item." }), + "encrypted_content": Schema.String.annotate({ + "description": "The encrypted content that was produced by compaction." + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item." }) + ) +}).annotate({ + "title": "Compaction item", + "description": + "A compaction item generated by the [`v1/responses/compact` API](/docs/api-reference/responses/compact)." +}) +export type CodeInterpreterOutputLogs = { readonly "type": "logs"; readonly "logs": string } +export const CodeInterpreterOutputLogs = Schema.Struct({ + "type": Schema.Literal("logs").annotate({ "description": "The type of the output. Always `logs`." }), + "logs": Schema.String.annotate({ "description": "The logs output from the code interpreter." }) +}).annotate({ "title": "Code interpreter output logs", "description": "The logs output from the code interpreter." }) +export type CodeInterpreterOutputImage = { readonly "type": "image"; readonly "url": string } +export const CodeInterpreterOutputImage = Schema.Struct({ + "type": Schema.Literal("image").annotate({ "description": "The type of the output. Always `image`." }), + "url": Schema.String.annotate({ + "description": "The URL of the image output from the code interpreter.", + "format": "uri" + }) +}).annotate({ "title": "Code interpreter output image", "description": "The image output from the code interpreter." }) +export type LocalShellExecAction = { + readonly "type": "exec" + readonly "command": ReadonlyArray + readonly "timeout_ms"?: number | null + readonly "working_directory"?: string | null + readonly "env": {} + readonly "user"?: string | null +} +export const LocalShellExecAction = Schema.Struct({ + "type": Schema.Literal("exec").annotate({ "description": "The type of the local shell action. Always `exec`." }), + "command": Schema.Array(Schema.String).annotate({ "description": "The command to run." }), + "timeout_ms": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "Optional timeout in milliseconds for the command." }).check( + Schema.isInt() + ), + Schema.Null + ]) + ), + "working_directory": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Optional working directory to run the command in." }), + Schema.Null + ]) + ), + "env": Schema.Struct({}).annotate({ "description": "Environment variables to set for the command." }), + "user": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Optional user to run the command as." }), Schema.Null]) + ) +}).annotate({ "title": "Local shell exec action", "description": "Execute a shell command on the server." }) +export type LocalEnvironmentResource = { readonly "type": "local" } +export const LocalEnvironmentResource = Schema.Struct({ + "type": Schema.Literal("local").annotate({ "description": "The environment type. Always `local`." }) +}).annotate({ + "title": "Local Environment", + "description": "Represents the use of a local environment to perform shell actions." +}) +export type ContainerReferenceResource = { readonly "type": "container_reference"; readonly "container_id": string } +export const ContainerReferenceResource = Schema.Struct({ + "type": Schema.Literal("container_reference").annotate({ + "description": "The environment type. Always `container_reference`." + }), + "container_id": Schema.String +}).annotate({ "title": "Container Reference", "description": "Represents a container created with /v1/containers." }) +export type FunctionShellCallOutputTimeoutOutcome = { readonly "type": "timeout" } +export const FunctionShellCallOutputTimeoutOutcome = Schema.Struct({ + "type": Schema.Literal("timeout").annotate({ "description": "The outcome type. Always `timeout`." }) +}).annotate({ + "title": "Shell call timeout outcome", + "description": "Indicates that the shell call exceeded its configured time limit." +}) +export type FunctionShellCallOutputExitOutcome = { readonly "type": "exit"; readonly "exit_code": number } +export const FunctionShellCallOutputExitOutcome = Schema.Struct({ + "type": Schema.Literal("exit").annotate({ "description": "The outcome type. Always `exit`." }), + "exit_code": Schema.Number.annotate({ "description": "Exit code from the shell process." }).check(Schema.isInt()) +}).annotate({ + "title": "Shell call exit outcome", + "description": "Indicates that the shell commands finished and returned an exit code." +}) +export type ApplyPatchCreateFileOperation = { + readonly "type": "create_file" + readonly "path": string + readonly "diff": string +} +export const ApplyPatchCreateFileOperation = Schema.Struct({ + "type": Schema.Literal("create_file").annotate({ "description": "Create a new file with the provided diff." }), + "path": Schema.String.annotate({ "description": "Path of the file to create." }), + "diff": Schema.String.annotate({ "description": "Diff to apply." }) +}).annotate({ + "title": "Apply patch create file operation", + "description": "Instruction describing how to create a file via the apply_patch tool." +}) +export type ApplyPatchDeleteFileOperation = { readonly "type": "delete_file"; readonly "path": string } +export const ApplyPatchDeleteFileOperation = Schema.Struct({ + "type": Schema.Literal("delete_file").annotate({ "description": "Delete the specified file." }), + "path": Schema.String.annotate({ "description": "Path of the file to delete." }) +}).annotate({ + "title": "Apply patch delete file operation", + "description": "Instruction describing how to delete a file via the apply_patch tool." +}) +export type ApplyPatchUpdateFileOperation = { + readonly "type": "update_file" + readonly "path": string + readonly "diff": string +} +export const ApplyPatchUpdateFileOperation = Schema.Struct({ + "type": Schema.Literal("update_file").annotate({ "description": "Update an existing file with the provided diff." }), + "path": Schema.String.annotate({ "description": "Path of the file to update." }), + "diff": Schema.String.annotate({ "description": "Diff to apply." }) +}).annotate({ + "title": "Apply patch update file operation", + "description": "Instruction describing how to update a file via the apply_patch tool." +}) +export type ApplyPatchToolCallOutput = { + readonly "type": "apply_patch_call_output" + readonly "id": string + readonly "call_id": string + readonly "status": "completed" | "failed" + readonly "output"?: string | null + readonly "created_by"?: string +} +export const ApplyPatchToolCallOutput = Schema.Struct({ + "type": Schema.Literal("apply_patch_call_output").annotate({ + "description": "The type of the item. Always `apply_patch_call_output`." + }), + "id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call output. Populated when this item is returned via API." + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call generated by the model." + }), + "status": Schema.Literals(["completed", "failed"]).annotate({ + "description": "The status of the apply patch tool call output. One of `completed` or `failed`." + }), + "output": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Optional textual output returned by the apply patch tool." }), + Schema.Null + ]) + ), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the entity that created this tool call output." }) + ) +}).annotate({ + "title": "Apply patch tool call output", + "description": "The output emitted by an apply patch tool call." +}) +export type InputTextContentParam = { readonly "type": "input_text"; readonly "text": string } +export const InputTextContentParam = Schema.Struct({ + "type": Schema.Literal("input_text").annotate({ "description": "The type of the input item. Always `input_text`." }), + "text": Schema.String.annotate({ "description": "The text input to the model." }).check(Schema.isMaxLength(10485760)) +}).annotate({ "title": "Input text", "description": "A text input to the model." }) +export type InputImageContentParamAutoParam = { + readonly "type": "input_image" + readonly "image_url"?: string | null + readonly "file_id"?: string | null + readonly "detail"?: "low" | "high" | "auto" | "original" | null +} +export const InputImageContentParamAutoParam = Schema.Struct({ + "type": Schema.Literal("input_image").annotate({ + "description": "The type of the input item. Always `input_image`." + }), + "image_url": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL.", + "format": "uri" + }).check(Schema.isMaxLength(20971520)), + Schema.Null + ]) + ), + "file_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of the file to be sent to the model." }), + Schema.Null + ]) + ), + "detail": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["low", "high", "auto", "original"]).annotate({ + "description": + "The detail level of the image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`." + }), + Schema.Null + ]) + ) +}).annotate({ + "title": "Input image", + "description": "An image input to the model. Learn about [image inputs](/docs/guides/vision)" +}) +export type InputFileContentParam = { + readonly "type": "input_file" + readonly "file_id"?: string | null + readonly "filename"?: string | null + readonly "file_data"?: string | null + readonly "file_url"?: string | null + readonly "detail"?: "low" | "high" +} +export const InputFileContentParam = Schema.Struct({ + "type": Schema.Literal("input_file").annotate({ "description": "The type of the input item. Always `input_file`." }), + "file_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of the file to be sent to the model." }), + Schema.Null + ]) + ), + "filename": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The name of the file to be sent to the model." }), + Schema.Null + ]) + ), + "file_data": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The base64-encoded data of the file to be sent to the model." }).check( + Schema.isMaxLength(73400320) + ), + Schema.Null + ]) + ), + "file_url": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The URL of the file to be sent to the model.", "format": "uri" }), + Schema.Null + ]) + ), + "detail": Schema.optionalKey( + Schema.Literals(["low", "high"]).annotate({ + "description": + "The detail level of the file to be sent to the model. Use `low` for the default rendering behavior, or `high` to render the file at higher quality. Defaults to `low`." + }) + ) +}).annotate({ "title": "Input file", "description": "A file input to the model." }) +export type FunctionShellCallOutputTimeoutOutcomeParam = { readonly "type": "timeout" } +export const FunctionShellCallOutputTimeoutOutcomeParam = Schema.Struct({ + "type": Schema.Literal("timeout").annotate({ "description": "The outcome type. Always `timeout`." }) +}).annotate({ + "title": "Shell call timeout outcome", + "description": "Indicates that the shell call exceeded its configured time limit." +}) +export type FunctionShellCallOutputExitOutcomeParam = { readonly "type": "exit"; readonly "exit_code": number } +export const FunctionShellCallOutputExitOutcomeParam = Schema.Struct({ + "type": Schema.Literal("exit").annotate({ "description": "The outcome type. Always `exit`." }), + "exit_code": Schema.Number.annotate({ "description": "The exit code returned by the shell process." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "Shell call exit outcome", + "description": "Indicates that the shell commands finished and returned an exit code." +}) +export type ApplyPatchCreateFileOperationParam = { + readonly "type": "create_file" + readonly "path": string + readonly "diff": string +} +export const ApplyPatchCreateFileOperationParam = Schema.Struct({ + "type": Schema.Literal("create_file").annotate({ "description": "The operation type. Always `create_file`." }), + "path": Schema.String.annotate({ "description": "Path of the file to create relative to the workspace root." }).check( + Schema.isMinLength(1) + ), + "diff": Schema.String.annotate({ "description": "Unified diff content to apply when creating the file." }).check( + Schema.isMaxLength(10485760) + ) +}).annotate({ + "title": "Apply patch create file operation", + "description": "Instruction for creating a new file via the apply_patch tool." +}) +export type ApplyPatchDeleteFileOperationParam = { readonly "type": "delete_file"; readonly "path": string } +export const ApplyPatchDeleteFileOperationParam = Schema.Struct({ + "type": Schema.Literal("delete_file").annotate({ "description": "The operation type. Always `delete_file`." }), + "path": Schema.String.annotate({ "description": "Path of the file to delete relative to the workspace root." }).check( + Schema.isMinLength(1) + ) +}).annotate({ + "title": "Apply patch delete file operation", + "description": "Instruction for deleting an existing file via the apply_patch tool." +}) +export type ApplyPatchUpdateFileOperationParam = { + readonly "type": "update_file" + readonly "path": string + readonly "diff": string +} +export const ApplyPatchUpdateFileOperationParam = Schema.Struct({ + "type": Schema.Literal("update_file").annotate({ "description": "The operation type. Always `update_file`." }), + "path": Schema.String.annotate({ "description": "Path of the file to update relative to the workspace root." }).check( + Schema.isMinLength(1) + ), + "diff": Schema.String.annotate({ "description": "Unified diff content to apply to the existing file." }).check( + Schema.isMaxLength(10485760) + ) +}).annotate({ + "title": "Apply patch update file operation", + "description": "Instruction for updating an existing file via the apply_patch tool." +}) +export type CompactionTriggerItemParam = { readonly "type": "compaction_trigger" } +export const CompactionTriggerItemParam = Schema.Struct({ + "type": Schema.Literal("compaction_trigger").annotate({ + "description": "The type of the item. Always `compaction_trigger`." + }) +}).annotate({ + "title": "Compaction trigger", + "description": "Compacts the current context. Must be the final input item." +}) +export type ItemReferenceParam = { readonly "type"?: "item_reference" | null; readonly "id": string } +export const ItemReferenceParam = Schema.Struct({ + "type": Schema.optionalKey( + Schema.Union([ + Schema.Literal("item_reference").annotate({ + "description": "The type of item to reference. Always `item_reference`." + }), + Schema.Null + ]) + ), + "id": Schema.String.annotate({ "description": "The ID of the item to reference." }) +}).annotate({ "title": "Item reference", "description": "An internal identifier for an item to reference." }) +export type ConversationResource = { + readonly "id": string + readonly "object": "conversation" + readonly "metadata": unknown + readonly "created_at": number +} +export const ConversationResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the conversation." }), + "object": Schema.Literal("conversation").annotate({ + "description": "The object type, which is always `conversation`." + }), + "metadata": Schema.Unknown.annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard.\n Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters." + }), + "created_at": Schema.Number.annotate({ + "description": "The time at which the conversation was created, measured in seconds since the Unix epoch.", + "format": "unixtime" + }).check(Schema.isInt()) +}) +export type ImageGenOutputTokensDetails = { readonly "image_tokens": number; readonly "text_tokens": number } +export const ImageGenOutputTokensDetails = Schema.Struct({ + "image_tokens": Schema.Number.annotate({ "description": "The number of image output tokens generated by the model." }) + .check(Schema.isInt()), + "text_tokens": Schema.Number.annotate({ "description": "The number of text output tokens generated by the model." }) + .check(Schema.isInt()) +}).annotate({ + "title": "Image generation output token details", + "description": "The output token details for the image generation." +}) +export type ImageGenInputUsageDetails = { readonly "text_tokens": number; readonly "image_tokens": number } +export const ImageGenInputUsageDetails = Schema.Struct({ + "text_tokens": Schema.Number.annotate({ "description": "The number of text tokens in the input prompt." }).check( + Schema.isInt() + ), + "image_tokens": Schema.Number.annotate({ "description": "The number of image tokens in the input prompt." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "Input usage details", + "description": "The input tokens detailed information for the image generation." +}) +export type SpecificApplyPatchParam = { readonly "type": "apply_patch" } +export const SpecificApplyPatchParam = Schema.Struct({ + "type": Schema.Literal("apply_patch").annotate({ "description": "The tool to call. Always `apply_patch`." }) +}).annotate({ + "title": "Specific apply patch tool choice", + "description": "Forces the model to call the apply_patch tool when executing a tool call." +}) +export type SpecificFunctionShellParam = { readonly "type": "shell" } +export const SpecificFunctionShellParam = Schema.Struct({ + "type": Schema.Literal("shell").annotate({ "description": "The tool to call. Always `shell`." }) +}).annotate({ + "title": "Specific shell tool choice", + "description": "Forces the model to call the shell tool when a tool call is required." +}) +export type ConversationParam_2 = { readonly "id": string } +export const ConversationParam_2 = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the conversation." }) +}).annotate({ "title": "Conversation object", "description": "The conversation that this response belongs to." }) +export type ContextManagementParam = { readonly "type": string; readonly "compact_threshold"?: number | null } +export const ContextManagementParam = Schema.Struct({ + "type": Schema.String.annotate({ + "description": "The context management entry type. Currently only 'compaction' is supported." + }), + "compact_threshold": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "Token threshold at which compaction should be triggered for this entry." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1000)), + Schema.Null + ]) + ) +}) +export type Conversation_2 = { readonly "id": string } +export const Conversation_2 = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The unique ID of the conversation that this response was associated with." + }) +}).annotate({ + "title": "Conversation", + "description": + "The conversation that this response belonged to. Input items and output items from this response were automatically added to this conversation." +}) +export type UpdateConversationBody = { readonly "metadata": {} | null } +export const UpdateConversationBody = Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard.\n Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters." + }) +}) +export type DeletedConversationResource = { + readonly "object": "conversation.deleted" + readonly "deleted": boolean + readonly "id": string +} +export const DeletedConversationResource = Schema.Struct({ + "object": Schema.Literal("conversation.deleted"), + "deleted": Schema.Boolean, + "id": Schema.String +}) +export type OrderEnum = "asc" | "desc" +export const OrderEnum = Schema.Literals(["asc", "desc"]) +export type VideoResource = { + readonly "id": string + readonly "object": "video" + readonly "model": + | string + | "sora-2" + | "sora-2-pro" + | "sora-2-2025-10-06" + | "sora-2-pro-2025-10-06" + | "sora-2-2025-12-08" + readonly "status": "queued" | "in_progress" | "completed" | "failed" + readonly "progress": number + readonly "created_at": number + readonly "completed_at": number | null + readonly "expires_at": number | null + readonly "prompt": string | null + readonly "size": "720x1280" | "1280x720" | "1024x1792" | "1792x1024" + readonly "seconds": string + readonly "remixed_from_video_id": string | null + readonly "error": { readonly "code": string; readonly "message": string } | null +} +export const VideoResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the video job." }), + "object": Schema.Literal("video").annotate({ "description": "The object type, which is always `video`." }), + "model": Schema.Union([ + Schema.String, + Schema.Literals(["sora-2", "sora-2-pro", "sora-2-2025-10-06", "sora-2-pro-2025-10-06", "sora-2-2025-12-08"]) + ]).annotate({ "description": "The video generation model that produced the job." }), + "status": Schema.Literals(["queued", "in_progress", "completed", "failed"]).annotate({ + "description": "Current lifecycle status of the video job." + }), + "progress": Schema.Number.annotate({ "description": "Approximate completion percentage for the generation task." }) + .check(Schema.isInt()), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (seconds) for when the job was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "completed_at": Schema.Union([ + Schema.Number.annotate({ + "description": "Unix timestamp (seconds) for when the job completed, if finished.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "expires_at": Schema.Union([ + Schema.Number.annotate({ + "description": "Unix timestamp (seconds) for when the downloadable assets expire, if set.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "prompt": Schema.Union([ + Schema.String.annotate({ "description": "The prompt that was used to generate the video." }), + Schema.Null + ]), + "size": Schema.Literals(["720x1280", "1280x720", "1024x1792", "1792x1024"]).annotate({ + "description": "The resolution of the generated video." + }), + "seconds": Schema.String.annotate({ + "description": "Duration of the generated clip in seconds. For extensions, this is the stitched total duration." + }), + "remixed_from_video_id": Schema.Union([ + Schema.String.annotate({ "description": "Identifier of the source video if this video is a remix." }), + Schema.Null + ]), + "error": Schema.Union([ + Schema.Struct({ + "code": Schema.String.annotate({ "description": "A machine-readable error code that was returned." }), + "message": Schema.String.annotate({ + "description": "A human-readable description of the error that was returned." + }) + }).annotate({ + "title": "Error", + "description": "Error payload that explains why generation failed, if applicable." + }), + Schema.Null + ]) +}).annotate({ "title": "Video job", "description": "Structured information describing a generated video job." }) +export type ImageRefParam_2 = { readonly "image_url"?: string; readonly "file_id"?: string } +export const ImageRefParam_2 = Schema.Struct({ + "image_url": Schema.optionalKey( + Schema.String.annotate({ "description": "A fully qualified URL or base64-encoded data URL.", "format": "uri" }) + .check(Schema.isMaxLength(20971520)) + ), + "file_id": Schema.optionalKey(Schema.String) +}) +export type CreateVideoJsonBody = { + readonly "model"?: + | string + | "sora-2" + | "sora-2-pro" + | "sora-2-2025-10-06" + | "sora-2-pro-2025-10-06" + | "sora-2-2025-12-08" + readonly "prompt": string + readonly "input_reference"?: { readonly "image_url"?: string; readonly "file_id"?: string } + readonly "seconds"?: "4" | "8" | "12" + readonly "size"?: "720x1280" | "1280x720" | "1024x1792" | "1792x1024" +} +export const CreateVideoJsonBody = Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["sora-2", "sora-2-pro", "sora-2-2025-10-06", "sora-2-pro-2025-10-06", "sora-2-2025-12-08"]) + ]).annotate({ + "description": "The video generation model to use (allowed values: sora-2, sora-2-pro). Defaults to `sora-2`." + }) + ), + "prompt": Schema.String.annotate({ "description": "Text prompt that describes the video to generate." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)), + "input_reference": Schema.optionalKey( + Schema.Struct({ + "image_url": Schema.optionalKey( + Schema.String.annotate({ "description": "A fully qualified URL or base64-encoded data URL.", "format": "uri" }) + .check(Schema.isMaxLength(20971520)) + ), + "file_id": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "Optional reference object that guides generation. Provide exactly one of `image_url` or `file_id`." + }) + ), + "seconds": Schema.optionalKey( + Schema.Literals(["4", "8", "12"]).annotate({ + "description": "Clip duration in seconds (allowed values: 4, 8, 12). Defaults to 4 seconds." + }) + ), + "size": Schema.optionalKey( + Schema.Literals(["720x1280", "1280x720", "1024x1792", "1792x1024"]).annotate({ + "description": + "Output resolution formatted as width x height (allowed values: 720x1280, 1280x720, 1024x1792, 1792x1024). Defaults to 720x1280." + }) + ) +}).annotate({ + "title": "Create video JSON request", + "description": "JSON parameters for creating a new video generation job." +}) +export type CreateVideoCharacterBody = { readonly "video": string; readonly "name": string } +export const CreateVideoCharacterBody = Schema.Struct({ + "video": Schema.String.annotate({ "description": "Video file used to create a character.", "format": "binary" }), + "name": Schema.String.annotate({ "description": "Display name for this API character." }).check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(80)) +}).annotate({ + "title": "Create character request", + "description": "Parameters for creating a character from an uploaded video." +}) +export type VideoCharacterResource = { + readonly "id": string | null + readonly "name": string | null + readonly "created_at": number +} +export const VideoCharacterResource = Schema.Struct({ + "id": Schema.Union([ + Schema.String.annotate({ "description": "Identifier for the character creation cameo." }), + Schema.Null + ]), + "name": Schema.Union([Schema.String.annotate({ "description": "Display name for the character." }), Schema.Null]), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the character was created.", + "format": "unixtime" + }).check(Schema.isInt()) +}) +export type VideoReferenceInputParam = { readonly "id": string } +export const VideoReferenceInputParam = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier of the completed video." }) +}).annotate({ "description": "Reference to the completed video." }) +export type CreateVideoEditJsonBody = { readonly "video": { readonly "id": string }; readonly "prompt": string } +export const CreateVideoEditJsonBody = Schema.Struct({ + "video": Schema.Struct({ "id": Schema.String.annotate({ "description": "The identifier of the completed video." }) }) + .annotate({ "description": "Reference to the completed video to edit." }), + "prompt": Schema.String.annotate({ "description": "Text prompt that describes how to edit the source video." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)) +}).annotate({ + "title": "Create video edit JSON request", + "description": "JSON parameters for editing an existing generated video." +}) +export type CreateVideoExtendJsonBody = { + readonly "video": { readonly "id": string } + readonly "prompt": string + readonly "seconds": "4" | "8" | "12" +} +export const CreateVideoExtendJsonBody = Schema.Struct({ + "video": Schema.Struct({ "id": Schema.String.annotate({ "description": "The identifier of the completed video." }) }) + .annotate({ "description": "Reference to the completed video to extend." }), + "prompt": Schema.String.annotate({ "description": "Updated text prompt that directs the extension generation." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(32000)), + "seconds": Schema.Literals(["4", "8", "12"]).annotate({ + "description": "Length of the newly generated extension segment in seconds (allowed values: 4, 8, 12, 16, 20)." + }) +}).annotate({ + "title": "Create video extension JSON request", + "description": "JSON parameters for extending an existing generated video." +}) +export type DeletedVideoResource = { + readonly "object": "video.deleted" + readonly "deleted": boolean + readonly "id": string +} +export const DeletedVideoResource = Schema.Struct({ + "object": Schema.Literal("video.deleted").annotate({ + "description": "The object type that signals the deletion response." + }), + "deleted": Schema.Boolean.annotate({ "description": "Indicates that the video resource was deleted." }), + "id": Schema.String.annotate({ "description": "Identifier of the deleted video." }) +}).annotate({ + "title": "Deleted video response", + "description": "Confirmation payload returned after deleting a video." +}) +export type VideoContentVariant = "video" | "thumbnail" | "spritesheet" +export const VideoContentVariant = Schema.Literals(["video", "thumbnail", "spritesheet"]) +export type CreateVideoRemixBody = { readonly "prompt": string } +export const CreateVideoRemixBody = Schema.Struct({ + "prompt": Schema.String.annotate({ "description": "Updated text prompt that directs the remix generation." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)) +}).annotate({ + "title": "Create video remix request", + "description": "Parameters for remixing an existing generated video." +}) +export type TokenCountsResource = { readonly "object": "response.input_tokens"; readonly "input_tokens": number } +export const TokenCountsResource = Schema.Struct({ + "object": Schema.Literal("response.input_tokens"), + "input_tokens": Schema.Number.check(Schema.isInt()) +}).annotate({ "title": "Token counts" }) +export type SkillResource = { + readonly "id": string + readonly "object": "skill" + readonly "name": string + readonly "description": string + readonly "created_at": number + readonly "default_version": string + readonly "latest_version": string +} +export const SkillResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the skill." }), + "object": Schema.Literal("skill").annotate({ "description": "The object type, which is `skill`." }), + "name": Schema.String.annotate({ "description": "Name of the skill." }), + "description": Schema.String.annotate({ "description": "Description of the skill." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (seconds) for when the skill was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "default_version": Schema.String.annotate({ "description": "Default version for the skill." }), + "latest_version": Schema.String.annotate({ "description": "Latest version for the skill." }) +}) +export type CreateSkillBody = { readonly "files": ReadonlyArray | string } +export const CreateSkillBody = Schema.Struct({ + "files": Schema.Union([ + Schema.Array(Schema.String.annotate({ "format": "binary" })).annotate({ + "description": "Skill files to upload (directory upload) or a single zip file." + }).check(Schema.isMaxLength(500)), + Schema.String.annotate({ "description": "Skill zip file to upload.", "format": "binary" }) + ], { mode: "oneOf" }) +}).annotate({ + "title": "Create skill request", + "description": "Uploads a skill either as a directory (multipart `files[]`) or as a single zip file." +}) +export type SetDefaultSkillVersionBody = { readonly "default_version": string } +export const SetDefaultSkillVersionBody = Schema.Struct({ + "default_version": Schema.String.annotate({ "description": "The skill version number to set as default." }) +}).annotate({ "title": "Update skill request", "description": "Updates the default version pointer for a skill." }) +export type DeletedSkillResource = { + readonly "object": "skill.deleted" + readonly "deleted": boolean + readonly "id": string +} +export const DeletedSkillResource = Schema.Struct({ + "object": Schema.Literal("skill.deleted"), + "deleted": Schema.Boolean, + "id": Schema.String +}) +export type SkillVersionResource = { + readonly "object": "skill.version" + readonly "id": string + readonly "skill_id": string + readonly "version": string + readonly "created_at": number + readonly "name": string + readonly "description": string +} +export const SkillVersionResource = Schema.Struct({ + "object": Schema.Literal("skill.version").annotate({ "description": "The object type, which is `skill.version`." }), + "id": Schema.String.annotate({ "description": "Unique identifier for the skill version." }), + "skill_id": Schema.String.annotate({ "description": "Identifier of the skill for this version." }), + "version": Schema.String.annotate({ "description": "Version number for this skill." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (seconds) for when the version was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "name": Schema.String.annotate({ "description": "Name of the skill version." }), + "description": Schema.String.annotate({ "description": "Description of the skill version." }) +}) +export type CreateSkillVersionBody = { readonly "files": ReadonlyArray | string; readonly "default"?: boolean } +export const CreateSkillVersionBody = Schema.Struct({ + "files": Schema.Union([ + Schema.Array(Schema.String.annotate({ "format": "binary" })).annotate({ + "description": "Skill files to upload (directory upload) or a single zip file." + }).check(Schema.isMaxLength(500)), + Schema.String.annotate({ "description": "Skill zip file to upload.", "format": "binary" }) + ], { mode: "oneOf" }), + "default": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to set this version as the default." }) + ) +}).annotate({ "title": "Create skill version request", "description": "Uploads a new immutable version of a skill." }) +export type DeletedSkillVersionResource = { + readonly "object": "skill.version.deleted" + readonly "deleted": boolean + readonly "id": string + readonly "version": string +} +export const DeletedSkillVersionResource = Schema.Struct({ + "object": Schema.Literal("skill.version.deleted"), + "deleted": Schema.Boolean, + "id": Schema.String, + "version": Schema.String.annotate({ "description": "The deleted skill version." }) +}) +export type ChatSessionResource = { + readonly "id": string + readonly "object": "chatkit.session" + readonly "expires_at": number + readonly "client_secret": string + readonly "workflow": { + readonly "id": string + readonly "version": string | null + readonly "state_variables": {} | null + readonly "tracing": { readonly "enabled": boolean } + } + readonly "user": string + readonly "rate_limits": { readonly "max_requests_per_1_minute": number } + readonly "max_requests_per_1_minute": number + readonly "status": "active" | "expired" | "cancelled" + readonly "chatkit_configuration": { + readonly "automatic_thread_titling": { readonly "enabled": boolean } + readonly "file_upload": { + readonly "enabled": boolean + readonly "max_file_size": number | null + readonly "max_files": number | null + } + readonly "history": { readonly "enabled": boolean; readonly "recent_threads": number | null } + } +} +export const ChatSessionResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier for the ChatKit session." }), + "object": Schema.Literal("chatkit.session").annotate({ + "description": "Type discriminator that is always `chatkit.session`." + }), + "expires_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the session expires.", + "format": "unixtime" + }).check(Schema.isInt()), + "client_secret": Schema.String.annotate({ + "description": "Ephemeral client secret that authenticates session requests." + }), + "workflow": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the workflow backing the session." }), + "version": Schema.Union([ + Schema.String.annotate({ + "description": + "Specific workflow version used for the session. Defaults to null when using the latest deployment." + }), + Schema.Null + ]), + "state_variables": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "State variable key-value pairs applied when invoking the workflow. Defaults to null when no overrides were provided." + }), + Schema.Null + ]), + "tracing": Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Indicates whether tracing is enabled." }) + }).annotate({ "title": "Tracing Configuration", "description": "Tracing settings applied to the workflow." }) + }).annotate({ "title": "Workflow", "description": "Workflow metadata for the session." }), + "user": Schema.String.annotate({ "description": "User identifier associated with the session." }), + "rate_limits": Schema.Struct({ + "max_requests_per_1_minute": Schema.Number.annotate({ + "description": "Maximum allowed requests per one-minute window." + }).check(Schema.isInt()) + }).annotate({ "title": "Rate limits", "description": "Resolved rate limit values." }), + "max_requests_per_1_minute": Schema.Number.annotate({ + "description": "Convenience copy of the per-minute request limit." + }).check(Schema.isInt()), + "status": Schema.Literals(["active", "expired", "cancelled"]).annotate({ + "description": "Current lifecycle state of the session." + }), + "chatkit_configuration": Schema.Struct({ + "automatic_thread_titling": Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Whether automatic thread titling is enabled." }) + }).annotate({ "title": "Automatic thread titling", "description": "Automatic thread titling preferences." }), + "file_upload": Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Indicates if uploads are enabled for the session." }), + "max_file_size": Schema.Union([ + Schema.Number.annotate({ "description": "Maximum upload size in megabytes." }).check(Schema.isInt()), + Schema.Null + ]), + "max_files": Schema.Union([ + Schema.Number.annotate({ "description": "Maximum number of uploads allowed during the session." }).check( + Schema.isInt() + ), + Schema.Null + ]) + }).annotate({ "title": "File upload settings", "description": "Upload settings for the session." }), + "history": Schema.Struct({ + "enabled": Schema.Boolean.annotate({ "description": "Indicates if chat history is persisted for the session." }), + "recent_threads": Schema.Union([ + Schema.Number.annotate({ + "description": + "Number of prior threads surfaced in history views. Defaults to null when all history is retained." + }).check(Schema.isInt()), + Schema.Null + ]) + }).annotate({ "title": "History settings", "description": "History retention configuration." }) + }).annotate({ + "title": "ChatKit configuration", + "description": "Resolved ChatKit feature configuration for the session." + }) +}).annotate({ + "title": "The chat session object", + "description": "Represents a ChatKit session and its resolved configuration." +}) +export type CreateChatSessionBody = { + readonly "workflow": { + readonly "id": string + readonly "version"?: string + readonly "state_variables"?: {} + readonly "tracing"?: { readonly "enabled"?: boolean } + } + readonly "user": string + readonly "expires_after"?: { readonly "anchor": "created_at"; readonly "seconds": number } + readonly "rate_limits"?: { readonly "max_requests_per_1_minute"?: number } + readonly "chatkit_configuration"?: { + readonly "automatic_thread_titling"?: { readonly "enabled"?: boolean } + readonly "file_upload"?: { + readonly "enabled"?: boolean + readonly "max_file_size"?: number + readonly "max_files"?: number + } + readonly "history"?: { readonly "enabled"?: boolean; readonly "recent_threads"?: number } + } +} +export const CreateChatSessionBody = Schema.Struct({ + "workflow": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier for the workflow invoked by the session." }), + "version": Schema.optionalKey( + Schema.String.annotate({ + "description": "Specific workflow version to run. Defaults to the latest deployed version." + }) + ), + "state_variables": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "State variables forwarded to the workflow. Keys may be up to 64 characters, values must be primitive types, and the map defaults to an empty object." + }).check(Schema.isMaxProperties(64)) + ), + "tracing": Schema.optionalKey( + Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether tracing is enabled during the session. Defaults to true." }) + ) + }).annotate({ + "title": "Tracing Configuration", + "description": + "Optional tracing overrides for the workflow invocation. When omitted, tracing is enabled by default." + }) + ) + }).annotate({ "title": "Workflow settings", "description": "Workflow that powers the session." }), + "user": Schema.String.annotate({ + "description": + "A free-form string that identifies your end user; ensures this Session can access other objects that have the same `user` scope." + }).check(Schema.isMinLength(1)), + "expires_after": Schema.optionalKey( + Schema.Struct({ + "anchor": Schema.Literal("created_at").annotate({ + "description": "Base timestamp used to calculate expiration. Currently fixed to `created_at`." + }), + "seconds": Schema.Number.annotate({ + "description": "Number of seconds after the anchor when the session expires.", + "format": "int64" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(600)) + }).annotate({ + "title": "Expiration overrides", + "description": "Optional override for session expiration timing in seconds from creation. Defaults to 10 minutes." + }) + ), + "rate_limits": Schema.optionalKey( + Schema.Struct({ + "max_requests_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum number of requests allowed per minute for the session. Defaults to 10." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) + ) + }).annotate({ + "title": "Rate limit overrides", + "description": "Optional override for per-minute request limits. When omitted, defaults to 10." + }) + ), + "chatkit_configuration": Schema.optionalKey( + Schema.Struct({ + "automatic_thread_titling": Schema.optionalKey( + Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Enable automatic thread title generation. Defaults to true." }) + ) + }).annotate({ + "title": "Automatic thread titling configuration", + "description": + "Configuration for automatic thread titling. When omitted, automatic thread titling is enabled by default." + }) + ), + "file_upload": Schema.optionalKey( + Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Enable uploads for this session. Defaults to false." }) + ), + "max_file_size": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum size in megabytes for each uploaded file. Defaults to 512 MB, which is the maximum allowable size." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(512)) + ), + "max_files": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum number of files that can be uploaded to the session. Defaults to 10." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) + ) + }).annotate({ + "title": "File upload configuration", + "description": + "Configuration for upload enablement and limits. When omitted, uploads are disabled by default (max_files 10, max_file_size 512 MB)." + }) + ), + "history": Schema.optionalKey( + Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Enables chat users to access previous ChatKit threads. Defaults to true." + }) + ), + "recent_threads": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Number of recent ChatKit threads users have access to. Defaults to unlimited when unset." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)) + ) + }).annotate({ + "title": "Chat history configuration", + "description": + "Configuration for chat history retention. When omitted, history is enabled by default with no limit on recent_threads (null)." + }) + ) + }).annotate({ + "title": "ChatKit configuration overrides", + "description": "Optional overrides for ChatKit runtime configuration features" + }) + ) +}).annotate({ + "title": "Create chat session request", + "description": "Parameters for provisioning a new ChatKit session." +}) +export type UserMessageInputText = { readonly "type": "input_text"; readonly "text": string } +export const UserMessageInputText = Schema.Struct({ + "type": Schema.Literal("input_text").annotate({ "description": "Type discriminator that is always `input_text`." }), + "text": Schema.String.annotate({ "description": "Plain-text content supplied by the user." }) +}).annotate({ "title": "User message input", "description": "Text block that a user contributed to the thread." }) +export type UserMessageQuotedText = { readonly "type": "quoted_text"; readonly "text": string } +export const UserMessageQuotedText = Schema.Struct({ + "type": Schema.Literal("quoted_text").annotate({ "description": "Type discriminator that is always `quoted_text`." }), + "text": Schema.String.annotate({ "description": "Quoted text content." }) +}).annotate({ + "title": "User message quoted text", + "description": "Quoted snippet that the user referenced in their message." +}) +export type Attachment = { + readonly "type": "image" | "file" + readonly "id": string + readonly "name": string + readonly "mime_type": string + readonly "preview_url": string | null +} +export const Attachment = Schema.Struct({ + "type": Schema.Literals(["image", "file"]).annotate({ "description": "Attachment discriminator." }), + "id": Schema.String.annotate({ "description": "Identifier for the attachment." }), + "name": Schema.String.annotate({ "description": "Original display name for the attachment." }), + "mime_type": Schema.String.annotate({ "description": "MIME type of the attachment." }), + "preview_url": Schema.Union([ + Schema.String.annotate({ "description": "Preview URL for rendering the attachment inline.", "format": "uri" }), + Schema.Null + ]) +}).annotate({ "title": "Attachment", "description": "Attachment metadata included on thread items." }) +export type FileAnnotation = { + readonly "type": "file" + readonly "source": { readonly "type": "file"; readonly "filename": string } +} +export const FileAnnotation = Schema.Struct({ + "type": Schema.Literal("file").annotate({ + "description": "Type discriminator that is always `file` for this annotation." + }), + "source": Schema.Struct({ + "type": Schema.Literal("file").annotate({ "description": "Type discriminator that is always `file`." }), + "filename": Schema.String.annotate({ "description": "Filename referenced by the annotation." }) + }).annotate({ "title": "File annotation source", "description": "File attachment referenced by the annotation." }) +}).annotate({ "title": "File annotation", "description": "Annotation that references an uploaded file." }) +export type UrlAnnotation = { + readonly "type": "url" + readonly "source": { readonly "type": "url"; readonly "url": string } +} +export const UrlAnnotation = Schema.Struct({ + "type": Schema.Literal("url").annotate({ + "description": "Type discriminator that is always `url` for this annotation." + }), + "source": Schema.Struct({ + "type": Schema.Literal("url").annotate({ "description": "Type discriminator that is always `url`." }), + "url": Schema.String.annotate({ "description": "URL referenced by the annotation.", "format": "uri" }) + }).annotate({ "title": "URL annotation source", "description": "URL referenced by the annotation." }) +}).annotate({ "title": "URL annotation", "description": "Annotation that references a URL." }) +export type WidgetMessageItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.widget" + readonly "widget": string +} +export const WidgetMessageItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.widget").annotate({ + "description": "Type discriminator that is always `chatkit.widget`." + }), + "widget": Schema.String.annotate({ "description": "Serialized widget payload rendered in the UI." }) +}).annotate({ "title": "Widget message", "description": "Thread item that renders a widget payload." }) +export type ClientToolCallItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.client_tool_call" + readonly "status": "in_progress" | "completed" + readonly "call_id": string + readonly "name": string + readonly "arguments": string + readonly "output": string | null +} +export const ClientToolCallItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.client_tool_call").annotate({ + "description": "Type discriminator that is always `chatkit.client_tool_call`." + }), + "status": Schema.Literals(["in_progress", "completed"]).annotate({ + "description": "Execution status for the tool call." + }), + "call_id": Schema.String.annotate({ "description": "Identifier for the client tool call." }), + "name": Schema.String.annotate({ "description": "Tool name that was invoked." }), + "arguments": Schema.String.annotate({ "description": "JSON-encoded arguments that were sent to the tool." }), + "output": Schema.Union([ + Schema.String.annotate({ + "description": "JSON-encoded output captured from the tool. Defaults to null while execution is in progress." + }), + Schema.Null + ]) +}).annotate({ + "title": "Client tool call", + "description": "Record of a client side tool invocation initiated by the assistant." +}) +export type TaskItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.task" + readonly "task_type": "custom" | "thought" + readonly "heading": string | null + readonly "summary": string | null +} +export const TaskItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.task").annotate({ + "description": "Type discriminator that is always `chatkit.task`." + }), + "task_type": Schema.Literals(["custom", "thought"]).annotate({ "description": "Subtype for the task." }), + "heading": Schema.Union([ + Schema.String.annotate({ "description": "Optional heading for the task. Defaults to null when not provided." }), + Schema.Null + ]), + "summary": Schema.Union([ + Schema.String.annotate({ + "description": "Optional summary that describes the task. Defaults to null when omitted." + }), + Schema.Null + ]) +}).annotate({ + "title": "Task item", + "description": "Task emitted by the workflow to show progress and status updates." +}) +export type TaskGroupTask = { + readonly "type": "custom" | "thought" + readonly "heading": string | null + readonly "summary": string | null +} +export const TaskGroupTask = Schema.Struct({ + "type": Schema.Literals(["custom", "thought"]).annotate({ "description": "Subtype for the grouped task." }), + "heading": Schema.Union([ + Schema.String.annotate({ + "description": "Optional heading for the grouped task. Defaults to null when not provided." + }), + Schema.Null + ]), + "summary": Schema.Union([ + Schema.String.annotate({ + "description": "Optional summary that describes the grouped task. Defaults to null when omitted." + }), + Schema.Null + ]) +}).annotate({ "title": "Task group task", "description": "Task entry that appears within a TaskGroup." }) +export type ActiveStatus = { readonly "type": "active" } +export const ActiveStatus = Schema.Struct({ + "type": Schema.Literal("active").annotate({ "description": "Status discriminator that is always `active`." }) +}).annotate({ "title": "Active thread status", "description": "Indicates that a thread is active." }) +export type LockedStatus = { readonly "type": "locked"; readonly "reason": string | null } +export const LockedStatus = Schema.Struct({ + "type": Schema.Literal("locked").annotate({ "description": "Status discriminator that is always `locked`." }), + "reason": Schema.Union([ + Schema.String.annotate({ + "description": "Reason that the thread was locked. Defaults to null when no reason is recorded." + }), + Schema.Null + ]) +}).annotate({ + "title": "Locked thread status", + "description": "Indicates that a thread is locked and cannot accept new input." +}) +export type ClosedStatus = { readonly "type": "closed"; readonly "reason": string | null } +export const ClosedStatus = Schema.Struct({ + "type": Schema.Literal("closed").annotate({ "description": "Status discriminator that is always `closed`." }), + "reason": Schema.Union([ + Schema.String.annotate({ + "description": "Reason that the thread was closed. Defaults to null when no reason is recorded." + }), + Schema.Null + ]) +}).annotate({ "title": "Closed thread status", "description": "Indicates that a thread has been closed." }) +export type DeletedThreadResource = { + readonly "id": string + readonly "object": "chatkit.thread.deleted" + readonly "deleted": boolean +} +export const DeletedThreadResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the deleted thread." }), + "object": Schema.Literal("chatkit.thread.deleted").annotate({ + "description": "Type discriminator that is always `chatkit.thread.deleted`." + }), + "deleted": Schema.Boolean.annotate({ "description": "Indicates that the thread has been deleted." }) +}).annotate({ "title": "Deleted thread", "description": "Confirmation payload returned after deleting a thread." }) +export type ResponseKeepAliveEvent = { readonly "type": "keepalive"; readonly "sequence_number": number } +export const ResponseKeepAliveEvent = Schema.Struct({ + "type": Schema.Literal("keepalive").annotate({ + "description": "The type of the keepalive event. Always `keepalive`." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this keepalive event." }).check( + Schema.isInt() + ) +}).annotate({ "title": "Keep alive", "description": "A keepalive event emitted during long-running response streams." }) +export type ResponseApplyPatchCallOperationDiffDeltaEvent = { + readonly "type": "response.apply_patch_call_operation_diff.delta" + readonly "sequence_number": number + readonly "output_index": number + readonly "item_id": string + readonly "delta": string +} +export const ResponseApplyPatchCallOperationDiffDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.apply_patch_call_operation_diff.delta").annotate({ + "description": "The event type identifier." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "output_index": Schema.Number.annotate({ "description": "The index of the output this delta applies to." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ + "description": "Unique identifier for the API item associated with this event." + }), + "delta": Schema.String.annotate({ "description": "The incremental diff data for the apply_patch tool call." }) +}).annotate({ + "title": "ResponseApplyPatchCallOperationDiffDelta", + "description": "Event representing a delta for an apply_patch tool call operation diff." +}) +export type ResponseApplyPatchCallOperationDiffDoneEvent = { + readonly "type": "response.apply_patch_call_operation_diff.done" + readonly "sequence_number": number + readonly "output_index": number + readonly "item_id": string + readonly "delta"?: string +} +export const ResponseApplyPatchCallOperationDiffDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.apply_patch_call_operation_diff.done").annotate({ + "description": "The event type identifier." + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "output_index": Schema.Number.annotate({ "description": "The index of the output this event applies to." }).check( + Schema.isInt() + ), + "item_id": Schema.String.annotate({ + "description": "Unique identifier for the API item associated with this event." + }), + "delta": Schema.optionalKey( + Schema.String.annotate({ "description": "The final diff data for the apply_patch tool call." }) + ) +}).annotate({ + "title": "ResponseApplyPatchCallOperationDiffDone", + "description": "Event indicating that the operation diff for an apply_patch tool call is complete." +}) +export type ApiKeyList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "first_id"?: string | null + readonly "last_id"?: string | null +} +export const ApiKeyList = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(AdminApiKey), + "has_more": Schema.Boolean, + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}) +export type RoleListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next": string | null +} +export const RoleListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "Always `list`." }), + "data": Schema.Array(AssignedRoleDetails).annotate({ + "description": "Role assignments returned in the current page." + }), + "has_more": Schema.Boolean.annotate({ + "description": "Whether additional assignments are available when paginating." + }), + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Cursor to fetch the next page of results, or `null` when there are no more assignments." + }) +}).annotate({ "description": "Paginated list of roles assigned to a principal." }) +export type AuditLogActorApiKey = { + readonly "id"?: string + readonly "type"?: "user" | "service_account" + readonly "user"?: AuditLogActorUser + readonly "service_account"?: AuditLogActorServiceAccount +} +export const AuditLogActorApiKey = Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The tracking id of the API key." })), + "type": Schema.optionalKey( + Schema.Literals(["user", "service_account"]).annotate({ + "description": "The type of API key. Can be either `user` or `service_account`." + }) + ), + "user": Schema.optionalKey(AuditLogActorUser), + "service_account": Schema.optionalKey(AuditLogActorServiceAccount) +}).annotate({ "description": "The API Key used to perform the audit logged action." }) +export type AuditLogActorSession = { readonly "user"?: AuditLogActorUser; readonly "ip_address"?: string } +export const AuditLogActorSession = Schema.Struct({ + "user": Schema.optionalKey(AuditLogActorUser), + "ip_address": Schema.optionalKey( + Schema.String.annotate({ "description": "The IP address from which the action was performed." }) + ) +}).annotate({ "description": "The session in which the audit logged action was performed." }) +export type ChatCompletionAllowedToolsChoice = { + readonly "type": "allowed_tools" + readonly "allowed_tools": ChatCompletionAllowedTools +} +export const ChatCompletionAllowedToolsChoice = Schema.Struct({ + "type": Schema.Literal("allowed_tools").annotate({ + "description": "Allowed tool configuration type. Always `allowed_tools`." + }), + "allowed_tools": ChatCompletionAllowedTools +}).annotate({ + "title": "Allowed tools", + "description": "Constrains the tools available to the model to a pre-defined set.\n" +}) +export type ChatCompletionMessageToolCalls = ReadonlyArray< + ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall +> +export const ChatCompletionMessageToolCalls = Schema.Array( + Schema.Union([ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall], { mode: "oneOf" }) +).annotate({ "description": "The tool calls generated by the model, such as function calls." }) +export type ChatCompletionStreamResponseDelta = { + readonly "content"?: string | null + readonly "function_call"?: { readonly "arguments"?: string; readonly "name"?: string } + readonly "tool_calls"?: ReadonlyArray + readonly "role"?: "developer" | "system" | "user" | "assistant" | "tool" + readonly "refusal"?: string | null +} +export const ChatCompletionStreamResponseDelta = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The contents of the chunk message." }), Schema.Null]) + ), + "function_call": Schema.optionalKey( + Schema.Struct({ + "arguments": Schema.optionalKey(Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function to call." })) + }).annotate({ + "description": + "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model." + }) + ), + "tool_calls": Schema.optionalKey(Schema.Array(ChatCompletionMessageToolCallChunk)), + "role": Schema.optionalKey( + Schema.Literals(["developer", "system", "user", "assistant", "tool"]).annotate({ + "description": "The role of the author of this message." + }) + ), + "refusal": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The refusal message generated by the model." }), + Schema.Null + ]) + ) +}).annotate({ "description": "A chat completion delta generated by streamed model responses." }) +export type ChatCompletionRequestAssistantMessageContentPart = + | ChatCompletionRequestMessageContentPartText + | ChatCompletionRequestMessageContentPartRefusal +export const ChatCompletionRequestAssistantMessageContentPart = Schema.Union([ + ChatCompletionRequestMessageContentPartText, + ChatCompletionRequestMessageContentPartRefusal +], { mode: "oneOf" }) +export type ChatCompletionRequestDeveloperMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "developer" + readonly "name"?: string +} +export const ChatCompletionRequestDeveloperMessage = Schema.Struct({ + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The contents of the developer message." }), + Schema.Array(ChatCompletionRequestMessageContentPartText).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type. For developer messages, only type `text` is supported." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ "description": "The contents of the developer message." }), + "role": Schema.Literal("developer").annotate({ + "description": "The role of the messages author, in this case `developer`." + }), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional name for the participant. Provides the model information to differentiate between participants of the same role." + }) + ) +}).annotate({ + "title": "Developer message", + "description": + "Developer-provided instructions that the model should follow, regardless of\nmessages sent by the user. With o1 models and newer, `developer` messages\nreplace the previous `system` messages.\n" +}) +export type ChatCompletionRequestSystemMessageContentPart = ChatCompletionRequestMessageContentPartText +export const ChatCompletionRequestSystemMessageContentPart = Schema.Union( + [ChatCompletionRequestMessageContentPartText], + { mode: "oneOf" } +) +export type ChatCompletionRequestToolMessageContentPart = ChatCompletionRequestMessageContentPartText +export const ChatCompletionRequestToolMessageContentPart = Schema.Union([ChatCompletionRequestMessageContentPartText], { + mode: "oneOf" +}) +export type ChatCompletionRequestUserMessageContentPart = + | ChatCompletionRequestMessageContentPartText + | ChatCompletionRequestMessageContentPartImage + | ChatCompletionRequestMessageContentPartAudio + | ChatCompletionRequestMessageContentPartFile +export const ChatCompletionRequestUserMessageContentPart = Schema.Union([ + ChatCompletionRequestMessageContentPartText, + ChatCompletionRequestMessageContentPartImage, + ChatCompletionRequestMessageContentPartAudio, + ChatCompletionRequestMessageContentPartFile +], { mode: "oneOf" }) +export type PredictionContent = { + readonly "type": "content" + readonly "content": string | ReadonlyArray +} +export const PredictionContent = Schema.Struct({ + "type": Schema.Literal("content").annotate({ + "description": "The type of the predicted content you want to provide. This type is\ncurrently always `content`.\n" + }), + "content": Schema.Union([ + Schema.String.annotate({ + "title": "Text content", + "description": + "The content used for a Predicted Output. This is often the\ntext of a file you are regenerating with minor changes.\n" + }), + Schema.Array(ChatCompletionRequestMessageContentPartText).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type. Supported options differ based on the [model](/docs/models) being used to generate the response. Can contain text inputs." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ + "description": + "The content that should be matched when generating a model response.\nIf generated tokens would match this content, the entire model response\ncan be returned much more quickly.\n" + }) +}).annotate({ + "title": "Static Content", + "description": "Static predicted output content, such as the content of a text file that is\nbeing regenerated.\n" +}) +export type CompoundFilter = { + readonly "type": "and" | "or" + readonly "filters": ReadonlyArray +} +export const CompoundFilter = Schema.Struct({ + "type": Schema.Literals(["and", "or"]).annotate({ "description": "Type of operation: `and` or `or`." }), + "filters": Schema.Array(Schema.Union([ComparisonFilter, Schema.Unknown], { mode: "oneOf" })).annotate({ + "description": "Array of filters to combine. Items can be `ComparisonFilter` or `CompoundFilter`." + }) +}).annotate({ "title": "Compound Filter", "description": "Combine multiple filters using `and` or `or`." }) +export type CreateCompletionResponse = { + readonly "id": string + readonly "choices": ReadonlyArray< + { + readonly "finish_reason": "stop" | "length" | "content_filter" + readonly "index": number + readonly "logprobs": { + readonly "text_offset"?: ReadonlyArray + readonly "token_logprobs"?: ReadonlyArray + readonly "tokens"?: ReadonlyArray + readonly "top_logprobs"?: ReadonlyArray<{}> + } | null + readonly "text": string + } + > + readonly "created": number + readonly "model": string + readonly "system_fingerprint"?: string + readonly "object": "text_completion" + readonly "usage"?: CompletionUsage +} +export const CreateCompletionResponse = Schema.Struct({ + "id": Schema.String.annotate({ "description": "A unique identifier for the completion." }), + "choices": Schema.Array(Schema.Struct({ + "finish_reason": Schema.Literals(["stop", "length", "content_filter"]).annotate({ + "description": + "The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence,\n`length` if the maximum number of tokens specified in the request was reached,\nor `content_filter` if content was omitted due to a flag from our content filters.\n" + }), + "index": Schema.Number.check(Schema.isInt()), + "logprobs": Schema.Union([ + Schema.Struct({ + "text_offset": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isInt()))), + "token_logprobs": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isFinite()))), + "tokens": Schema.optionalKey(Schema.Array(Schema.String)), + "top_logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({}))) + }), + Schema.Null + ]), + "text": Schema.String + })).annotate({ "description": "The list of completion choices the model generated for the input prompt." }), + "created": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the completion was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ "description": "The model used for completion." }), + "system_fingerprint": Schema.optionalKey( + Schema.String.annotate({ + "description": + "This fingerprint represents the backend configuration that the model runs with.\n\nCan be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism.\n" + }) + ), + "object": Schema.Literal("text_completion").annotate({ + "description": "The object type, which is always \"text_completion\"" + }), + "usage": Schema.optionalKey(CompletionUsage) +}).annotate({ + "description": + "Represents a completion response from the API. Note: both the streamed and non-streamed response objects share the same shape (unlike the chat endpoint).\n" +}) +export type ContainerFileListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ContainerFileListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be 'list'." }), + "data": Schema.Array(ContainerFileResource).annotate({ "description": "A list of container files." }), + "first_id": Schema.String.annotate({ "description": "The ID of the first file in the list." }), + "last_id": Schema.String.annotate({ "description": "The ID of the last file in the list." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more files available." }) +}) +export type ContainerListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ContainerListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be 'list'." }), + "data": Schema.Array(ContainerResource).annotate({ "description": "A list of containers." }), + "first_id": Schema.String.annotate({ "description": "The ID of the first container in the list." }), + "last_id": Schema.String.annotate({ "description": "The ID of the last container in the list." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more containers available." }) +}) +export type CreateEmbeddingResponse = { + readonly "data": ReadonlyArray + readonly "model": string + readonly "object": "list" + readonly "usage": { readonly "prompt_tokens": number; readonly "total_tokens": number } +} +export const CreateEmbeddingResponse = Schema.Struct({ + "data": Schema.Array(Embedding).annotate({ "description": "The list of embeddings generated by the model." }), + "model": Schema.String.annotate({ "description": "The name of the model used to generate the embedding." }), + "object": Schema.Literal("list").annotate({ "description": "The object type, which is always \"list\"." }), + "usage": Schema.Struct({ + "prompt_tokens": Schema.Number.annotate({ "description": "The number of tokens used by the prompt." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used by the request." }).check( + Schema.isInt() + ) + }).annotate({ "description": "The usage information for the request." }) +}) +export type ErrorEvent = { readonly "event": "error"; readonly "data": Error } +export const ErrorEvent = Schema.Struct({ "event": Schema.Literal("error"), "data": Error }).annotate({ + "description": + "Occurs when an [error](/docs/guides/error-codes#api-errors) occurs. This can happen due to an internal server error or a timeout." +}) +export type ErrorResponse = { readonly "error": Error } +export const ErrorResponse = Schema.Struct({ "error": Error }) +export type EvalRunOutputItem = { + readonly "object": "eval.run.output_item" + readonly "id": string + readonly "run_id": string + readonly "eval_id": string + readonly "created_at": number + readonly "status": string + readonly "datasource_item_id": number + readonly "datasource_item": {} + readonly "results": ReadonlyArray + readonly "sample": { + readonly "input": ReadonlyArray<{ readonly "role": string; readonly "content": string }> + readonly "output": ReadonlyArray<{ readonly "role"?: string; readonly "content"?: string }> + readonly "finish_reason": string + readonly "model": string + readonly "usage": { + readonly "total_tokens": number + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "cached_tokens": number + } + readonly "error": EvalApiError + readonly "temperature": number + readonly "max_completion_tokens": number + readonly "top_p": number + readonly "seed": number + } +} +export const EvalRunOutputItem = Schema.Struct({ + "object": Schema.Literal("eval.run.output_item").annotate({ + "description": "The type of the object. Always \"eval.run.output_item\"." + }), + "id": Schema.String.annotate({ "description": "Unique identifier for the evaluation run output item." }), + "run_id": Schema.String.annotate({ + "description": "The identifier of the evaluation run associated with this output item." + }), + "eval_id": Schema.String.annotate({ "description": "The identifier of the evaluation group." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the evaluation run was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "status": Schema.String.annotate({ "description": "The status of the evaluation run." }), + "datasource_item_id": Schema.Number.annotate({ "description": "The identifier for the data source item." }).check( + Schema.isInt() + ), + "datasource_item": Schema.Struct({}).annotate({ "description": "Details of the input data source item." }), + "results": Schema.Array(EvalRunOutputItemResult).annotate({ + "description": "A list of grader results for this output item." + }), + "sample": Schema.Struct({ + "input": Schema.Array( + Schema.Struct({ + "role": Schema.String.annotate({ + "description": "The role of the message sender (e.g., system, user, developer)." + }), + "content": Schema.String.annotate({ "description": "The content of the message." }) + }).annotate({ "description": "An input message." }) + ).annotate({ "description": "An array of input messages." }), + "output": Schema.Array( + Schema.Struct({ + "role": Schema.optionalKey( + Schema.String.annotate({ + "description": "The role of the message (e.g. \"system\", \"assistant\", \"user\")." + }) + ), + "content": Schema.optionalKey(Schema.String.annotate({ "description": "The content of the message." })) + }) + ).annotate({ "description": "An array of output messages." }), + "finish_reason": Schema.String.annotate({ "description": "The reason why the sample generation was finished." }), + "model": Schema.String.annotate({ "description": "The model used for generating the sample." }), + "usage": Schema.Struct({ + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used." }).check( + Schema.isInt() + ), + "completion_tokens": Schema.Number.annotate({ "description": "The number of completion tokens generated." }) + .check(Schema.isInt()), + "prompt_tokens": Schema.Number.annotate({ "description": "The number of prompt tokens used." }).check( + Schema.isInt() + ), + "cached_tokens": Schema.Number.annotate({ "description": "The number of tokens retrieved from cache." }).check( + Schema.isInt() + ) + }).annotate({ "description": "Token usage details for the sample." }), + "error": EvalApiError, + "temperature": Schema.Number.annotate({ "description": "The sampling temperature used." }).check(Schema.isFinite()), + "max_completion_tokens": Schema.Number.annotate({ + "description": "The maximum number of tokens allowed for completion." + }).check(Schema.isInt()), + "top_p": Schema.Number.annotate({ "description": "The top_p value used for sampling." }).check(Schema.isFinite()), + "seed": Schema.Number.annotate({ "description": "The seed used for generating the sample." }).check(Schema.isInt()) + }).annotate({ "description": "A sample containing the input and output of the evaluation run." }) +}).annotate({ "title": "EvalRunOutputItem", "description": "A schema representing an evaluation run output item.\n" }) +export type CreateFileRequest = { + readonly "file": string + readonly "purpose": "assistants" | "batch" | "fine-tune" | "vision" | "user_data" | "evals" + readonly "expires_after"?: FileExpirationAfter +} +export const CreateFileRequest = Schema.Struct({ + "file": Schema.String.annotate({ + "description": "The File object (not file name) to be uploaded.\n", + "format": "binary" + }), + "purpose": Schema.Literals(["assistants", "batch", "fine-tune", "vision", "user_data", "evals"]).annotate({ + "description": + "The intended purpose of the uploaded file. One of:\n- `assistants`: Used in the Assistants API\n- `batch`: Used in the Batch API\n- `fine-tune`: Used for fine-tuning\n- `vision`: Images used for vision fine-tuning\n- `user_data`: Flexible file type for any purpose\n- `evals`: Used for eval data sets\n" + }), + "expires_after": Schema.optionalKey(FileExpirationAfter) +}) +export type CreateUploadRequest = { + readonly "filename": string + readonly "purpose": "assistants" | "batch" | "fine-tune" | "vision" + readonly "bytes": number + readonly "mime_type": string + readonly "expires_after"?: FileExpirationAfter +} +export const CreateUploadRequest = Schema.Struct({ + "filename": Schema.String.annotate({ "description": "The name of the file to upload.\n" }), + "purpose": Schema.Literals(["assistants", "batch", "fine-tune", "vision"]).annotate({ + "description": + "The intended purpose of the uploaded file.\n\nSee the [documentation on File\npurposes](/docs/api-reference/files/create#files-create-purpose).\n" + }), + "bytes": Schema.Number.annotate({ "description": "The number of bytes in the file you are uploading.\n" }).check( + Schema.isInt() + ), + "mime_type": Schema.String.annotate({ + "description": + "The MIME type of the file.\n\n\nThis must fall within the supported MIME types for your file purpose. See\nthe supported MIME types for assistants and vision.\n" + }), + "expires_after": Schema.optionalKey(FileExpirationAfter) +}) +export type FileSearchRankingOptions = { readonly "ranker"?: FileSearchRanker; readonly "score_threshold": number } +export const FileSearchRankingOptions = Schema.Struct({ + "ranker": Schema.optionalKey(FileSearchRanker), + "score_threshold": Schema.Number.annotate({ + "description": + "The score threshold for the file search. All values must be a floating point number between 0 and 1." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) +}).annotate({ + "title": "File search tool call ranking options", + "description": + "The ranking options for the file search. If not specified, the file search tool will use the `auto` ranker and a score_threshold of 0.\n\nSee the [file search tool documentation](/docs/assistants/tools/file-search#customizing-file-search-settings) for more information.\n" +}) +export type RunStepDetailsToolCallsFileSearchRankingOptionsObject = { + readonly "ranker": FileSearchRanker + readonly "score_threshold": number +} +export const RunStepDetailsToolCallsFileSearchRankingOptionsObject = Schema.Struct({ + "ranker": FileSearchRanker, + "score_threshold": Schema.Number.annotate({ + "description": + "The score threshold for the file search. All values must be a floating point number between 0 and 1." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) +}).annotate({ + "title": "File search tool call ranking options", + "description": "The ranking options for the file search." +}) +export type FineTuneDPOMethod = { readonly "hyperparameters"?: FineTuneDPOHyperparameters } +export const FineTuneDPOMethod = Schema.Struct({ "hyperparameters": Schema.optionalKey(FineTuneDPOHyperparameters) }) + .annotate({ "description": "Configuration for the DPO fine-tuning method." }) +export type FineTuneSupervisedMethod = { readonly "hyperparameters"?: FineTuneSupervisedHyperparameters } +export const FineTuneSupervisedMethod = Schema.Struct({ + "hyperparameters": Schema.optionalKey(FineTuneSupervisedHyperparameters) +}).annotate({ "description": "Configuration for the supervised fine-tuning method." }) +export type ListFineTuningCheckpointPermissionResponse = { + readonly "data": ReadonlyArray + readonly "object": "list" + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ListFineTuningCheckpointPermissionResponse = Schema.Struct({ + "data": Schema.Array(FineTuningCheckpointPermission), + "object": Schema.Literal("list"), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ListFineTuningJobCheckpointsResponse = { + readonly "data": ReadonlyArray + readonly "object": "list" + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ListFineTuningJobCheckpointsResponse = Schema.Struct({ + "data": Schema.Array(FineTuningJobCheckpoint), + "object": Schema.Literal("list"), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ListFineTuningJobEventsResponse = { + readonly "data": ReadonlyArray + readonly "object": "list" + readonly "has_more": boolean +} +export const ListFineTuningJobEventsResponse = Schema.Struct({ + "data": Schema.Array(FineTuningJobEvent), + "object": Schema.Literal("list"), + "has_more": Schema.Boolean +}) +export type ChatCompletionFunctions = { + readonly "description"?: string + readonly "name": string + readonly "parameters"?: FunctionParameters +} +export const ChatCompletionFunctions = Schema.Struct({ + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A description of what the function does, used by the model to choose when and how to call the function." + }) + ), + "name": Schema.String.annotate({ + "description": + "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." + }), + "parameters": Schema.optionalKey(FunctionParameters) +}) +export type FunctionObject = { + readonly "description"?: string + readonly "name": string + readonly "parameters"?: FunctionParameters + readonly "strict"?: boolean | null +} +export const FunctionObject = Schema.Struct({ + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A description of what the function does, used by the model to choose when and how to call the function." + }) + ), + "name": Schema.String.annotate({ + "description": + "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64." + }), + "parameters": Schema.optionalKey(FunctionParameters), + "strict": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to enable strict schema adherence when generating the function call. If set to true, the model will follow the exact schema defined in the `parameters` field. Only a subset of JSON Schema is supported when `strict` is `true`. Learn more about Structured Outputs in the [function calling guide](/docs/guides/function-calling)." + }), + Schema.Null + ])) +}) +export type GroupListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next": string | null +} +export const GroupListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "Always `list`." }), + "data": Schema.Array(GroupResponse).annotate({ "description": "Groups returned in the current page." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether additional groups are available when paginating." }), + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Cursor to fetch the next page of results, or `null` if there are no more results." + }) +}).annotate({ "description": "Paginated list of organization groups." }) +export type UserListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next": string | null +} +export const UserListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "Always `list`." }), + "data": Schema.Array(GroupUser).annotate({ "description": "Users in the current page." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether more users are available when paginating." }), + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Cursor to fetch the next page of results, or `null` when no further users are available." + }) +}).annotate({ "description": "Paginated list of user objects returned when inspecting group membership." }) +export type ProjectHostedToolPermissions = { + readonly "file_search": HostedToolPermission + readonly "web_search": HostedToolPermission + readonly "image_generation": HostedToolPermission + readonly "mcp": HostedToolPermission + readonly "code_interpreter": HostedToolPermission +} +export const ProjectHostedToolPermissions = Schema.Struct({ + "file_search": HostedToolPermission, + "web_search": HostedToolPermission, + "image_generation": HostedToolPermission, + "mcp": HostedToolPermission, + "code_interpreter": HostedToolPermission +}).annotate({ "description": "Represents hosted tool permissions for a project." }) +export type ProjectHostedToolPermissionsUpdateRequest = { + readonly "file_search"?: HostedToolPermissionUpdate | null + readonly "web_search"?: HostedToolPermissionUpdate | null + readonly "image_generation"?: HostedToolPermissionUpdate | null + readonly "mcp"?: HostedToolPermissionUpdate | null + readonly "code_interpreter"?: HostedToolPermissionUpdate | null +} +export const ProjectHostedToolPermissionsUpdateRequest = Schema.Struct({ + "file_search": Schema.optionalKey( + Schema.Union([HostedToolPermissionUpdate, Schema.Null]).annotate({ + "description": "The file search permission update." + }) + ), + "web_search": Schema.optionalKey( + Schema.Union([HostedToolPermissionUpdate, Schema.Null]).annotate({ + "description": "The web search permission update." + }) + ), + "image_generation": Schema.optionalKey( + Schema.Union([HostedToolPermissionUpdate, Schema.Null]).annotate({ + "description": "The image generation permission update." + }) + ), + "mcp": Schema.optionalKey( + Schema.Union([HostedToolPermissionUpdate, Schema.Null]).annotate({ "description": "The MCP permission update." }) + ), + "code_interpreter": Schema.optionalKey( + Schema.Union([HostedToolPermissionUpdate, Schema.Null]).annotate({ + "description": "The code interpreter permission update." + }) + ) +}) +export type ImageEditCompletedEvent = { + readonly "type": "image_edit.completed" + readonly "b64_json": string + readonly "created_at": number + readonly "size": "1024x1024" | "1024x1536" | "1536x1024" | "auto" + readonly "quality": "low" | "medium" | "high" | "auto" + readonly "background": "transparent" | "opaque" | "auto" + readonly "output_format": "png" | "webp" | "jpeg" + readonly "usage": ImagesUsage +} +export const ImageEditCompletedEvent = Schema.Struct({ + "type": Schema.Literal("image_edit.completed").annotate({ + "description": "The type of the event. Always `image_edit.completed`.\n" + }), + "b64_json": Schema.String.annotate({ + "description": "Base64-encoded final edited image data, suitable for rendering as an image.\n" + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp when the event was created.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "size": Schema.Literals(["1024x1024", "1024x1536", "1536x1024", "auto"]).annotate({ + "description": "The size of the edited image.\n" + }), + "quality": Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": "The quality setting for the edited image.\n" + }), + "background": Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": "The background setting for the edited image.\n" + }), + "output_format": Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format for the edited image.\n" + }), + "usage": ImagesUsage +}).annotate({ "description": "Emitted when image editing has completed and the final image is available.\n" }) +export type ImageGenCompletedEvent = { + readonly "type": "image_generation.completed" + readonly "b64_json": string + readonly "created_at": number + readonly "size": "1024x1024" | "1024x1536" | "1536x1024" | "auto" + readonly "quality": "low" | "medium" | "high" | "auto" + readonly "background": "transparent" | "opaque" | "auto" + readonly "output_format": "png" | "webp" | "jpeg" + readonly "usage": ImagesUsage +} +export const ImageGenCompletedEvent = Schema.Struct({ + "type": Schema.Literal("image_generation.completed").annotate({ + "description": "The type of the event. Always `image_generation.completed`.\n" + }), + "b64_json": Schema.String.annotate({ + "description": "Base64-encoded image data, suitable for rendering as an image.\n" + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp when the event was created.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "size": Schema.Literals(["1024x1024", "1024x1536", "1536x1024", "auto"]).annotate({ + "description": "The size of the generated image.\n" + }), + "quality": Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": "The quality setting for the generated image.\n" + }), + "background": Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": "The background setting for the generated image.\n" + }), + "output_format": Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format for the generated image.\n" + }), + "usage": ImagesUsage +}).annotate({ "description": "Emitted when image generation has completed and the final image is available.\n" }) +export type InviteListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const InviteListResponse = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The object type, which is always `list`" }), + "data": Schema.Array(Invite), + "first_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The first `invite_id` in the retrieved `list`" + }) + ), + "last_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The last `invite_id` in the retrieved `list`" + }) + ), + "has_more": Schema.Boolean.annotate({ + "description": "The `has_more` property is used for pagination to indicate there are additional results." + }) +}) +export type RealtimeServerEventConversationItemInputAudioTranscriptionCompleted = { + readonly "event_id": string + readonly "type": "conversation.item.input_audio_transcription.completed" + readonly "item_id": string + readonly "content_index": number + readonly "transcript": string + readonly "logprobs"?: ReadonlyArray | null + readonly "usage": { + readonly "type": "tokens" + readonly "input_tokens": number + readonly "input_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + readonly "output_tokens": number + readonly "total_tokens": number + } | { readonly "type": "duration"; readonly "seconds": number } +} +export const RealtimeServerEventConversationItemInputAudioTranscriptionCompleted = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.input_audio_transcription.completed").annotate({ + "description": "The event type, must be\n`conversation.item.input_audio_transcription.completed`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the item containing the audio that is being transcribed." + }), + "content_index": Schema.Number.annotate({ "description": "The index of the content part containing the audio." }) + .check(Schema.isInt()), + "transcript": Schema.String.annotate({ "description": "The transcribed text." }), + "logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Array(LogProbProperties).annotate({ "description": "The log probabilities of the transcription." }), + Schema.Null + ]) + ), + "usage": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("tokens").annotate({ + "description": "The type of the usage object. Always `tokens` for this variant." + }), + "input_tokens": Schema.Number.annotate({ "description": "Number of input tokens billed for this request." }) + .check(Schema.isInt()), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of text tokens billed for this request." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio tokens billed for this request." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the input tokens billed for this request." }) + ), + "output_tokens": Schema.Number.annotate({ "description": "Number of output tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (input + output)." }).check( + Schema.isInt() + ) + }).annotate({ + "title": "Token Usage", + "description": + "Usage statistics for the transcription, this is billed according to the ASR model's pricing rather than the realtime model's pricing." + }), + Schema.Struct({ + "type": Schema.Literal("duration").annotate({ + "description": "The type of the usage object. Always `duration` for this variant." + }), + "seconds": Schema.Number.annotate({ + "description": "Duration of the input audio in seconds.", + "format": "double" + }).check(Schema.isFinite()) + }).annotate({ + "title": "Duration Usage", + "description": + "Usage statistics for the transcription, this is billed according to the ASR model's pricing rather than the realtime model's pricing." + }) + ], { mode: "oneOf" }) +}).annotate({ + "description": + "This event is the output of audio transcription for user audio written to the\nuser audio buffer. Transcription begins when the input audio buffer is\ncommitted by the client or server (when VAD is enabled). Transcription runs\nasynchronously with Response creation, so this event may come before or after\nthe Response events.\n\nRealtime API models accept audio natively, and thus input transcription is a\nseparate process run on a separate ASR (Automatic Speech Recognition) model.\nThe transcript may diverge somewhat from the model's interpretation, and\nshould be treated as a rough guide.\n" +}) +export type RealtimeServerEventConversationItemInputAudioTranscriptionDelta = { + readonly "event_id": string + readonly "type": "conversation.item.input_audio_transcription.delta" + readonly "item_id": string + readonly "content_index"?: number + readonly "delta"?: string + readonly "logprobs"?: ReadonlyArray | null +} +export const RealtimeServerEventConversationItemInputAudioTranscriptionDelta = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.input_audio_transcription.delta").annotate({ + "description": "The event type, must be `conversation.item.input_audio_transcription.delta`." + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the item containing the audio that is being transcribed." + }), + "content_index": Schema.optionalKey( + Schema.Number.annotate({ "description": "The index of the content part in the item's content array." }).check( + Schema.isInt() + ) + ), + "delta": Schema.optionalKey(Schema.String.annotate({ "description": "The text delta." })), + "logprobs": Schema.optionalKey(Schema.Union([ + Schema.Array(LogProbProperties).annotate({ + "description": + "The log probabilities of the transcription. These can be enabled by configurating the session with `\"include\": [\"item.input_audio_transcription.logprobs\"]`. Each entry in the array corresponds a log probability of which token would be selected for this chunk of transcription. This can help to identify if it was possible there were multiple valid options for a given chunk of transcription." + }), + Schema.Null + ])) +}).annotate({ + "description": + "Returned when the text value of an input audio transcription content part is updated with incremental transcription results.\n" +}) +export type MCPListTools = { + readonly "type": "mcp_list_tools" + readonly "id": string + readonly "server_label": string + readonly "tools": ReadonlyArray + readonly "error"?: string | null +} +export const MCPListTools = Schema.Struct({ + "type": Schema.Literal("mcp_list_tools").annotate({ + "description": "The type of the item. Always `mcp_list_tools`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the list.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server.\n" }), + "tools": Schema.Array(MCPListToolsTool).annotate({ "description": "The tools available on the server.\n" }), + "error": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Error message if the server could not list tools.\n" }), + Schema.Null + ]) + ) +}).annotate({ "title": "MCP list tools", "description": "A list of tools available on an MCP server.\n" }) +export type RealtimeMCPListTools = { + readonly "type": "mcp_list_tools" + readonly "id"?: string + readonly "server_label": string + readonly "tools": ReadonlyArray +} +export const RealtimeMCPListTools = Schema.Struct({ + "type": Schema.Literal("mcp_list_tools").annotate({ + "description": "The type of the item. Always `mcp_list_tools`." + }), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the list." })), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server." }), + "tools": Schema.Array(MCPListToolsTool).annotate({ "description": "The tools available on the server." }) +}).annotate({ + "title": "Realtime MCP list tools", + "description": "A Realtime item listing tools available on an MCP server.\n" +}) +export type MCPTool = { + readonly "type": "mcp" + readonly "server_label": string + readonly "server_url"?: string + readonly "connector_id"?: + | "connector_dropbox" + | "connector_gmail" + | "connector_googlecalendar" + | "connector_googledrive" + | "connector_microsoftteams" + | "connector_outlookcalendar" + | "connector_outlookemail" + | "connector_sharepoint" + readonly "authorization"?: string + readonly "server_description"?: string + readonly "headers"?: {} | null + readonly "allowed_tools"?: ReadonlyArray | MCPToolFilter | null + readonly "require_approval"?: + | { readonly "always"?: MCPToolFilter; readonly "never"?: MCPToolFilter } + | "always" + | "never" + | null + readonly "defer_loading"?: boolean +} +export const MCPTool = Schema.Struct({ + "type": Schema.Literal("mcp").annotate({ "description": "The type of the MCP tool. Always `mcp`." }), + "server_label": Schema.String.annotate({ + "description": "A label for this MCP server, used to identify it in tool calls.\n" + }), + "server_url": Schema.optionalKey( + Schema.String.annotate({ + "description": "The URL for the MCP server. One of `server_url` or `connector_id` must be\nprovided.\n", + "format": "uri" + }) + ), + "connector_id": Schema.optionalKey( + Schema.Literals([ + "connector_dropbox", + "connector_gmail", + "connector_googlecalendar", + "connector_googledrive", + "connector_microsoftteams", + "connector_outlookcalendar", + "connector_outlookemail", + "connector_sharepoint" + ]).annotate({ + "description": + "Identifier for service connectors, like those available in ChatGPT. One of\n`server_url` or `connector_id` must be provided. Learn more about service\nconnectors [here](/docs/guides/tools-remote-mcp#connectors).\n\nCurrently supported `connector_id` values are:\n\n- Dropbox: `connector_dropbox`\n- Gmail: `connector_gmail`\n- Google Calendar: `connector_googlecalendar`\n- Google Drive: `connector_googledrive`\n- Microsoft Teams: `connector_microsoftteams`\n- Outlook Calendar: `connector_outlookcalendar`\n- Outlook Email: `connector_outlookemail`\n- SharePoint: `connector_sharepoint`\n" + }) + ), + "authorization": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An OAuth access token that can be used with a remote MCP server, either\nwith a custom MCP server URL or a service connector. Your application\nmust handle the OAuth authorization flow and provide the token here.\n" + }) + ), + "server_description": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional description of the MCP server, used to provide more context.\n" }) + ), + "headers": Schema.optionalKey( + Schema.Union([ + Schema.Struct({}).annotate({ + "description": "Optional HTTP headers to send to the MCP server. Use for authentication\nor other purposes.\n" + }), + Schema.Null + ]) + ), + "allowed_tools": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Array(Schema.String).annotate({ + "title": "MCP allowed tools", + "description": "A string array of allowed tool names" + }), + MCPToolFilter + ], { mode: "oneOf" }).annotate({ "description": "List of allowed tool names or a filter object.\n" }), + Schema.Null + ]) + ), + "require_approval": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Struct({ "always": Schema.optionalKey(MCPToolFilter), "never": Schema.optionalKey(MCPToolFilter) }) + .annotate({ + "title": "MCP tool approval filter", + "description": + "Specify which of the MCP server's tools require approval. Can be\n`always`, `never`, or a filter object associated with tools\nthat require approval.\n" + }), + Schema.Literals(["always", "never"]).annotate({ + "title": "MCP tool approval setting", + "description": + "Specify a single approval policy for all tools. One of `always` or\n`never`. When set to `always`, all tools will require approval. When\nset to `never`, all tools will not require approval.\n" + }) + ], { mode: "oneOf" }).annotate({ "description": "Specify which of the MCP server's tools require approval." }), + Schema.Null + ]) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this MCP tool is deferred and discovered via tool search.\n" }) + ) +}).annotate({ + "title": "MCP tool", + "description": + "Give the model access to additional tools via remote Model Context Protocol\n(MCP) servers. [Learn more about MCP](/docs/guides/tools-remote-mcp).\n" +}) +export type MessageContentTextObject = { + readonly "type": "text" + readonly "text": { + readonly "value": string + readonly "annotations": ReadonlyArray< + MessageContentTextAnnotationsFileCitationObject | MessageContentTextAnnotationsFilePathObject + > + } +} +export const MessageContentTextObject = Schema.Struct({ + "type": Schema.Literal("text").annotate({ "description": "Always `text`." }), + "text": Schema.Struct({ + "value": Schema.String.annotate({ "description": "The data that makes up the text." }), + "annotations": Schema.Array( + Schema.Union([MessageContentTextAnnotationsFileCitationObject, MessageContentTextAnnotationsFilePathObject], { + mode: "oneOf" + }) + ) + }) +}).annotate({ "title": "Text", "description": "The text content that is part of a message." }) +export type MessageDeltaContentTextObject = { + readonly "index": number + readonly "type": "text" + readonly "text"?: { + readonly "value"?: string + readonly "annotations"?: ReadonlyArray< + MessageDeltaContentTextAnnotationsFileCitationObject | MessageDeltaContentTextAnnotationsFilePathObject + > + } +} +export const MessageDeltaContentTextObject = Schema.Struct({ + "index": Schema.Number.annotate({ "description": "The index of the content part in the message." }).check( + Schema.isInt() + ), + "type": Schema.Literal("text").annotate({ "description": "Always `text`." }), + "text": Schema.optionalKey(Schema.Struct({ + "value": Schema.optionalKey(Schema.String.annotate({ "description": "The data that makes up the text." })), + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Union([ + MessageDeltaContentTextAnnotationsFileCitationObject, + MessageDeltaContentTextAnnotationsFilePathObject + ], { mode: "oneOf" }) + ) + ) + })) +}).annotate({ "title": "Text", "description": "The text content that is part of a message." }) +export type Batch = { + readonly "id": string + readonly "object": "batch" + readonly "endpoint": string + readonly "model"?: string + readonly "errors"?: { + readonly "object"?: string + readonly "data"?: ReadonlyArray< + { + readonly "code"?: string + readonly "message"?: string + readonly "param"?: string | null + readonly "line"?: number | null + } + > + } + readonly "input_file_id": string + readonly "completion_window": string + readonly "status": + | "validating" + | "failed" + | "in_progress" + | "finalizing" + | "completed" + | "expired" + | "cancelling" + | "cancelled" + readonly "output_file_id"?: string + readonly "error_file_id"?: string + readonly "created_at": number + readonly "in_progress_at"?: number + readonly "expires_at"?: number + readonly "finalizing_at"?: number + readonly "completed_at"?: number + readonly "failed_at"?: number + readonly "expired_at"?: number + readonly "cancelling_at"?: number + readonly "cancelled_at"?: number + readonly "request_counts"?: { readonly "total": number; readonly "completed": number; readonly "failed": number } + readonly "usage"?: { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number + } + readonly "metadata"?: Metadata +} +export const Batch = Schema.Struct({ + "id": Schema.String, + "object": Schema.Literal("batch").annotate({ "description": "The object type, which is always `batch`." }), + "endpoint": Schema.String.annotate({ "description": "The OpenAI API endpoint used by the batch." }), + "model": Schema.optionalKey(Schema.String.annotate({ + "description": + "Model ID used to process the batch, like `gpt-5-2025-08-07`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model\nguide](/docs/models) to browse and compare available models.\n" + })), + "errors": Schema.optionalKey(Schema.Struct({ + "object": Schema.optionalKey(Schema.String.annotate({ "description": "The object type, which is always `list`." })), + "data": Schema.optionalKey(Schema.Array(Schema.Struct({ + "code": Schema.optionalKey( + Schema.String.annotate({ "description": "An error code identifying the error type." }) + ), + "message": Schema.optionalKey( + Schema.String.annotate({ "description": "A human-readable message providing more details about the error." }) + ), + "param": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The name of the parameter that caused the error, if applicable." }), + Schema.Null + ]) + ), + "line": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The line number of the input file where the error occurred, if applicable." + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }))) + })), + "input_file_id": Schema.String.annotate({ "description": "The ID of the input file for the batch." }), + "completion_window": Schema.String.annotate({ + "description": "The time frame within which the batch should be processed." + }), + "status": Schema.Literals([ + "validating", + "failed", + "in_progress", + "finalizing", + "completed", + "expired", + "cancelling", + "cancelled" + ]).annotate({ "description": "The current status of the batch." }), + "output_file_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The ID of the file containing the outputs of successfully executed requests." + }) + ), + "error_file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the file containing the outputs of requests with errors." }) + ), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "in_progress_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch started processing.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch will expire.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "finalizing_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch started finalizing.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "completed_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch was completed.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "failed_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch failed.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "expired_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch expired.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "cancelling_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch started cancelling.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "cancelled_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the batch was cancelled.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "request_counts": Schema.optionalKey( + Schema.Struct({ + "total": Schema.Number.annotate({ "description": "Total number of requests in the batch." }).check( + Schema.isInt() + ), + "completed": Schema.Number.annotate({ + "description": "Number of requests that have been completed successfully." + }).check(Schema.isInt()), + "failed": Schema.Number.annotate({ "description": "Number of requests that have failed." }).check(Schema.isInt()) + }).annotate({ "description": "The request counts for different statuses within the batch." }) + ), + "usage": Schema.optionalKey( + Schema.Struct({ + "input_tokens": Schema.Number.annotate({ "description": "The number of input tokens." }).check(Schema.isInt()), + "input_tokens_details": Schema.Struct({ + "cached_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that were retrieved from the cache. [More on\nprompt caching](/docs/guides/prompt-caching).\n" + }).check(Schema.isInt()) + }).annotate({ "description": "A detailed breakdown of the input tokens." }), + "output_tokens": Schema.Number.annotate({ "description": "The number of output tokens." }).check(Schema.isInt()), + "output_tokens_details": Schema.Struct({ + "reasoning_tokens": Schema.Number.annotate({ "description": "The number of reasoning tokens." }).check( + Schema.isInt() + ) + }).annotate({ "description": "A detailed breakdown of the output tokens." }), + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used." }).check( + Schema.isInt() + ) + }).annotate({ + "description": + "Represents token usage details including input tokens, output tokens, a\nbreakdown of output tokens, and the total tokens used. Only populated on\nbatches created after September 7, 2025.\n" + }) + ), + "metadata": Schema.optionalKey(Metadata) +}) +export type CreateMessageRequest = { + readonly "role": "user" | "assistant" + readonly "content": + | string + | ReadonlyArray + readonly "attachments"?: + | ReadonlyArray< + { + readonly "file_id"?: string + readonly "tools"?: ReadonlyArray + } + > + | null + readonly "metadata"?: Metadata +} +export const CreateMessageRequest = Schema.Struct({ + "role": Schema.Literals(["user", "assistant"]).annotate({ + "description": + "The role of the entity that is creating the message. Allowed values include:\n- `user`: Indicates the message is sent by an actual user and should be used in most cases to represent user-generated messages.\n- `assistant`: Indicates the message is generated by the assistant. Use this value to insert messages from the assistant into the conversation.\n" + }), + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The text contents of the message." }), + Schema.Array( + Schema.Union([MessageContentImageFileObject, MessageContentImageUrlObject, MessageRequestContentTextObject], { + mode: "oneOf" + }) + ).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type, each can be of type `text` or images can be passed with `image_url` or `image_file`. Image types are only supported on [Vision-compatible models](/docs/models)." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }), + "attachments": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the file to attach to the message." }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([AssistantToolsCode, AssistantToolsFileSearchTypeOnly], { mode: "oneOf" })).annotate({ + "description": "The tools to add this file to." + }) + ) + })).annotate({ "description": "A list of files attached to the message, and the tools they should be added to." }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata) +}) +export type EvalStoredCompletionsSource = { + readonly "type": "stored_completions" + readonly "metadata"?: Metadata + readonly "model"?: string | null + readonly "created_after"?: number | null + readonly "created_before"?: number | null + readonly "limit"?: number | null +} +export const EvalStoredCompletionsSource = Schema.Struct({ + "type": Schema.Literal("stored_completions").annotate({ + "description": "The type of source. Always `stored_completions`." + }), + "metadata": Schema.optionalKey(Metadata), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "An optional model to filter by (e.g., 'gpt-4o')." }), + Schema.Null + ]) + ), + "created_after": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "An optional Unix timestamp to filter items created after this time." }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "created_before": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "An optional Unix timestamp to filter items created before this time." }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "limit": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "An optional maximum number of items to return." }).check(Schema.isInt()), + Schema.Null + ]) + ) +}).annotate({ + "title": "StoredCompletionsRunDataSource", + "description": "A StoredCompletionsRunDataSource configuration describing a set of filters\n" +}) +export type ModifyMessageRequest = { readonly "metadata"?: Metadata } +export const ModifyMessageRequest = Schema.Struct({ "metadata": Schema.optionalKey(Metadata) }) +export type ModifyRunRequest = { readonly "metadata"?: Metadata } +export const ModifyRunRequest = Schema.Struct({ "metadata": Schema.optionalKey(Metadata) }) +export type ModifyThreadRequest = { + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { readonly "vector_store_ids"?: ReadonlyArray } + } | null + readonly "metadata"?: Metadata +} +export const ModifyThreadRequest = Schema.Struct({ + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Struct({ + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)) + ) + })) + }).annotate({ + "description": + "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata) +}) +export type ThreadObject = { + readonly "id": string + readonly "object": "thread" + readonly "created_at": number + readonly "tool_resources": { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { readonly "vector_store_ids"?: ReadonlyArray } + } | null + readonly "metadata": Metadata +} +export const ThreadObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("thread").annotate({ "description": "The object type, which is always `thread`." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the thread was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "tool_resources": Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Struct({ + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)) + ) + })) + }).annotate({ + "description": + "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ]), + "metadata": Metadata +}).annotate({ + "title": "Thread", + "description": "Represents a thread that contains [messages](/docs/api-reference/messages)." +}) +export type UpdateVectorStoreRequest = { + readonly "name"?: string | null + readonly "expires_after"?: { readonly "anchor": "last_active_at"; readonly "days": number } + readonly "metadata"?: Metadata +} +export const UpdateVectorStoreRequest = Schema.Struct({ + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The name of the vector store." }) + ), + "expires_after": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "anchor": Schema.Literal("last_active_at").annotate({ + "description": + "Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`." + }), + "days": Schema.Number.annotate({ + "description": "The number of days after the anchor time that the vector store will expire." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(365)) + }).annotate({ + "title": "Vector store expiration policy", + "description": "The expiration policy for a vector store." + }) + ])), + "metadata": Schema.optionalKey(Metadata) +}) +export type ListModelsResponse = { readonly "object": "list"; readonly "data": ReadonlyArray } +export const ListModelsResponse = Schema.Struct({ "object": Schema.Literal("list"), "data": Schema.Array(Model) }) +export type ModelIdsResponses = + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" +export const ModelIdsResponses = Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) +]) +export type RealtimeTranscriptionSessionCreateRequest = { + readonly "turn_detection"?: { + readonly "type"?: "server_vad" + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } + readonly "input_audio_noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "input_audio_format"?: "pcm16" | "g711_ulaw" | "g711_alaw" + readonly "input_audio_transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> +} +export const RealtimeTranscriptionSessionCreateRequest = Schema.Struct({ + "turn_detection": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("server_vad").annotate({ + "description": + "Type of turn detection. Only `server_vad` is currently supported for transcription sessions.\n" + }) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "Configuration for turn detection. Can be set to `null` to turn off. Server VAD means that the model will detect the start and end of speech based on audio volume and respond at the end of user speech.\n" + }) + ), + "input_audio_noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "input_audio_format": Schema.optionalKey( + Schema.Literals(["pcm16", "g711_ulaw", "g711_alaw"]).annotate({ + "description": + "The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\nFor `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate,\nsingle channel (mono), and little-endian byte order.\n" + }) + ), + "input_audio_transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "The set of items to include in the transcription. Current available items are:\n`item.input_audio_transcription.logprobs`\n" + }) + ) +}).annotate({ + "title": "Realtime transcription session configuration", + "description": "Realtime transcription session object configuration." +}) +export type RealtimeTranslationClientEventSessionUpdate = { + readonly "event_id"?: string + readonly "type": "session.update" + readonly "session": { + readonly "audio"?: { + readonly "input"?: { + readonly "transcription"?: { readonly "model": string } | null + readonly "noise_reduction"?: { readonly "type": NoiseReductionType } | null + } + readonly "output"?: { readonly "language"?: string } + } + } +} +export const RealtimeTranslationClientEventSessionUpdate = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("session.update").annotate({ "description": "The event type, must be `session.update`." }), + "session": Schema.Struct({ + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "transcription": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "model": Schema.String.annotate({ + "description": "The transcription model to use for source transcript deltas." + }) + }).annotate({ + "description": + "Optional source-language transcription. When configured, the server emits\n`session.input_transcript.delta` events. Translation itself still runs from\nthe input audio stream.\n" + }), + Schema.Null + ]) + ), + "noise_reduction": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": NoiseReductionType }).annotate({ + "description": "Optional input noise reduction. Set to `null` to disable it.\n" + }), + Schema.Null + ]) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Target language for translated output audio and transcript deltas.\n" + }) + ) + }) + ) + }).annotate({ "description": "Configuration for translation input and output audio.\n" }) + ) + }).annotate({ + "title": "Realtime translation session update", + "description": + "Translation session fields to update. The session `type` and `model` are set\nat creation and cannot be changed with `session.update`.\n" + }) +}).annotate({ + "description": + "Send this event to update the translation session configuration. Translation\nsessions support updates to `audio.output.language`, `audio.input.transcription`,\nand `audio.input.noise_reduction`.\n" +}) +export type RealtimeTranslationServerEventSessionCreated = { + readonly "event_id": string + readonly "type": "session.created" + readonly "session": { + readonly "id": string + readonly "type": "translation" + readonly "expires_at": number + readonly "model": string + readonly "audio": { + readonly "input"?: { + readonly "transcription"?: { readonly "model": string } | null + readonly "noise_reduction"?: { readonly "type": NoiseReductionType } | null + } + readonly "output"?: { readonly "language"?: string } + } + } +} +export const RealtimeTranslationServerEventSessionCreated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.created").annotate({ "description": "The event type, must be `session.created`." }), + "session": Schema.Struct({ + "id": Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }), + "type": Schema.Literal("translation").annotate({ + "description": "The session type. Always `translation` for Realtime translation sessions.\n" + }), + "expires_at": Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ + "description": + "The Realtime translation model used for this session. This field is set at\nsession creation and cannot be changed with `session.update`.\n" + }), + "audio": Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "transcription": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "model": Schema.String.annotate({ + "description": "The transcription model used for source transcript deltas." + }) + }).annotate({ + "description": + "Optional source-language transcription. When configured, the server emits\n`session.input_transcript.delta` events. Translation itself still runs from\nthe input audio stream.\n" + }), + Schema.Null + ]) + ), + "noise_reduction": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": NoiseReductionType }).annotate({ + "description": "Optional input noise reduction.\n" + }), + Schema.Null + ]) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Target language for translated output audio and transcript deltas.\n" + }) + ) + }) + ) + }).annotate({ "description": "Configuration for translation input and output audio.\n" }) + }).annotate({ "title": "Realtime translation session", "description": "The translation session configuration." }) +}).annotate({ + "description": + "Returned when a translation session is created. Emitted automatically when a\nnew connection is established as the first server event. This event contains\nthe default translation session configuration.\n" +}) +export type RealtimeTranslationServerEventSessionUpdated = { + readonly "event_id": string + readonly "type": "session.updated" + readonly "session": { + readonly "id": string + readonly "type": "translation" + readonly "expires_at": number + readonly "model": string + readonly "audio": { + readonly "input"?: { + readonly "transcription"?: { readonly "model": string } | null + readonly "noise_reduction"?: { readonly "type": NoiseReductionType } | null + } + readonly "output"?: { readonly "language"?: string } + } + } +} +export const RealtimeTranslationServerEventSessionUpdated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.updated").annotate({ "description": "The event type, must be `session.updated`." }), + "session": Schema.Struct({ + "id": Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }), + "type": Schema.Literal("translation").annotate({ + "description": "The session type. Always `translation` for Realtime translation sessions.\n" + }), + "expires_at": Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ + "description": + "The Realtime translation model used for this session. This field is set at\nsession creation and cannot be changed with `session.update`.\n" + }), + "audio": Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "transcription": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "model": Schema.String.annotate({ + "description": "The transcription model used for source transcript deltas." + }) + }).annotate({ + "description": + "Optional source-language transcription. When configured, the server emits\n`session.input_transcript.delta` events. Translation itself still runs from\nthe input audio stream.\n" + }), + Schema.Null + ]) + ), + "noise_reduction": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": NoiseReductionType }).annotate({ + "description": "Optional input noise reduction.\n" + }), + Schema.Null + ]) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Target language for translated output audio and transcript deltas.\n" + }) + ) + }) + ) + }).annotate({ "description": "Configuration for translation input and output audio.\n" }) + }).annotate({ "title": "Realtime translation session", "description": "The translation session configuration." }) +}).annotate({ + "description": + "Returned when a translation session is updated with a `session.update` event,\nunless there is an error.\n" +}) +export type RealtimeTranslationSession = { + readonly "id": string + readonly "type": "translation" + readonly "expires_at": number + readonly "model": string + readonly "audio": { + readonly "input"?: { + readonly "transcription"?: { readonly "model": string } | null + readonly "noise_reduction"?: { readonly "type": NoiseReductionType } | null + } + readonly "output"?: { readonly "language"?: string } + } +} +export const RealtimeTranslationSession = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }), + "type": Schema.Literal("translation").annotate({ + "description": "The session type. Always `translation` for Realtime translation sessions.\n" + }), + "expires_at": Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ + "description": + "The Realtime translation model used for this session. This field is set at\nsession creation and cannot be changed with `session.update`.\n" + }), + "audio": Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "transcription": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "model": Schema.String.annotate({ + "description": "The transcription model used for source transcript deltas." + }) + }).annotate({ + "description": + "Optional source-language transcription. When configured, the server emits\n`session.input_transcript.delta` events. Translation itself still runs from\nthe input audio stream.\n" + }), + Schema.Null + ]) + ), + "noise_reduction": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": NoiseReductionType }).annotate({ + "description": "Optional input noise reduction.\n" + }), + Schema.Null + ]) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Target language for translated output audio and transcript deltas.\n" + }) + ) + }) + ) + }).annotate({ "description": "Configuration for translation input and output audio.\n" }) +}).annotate({ + "title": "Realtime translation session", + "description": + "A Realtime translation session. Translation sessions continuously translate input\naudio into the configured output language.\n" +}) +export type RealtimeTranslationSessionCreateRequest = { + readonly "model": string + readonly "audio"?: { + readonly "input"?: { + readonly "transcription"?: { readonly "model": string } | null + readonly "noise_reduction"?: { readonly "type": NoiseReductionType } | null + } + readonly "output"?: { readonly "language"?: string } + } +} +export const RealtimeTranslationSessionCreateRequest = Schema.Struct({ + "model": Schema.String.annotate({ "description": "The Realtime translation model used for this session.\n" }), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "transcription": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "model": Schema.String.annotate({ + "description": "The transcription model to use for source transcript deltas." + }) + }).annotate({ + "description": + "Optional source-language transcription. When configured, the server emits\n`session.input_transcript.delta` events. Translation itself still runs from\nthe input audio stream.\n" + }), + Schema.Null + ]) + ), + "noise_reduction": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": NoiseReductionType }).annotate({ + "description": "Optional input noise reduction. Set to `null` to disable it.\n" + }), + Schema.Null + ]) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Target language for translated output audio and transcript deltas.\n" + }) + ) + }) + ) + }).annotate({ "description": "Configuration for translation input and output audio.\n" }) + ) +}).annotate({ + "title": "Realtime translation session configuration", + "description": + "Realtime translation session configuration. Translation sessions stream source\naudio in and translated audio plus transcript deltas out continuously.\n" +}) +export type ListFilesResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ListFilesResponse = Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(OpenAIFile), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean +}) +export type ListCertificatesResponse = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean + readonly "object": "list" +} +export const ListCertificatesResponse = Schema.Struct({ + "data": Schema.Array(OrganizationCertificate), + "first_id": Schema.Union([Schema.String, Schema.Null]), + "last_id": Schema.Union([Schema.String, Schema.Null]), + "has_more": Schema.Boolean, + "object": Schema.Literal("list") +}) +export type OrganizationCertificateActivationResponse = { + readonly "object": "organization.certificate.activation" + readonly "data": ReadonlyArray +} +export const OrganizationCertificateActivationResponse = Schema.Struct({ + "object": Schema.Literal("organization.certificate.activation").annotate({ + "description": "The organization certificate activation result type." + }), + "data": Schema.Array(OrganizationCertificate) +}) +export type OrganizationCertificateDeactivationResponse = { + readonly "object": "organization.certificate.deactivation" + readonly "data": ReadonlyArray +} +export const OrganizationCertificateDeactivationResponse = Schema.Struct({ + "object": Schema.Literal("organization.certificate.deactivation").annotate({ + "description": "The organization certificate deactivation result type." + }), + "data": Schema.Array(OrganizationCertificate) +}) +export type ListProjectCertificatesResponse = { + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean + readonly "object": "list" +} +export const ListProjectCertificatesResponse = Schema.Struct({ + "data": Schema.Array(OrganizationProjectCertificate), + "first_id": Schema.Union([Schema.String, Schema.Null]), + "last_id": Schema.Union([Schema.String, Schema.Null]), + "has_more": Schema.Boolean, + "object": Schema.Literal("list") +}) +export type OrganizationProjectCertificateActivationResponse = { + readonly "object": "organization.project.certificate.activation" + readonly "data": ReadonlyArray +} +export const OrganizationProjectCertificateActivationResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.certificate.activation").annotate({ + "description": "The project certificate activation result type." + }), + "data": Schema.Array(OrganizationProjectCertificate) +}) +export type OrganizationProjectCertificateDeactivationResponse = { + readonly "object": "organization.project.certificate.deactivation" + readonly "data": ReadonlyArray +} +export const OrganizationProjectCertificateDeactivationResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.certificate.deactivation").annotate({ + "description": "The project certificate deactivation result type." + }), + "data": Schema.Array(OrganizationProjectCertificate) +}) +export type CreateImageRequest = { + readonly "prompt": string + readonly "model"?: string | "gpt-image-1.5" | "dall-e-2" | "dall-e-3" | "gpt-image-1" | "gpt-image-1-mini" | null + readonly "n"?: number + readonly "quality"?: "standard" | "hd" | "low" | "medium" | "high" | "auto" | null + readonly "response_format"?: "url" | "b64_json" | null + readonly "output_format"?: "png" | "jpeg" | "webp" | null + readonly "output_compression"?: number | null + readonly "stream"?: boolean | null + readonly "partial_images"?: PartialImages + readonly "size"?: + | string + | "auto" + | "1024x1024" + | "1536x1024" + | "1024x1536" + | "256x256" + | "512x512" + | "1792x1024" + | "1024x1792" + | null + readonly "moderation"?: "low" | "auto" | null + readonly "background"?: "transparent" | "opaque" | "auto" | null + readonly "style"?: "vivid" | "natural" | null + readonly "user"?: string +} +export const CreateImageRequest = Schema.Struct({ + "prompt": Schema.String.annotate({ + "description": + "A text description of the desired image(s). The maximum length is 32000 characters for the GPT image models, 1000 characters for `dall-e-2` and 4000 characters for `dall-e-3`." + }), + "model": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Literals(["gpt-image-1.5", "dall-e-2", "dall-e-3", "gpt-image-1", "gpt-image-1-mini"]) + ]).annotate({ + "description": + "The model to use for image generation. One of `dall-e-2`, `dall-e-3`, or a GPT image model (`gpt-image-1`, `gpt-image-1-mini`, `gpt-image-1.5`). Defaults to `dall-e-2` unless a parameter specific to the GPT image models is used." + }), + Schema.Null + ]) + ), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(10)], { + "description": + "The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported." + }) + ) + ]) + ), + "quality": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["standard", "hd", "low", "medium", "high", "auto"]).annotate({ + "description": + "The quality of the image that will be generated.\n\n- `auto` (default value) will automatically select the best quality for the given model.\n- `high`, `medium` and `low` are supported for the GPT image models.\n- `hd` and `standard` are supported for `dall-e-3`.\n- `standard` is the only option for `dall-e-2`.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The quality of the image that will be generated.\n\n- `auto` (default value) will automatically select the best quality for the given model.\n- `high`, `medium` and `low` are supported for the GPT image models.\n- `hd` and `standard` are supported for `dall-e-3`.\n- `standard` is the only option for `dall-e-2`.\n" + }) + ]) + ), + "response_format": Schema.optionalKey(Schema.Union([ + Schema.Literals(["url", "b64_json"]).annotate({ + "description": + "The format in which generated images with `dall-e-2` and `dall-e-3` are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter isn't supported for the GPT image models, which always return base64-encoded images." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The format in which generated images with `dall-e-2` and `dall-e-3` are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter isn't supported for the GPT image models, which always return base64-encoded images." + }) + ])), + "output_format": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["png", "jpeg", "webp"]).annotate({ + "description": + "The format in which the generated images are returned. This parameter is only supported for the GPT image models. Must be one of `png`, `jpeg`, or `webp`." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The format in which the generated images are returned. This parameter is only supported for the GPT image models. Must be one of `png`, `jpeg`, or `webp`." + }) + ]) + ), + "output_compression": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "description": + "The compression level (0-100%) for the generated images. This parameter is only supported for the GPT image models with the `webp` or `jpeg` output formats, and defaults to 100." + }) + ), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Generate the image in streaming mode. Defaults to `false`. See the\n[Image generation guide](/docs/guides/image-generation) for more information.\nThis parameter is only supported for the GPT image models.\n" + }) + ), + "partial_images": Schema.optionalKey(PartialImages), + "size": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Literals(["auto", "1024x1024", "1536x1024", "1024x1536", "256x256", "512x512", "1792x1024", "1024x1792"]) + ]).annotate({ + "description": + "The size of the generated images. For `gpt-image-2` and `gpt-image-2-2026-04-21`, arbitrary resolutions are supported as `WIDTHxHEIGHT` strings, for example `1536x864`. Width and height must both be divisible by 16 and the requested aspect ratio must be between 1:3 and 3:1. Resolutions above `2560x1440` are experimental, and the maximum supported resolution is `3840x2160`. The requested size must also satisfy the model's current pixel and edge limits. The standard sizes `1024x1024`, `1536x1024`, and `1024x1536` are supported by the GPT image models; `auto` is supported for models that allow automatic sizing. For `dall-e-2`, use one of `256x256`, `512x512`, or `1024x1024`. For `dall-e-3`, use one of `1024x1024`, `1792x1024`, or `1024x1792`." + }), + Schema.Null + ]) + ), + "moderation": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["low", "auto"]).annotate({ + "description": + "Control the content-moderation level for images generated by the GPT image models. Must be either `low` for less restrictive filtering or `auto` (default value)." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "Control the content-moderation level for images generated by the GPT image models. Must be either `low` for less restrictive filtering or `auto` (default value)." + }) + ]) + ), + "background": Schema.optionalKey(Schema.Union([ + Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": + "Allows to set transparency for the background of the generated image(s).\nThis parameter is only supported for the GPT image models. Must be one of\n`transparent`, `opaque` or `auto` (default value). When `auto` is used, the\nmodel will automatically determine the best background for the image.\n\nIf `transparent`, the output format needs to support transparency, so it\nshould be set to either `png` (default value) or `webp`.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "Allows to set transparency for the background of the generated image(s).\nThis parameter is only supported for the GPT image models. Must be one of\n`transparent`, `opaque` or `auto` (default value). When `auto` is used, the\nmodel will automatically determine the best background for the image.\n\nIf `transparent`, the output format needs to support transparency, so it\nshould be set to either `png` (default value) or `webp`.\n" + }) + ])), + "style": Schema.optionalKey(Schema.Union([ + Schema.Literals(["vivid", "natural"]).annotate({ + "description": + "The style of the generated images. This parameter is only supported for `dall-e-3`. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The style of the generated images. This parameter is only supported for `dall-e-3`. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images." + }) + ])), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).\n" + }) + ) +}) +export type EditImageBodyJsonParam = { + readonly "model"?: string | "gpt-image-1.5" | "gpt-image-1" | "gpt-image-1-mini" | "chatgpt-image-latest" | null + readonly "images": ReadonlyArray + readonly "mask"?: ImageRefParam + readonly "prompt": string + readonly "n"?: number | null + readonly "quality"?: "low" | "medium" | "high" | "auto" | null + readonly "input_fidelity"?: "high" | "low" | null + readonly "size"?: "auto" | "1024x1024" | "1536x1024" | "1024x1536" | null + readonly "user"?: string + readonly "output_format"?: "png" | "jpeg" | "webp" | null + readonly "output_compression"?: number | null + readonly "moderation"?: "low" | "auto" | null + readonly "background"?: "transparent" | "opaque" | "auto" | null + readonly "stream"?: boolean | null + readonly "partial_images"?: PartialImages +} +export const EditImageBodyJsonParam = Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini", "chatgpt-image-latest"]), + Schema.Null + ]).annotate({ "description": "The model to use for image editing." }) + ), + "images": Schema.Array(ImageRefParam).annotate({ + "description": "Input image references to edit.\nFor GPT image models, you can provide up to 16 images.\n" + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(16)), + "mask": Schema.optionalKey(ImageRefParam), + "prompt": Schema.String.annotate({ "description": "A text description of the desired image edit." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(10)), + Schema.Null + ]).annotate({ "description": "The number of edited images to generate." }) + ), + "quality": Schema.optionalKey( + Schema.Union([Schema.Literals(["low", "medium", "high", "auto"]), Schema.Null]).annotate({ + "description": "Output quality for GPT image models.\n" + }) + ), + "input_fidelity": Schema.optionalKey( + Schema.Union([Schema.Literals(["high", "low"]), Schema.Null]).annotate({ + "description": "Controls fidelity to the original input image(s)." + }) + ), + "size": Schema.optionalKey( + Schema.Union([Schema.Literals(["auto", "1024x1024", "1536x1024", "1024x1536"]), Schema.Null]).annotate({ + "description": "Requested output image size." + }) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI\nmonitor and detect abuse.\n" + }) + ), + "output_format": Schema.optionalKey( + Schema.Union([Schema.Literals(["png", "jpeg", "webp"]), Schema.Null]).annotate({ + "description": "Output image format. Supported for GPT image models." + }) + ), + "output_compression": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check( + Schema.isLessThanOrEqualTo(100) + ), + Schema.Null + ]).annotate({ "description": "Compression level for `jpeg` or `webp` output." }) + ), + "moderation": Schema.optionalKey( + Schema.Union([Schema.Literals(["low", "auto"]), Schema.Null]).annotate({ + "description": "Moderation level for GPT image models." + }) + ), + "background": Schema.optionalKey( + Schema.Union([Schema.Literals(["transparent", "opaque", "auto"]), Schema.Null]).annotate({ + "description": "Background behavior for generated image output." + }) + ), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ "description": "Stream partial image results as events." }) + ), + "partial_images": Schema.optionalKey(PartialImages) +}).annotate({ + "description": + "JSON request body for image edits.\n\nUse `images` (array of `ImageRefParam`) instead of multipart `image` uploads.\nYou can reference images via external URLs, data URLs, or uploaded file IDs.\nJSON edits support GPT image models only; DALL-E edits require multipart (`dall-e-2` only).\n" +}) +export type ProjectListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ProjectListResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(Project), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ProjectApiKey = { + readonly "object": "organization.project.api_key" + readonly "redacted_value": string + readonly "name": string + readonly "created_at": number + readonly "last_used_at": number | null + readonly "id": string + readonly "owner": { + readonly "type"?: "user" | "service_account" + readonly "user"?: ProjectApiKeyOwnerUser + readonly "service_account"?: ProjectApiKeyOwnerServiceAccount + } +} +export const ProjectApiKey = Schema.Struct({ + "object": Schema.Literal("organization.project.api_key").annotate({ + "description": "The object type, which is always `organization.project.api_key`" + }), + "redacted_value": Schema.String.annotate({ "description": "The redacted value of the API key" }), + "name": Schema.String.annotate({ "description": "The name of the API key" }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the API key was created", + "format": "unixtime" + }).check(Schema.isInt()), + "last_used_at": Schema.Union([Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), Schema.Null]) + .annotate({ "description": "The Unix timestamp (in seconds) of when the API key was last used." }), + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints" }), + "owner": Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["user", "service_account"]).annotate({ "description": "`user` or `service_account`" }) + ), + "user": Schema.optionalKey(ProjectApiKeyOwnerUser), + "service_account": Schema.optionalKey(ProjectApiKeyOwnerServiceAccount) + }) +}).annotate({ "description": "Represents an individual API key in a project." }) +export type ProjectGroupListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next": string | null +} +export const ProjectGroupListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "Always `list`." }), + "data": Schema.Array(ProjectGroup).annotate({ + "description": "Project group memberships returned in the current page." + }), + "has_more": Schema.Boolean.annotate({ "description": "Whether additional project group memberships are available." }), + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Cursor to fetch the next page of results, or `null` when there are no more results." + }) +}).annotate({ "description": "Paginated list of groups that have access to a project." }) +export type ProjectRateLimitListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ProjectRateLimitListResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(ProjectRateLimit), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ProjectServiceAccountListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ProjectServiceAccountListResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(ProjectServiceAccount), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ProjectServiceAccountCreateResponse = { + readonly "object": "organization.project.service_account" + readonly "id": string + readonly "name": string + readonly "role": "member" + readonly "created_at": number + readonly "api_key": ProjectServiceAccountApiKey | null +} +export const ProjectServiceAccountCreateResponse = Schema.Struct({ + "object": Schema.Literal("organization.project.service_account"), + "id": Schema.String, + "name": Schema.String, + "role": Schema.Literal("member").annotate({ + "description": "Service accounts can only have one role of type `member`" + }), + "created_at": Schema.Number.annotate({ "format": "unixtime" }).check(Schema.isInt()), + "api_key": Schema.Union([ProjectServiceAccountApiKey, Schema.Null]) +}) +export type ProjectUserListResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ProjectUserListResponse = Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(ProjectUser), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type RealtimeTranscriptionSessionCreateResponseGA = { + readonly "type": "transcription" + readonly "id": string + readonly "object": string + readonly "expires_at"?: number + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: RealtimeAudioFormats + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: { + readonly "type"?: string + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } | null + } + } +} +export const RealtimeTranscriptionSessionCreateResponseGA = Schema.Struct({ + "type": Schema.Literal("transcription").annotate({ + "description": "The type of session. Always `transcription` for transcription sessions.\n" + }), + "id": Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }), + "object": Schema.String.annotate({ "description": "The object type. Always `realtime.transcription_session`." }), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n- `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey(RealtimeAudioFormats), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model used for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ "description": "The language of the input audio.\n" }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt configured for input audio transcription, when present.\n" + }) + ) + }).annotate({ "description": "Configuration of the transcription model.\n" }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": "Configuration for input audio noise reduction.\n" + }) + ), + "turn_detection": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ + "description": "Type of turn detection, only `server_vad` is currently supported.\n" + }) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "Configuration for turn detection. Can be set to `null` to turn off. Server\nVAD means that the model will detect the start and end of speech based on\naudio volume and respond at the end of user speech. For `gpt-realtime-whisper`, this must be `null`; VAD is not supported.\n" + }), + Schema.Null + ]).annotate({ + "description": + "Configuration for turn detection. For `gpt-realtime-whisper`, this must be `null`; VAD is not supported.\n" + }) + ) + })) + }).annotate({ "description": "Configuration for input audio for the session.\n" }) + ) +}).annotate({ + "title": "Realtime transcription session configuration object", + "description": "A Realtime transcription session configuration object.\n" +}) +export type RealtimeMCPToolCall = { + readonly "type": "mcp_call" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string + readonly "approval_request_id"?: string | null + readonly "output"?: string | null + readonly "error"?: RealtimeMCPProtocolError | RealtimeMCPToolExecutionError | RealtimeMCPHTTPError | null +} +export const RealtimeMCPToolCall = Schema.Struct({ + "type": Schema.Literal("mcp_call").annotate({ "description": "The type of the item. Always `mcp_call`." }), + "id": Schema.String.annotate({ "description": "The unique ID of the tool call." }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server running the tool." }), + "name": Schema.String.annotate({ "description": "The name of the tool that was run." }), + "arguments": Schema.String.annotate({ "description": "A JSON string of the arguments passed to the tool." }), + "approval_request_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of an associated approval request, if any." }), + Schema.Null + ]) + ), + "output": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The output from the tool call." }), Schema.Null]) + ), + "error": Schema.optionalKey( + Schema.Union([ + Schema.Union([RealtimeMCPProtocolError, RealtimeMCPToolExecutionError, RealtimeMCPHTTPError], { mode: "oneOf" }) + .annotate({ "description": "The error from the tool call, if any." }), + Schema.Null + ]) + ) +}).annotate({ + "title": "Realtime MCP tool call", + "description": "A Realtime item representing an invocation of a tool on an MCP server.\n" +}) +export type RealtimeReasoning = { readonly "effort"?: RealtimeReasoningEffort } +export const RealtimeReasoning = Schema.Struct({ "effort": Schema.optionalKey(RealtimeReasoningEffort) }).annotate({ + "title": "Realtime reasoning configuration", + "description": "Configuration for reasoning-capable Realtime models such as `gpt-realtime-2`.\n" +}) +export type RealtimeTranscriptionSessionCreateRequestGA = { + readonly "type": "transcription" + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: RealtimeAudioFormats + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> +} +export const RealtimeTranscriptionSessionCreateRequestGA = Schema.Struct({ + "type": Schema.Literal("transcription").annotate({ + "description": "The type of session to create. Always `transcription` for transcription sessions.\n" + }), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey(RealtimeAudioFormats), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ) +}).annotate({ + "title": "Realtime transcription session configuration", + "description": "Realtime transcription session object configuration." +}) +export type Reasoning = { + readonly "effort"?: ReasoningEffort + readonly "summary"?: "auto" | "concise" | "detailed" | null + readonly "generate_summary"?: "auto" | "concise" | "detailed" | null +} +export const Reasoning = Schema.Struct({ + "effort": Schema.optionalKey(ReasoningEffort), + "summary": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "concise", "detailed"]).annotate({ + "description": + "A summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n\n`concise` is supported for `computer-use-preview` models and all reasoning models after `gpt-5`.\n" + }), + Schema.Null + ])), + "generate_summary": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["auto", "concise", "detailed"]).annotate({ + "description": + "**Deprecated:** use `summary` instead.\n\nA summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n" + }), + Schema.Null + ]) + ) +}).annotate({ + "title": "Reasoning", + "description": + "**gpt-5 and o-series models only**\n\nConfiguration options for\n[reasoning models](https://platform.openai.com/docs/guides/reasoning).\n" +}) +export type ResponseError = { readonly "code": ResponseErrorCode; readonly "message": string } | null +export const ResponseError = Schema.Union([ + Schema.Struct({ + "code": ResponseErrorCode, + "message": Schema.String.annotate({ "description": "A human-readable description of the error.\n" }) + }).annotate({ "description": "An error object returned when the model fails to generate a Response.\n" }), + Schema.Null +]) +export type ResponseFormatJsonSchema = { + readonly "type": "json_schema" + readonly "json_schema": { + readonly "description"?: string + readonly "name": string + readonly "schema"?: ResponseFormatJsonSchemaSchema + readonly "strict"?: boolean | null + } +} +export const ResponseFormatJsonSchema = Schema.Struct({ + "type": Schema.Literal("json_schema").annotate({ + "description": "The type of response format being defined. Always `json_schema`." + }), + "json_schema": Schema.Struct({ + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A description of what the response format is for, used by the model to\ndetermine how to respond in the format.\n" + }) + ), + "name": Schema.String.annotate({ + "description": + "The name of the response format. Must be a-z, A-Z, 0-9, or contain\nunderscores and dashes, with a maximum length of 64.\n" + }), + "schema": Schema.optionalKey(ResponseFormatJsonSchemaSchema), + "strict": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to enable strict schema adherence when generating the output.\nIf set to true, the model will always follow the exact schema defined\nin the `schema` field. Only a subset of JSON Schema is supported when\n`strict` is `true`. To learn more, read the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n" + }), + Schema.Null + ])) + }).annotate({ + "title": "JSON schema", + "description": "Structured Outputs configuration options, including a JSON Schema.\n" + }) +}).annotate({ + "title": "JSON schema", + "description": + "JSON Schema response format. Used to generate structured JSON responses.\nLearn more about [Structured Outputs](/docs/guides/structured-outputs).\n" +}) +export type TextResponseFormatJsonSchema = { + readonly "type": "json_schema" + readonly "description"?: string + readonly "name": string + readonly "schema": ResponseFormatJsonSchemaSchema + readonly "strict"?: boolean | null +} +export const TextResponseFormatJsonSchema = Schema.Struct({ + "type": Schema.Literal("json_schema").annotate({ + "description": "The type of response format being defined. Always `json_schema`." + }), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A description of what the response format is for, used by the model to\ndetermine how to respond in the format.\n" + }) + ), + "name": Schema.String.annotate({ + "description": + "The name of the response format. Must be a-z, A-Z, 0-9, or contain\nunderscores and dashes, with a maximum length of 64.\n" + }), + "schema": ResponseFormatJsonSchemaSchema, + "strict": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to enable strict schema adherence when generating the output.\nIf set to true, the model will always follow the exact schema defined\nin the `schema` field. Only a subset of JSON Schema is supported when\n`strict` is `true`. To learn more, read the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n" + }), + Schema.Null + ])) +}).annotate({ + "title": "JSON schema", + "description": + "JSON Schema response format. Used to generate structured JSON responses.\nLearn more about [Structured Outputs](/docs/guides/structured-outputs).\n" +}) +export type ResponseTextDeltaEvent = { + readonly "type": "response.output_text.delta" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number + readonly "logprobs": ReadonlyArray +} +export const ResponseTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.delta").annotate({ + "description": "The type of the event. Always `response.output_text.delta`.\n" + }), + "item_id": Schema.String.annotate({ "description": "The ID of the output item that the text delta was added to.\n" }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the text delta was added to.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part that the text delta was added to.\n" + }).check(Schema.isInt()), + "delta": Schema.String.annotate({ "description": "The text delta that was added.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number for this event." }).check( + Schema.isInt() + ), + "logprobs": Schema.Array(ResponseLogProb).annotate({ + "description": "The log probabilities of the tokens in the delta.\n" + }) +}).annotate({ "description": "Emitted when there is an additional text delta." }) +export type ResponseTextDoneEvent = { + readonly "type": "response.output_text.done" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "text": string + readonly "sequence_number": number + readonly "logprobs": ReadonlyArray +} +export const ResponseTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.done").annotate({ + "description": "The type of the event. Always `response.output_text.done`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the text content is finalized.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the text content is finalized.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ + "description": "The index of the content part that the text content is finalized.\n" + }).check(Schema.isInt()), + "text": Schema.String.annotate({ "description": "The text content that is finalized.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number for this event." }).check( + Schema.isInt() + ), + "logprobs": Schema.Array(ResponseLogProb).annotate({ + "description": "The log probabilities of the tokens in the delta.\n" + }) +}).annotate({ "description": "Emitted when text content is finalized." }) +export type Prompt = { + readonly "id": string + readonly "version"?: string | null + readonly "variables"?: ResponsePromptVariables +} | null +export const Prompt = Schema.Union([ + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique identifier of the prompt template to use." }), + "version": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Optional version of the prompt template." }), Schema.Null]) + ), + "variables": Schema.optionalKey(ResponsePromptVariables) + }).annotate({ + "description": + "Reference to a prompt template and its variables.\n[Learn more](/docs/guides/text?api-mode=responses#reusable-prompts).\n" + }), + Schema.Null +]) +export type GroupRoleAssignment = { readonly "object": "group.role"; readonly "group": Group; readonly "role": Role } +export const GroupRoleAssignment = Schema.Struct({ + "object": Schema.Literal("group.role").annotate({ "description": "Always `group.role`." }), + "group": Group, + "role": Role +}).annotate({ "description": "Role assignment linking a group to a role." }) +export type PublicRoleListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next": string | null +} +export const PublicRoleListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "Always `list`." }), + "data": Schema.Array(Role).annotate({ "description": "Roles returned in the current page." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether more roles are available when paginating." }), + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Cursor to fetch the next page of results, or `null` when there are no additional roles." + }) +}).annotate({ "description": "Paginated list of roles available on an organization or project." }) +export type RunStepDeltaObject = { + readonly "id": string + readonly "object": "thread.run.step.delta" + readonly "delta": { + readonly "step_details"?: { + readonly "type": "message_creation" + readonly "message_creation"?: { readonly "message_id"?: string } + } | { + readonly "type": "tool_calls" + readonly "tool_calls"?: ReadonlyArray< + | RunStepDeltaStepDetailsToolCallsCodeObject + | RunStepDeltaStepDetailsToolCallsFileSearchObject + | RunStepDeltaStepDetailsToolCallsFunctionObject + > + } + } +} +export const RunStepDeltaObject = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The identifier of the run step, which can be referenced in API endpoints." + }), + "object": Schema.Literal("thread.run.step.delta").annotate({ + "description": "The object type, which is always `thread.run.step.delta`." + }), + "delta": Schema.Struct({ + "step_details": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("message_creation").annotate({ "description": "Always `message_creation`." }), + "message_creation": Schema.optionalKey( + Schema.Struct({ + "message_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the message that was created by this run step." }) + ) + }) + ) + }).annotate({ "title": "Message creation", "description": "The details of the run step." }), + Schema.Struct({ + "type": Schema.Literal("tool_calls").annotate({ "description": "Always `tool_calls`." }), + "tool_calls": Schema.optionalKey( + Schema.Array( + Schema.Union([ + RunStepDeltaStepDetailsToolCallsCodeObject, + RunStepDeltaStepDetailsToolCallsFileSearchObject, + RunStepDeltaStepDetailsToolCallsFunctionObject + ], { mode: "oneOf" }) + ).annotate({ + "description": + "An array of tool calls the run step was involved in. These can be associated with one of three types of tools: `code_interpreter`, `file_search`, or `function`.\n" + }) + ) + }).annotate({ "title": "Tool calls", "description": "The details of the run step." }) + ], { mode: "oneOf" })) + }).annotate({ "description": "The delta containing the fields that have changed on the run step." }) +}).annotate({ + "title": "Run step delta object", + "description": "Represents a run step delta i.e. any changed fields on a run step during streaming.\n" +}) +export type CreateSpeechResponseStreamEvent = SpeechAudioDeltaEvent | SpeechAudioDoneEvent +export const CreateSpeechResponseStreamEvent = Schema.Union([SpeechAudioDeltaEvent, SpeechAudioDoneEvent]) +export type ChunkingStrategyRequestParam = { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": StaticChunkingStrategy +} +export const ChunkingStrategyRequestParam = Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }).annotate({ + "title": "Auto Chunking Strategy", + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": StaticChunkingStrategy + }).annotate({ + "title": "Static Chunking Strategy", + "description": "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }) +], { mode: "oneOf" }) +export type CreateCompletionRequest = { + readonly "model": string | "gpt-3.5-turbo-instruct" | "davinci-002" | "babbage-002" + readonly "prompt": + | string + | ReadonlyArray + | ReadonlyArray + | ReadonlyArray> + | null + readonly "best_of"?: number + readonly "echo"?: boolean | null + readonly "frequency_penalty"?: number + readonly "logit_bias"?: {} + readonly "logprobs"?: number + readonly "max_tokens"?: number + readonly "n"?: number + readonly "presence_penalty"?: number + readonly "seed"?: never + readonly "stop"?: StopConfiguration + readonly "stream"?: boolean | null + readonly "stream_options"?: ChatCompletionStreamOptions + readonly "suffix"?: string | null + readonly "temperature"?: number + readonly "top_p"?: number + readonly "user"?: string +} +export const CreateCompletionRequest = Schema.Struct({ + "model": Schema.Union([Schema.String, Schema.Literals(["gpt-3.5-turbo-instruct", "davinci-002", "babbage-002"])]) + .annotate({ + "description": + "ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n" + }), + "prompt": Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Array(Schema.String), + Schema.Array(Schema.Number.check(Schema.isInt())).check(Schema.isMinLength(1)), + Schema.Array(Schema.Array(Schema.Number.check(Schema.isInt())).check(Schema.isMinLength(1))).check( + Schema.isMinLength(1) + ) + ], { mode: "oneOf" }).annotate({ + "description": + "The prompt(s) to generate completions for, encoded as a string, array of strings, array of tokens, or array of token arrays.\n\nNote that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document.\n" + }), + Schema.Null + ]), + "best_of": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(20)], { + "description": + "Generates `best_of` completions server-side and returns the \"best\" (the one with the highest log probability per token). Results cannot be streamed.\n\nWhen used with `n`, `best_of` controls the number of candidate completions and `n` specifies how many to return – `best_of` must be greater than `n`.\n\n**Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.\n" + }) + ) + ]) + ), + "echo": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Echo back the prompt in addition to the completion\n" + }) + ), + "frequency_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(-2), Schema.isLessThanOrEqualTo(2)], { + "description": + "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.\n\n[See more information about frequency and presence penalties.](/docs/guides/text-generation)\n" + }) + ) + ]) + ), + "logit_bias": Schema.optionalKey(Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Modify the likelihood of specified tokens appearing in the completion.\n\nAccepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.\n\nAs an example, you can pass `{\"50256\": -100}` to prevent the <|endoftext|> token from being generated.\n" + }) + ])), + "logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(5)], { + "description": + "Include the log probabilities on the `logprobs` most likely output tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response.\n\nThe maximum value for `logprobs` is 5.\n" + }) + ) + ]) + ), + "max_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0)], { + "description": + "The maximum number of [tokens](/tokenizer) that can be generated in the completion.\n\nThe token count of your prompt plus `max_tokens` cannot exceed the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.\n" + }) + ) + ]) + ), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(128)], { + "description": + "How many completions to generate for each prompt.\n\n**Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.\n" + }) + ) + ]) + ), + "presence_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(-2), Schema.isLessThanOrEqualTo(2)], { + "description": + "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.\n\n[See more information about frequency and presence penalties.](/docs/guides/text-generation)\n" + }) + ) + ]) + ), + "seed": Schema.optionalKey(Schema.Never), + "stop": Schema.optionalKey(StopConfiguration), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).\n" + }) + ), + "stream_options": Schema.optionalKey(ChatCompletionStreamOptions), + "suffix": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The suffix that comes after a completion of inserted text.\n\nThis parameter is only supported for `gpt-3.5-turbo-instruct`.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)], { + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n\nWe generally recommend altering this or `top_p` but not both.\n" + }) + ) + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)], { + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }) + ) + ]) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).\n" + }) + ) +}) +export type TranscriptTextDoneEvent = { + readonly "type": "transcript.text.done" + readonly "text": string + readonly "logprobs"?: ReadonlyArray< + { readonly "token"?: string; readonly "logprob"?: number; readonly "bytes"?: ReadonlyArray } + > + readonly "usage"?: TranscriptTextUsageTokens +} +export const TranscriptTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("transcript.text.done").annotate({ + "description": "The type of the event. Always `transcript.text.done`.\n" + }), + "text": Schema.String.annotate({ "description": "The text that was transcribed.\n" }), + "logprobs": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "token": Schema.optionalKey( + Schema.String.annotate({ "description": "The token that was used to generate the log probability.\n" }) + ), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "description": "The log probability of the token.\n" }).check(Schema.isFinite()) + ), + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": "The bytes that were used to generate the log probability.\n" + }) + ) + })).annotate({ + "description": + "The log probabilities of the individual tokens in the transcription. Only included if you [create a transcription](/docs/api-reference/audio/create-transcription) with the `include[]` parameter set to `logprobs`.\n" + }) + ), + "usage": Schema.optionalKey(TranscriptTextUsageTokens) +}).annotate({ + "description": + "Emitted when the transcription is complete. Contains the complete transcription text. Only emitted when you [create a transcription](/docs/api-reference/audio/create-transcription) with the `Stream` parameter set to `true`." +}) +export type CreateTranscriptionResponseDiarizedJson = { + readonly "task": "transcribe" + readonly "duration": number + readonly "text": string + readonly "segments": ReadonlyArray + readonly "usage"?: { + readonly "type": "tokens" + readonly "input_tokens": number + readonly "input_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + readonly "output_tokens": number + readonly "total_tokens": number + } | { readonly "type": "duration"; readonly "seconds": number } +} +export const CreateTranscriptionResponseDiarizedJson = Schema.Struct({ + "task": Schema.Literal("transcribe").annotate({ + "description": "The type of task that was run. Always `transcribe`." + }), + "duration": Schema.Number.annotate({ "description": "Duration of the input audio in seconds.", "format": "double" }) + .check(Schema.isFinite()), + "text": Schema.String.annotate({ "description": "The concatenated transcript text for the entire audio input." }), + "segments": Schema.Array(TranscriptionDiarizedSegment).annotate({ + "description": "Segments of the transcript annotated with timestamps and speaker labels." + }), + "usage": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("tokens").annotate({ + "description": "The type of the usage object. Always `tokens` for this variant." + }), + "input_tokens": Schema.Number.annotate({ "description": "Number of input tokens billed for this request." }) + .check(Schema.isInt()), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of text tokens billed for this request." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio tokens billed for this request." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the input tokens billed for this request." }) + ), + "output_tokens": Schema.Number.annotate({ "description": "Number of output tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used (input + output)." }).check( + Schema.isInt() + ) + }).annotate({ "title": "Token Usage", "description": "Token or duration usage statistics for the request." }), + Schema.Struct({ + "type": Schema.Literal("duration").annotate({ + "description": "The type of the usage object. Always `duration` for this variant." + }), + "seconds": Schema.Number.annotate({ + "description": "Duration of the input audio in seconds.", + "format": "double" + }).check(Schema.isFinite()) + }).annotate({ "title": "Duration Usage", "description": "Token or duration usage statistics for the request." }) + ], { mode: "oneOf" })) +}).annotate({ + "description": + "Represents a diarized transcription response returned by the model, including the combined transcript and speaker-segment annotations.\n" +}) +export type CreateTranslationResponseVerboseJson = { + readonly "language": string + readonly "duration": number + readonly "text": string + readonly "segments"?: ReadonlyArray +} +export const CreateTranslationResponseVerboseJson = Schema.Struct({ + "language": Schema.String.annotate({ "description": "The language of the output translation (always `english`)." }), + "duration": Schema.Number.annotate({ "description": "The duration of the input audio.", "format": "double" }).check( + Schema.isFinite() + ), + "text": Schema.String.annotate({ "description": "The translated text." }), + "segments": Schema.optionalKey( + Schema.Array(TranscriptionSegment).annotate({ + "description": "Segments of the translated text and their corresponding details." + }) + ) +}) +export type CreateTranscriptionResponseVerboseJson = { + readonly "language": string + readonly "duration": number + readonly "text": string + readonly "words"?: ReadonlyArray + readonly "segments"?: ReadonlyArray + readonly "usage"?: TranscriptTextUsageDuration +} +export const CreateTranscriptionResponseVerboseJson = Schema.Struct({ + "language": Schema.String.annotate({ "description": "The language of the input audio." }), + "duration": Schema.Number.annotate({ "description": "The duration of the input audio.", "format": "double" }).check( + Schema.isFinite() + ), + "text": Schema.String.annotate({ "description": "The transcribed text." }), + "words": Schema.optionalKey( + Schema.Array(TranscriptionWord).annotate({ "description": "Extracted words and their corresponding timestamps." }) + ), + "segments": Schema.optionalKey( + Schema.Array(TranscriptionSegment).annotate({ + "description": "Segments of the transcribed text and their corresponding details." + }) + ), + "usage": Schema.optionalKey(TranscriptTextUsageDuration) +}).annotate({ + "description": "Represents a verbose json transcription response returned by model, based on the provided input." +}) +export type UsageTimeBucket = { + readonly "object": "bucket" + readonly "start_time": number + readonly "end_time": number + readonly "results": ReadonlyArray< + | UsageCompletionsResult + | UsageEmbeddingsResult + | UsageModerationsResult + | UsageImagesResult + | UsageAudioSpeechesResult + | UsageAudioTranscriptionsResult + | UsageVectorStoresResult + | UsageCodeInterpreterSessionsResult + | UsageFileSearchCallsResult + | UsageWebSearchCallsResult + | CostsResult + > +} +export const UsageTimeBucket = Schema.Struct({ + "object": Schema.Literal("bucket"), + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.Number.check(Schema.isInt()), + "results": Schema.Array( + Schema.Union([ + UsageCompletionsResult, + UsageEmbeddingsResult, + UsageModerationsResult, + UsageImagesResult, + UsageAudioSpeechesResult, + UsageAudioTranscriptionsResult, + UsageVectorStoresResult, + UsageCodeInterpreterSessionsResult, + UsageFileSearchCallsResult, + UsageWebSearchCallsResult, + CostsResult + ], { mode: "oneOf" }) + ) +}) +export type UserListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const UserListResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(User), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type UserRoleAssignment = { readonly "object": "user.role"; readonly "user": User; readonly "role": Role } +export const UserRoleAssignment = Schema.Struct({ + "object": Schema.Literal("user.role").annotate({ "description": "Always `user.role`." }), + "user": User, + "role": Role +}).annotate({ "description": "Role assignment linking a user to a role." }) +export type CreateTranscriptionRequest = { + readonly "file": string + readonly "model": + | string + | "whisper-1" + | "gpt-4o-transcribe" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe-diarize" + readonly "language"?: string + readonly "prompt"?: string + readonly "response_format"?: AudioResponseFormat + readonly "temperature"?: number + readonly "include"?: ReadonlyArray + readonly "timestamp_granularities"?: ReadonlyArray<"word" | "segment"> + readonly "stream"?: boolean | null + readonly "chunking_strategy"?: "auto" | VadConfig | null + readonly "known_speaker_names"?: ReadonlyArray + readonly "known_speaker_references"?: ReadonlyArray +} +export const CreateTranscriptionRequest = Schema.Struct({ + "file": Schema.String.annotate({ + "description": + "The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.\n", + "format": "binary" + }), + "model": Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-transcribe", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe-diarize" + ]) + ]).annotate({ + "description": + "ID of the model to use. The options are `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `whisper-1` (which is powered by our open source Whisper V2 model), and `gpt-4o-transcribe-diarize`.\n" + }), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format will improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio segment. The [prompt](/docs/guides/speech-to-text#prompting) should match the audio language. This field is not supported when using `gpt-4o-transcribe-diarize`.\n" + }) + ), + "response_format": Schema.optionalKey(AudioResponseFormat), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use [log probability](https://en.wikipedia.org/wiki/Log_probability) to automatically increase the temperature until certain thresholds are hit.\n" + }).check(Schema.isFinite()) + ), + "include": Schema.optionalKey( + Schema.Array(TranscriptionInclude).annotate({ + "description": + "Additional information to include in the transcription response.\n`logprobs` will return the log probabilities of the tokens in the\nresponse to understand the model's confidence in the transcription.\n`logprobs` only works with response_format set to `json` and only with\nthe models `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, and `gpt-4o-mini-transcribe-2025-12-15`. This field is not supported when using `gpt-4o-transcribe-diarize`.\n" + }) + ), + "timestamp_granularities": Schema.optionalKey( + Schema.Array(Schema.Literals(["word", "segment"])).annotate({ + "description": + "The timestamp granularities to populate for this transcription. `response_format` must be set `verbose_json` to use timestamp granularities. Either or both of these options are supported: `word`, or `segment`. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency.\nThis option is not available for `gpt-4o-transcribe-diarize`.\n" + }) + ), + "stream": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "If set to true, the model response data will be streamed to the client\nas it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).\nSee the [Streaming section of the Speech-to-Text guide](/docs/guides/speech-to-text?lang=curl#streaming-transcriptions)\nfor more information.\n\nNote: Streaming is not supported for the `whisper-1` model and will be ignored.\n" + }), + Schema.Null + ])), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ + "description": "Automatically set chunking parameters based on the audio. Must be set to `\"auto\"`.\n" + }), + VadConfig + ]).annotate({ + "description": + "Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. Required when using `gpt-4o-transcribe-diarize` for inputs longer than 30 seconds. " + }), + Schema.Null + ]) + ), + "known_speaker_names": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Optional list of speaker names that correspond to the audio samples provided in `known_speaker_references[]`. Each entry should be a short identifier (for example `customer` or `agent`). Up to 4 speakers are supported.\n" + }).check(Schema.isMaxLength(4)) + ), + "known_speaker_references": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Optional list of audio samples (as [data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)) that contain known speaker references matching `known_speaker_names[]`. Each sample must be between 2 and 10 seconds, and can use any of the same input audio formats supported by `file`.\n" + }).check(Schema.isMaxLength(4)) + ) +}) +export type CreateVectorStoreRequest = { + readonly "file_ids"?: ReadonlyArray + readonly "name"?: string + readonly "description"?: string + readonly "expires_after"?: VectorStoreExpirationAfter + readonly "chunking_strategy"?: { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": StaticChunkingStrategy + } + readonly "metadata"?: Metadata +} +export const CreateVectorStoreRequest = Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [File](/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files." + }).check(Schema.isMaxLength(500)) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the vector store." })), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": "A description for the vector store. Can be used to describe the vector store's purpose." + }) + ), + "expires_after": Schema.optionalKey(VectorStoreExpirationAfter), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }).annotate({ + "title": "Auto Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. Only applicable if `file_ids` is non-empty." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": StaticChunkingStrategy + }).annotate({ + "title": "Static Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. Only applicable if `file_ids` is non-empty." + }) + ], { mode: "oneOf" }) + ), + "metadata": Schema.optionalKey(Metadata) +}) +export type VectorStoreObject = { + readonly "id": string + readonly "object": "vector_store" + readonly "created_at": number + readonly "name": string + readonly "usage_bytes": number + readonly "file_counts": { + readonly "in_progress": number + readonly "completed": number + readonly "failed": number + readonly "cancelled": number + readonly "total": number + } + readonly "status": "expired" | "in_progress" | "completed" + readonly "expires_after"?: VectorStoreExpirationAfter + readonly "expires_at"?: number | null + readonly "last_active_at": number | null + readonly "metadata": Metadata +} +export const VectorStoreObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("vector_store").annotate({ + "description": "The object type, which is always `vector_store`." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the vector store was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "name": Schema.String.annotate({ "description": "The name of the vector store." }), + "usage_bytes": Schema.Number.annotate({ + "description": "The total number of bytes used by the files in the vector store." + }).check(Schema.isInt()), + "file_counts": Schema.Struct({ + "in_progress": Schema.Number.annotate({ "description": "The number of files that are currently being processed." }) + .check(Schema.isInt()), + "completed": Schema.Number.annotate({ "description": "The number of files that have been successfully processed." }) + .check(Schema.isInt()), + "failed": Schema.Number.annotate({ "description": "The number of files that have failed to process." }).check( + Schema.isInt() + ), + "cancelled": Schema.Number.annotate({ "description": "The number of files that were cancelled." }).check( + Schema.isInt() + ), + "total": Schema.Number.annotate({ "description": "The total number of files." }).check(Schema.isInt()) + }), + "status": Schema.Literals(["expired", "in_progress", "completed"]).annotate({ + "description": + "The status of the vector store, which can be either `expired`, `in_progress`, or `completed`. A status of `completed` indicates that the vector store is ready for use." + }), + "expires_after": Schema.optionalKey(VectorStoreExpirationAfter), + "expires_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the vector store will expire.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "last_active_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the vector store was last active.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "metadata": Metadata +}).annotate({ + "title": "Vector store", + "description": "A vector store is a collection of processed files can be used by the `file_search` tool." +}) +export type FileSearchToolCall = { + readonly "id": string + readonly "type": "file_search_call" + readonly "status": "in_progress" | "searching" | "completed" | "incomplete" | "failed" + readonly "queries": ReadonlyArray + readonly "results"?: + | ReadonlyArray< + { + readonly "file_id"?: string + readonly "text"?: string + readonly "filename"?: string + readonly "attributes"?: VectorStoreFileAttributes + readonly "score"?: number + } + > + | null +} +export const FileSearchToolCall = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the file search tool call.\n" }), + "type": Schema.Literal("file_search_call").annotate({ + "description": "The type of the file search tool call. Always `file_search_call`.\n" + }), + "status": Schema.Literals(["in_progress", "searching", "completed", "incomplete", "failed"]).annotate({ + "description": + "The status of the file search tool call. One of `in_progress`,\n`searching`, `incomplete` or `failed`,\n" + }), + "queries": Schema.Array(Schema.String).annotate({ "description": "The queries used to search for files.\n" }), + "results": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the file.\n" })), + "text": Schema.optionalKey( + Schema.String.annotate({ "description": "The text that was retrieved from the file.\n" }) + ), + "filename": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the file.\n" })), + "attributes": Schema.optionalKey(VectorStoreFileAttributes), + "score": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The relevance score of the file - a value between 0 and 1.\n", + "format": "float" + }).check(Schema.isFinite()) + ) + })).annotate({ "description": "The results of the file search tool call.\n" }), + Schema.Null + ])) +}).annotate({ + "title": "File search tool call", + "description": + "The results of a file search tool call. See the\n[file search guide](/docs/guides/tools-file-search) for more information.\n" +}) +export type UpdateVectorStoreFileAttributesRequest = { readonly "attributes": VectorStoreFileAttributes } +export const UpdateVectorStoreFileAttributesRequest = Schema.Struct({ "attributes": VectorStoreFileAttributes }) +export type VectorStoreFileObject = { + readonly "id": string + readonly "object": "vector_store.file" + readonly "usage_bytes": number + readonly "created_at": number + readonly "vector_store_id": string + readonly "status": "in_progress" | "completed" | "cancelled" | "failed" + readonly "last_error": { + readonly "code": "server_error" | "unsupported_file" | "invalid_file" + readonly "message": string + } | null + readonly "chunking_strategy"?: { readonly "type": "static"; readonly "static": StaticChunkingStrategy } | { + readonly "type": "other" + } + readonly "attributes"?: VectorStoreFileAttributes +} +export const VectorStoreFileObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("vector_store.file").annotate({ + "description": "The object type, which is always `vector_store.file`." + }), + "usage_bytes": Schema.Number.annotate({ + "description": "The total vector store usage in bytes. Note that this may be different from the original file size." + }).check(Schema.isInt()), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the vector store file was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "vector_store_id": Schema.String.annotate({ + "description": + "The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) is attached to." + }), + "status": Schema.Literals(["in_progress", "completed", "cancelled", "failed"]).annotate({ + "description": + "The status of the vector store file, which can be either `in_progress`, `completed`, `cancelled`, or `failed`. The status `completed` indicates that the vector store file is ready for use." + }), + "last_error": Schema.Union([ + Schema.Struct({ + "code": Schema.Literals(["server_error", "unsupported_file", "invalid_file"]).annotate({ + "description": "One of `server_error`, `unsupported_file`, or `invalid_file`." + }), + "message": Schema.String.annotate({ "description": "A human-readable description of the error." }) + }).annotate({ + "description": "The last error associated with this vector store file. Will be `null` if there are no errors." + }), + Schema.Null + ]), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": StaticChunkingStrategy + }).annotate({ "title": "Static Chunking Strategy", "description": "The strategy used to chunk the file." }), + Schema.Struct({ "type": Schema.Literal("other").annotate({ "description": "Always `other`." }) }).annotate({ + "title": "Other Chunking Strategy", + "description": "The strategy used to chunk the file." + }) + ], { mode: "oneOf" }) + ), + "attributes": Schema.optionalKey(VectorStoreFileAttributes) +}).annotate({ "title": "Vector store files", "description": "A list of files attached to a vector store." }) +export type VectorStoreSearchResultItem = { + readonly "file_id": string + readonly "filename": string + readonly "score": number + readonly "attributes": VectorStoreFileAttributes + readonly "content": ReadonlyArray +} +export const VectorStoreSearchResultItem = Schema.Struct({ + "file_id": Schema.String.annotate({ "description": "The ID of the vector store file." }), + "filename": Schema.String.annotate({ "description": "The name of the vector store file." }), + "score": Schema.Number.annotate({ "description": "The similarity score for the result." }).check(Schema.isFinite()) + .check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + "attributes": VectorStoreFileAttributes, + "content": Schema.Array(VectorStoreSearchResultContentObject).annotate({ + "description": "Content chunks from the file." + }) +}) +export type VoiceConsentListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const VoiceConsentListResource = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(VoiceConsentResource), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type CreateSpeechRequest = { + readonly "model": string | "tts-1" | "tts-1-hd" | "gpt-4o-mini-tts" | "gpt-4o-mini-tts-2025-12-15" + readonly "input": string + readonly "instructions"?: string + readonly "voice": VoiceIdsShared | { readonly "id": string } + readonly "response_format"?: "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm" + readonly "speed"?: number + readonly "stream_format"?: "sse" | "audio" +} +export const CreateSpeechRequest = Schema.Struct({ + "model": Schema.Union([ + Schema.String, + Schema.Literals(["tts-1", "tts-1-hd", "gpt-4o-mini-tts", "gpt-4o-mini-tts-2025-12-15"]) + ]).annotate({ + "description": + "One of the available [TTS models](/docs/models#tts): `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`, or `gpt-4o-mini-tts-2025-12-15`.\n" + }), + "input": Schema.String.annotate({ + "description": "The text to generate audio for. The maximum length is 4096 characters." + }).check(Schema.isMaxLength(4096)), + "instructions": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Control the voice of your generated audio with additional instructions. Does not work with `tts-1` or `tts-1-hd`." + }).check(Schema.isMaxLength(4096)) + ), + "voice": Schema.Union([ + VoiceIdsShared, + Schema.Struct({ "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) }) + .annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice to use when generating the audio. Supported built-in voices are `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `onyx`, `nova`, `sage`, `shimmer`, `verse`, `marin`, and `cedar`. You may also provide a custom voice object with an `id`, for example `{ \"id\": \"voice_1234\" }`. Previews of the voices are available in the [Text to speech guide](/docs/guides/text-to-speech#voice-options)." + }), + "response_format": Schema.optionalKey( + Schema.Literals(["mp3", "opus", "aac", "flac", "wav", "pcm"]).annotate({ + "description": "The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`." + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The speed of the generated audio. Select a value from `0.25` to `4.0`. `1.0` is the default." + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check(Schema.isLessThanOrEqualTo(4)) + ), + "stream_format": Schema.optionalKey( + Schema.Literals(["sse", "audio"]).annotate({ + "description": + "The format to stream the audio in. Supported formats are `sse` and `audio`. `sse` is not supported for `tts-1` or `tts-1-hd`." + }) + ) +}) +export type RealtimeSessionCreateResponse = { + readonly "id"?: string + readonly "object"?: string + readonly "expires_at"?: number + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "model"?: string + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "instructions"?: string + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: RealtimeAudioFormats + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: { + readonly "type"?: string + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } + } + readonly "output"?: { + readonly "format"?: RealtimeAudioFormats + readonly "voice"?: VoiceIdsShared + readonly "speed"?: number + } + } + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } + readonly "turn_detection"?: { + readonly "type"?: string + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: string + readonly "max_output_tokens"?: number | "inf" +} +export const RealtimeSessionCreateResponse = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }) + ), + "object": Schema.optionalKey( + Schema.String.annotate({ "description": "The object type. Always `realtime.session`." }) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n- `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "model": Schema.optionalKey(Schema.String.annotate({ "description": "The Realtime model used for this session." })), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": "The set of modalities the model can respond with. To disable audio,\nset this to [\"text\"].\n" + }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model\ncalls. This field allows the client to guide the model on desired\nresponses. The model can be instructed on response content and format,\n(e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good\nresponses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion\ninto your voice\", \"laugh frequently\"). The instructions are not guaranteed\nto be followed by the model, but they provide guidance to the model on the\ndesired behavior.\n\nNote that the server sets default instructions which will be used if this\nfield is not set and are visible in the `session.created` event at the\nstart of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey(RealtimeAudioFormats), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model used for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ "description": "The language of the input audio.\n" }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt configured for input audio transcription, when present.\n" + }) + ) + }).annotate({ "description": "Configuration for input audio transcription.\n" }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": "Configuration for input audio noise reduction.\n" + }) + ), + "turn_detection": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ + "description": "Type of turn detection, only `server_vad` is currently supported.\n" + }) + ), + "threshold": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "prefix_padding_ms": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "silence_duration_ms": Schema.optionalKey(Schema.Number.check(Schema.isInt())) + }).annotate({ "description": "Configuration for turn detection.\n" }) + ) + })), + "output": Schema.optionalKey( + Schema.Struct({ + "format": Schema.optionalKey(RealtimeAudioFormats), + "voice": Schema.optionalKey(VoiceIdsShared), + "speed": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }) + ) + }).annotate({ "description": "Configuration for input and output audio for the session.\n" }) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto").annotate({ "description": "Default tracing mode for the session.\n" }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the traces dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the traces dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the traces dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Configuration options for tracing. Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }) + ), + "turn_detection": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ "description": "Type of turn detection, only `server_vad` is currently supported.\n" }) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "Configuration for turn detection. Can be set to `null` to turn off. Server\nVAD means that the model will detect the start and end of speech based on\naudio volume and respond at the end of user speech.\n" + }) + ), + "tools": Schema.optionalKey( + Schema.Array(RealtimeFunctionTool).annotate({ "description": "Tools (functions) available to the model." }) + ), + "tool_choice": Schema.optionalKey( + Schema.String.annotate({ + "description": "How the model chooses tools. Options are `auto`, `none`, `required`, or\nspecify a function.\n" + }) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ) +}).annotate({ + "title": "Realtime session configuration object", + "description": "A Realtime session configuration object.\n" +}) +export type WebSearchTool = { + readonly "type": "web_search" | "web_search_2025_08_26" + readonly "filters"?: { readonly "allowed_domains"?: ReadonlyArray | null } | null + readonly "user_location"?: WebSearchApproximateLocation + readonly "search_context_size"?: "low" | "medium" | "high" +} +export const WebSearchTool = Schema.Struct({ + "type": Schema.Literals(["web_search", "web_search_2025_08_26"]).annotate({ + "description": "The type of the web search tool. One of `web_search` or `web_search_2025_08_26`." + }), + "filters": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "allowed_domains": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String.annotate({ "description": "Allowed domain for the search." })).annotate({ + "title": "Allowed domains for the search.", + "description": + "Allowed domains for the search. If not provided, all domains are allowed.\nSubdomains of the provided domains are allowed as well.\n\nExample: `[\"pubmed.ncbi.nlm.nih.gov\"]`\n" + }), + Schema.Null + ]) + ) + }).annotate({ "description": "Filters for the search.\n" }), + Schema.Null + ])), + "user_location": Schema.optionalKey(WebSearchApproximateLocation), + "search_context_size": Schema.optionalKey( + Schema.Literals(["low", "medium", "high"]).annotate({ + "description": + "High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default." + }) + ) +}).annotate({ + "title": "Web search", + "description": + "Search the Internet for sources related to the prompt. Learn more about the\n[web search tool](/docs/guides/tools-web-search).\n" +}) +export type ContainerNetworkPolicyAllowlistParam = { + readonly "type": "allowlist" + readonly "allowed_domains": ReadonlyArray + readonly "domain_secrets"?: ReadonlyArray +} +export const ContainerNetworkPolicyAllowlistParam = Schema.Struct({ + "type": Schema.Literal("allowlist").annotate({ + "description": "Allow outbound network access only to specified domains. Always `allowlist`." + }), + "allowed_domains": Schema.Array(Schema.String).annotate({ + "description": "A list of allowed domains when type is `allowlist`." + }).check(Schema.isMinLength(1)), + "domain_secrets": Schema.optionalKey( + Schema.Array(ContainerNetworkPolicyDomainSecretParam).annotate({ + "description": "Optional domain-scoped secrets for allowlisted domains." + }).check(Schema.isMinLength(1)) + ) +}) +export type EvalItemContentItem = + | EvalItemContentText + | InputTextContent + | EvalItemContentOutputText + | EvalItemInputImage + | InputAudio +export const EvalItemContentItem = Schema.Union([ + EvalItemContentText, + InputTextContent, + EvalItemContentOutputText, + EvalItemInputImage, + InputAudio +], { mode: "oneOf" }).annotate({ + "title": "Eval content item", + "description": "A single content item: input text, output text, input image, or input audio.\n" +}) +export type Annotation = FileCitationBody | UrlCitationBody | ContainerFileCitationBody | FilePath +export const Annotation = Schema.Union([FileCitationBody, UrlCitationBody, ContainerFileCitationBody, FilePath], { + mode: "oneOf" +}).annotate({ "description": "An annotation that applies to a span of output text." }) +export type LogProb = { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray + readonly "top_logprobs": ReadonlyArray +} +export const LogProb = Schema.Struct({ + "token": Schema.String, + "logprob": Schema.Number.check(Schema.isFinite()), + "bytes": Schema.Array(Schema.Number.check(Schema.isInt())), + "top_logprobs": Schema.Array(TopLogProb) +}).annotate({ "title": "Log probability", "description": "The log probability of a token." }) +export type ReasoningItem = { + readonly "type": "reasoning" + readonly "id": string + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + readonly "content"?: ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" +} +export const ReasoningItem = Schema.Struct({ + "type": Schema.Literal("reasoning").annotate({ "description": "The type of the object. Always `reasoning`.\n" }), + "id": Schema.String.annotate({ "description": "The unique identifier of the reasoning content.\n" }), + "encrypted_content": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The encrypted content of the reasoning item - populated when a response is\ngenerated with `reasoning.encrypted_content` in the `include` parameter.\n" + }), + Schema.Null + ]) + ), + "summary": Schema.Array(SummaryTextContent).annotate({ "description": "Reasoning summary content.\n" }), + "content": Schema.optionalKey( + Schema.Array(ReasoningTextContent).annotate({ "description": "Reasoning text content.\n" }) + ), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ) +}).annotate({ + "title": "Reasoning", + "description": + "A description of the chain of thought used by a reasoning model while generating\na response. Be sure to include these items in your `input` to the Responses API\nfor subsequent turns of a conversation if you are manually\n[managing context](/docs/guides/conversation-state).\n" +}) +export type FunctionAndCustomToolCallOutput = InputTextContent | InputImageContent | InputFileContent +export const FunctionAndCustomToolCallOutput = Schema.Union([InputTextContent, InputImageContent, InputFileContent], { + mode: "oneOf" +}) +export type InputContent = InputTextContent | InputImageContent | InputFileContent +export const InputContent = Schema.Union([InputTextContent, InputImageContent, InputFileContent], { mode: "oneOf" }) +export type DragParam = { + readonly "type": "drag" + readonly "path": ReadonlyArray + readonly "keys"?: ReadonlyArray | null +} +export const DragParam = Schema.Struct({ + "type": Schema.Literal("drag").annotate({ + "description": "Specifies the event type. For a drag action, this property is always set to `drag`." + }), + "path": Schema.Array(CoordParam).annotate({ + "description": + "An array of coordinates representing the path of the drag action. Coordinates will appear as an array of objects, eg\n```\n[\n { x: 100, y: 200 },\n { x: 200, y: 300 }\n]\n```" + }), + "keys": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).annotate({ "description": "The keys being held while dragging the mouse." }), + Schema.Null + ]) + ) +}).annotate({ "title": "Drag", "description": "A drag action." }) +export type ComputerToolCallOutputResource = { + readonly "type": "computer_call_output" + readonly "id": string + readonly "call_id": string + readonly "acknowledged_safety_checks"?: ReadonlyArray + readonly "output": ComputerScreenshotImage + readonly "status": "completed" | "incomplete" + readonly "created_by"?: string +} +export const ComputerToolCallOutputResource = Schema.Struct({ + "type": Schema.Literal("computer_call_output").annotate({ + "description": "The type of the computer tool call output. Always `computer_call_output`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the computer call tool output.\n" }), + "call_id": Schema.String.annotate({ "description": "The ID of the computer tool call that produced the output.\n" }), + "acknowledged_safety_checks": Schema.optionalKey( + Schema.Array(ComputerCallSafetyCheckParam).annotate({ + "description": "The safety checks reported by the API that have been acknowledged by the\ndeveloper.\n" + }) + ), + "output": ComputerScreenshotImage, + "status": Schema.Union([ + Schema.Literal("completed").annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or\n`incomplete`. Populated when input items are returned via API.\n" + }), + Schema.Literal("incomplete").annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or\n`incomplete`. Populated when input items are returned via API.\n" + }) + ]).annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or\n`incomplete`. Populated when input items are returned via API.\n" + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item.\n" }) + ) +}).annotate({ "title": "Computer tool call output", "description": "The output of a computer tool call.\n" }) +export type CreateImageEditRequest = { + readonly "image": string | ReadonlyArray + readonly "prompt": string + readonly "mask"?: string + readonly "background"?: "transparent" | "opaque" | "auto" | null + readonly "model"?: + | string + | "gpt-image-1.5" + | "dall-e-2" + | "gpt-image-1" + | "gpt-image-1-mini" + | "chatgpt-image-latest" + | null + readonly "n"?: number + readonly "size"?: string | "256x256" | "512x512" | "1024x1024" | "1536x1024" | "1024x1536" | "auto" | null + readonly "response_format"?: "url" | "b64_json" | null + readonly "output_format"?: "png" | "jpeg" | "webp" | null + readonly "output_compression"?: number | null + readonly "user"?: string + readonly "input_fidelity"?: InputFidelity | null + readonly "stream"?: boolean | null + readonly "partial_images"?: PartialImages + readonly "quality"?: "standard" | "low" | "medium" | "high" | "auto" | null +} +export const CreateImageEditRequest = Schema.Struct({ + "image": Schema.Union([ + Schema.String.annotate({ "format": "binary" }), + Schema.Array(Schema.String.annotate({ "format": "binary" })).check(Schema.isMaxLength(16)) + ]).annotate({ + "description": + "The image(s) to edit. Must be a supported image file or an array of images.\n\nFor the GPT image models (`gpt-image-1`, `gpt-image-1-mini`, and `gpt-image-1.5`), each image should be a `png`, `webp`, or `jpg`\nfile less than 50MB. You can provide up to 16 images.\n`chatgpt-image-latest` follows the same input constraints as GPT image models.\n\nFor `dall-e-2`, you can only provide one image, and it should be a square\n`png` file less than 4MB.\n" + }), + "prompt": Schema.String.annotate({ + "description": + "A text description of the desired image(s). The maximum length is 1000 characters for `dall-e-2`, and 32000 characters for the GPT image models." + }), + "mask": Schema.optionalKey(Schema.String.annotate({ + "description": + "An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where `image` should be edited. If there are multiple images provided, the mask will be applied on the first image. Must be a valid PNG file, less than 4MB, and have the same dimensions as `image`.", + "format": "binary" + })), + "background": Schema.optionalKey(Schema.Union([ + Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": + "Allows to set transparency for the background of the generated image(s).\nThis parameter is only supported for the GPT image models. Must be one of\n`transparent`, `opaque` or `auto` (default value). When `auto` is used, the\nmodel will automatically determine the best background for the image.\n\nIf `transparent`, the output format needs to support transparency, so it\nshould be set to either `png` (default value) or `webp`.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "Allows to set transparency for the background of the generated image(s).\nThis parameter is only supported for the GPT image models. Must be one of\n`transparent`, `opaque` or `auto` (default value). When `auto` is used, the\nmodel will automatically determine the best background for the image.\n\nIf `transparent`, the output format needs to support transparency, so it\nshould be set to either `png` (default value) or `webp`.\n" + }) + ])), + "model": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Literals(["gpt-image-1.5", "dall-e-2", "gpt-image-1", "gpt-image-1-mini", "chatgpt-image-latest"]) + ]).annotate({ "description": "The model to use for image generation. Defaults to `gpt-image-1.5`." }), + Schema.Null + ]) + ), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(10)], { + "description": "The number of images to generate. Must be between 1 and 10." + }) + ) + ]) + ), + "size": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Literals(["256x256", "512x512", "1024x1024", "1536x1024", "1024x1536", "auto"]) + ]).annotate({ + "description": + "The size of the generated images. For `gpt-image-2` and `gpt-image-2-2026-04-21`, arbitrary resolutions are supported as `WIDTHxHEIGHT` strings, for example `1536x864`. Width and height must both be divisible by 16 and the requested aspect ratio must be between 1:3 and 3:1. Resolutions above `2560x1440` are experimental, and the maximum supported resolution is `3840x2160`. The requested size must also satisfy the model's current pixel and edge limits. The standard sizes `1024x1024`, `1536x1024`, and `1024x1536` are supported by the GPT image models; `auto` is supported for models that allow automatic sizing. For `dall-e-2`, use one of `256x256`, `512x512`, or `1024x1024`. For `dall-e-3`, use one of `1024x1024`, `1792x1024`, or `1024x1792`." + }), + Schema.Null + ]) + ), + "response_format": Schema.optionalKey(Schema.Union([ + Schema.Literals(["url", "b64_json"]).annotate({ + "description": + "The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter is only supported for `dall-e-2` (default is `url` for `dall-e-2`), as GPT image models always return base64-encoded images." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter is only supported for `dall-e-2` (default is `url` for `dall-e-2`), as GPT image models always return base64-encoded images." + }) + ])), + "output_format": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["png", "jpeg", "webp"]).annotate({ + "description": + "The format in which the generated images are returned. This parameter is\nonly supported for the GPT image models. Must be one of `png`, `jpeg`, or `webp`.\nThe default value is `png`.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The format in which the generated images are returned. This parameter is\nonly supported for the GPT image models. Must be one of `png`, `jpeg`, or `webp`.\nThe default value is `png`.\n" + }) + ]) + ), + "output_compression": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "description": + "The compression level (0-100%) for the generated images. This parameter\nis only supported for the GPT image models with the `webp` or `jpeg` output\nformats, and defaults to 100.\n" + }) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).\n" + }) + ), + "input_fidelity": Schema.optionalKey(Schema.Union([InputFidelity, Schema.Null])), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Edit the image in streaming mode. Defaults to `false`. See the\n[Image generation guide](/docs/guides/image-generation) for more information.\n" + }) + ), + "partial_images": Schema.optionalKey(PartialImages), + "quality": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["standard", "low", "medium", "high", "auto"]).annotate({ + "description": "The quality of the image that will be generated for GPT image models. Defaults to `auto`.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": "The quality of the image that will be generated for GPT image models. Defaults to `auto`.\n" + }) + ]) + ) +}) +export type ImageGenTool = { + readonly "type": "image_generation" + readonly "model"?: string | "gpt-image-1" | "gpt-image-1-mini" | "gpt-image-1.5" + readonly "quality"?: "low" | "medium" | "high" | "auto" + readonly "size"?: string | "1024x1024" | "1024x1536" | "1536x1024" | "auto" + readonly "output_format"?: "png" | "webp" | "jpeg" + readonly "output_compression"?: number + readonly "moderation"?: "auto" | "low" + readonly "background"?: "transparent" | "opaque" | "auto" + readonly "input_fidelity"?: InputFidelity | null + readonly "input_image_mask"?: { readonly "image_url"?: string; readonly "file_id"?: string } + readonly "partial_images"?: number + readonly "action"?: "generate" | "edit" | "auto" +} +export const ImageGenTool = Schema.Struct({ + "type": Schema.Literal("image_generation").annotate({ + "description": "The type of the image generation tool. Always `image_generation`.\n" + }), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["gpt-image-1", "gpt-image-1-mini", "gpt-image-1.5"]).annotate({ + "description": "The image generation model to use. Default: `gpt-image-1`.\n" + }) + ]) + ), + "quality": Schema.optionalKey( + Schema.Literals(["low", "medium", "high", "auto"]).annotate({ + "description": + "The quality of the generated image. One of `low`, `medium`, `high`,\nor `auto`. Default: `auto`.\n" + }) + ), + "size": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Literals(["1024x1024", "1024x1536", "1536x1024", "auto"])]).annotate({ + "description": + "The size of the generated images. For `gpt-image-2` and `gpt-image-2-2026-04-21`, arbitrary resolutions are supported as `WIDTHxHEIGHT` strings, for example `1536x864`. Width and height must both be divisible by 16 and the requested aspect ratio must be between 1:3 and 3:1. Resolutions above `2560x1440` are experimental, and the maximum supported resolution is `3840x2160`. The requested size must also satisfy the model's current pixel and edge limits. The standard sizes `1024x1024`, `1536x1024`, and `1024x1536` are supported by the GPT image models; `auto` is supported for models that allow automatic sizing. For `dall-e-2`, use one of `256x256`, `512x512`, or `1024x1024`. For `dall-e-3`, use one of `1024x1024`, `1792x1024`, or `1024x1792`." + }) + ), + "output_format": Schema.optionalKey( + Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format of the generated image. One of `png`, `webp`, or\n`jpeg`. Default: `png`.\n" + }) + ), + "output_compression": Schema.optionalKey( + Schema.Number.annotate({ "description": "Compression level for the output image. Default: 100.\n" }).check( + Schema.isInt() + ).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "moderation": Schema.optionalKey( + Schema.Literals(["auto", "low"]).annotate({ + "description": "Moderation level for the generated image. Default: `auto`.\n" + }) + ), + "background": Schema.optionalKey( + Schema.Literals(["transparent", "opaque", "auto"]).annotate({ + "description": + "Background type for the generated image. One of `transparent`,\n`opaque`, or `auto`. Default: `auto`.\n" + }) + ), + "input_fidelity": Schema.optionalKey(Schema.Union([InputFidelity, Schema.Null])), + "input_image_mask": Schema.optionalKey( + Schema.Struct({ + "image_url": Schema.optionalKey(Schema.String.annotate({ "description": "Base64-encoded mask image.\n" })), + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "File ID for the mask image.\n" })) + }).annotate({ + "description": + "Optional mask for inpainting. Contains `image_url`\n(string, optional) and `file_id` (string, optional).\n" + }) + ), + "partial_images": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Number of partial images to generate in streaming mode, from 0 (default value) to 3.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(3)) + ), + "action": Schema.optionalKey( + Schema.Literals(["generate", "edit", "auto"]).annotate({ + "description": "Whether to generate a new image or edit an existing image. Default: `auto`.\n" + }) + ) +}).annotate({ + "title": "Image generation tool", + "description": "A tool that generates images using the GPT image models.\n" +}) +export type LocalEnvironmentParam = { readonly "type": "local"; readonly "skills"?: ReadonlyArray } +export const LocalEnvironmentParam = Schema.Struct({ + "type": Schema.Literal("local").annotate({ "description": "Use a local computer environment." }), + "skills": Schema.optionalKey( + Schema.Array(LocalSkillParam).annotate({ "description": "An optional list of skills." }).check( + Schema.isMaxLength(200) + ) + ) +}) +export type CustomToolParam = { + readonly "type": "custom" + readonly "name": string + readonly "description"?: string + readonly "format"?: CustomTextFormatParam | CustomGrammarFormatParam + readonly "defer_loading"?: boolean +} +export const CustomToolParam = Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "The type of the custom tool. Always `custom`." }), + "name": Schema.String.annotate({ "description": "The name of the custom tool, used to identify it in tool calls." }), + "description": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional description of the custom tool, used to provide more context." }) + ), + "format": Schema.optionalKey( + Schema.Union([CustomTextFormatParam, CustomGrammarFormatParam], { mode: "oneOf" }).annotate({ + "description": "The input format for the custom tool. Default is unconstrained text." + }) + ), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this tool should be deferred and discovered via tool search." }) + ) +}).annotate({ + "title": "Custom tool", + "description": + "A custom tool that processes input using a specified format. Learn more about [custom tools](/docs/guides/function-calling#custom-tools)" +}) +export type FunctionToolParam = { + readonly "name": string + readonly "description"?: string | null + readonly "parameters"?: EmptyModelParam | null + readonly "strict"?: boolean | null + readonly "type": "function" + readonly "defer_loading"?: boolean +} +export const FunctionToolParam = Schema.Struct({ + "name": Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(128)).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9_-]+$")) + ), + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "parameters": Schema.optionalKey(Schema.Union([EmptyModelParam, Schema.Null])), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function"), + "defer_loading": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Whether this function should be deferred and discovered via tool search." + }) + ) +}) +export type WebSearchPreviewTool = { + readonly "type": "web_search_preview" | "web_search_preview_2025_03_11" + readonly "user_location"?: { + readonly "type": "approximate" + readonly "country"?: string | null + readonly "region"?: string | null + readonly "city"?: string | null + readonly "timezone"?: string | null + } | null + readonly "search_context_size"?: "low" | "medium" | "high" + readonly "search_content_types"?: ReadonlyArray +} +export const WebSearchPreviewTool = Schema.Struct({ + "type": Schema.Literals(["web_search_preview", "web_search_preview_2025_03_11"]).annotate({ + "description": "The type of the web search tool. One of `web_search_preview` or `web_search_preview_2025_03_11`." + }), + "user_location": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("approximate").annotate({ + "description": "The type of location approximation. Always `approximate`." + }), + "country": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The two-letter [ISO country code](https://en.wikipedia.org/wiki/ISO_3166-1) of the user, e.g. `US`." + }), + Schema.Null + ]) + ), + "region": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Free text input for the region of the user, e.g. `California`." }), + Schema.Null + ]) + ), + "city": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Free text input for the city of the user, e.g. `San Francisco`." }), + Schema.Null + ]) + ), + "timezone": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The [IANA timezone](https://timeapi.io/documentation/iana-timezones) of the user, e.g. `America/Los_Angeles`." + }), + Schema.Null + ]) + ) + }).annotate({ "description": "The user's location." }), + Schema.Null + ])), + "search_context_size": Schema.optionalKey( + Schema.Literals(["low", "medium", "high"]).annotate({ + "description": + "High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default." + }) + ), + "search_content_types": Schema.optionalKey(Schema.Array(SearchContentType)) +}).annotate({ + "title": "Web search preview", + "description": + "This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search)." +}) +export type CodeInterpreterToolCall = { + readonly "type": "code_interpreter_call" + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" | "interpreting" | "failed" + readonly "container_id": string + readonly "code": string | null + readonly "outputs": ReadonlyArray | null +} +export const CodeInterpreterToolCall = Schema.Struct({ + "type": Schema.Literal("code_interpreter_call").annotate({ + "description": "The type of the code interpreter tool call. Always `code_interpreter_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the code interpreter tool call.\n" }), + "status": Schema.Literals(["in_progress", "completed", "incomplete", "interpreting", "failed"]).annotate({ + "description": + "The status of the code interpreter tool call. Valid values are `in_progress`, `completed`, `incomplete`, `interpreting`, and `failed`.\n" + }), + "container_id": Schema.String.annotate({ "description": "The ID of the container used to run the code.\n" }), + "code": Schema.Union([ + Schema.String.annotate({ "description": "The code to run, or null if not available.\n" }), + Schema.Null + ]), + "outputs": Schema.Union([ + Schema.Array(Schema.Union([CodeInterpreterOutputLogs, CodeInterpreterOutputImage], { mode: "oneOf" })).annotate({ + "description": + "The outputs generated by the code interpreter, such as logs or images.\nCan be null if no outputs are available.\n" + }), + Schema.Null + ]) +}).annotate({ "title": "Code interpreter tool call", "description": "A tool call to run code.\n" }) +export type LocalShellToolCall = { + readonly "type": "local_shell_call" + readonly "id": string + readonly "call_id": string + readonly "action": LocalShellExecAction + readonly "status": "in_progress" | "completed" | "incomplete" +} +export const LocalShellToolCall = Schema.Struct({ + "type": Schema.Literal("local_shell_call").annotate({ + "description": "The type of the local shell call. Always `local_shell_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the local shell call.\n" }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the local shell tool call generated by the model.\n" + }), + "action": LocalShellExecAction, + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the local shell call.\n" + }) +}).annotate({ "title": "Local shell call", "description": "A tool call to run a command on the local shell.\n" }) +export type FunctionShellCall = { + readonly "type": "shell_call" + readonly "id": string + readonly "call_id": string + readonly "action": { + readonly "commands": ReadonlyArray + readonly "timeout_ms": number | null + readonly "max_output_length": number | null + } + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "environment": LocalEnvironmentResource | ContainerReferenceResource | null + readonly "created_by"?: string +} +export const FunctionShellCall = Schema.Struct({ + "type": Schema.Literal("shell_call").annotate({ "description": "The type of the item. Always `shell_call`." }), + "id": Schema.String.annotate({ + "description": "The unique ID of the shell tool call. Populated when this item is returned via API." + }), + "call_id": Schema.String.annotate({ "description": "The unique ID of the shell tool call generated by the model." }), + "action": Schema.Struct({ + "commands": Schema.Array(Schema.String.annotate({ "description": "A list of commands to run." })), + "timeout_ms": Schema.Union([ + Schema.Number.annotate({ "description": "Optional timeout in milliseconds for the commands." }).check( + Schema.isInt() + ), + Schema.Null + ]), + "max_output_length": Schema.Union([ + Schema.Number.annotate({ "description": "Optional maximum number of characters to return from each command." }) + .check(Schema.isInt()), + Schema.Null + ]) + }).annotate({ + "title": "Shell exec action", + "description": "The shell commands and limits that describe how to run the tool call." + }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the shell call. One of `in_progress`, `completed`, or `incomplete`." + }), + "environment": Schema.Union([ + Schema.Union([LocalEnvironmentResource, ContainerReferenceResource], { mode: "oneOf" }), + Schema.Null + ]), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the entity that created this tool call." }) + ) +}).annotate({ + "title": "Shell tool call", + "description": "A tool call that executes one or more shell commands in a managed environment." +}) +export type FunctionShellCallOutputContent = { + readonly "stdout": string + readonly "stderr": string + readonly "outcome": FunctionShellCallOutputTimeoutOutcome | FunctionShellCallOutputExitOutcome + readonly "created_by"?: string +} +export const FunctionShellCallOutputContent = Schema.Struct({ + "stdout": Schema.String.annotate({ "description": "The standard output that was captured." }), + "stderr": Schema.String.annotate({ "description": "The standard error output that was captured." }), + "outcome": Schema.Union([FunctionShellCallOutputTimeoutOutcome, FunctionShellCallOutputExitOutcome], { + mode: "oneOf" + }).annotate({ + "title": "Shell call outcome", + "description": + "Represents either an exit outcome (with an exit code) or a timeout outcome for a shell call output chunk." + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item." }) + ) +}).annotate({ + "title": "Shell call output content", + "description": "The content of a shell tool call output that was emitted." +}) +export type ApplyPatchToolCall = { + readonly "type": "apply_patch_call" + readonly "id": string + readonly "call_id": string + readonly "status": "in_progress" | "completed" + readonly "operation": ApplyPatchCreateFileOperation | ApplyPatchDeleteFileOperation | ApplyPatchUpdateFileOperation + readonly "created_by"?: string +} +export const ApplyPatchToolCall = Schema.Struct({ + "type": Schema.Literal("apply_patch_call").annotate({ + "description": "The type of the item. Always `apply_patch_call`." + }), + "id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call. Populated when this item is returned via API." + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call generated by the model." + }), + "status": Schema.Literals(["in_progress", "completed"]).annotate({ + "description": "The status of the apply patch tool call. One of `in_progress` or `completed`." + }), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchDeleteFileOperation, + ApplyPatchUpdateFileOperation + ], { mode: "oneOf" }).annotate({ + "title": "Apply patch operation", + "description": "One of the create_file, delete_file, or update_file operations applied via apply_patch." + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the entity that created this tool call." }) + ) +}).annotate({ + "title": "Apply patch tool call", + "description": "A tool call that applies file diffs by creating, deleting, or updating files." +}) +export type FunctionShellCallOutputContentParam = { + readonly "stdout": string + readonly "stderr": string + readonly "outcome": FunctionShellCallOutputTimeoutOutcomeParam | FunctionShellCallOutputExitOutcomeParam +} +export const FunctionShellCallOutputContentParam = Schema.Struct({ + "stdout": Schema.String.annotate({ "description": "Captured stdout output for the shell call." }).check( + Schema.isMaxLength(10485760) + ), + "stderr": Schema.String.annotate({ "description": "Captured stderr output for the shell call." }).check( + Schema.isMaxLength(10485760) + ), + "outcome": Schema.Union([FunctionShellCallOutputTimeoutOutcomeParam, FunctionShellCallOutputExitOutcomeParam], { + mode: "oneOf" + }).annotate({ + "title": "Shell call outcome", + "description": "The exit or timeout outcome associated with this shell call." + }) +}).annotate({ + "title": "Shell output content", + "description": "Captured stdout and stderr for a portion of a shell tool call output." +}) +export type ImageGenUsage = { + readonly "input_tokens": number + readonly "total_tokens": number + readonly "output_tokens": number + readonly "output_tokens_details"?: ImageGenOutputTokensDetails + readonly "input_tokens_details": ImageGenInputUsageDetails +} +export const ImageGenUsage = Schema.Struct({ + "input_tokens": Schema.Number.annotate({ + "description": "The number of tokens (images and text) in the input prompt." + }).check(Schema.isInt()), + "total_tokens": Schema.Number.annotate({ + "description": "The total number of tokens (images and text) used for the image generation." + }).check(Schema.isInt()), + "output_tokens": Schema.Number.annotate({ "description": "The number of output tokens generated by the model." }) + .check(Schema.isInt()), + "output_tokens_details": Schema.optionalKey(ImageGenOutputTokensDetails), + "input_tokens_details": ImageGenInputUsageDetails +}).annotate({ + "title": "Image generation usage", + "description": "For `gpt-image-1` only, the token usage information for the image generation." +}) +export type ToolChoiceParam = + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam +export const ToolChoiceParam = Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam +], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" +}) +export type ConversationParam = string | ConversationParam_2 +export const ConversationParam = Schema.Union([ + Schema.String.annotate({ "title": "Conversation ID", "description": "The unique ID of the conversation.\n" }), + ConversationParam_2 +], { mode: "oneOf" }).annotate({ + "description": + "The conversation that this response belongs to. Items from this conversation are prepended to `input_items` for this response request.\nInput items and output items from this response are automatically added to this conversation after this response completes.\n" +}) +export type VideoListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean +} +export const VideoListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(VideoResource).annotate({ "description": "A list of items" }), + "first_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the first item in the list." }), + Schema.Null + ]), + "last_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the last item in the list." }), + Schema.Null + ]), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }) +}) +export type CreateVideoMultipartBody = { + readonly "model"?: + | string + | "sora-2" + | "sora-2-pro" + | "sora-2-2025-10-06" + | "sora-2-pro-2025-10-06" + | "sora-2-2025-12-08" + readonly "prompt": string + readonly "input_reference"?: string | ImageRefParam_2 + readonly "seconds"?: "4" | "8" | "12" + readonly "size"?: "720x1280" | "1280x720" | "1024x1792" | "1792x1024" +} +export const CreateVideoMultipartBody = Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["sora-2", "sora-2-pro", "sora-2-2025-10-06", "sora-2-pro-2025-10-06", "sora-2-2025-12-08"]) + ]).annotate({ + "description": "The video generation model to use (allowed values: sora-2, sora-2-pro). Defaults to `sora-2`." + }) + ), + "prompt": Schema.String.annotate({ "description": "Text prompt that describes the video to generate." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)), + "input_reference": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "Optional reference asset upload or reference object that guides generation.", + "format": "binary" + }), + ImageRefParam_2 + ], { mode: "oneOf" }) + ), + "seconds": Schema.optionalKey( + Schema.Literals(["4", "8", "12"]).annotate({ + "description": "Clip duration in seconds (allowed values: 4, 8, 12). Defaults to 4 seconds." + }) + ), + "size": Schema.optionalKey( + Schema.Literals(["720x1280", "1280x720", "1024x1792", "1792x1024"]).annotate({ + "description": + "Output resolution formatted as width x height (allowed values: 720x1280, 1280x720, 1024x1792, 1792x1024). Defaults to 720x1280." + }) + ) +}).annotate({ + "title": "Create video multipart request", + "description": "Multipart parameters for creating a new video generation job." +}) +export type CreateVideoEditMultipartBody = { + readonly "video": string | VideoReferenceInputParam + readonly "prompt": string +} +export const CreateVideoEditMultipartBody = Schema.Struct({ + "video": Schema.Union([ + Schema.String.annotate({ "description": "Reference to the completed video to edit.", "format": "binary" }), + VideoReferenceInputParam + ], { mode: "oneOf" }), + "prompt": Schema.String.annotate({ "description": "Text prompt that describes how to edit the source video." }).check( + Schema.isMinLength(1) + ).check(Schema.isMaxLength(32000)) +}).annotate({ + "title": "Create video edit multipart request", + "description": "Parameters for editing an existing generated video." +}) +export type CreateVideoExtendMultipartBody = { + readonly "video": VideoReferenceInputParam | string + readonly "prompt": string + readonly "seconds": "4" | "8" | "12" +} +export const CreateVideoExtendMultipartBody = Schema.Struct({ + "video": Schema.Union([ + VideoReferenceInputParam, + Schema.String.annotate({ "description": "Reference to the completed video to extend.", "format": "binary" }) + ], { mode: "oneOf" }), + "prompt": Schema.String.annotate({ "description": "Updated text prompt that directs the extension generation." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(32000)), + "seconds": Schema.Literals(["4", "8", "12"]).annotate({ + "description": "Length of the newly generated extension segment in seconds (allowed values: 4, 8, 12, 16, 20)." + }) +}).annotate({ + "title": "Create video extension multipart request", + "description": "Multipart parameters for extending an existing generated video." +}) +export type SkillListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean +} +export const SkillListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(SkillResource).annotate({ "description": "A list of items" }), + "first_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the first item in the list." }), + Schema.Null + ]), + "last_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the last item in the list." }), + Schema.Null + ]), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }) +}) +export type SkillVersionListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean +} +export const SkillVersionListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(SkillVersionResource).annotate({ "description": "A list of items" }), + "first_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the first item in the list." }), + Schema.Null + ]), + "last_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the last item in the list." }), + Schema.Null + ]), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }) +}) +export type UserMessageItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.user_message" + readonly "content": ReadonlyArray + readonly "attachments": ReadonlyArray + readonly "inference_options": { + readonly "tool_choice": { readonly "id": string } | null + readonly "model": string | null + } | null +} +export const UserMessageItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.user_message"), + "content": Schema.Array( + Schema.Union([UserMessageInputText, UserMessageQuotedText], { mode: "oneOf" }).annotate({ + "description": "Content blocks that comprise a user message." + }) + ).annotate({ "description": "Ordered content elements supplied by the user." }), + "attachments": Schema.Array(Attachment).annotate({ + "description": "Attachments associated with the user message. Defaults to an empty list." + }), + "inference_options": Schema.Union([ + Schema.Struct({ + "tool_choice": Schema.Union([ + Schema.Struct({ "id": Schema.String.annotate({ "description": "Identifier of the requested tool." }) }) + .annotate({ + "title": "Tool choice", + "description": "Preferred tool to invoke. Defaults to null when ChatKit should auto-select." + }), + Schema.Null + ]), + "model": Schema.Union([ + Schema.String.annotate({ + "description": "Model name that generated the response. Defaults to null when using the session default." + }), + Schema.Null + ]) + }).annotate({ + "title": "Inference options", + "description": "Inference overrides applied to the message. Defaults to null when unset." + }), + Schema.Null + ]) +}).annotate({ "title": "User Message Item", "description": "User-authored messages within a thread." }) +export type ResponseOutputText = { + readonly "type": "output_text" + readonly "text": string + readonly "annotations": ReadonlyArray +} +export const ResponseOutputText = Schema.Struct({ + "type": Schema.Literal("output_text").annotate({ "description": "Type discriminator that is always `output_text`." }), + "text": Schema.String.annotate({ "description": "Assistant generated text." }), + "annotations": Schema.Array( + Schema.Union([FileAnnotation, UrlAnnotation], { mode: "oneOf" }).annotate({ + "description": "Annotation object describing a cited source." + }) + ).annotate({ "description": "Ordered list of annotations attached to the response text." }) +}).annotate({ + "title": "Assistant message content", + "description": "Assistant response text accompanied by optional annotations." +}) +export type TaskGroupItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.task_group" + readonly "tasks": ReadonlyArray +} +export const TaskGroupItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.task_group").annotate({ + "description": "Type discriminator that is always `chatkit.task_group`." + }), + "tasks": Schema.Array(TaskGroupTask).annotate({ "description": "Tasks included in the group." }) +}).annotate({ "title": "Task group", "description": "Collection of workflow tasks grouped together in the thread." }) +export type ThreadResource = { + readonly "id": string + readonly "object": "chatkit.thread" + readonly "created_at": number + readonly "title": string | null + readonly "status": ActiveStatus | LockedStatus | ClosedStatus + readonly "user": string +} +export const ThreadResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread." }), + "object": Schema.Literal("chatkit.thread").annotate({ + "description": "Type discriminator that is always `chatkit.thread`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the thread was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "title": Schema.Union([ + Schema.String.annotate({ + "description": "Optional human-readable title for the thread. Defaults to null when no title has been generated." + }), + Schema.Null + ]), + "status": Schema.Union([ActiveStatus, LockedStatus, ClosedStatus], { mode: "oneOf" }).annotate({ + "description": "Current status for the thread. Defaults to `active` for newly created threads." + }), + "user": Schema.String.annotate({ + "description": "Free-form string that identifies your end user who owns the thread." + }) +}).annotate({ "title": "The thread object", "description": "Represents a ChatKit thread and its current status." }) +export type AuditLogActor = { + readonly "type"?: "session" | "api_key" + readonly "session"?: AuditLogActorSession + readonly "api_key"?: AuditLogActorApiKey +} +export const AuditLogActor = Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["session", "api_key"]).annotate({ + "description": "The type of actor. Is either `session` or `api_key`." + }) + ), + "session": Schema.optionalKey(AuditLogActorSession), + "api_key": Schema.optionalKey(AuditLogActorApiKey) +}).annotate({ "description": "The actor who performed the audit logged action." }) +export type ChatCompletionToolChoiceOption = + | "none" + | "auto" + | "required" + | ChatCompletionAllowedToolsChoice + | ChatCompletionNamedToolChoice + | ChatCompletionNamedToolChoiceCustom +export const ChatCompletionToolChoiceOption = Schema.Union([ + Schema.Literals(["none", "auto", "required"]).annotate({ + "title": "Tool choice mode", + "description": + "`none` means the model will not call any tool and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools.\n" + }), + ChatCompletionAllowedToolsChoice, + ChatCompletionNamedToolChoice, + ChatCompletionNamedToolChoiceCustom +], { mode: "oneOf" }).annotate({ + "description": + "Controls which (if any) tool is called by the model.\n`none` means the model will not call any tool and instead generates a message.\n`auto` means the model can pick between generating a message or calling one or more tools.\n`required` means the model must call one or more tools.\nSpecifying a particular tool via `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that tool.\n\n`none` is the default when no tools are present. `auto` is the default if tools are present.\n" +}) +export type ChatCompletionMessageList = { + readonly "object": "list" + readonly "data": ReadonlyArray< + { + readonly "content": string | null + readonly "refusal": string | null + readonly "tool_calls"?: ChatCompletionMessageToolCalls + readonly "annotations"?: ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "end_index": number + readonly "start_index": number + readonly "url": string + readonly "title": string + } + } + > + readonly "role": "assistant" + readonly "function_call"?: { readonly "arguments": string; readonly "name": string } + readonly "audio"?: { + readonly "id": string + readonly "expires_at": number + readonly "data": string + readonly "transcript": string + } | null + readonly "id": string + readonly "content_parts"?: + | ReadonlyArray + | null + } + > + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ChatCompletionMessageList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ + "description": "The type of this object. It is always set to \"list\".\n" + }), + "data": Schema.Array( + Schema.Struct({ + "content": Schema.Union([Schema.String.annotate({ "description": "The contents of the message." }), Schema.Null]), + "refusal": Schema.Union([ + Schema.String.annotate({ "description": "The refusal message generated by the model." }), + Schema.Null + ]), + "tool_calls": Schema.optionalKey(ChatCompletionMessageToolCalls), + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("url_citation").annotate({ + "description": "The type of the URL citation. Always `url_citation`." + }), + "url_citation": Schema.Struct({ + "end_index": Schema.Number.annotate({ + "description": "The index of the last character of the URL citation in the message." + }).check(Schema.isInt()), + "start_index": Schema.Number.annotate({ + "description": "The index of the first character of the URL citation in the message." + }).check(Schema.isInt()), + "url": Schema.String.annotate({ "description": "The URL of the web resource.", "format": "uri" }), + "title": Schema.String.annotate({ "description": "The title of the web resource." }) + }).annotate({ "description": "A URL citation when using web search." }) + }).annotate({ "description": "A URL citation when using web search.\n" }) + ).annotate({ + "description": + "Annotations for the message, when applicable, as when using the\n[web search tool](/docs/guides/tools-web-search?api-mode=chat).\n" + }) + ), + "role": Schema.Literal("assistant").annotate({ "description": "The role of the author of this message." }), + "function_call": Schema.optionalKey( + Schema.Struct({ + "arguments": Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + }), + "name": Schema.String.annotate({ "description": "The name of the function to call." }) + }).annotate({ + "description": + "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model." + }) + ), + "audio": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for this audio response." }), + "expires_at": Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) for when this audio response will\nno longer be accessible on the server for use in multi-turn\nconversations.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "data": Schema.String.annotate({ + "description": + "Base64 encoded audio bytes generated by the model, in the format\nspecified in the request.\n" + }), + "transcript": Schema.String.annotate({ "description": "Transcript of the audio generated by the model." }) + }).annotate({ + "description": + "If the audio output modality is requested, this object contains data\nabout the audio response from the model. [Learn more](/docs/guides/audio).\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "The identifier of the chat message." }), + "content_parts": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ChatCompletionRequestMessageContentPartText, ChatCompletionRequestMessageContentPartImage], { + mode: "oneOf" + }) + ).annotate({ + "description": + "If a content parts array was provided, this is an array of `text` and `image_url` parts.\nOtherwise, null.\n" + }), + Schema.Null + ]) + ) + }).annotate({ "description": "A chat completion message generated by the model." }) + ).annotate({ "description": "An array of chat completion message objects.\n" }), + "first_id": Schema.String.annotate({ "description": "The identifier of the first chat message in the data array." }), + "last_id": Schema.String.annotate({ "description": "The identifier of the last chat message in the data array." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates whether there are more chat messages available." }) +}).annotate({ + "title": "ChatCompletionMessageList", + "description": "An object representing a list of chat completion messages.\n" +}) +export type ChatCompletionResponseMessage = { + readonly "content": string | null + readonly "refusal": string | null + readonly "tool_calls"?: ChatCompletionMessageToolCalls + readonly "annotations"?: ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "end_index": number + readonly "start_index": number + readonly "url": string + readonly "title": string + } + } + > + readonly "role": "assistant" + readonly "function_call"?: { readonly "arguments": string; readonly "name": string } + readonly "audio"?: { + readonly "id": string + readonly "expires_at": number + readonly "data": string + readonly "transcript": string + } | null +} +export const ChatCompletionResponseMessage = Schema.Struct({ + "content": Schema.Union([Schema.String.annotate({ "description": "The contents of the message." }), Schema.Null]), + "refusal": Schema.Union([ + Schema.String.annotate({ "description": "The refusal message generated by the model." }), + Schema.Null + ]), + "tool_calls": Schema.optionalKey(ChatCompletionMessageToolCalls), + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("url_citation").annotate({ + "description": "The type of the URL citation. Always `url_citation`." + }), + "url_citation": Schema.Struct({ + "end_index": Schema.Number.annotate({ + "description": "The index of the last character of the URL citation in the message." + }).check(Schema.isInt()), + "start_index": Schema.Number.annotate({ + "description": "The index of the first character of the URL citation in the message." + }).check(Schema.isInt()), + "url": Schema.String.annotate({ "description": "The URL of the web resource.", "format": "uri" }), + "title": Schema.String.annotate({ "description": "The title of the web resource." }) + }).annotate({ "description": "A URL citation when using web search." }) + }).annotate({ "description": "A URL citation when using web search.\n" }) + ).annotate({ + "description": + "Annotations for the message, when applicable, as when using the\n[web search tool](/docs/guides/tools-web-search?api-mode=chat).\n" + }) + ), + "role": Schema.Literal("assistant").annotate({ "description": "The role of the author of this message." }), + "function_call": Schema.optionalKey( + Schema.Struct({ + "arguments": Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + }), + "name": Schema.String.annotate({ "description": "The name of the function to call." }) + }).annotate({ + "description": + "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model." + }) + ), + "audio": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for this audio response." }), + "expires_at": Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) for when this audio response will\nno longer be accessible on the server for use in multi-turn\nconversations.\n", + "format": "unixtime" + }).check(Schema.isInt()), + "data": Schema.String.annotate({ + "description": "Base64 encoded audio bytes generated by the model, in the format\nspecified in the request.\n" + }), + "transcript": Schema.String.annotate({ "description": "Transcript of the audio generated by the model." }) + }).annotate({ + "description": + "If the audio output modality is requested, this object contains data\nabout the audio response from the model. [Learn more](/docs/guides/audio).\n" + }), + Schema.Null + ])) +}).annotate({ "description": "A chat completion message generated by the model." }) +export type CreateChatCompletionStreamResponse = { + readonly "id": string + readonly "choices": ReadonlyArray< + { + readonly "delta": ChatCompletionStreamResponseDelta + readonly "logprobs"?: { + readonly "content": ReadonlyArray< + { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray | null + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "logprob": number; readonly "bytes": ReadonlyArray | null } + > + } + > + readonly "refusal": ReadonlyArray< + { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray | null + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "logprob": number; readonly "bytes": ReadonlyArray | null } + > + } + > + } + readonly "finish_reason": "stop" | "length" | "tool_calls" | "content_filter" | "function_call" | null + readonly "index": number + } + > + readonly "created": number + readonly "model": string + readonly "service_tier"?: ServiceTier + readonly "system_fingerprint"?: string + readonly "object": "chat.completion.chunk" + readonly "usage"?: { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "total_tokens": number + readonly "completion_tokens_details"?: { + readonly "accepted_prediction_tokens"?: number + readonly "audio_tokens"?: number + readonly "reasoning_tokens"?: number + readonly "rejected_prediction_tokens"?: number + } + readonly "prompt_tokens_details"?: { readonly "audio_tokens"?: number; readonly "cached_tokens"?: number } + } | null +} +export const CreateChatCompletionStreamResponse = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "A unique identifier for the chat completion. Each chunk has the same ID." + }), + "choices": Schema.Array(Schema.Struct({ + "delta": ChatCompletionStreamResponseDelta, + "logprobs": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "content": Schema.Union([ + Schema.Array(Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]), + "top_logprobs": Schema.Array(Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]) + })).annotate({ + "description": + "List of the most likely tokens and their log probability, at this token position. The number of entries may be fewer than the requested `top_logprobs`." + }) + })).annotate({ "description": "A list of message content tokens with log probability information." }) + ]), + "refusal": Schema.Union([ + Schema.Array(Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]), + "top_logprobs": Schema.Array(Schema.Struct({ + "token": Schema.String.annotate({ "description": "The token." }), + "logprob": Schema.Number.annotate({ + "description": + "The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely." + }).check(Schema.isFinite()), + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt())).annotate({ + "description": + "A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token." + }), + Schema.Null + ]) + })).annotate({ + "description": + "List of the most likely tokens and their log probability, at this token position. The number of entries may be fewer than the requested `top_logprobs`." + }) + })).annotate({ "description": "A list of message refusal tokens with log probability information." }) + ]) + }).annotate({ "description": "Log probability information for the choice." }) + ])), + "finish_reason": Schema.Union([ + Schema.Literals(["stop", "length", "tool_calls", "content_filter", "function_call"]).annotate({ + "description": + "The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence,\n`length` if the maximum number of tokens specified in the request was reached,\n`content_filter` if content was omitted due to a flag from our content filters,\n`tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.\n" + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence,\n`length` if the maximum number of tokens specified in the request was reached,\n`content_filter` if content was omitted due to a flag from our content filters,\n`tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.\n" + }) + ]), + "index": Schema.Number.annotate({ "description": "The index of the choice in the list of choices." }).check( + Schema.isInt() + ) + })).annotate({ + "description": + "A list of chat completion choices. Can contain more than one elements if `n` is greater than 1. Can also be empty for the\nlast chunk if you set `stream_options: {\"include_usage\": true}`.\n" + }), + "created": Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ "description": "The model to generate the completion." }), + "service_tier": Schema.optionalKey(ServiceTier), + "system_fingerprint": Schema.optionalKey( + Schema.String.annotate({ + "description": + "This fingerprint represents the backend configuration that the model runs with.\nCan be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism.\n" + }) + ), + "object": Schema.Literal("chat.completion.chunk").annotate({ + "description": "The object type, which is always `chat.completion.chunk`." + }), + "usage": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ "description": "Number of tokens in the generated completion." }) + .check(Schema.isInt()), + "prompt_tokens": Schema.Number.annotate({ "description": "Number of tokens in the prompt." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ + "description": "Total number of tokens used in the request (prompt + completion)." + }).check(Schema.isInt()), + "completion_tokens_details": Schema.optionalKey( + Schema.Struct({ + "accepted_prediction_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "When using Predicted Outputs, the number of tokens in the\nprediction that appeared in the completion.\n" + }).check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio input tokens generated by the model." }).check( + Schema.isInt() + ) + ), + "reasoning_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Tokens generated by the model for reasoning." }).check( + Schema.isInt() + ) + ), + "rejected_prediction_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "When using Predicted Outputs, the number of tokens in the\nprediction that did not appear in the completion. However, like\nreasoning tokens, these tokens are still counted in the total\ncompletion tokens for purposes of billing, output, and context window\nlimits.\n" + }).check(Schema.isInt()) + ) + }).annotate({ "description": "Breakdown of tokens used in a completion." }) + ), + "prompt_tokens_details": Schema.optionalKey( + Schema.Struct({ + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio input tokens present in the prompt." }).check(Schema.isInt()) + ), + "cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cached tokens present in the prompt." }).check(Schema.isInt()) + ) + }).annotate({ "description": "Breakdown of tokens used in the prompt." }) + ) + }).annotate({ + "description": + "An optional field that will only be present when you set\n`stream_options: {\"include_usage\": true}` in your request. When present, it\ncontains a null value **except for the last chunk** which contains the\ntoken usage statistics for the entire request.\n\n**NOTE:** If the stream is interrupted or cancelled, you may not\nreceive the final usage chunk which contains the total token usage for\nthe request.\n" + }), + Schema.Null + ])) +}).annotate({ + "description": + "Represents a streamed chunk of a chat completion response returned\nby the model, based on the provided input. \n[Learn more](/docs/guides/streaming-responses).\n" +}) +export type ChatCompletionRequestAssistantMessage = { + readonly "content"?: string | ReadonlyArray | null + readonly "refusal"?: string | null + readonly "role": "assistant" + readonly "name"?: string + readonly "audio"?: { readonly "id": string } | null + readonly "tool_calls"?: ChatCompletionMessageToolCalls + readonly "function_call"?: { readonly "arguments": string; readonly "name": string } | null +} +export const ChatCompletionRequestAssistantMessage = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The contents of the assistant message." }), + Schema.Array(ChatCompletionRequestAssistantMessageContentPart).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type. Can be one or more of type `text`, or exactly one of type `refusal`." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ + "description": + "The contents of the assistant message. Required unless `tool_calls` or `function_call` is specified.\n" + }), + Schema.Null + ]) + ), + "refusal": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The refusal message by the assistant." }), Schema.Null]) + ), + "role": Schema.Literal("assistant").annotate({ + "description": "The role of the messages author, in this case `assistant`." + }), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional name for the participant. Provides the model information to differentiate between participants of the same role." + }) + ), + "audio": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "id": Schema.String.annotate({ + "description": "Unique identifier for a previous audio response from the model.\n" + }) + }).annotate({ + "description": "Data about a previous audio response from the model.\n[Learn more](/docs/guides/audio).\n" + }), + Schema.Null + ]) + ), + "tool_calls": Schema.optionalKey(ChatCompletionMessageToolCalls), + "function_call": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "arguments": Schema.String.annotate({ + "description": + "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." + }), + "name": Schema.String.annotate({ "description": "The name of the function to call." }) + }).annotate({ + "description": + "Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model." + }), + Schema.Null + ])) +}).annotate({ + "title": "Assistant message", + "description": "Messages sent by the model in response to user messages.\n" +}) +export type ChatCompletionRequestSystemMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "system" + readonly "name"?: string +} +export const ChatCompletionRequestSystemMessage = Schema.Struct({ + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The contents of the system message." }), + Schema.Array(ChatCompletionRequestSystemMessageContentPart).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type. For system messages, only type `text` is supported." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ "description": "The contents of the system message." }), + "role": Schema.Literal("system").annotate({ + "description": "The role of the messages author, in this case `system`." + }), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional name for the participant. Provides the model information to differentiate between participants of the same role." + }) + ) +}).annotate({ + "title": "System message", + "description": + "Developer-provided instructions that the model should follow, regardless of\nmessages sent by the user. With o1 models and newer, use `developer` messages\nfor this purpose instead.\n" +}) +export type ChatCompletionRequestToolMessage = { + readonly "role": "tool" + readonly "content": string | ReadonlyArray + readonly "tool_call_id": string +} +export const ChatCompletionRequestToolMessage = Schema.Struct({ + "role": Schema.Literal("tool").annotate({ "description": "The role of the messages author, in this case `tool`." }), + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The contents of the tool message." }), + Schema.Array(ChatCompletionRequestToolMessageContentPart).annotate({ + "title": "Array of content parts", + "description": "An array of content parts with a defined type. For tool messages, only type `text` is supported." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ "description": "The contents of the tool message." }), + "tool_call_id": Schema.String.annotate({ "description": "Tool call that this message is responding to." }) +}).annotate({ "title": "Tool message" }) +export type ChatCompletionRequestUserMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "user" + readonly "name"?: string +} +export const ChatCompletionRequestUserMessage = Schema.Struct({ + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The text contents of the message." }), + Schema.Array(ChatCompletionRequestUserMessageContentPart).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type. Supported options differ based on the [model](/docs/models) being used to generate the response. Can contain text, image, or audio inputs." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }).annotate({ "description": "The contents of the user message.\n" }), + "role": Schema.Literal("user").annotate({ "description": "The role of the messages author, in this case `user`." }), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An optional name for the participant. Provides the model information to differentiate between participants of the same role." + }) + ) +}).annotate({ + "title": "User message", + "description": "Messages sent by an end user, containing prompts or additional context\ninformation.\n" +}) +export type VectorStoreSearchRequest = { + readonly "query": string | ReadonlyArray + readonly "rewrite_query"?: boolean + readonly "max_num_results"?: number + readonly "filters"?: ComparisonFilter | CompoundFilter + readonly "ranking_options"?: { + readonly "ranker"?: "none" | "auto" | "default-2024-11-15" + readonly "score_threshold"?: number + } +} +export const VectorStoreSearchRequest = Schema.Struct({ + "query": Schema.Union([ + Schema.String, + Schema.Array(Schema.String.annotate({ "description": "A list of queries to search for." })) + ], { mode: "oneOf" }).annotate({ "description": "A query string for a search" }), + "rewrite_query": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to rewrite the natural language query for vector search." }) + ), + "max_num_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum number of results to return. This number should be between 1 and 50 inclusive." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(50)) + ), + "filters": Schema.optionalKey( + Schema.Union([ComparisonFilter, CompoundFilter], { mode: "oneOf" }).annotate({ + "description": "A filter to apply based on file attributes." + }) + ), + "ranking_options": Schema.optionalKey( + Schema.Struct({ + "ranker": Schema.optionalKey( + Schema.Literals(["none", "auto", "default-2024-11-15"]).annotate({ + "description": "Enable re-ranking; set to `none` to disable, which can help reduce latency." + }) + ), + "score_threshold": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( + Schema.isLessThanOrEqualTo(1) + ) + ) + }).annotate({ "description": "Ranking options for search." }) + ) +}) +export type FileSearchTool = { + readonly "type": "file_search" + readonly "vector_store_ids": ReadonlyArray + readonly "max_num_results"?: number + readonly "ranking_options"?: { + readonly "ranker"?: "auto" | "default-2024-11-15" + readonly "score_threshold"?: number + readonly "hybrid_search"?: { readonly "embedding_weight": number; readonly "text_weight": number } + } + readonly "filters"?: ComparisonFilter | CompoundFilter | null +} +export const FileSearchTool = Schema.Struct({ + "type": Schema.Literal("file_search").annotate({ + "description": "The type of the file search tool. Always `file_search`." + }), + "vector_store_ids": Schema.Array(Schema.String).annotate({ + "description": "The IDs of the vector stores to search." + }), + "max_num_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum number of results to return. This number should be between 1 and 50 inclusive." + }).check(Schema.isInt()) + ), + "ranking_options": Schema.optionalKey( + Schema.Struct({ + "ranker": Schema.optionalKey( + Schema.Literals(["auto", "default-2024-11-15"]).annotate({ + "description": "The ranker to use for the file search." + }) + ), + "score_threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The score threshold for the file search, a number between 0 and 1. Numbers closer to 1 will attempt to return only the most relevant results, but may return fewer results." + }).check(Schema.isFinite()) + ), + "hybrid_search": Schema.optionalKey( + Schema.Struct({ + "embedding_weight": Schema.Number.annotate({ + "description": "The weight of the embedding in the reciprocal ranking fusion." + }).check(Schema.isFinite()), + "text_weight": Schema.Number.annotate({ + "description": "The weight of the text in the reciprocal ranking fusion." + }).check(Schema.isFinite()) + }).annotate({ + "description": + "Weights that control how reciprocal rank fusion balances semantic embedding matches versus sparse keyword matches when hybrid search is enabled." + }) + ) + }).annotate({ "description": "Ranking options for search." }) + ), + "filters": Schema.optionalKey( + Schema.Union([ + Schema.Union([ComparisonFilter, CompoundFilter]).annotate({ "description": "A filter to apply." }), + Schema.Null + ]) + ) +}).annotate({ + "title": "File search", + "description": + "A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search)." +}) +export type EvalRunOutputItemList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const EvalRunOutputItemList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ + "description": "The type of this object. It is always set to \"list\".\n" + }), + "data": Schema.Array(EvalRunOutputItem).annotate({ "description": "An array of eval run output item objects.\n" }), + "first_id": Schema.String.annotate({ + "description": "The identifier of the first eval run output item in the data array." + }), + "last_id": Schema.String.annotate({ + "description": "The identifier of the last eval run output item in the data array." + }), + "has_more": Schema.Boolean.annotate({ + "description": "Indicates whether there are more eval run output items available." + }) +}).annotate({ + "title": "EvalRunOutputItemList", + "description": "An object representing a list of output items for an evaluation run.\n" +}) +export type AssistantToolsFileSearch = { + readonly "type": "file_search" + readonly "file_search"?: { + readonly "max_num_results"?: number + readonly "ranking_options"?: FileSearchRankingOptions + } +} +export const AssistantToolsFileSearch = Schema.Struct({ + "type": Schema.Literal("file_search").annotate({ "description": "The type of tool being defined: `file_search`" }), + "file_search": Schema.optionalKey( + Schema.Struct({ + "max_num_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The maximum number of results the file search tool should output. The default is 20 for `gpt-4*` models and 5 for `gpt-3.5-turbo`. This number should be between 1 and 50 inclusive.\n\nNote that the file search tool may output fewer than `max_num_results` results. See the [file search tool documentation](/docs/assistants/tools/file-search#customizing-file-search-settings) for more information.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check(Schema.isLessThanOrEqualTo(50)) + ), + "ranking_options": Schema.optionalKey(FileSearchRankingOptions) + }).annotate({ "description": "Overrides for the file search tool." }) + ) +}).annotate({ "title": "FileSearch tool" }) +export type RunStepDetailsToolCallsFileSearchObject = { + readonly "id": string + readonly "type": "file_search" + readonly "file_search": { + readonly "ranking_options"?: RunStepDetailsToolCallsFileSearchRankingOptionsObject + readonly "results"?: ReadonlyArray + } +} +export const RunStepDetailsToolCallsFileSearchObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of the tool call object." }), + "type": Schema.Literal("file_search").annotate({ + "description": "The type of tool call. This is always going to be `file_search` for this type of tool call." + }), + "file_search": Schema.Struct({ + "ranking_options": Schema.optionalKey(RunStepDetailsToolCallsFileSearchRankingOptionsObject), + "results": Schema.optionalKey( + Schema.Array(RunStepDetailsToolCallsFileSearchResultObject).annotate({ + "description": "The results of the file search." + }) + ) + }).annotate({ "description": "For now, this is always going to be an empty object." }) +}).annotate({ "title": "File search tool call" }) +export type AssistantToolsFunction = { readonly "type": "function"; readonly "function": FunctionObject } +export const AssistantToolsFunction = Schema.Struct({ + "type": Schema.Literal("function").annotate({ "description": "The type of tool being defined: `function`" }), + "function": FunctionObject +}).annotate({ "title": "Function tool" }) +export type ChatCompletionTool = { readonly "type": "function"; readonly "function": FunctionObject } +export const ChatCompletionTool = Schema.Struct({ + "type": Schema.Literal("function").annotate({ + "description": "The type of the tool. Currently, only `function` is supported." + }), + "function": FunctionObject +}).annotate({ "title": "Function tool", "description": "A function tool that can be used to generate a response.\n" }) +export type ImageEditStreamEvent = ImageEditPartialImageEvent | ImageEditCompletedEvent +export const ImageEditStreamEvent = Schema.Union([ImageEditPartialImageEvent, ImageEditCompletedEvent]) +export type ImageGenStreamEvent = ImageGenPartialImageEvent | ImageGenCompletedEvent +export const ImageGenStreamEvent = Schema.Union([ImageGenPartialImageEvent, ImageGenCompletedEvent]) +export type MessageObject = { + readonly "id": string + readonly "object": "thread.message" + readonly "created_at": number + readonly "thread_id": string + readonly "status": "in_progress" | "incomplete" | "completed" + readonly "incomplete_details": { + readonly "reason": "content_filter" | "max_tokens" | "run_cancelled" | "run_expired" | "run_failed" + } | null + readonly "completed_at": number | null + readonly "incomplete_at": number | null + readonly "role": "user" | "assistant" + readonly "content": ReadonlyArray< + | MessageContentImageFileObject + | MessageContentImageUrlObject + | MessageContentTextObject + | MessageContentRefusalObject + > + readonly "assistant_id": string | null + readonly "run_id": string | null + readonly "attachments": + | ReadonlyArray< + { + readonly "file_id"?: string + readonly "tools"?: ReadonlyArray + } + > + | null + readonly "metadata": Metadata +} +export const MessageObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("thread.message").annotate({ + "description": "The object type, which is always `thread.message`." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the message was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ + "description": "The [thread](/docs/api-reference/threads) ID that this message belongs to." + }), + "status": Schema.Literals(["in_progress", "incomplete", "completed"]).annotate({ + "description": "The status of the message, which can be either `in_progress`, `incomplete`, or `completed`." + }), + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.Literals(["content_filter", "max_tokens", "run_cancelled", "run_expired", "run_failed"]) + .annotate({ "description": "The reason the message is incomplete." }) + }).annotate({ "description": "On an incomplete message, details about why the message is incomplete." }), + Schema.Null + ]), + "completed_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the message was completed.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "incomplete_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the message was marked as incomplete.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "role": Schema.Literals(["user", "assistant"]).annotate({ + "description": "The entity that produced the message. One of `user` or `assistant`." + }), + "content": Schema.Array( + Schema.Union([ + MessageContentImageFileObject, + MessageContentImageUrlObject, + MessageContentTextObject, + MessageContentRefusalObject + ], { mode: "oneOf" }) + ).annotate({ "description": "The content of the message in array of text and/or images." }), + "assistant_id": Schema.Union([ + Schema.String.annotate({ + "description": + "If applicable, the ID of the [assistant](/docs/api-reference/assistants) that authored this message." + }), + Schema.Null + ]), + "run_id": Schema.Union([ + Schema.String.annotate({ + "description": + "The ID of the [run](/docs/api-reference/runs) associated with the creation of this message. Value is `null` when messages are created manually using the create message or create thread endpoints." + }), + Schema.Null + ]), + "attachments": Schema.Union([ + Schema.Array(Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the file to attach to the message." }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([AssistantToolsCode, AssistantToolsFileSearchTypeOnly], { mode: "oneOf" })).annotate({ + "description": "The tools to add this file to." + }) + ) + })).annotate({ "description": "A list of files attached to the message, and the tools they were added to." }), + Schema.Null + ]), + "metadata": Metadata +}).annotate({ + "title": "The message object", + "description": "Represents a message within a [thread](/docs/api-reference/threads)." +}) +export type MessageDeltaObject = { + readonly "id": string + readonly "object": "thread.message.delta" + readonly "delta": { + readonly "role"?: "user" | "assistant" + readonly "content"?: ReadonlyArray< + | MessageDeltaContentImageFileObject + | MessageDeltaContentTextObject + | MessageDeltaContentRefusalObject + | MessageDeltaContentImageUrlObject + > + } +} +export const MessageDeltaObject = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The identifier of the message, which can be referenced in API endpoints." + }), + "object": Schema.Literal("thread.message.delta").annotate({ + "description": "The object type, which is always `thread.message.delta`." + }), + "delta": Schema.Struct({ + "role": Schema.optionalKey( + Schema.Literals(["user", "assistant"]).annotate({ + "description": "The entity that produced the message. One of `user` or `assistant`." + }) + ), + "content": Schema.optionalKey( + Schema.Array( + Schema.Union([ + MessageDeltaContentImageFileObject, + MessageDeltaContentTextObject, + MessageDeltaContentRefusalObject, + MessageDeltaContentImageUrlObject + ], { mode: "oneOf" }) + ).annotate({ "description": "The content of the message in array of text and/or images." }) + ) + }).annotate({ "description": "The delta containing the fields that have changed on the Message." }) +}).annotate({ + "title": "Message delta object", + "description": "Represents a message delta i.e. any changed fields on a message during streaming.\n" +}) +export type ListBatchesResponse = { + readonly "data": ReadonlyArray + readonly "first_id"?: string + readonly "last_id"?: string + readonly "has_more": boolean + readonly "object": "list" +} +export const ListBatchesResponse = Schema.Struct({ + "data": Schema.Array(Batch), + "first_id": Schema.optionalKey(Schema.String), + "last_id": Schema.optionalKey(Schema.String), + "has_more": Schema.Boolean, + "object": Schema.Literal("list") +}) +export type CreateThreadRequest = { + readonly "messages"?: ReadonlyArray + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { + readonly "vector_store_ids": ReadonlyArray + readonly "vector_stores"?: ReadonlyArray< + { + readonly "file_ids"?: ReadonlyArray + readonly "chunking_strategy"?: { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": { readonly "max_chunk_size_tokens": number; readonly "chunk_overlap_tokens": number } + } + readonly "metadata"?: Metadata + } + > + } | { + readonly "vector_stores": ReadonlyArray< + { + readonly "file_ids"?: ReadonlyArray + readonly "chunking_strategy"?: { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": { readonly "max_chunk_size_tokens": number; readonly "chunk_overlap_tokens": number } + } + readonly "metadata"?: Metadata + } + > + readonly "vector_store_ids"?: ReadonlyArray + } + } | null + readonly "metadata"?: Metadata +} +export const CreateThreadRequest = Schema.Struct({ + "messages": Schema.optionalKey( + Schema.Array(CreateMessageRequest).annotate({ + "description": "A list of [messages](/docs/api-reference/messages) to start the thread with." + }) + ), + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "vector_store_ids": Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)), + "vector_stores": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs to add to the vector store. For vector stores created before Nov 2025, there can be a maximum of 10,000 files in a vector store. For vector stores created starting in Nov 2025, the limit is 100,000,000 files.\n" + }).check(Schema.isMaxLength(100000000)) + ), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }) + .annotate({ + "title": "Auto Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": Schema.Struct({ + "max_chunk_size_tokens": Schema.Number.annotate({ + "description": + "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(100)).check( + Schema.isLessThanOrEqualTo(4096) + ), + "chunk_overlap_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + }).check(Schema.isInt()) + }) + }).annotate({ + "title": "Static Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }) + ], { mode: "oneOf" }) + ), + "metadata": Schema.optionalKey(Metadata) + })).annotate({ + "description": + "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)) + ) + }), + Schema.Struct({ + "vector_stores": Schema.Array(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs to add to the vector store. For vector stores created before Nov 2025, there can be a maximum of 10,000 files in a vector store. For vector stores created starting in Nov 2025, the limit is 100,000,000 files.\n" + }).check(Schema.isMaxLength(100000000)) + ), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }) + .annotate({ + "title": "Auto Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": Schema.Struct({ + "max_chunk_size_tokens": Schema.Number.annotate({ + "description": + "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(100)).check( + Schema.isLessThanOrEqualTo(4096) + ), + "chunk_overlap_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + }).check(Schema.isInt()) + }) + }).annotate({ + "title": "Static Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }) + ], { mode: "oneOf" }) + ), + "metadata": Schema.optionalKey(Metadata) + })).annotate({ + "description": + "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)), + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread.\n" + }).check(Schema.isMaxLength(1)) + ) + }) + ], { mode: "oneOf" })) + }).annotate({ + "description": + "A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata) +}).annotate({ + "description": + "Options to create a new thread. If no thread is provided when running a\nrequest, an empty thread will be created.\n" +}) +export type ThreadStreamEvent = { + readonly "enabled"?: boolean + readonly "event": "thread.created" + readonly "data": ThreadObject +} +export const ThreadStreamEvent = Schema.Union([ + Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enable input audio transcription." }) + ), + "event": Schema.Literal("thread.created"), + "data": ThreadObject + }).annotate({ "description": "Occurs when a new [thread](/docs/api-reference/threads/object) is created." }) +], { mode: "oneOf" }) +export type ModelIdsCompaction = ModelIdsResponses | string | null +export const ModelIdsCompaction = Schema.Union([ModelIdsResponses, Schema.String, Schema.Null]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-5` or `o3`. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the [model guide](/docs/models) to browse and compare available models." +}) +export type RealtimeTranslationClientSecretCreateResponse = { + readonly "value": string + readonly "expires_at": number + readonly "session": RealtimeTranslationSession +} +export const RealtimeTranslationClientSecretCreateResponse = Schema.Struct({ + "value": Schema.String.annotate({ "description": "The generated client secret value." }), + "expires_at": Schema.Number.annotate({ + "description": "Expiration timestamp for the client secret, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()), + "session": RealtimeTranslationSession +}).annotate({ + "title": "Realtime translation session and client secret", + "description": "Response from creating a translation session and client secret for the Realtime API.\n" +}) +export type RealtimeTranslationClientSecretCreateRequest = { + readonly "expires_after"?: { readonly "anchor"?: "created_at"; readonly "seconds"?: number } + readonly "session": RealtimeTranslationSessionCreateRequest +} +export const RealtimeTranslationClientSecretCreateRequest = Schema.Struct({ + "expires_after": Schema.optionalKey( + Schema.Struct({ + "anchor": Schema.optionalKey( + Schema.Literal("created_at").annotate({ + "description": + "The anchor point for the client secret expiration, meaning that `seconds` will be added to the `created_at` time of the client secret to produce an expiration timestamp. Only `created_at` is currently supported.\n" + }) + ), + "seconds": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The number of seconds from the anchor point to the expiration. Select a value between `10` and `7200` (2 hours). This default to 600 seconds (10 minutes) if not specified.\n", + "format": "int64" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(10)).check(Schema.isLessThanOrEqualTo(7200)) + ) + }).annotate({ + "title": "Client secret expiration", + "description": + "Configuration for the client secret expiration. Expiration refers to the time after which\na client secret will no longer be valid for creating sessions. The session itself may\ncontinue after that time once started. A secret can be used to create multiple sessions\nuntil it expires.\n" + }) + ), + "session": RealtimeTranslationSessionCreateRequest +}).annotate({ + "title": "Realtime translation client secret creation request", + "description": "Create a translation session and client secret for the Realtime API.\n" +}) +export type ProjectApiKeyListResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ProjectApiKeyListResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(ProjectApiKey), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type RealtimeConversationItem = + | RealtimeConversationItemMessageSystem + | RealtimeConversationItemMessageUser + | RealtimeConversationItemMessageAssistant + | RealtimeConversationItemFunctionCall + | RealtimeConversationItemFunctionCallOutput + | RealtimeMCPApprovalResponse + | RealtimeMCPListTools + | RealtimeMCPToolCall + | RealtimeMCPApprovalRequest +export const RealtimeConversationItem = Schema.Union([ + RealtimeConversationItemMessageSystem, + RealtimeConversationItemMessageUser, + RealtimeConversationItemMessageAssistant, + RealtimeConversationItemFunctionCall, + RealtimeConversationItemFunctionCallOutput, + RealtimeMCPApprovalResponse, + RealtimeMCPListTools, + RealtimeMCPToolCall, + RealtimeMCPApprovalRequest +]).annotate({ "description": "A single item within a Realtime conversation." }) +export type AssistantsApiResponseFormatOption = + | "auto" + | ResponseFormatText + | ResponseFormatJsonObject + | ResponseFormatJsonSchema +export const AssistantsApiResponseFormatOption = Schema.Union([ + Schema.Literal("auto").annotate({ "description": "`auto` is the default value\n" }), + ResponseFormatText, + ResponseFormatJsonObject, + ResponseFormatJsonSchema +], { mode: "oneOf" }).annotate({ + "description": + "Specifies the format that the model must output. Compatible with [GPT-4o](/docs/models#gpt-4o), [GPT-4 Turbo](/docs/models#gpt-4-turbo-and-gpt-4), and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`.\n\nSetting to `{ \"type\": \"json_schema\", \"json_schema\": {...} }` enables Structured Outputs which ensures the model will match your supplied JSON schema. Learn more in the [Structured Outputs guide](/docs/guides/structured-outputs).\n\nSetting to `{ \"type\": \"json_object\" }` enables JSON mode, which ensures the message the model generates is valid JSON.\n\n**Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly \"stuck\" request. Also note that the message content may be partially cut off if `finish_reason=\"length\"`, which indicates the generation exceeded `max_tokens` or the conversation exceeded the max context length.\n" +}) +export type TextResponseFormatConfiguration = + | ResponseFormatText + | TextResponseFormatJsonSchema + | ResponseFormatJsonObject +export const TextResponseFormatConfiguration = Schema.Union([ + ResponseFormatText, + TextResponseFormatJsonSchema, + ResponseFormatJsonObject +], { mode: "oneOf" }).annotate({ + "description": + "An object specifying the format that the model must output.\n\nConfiguring `{ \"type\": \"json_schema\" }` enables Structured Outputs, \nwhich ensures the model will match your supplied JSON schema. Learn more in the \n[Structured Outputs guide](/docs/guides/structured-outputs).\n\nThe default format is `{ \"type\": \"text\" }` with no additional options.\n\n**Not recommended for gpt-4o and newer models:**\n\nSetting to `{ \"type\": \"json_object\" }` enables the older JSON mode, which\nensures the message the model generates is valid JSON. Using `json_schema`\nis preferred for models that support it.\n" +}) +export type RealtimeCallCreateRequest = { + readonly "sdp": string + readonly "session"?: { + readonly "type": "realtime" + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "model"?: + | string + | "gpt-realtime" + | "gpt-realtime-1.5" + | "gpt-realtime-2" + | "gpt-realtime-2025-08-28" + | "gpt-4o-realtime-preview" + | "gpt-4o-realtime-preview-2024-10-01" + | "gpt-4o-realtime-preview-2024-12-17" + | "gpt-4o-realtime-preview-2025-06-03" + | "gpt-4o-mini-realtime-preview" + | "gpt-4o-mini-realtime-preview-2024-12-17" + | "gpt-realtime-mini" + | "gpt-realtime-mini-2025-10-06" + | "gpt-realtime-mini-2025-12-15" + | "gpt-audio-1.5" + | "gpt-audio-mini" + | "gpt-audio-mini-2025-10-06" + | "gpt-audio-mini-2025-12-15" + readonly "instructions"?: string + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + readonly "speed"?: number + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } | null + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "parallel_tool_calls"?: boolean + readonly "reasoning"?: RealtimeReasoning + readonly "max_output_tokens"?: number | "inf" + readonly "truncation"?: RealtimeTruncation + readonly "prompt"?: Prompt + } +} +export const RealtimeCallCreateRequest = Schema.Struct({ + "sdp": Schema.String.annotate({ + "description": "WebRTC Session Description Protocol (SDP) offer generated by the caller." + }), + "session": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("realtime").annotate({ + "description": "The type of session to create. Always `realtime` for the Realtime API.\n" + }), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model can respond with. It defaults to `[\"audio\"]`, indicating\nthat the model will respond with audio plus a transcript. `[\"text\"]` can be used to make\nthe model respond with text only. It is not possible to request both `text` and `audio` at the same time.\n" + }) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-realtime", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-realtime-mini-2025-12-15", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-audio-mini-2025-12-15" + ]) + ]).annotate({ "description": "The Realtime model used for this session.\n" }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\n\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the input audio." }) + ), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })), + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) + }).annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with\nan `id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed\nduring the session once the model has responded with audio at least once.\nWe recommend `marin` and `cedar` for best quality.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response as a multiple of the original speed.\n1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress.\n\nThis parameter is a post-processing adjustment to the audio after it is generated, it's\nalso possible to prompt the model to speak faster or slower.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check( + Schema.isLessThanOrEqualTo(1.5) + ) + ) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ + "title": "auto", + "description": + "Enables tracing and sets default values for tracing configuration options. Always `auto`.\n" + }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the Traces Dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the Traces Dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the Traces Dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Realtime API can write session traces to the [Traces Dashboard](https://platform.openai.com/logs?api=traces). Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([RealtimeFunctionTool, MCPTool], { mode: "oneOf" })).annotate({ + "description": "Tools available to the model." + }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the model may call multiple tools in parallel. Only supported by\nreasoning Realtime models such as `gpt-realtime-2`.\n" + }) + ), + "reasoning": Schema.optionalKey(RealtimeReasoning), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "truncation": Schema.optionalKey(RealtimeTruncation), + "prompt": Schema.optionalKey(Prompt) + }).annotate({ "title": "Realtime session configuration", "description": "Realtime session object configuration." }) + ) +}).annotate({ + "title": "Realtime call creation request", + "description": + "Parameters required to initiate a realtime call and receive the SDP answer\nneeded to complete a WebRTC peer connection. Provide an SDP offer generated\nby your client and optionally configure the session that will answer the call." +}) +export type RealtimeClientEventSessionUpdate = { + readonly "event_id"?: string + readonly "type": "session.update" + readonly "session": { + readonly "type": "realtime" + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "model"?: + | string + | "gpt-realtime" + | "gpt-realtime-1.5" + | "gpt-realtime-2" + | "gpt-realtime-2025-08-28" + | "gpt-4o-realtime-preview" + | "gpt-4o-realtime-preview-2024-10-01" + | "gpt-4o-realtime-preview-2024-12-17" + | "gpt-4o-realtime-preview-2025-06-03" + | "gpt-4o-mini-realtime-preview" + | "gpt-4o-mini-realtime-preview-2024-12-17" + | "gpt-realtime-mini" + | "gpt-realtime-mini-2025-10-06" + | "gpt-realtime-mini-2025-12-15" + | "gpt-audio-1.5" + | "gpt-audio-mini" + | "gpt-audio-mini-2025-10-06" + | "gpt-audio-mini-2025-12-15" + readonly "instructions"?: string + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + readonly "speed"?: number + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } | null + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "parallel_tool_calls"?: boolean + readonly "reasoning"?: RealtimeReasoning + readonly "max_output_tokens"?: number | "inf" + readonly "truncation"?: RealtimeTruncation + readonly "prompt"?: Prompt + } | { + readonly "type": "transcription" + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: RealtimeAudioFormats + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + } +} +export const RealtimeClientEventSessionUpdate = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Optional client-generated ID used to identify this event. This is an arbitrary string that a client may assign. It will be passed back if there is an error with the event, but the corresponding `session.updated` event will not include it." + }).check(Schema.isMaxLength(512)) + ), + "type": Schema.Literal("session.update").annotate({ "description": "The event type, must be `session.update`." }), + "session": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("realtime").annotate({ + "description": "The type of session to create. Always `realtime` for the Realtime API.\n" + }), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model can respond with. It defaults to `[\"audio\"]`, indicating\nthat the model will respond with audio plus a transcript. `[\"text\"]` can be used to make\nthe model respond with text only. It is not possible to request both `text` and `audio` at the same time.\n" + }) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-realtime", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-realtime-mini-2025-12-15", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-audio-mini-2025-12-15" + ]) + ]).annotate({ "description": "The Realtime model used for this session.\n" }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\n\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the input audio." }) + ), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })), + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) + }).annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with\nan `id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed\nduring the session once the model has responded with audio at least once.\nWe recommend `marin` and `cedar` for best quality.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response as a multiple of the original speed.\n1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress.\n\nThis parameter is a post-processing adjustment to the audio after it is generated, it's\nalso possible to prompt the model to speak faster or slower.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check( + Schema.isLessThanOrEqualTo(1.5) + ) + ) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ + "title": "auto", + "description": + "Enables tracing and sets default values for tracing configuration options. Always `auto`.\n" + }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the Traces Dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the Traces Dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the Traces Dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Realtime API can write session traces to the [Traces Dashboard](https://platform.openai.com/logs?api=traces). Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([RealtimeFunctionTool, MCPTool], { mode: "oneOf" })).annotate({ + "description": "Tools available to the model." + }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the model may call multiple tools in parallel. Only supported by\nreasoning Realtime models such as `gpt-realtime-2`.\n" + }) + ), + "reasoning": Schema.optionalKey(RealtimeReasoning), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "truncation": Schema.optionalKey(RealtimeTruncation), + "prompt": Schema.optionalKey(Prompt) + }).annotate({ + "title": "Realtime session configuration", + "description": "Update the Realtime session. Choose either a realtime\nsession or a transcription session.\n" + }), + Schema.Struct({ + "type": Schema.Literal("transcription").annotate({ + "description": "The type of session to create. Always `transcription` for transcription sessions.\n" + }), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey(RealtimeAudioFormats), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ) + }).annotate({ + "title": "Realtime transcription session configuration", + "description": "Update the Realtime session. Choose either a realtime\nsession or a transcription session.\n" + }) + ], { mode: "oneOf" }) +}).annotate({ + "description": + "Send this event to update the session’s configuration.\nThe client may send this event at any time to update any field\nexcept for `voice` and `model`. `voice` can be updated only if there have been no other audio outputs yet.\n\nWhen the server receives a `session.update`, it will respond\nwith a `session.updated` event showing the full, effective configuration.\nOnly the fields that are present in the `session.update` are updated. To clear a field like\n`instructions`, pass an empty string. To clear a field like `tools`, pass an empty array.\nTo clear a field like `turn_detection`, pass `null`.\n" +}) +export type RealtimeSession = { + readonly "id"?: string + readonly "object"?: "realtime.session" + readonly "modalities"?: ReadonlyArray<"text" | "audio"> + readonly "model"?: + | string + | "gpt-realtime" + | "gpt-realtime-1.5" + | "gpt-realtime-2025-08-28" + | "gpt-4o-realtime-preview" + | "gpt-4o-realtime-preview-2024-10-01" + | "gpt-4o-realtime-preview-2024-12-17" + | "gpt-4o-realtime-preview-2025-06-03" + | "gpt-4o-mini-realtime-preview" + | "gpt-4o-mini-realtime-preview-2024-12-17" + | "gpt-realtime-mini" + | "gpt-realtime-mini-2025-10-06" + | "gpt-realtime-mini-2025-12-15" + | "gpt-audio-1.5" + | "gpt-audio-mini" + | "gpt-audio-mini-2025-10-06" + | "gpt-audio-mini-2025-12-15" + readonly "instructions"?: string + readonly "voice"?: + | string + | "alloy" + | "ash" + | "ballad" + | "coral" + | "echo" + | "sage" + | "shimmer" + | "verse" + | "marin" + | "cedar" + readonly "input_audio_format"?: "pcm16" | "g711_ulaw" | "g711_alaw" + readonly "output_audio_format"?: "pcm16" | "g711_ulaw" | "g711_alaw" + readonly "input_audio_transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + } | null + readonly "turn_detection"?: RealtimeTurnDetection + readonly "input_audio_noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "speed"?: number + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } | null + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: string + readonly "temperature"?: number + readonly "max_response_output_tokens"?: number | "inf" + readonly "expires_at"?: number + readonly "prompt"?: Prompt | null + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> | null +} +export const RealtimeSession = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.session").annotate({ "description": "The object type. Always `realtime.session`." }) + ), + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": "The set of modalities the model can respond with. To disable audio,\nset this to [\"text\"].\n" + }) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-realtime", + "gpt-realtime-1.5", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-realtime-mini-2025-12-15", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-audio-mini-2025-12-15" + ]) + ]).annotate({ "description": "The Realtime model used for this session.\n" }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model\ncalls. This field allows the client to guide the model on desired\nresponses. The model can be instructed on response content and format,\n(e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good\nresponses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion\ninto your voice\", \"laugh frequently\"). The instructions are not\nguaranteed to be followed by the model, but they provide guidance to the\nmodel on the desired behavior.\n\n\nNote that the server sets default instructions which will be used if this\nfield is not set and are visible in the `session.created` event at the\nstart of the session.\n" + })), + "voice": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]) + ]).annotate({ + "description": + "The voice the model uses to respond. Voice cannot be changed during the\nsession once the model has responded with audio at least once. Current\nvoice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`,\n`shimmer`, and `verse`.\n" + }) + ), + "input_audio_format": Schema.optionalKey( + Schema.Literals(["pcm16", "g711_ulaw", "g711_alaw"]).annotate({ + "description": + "The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\nFor `pcm16`, input audio must be 16-bit PCM at a 24kHz sample rate,\nsingle channel (mono), and little-endian byte order.\n" + }) + ), + "output_audio_format": Schema.optionalKey( + Schema.Literals(["pcm16", "g711_ulaw", "g711_alaw"]).annotate({ + "description": + "The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\nFor `pcm16`, output audio is sampled at a rate of 24kHz.\n" + }) + ), + "input_audio_transcription": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model used for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`.\n" + }) + ), + "language": Schema.optionalKey(Schema.String.annotate({ "description": "The language of the input audio.\n" })), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt configured for input audio transcription, when present.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](https://platform.openai.com/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }), + Schema.Null + ])), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection), + "input_audio_noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response. 1.0 is the default speed. 0.25 is\nthe minimum speed. 1.5 is the maximum speed. This value can only be changed\nin between model turns, not while a response is in progress.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check(Schema.isLessThanOrEqualTo(1.5)) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ "description": "Default tracing mode for the session.\n" }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the traces dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the traces dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the traces dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Configuration options for tracing. Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array(RealtimeFunctionTool).annotate({ "description": "Tools (functions) available to the model." }) + ), + "tool_choice": Schema.optionalKey( + Schema.String.annotate({ + "description": "How the model chooses tools. Options are `auto`, `none`, `required`, or\nspecify a function.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Sampling temperature for the model, limited to [0.6, 1.2]. For audio models a temperature of 0.8 is highly recommended for best performance.\n" + }).check(Schema.isFinite()) + ), + "max_response_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "prompt": Schema.optionalKey(Schema.Union([Prompt, Schema.Null])), + "include": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n- `item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }), + Schema.Null + ]) + ) +}).annotate({ "description": "Realtime session object for the beta interface." }) +export type RealtimeSessionCreateRequest = { + readonly "client_secret": { readonly "value": string; readonly "expires_at": number } + readonly "modalities"?: ReadonlyArray<"text" | "audio"> + readonly "instructions"?: string + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + readonly "input_audio_format"?: string + readonly "output_audio_format"?: string + readonly "input_audio_transcription"?: { readonly "model"?: string } + readonly "speed"?: number + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } + readonly "turn_detection"?: { + readonly "type"?: string + readonly "threshold"?: number + readonly "prefix_padding_ms"?: number + readonly "silence_duration_ms"?: number + } + readonly "tools"?: ReadonlyArray< + { + readonly "type"?: "function" + readonly "name"?: string + readonly "description"?: string + readonly "parameters"?: {} + } + > + readonly "tool_choice"?: string + readonly "temperature"?: number + readonly "max_response_output_tokens"?: number | "inf" + readonly "truncation"?: RealtimeTruncation + readonly "prompt"?: Prompt +} +export const RealtimeSessionCreateRequest = Schema.Struct({ + "client_secret": Schema.Struct({ + "value": Schema.String.annotate({ + "description": + "Ephemeral key usable in client environments to authenticate connections\nto the Realtime API. Use this in client-side environments rather than\na standard API token, which should only be used server-side.\n" + }), + "expires_at": Schema.Number.annotate({ + "description": "Timestamp for when the token expires. Currently, all tokens expire\nafter one minute.\n", + "format": "unixtime" + }).check(Schema.isInt()) + }).annotate({ "description": "Ephemeral key returned by the API." }), + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": "The set of modalities the model can respond with. To disable audio,\nset this to [\"text\"].\n" + }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) }) + .annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with an\n`id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed during\nthe session once the model has responded with audio at least once.\n" + }) + ), + "input_audio_format": Schema.optionalKey( + Schema.String.annotate({ + "description": "The format of input audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\n" + }) + ), + "output_audio_format": Schema.optionalKey( + Schema.String.annotate({ + "description": "The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\n" + }) + ), + "input_audio_transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey(Schema.String.annotate({ "description": "The model to use for transcription.\n" })) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be\nset to `null` to turn off once on. Input audio transcription is not native\nto the model, since the model consumes audio directly. Transcription runs\nasynchronously and should be treated as rough guidance\nrather than the representation understood by the model.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response. 1.0 is the default speed. 0.25 is\nthe minimum speed. 1.5 is the maximum speed. This value can only be changed\nin between model turns, not while a response is in progress.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check(Schema.isLessThanOrEqualTo(1.5)) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto").annotate({ "description": "Default tracing mode for the session.\n" }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the traces dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the traces dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the traces dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Configuration options for tracing. Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }) + ), + "turn_detection": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.String.annotate({ "description": "Type of turn detection, only `server_vad` is currently supported.\n" }) + ), + "threshold": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Activation threshold for VAD (0.0 to 1.0), this defaults to 0.5. A\nhigher threshold will require louder audio to activate the model, and\nthus might perform better in noisy environments.\n" + }).check(Schema.isFinite()) + ), + "prefix_padding_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Amount of audio to include before the VAD detected speech (in\nmilliseconds). Defaults to 300ms.\n" + }).check(Schema.isInt()) + ), + "silence_duration_ms": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Duration of silence to detect speech stop (in milliseconds). Defaults\nto 500ms. With shorter values the model will respond more quickly,\nbut may jump in on short pauses from the user.\n" + }).check(Schema.isInt()) + ) + }).annotate({ + "description": + "Configuration for turn detection. Can be set to `null` to turn off. Server\nVAD means that the model will detect the start and end of speech based on\naudio volume and respond at the end of user speech.\n" + }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("function").annotate({ "description": "The type of the tool, i.e. `function`." }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function." })), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The description of the function, including guidance on when and how\nto call it, and guidance about what to tell the user when calling\n(if anything).\n" + }) + ), + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Parameters of the function in JSON Schema." }) + ) + })).annotate({ "description": "Tools (functions) available to the model." }) + ), + "tool_choice": Schema.optionalKey( + Schema.String.annotate({ + "description": "How the model chooses tools. Options are `auto`, `none`, `required`, or\nspecify a function.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8.\n" + }).check(Schema.isFinite()) + ), + "max_response_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "truncation": Schema.optionalKey(RealtimeTruncation), + "prompt": Schema.optionalKey(Prompt) +}).annotate({ + "description": "A new Realtime session configuration, with an ephemeral key. Default TTL\nfor keys is one minute.\n" +}) +export type RealtimeSessionCreateRequestGA = { + readonly "type": "realtime" + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "model"?: + | string + | "gpt-realtime" + | "gpt-realtime-1.5" + | "gpt-realtime-2" + | "gpt-realtime-2025-08-28" + | "gpt-4o-realtime-preview" + | "gpt-4o-realtime-preview-2024-10-01" + | "gpt-4o-realtime-preview-2024-12-17" + | "gpt-4o-realtime-preview-2025-06-03" + | "gpt-4o-mini-realtime-preview" + | "gpt-4o-mini-realtime-preview-2024-12-17" + | "gpt-realtime-mini" + | "gpt-realtime-mini-2025-10-06" + | "gpt-realtime-mini-2025-12-15" + | "gpt-audio-1.5" + | "gpt-audio-mini" + | "gpt-audio-mini-2025-10-06" + | "gpt-audio-mini-2025-12-15" + readonly "instructions"?: string + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + readonly "delay"?: "minimal" | "low" | "medium" | "high" | "xhigh" + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + readonly "speed"?: number + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } | null + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "parallel_tool_calls"?: boolean + readonly "reasoning"?: RealtimeReasoning + readonly "max_output_tokens"?: number | "inf" + readonly "truncation"?: RealtimeTruncation + readonly "prompt"?: Prompt +} +export const RealtimeSessionCreateRequestGA = Schema.Struct({ + "type": Schema.Literal("realtime").annotate({ + "description": "The type of session to create. Always `realtime` for the Realtime API.\n" + }), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model can respond with. It defaults to `[\"audio\"]`, indicating\nthat the model will respond with audio plus a transcript. `[\"text\"]` can be used to make\nthe model respond with text only. It is not possible to request both `text` and `audio` at the same time.\n" + }) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-realtime", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-realtime-mini-2025-12-15", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-audio-mini-2025-12-15" + ]) + ]).annotate({ "description": "The Realtime model used for this session.\n" }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\n\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the input audio." }) + ), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model to use for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`. Use `gpt-4o-transcribe-diarize` when you need diarization with speaker labels.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The language of the input audio. Supplying the input language in\n[ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (e.g. `en`) format\nwill improve accuracy and latency.\n" + }) + ), + "prompt": Schema.optionalKey(Schema.String.annotate({ + "description": + "An optional text to guide the model's style or continue a previous audio\nsegment.\nFor `whisper-1`, the [prompt is a list of keywords](/docs/guides/speech-to-text#prompting).\nFor `gpt-4o-transcribe` models (excluding `gpt-4o-transcribe-diarize`), the prompt is a free text string, for example \"expect words related to technology\".\nPrompt is not supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + })), + "delay": Schema.optionalKey( + Schema.Literals(["minimal", "low", "medium", "high", "xhigh"]).annotate({ + "description": + "Controls how long the model waits before emitting transcription text.\nHigher values can improve transcription accuracy at the cost of latency.\nOnly supported with `gpt-realtime-whisper` in GA Realtime sessions.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })), + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) + }).annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with\nan `id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed\nduring the session once the model has responded with audio at least once.\nWe recommend `marin` and `cedar` for best quality.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response as a multiple of the original speed.\n1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress.\n\nThis parameter is a post-processing adjustment to the audio after it is generated, it's\nalso possible to prompt the model to speak faster or slower.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check(Schema.isLessThanOrEqualTo(1.5)) + ) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ + "title": "auto", + "description": "Enables tracing and sets default values for tracing configuration options. Always `auto`.\n" + }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the Traces Dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the Traces Dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the Traces Dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Realtime API can write session traces to the [Traces Dashboard](https://platform.openai.com/logs?api=traces). Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([RealtimeFunctionTool, MCPTool], { mode: "oneOf" })).annotate({ + "description": "Tools available to the model." + }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the model may call multiple tools in parallel. Only supported by\nreasoning Realtime models such as `gpt-realtime-2`.\n" + }) + ), + "reasoning": Schema.optionalKey(RealtimeReasoning), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "truncation": Schema.optionalKey(RealtimeTruncation), + "prompt": Schema.optionalKey(Prompt) +}).annotate({ "title": "Realtime session configuration", "description": "Realtime session object configuration." }) +export type RealtimeSessionCreateResponseGA = { + readonly "type": "realtime" + readonly "id": string + readonly "object": "realtime.session" + readonly "expires_at"?: number + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "model"?: + | string + | "gpt-realtime" + | "gpt-realtime-1.5" + | "gpt-realtime-2" + | "gpt-realtime-2025-08-28" + | "gpt-4o-realtime-preview" + | "gpt-4o-realtime-preview-2024-10-01" + | "gpt-4o-realtime-preview-2024-12-17" + | "gpt-4o-realtime-preview-2025-06-03" + | "gpt-4o-mini-realtime-preview" + | "gpt-4o-mini-realtime-preview-2024-12-17" + | "gpt-realtime-mini" + | "gpt-realtime-mini-2025-10-06" + | "gpt-realtime-mini-2025-12-15" + | "gpt-audio-1.5" + | "gpt-audio-mini" + | "gpt-audio-mini-2025-10-06" + | "gpt-audio-mini-2025-12-15" + readonly "instructions"?: string + readonly "audio"?: { + readonly "input"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "transcription"?: { + readonly "model"?: + | string + | "whisper-1" + | "gpt-4o-mini-transcribe" + | "gpt-4o-mini-transcribe-2025-12-15" + | "gpt-4o-transcribe" + | "gpt-4o-transcribe-diarize" + | "gpt-realtime-whisper" + readonly "language"?: string + readonly "prompt"?: string + } + readonly "noise_reduction"?: { readonly "type"?: NoiseReductionType } + readonly "turn_detection"?: RealtimeTurnDetection + } + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: + | string + | "alloy" + | "ash" + | "ballad" + | "coral" + | "echo" + | "sage" + | "shimmer" + | "verse" + | "marin" + | "cedar" + readonly "speed"?: number + } + } + readonly "include"?: ReadonlyArray<"item.input_audio_transcription.logprobs"> + readonly "tracing"?: "auto" | { + readonly "workflow_name"?: string + readonly "group_id"?: string + readonly "metadata"?: {} + } | null + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "reasoning"?: RealtimeReasoning + readonly "max_output_tokens"?: number | "inf" + readonly "truncation"?: RealtimeTruncation + readonly "prompt"?: Prompt +} +export const RealtimeSessionCreateResponseGA = Schema.Struct({ + "type": Schema.Literal("realtime").annotate({ + "description": "The type of session to create. Always `realtime` for the Realtime API.\n" + }), + "id": Schema.String.annotate({ + "description": "Unique identifier for the session that looks like `sess_1234567890abcdef`.\n" + }), + "object": Schema.Literal("realtime.session").annotate({ + "description": "The object type. Always `realtime.session`." + }), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Expiration timestamp for the session, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()) + ), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model can respond with. It defaults to `[\"audio\"]`, indicating\nthat the model will respond with audio plus a transcript. `[\"text\"]` can be used to make\nthe model respond with text only. It is not possible to request both `text` and `audio` at the same time.\n" + }) + ), + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-realtime", + "gpt-realtime-1.5", + "gpt-realtime-2", + "gpt-realtime-2025-08-28", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "gpt-realtime-mini-2025-12-15", + "gpt-audio-1.5", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-audio-mini-2025-12-15" + ]) + ]).annotate({ "description": "The Realtime model used for this session.\n" }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\n\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "input": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the input audio." }) + ), + "transcription": Schema.optionalKey( + Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals([ + "whisper-1", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-transcribe-2025-12-15", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-realtime-whisper" + ]) + ]).annotate({ + "description": + "The model used for transcription. Current options are `whisper-1`, `gpt-4o-mini-transcribe`, `gpt-4o-mini-transcribe-2025-12-15`, `gpt-4o-transcribe`, `gpt-4o-transcribe-diarize`, and `gpt-realtime-whisper`.\n" + }) + ), + "language": Schema.optionalKey( + Schema.String.annotate({ "description": "The language of the input audio.\n" }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt configured for input audio transcription, when present.\n" + }) + ) + }).annotate({ + "description": + "Configuration for input audio transcription, defaults to off and can be set to `null` to turn off once on. Input audio transcription is not native to the model, since the model consumes audio directly. Transcription runs asynchronously through [the /audio/transcriptions endpoint](/docs/api-reference/audio/createTranscription) and should be treated as guidance of input audio content rather than precisely what the model heard. The client can optionally set the language and prompt for transcription, these offer additional guidance to the transcription service.\n" + }) + ), + "noise_reduction": Schema.optionalKey( + Schema.Struct({ "type": Schema.optionalKey(NoiseReductionType) }).annotate({ + "description": + "Configuration for input audio noise reduction. This can be set to `null` to turn off.\nNoise reduction filters audio added to the input audio buffer before it is sent to VAD and the model.\nFiltering the audio can improve VAD and turn detection accuracy (reducing false positives) and model performance by improving perception of the input audio.\n" + }) + ), + "turn_detection": Schema.optionalKey(RealtimeTurnDetection) + })), + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]) + ]).annotate({ + "description": + "The voice the model uses to respond. Voice cannot be changed during the\nsession once the model has responded with audio at least once. Current\nvoice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`,\n`shimmer`, `verse`, `marin`, and `cedar`. We recommend `marin` and `cedar` for\nbest quality.\n" + }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The speed of the model's spoken response as a multiple of the original speed.\n1.0 is the default speed. 0.25 is the minimum speed. 1.5 is the maximum speed. This value can only be changed in between model turns, not while a response is in progress.\n\nThis parameter is a post-processing adjustment to the audio after it is generated, it's\nalso possible to prompt the model to speak faster or slower.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0.25)).check(Schema.isLessThanOrEqualTo(1.5)) + ) + })) + }).annotate({ "description": "Configuration for input and output audio.\n" }) + ), + "include": Schema.optionalKey( + Schema.Array(Schema.Literal("item.input_audio_transcription.logprobs")).annotate({ + "description": + "Additional fields to include in server outputs.\n\n`item.input_audio_transcription.logprobs`: Include logprobs for input audio transcription.\n" + }) + ), + "tracing": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto").annotate({ + "title": "auto", + "description": "Enables tracing and sets default values for tracing configuration options. Always `auto`.\n" + }), + Schema.Struct({ + "workflow_name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The name of the workflow to attach to this trace. This is used to\nname the trace in the Traces Dashboard.\n" + }) + ), + "group_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The group id to attach to this trace to enable filtering and\ngrouping in the Traces Dashboard.\n" + }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The arbitrary metadata to attach to this trace to enable\nfiltering in the Traces Dashboard.\n" + }) + ) + }).annotate({ "title": "Tracing Configuration", "description": "Granular configuration for tracing.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Tracing Configuration", + "description": + "Realtime API can write session traces to the [Traces Dashboard](https://platform.openai.com/logs?api=traces). Set to null to disable tracing. Once\ntracing is enabled for a session, the configuration cannot be modified.\n\n`auto` will create a trace for the session with default values for the\nworkflow name, group id, and metadata.\n" + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([RealtimeFunctionTool, MCPTool], { mode: "oneOf" })).annotate({ + "description": "Tools available to the model." + }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "reasoning": Schema.optionalKey(RealtimeReasoning), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "truncation": Schema.optionalKey(RealtimeTruncation), + "prompt": Schema.optionalKey(Prompt) +}).annotate({ + "title": "Realtime session configuration object", + "description": "A Realtime session configuration object.\n" +}) +export type CreateVectorStoreFileRequest = { + readonly "file_id": string + readonly "chunking_strategy"?: ChunkingStrategyRequestParam + readonly "attributes"?: VectorStoreFileAttributes +} +export const CreateVectorStoreFileRequest = Schema.Struct({ + "file_id": Schema.String.annotate({ + "description": + "A [File](/docs/api-reference/files) ID that the vector store should use. Useful for tools like `file_search` that can access files. For multi-file ingestion, we recommend [`file_batches`](/docs/api-reference/vector-stores-file-batches/createBatch) to minimize per-vector-store write requests." + }), + "chunking_strategy": Schema.optionalKey(ChunkingStrategyRequestParam), + "attributes": Schema.optionalKey(VectorStoreFileAttributes) +}) +export type CreateTranscriptionResponseStreamEvent = + | TranscriptTextSegmentEvent + | TranscriptTextDeltaEvent + | TranscriptTextDoneEvent +export const CreateTranscriptionResponseStreamEvent = Schema.Union([ + TranscriptTextSegmentEvent, + TranscriptTextDeltaEvent, + TranscriptTextDoneEvent +]) +export type UsageResponse = { + readonly "object": "page" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const UsageResponse = Schema.Struct({ + "object": Schema.Literal("page"), + "data": Schema.Array(UsageTimeBucket), + "has_more": Schema.Boolean, + "next_page": Schema.Union([Schema.String, Schema.Null]) +}) +export type ListVectorStoresResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean + readonly [x: string]: unknown +} +export const ListVectorStoresResponse = Schema.StructWithRest( + Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(VectorStoreObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean + }), + [Schema.Record(Schema.String, Schema.Json)] +) +export type ListVectorStoreFilesResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean + readonly [x: string]: unknown +} +export const ListVectorStoreFilesResponse = Schema.StructWithRest( + Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(VectorStoreFileObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean + }), + [Schema.Record(Schema.String, Schema.Json)] +) +export type VectorStoreSearchResultsPage = { + readonly "object": "vector_store.search_results.page" + readonly "search_query": ReadonlyArray + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "next_page": string | null +} +export const VectorStoreSearchResultsPage = Schema.Struct({ + "object": Schema.Literal("vector_store.search_results.page").annotate({ + "description": "The object type, which is always `vector_store.search_results.page`" + }), + "search_query": Schema.Array(Schema.String.annotate({ "description": "The query used for this search." })), + "data": Schema.Array(VectorStoreSearchResultItem).annotate({ "description": "The list of search result items." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates if there are more results to fetch." }), + "next_page": Schema.Union([ + Schema.String.annotate({ "description": "The token for the next page, if any." }), + Schema.Null + ]) +}) +export type CreateContainerBody = { + readonly "name": string + readonly "file_ids"?: ReadonlyArray + readonly "expires_after"?: { readonly "anchor": "last_active_at"; readonly "minutes": number } + readonly "skills"?: ReadonlyArray + readonly "memory_limit"?: "1g" | "4g" | "16g" | "64g" + readonly "network_policy"?: ContainerNetworkPolicyDisabledParam | ContainerNetworkPolicyAllowlistParam +} +export const CreateContainerBody = Schema.Struct({ + "name": Schema.String.annotate({ "description": "Name of the container to create." }), + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "IDs of files to copy to the container." }) + ), + "expires_after": Schema.optionalKey( + Schema.Struct({ + "anchor": Schema.Literal("last_active_at").annotate({ + "description": "Time anchor for the expiration time. Currently only 'last_active_at' is supported." + }), + "minutes": Schema.Number.check(Schema.isInt()) + }).annotate({ "description": "Container expiration time in seconds relative to the 'anchor' time." }) + ), + "skills": Schema.optionalKey( + Schema.Array(Schema.Union([SkillReferenceParam, InlineSkillParam], { mode: "oneOf" })).annotate({ + "description": "An optional list of skills referenced by id or inline data." + }) + ), + "memory_limit": Schema.optionalKey( + Schema.Literals(["1g", "4g", "16g", "64g"]).annotate({ + "description": "Optional memory limit for the container. Defaults to \"1g\"." + }) + ), + "network_policy": Schema.optionalKey( + Schema.Union([ContainerNetworkPolicyDisabledParam, ContainerNetworkPolicyAllowlistParam], { mode: "oneOf" }) + .annotate({ "description": "Network access policy for the container." }) + ) +}) +export type AutoCodeInterpreterToolParam = { + readonly "type": "auto" + readonly "file_ids"?: ReadonlyArray + readonly "memory_limit"?: "1g" | "4g" | "16g" | "64g" | null + readonly "network_policy"?: ContainerNetworkPolicyDisabledParam | ContainerNetworkPolicyAllowlistParam +} +export const AutoCodeInterpreterToolParam = Schema.Struct({ + "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }), + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "An optional list of uploaded files to make available to your code." + }).check(Schema.isMaxLength(50)) + ), + "memory_limit": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["1g", "4g", "16g", "64g"]).annotate({ + "description": "The memory limit for the code interpreter container." + }), + Schema.Null + ]) + ), + "network_policy": Schema.optionalKey( + Schema.Union([ContainerNetworkPolicyDisabledParam, ContainerNetworkPolicyAllowlistParam], { mode: "oneOf" }) + .annotate({ "description": "Network access policy for the container." }) + ) +}).annotate({ + "title": "CodeInterpreterToolAuto", + "description": + "Configuration for a code interpreter container. Optionally specify the IDs of the files to run the code on." +}) +export type ContainerAutoParam = { + readonly "type": "container_auto" + readonly "file_ids"?: ReadonlyArray + readonly "memory_limit"?: "1g" | "4g" | "16g" | "64g" | null + readonly "network_policy"?: ContainerNetworkPolicyDisabledParam | ContainerNetworkPolicyAllowlistParam + readonly "skills"?: ReadonlyArray +} +export const ContainerAutoParam = Schema.Struct({ + "type": Schema.Literal("container_auto").annotate({ + "description": "Automatically creates a container for this request" + }), + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "An optional list of uploaded files to make available to your code." + }).check(Schema.isMaxLength(50)) + ), + "memory_limit": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["1g", "4g", "16g", "64g"]).annotate({ "description": "The memory limit for the container." }), + Schema.Null + ]) + ), + "network_policy": Schema.optionalKey( + Schema.Union([ContainerNetworkPolicyDisabledParam, ContainerNetworkPolicyAllowlistParam], { mode: "oneOf" }) + .annotate({ "description": "Network access policy for the container." }) + ), + "skills": Schema.optionalKey( + Schema.Array(Schema.Union([SkillReferenceParam, InlineSkillParam], { mode: "oneOf" })).annotate({ + "description": "An optional list of skills referenced by id or inline data." + }).check(Schema.isMaxLength(200)) + ) +}) +export type EvalItemContentArray = ReadonlyArray +export const EvalItemContentArray = Schema.Array(EvalItemContentItem).annotate({ + "title": "An array of Input text, Output text, Input image, and Input audio", + "description": + "A list of inputs, each of which may be either an input text, output text, input\nimage, or input audio object.\n" +}) +export type OutputTextContent = { + readonly "type": "output_text" + readonly "text": string + readonly "annotations": ReadonlyArray + readonly "logprobs": ReadonlyArray +} +export const OutputTextContent = Schema.Struct({ + "type": Schema.Literal("output_text").annotate({ + "description": "The type of the output text. Always `output_text`." + }), + "text": Schema.String.annotate({ "description": "The text output from the model." }), + "annotations": Schema.Array(Annotation).annotate({ "description": "The annotations of the text output." }), + "logprobs": Schema.Array(LogProb) +}).annotate({ "title": "Output text", "description": "A text output from the model." }) +export type CustomToolCallOutput = { + readonly "type": "custom_tool_call_output" + readonly "id"?: string + readonly "call_id": string + readonly "output": string | ReadonlyArray +} +export const CustomToolCallOutput = Schema.Struct({ + "type": Schema.Literal("custom_tool_call_output").annotate({ + "description": "The type of the custom tool call output. Always `custom_tool_call_output`.\n" + }), + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The unique ID of the custom tool call output in the OpenAI platform.\n" }) + ), + "call_id": Schema.String.annotate({ + "description": "The call ID, used to map this custom tool call output to a custom tool call.\n" + }), + "output": Schema.Union([ + Schema.String.annotate({ + "title": "string output", + "description": "A string of the output of the custom tool call.\n" + }), + Schema.Array(FunctionAndCustomToolCallOutput).annotate({ + "title": "output content list", + "description": "Text, image, or file output of the custom tool call.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "The output from the custom tool call generated by your code.\nCan be a string or an list of output content.\n" + }) +}).annotate({ + "title": "Custom tool call output", + "description": "The output of a custom tool call from your code, being sent back to the model.\n" +}) +export type CustomToolCallOutputResource = { + readonly "type": "custom_tool_call_output" + readonly "id": string + readonly "call_id": string + readonly "output": string | ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const CustomToolCallOutputResource = Schema.Struct({ + "type": Schema.Literal("custom_tool_call_output").annotate({ + "description": "The type of the custom tool call output. Always `custom_tool_call_output`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the custom tool call output item.\n" }), + "call_id": Schema.String.annotate({ + "description": "The call ID, used to map this custom tool call output to a custom tool call.\n" + }), + "output": Schema.Union([ + Schema.String.annotate({ + "title": "string output", + "description": "A string of the output of the custom tool call.\n" + }), + Schema.Array(FunctionAndCustomToolCallOutput).annotate({ + "title": "output content list", + "description": "Text, image, or file output of the custom tool call.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "The output from the custom tool call generated by your code.\nCan be a string or an list of output content.\n" + }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item.\n" }) + ) +}).annotate({ + "title": "Custom tool call output", + "description": "The output of a custom tool call from your code, being sent back to the model.\n" +}) +export type FunctionToolCallOutput = { + readonly "id"?: string + readonly "type": "function_call_output" + readonly "call_id": string + readonly "output": string | ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" +} +export const FunctionToolCallOutput = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the function tool call output. Populated when this item\nis returned via API.\n" + }) + ), + "type": Schema.Literal("function_call_output").annotate({ + "description": "The type of the function tool call output. Always `function_call_output`.\n" + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model.\n" + }), + "output": Schema.Union([ + Schema.String.annotate({ + "title": "string output", + "description": "A string of the output of the function call.\n" + }), + Schema.Array(FunctionAndCustomToolCallOutput).annotate({ + "title": "output content list", + "description": "Text, image, or file output of the function call.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "The output from the function call generated by your code.\nCan be a string or an list of output content.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ) +}).annotate({ "title": "Function tool call output", "description": "The output of a function tool call.\n" }) +export type FunctionToolCallOutputResource = { + readonly "id": string + readonly "type": "function_call_output" + readonly "call_id": string + readonly "output": string | ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const FunctionToolCallOutputResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the function call tool output.\n" }), + "type": Schema.Literal("function_call_output").annotate({ + "description": "The type of the function tool call output. Always `function_call_output`.\n" + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model.\n" + }), + "output": Schema.Union([ + Schema.String.annotate({ + "title": "string output", + "description": "A string of the output of the function call.\n" + }), + Schema.Array(FunctionAndCustomToolCallOutput).annotate({ + "title": "output content list", + "description": "Text, image, or file output of the function call.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "The output from the function call generated by your code.\nCan be a string or an list of output content.\n" + }), + "status": Schema.Union([ + Schema.Literal("in_progress").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + Schema.Literal("completed").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + Schema.Literal("incomplete").annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item.\n" }) + ) +}).annotate({ "title": "Function tool call output", "description": "The output of a function tool call.\n" }) +export type InputMessageContentList = ReadonlyArray +export const InputMessageContentList = Schema.Array(InputContent).annotate({ + "title": "Input item content list", + "description": "A list of one or many input items to the model, containing different content \ntypes.\n" +}) +export type ComputerAction = + | ClickParam + | DoubleClickAction + | DragParam + | KeyPressAction + | MoveParam + | ScreenshotParam + | ScrollParam + | TypeParam + | WaitParam +export const ComputerAction = Schema.Union([ + ClickParam, + DoubleClickAction, + DragParam, + KeyPressAction, + MoveParam, + ScreenshotParam, + ScrollParam, + TypeParam, + WaitParam +], { mode: "oneOf" }) +export type NamespaceToolParam = { + readonly "type": "namespace" + readonly "name": string + readonly "description": string + readonly "tools": ReadonlyArray +} +export const NamespaceToolParam = Schema.Struct({ + "type": Schema.Literal("namespace").annotate({ "description": "The type of the tool. Always `namespace`." }), + "name": Schema.String.annotate({ "description": "The namespace name used in tool calls (for example, `crm`)." }) + .check(Schema.isMinLength(1)), + "description": Schema.String.annotate({ "description": "A description of the namespace shown to the model." }).check( + Schema.isMinLength(1) + ), + "tools": Schema.Array( + Schema.Union([FunctionToolParam, CustomToolParam], { mode: "oneOf" }).annotate({ + "description": "A function or custom tool that belongs to a namespace." + }) + ).annotate({ "description": "The function/custom tools available inside this namespace." }).check( + Schema.isMinLength(1) + ) +}).annotate({ "title": "Namespace", "description": "Groups function/custom tools under a shared namespace." }) +export type FunctionShellCallOutput = { + readonly "type": "shell_call_output" + readonly "id": string + readonly "call_id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "output": ReadonlyArray + readonly "max_output_length": number | null + readonly "created_by"?: string +} +export const FunctionShellCallOutput = Schema.Struct({ + "type": Schema.Literal("shell_call_output").annotate({ + "description": "The type of the shell call output. Always `shell_call_output`." + }), + "id": Schema.String.annotate({ + "description": "The unique ID of the shell call output. Populated when this item is returned via API." + }), + "call_id": Schema.String.annotate({ "description": "The unique ID of the shell tool call generated by the model." }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the shell call output. One of `in_progress`, `completed`, or `incomplete`." + }), + "output": Schema.Array(FunctionShellCallOutputContent).annotate({ + "description": "An array of shell call output contents" + }), + "max_output_length": Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum length of the shell command output. This is generated by the model and should be passed back with the raw output." + }).check(Schema.isInt()), + Schema.Null + ]), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item." }) + ) +}).annotate({ "title": "Shell call output", "description": "The output of a shell tool call that was emitted." }) +export type ImagesResponse = { + readonly "created": number + readonly "data"?: ReadonlyArray + readonly "background"?: "transparent" | "opaque" + readonly "output_format"?: "png" | "webp" | "jpeg" + readonly "size"?: "1024x1024" | "1024x1536" | "1536x1024" + readonly "quality"?: "low" | "medium" | "high" + readonly "usage"?: ImageGenUsage +} +export const ImagesResponse = Schema.Struct({ + "created": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the image was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "data": Schema.optionalKey(Schema.Array(Image).annotate({ "description": "The list of generated images." })), + "background": Schema.optionalKey( + Schema.Literals(["transparent", "opaque"]).annotate({ + "description": "The background parameter used for the image generation. Either `transparent` or `opaque`." + }) + ), + "output_format": Schema.optionalKey( + Schema.Literals(["png", "webp", "jpeg"]).annotate({ + "description": "The output format of the image generation. Either `png`, `webp`, or `jpeg`." + }) + ), + "size": Schema.optionalKey( + Schema.Literals(["1024x1024", "1024x1536", "1536x1024"]).annotate({ + "description": "The size of the image generated. Either `1024x1024`, `1024x1536`, or `1536x1024`." + }) + ), + "quality": Schema.optionalKey( + Schema.Literals(["low", "medium", "high"]).annotate({ + "description": "The quality of the image generated. Either `low`, `medium`, or `high`." + }) + ), + "usage": Schema.optionalKey(ImageGenUsage) +}).annotate({ "title": "Image generation response", "description": "The response from the image generation endpoint." }) +export type AssistantMessageItem = { + readonly "id": string + readonly "object": "chatkit.thread_item" + readonly "created_at": number + readonly "thread_id": string + readonly "type": "chatkit.assistant_message" + readonly "content": ReadonlyArray +} +export const AssistantMessageItem = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Identifier of the thread item." }), + "object": Schema.Literal("chatkit.thread_item").annotate({ + "description": "Type discriminator that is always `chatkit.thread_item`." + }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) for when the item was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ "description": "Identifier of the parent thread." }), + "type": Schema.Literal("chatkit.assistant_message").annotate({ + "description": "Type discriminator that is always `chatkit.assistant_message`." + }), + "content": Schema.Array(ResponseOutputText).annotate({ "description": "Ordered assistant response segments." }) +}).annotate({ "title": "Assistant message", "description": "Assistant-authored message within a thread." }) +export type ThreadListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean +} +export const ThreadListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(ThreadResource).annotate({ "description": "A list of items" }), + "first_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the first item in the list." }), + Schema.Null + ]), + "last_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the last item in the list." }), + Schema.Null + ]), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }) +}).annotate({ "title": "Threads", "description": "A paginated list of ChatKit threads." }) +export type AuditLog = { + readonly "id": string + readonly "type": AuditLogEventType + readonly "effective_at": number + readonly "project"?: { readonly "id"?: string; readonly "name"?: string } + readonly "actor"?: AuditLogActor | null + readonly "api_key.created"?: { + readonly "id"?: string + readonly "data"?: { readonly "scopes"?: ReadonlyArray } + } + readonly "api_key.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { readonly "scopes"?: ReadonlyArray } + } + readonly "api_key.deleted"?: { readonly "id"?: string } + readonly "checkpoint.permission.created"?: { + readonly "id"?: string + readonly "data"?: { readonly "project_id"?: string; readonly "fine_tuned_model_checkpoint"?: string } + } + readonly "checkpoint.permission.deleted"?: { readonly "id"?: string } + readonly "external_key.registered"?: { readonly "id"?: string; readonly "data"?: {} } + readonly "external_key.removed"?: { readonly "id"?: string } + readonly "group.created"?: { readonly "id"?: string; readonly "data"?: { readonly "group_name"?: string } } + readonly "group.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { readonly "group_name"?: string } + } + readonly "group.deleted"?: { readonly "id"?: string } + readonly "scim.enabled"?: { readonly "id"?: string } + readonly "scim.disabled"?: { readonly "id"?: string } + readonly "invite.sent"?: { + readonly "id"?: string + readonly "data"?: { readonly "email"?: string; readonly "role"?: string } + } + readonly "invite.accepted"?: { readonly "id"?: string } + readonly "invite.deleted"?: { readonly "id"?: string } + readonly "ip_allowlist.created"?: { + readonly "id"?: string + readonly "name"?: string + readonly "allowed_ips"?: ReadonlyArray + } + readonly "ip_allowlist.updated"?: { readonly "id"?: string; readonly "allowed_ips"?: ReadonlyArray } + readonly "ip_allowlist.deleted"?: { + readonly "id"?: string + readonly "name"?: string + readonly "allowed_ips"?: ReadonlyArray + } + readonly "ip_allowlist.config.activated"?: { + readonly "configs"?: ReadonlyArray<{ readonly "id"?: string; readonly "name"?: string }> + } + readonly "ip_allowlist.config.deactivated"?: { + readonly "configs"?: ReadonlyArray<{ readonly "id"?: string; readonly "name"?: string }> + } + readonly "login.succeeded"?: {} + readonly "login.failed"?: { readonly "error_code"?: string; readonly "error_message"?: string } + readonly "logout.succeeded"?: {} + readonly "logout.failed"?: { readonly "error_code"?: string; readonly "error_message"?: string } + readonly "organization.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { + readonly "title"?: string + readonly "description"?: string + readonly "name"?: string + readonly "threads_ui_visibility"?: string + readonly "usage_dashboard_visibility"?: string + readonly "api_call_logging"?: string + readonly "api_call_logging_project_ids"?: string + } + } + readonly "project.created"?: { + readonly "id"?: string + readonly "data"?: { readonly "name"?: string; readonly "title"?: string } + } + readonly "project.updated"?: { readonly "id"?: string; readonly "changes_requested"?: { readonly "title"?: string } } + readonly "project.archived"?: { readonly "id"?: string } + readonly "project.deleted"?: { readonly "id"?: string } + readonly "rate_limit.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { + readonly "max_requests_per_1_minute"?: number + readonly "max_tokens_per_1_minute"?: number + readonly "max_images_per_1_minute"?: number + readonly "max_audio_megabytes_per_1_minute"?: number + readonly "max_requests_per_1_day"?: number + readonly "batch_1_day_max_input_tokens"?: number + } + } + readonly "rate_limit.deleted"?: { readonly "id"?: string } + readonly "role.created"?: { + readonly "id"?: string + readonly "role_name"?: string + readonly "permissions"?: ReadonlyArray + readonly "resource_type"?: string + readonly "resource_id"?: string + } + readonly "role.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { + readonly "role_name"?: string + readonly "resource_id"?: string + readonly "resource_type"?: string + readonly "permissions_added"?: ReadonlyArray + readonly "permissions_removed"?: ReadonlyArray + readonly "description"?: string + readonly "metadata"?: {} + } + } + readonly "role.deleted"?: { readonly "id"?: string } + readonly "role.assignment.created"?: { + readonly "id"?: string + readonly "principal_id"?: string + readonly "principal_type"?: string + readonly "resource_id"?: string + readonly "resource_type"?: string + } + readonly "role.assignment.deleted"?: { + readonly "id"?: string + readonly "principal_id"?: string + readonly "principal_type"?: string + readonly "resource_id"?: string + readonly "resource_type"?: string + } + readonly "service_account.created"?: { readonly "id"?: string; readonly "data"?: { readonly "role"?: string } } + readonly "service_account.updated"?: { + readonly "id"?: string + readonly "changes_requested"?: { readonly "role"?: string } + } + readonly "service_account.deleted"?: { readonly "id"?: string } + readonly "user.added"?: { readonly "id"?: string; readonly "data"?: { readonly "role"?: string } } + readonly "user.updated"?: { readonly "id"?: string; readonly "changes_requested"?: { readonly "role"?: string } } + readonly "user.deleted"?: { readonly "id"?: string } + readonly "certificate.created"?: { readonly "id"?: string; readonly "name"?: string } + readonly "certificate.updated"?: { readonly "id"?: string; readonly "name"?: string } + readonly "certificate.deleted"?: { readonly "id"?: string; readonly "name"?: string; readonly "certificate"?: string } + readonly "certificates.activated"?: { + readonly "certificates"?: ReadonlyArray<{ readonly "id"?: string; readonly "name"?: string }> + } + readonly "certificates.deactivated"?: { + readonly "certificates"?: ReadonlyArray<{ readonly "id"?: string; readonly "name"?: string }> + } +} +export const AuditLog = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The ID of this log." }), + "type": AuditLogEventType, + "effective_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of the event.", + "format": "unixtime" + }).check(Schema.isInt()), + "project": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The project title." })) + }).annotate({ + "description": + "The project that the action was scoped to. Absent for actions not scoped to projects. Note that any admin actions taken via Admin API keys are associated with the default project." + }) + ), + "actor": Schema.optionalKey(Schema.Union([AuditLogActor, Schema.Null])), + "api_key.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The tracking ID of the API key." })), + "data": Schema.optionalKey( + Schema.Struct({ + "scopes": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "A list of scopes allowed for the API key, e.g. `[\"api.model.request\"]`" + }) + ) + }).annotate({ "description": "The payload used to create the API key." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "api_key.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The tracking ID of the API key." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "scopes": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "A list of scopes allowed for the API key, e.g. `[\"api.model.request\"]`" + }) + ) + }).annotate({ "description": "The payload used to update the API key." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "api_key.deleted": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The tracking ID of the API key." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "checkpoint.permission.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the checkpoint permission." })), + "data": Schema.optionalKey( + Schema.Struct({ + "project_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The ID of the project that the checkpoint permission was created for." + }) + ), + "fine_tuned_model_checkpoint": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the fine-tuned model checkpoint." }) + ) + }).annotate({ "description": "The payload used to create the checkpoint permission." }) + ) + }).annotate({ + "description": "The project and fine-tuned model checkpoint that the checkpoint permission was created for." + }) + ), + "checkpoint.permission.deleted": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the checkpoint permission." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "external_key.registered": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the external key configuration." })), + "data": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "The configuration for the external key." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "external_key.removed": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the external key configuration." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "group.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the group." })), + "data": Schema.optionalKey( + Schema.Struct({ + "group_name": Schema.optionalKey(Schema.String.annotate({ "description": "The group name." })) + }).annotate({ "description": "Information about the created group." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "group.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the group." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "group_name": Schema.optionalKey(Schema.String.annotate({ "description": "The updated group name." })) + }).annotate({ "description": "The payload used to update the group." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "group.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the group." })) }) + .annotate({ "description": "The details for events with this `type`." }) + ), + "scim.enabled": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the SCIM was enabled for." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "scim.disabled": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the SCIM was disabled for." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "invite.sent": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the invite." })), + "data": Schema.optionalKey( + Schema.Struct({ + "email": Schema.optionalKey( + Schema.String.annotate({ "description": "The email invited to the organization." }) + ), + "role": Schema.optionalKey( + Schema.String.annotate({ + "description": "The role the email was invited to be. Is either `owner` or `member`." + }) + ) + }).annotate({ "description": "The payload used to create the invite." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "invite.accepted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the invite." })) }) + .annotate({ "description": "The details for events with this `type`." }) + ), + "invite.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the invite." })) }) + .annotate({ "description": "The details for events with this `type`." }) + ), + "ip_allowlist.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the IP allowlist configuration." })), + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "The name of the IP allowlist configuration." }) + ), + "allowed_ips": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "The IP addresses or CIDR ranges included in the configuration." + }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "ip_allowlist.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the IP allowlist configuration." })), + "allowed_ips": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "The updated set of IP addresses or CIDR ranges in the configuration." + }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "ip_allowlist.deleted": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The ID of the IP allowlist configuration." })), + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "The name of the IP allowlist configuration." }) + ), + "allowed_ips": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "The IP addresses or CIDR ranges that were in the configuration." + }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "ip_allowlist.config.activated": Schema.optionalKey( + Schema.Struct({ + "configs": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the IP allowlist configuration." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "The name of the IP allowlist configuration." }) + ) + }) + ).annotate({ "description": "The configurations that were activated." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "ip_allowlist.config.deactivated": Schema.optionalKey( + Schema.Struct({ + "configs": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the IP allowlist configuration." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "The name of the IP allowlist configuration." }) + ) + }) + ).annotate({ "description": "The configurations that were deactivated." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "login.succeeded": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": "This event has no additional fields beyond the standard audit log attributes." + }) + ), + "login.failed": Schema.optionalKey( + Schema.Struct({ + "error_code": Schema.optionalKey(Schema.String.annotate({ "description": "The error code of the failure." })), + "error_message": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message of the failure." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "logout.succeeded": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": "This event has no additional fields beyond the standard audit log attributes." + }) + ), + "logout.failed": Schema.optionalKey( + Schema.Struct({ + "error_code": Schema.optionalKey(Schema.String.annotate({ "description": "The error code of the failure." })), + "error_message": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message of the failure." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "organization.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The organization ID." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "title": Schema.optionalKey(Schema.String.annotate({ "description": "The organization title." })), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "The organization description." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The organization name." })), + "threads_ui_visibility": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Visibility of the threads page which shows messages created with the Assistants API and Playground. One of `ANY_ROLE`, `OWNERS`, or `NONE`." + }) + ), + "usage_dashboard_visibility": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Visibility of the usage dashboard which shows activity and costs for your organization. One of `ANY_ROLE` or `OWNERS`." + }) + ), + "api_call_logging": Schema.optionalKey( + Schema.String.annotate({ + "description": + "How your organization logs data from supported API calls. One of `disabled`, `enabled_per_call`, `enabled_for_all_projects`, or `enabled_for_selected_projects`" + }) + ), + "api_call_logging_project_ids": Schema.optionalKey( + Schema.String.annotate({ + "description": "The list of project ids if api_call_logging is set to `enabled_for_selected_projects`" + }) + ) + }).annotate({ "description": "The payload used to update the organization settings." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "project.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })), + "data": Schema.optionalKey( + Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The project name." })), + "title": Schema.optionalKey( + Schema.String.annotate({ "description": "The title of the project as seen on the dashboard." }) + ) + }).annotate({ "description": "The payload used to create the project." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "project.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "title": Schema.optionalKey( + Schema.String.annotate({ "description": "The title of the project as seen on the dashboard." }) + ) + }).annotate({ "description": "The payload used to update the project." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "project.archived": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })) }).annotate({ + "description": "The details for events with this `type`." + }) + ), + "project.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })) }).annotate({ + "description": "The details for events with this `type`." + }) + ), + "rate_limit.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The rate limit ID" })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "max_requests_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum requests per minute." }).check(Schema.isInt()) + ), + "max_tokens_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum tokens per minute." }).check(Schema.isInt()) + ), + "max_images_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum images per minute. Only relevant for certain models." + }).check(Schema.isInt()) + ), + "max_audio_megabytes_per_1_minute": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum audio megabytes per minute. Only relevant for certain models." + }).check(Schema.isInt()) + ), + "max_requests_per_1_day": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum requests per day. Only relevant for certain models." }) + .check(Schema.isInt()) + ), + "batch_1_day_max_input_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The maximum batch input tokens per day. Only relevant for certain models." + }).check(Schema.isInt()) + ) + }).annotate({ "description": "The payload used to update the rate limits." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "rate_limit.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The rate limit ID" })) }) + .annotate({ "description": "The details for events with this `type`." }) + ), + "role.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The role ID." })), + "role_name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the role." })), + "permissions": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "The permissions granted by the role." }) + ), + "resource_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of resource the role belongs to." }) + ), + "resource_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The resource the role is scoped to." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "role.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The role ID." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "role_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The updated role name, when provided." }) + ), + "resource_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The resource the role is scoped to." }) + ), + "resource_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of resource the role belongs to." }) + ), + "permissions_added": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "The permissions added to the role." }) + ), + "permissions_removed": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "The permissions removed from the role." }) + ), + "description": Schema.optionalKey( + Schema.String.annotate({ "description": "The updated role description, when provided." }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Additional metadata stored on the role." }) + ) + }).annotate({ "description": "The payload used to update the role." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "role.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The role ID." })) }).annotate({ + "description": "The details for events with this `type`." + }) + ), + "role.assignment.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The identifier of the role assignment." })), + "principal_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The principal (user or group) that received the role." }) + ), + "principal_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of principal (user or group) that received the role." }) + ), + "resource_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The resource the role assignment is scoped to." }) + ), + "resource_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of resource the role assignment is scoped to." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "role.assignment.deleted": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The identifier of the role assignment." })), + "principal_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The principal (user or group) that had the role removed." }) + ), + "principal_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of principal (user or group) that had the role removed." }) + ), + "resource_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The resource the role assignment was scoped to." }) + ), + "resource_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of resource the role assignment was scoped to." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "service_account.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The service account ID." })), + "data": Schema.optionalKey( + Schema.Struct({ + "role": Schema.optionalKey( + Schema.String.annotate({ "description": "The role of the service account. Is either `owner` or `member`." }) + ) + }).annotate({ "description": "The payload used to create the service account." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "service_account.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The service account ID." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "role": Schema.optionalKey( + Schema.String.annotate({ "description": "The role of the service account. Is either `owner` or `member`." }) + ) + }).annotate({ "description": "The payload used to updated the service account." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "service_account.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The service account ID." })) }) + .annotate({ "description": "The details for events with this `type`." }) + ), + "user.added": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The user ID." })), + "data": Schema.optionalKey( + Schema.Struct({ + "role": Schema.optionalKey( + Schema.String.annotate({ "description": "The role of the user. Is either `owner` or `member`." }) + ) + }).annotate({ "description": "The payload used to add the user to the project." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "user.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The project ID." })), + "changes_requested": Schema.optionalKey( + Schema.Struct({ + "role": Schema.optionalKey( + Schema.String.annotate({ "description": "The role of the user. Is either `owner` or `member`." }) + ) + }).annotate({ "description": "The payload used to update the user." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "user.deleted": Schema.optionalKey( + Schema.Struct({ "id": Schema.optionalKey(Schema.String.annotate({ "description": "The user ID." })) }).annotate({ + "description": "The details for events with this `type`." + }) + ), + "certificate.created": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The certificate ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the certificate." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "certificate.updated": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The certificate ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the certificate." })) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "certificate.deleted": Schema.optionalKey( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The certificate ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the certificate." })), + "certificate": Schema.optionalKey( + Schema.String.annotate({ "description": "The certificate content in PEM format." }) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "certificates.activated": Schema.optionalKey( + Schema.Struct({ + "certificates": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The certificate ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the certificate." })) + }) + ) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ), + "certificates.deactivated": Schema.optionalKey( + Schema.Struct({ + "certificates": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The certificate ID." })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the certificate." })) + }) + ) + ) + }).annotate({ "description": "The details for events with this `type`." }) + ) +}).annotate({ "description": "A log of a user action or configuration change within this organization." }) +export type CreateChatCompletionResponse = { + readonly "id": string + readonly "choices": ReadonlyArray< + { + readonly "finish_reason": "stop" | "length" | "tool_calls" | "content_filter" | "function_call" + readonly "index": number + readonly "message": ChatCompletionResponseMessage + readonly "logprobs": { + readonly "content": ReadonlyArray | null + readonly "refusal": ReadonlyArray | null + } | null + } + > + readonly "created": number + readonly "model": string + readonly "service_tier"?: ServiceTier + readonly "system_fingerprint"?: string + readonly "object": "chat.completion" + readonly "usage"?: CompletionUsage +} +export const CreateChatCompletionResponse = Schema.Struct({ + "id": Schema.String.annotate({ "description": "A unique identifier for the chat completion." }), + "choices": Schema.Array(Schema.Struct({ + "finish_reason": Schema.Literals(["stop", "length", "tool_calls", "content_filter", "function_call"]).annotate({ + "description": + "The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence,\n`length` if the maximum number of tokens specified in the request was reached,\n`content_filter` if content was omitted due to a flag from our content filters,\n`tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.\n" + }), + "index": Schema.Number.annotate({ "description": "The index of the choice in the list of choices." }).check( + Schema.isInt() + ), + "message": ChatCompletionResponseMessage, + "logprobs": Schema.Union([ + Schema.Struct({ + "content": Schema.Union([ + Schema.Array(ChatCompletionTokenLogprob).annotate({ + "description": "A list of message content tokens with log probability information." + }), + Schema.Null + ]), + "refusal": Schema.Union([ + Schema.Array(ChatCompletionTokenLogprob).annotate({ + "description": "A list of message refusal tokens with log probability information." + }), + Schema.Null + ]) + }).annotate({ "description": "Log probability information for the choice." }), + Schema.Null + ]) + })).annotate({ "description": "A list of chat completion choices. Can be more than one if `n` is greater than 1." }), + "created": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) of when the chat completion was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "model": Schema.String.annotate({ "description": "The model used for the chat completion." }), + "service_tier": Schema.optionalKey(ServiceTier), + "system_fingerprint": Schema.optionalKey( + Schema.String.annotate({ + "description": + "This fingerprint represents the backend configuration that the model runs with.\n\nCan be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism.\n" + }) + ), + "object": Schema.Literal("chat.completion").annotate({ + "description": "The object type, which is always `chat.completion`." + }), + "usage": Schema.optionalKey(CompletionUsage) +}).annotate({ "description": "Represents a chat completion response returned by model, based on the provided input." }) +export type ChatCompletionRequestMessage = + | ChatCompletionRequestDeveloperMessage + | ChatCompletionRequestSystemMessage + | ChatCompletionRequestUserMessage + | ChatCompletionRequestAssistantMessage + | ChatCompletionRequestToolMessage + | ChatCompletionRequestFunctionMessage +export const ChatCompletionRequestMessage = Schema.Union([ + ChatCompletionRequestDeveloperMessage, + ChatCompletionRequestSystemMessage, + ChatCompletionRequestUserMessage, + ChatCompletionRequestAssistantMessage, + ChatCompletionRequestToolMessage, + ChatCompletionRequestFunctionMessage +], { mode: "oneOf" }) +export type RunStepObject = { + readonly "id": string + readonly "object": "thread.run.step" + readonly "created_at": number + readonly "assistant_id": string + readonly "thread_id": string + readonly "run_id": string + readonly "type": "message_creation" | "tool_calls" + readonly "status": "in_progress" | "cancelled" | "failed" | "completed" | "expired" + readonly "step_details": { + readonly "type": "message_creation" + readonly "message_creation": { readonly "message_id": string } + } | { + readonly "type": "tool_calls" + readonly "tool_calls": ReadonlyArray< + | RunStepDetailsToolCallsCodeObject + | RunStepDetailsToolCallsFileSearchObject + | RunStepDetailsToolCallsFunctionObject + > + } + readonly "last_error": { readonly "code": "server_error" | "rate_limit_exceeded"; readonly "message": string } | null + readonly "expired_at": number | null + readonly "cancelled_at": number | null + readonly "failed_at": number | null + readonly "completed_at": number | null + readonly "metadata": Metadata + readonly "usage": RunStepCompletionUsage +} +export const RunStepObject = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The identifier of the run step, which can be referenced in API endpoints." + }), + "object": Schema.Literal("thread.run.step").annotate({ + "description": "The object type, which is always `thread.run.step`." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the run step was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "assistant_id": Schema.String.annotate({ + "description": "The ID of the [assistant](/docs/api-reference/assistants) associated with the run step." + }), + "thread_id": Schema.String.annotate({ + "description": "The ID of the [thread](/docs/api-reference/threads) that was run." + }), + "run_id": Schema.String.annotate({ + "description": "The ID of the [run](/docs/api-reference/runs) that this run step is a part of." + }), + "type": Schema.Literals(["message_creation", "tool_calls"]).annotate({ + "description": "The type of run step, which can be either `message_creation` or `tool_calls`." + }), + "status": Schema.Literals(["in_progress", "cancelled", "failed", "completed", "expired"]).annotate({ + "description": + "The status of the run step, which can be either `in_progress`, `cancelled`, `failed`, `completed`, or `expired`." + }), + "step_details": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("message_creation").annotate({ "description": "Always `message_creation`." }), + "message_creation": Schema.Struct({ + "message_id": Schema.String.annotate({ + "description": "The ID of the message that was created by this run step." + }) + }) + }).annotate({ "title": "Message creation", "description": "The details of the run step." }), + Schema.Struct({ + "type": Schema.Literal("tool_calls").annotate({ "description": "Always `tool_calls`." }), + "tool_calls": Schema.Array( + Schema.Union([ + RunStepDetailsToolCallsCodeObject, + RunStepDetailsToolCallsFileSearchObject, + RunStepDetailsToolCallsFunctionObject + ], { mode: "oneOf" }) + ).annotate({ + "description": + "An array of tool calls the run step was involved in. These can be associated with one of three types of tools: `code_interpreter`, `file_search`, or `function`.\n" + }) + }).annotate({ "title": "Tool calls", "description": "The details of the run step." }) + ], { mode: "oneOf" }), + "last_error": Schema.Union([ + Schema.Struct({ + "code": Schema.Literals(["server_error", "rate_limit_exceeded"]).annotate({ + "description": "One of `server_error` or `rate_limit_exceeded`." + }), + "message": Schema.String.annotate({ "description": "A human-readable description of the error." }) + }).annotate({ + "description": "The last error associated with this run step. Will be `null` if there are no errors." + }), + Schema.Null + ]), + "expired_at": Schema.Union([ + Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) for when the run step expired. A step is considered expired if the parent run is expired.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "cancelled_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the run step was cancelled.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "failed_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the run step failed.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "completed_at": Schema.Union([ + Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the run step completed.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "metadata": Metadata, + "usage": RunStepCompletionUsage +}).annotate({ "title": "Run steps", "description": "Represents a step in execution of a run.\n" }) +export type ListMessagesResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean + readonly [x: string]: unknown +} +export const ListMessagesResponse = Schema.StructWithRest( + Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(MessageObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean + }), + [Schema.Record(Schema.String, Schema.Json)] +) +export type MessageStreamEvent = + | { readonly "event": "thread.message.created"; readonly "data": MessageObject } + | { readonly "event": "thread.message.in_progress"; readonly "data": MessageObject } + | { readonly "event": "thread.message.delta"; readonly "data": MessageDeltaObject } + | { readonly "event": "thread.message.completed"; readonly "data": MessageObject } + | { readonly "event": "thread.message.incomplete"; readonly "data": MessageObject } +export const MessageStreamEvent = Schema.Union([ + Schema.Struct({ "event": Schema.Literal("thread.message.created"), "data": MessageObject }).annotate({ + "description": "Occurs when a [message](/docs/api-reference/messages/object) is created." + }), + Schema.Struct({ "event": Schema.Literal("thread.message.in_progress"), "data": MessageObject }).annotate({ + "description": "Occurs when a [message](/docs/api-reference/messages/object) moves to an `in_progress` state." + }), + Schema.Struct({ "event": Schema.Literal("thread.message.delta"), "data": MessageDeltaObject }).annotate({ + "description": "Occurs when parts of a [Message](/docs/api-reference/messages/object) are being streamed." + }), + Schema.Struct({ "event": Schema.Literal("thread.message.completed"), "data": MessageObject }).annotate({ + "description": "Occurs when a [message](/docs/api-reference/messages/object) is completed." + }), + Schema.Struct({ "event": Schema.Literal("thread.message.incomplete"), "data": MessageObject }).annotate({ + "description": "Occurs when a [message](/docs/api-reference/messages/object) ends before it is completed." + }) +], { mode: "oneOf" }) +export type RealtimeBetaResponse = { + readonly "id"?: string + readonly "object"?: "realtime.response" + readonly "status"?: "completed" | "cancelled" | "failed" | "incomplete" | "in_progress" + readonly "status_details"?: { + readonly "type"?: "completed" | "cancelled" | "failed" | "incomplete" + readonly "reason"?: "turn_detected" | "client_cancelled" | "max_output_tokens" | "content_filter" + readonly "error"?: { readonly "type"?: string; readonly "code"?: string } + } + readonly "output"?: ReadonlyArray + readonly "metadata"?: Metadata + readonly "usage"?: { + readonly "total_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "input_token_details"?: { + readonly "cached_tokens"?: number + readonly "text_tokens"?: number + readonly "image_tokens"?: number + readonly "audio_tokens"?: number + readonly "cached_tokens_details"?: { + readonly "text_tokens"?: number + readonly "image_tokens"?: number + readonly "audio_tokens"?: number + } + } + readonly "output_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + } + readonly "conversation_id"?: string + readonly "voice"?: + | string + | "alloy" + | "ash" + | "ballad" + | "coral" + | "echo" + | "sage" + | "shimmer" + | "verse" + | "marin" + | "cedar" + readonly "modalities"?: ReadonlyArray<"text" | "audio"> + readonly "output_audio_format"?: "pcm16" | "g711_ulaw" | "g711_alaw" + readonly "temperature"?: number + readonly "max_output_tokens"?: number | "inf" +} +export const RealtimeBetaResponse = Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the response." })), + "object": Schema.optionalKey( + Schema.Literal("realtime.response").annotate({ "description": "The object type, must be `realtime.response`." }) + ), + "status": Schema.optionalKey( + Schema.Literals(["completed", "cancelled", "failed", "incomplete", "in_progress"]).annotate({ + "description": + "The final status of the response (`completed`, `cancelled`, `failed`, or \n`incomplete`, `in_progress`).\n" + }) + ), + "status_details": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["completed", "cancelled", "failed", "incomplete"]).annotate({ + "description": + "The type of error that caused the response to fail, corresponding \nwith the `status` field (`completed`, `cancelled`, `incomplete`, \n`failed`).\n" + }) + ), + "reason": Schema.optionalKey( + Schema.Literals(["turn_detected", "client_cancelled", "max_output_tokens", "content_filter"]).annotate({ + "description": + "The reason the Response did not complete. For a `cancelled` Response, \none of `turn_detected` (the server VAD detected a new start of speech) \nor `client_cancelled` (the client sent a cancel event). For an \n`incomplete` Response, one of `max_output_tokens` or `content_filter` \n(the server-side safety filter activated and cut off the response).\n" + }) + ), + "error": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey(Schema.String.annotate({ "description": "The type of error." })), + "code": Schema.optionalKey(Schema.String.annotate({ "description": "Error code, if any." })) + }).annotate({ + "description": + "A description of the error that caused the response to fail, \npopulated when the `status` is `failed`.\n" + }) + ) + }).annotate({ "description": "Additional details about the status." }) + ), + "output": Schema.optionalKey( + Schema.Array(RealtimeConversationItem).annotate({ + "description": "The list of output items generated by the response." + }) + ), + "metadata": Schema.optionalKey(Metadata), + "usage": Schema.optionalKey( + Schema.Struct({ + "total_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The total number of tokens in the Response including input and output \ntext and audio tokens.\n" + }).check(Schema.isInt()) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of input tokens used in the Response, including text and \naudio tokens.\n" + }).check(Schema.isInt()) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of output tokens sent in the Response, including text and \naudio tokens.\n" + }).check(Schema.isInt()) + ), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of cached tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of text tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "image_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of image tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of audio tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "cached_tokens_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached text tokens used as input for the Response." + }).check(Schema.isInt()) + ), + "image_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached image tokens used as input for the Response." + }).check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached audio tokens used as input for the Response." + }).check(Schema.isInt()) + ) + }).annotate({ "description": "Details about the cached tokens used as input for the Response." }) + ) + }).annotate({ "description": "Details about the input tokens used in the Response." }) + ), + "output_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of text tokens used in the Response." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of audio tokens used in the Response." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the output tokens used in the Response." }) + ) + }).annotate({ + "description": + "Usage statistics for the Response, this will correspond to billing. A \nRealtime API session will maintain a conversation context and append new \nItems to the Conversation, thus output from previous turns (text and \naudio tokens) will become the input for later turns.\n" + }) + ), + "conversation_id": Schema.optionalKey(Schema.String.annotate({ + "description": + "Which conversation the response is added to, determined by the `conversation`\nfield in the `response.create` event. If `auto`, the response will be added to\nthe default conversation and the value of `conversation_id` will be an id like\n`conv_1234`. If `none`, the response will not be added to any conversation and\nthe value of `conversation_id` will be `null`. If responses are being triggered\nby server VAD, the response will be added to the default conversation, thus\nthe `conversation_id` will be an id like `conv_1234`.\n" + })), + "voice": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]) + ]).annotate({ + "description": + "The voice the model used to respond.\nCurrent voice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`,\n`shimmer`, and `verse`.\n" + }) + ), + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model used to respond. If there are multiple modalities,\nthe model will pick one, for example if `modalities` is `[\"text\", \"audio\"]`, the model\ncould be responding in either text or audio.\n" + }) + ), + "output_audio_format": Schema.optionalKey( + Schema.Literals(["pcm16", "g711_ulaw", "g711_alaw"]).annotate({ + "description": "The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8.\n" + }).check(Schema.isFinite()) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls, that was used in this response.\n" + }) + ) +}).annotate({ "description": "The response resource." }) +export type RealtimeBetaResponseCreateParams = { + readonly "modalities"?: ReadonlyArray<"text" | "audio"> + readonly "instructions"?: string + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + readonly "output_audio_format"?: "pcm16" | "g711_ulaw" | "g711_alaw" + readonly "tools"?: ReadonlyArray< + { + readonly "type"?: "function" + readonly "name"?: string + readonly "description"?: string + readonly "parameters"?: {} + } + > + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "temperature"?: number + readonly "max_output_tokens"?: number | "inf" + readonly "conversation"?: string | "auto" | "none" + readonly "metadata"?: Metadata + readonly "prompt"?: Prompt + readonly "input"?: ReadonlyArray +} +export const RealtimeBetaResponseCreateParams = Schema.Struct({ + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": "The set of modalities the model can respond with. To disable audio,\nset this to [\"text\"].\n" + }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model \ncalls. This field allows the client to guide the model on desired \nresponses. The model can be instructed on response content and format, \n(e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good \nresponses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion \ninto your voice\", \"laugh frequently\"). The instructions are not guaranteed \nto be followed by the model, but they provide guidance to the model on the \ndesired behavior.\n\nNote that the server sets default instructions which will be used if this \nfield is not set and are visible in the `session.created` event at the \nstart of the session.\n" + })), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) }) + .annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with an\n`id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed during\nthe session once the model has responded with audio at least once.\n" + }) + ), + "output_audio_format": Schema.optionalKey( + Schema.Literals(["pcm16", "g711_ulaw", "g711_alaw"]).annotate({ + "description": "The format of output audio. Options are `pcm16`, `g711_ulaw`, or `g711_alaw`.\n" + }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("function").annotate({ "description": "The type of the tool, i.e. `function`." }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the function." })), + "description": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The description of the function, including guidance on when and how \nto call it, and guidance about what to tell the user when calling \n(if anything).\n" + }) + ), + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Parameters of the function in JSON Schema." }) + ) + })).annotate({ "description": "Tools (functions) available to the model." }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Sampling temperature for the model, limited to [0.6, 1.2]. Defaults to 0.8.\n" + }).check(Schema.isFinite()) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "conversation": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Literals(["auto", "none"])], { mode: "oneOf" }).annotate({ + "description": + "Controls which conversation the response is added to. Currently supports\n`auto` and `none`, with `auto` as the default value. The `auto` value\nmeans that the contents of the response will be added to the default\nconversation. Set this to `none` to create an out-of-band response which \nwill not add items to default conversation.\n" + }) + ), + "metadata": Schema.optionalKey(Metadata), + "prompt": Schema.optionalKey(Prompt), + "input": Schema.optionalKey( + Schema.Array(RealtimeConversationItem).annotate({ + "description": + "Input items to include in the prompt for the model. Using this field\ncreates a new context for this Response instead of using the default\nconversation. An empty array `[]` will clear the context for this Response.\nNote that this can include references to items from the default conversation.\n" + }) + ) +}).annotate({ "description": "Create a new Realtime response with these parameters" }) +export type RealtimeClientEventConversationItemCreate = { + readonly "event_id"?: string + readonly "type": "conversation.item.create" + readonly "previous_item_id"?: string + readonly "item": RealtimeConversationItem +} +export const RealtimeClientEventConversationItemCreate = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("conversation.item.create").annotate({ + "description": "The event type, must be `conversation.item.create`." + }), + "previous_item_id": Schema.optionalKey(Schema.String.annotate({ + "description": + "The ID of the preceding item after which the new item will be inserted. If not set, the new item will be appended to the end of the conversation.\n\nIf set to `root`, the new item will be added to the beginning of the conversation.\n\nIf set to an existing ID, it allows an item to be inserted mid-conversation. If the ID cannot be found, an error will be returned and the item will not be added.\n" + })), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Add a new Item to the Conversation's context, including messages, function \ncalls, and function call responses. This event can be used both to populate a \n\"history\" of the conversation and to add new items mid-stream, but has the \ncurrent limitation that it cannot populate assistant audio messages.\n\nIf successful, the server will respond with a `conversation.item.created` \nevent, otherwise an `error` event will be sent.\n" +}) +export type RealtimeResponse = { + readonly "id"?: string + readonly "object"?: "realtime.response" + readonly "status"?: "completed" | "cancelled" | "failed" | "incomplete" | "in_progress" + readonly "status_details"?: { + readonly "type"?: "completed" | "cancelled" | "failed" | "incomplete" + readonly "reason"?: "turn_detected" | "client_cancelled" | "max_output_tokens" | "content_filter" + readonly "error"?: { readonly "type"?: string; readonly "code"?: string } + } + readonly "output"?: ReadonlyArray + readonly "metadata"?: Metadata + readonly "audio"?: { + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: + | string + | "alloy" + | "ash" + | "ballad" + | "coral" + | "echo" + | "sage" + | "shimmer" + | "verse" + | "marin" + | "cedar" + } + } + readonly "usage"?: { + readonly "total_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "input_token_details"?: { + readonly "cached_tokens"?: number + readonly "text_tokens"?: number + readonly "image_tokens"?: number + readonly "audio_tokens"?: number + readonly "cached_tokens_details"?: { + readonly "text_tokens"?: number + readonly "image_tokens"?: number + readonly "audio_tokens"?: number + } + } + readonly "output_token_details"?: { readonly "text_tokens"?: number; readonly "audio_tokens"?: number } + } + readonly "conversation_id"?: string + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "max_output_tokens"?: number | "inf" +} +export const RealtimeResponse = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The unique ID of the response, will look like `resp_1234`." }) + ), + "object": Schema.optionalKey( + Schema.Literal("realtime.response").annotate({ "description": "The object type, must be `realtime.response`." }) + ), + "status": Schema.optionalKey( + Schema.Literals(["completed", "cancelled", "failed", "incomplete", "in_progress"]).annotate({ + "description": + "The final status of the response (`completed`, `cancelled`, `failed`, or \n`incomplete`, `in_progress`).\n" + }) + ), + "status_details": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literals(["completed", "cancelled", "failed", "incomplete"]).annotate({ + "description": + "The type of error that caused the response to fail, corresponding \nwith the `status` field (`completed`, `cancelled`, `incomplete`, \n`failed`).\n" + }) + ), + "reason": Schema.optionalKey( + Schema.Literals(["turn_detected", "client_cancelled", "max_output_tokens", "content_filter"]).annotate({ + "description": + "The reason the Response did not complete. For a `cancelled` Response, one of `turn_detected` (the server VAD detected a new start of speech) or `client_cancelled` (the client sent a cancel event). For an `incomplete` Response, one of `max_output_tokens` or `content_filter` (the server-side safety filter activated and cut off the response).\n" + }) + ), + "error": Schema.optionalKey( + Schema.Struct({ + "type": Schema.optionalKey(Schema.String.annotate({ "description": "The type of error." })), + "code": Schema.optionalKey(Schema.String.annotate({ "description": "Error code, if any." })) + }).annotate({ + "description": + "A description of the error that caused the response to fail, \npopulated when the `status` is `failed`.\n" + }) + ) + }).annotate({ "description": "Additional details about the status." }) + ), + "output": Schema.optionalKey( + Schema.Array(RealtimeConversationItem).annotate({ + "description": "The list of output items generated by the response." + }) + ), + "metadata": Schema.optionalKey(Metadata), + "audio": Schema.optionalKey( + Schema.Struct({ + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Literals(["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse", "marin", "cedar"]) + ]).annotate({ + "description": + "The voice the model uses to respond. Voice cannot be changed during the\nsession once the model has responded with audio at least once. Current\nvoice options are `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`,\n`shimmer`, `verse`, `marin`, and `cedar`. We recommend `marin` and `cedar` for\nbest quality.\n" + }) + ) + })) + }).annotate({ "description": "Configuration for audio output." }) + ), + "usage": Schema.optionalKey( + Schema.Struct({ + "total_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The total number of tokens in the Response including input and output \ntext and audio tokens.\n" + }).check(Schema.isInt()) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of input tokens used in the Response, including text and \naudio tokens.\n" + }).check(Schema.isInt()) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of output tokens sent in the Response, including text and \naudio tokens.\n" + }).check(Schema.isInt()) + ), + "input_token_details": Schema.optionalKey( + Schema.Struct({ + "cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of cached tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of text tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "image_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of image tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of audio tokens used as input for the Response." }) + .check(Schema.isInt()) + ), + "cached_tokens_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached text tokens used as input for the Response." + }).check(Schema.isInt()) + ), + "image_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached image tokens used as input for the Response." + }).check(Schema.isInt()) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The number of cached audio tokens used as input for the Response." + }).check(Schema.isInt()) + ) + }).annotate({ "description": "Details about the cached tokens used as input for the Response." }) + ) + }).annotate({ + "description": + "Details about the input tokens used in the Response. Cached tokens are tokens from previous turns in the conversation that are included as context for the current response. Cached tokens here are counted as a subset of input tokens, meaning input tokens will include cached and uncached tokens." + }) + ), + "output_token_details": Schema.optionalKey( + Schema.Struct({ + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of text tokens used in the Response." }).check( + Schema.isInt() + ) + ), + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of audio tokens used in the Response." }).check( + Schema.isInt() + ) + ) + }).annotate({ "description": "Details about the output tokens used in the Response." }) + ) + }).annotate({ + "description": + "Usage statistics for the Response, this will correspond to billing. A \nRealtime API session will maintain a conversation context and append new \nItems to the Conversation, thus output from previous turns (text and \naudio tokens) will become the input for later turns.\n" + }) + ), + "conversation_id": Schema.optionalKey(Schema.String.annotate({ + "description": + "Which conversation the response is added to, determined by the `conversation`\nfield in the `response.create` event. If `auto`, the response will be added to\nthe default conversation and the value of `conversation_id` will be an id like\n`conv_1234`. If `none`, the response will not be added to any conversation and\nthe value of `conversation_id` will be `null`. If responses are being triggered\nautomatically by VAD the response will be added to the default conversation\n" + })), + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model used to respond, currently the only possible values are\n`[\\\"audio\\\"]`, `[\\\"text\\\"]`. Audio output always include a text transcript. Setting the\noutput to mode `text` will disable audio output from the model.\n" + }) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls, that was used in this response.\n" + }) + ) +}).annotate({ "description": "The response resource." }) +export type RealtimeResponseCreateParams = { + readonly "output_modalities"?: ReadonlyArray<"text" | "audio"> + readonly "instructions"?: string + readonly "audio"?: { + readonly "output"?: { + readonly "format"?: { readonly "type"?: "audio/pcm"; readonly "rate"?: 24000 } | { + readonly "type"?: "audio/pcmu" + } | { readonly "type"?: "audio/pcma" } + readonly "voice"?: VoiceIdsShared | { readonly "id": string } + } + } + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ToolChoiceOptions | ToolChoiceFunction | ToolChoiceMCP + readonly "parallel_tool_calls"?: boolean + readonly "reasoning"?: RealtimeReasoning + readonly "max_output_tokens"?: number | "inf" + readonly "conversation"?: string | "auto" | "none" + readonly "metadata"?: Metadata + readonly "prompt"?: Prompt + readonly "input"?: ReadonlyArray +} +export const RealtimeResponseCreateParams = Schema.Struct({ + "output_modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "audio"])).annotate({ + "description": + "The set of modalities the model used to respond, currently the only possible values are\n`[\\\"audio\\\"]`, `[\\\"text\\\"]`. Audio output always include a text transcript. Setting the\noutput to mode `text` will disable audio output from the model.\n" + }) + ), + "instructions": Schema.optionalKey(Schema.String.annotate({ + "description": + "The default system instructions (i.e. system message) prepended to model calls. This field allows the client to guide the model on desired responses. The model can be instructed on response content and format, (e.g. \"be extremely succinct\", \"act friendly\", \"here are examples of good responses\") and on audio behavior (e.g. \"talk quickly\", \"inject emotion into your voice\", \"laugh frequently\"). The instructions are not guaranteed to be followed by the model, but they provide guidance to the model on the desired behavior.\nNote that the server sets default instructions which will be used if this field is not set and are visible in the `session.created` event at the start of the session.\n" + })), + "audio": Schema.optionalKey( + Schema.Struct({ + "output": Schema.optionalKey(Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcm").annotate({ "description": "The audio format. Always `audio/pcm`." }) + ), + "rate": Schema.optionalKey( + Schema.Literal(24000).annotate({ "description": "The sample rate of the audio. Always `24000`." }) + ) + }).annotate({ + "title": "PCM audio format", + "description": "The PCM audio format. Only a 24kHz sample rate is supported." + }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcmu").annotate({ "description": "The audio format. Always `audio/pcmu`." }) + ) + }).annotate({ "title": "PCMU audio format", "description": "The G.711 μ-law format." }), + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("audio/pcma").annotate({ "description": "The audio format. Always `audio/pcma`." }) + ) + }).annotate({ "title": "PCMA audio format", "description": "The G.711 A-law format." }) + ]).annotate({ "description": "The format of the output audio." }) + ), + "voice": Schema.optionalKey( + Schema.Union([ + VoiceIdsShared, + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) + }).annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`,\n`marin`, and `cedar`. You may also provide a custom voice object with\nan `id`, for example `{ \"id\": \"voice_1234\" }`. Voice cannot be changed\nduring the session once the model has responded with audio at least once.\nWe recommend `marin` and `cedar` for best quality.\n" + }) + ) + })) + }).annotate({ "description": "Configuration for audio input and output." }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([RealtimeFunctionTool, MCPTool], { mode: "oneOf" })).annotate({ + "description": "Tools available to the model." + }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ToolChoiceOptions, ToolChoiceFunction, ToolChoiceMCP], { mode: "oneOf" }).annotate({ + "description": + "How the model chooses tools. Provide one of the string modes or force a specific\nfunction/MCP tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the model may call multiple tools in parallel. Only supported by\nreasoning Realtime models such as `gpt-realtime-2`.\n" + }) + ), + "reasoning": Schema.optionalKey(RealtimeReasoning), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Literal("inf")], { mode: "oneOf" }).annotate({ + "description": + "Maximum number of output tokens for a single assistant response,\ninclusive of tool calls. Provide an integer between 1 and 4096 to\nlimit output tokens, or `inf` for the maximum available tokens for a\ngiven model. Defaults to `inf`.\n" + }) + ), + "conversation": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Literals(["auto", "none"])], { mode: "oneOf" }).annotate({ + "description": + "Controls which conversation the response is added to. Currently supports\n`auto` and `none`, with `auto` as the default value. The `auto` value\nmeans that the contents of the response will be added to the default\nconversation. Set this to `none` to create an out-of-band response which\nwill not add items to default conversation.\n" + }) + ), + "metadata": Schema.optionalKey(Metadata), + "prompt": Schema.optionalKey(Prompt), + "input": Schema.optionalKey( + Schema.Array(RealtimeConversationItem).annotate({ + "description": + "Input items to include in the prompt for the model. Using this field\ncreates a new context for this Response instead of using the default\nconversation. An empty array `[]` will clear the context for this Response.\nNote that this can include references to items that previously appeared in the session\nusing their id.\n" + }) + ) +}).annotate({ "description": "Create a new Realtime response with these parameters" }) +export type RealtimeServerEventConversationItemAdded = { + readonly "event_id": string + readonly "type": "conversation.item.added" + readonly "previous_item_id"?: string | null + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventConversationItemAdded = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.added").annotate({ + "description": "The event type, must be `conversation.item.added`." + }), + "previous_item_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The ID of the item that precedes this one, if any. This is used to\nmaintain ordering when items are inserted.\n" + }), + Schema.Null + ]) + ), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Sent by the server when an Item is added to the default Conversation. This can happen in several cases:\n- When the client sends a `conversation.item.create` event.\n- When the input audio buffer is committed. In this case the item will be a user message containing the audio from the buffer.\n- When the model is generating a Response. In this case the `conversation.item.added` event will be sent when the model starts generating a specific Item, and thus it will not yet have any content (and `status` will be `in_progress`).\n\nThe event will include the full content of the Item (except when model is generating a Response) except for audio data, which can be retrieved separately with a `conversation.item.retrieve` event if necessary.\n" +}) +export type RealtimeServerEventConversationItemCreated = { + readonly "event_id": string + readonly "type": "conversation.item.created" + readonly "previous_item_id"?: string | null + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventConversationItemCreated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.created").annotate({ + "description": "The event type, must be `conversation.item.created`." + }), + "previous_item_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The ID of the preceding item in the Conversation context, allows the\nclient to understand the order of the conversation. Can be `null` if the\nitem has no predecessor.\n" + }), + Schema.Null + ]) + ), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Returned when a conversation item is created. There are several scenarios that produce this event:\n - The server is generating a Response, which if successful will produce\n either one or two Items, which will be of type `message`\n (role `assistant`) or type `function_call`.\n - The input audio buffer has been committed, either by the client or the\n server (in `server_vad` mode). The server will take the content of the\n input audio buffer and add it to a new user message Item.\n - The client has sent a `conversation.item.create` event to add a new Item\n to the Conversation.\n" +}) +export type RealtimeServerEventConversationItemDone = { + readonly "event_id": string + readonly "type": "conversation.item.done" + readonly "previous_item_id"?: string | null + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventConversationItemDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.done").annotate({ + "description": "The event type, must be `conversation.item.done`." + }), + "previous_item_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The ID of the item that precedes this one, if any. This is used to\nmaintain ordering when items are inserted.\n" + }), + Schema.Null + ]) + ), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Returned when a conversation item is finalized.\n\nThe event will include the full content of the Item except for audio data, which can be retrieved separately with a `conversation.item.retrieve` event if needed.\n" +}) +export type RealtimeServerEventConversationItemRetrieved = { + readonly "event_id": string + readonly "type": "conversation.item.retrieved" + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventConversationItemRetrieved = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("conversation.item.retrieved").annotate({ + "description": "The event type, must be `conversation.item.retrieved`." + }), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Returned when a conversation item is retrieved with `conversation.item.retrieve`. This is provided as a way to fetch the server's representation of an item, for example to get access to the post-processed audio data after noise cancellation and VAD. It includes the full content of the Item, including audio data.\n" +}) +export type RealtimeServerEventResponseOutputItemAdded = { + readonly "event_id": string + readonly "type": "response.output_item.added" + readonly "response_id": string + readonly "output_index": number + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventResponseOutputItemAdded = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_item.added").annotate({ + "description": "The event type, must be `response.output_item.added`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the Response to which the item belongs." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the Response." }).check( + Schema.isInt() + ), + "item": RealtimeConversationItem +}).annotate({ "description": "Returned when a new Item is created during Response generation." }) +export type RealtimeServerEventResponseOutputItemDone = { + readonly "event_id": string + readonly "type": "response.output_item.done" + readonly "response_id": string + readonly "output_index": number + readonly "item": RealtimeConversationItem +} +export const RealtimeServerEventResponseOutputItemDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.output_item.done").annotate({ + "description": "The event type, must be `response.output_item.done`." + }), + "response_id": Schema.String.annotate({ "description": "The ID of the Response to which the item belongs." }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item in the Response." }).check( + Schema.isInt() + ), + "item": RealtimeConversationItem +}).annotate({ + "description": + "Returned when an Item is done streaming. Also emitted when a Response is \ninterrupted, incomplete, or cancelled.\n" +}) +export type AssistantObject = { + readonly "id": string + readonly "object": "assistant" + readonly "created_at": number + readonly "name": string | null + readonly "description": string | null + readonly "model": string + readonly "instructions": string | null + readonly "tools": ReadonlyArray + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { readonly "vector_store_ids"?: ReadonlyArray } + } | null + readonly "metadata": Metadata + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "response_format"?: AssistantsApiResponseFormatOption | null +} +export const AssistantObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("assistant").annotate({ "description": "The object type, which is always `assistant`." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the assistant was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "name": Schema.Union([ + Schema.String.annotate({ "description": "The name of the assistant. The maximum length is 256 characters.\n" }) + .check(Schema.isMaxLength(256)), + Schema.Null + ]), + "description": Schema.Union([ + Schema.String.annotate({ + "description": "The description of the assistant. The maximum length is 512 characters.\n" + }).check(Schema.isMaxLength(512)), + Schema.Null + ]), + "model": Schema.String.annotate({ + "description": + "ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n" + }), + "instructions": Schema.Union([ + Schema.String.annotate({ + "description": "The system instructions that the assistant uses. The maximum length is 256,000 characters.\n" + }).check(Schema.isMaxLength(256000)), + Schema.Null + ]), + "tools": Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`.\n" + }).check(Schema.isMaxLength(128)), + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter`` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Struct({ + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The ID of the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)) + ) + })) + }).annotate({ + "description": + "A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ])), + "metadata": Metadata, + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or temperature but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "response_format": Schema.optionalKey(Schema.Union([AssistantsApiResponseFormatOption, Schema.Null])) +}).annotate({ "title": "Assistant", "description": "Represents an `assistant` that can call the model and use tools." }) +export type CreateAssistantRequest = { + readonly "model": string | AssistantSupportedModels + readonly "name"?: string | null + readonly "description"?: string | null + readonly "instructions"?: string | null + readonly "reasoning_effort"?: ReasoningEffort + readonly "tools"?: ReadonlyArray + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { + readonly "vector_store_ids": ReadonlyArray + readonly "vector_stores"?: ReadonlyArray< + { + readonly "file_ids"?: ReadonlyArray + readonly "chunking_strategy"?: { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": { readonly "max_chunk_size_tokens": number; readonly "chunk_overlap_tokens": number } + } + readonly "metadata"?: Metadata + } + > + } | { + readonly "vector_stores": ReadonlyArray< + { + readonly "file_ids"?: ReadonlyArray + readonly "chunking_strategy"?: { readonly "type": "auto" } | { + readonly "type": "static" + readonly "static": { readonly "max_chunk_size_tokens": number; readonly "chunk_overlap_tokens": number } + } + readonly "metadata"?: Metadata + } + > + readonly "vector_store_ids"?: ReadonlyArray + } + } | null + readonly "metadata"?: Metadata + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "response_format"?: AssistantsApiResponseFormatOption | null +} +export const CreateAssistantRequest = Schema.Struct({ + "model": Schema.Union([Schema.String, AssistantSupportedModels]).annotate({ + "description": + "ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n" + }), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The name of the assistant. The maximum length is 256 characters.\n" }) + .check(Schema.isMaxLength(256)), + Schema.Null + ]) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The description of the assistant. The maximum length is 512 characters.\n" + }).check(Schema.isMaxLength(512)), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The system instructions that the assistant uses. The maximum length is 256,000 characters.\n" + }).check(Schema.isMaxLength(256000)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`.\n" + }).check(Schema.isMaxLength(128)) + ), + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "vector_store_ids": Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)), + "vector_stores": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs to add to the vector store. For vector stores created before Nov 2025, there can be a maximum of 10,000 files in a vector store. For vector stores created starting in Nov 2025, the limit is 100,000,000 files.\n" + }).check(Schema.isMaxLength(100000000)) + ), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }) + .annotate({ + "title": "Auto Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": Schema.Struct({ + "max_chunk_size_tokens": Schema.Number.annotate({ + "description": + "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(100)).check( + Schema.isLessThanOrEqualTo(4096) + ), + "chunk_overlap_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + }).check(Schema.isInt()) + }) + }).annotate({ + "title": "Static Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }) + ], { mode: "oneOf" }) + ), + "metadata": Schema.optionalKey(Metadata) + })).annotate({ + "description": + "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)) + ) + }), + Schema.Struct({ + "vector_stores": Schema.Array(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs to add to the vector store. For vector stores created before Nov 2025, there can be a maximum of 10,000 files in a vector store. For vector stores created starting in Nov 2025, the limit is 100,000,000 files.\n" + }).check(Schema.isMaxLength(100000000)) + ), + "chunking_strategy": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("auto").annotate({ "description": "Always `auto`." }) }) + .annotate({ + "title": "Auto Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }), + Schema.Struct({ + "type": Schema.Literal("static").annotate({ "description": "Always `static`." }), + "static": Schema.Struct({ + "max_chunk_size_tokens": Schema.Number.annotate({ + "description": + "The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(100)).check( + Schema.isLessThanOrEqualTo(4096) + ), + "chunk_overlap_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that overlap between chunks. The default value is `400`.\n\nNote that the overlap must not exceed half of `max_chunk_size_tokens`.\n" + }).check(Schema.isInt()) + }) + }).annotate({ + "title": "Static Chunking Strategy", + "description": + "The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy." + }) + ], { mode: "oneOf" }) + ), + "metadata": Schema.optionalKey(Metadata) + })).annotate({ + "description": + "A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)), + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)) + ) + }) + ], { mode: "oneOf" })) + }).annotate({ + "description": + "A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or temperature but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "response_format": Schema.optionalKey(Schema.Union([AssistantsApiResponseFormatOption, Schema.Null])) +}) +export type CreateRunRequest = { + readonly "assistant_id": string + readonly "model"?: string | AssistantSupportedModels | null + readonly "reasoning_effort"?: ReasoningEffort + readonly "instructions"?: string | null + readonly "additional_instructions"?: string | null + readonly "additional_messages"?: ReadonlyArray< + { + readonly "role": "user" | "assistant" + readonly "content": + | string + | ReadonlyArray + readonly "attachments"?: + | ReadonlyArray< + { + readonly "file_id"?: string + readonly "tools"?: ReadonlyArray + } + > + | null + readonly "metadata"?: Metadata + } + > + readonly "tools"?: ReadonlyArray + readonly "metadata"?: Metadata + readonly "temperature"?: number + readonly "top_p"?: number + readonly "stream"?: boolean | null + readonly "max_prompt_tokens"?: number + readonly "max_completion_tokens"?: number + readonly "truncation_strategy"?: { + readonly "type": "auto" | "last_messages" + readonly "last_messages"?: number | null + } + readonly "tool_choice"?: "none" | "auto" | "required" | { + readonly "type": "function" | "code_interpreter" | "file_search" + readonly "function"?: { readonly "name": string } + } + readonly "parallel_tool_calls"?: ParallelToolCalls + readonly "response_format"?: AssistantsApiResponseFormatOption | null +} +export const CreateRunRequest = Schema.Struct({ + "assistant_id": Schema.String.annotate({ + "description": "The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run." + }), + "model": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.String, AssistantSupportedModels]).annotate({ + "description": + "The ID of the [Model](/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used." + }), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "instructions": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Overrides the [instructions](/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis." + }) + ), + "additional_instructions": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Appends additional instructions at the end of the instructions for the run. This is useful for modifying the behavior on a per-run basis without overriding other instructions." + }) + ), + "additional_messages": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "role": Schema.Literals(["user", "assistant"]).annotate({ + "description": + "The role of the entity that is creating the message. Allowed values include:\n- `user`: Indicates the message is sent by an actual user and should be used in most cases to represent user-generated messages.\n- `assistant`: Indicates the message is generated by the assistant. Use this value to insert messages from the assistant into the conversation.\n" + }), + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text content", "description": "The text contents of the message." }), + Schema.Array( + Schema.Union([MessageContentImageFileObject, MessageContentImageUrlObject, MessageRequestContentTextObject], { + mode: "oneOf" + }) + ).annotate({ + "title": "Array of content parts", + "description": + "An array of content parts with a defined type, each can be of type `text` or images can be passed with `image_url` or `image_file`. Image types are only supported on [Vision-compatible models](/docs/models)." + }).check(Schema.isMinLength(1)) + ], { mode: "oneOf" }), + "attachments": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The ID of the file to attach to the message." }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([AssistantToolsCode, AssistantToolsFileSearchTypeOnly], { mode: "oneOf" })) + .annotate({ "description": "The tools to add this file to." }) + ) + })).annotate({ + "description": "A list of files attached to the message, and the tools they should be added to." + }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata) + })).annotate({ "description": "Adds additional messages to the thread before creating the run." }) + ])), + "tools": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).check( + Schema.isMaxLength(20, { + "description": + "Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis." + }) + ) + ]) + ), + "metadata": Schema.optionalKey(Metadata), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)], { + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n" + }) + ) + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)], { + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or temperature but not both.\n" + }) + ) + ]) + ), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message.\n" + }) + ), + "max_prompt_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": + "The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info.\n" + }) + ) + ]) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": + "The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info.\n" + }) + ) + ]) + ), + "truncation_strategy": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["auto", "last_messages"]).annotate({ + "description": + "The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`." + }), + "last_messages": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The number of most recent messages from the thread when constructing the context for the run." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ) + }).annotate({ + "title": "Thread Truncation Controls", + "description": + "Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run." + }) + ])), + "tool_choice": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Literal("none"), Schema.Literal("auto"), Schema.Literal("required")]).annotate({ + "description": + "`none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user.\n" + }), + Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["function", "code_interpreter", "file_search"]).annotate({ + "description": "The type of the tool. If type is `function`, the function name must be set" + }), + "function": Schema.optionalKey( + Schema.Struct({ "name": Schema.String.annotate({ "description": "The name of the function to call." }) }) + ) + }).annotate({ + "description": "Specifies a tool the model should use. Use to force the model to call a specific tool." + }) + ]) + ], { mode: "oneOf" }).annotate({ + "description": + "Controls which (if any) tool is called by the model.\n`none` means the model will not call any tools and instead generates a message.\n`auto` is the default value and means the model can pick between generating a message or calling one or more tools.\n`required` means the model must call one or more tools before responding to the user.\nSpecifying a particular tool like `{\"type\": \"file_search\"}` or `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey(ParallelToolCalls), + "response_format": Schema.optionalKey(Schema.Union([AssistantsApiResponseFormatOption, Schema.Null])) +}) +export type CreateThreadAndRunRequest = { + readonly "assistant_id": string + readonly "thread"?: CreateThreadRequest + readonly "model"?: + | string + | "gpt-5" + | "gpt-5-mini" + | "gpt-5-nano" + | "gpt-5-2025-08-07" + | "gpt-5-mini-2025-08-07" + | "gpt-5-nano-2025-08-07" + | "gpt-4.1" + | "gpt-4.1-mini" + | "gpt-4.1-nano" + | "gpt-4.1-2025-04-14" + | "gpt-4.1-mini-2025-04-14" + | "gpt-4.1-nano-2025-04-14" + | "gpt-4o" + | "gpt-4o-2024-11-20" + | "gpt-4o-2024-08-06" + | "gpt-4o-2024-05-13" + | "gpt-4o-mini" + | "gpt-4o-mini-2024-07-18" + | "gpt-4.5-preview" + | "gpt-4.5-preview-2025-02-27" + | "gpt-4-turbo" + | "gpt-4-turbo-2024-04-09" + | "gpt-4-0125-preview" + | "gpt-4-turbo-preview" + | "gpt-4-1106-preview" + | "gpt-4-vision-preview" + | "gpt-4" + | "gpt-4-0314" + | "gpt-4-0613" + | "gpt-4-32k" + | "gpt-4-32k-0314" + | "gpt-4-32k-0613" + | "gpt-3.5-turbo" + | "gpt-3.5-turbo-16k" + | "gpt-3.5-turbo-0613" + | "gpt-3.5-turbo-1106" + | "gpt-3.5-turbo-0125" + | "gpt-3.5-turbo-16k-0613" + | null + readonly "instructions"?: string | null + readonly "tools"?: ReadonlyArray + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { readonly "vector_store_ids"?: ReadonlyArray } + } + readonly "metadata"?: Metadata + readonly "temperature"?: number + readonly "top_p"?: number + readonly "stream"?: boolean | null + readonly "max_prompt_tokens"?: number + readonly "max_completion_tokens"?: number + readonly "truncation_strategy"?: { + readonly "type": "auto" | "last_messages" + readonly "last_messages"?: number | null + } + readonly "tool_choice"?: "none" | "auto" | "required" | { + readonly "type": "function" | "code_interpreter" | "file_search" + readonly "function"?: { readonly "name": string } + } + readonly "parallel_tool_calls"?: ParallelToolCalls + readonly "response_format"?: AssistantsApiResponseFormatOption | null +} +export const CreateThreadAndRunRequest = Schema.Struct({ + "assistant_id": Schema.String.annotate({ + "description": "The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run." + }), + "thread": Schema.optionalKey(CreateThreadRequest), + "model": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-2025-08-07", + "gpt-5-mini-2025-08-07", + "gpt-5-nano-2025-08-07", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano-2025-04-14", + "gpt-4o", + "gpt-4o-2024-11-20", + "gpt-4o-2024-08-06", + "gpt-4o-2024-05-13", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4.5-preview", + "gpt-4.5-preview-2025-02-27", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613" + ]) + ]).annotate({ + "description": + "The ID of the [Model](/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used." + }), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Override the default system message of the assistant. This is useful for modifying the behavior on a per-run basis." + }) + ), + "tools": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).check( + Schema.isMaxLength(20, { + "description": + "Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis." + }) + ) + ]) + ), + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Struct({ + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "The ID of the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)) + ) + })) + }).annotate({ + "description": + "A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }) + ])), + "metadata": Schema.optionalKey(Metadata), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)], { + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n" + }) + ) + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)], { + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or temperature but not both.\n" + }) + ) + ]) + ), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message.\n" + }) + ), + "max_prompt_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": + "The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info.\n" + }) + ) + ]) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": + "The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info.\n" + }) + ) + ]) + ), + "truncation_strategy": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["auto", "last_messages"]).annotate({ + "description": + "The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`." + }), + "last_messages": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The number of most recent messages from the thread when constructing the context for the run." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ) + }).annotate({ + "title": "Thread Truncation Controls", + "description": + "Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run." + }) + ])), + "tool_choice": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Literal("none"), Schema.Literal("auto"), Schema.Literal("required")]).annotate({ + "description": + "`none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user.\n" + }), + Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["function", "code_interpreter", "file_search"]).annotate({ + "description": "The type of the tool. If type is `function`, the function name must be set" + }), + "function": Schema.optionalKey( + Schema.Struct({ "name": Schema.String.annotate({ "description": "The name of the function to call." }) }) + ) + }).annotate({ + "description": "Specifies a tool the model should use. Use to force the model to call a specific tool." + }) + ]) + ], { mode: "oneOf" }).annotate({ + "description": + "Controls which (if any) tool is called by the model.\n`none` means the model will not call any tools and instead generates a message.\n`auto` is the default value and means the model can pick between generating a message or calling one or more tools.\n`required` means the model must call one or more tools before responding to the user.\nSpecifying a particular tool like `{\"type\": \"file_search\"}` or `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that tool.\n" + }) + ), + "parallel_tool_calls": Schema.optionalKey(ParallelToolCalls), + "response_format": Schema.optionalKey(Schema.Union([AssistantsApiResponseFormatOption, Schema.Null])) +}) +export type ModifyAssistantRequest = { + readonly "model"?: string | AssistantSupportedModels + readonly "reasoning_effort"?: ReasoningEffort + readonly "name"?: string | null + readonly "description"?: string | null + readonly "instructions"?: string | null + readonly "tools"?: ReadonlyArray + readonly "tool_resources"?: { + readonly "code_interpreter"?: { readonly "file_ids"?: ReadonlyArray } + readonly "file_search"?: { readonly "vector_store_ids"?: ReadonlyArray } + } | null + readonly "metadata"?: Metadata + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "response_format"?: AssistantsApiResponseFormatOption | null +} +export const ModifyAssistantRequest = Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([Schema.String, AssistantSupportedModels]).annotate({ + "description": + "ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n" + }) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The name of the assistant. The maximum length is 256 characters.\n" }) + .check(Schema.isMaxLength(256)), + Schema.Null + ]) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The description of the assistant. The maximum length is 512 characters.\n" + }).check(Schema.isMaxLength(512)), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The system instructions that the assistant uses. The maximum length is 256,000 characters.\n" + }).check(Schema.isMaxLength(256000)), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`.\n" + }).check(Schema.isMaxLength(128)) + ), + "tool_resources": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "code_interpreter": Schema.optionalKey(Schema.Struct({ + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Overrides the list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool.\n" + }).check(Schema.isMaxLength(20)) + ) + })), + "file_search": Schema.optionalKey(Schema.Struct({ + "vector_store_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Overrides the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant.\n" + }).check(Schema.isMaxLength(1)) + ) + })) + }).annotate({ + "description": + "A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs.\n" + }), + Schema.Null + ])), + "metadata": Schema.optionalKey(Metadata), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or temperature but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "response_format": Schema.optionalKey(Schema.Union([AssistantsApiResponseFormatOption, Schema.Null])) +}) +export type RunObject = { + readonly "id": string + readonly "object": "thread.run" + readonly "created_at": number + readonly "thread_id": string + readonly "assistant_id": string + readonly "status": + | "queued" + | "in_progress" + | "requires_action" + | "cancelling" + | "cancelled" + | "failed" + | "completed" + | "incomplete" + | "expired" + readonly "required_action": { + readonly "type": "submit_tool_outputs" + readonly "submit_tool_outputs": { readonly "tool_calls": ReadonlyArray } + } + readonly "last_error": { + readonly "code": "server_error" | "rate_limit_exceeded" | "invalid_prompt" + readonly "message": string + } + readonly "expires_at": never + readonly "started_at": never + readonly "cancelled_at": never + readonly "failed_at": never + readonly "completed_at": never + readonly "incomplete_details": { readonly "reason"?: "max_completion_tokens" | "max_prompt_tokens" } + readonly "model": string + readonly "instructions": string + readonly "tools": ReadonlyArray + readonly "metadata": Metadata + readonly "usage": RunCompletionUsage + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "max_prompt_tokens": number + readonly "max_completion_tokens": number + readonly "truncation_strategy": { + readonly "type": "auto" | "last_messages" + readonly "last_messages"?: number | null + } + readonly "tool_choice": "none" | "auto" | "required" | { + readonly "type": "function" | "code_interpreter" | "file_search" + readonly "function"?: { readonly "name": string } + } + readonly "parallel_tool_calls": ParallelToolCalls + readonly "response_format": AssistantsApiResponseFormatOption | null +} +export const RunObject = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The identifier, which can be referenced in API endpoints." }), + "object": Schema.Literal("thread.run").annotate({ "description": "The object type, which is always `thread.run`." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the run was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "thread_id": Schema.String.annotate({ + "description": "The ID of the [thread](/docs/api-reference/threads) that was executed on as a part of this run." + }), + "assistant_id": Schema.String.annotate({ + "description": "The ID of the [assistant](/docs/api-reference/assistants) used for execution of this run." + }), + "status": Schema.Literals([ + "queued", + "in_progress", + "requires_action", + "cancelling", + "cancelled", + "failed", + "completed", + "incomplete", + "expired" + ]).annotate({ + "description": + "The status of the run, which can be either `queued`, `in_progress`, `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, `incomplete`, or `expired`." + }), + "required_action": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("submit_tool_outputs").annotate({ + "description": "For now, this is always `submit_tool_outputs`." + }), + "submit_tool_outputs": Schema.Struct({ + "tool_calls": Schema.Array(RunToolCallObject).annotate({ "description": "A list of the relevant tool calls." }) + }).annotate({ "description": "Details on the tool outputs needed for this run to continue." }) + }).annotate({ + "description": "Details on the action required to continue the run. Will be `null` if no action is required." + }) + ]), + "last_error": Schema.Union([ + Schema.Struct({ + "code": Schema.Literals(["server_error", "rate_limit_exceeded", "invalid_prompt"]).annotate({ + "description": "One of `server_error`, `rate_limit_exceeded`, or `invalid_prompt`." + }), + "message": Schema.String.annotate({ "description": "A human-readable description of the error." }) + }).annotate({ "description": "The last error associated with this run. Will be `null` if there are no errors." }) + ]), + "expires_at": Schema.Never, + "started_at": Schema.Never, + "cancelled_at": Schema.Never, + "failed_at": Schema.Never, + "completed_at": Schema.Never, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_completion_tokens", "max_prompt_tokens"]).annotate({ + "description": + "The reason why the run is incomplete. This will point to which specific token limit was reached over the course of the run." + }) + ) + }).annotate({ "description": "Details on why the run is incomplete. Will be `null` if the run is not incomplete." }) + ]), + "model": Schema.String.annotate({ + "description": "The model that the [assistant](/docs/api-reference/assistants) used for this run." + }), + "instructions": Schema.String.annotate({ + "description": "The instructions that the [assistant](/docs/api-reference/assistants) used for this run." + }), + "tools": Schema.Array( + Schema.Union([AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction], { mode: "oneOf" }) + ).annotate({ + "description": "The list of tools that the [assistant](/docs/api-reference/assistants) used for this run." + }).check(Schema.isMaxLength(20)), + "metadata": Metadata, + "usage": RunCompletionUsage, + "temperature": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]).annotate({ + "description": "The sampling temperature used for this run. If not set, defaults to 1." + }) + ), + "top_p": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]).annotate({ + "description": "The nucleus sampling value used for this run. If not set, defaults to 1." + }) + ), + "max_prompt_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": "The maximum number of prompt tokens specified to have been used over the course of the run.\n" + }) + ) + ]), + "max_completion_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(256)], { + "description": + "The maximum number of completion tokens specified to have been used over the course of the run.\n" + }) + ) + ]), + "truncation_strategy": Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["auto", "last_messages"]).annotate({ + "description": + "The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`." + }), + "last_messages": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The number of most recent messages from the thread when constructing the context for the run." + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ) + }).annotate({ + "title": "Thread Truncation Controls", + "description": + "Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run." + }) + ]), + "tool_choice": Schema.Union([ + Schema.Union([Schema.Literal("none"), Schema.Literal("auto"), Schema.Literal("required")]).annotate({ + "description": + "`none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user.\n" + }), + Schema.Union([ + Schema.Struct({ + "type": Schema.Literals(["function", "code_interpreter", "file_search"]).annotate({ + "description": "The type of the tool. If type is `function`, the function name must be set" + }), + "function": Schema.optionalKey( + Schema.Struct({ "name": Schema.String.annotate({ "description": "The name of the function to call." }) }) + ) + }).annotate({ + "description": "Specifies a tool the model should use. Use to force the model to call a specific tool." + }) + ]) + ], { mode: "oneOf" }).annotate({ + "description": + "Controls which (if any) tool is called by the model.\n`none` means the model will not call any tools and instead generates a message.\n`auto` is the default value and means the model can pick between generating a message or calling one or more tools.\n`required` means the model must call one or more tools before responding to the user.\nSpecifying a particular tool like `{\"type\": \"file_search\"}` or `{\"type\": \"function\", \"function\": {\"name\": \"my_function\"}}` forces the model to call that tool.\n" + }), + "parallel_tool_calls": ParallelToolCalls, + "response_format": Schema.Union([AssistantsApiResponseFormatOption, Schema.Null]) +}).annotate({ + "title": "A run on a thread", + "description": "Represents an execution run on a [thread](/docs/api-reference/threads)." +}) +export type ResponseTextParam = { + readonly "format"?: TextResponseFormatConfiguration + readonly "verbosity"?: Verbosity +} +export const ResponseTextParam = Schema.Struct({ + "format": Schema.optionalKey(TextResponseFormatConfiguration), + "verbosity": Schema.optionalKey(Verbosity) +}).annotate({ + "description": + "Configuration options for a text response from the model. Can be plain\ntext or structured JSON data. Learn more:\n- [Text inputs and outputs](/docs/guides/text)\n- [Structured Outputs](/docs/guides/structured-outputs)\n" +}) +export type RealtimeCreateClientSecretRequest = { + readonly "expires_after"?: { readonly "anchor"?: "created_at"; readonly "seconds"?: number } + readonly "session"?: RealtimeSessionCreateRequestGA | RealtimeTranscriptionSessionCreateRequestGA +} +export const RealtimeCreateClientSecretRequest = Schema.Struct({ + "expires_after": Schema.optionalKey( + Schema.Struct({ + "anchor": Schema.optionalKey( + Schema.Literal("created_at").annotate({ + "description": + "The anchor point for the client secret expiration, meaning that `seconds` will be added to the `created_at` time of the client secret to produce an expiration timestamp. Only `created_at` is currently supported.\n" + }) + ), + "seconds": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "The number of seconds from the anchor point to the expiration. Select a value between `10` and `7200` (2 hours). This default to 600 seconds (10 minutes) if not specified.\n", + "format": "int64" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(10)).check(Schema.isLessThanOrEqualTo(7200)) + ) + }).annotate({ + "title": "Client secret expiration", + "description": + "Configuration for the client secret expiration. Expiration refers to the time after which\na client secret will no longer be valid for creating sessions. The session itself may\ncontinue after that time once started. A secret can be used to create multiple sessions\nuntil it expires.\n" + }) + ), + "session": Schema.optionalKey( + Schema.Union([RealtimeSessionCreateRequestGA, RealtimeTranscriptionSessionCreateRequestGA], { mode: "oneOf" }) + .annotate({ + "title": "Session configuration", + "description": + "Session configuration to use for the client secret. Choose either a realtime\nsession or a transcription session.\n" + }) + ) +}).annotate({ + "title": "Realtime client secret creation request", + "description": + "Create a session and client secret for the Realtime API. The request can specify\neither a realtime or a transcription session configuration.\n[Learn more about the Realtime API](/docs/guides/realtime).\n" +}) +export type RealtimeCreateClientSecretResponse = { + readonly "value": string + readonly "expires_at": number + readonly "session": RealtimeSessionCreateResponseGA | RealtimeTranscriptionSessionCreateResponseGA +} +export const RealtimeCreateClientSecretResponse = Schema.Struct({ + "value": Schema.String.annotate({ "description": "The generated client secret value." }), + "expires_at": Schema.Number.annotate({ + "description": "Expiration timestamp for the client secret, in seconds since epoch.", + "format": "unixtime" + }).check(Schema.isInt()), + "session": Schema.Union([RealtimeSessionCreateResponseGA, RealtimeTranscriptionSessionCreateResponseGA], { + mode: "oneOf" + }).annotate({ + "title": "Session configuration", + "description": "The session configuration for either a realtime or transcription session.\n" + }) +}).annotate({ + "title": "Realtime session and client secret", + "description": "Response from creating a session and client secret for the Realtime API.\n" +}) +export type RealtimeServerEventSessionCreated = { + readonly "event_id": string + readonly "type": "session.created" + readonly "session": RealtimeSessionCreateResponseGA | RealtimeTranscriptionSessionCreateResponseGA +} +export const RealtimeServerEventSessionCreated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.created").annotate({ "description": "The event type, must be `session.created`." }), + "session": Schema.Union([RealtimeSessionCreateResponseGA, RealtimeTranscriptionSessionCreateResponseGA], { + mode: "oneOf" + }).annotate({ "description": "The session configuration." }) +}).annotate({ + "description": + "Returned when a Session is created. Emitted automatically when a new\nconnection is established as the first server event. This event will contain\nthe default Session configuration.\n" +}) +export type RealtimeServerEventSessionUpdated = { + readonly "event_id": string + readonly "type": "session.updated" + readonly "session": RealtimeSessionCreateResponseGA | RealtimeTranscriptionSessionCreateResponseGA +} +export const RealtimeServerEventSessionUpdated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("session.updated").annotate({ "description": "The event type, must be `session.updated`." }), + "session": Schema.Union([RealtimeSessionCreateResponseGA, RealtimeTranscriptionSessionCreateResponseGA], { + mode: "oneOf" + }).annotate({ "description": "The session configuration." }) +}).annotate({ + "description": "Returned when a session is updated with a `session.update` event, unless\nthere is an error.\n" +}) +export type CreateVectorStoreFileBatchRequest = { + readonly "file_ids": ReadonlyArray + readonly "files"?: ReadonlyArray + readonly "chunking_strategy"?: ChunkingStrategyRequestParam + readonly "attributes"?: VectorStoreFileAttributes +} | { + readonly "files": ReadonlyArray + readonly "file_ids"?: ReadonlyArray + readonly "chunking_strategy"?: ChunkingStrategyRequestParam + readonly "attributes"?: VectorStoreFileAttributes +} +export const CreateVectorStoreFileBatchRequest = Schema.Union([ + Schema.Struct({ + "file_ids": Schema.Array(Schema.String).annotate({ + "description": + "A list of [File](/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. If `attributes` or `chunking_strategy` are provided, they will be applied to all files in the batch. The maximum batch size is 2000 files. This endpoint is recommended for multi-file ingestion and helps reduce per-vector-store write request pressure. Mutually exclusive with `files`." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2000)), + "files": Schema.optionalKey( + Schema.Array(CreateVectorStoreFileRequest).annotate({ + "description": + "A list of objects that each include a `file_id` plus optional `attributes` or `chunking_strategy`. Use this when you need to override metadata for specific files. The global `attributes` or `chunking_strategy` will be ignored and must be specified for each file. The maximum batch size is 2000 files. This endpoint is recommended for multi-file ingestion and helps reduce per-vector-store write request pressure. Mutually exclusive with `file_ids`." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2000)) + ), + "chunking_strategy": Schema.optionalKey(ChunkingStrategyRequestParam), + "attributes": Schema.optionalKey(VectorStoreFileAttributes) + }), + Schema.Struct({ + "files": Schema.Array(CreateVectorStoreFileRequest).annotate({ + "description": + "A list of objects that each include a `file_id` plus optional `attributes` or `chunking_strategy`. Use this when you need to override metadata for specific files. The global `attributes` or `chunking_strategy` will be ignored and must be specified for each file. The maximum batch size is 2000 files. This endpoint is recommended for multi-file ingestion and helps reduce per-vector-store write request pressure. Mutually exclusive with `file_ids`." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2000)), + "file_ids": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of [File](/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. If `attributes` or `chunking_strategy` are provided, they will be applied to all files in the batch. The maximum batch size is 2000 files. This endpoint is recommended for multi-file ingestion and helps reduce per-vector-store write request pressure. Mutually exclusive with `files`." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(2000)) + ), + "chunking_strategy": Schema.optionalKey(ChunkingStrategyRequestParam), + "attributes": Schema.optionalKey(VectorStoreFileAttributes) + }) +]) +export type CodeInterpreterTool = { + readonly "type": "code_interpreter" + readonly "container": string | AutoCodeInterpreterToolParam +} +export const CodeInterpreterTool = Schema.Struct({ + "type": Schema.Literal("code_interpreter").annotate({ + "description": "The type of the code interpreter tool. Always `code_interpreter`.\n" + }), + "container": Schema.Union([ + Schema.String.annotate({ "description": "The container ID." }), + AutoCodeInterpreterToolParam + ], { mode: "oneOf" }).annotate({ + "description": + "The code interpreter container. Can be a container ID or an object that\nspecifies uploaded file IDs to make available to your code, along with an\noptional `memory_limit` setting.\n" + }) +}).annotate({ + "title": "Code interpreter", + "description": "A tool that runs Python code to help generate a response to a prompt.\n" +}) +export type FunctionShellToolParam = { + readonly "type": "shell" + readonly "environment"?: ContainerAutoParam | LocalEnvironmentParam | ContainerReferenceParam | null +} +export const FunctionShellToolParam = Schema.Struct({ + "type": Schema.Literal("shell").annotate({ "description": "The type of the shell tool. Always `shell`." }), + "environment": Schema.optionalKey( + Schema.Union([ + Schema.Union([ContainerAutoParam, LocalEnvironmentParam, ContainerReferenceParam], { mode: "oneOf" }), + Schema.Null + ]) + ) +}).annotate({ "title": "Shell tool", "description": "A tool that allows the model to execute shell commands." }) +export type EvalItemContent = EvalItemContentItem | EvalItemContentArray +export const EvalItemContent = Schema.Union([EvalItemContentItem, EvalItemContentArray], { mode: "oneOf" }).annotate({ + "title": "Eval content", + "description": + "Inputs to the model - can contain template strings. Supports text, output text, input images, and input audio, either as a single item or an array of items.\n" +}) +export type OutputMessageContent = OutputTextContent | RefusalContent +export const OutputMessageContent = Schema.Union([OutputTextContent, RefusalContent], { mode: "oneOf" }) +export type ResponseContentPartAddedEvent = { + readonly "type": "response.content_part.added" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "part": OutputTextContent | RefusalContent | ReasoningTextContent + readonly "sequence_number": number +} +export const ResponseContentPartAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.content_part.added").annotate({ + "description": "The type of the event. Always `response.content_part.added`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the content part was added to.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the content part was added to.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ "description": "The index of the content part that was added.\n" }).check( + Schema.isInt() + ), + "part": Schema.Union([OutputTextContent, RefusalContent, ReasoningTextContent], { mode: "oneOf" }).annotate({ + "description": "The content part that was added.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when a new content part is added." }) +export type ResponseContentPartDoneEvent = { + readonly "type": "response.content_part.done" + readonly "item_id": string + readonly "output_index": number + readonly "content_index": number + readonly "sequence_number": number + readonly "part": OutputTextContent | RefusalContent | ReasoningTextContent +} +export const ResponseContentPartDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.content_part.done").annotate({ + "description": "The type of the event. Always `response.content_part.done`.\n" + }), + "item_id": Schema.String.annotate({ + "description": "The ID of the output item that the content part was added to.\n" + }), + "output_index": Schema.Number.annotate({ + "description": "The index of the output item that the content part was added to.\n" + }).check(Schema.isInt()), + "content_index": Schema.Number.annotate({ "description": "The index of the content part that is done.\n" }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "part": Schema.Union([OutputTextContent, RefusalContent, ReasoningTextContent], { mode: "oneOf" }).annotate({ + "description": "The content part that is done.\n" + }) +}).annotate({ "description": "Emitted when a content part is done." }) +export type Message = { + readonly "type": "message" + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "role": "unknown" | "user" | "assistant" | "system" | "critic" | "discriminator" | "developer" | "tool" + readonly "content": ReadonlyArray< + | InputTextContent + | OutputTextContent + | TextContent + | SummaryTextContent + | ReasoningTextContent + | RefusalContent + | InputImageContent + | ComputerScreenshotContent + | InputFileContent + > + readonly "phase"?: "commentary" | "final_answer" | null +} +export const Message = Schema.Struct({ + "type": Schema.Literal("message").annotate({ "description": "The type of the message. Always set to `message`." }), + "id": Schema.String.annotate({ "description": "The unique ID of the message." }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of item. One of `in_progress`, `completed`, or `incomplete`. Populated when items are returned via API." + }), + "role": Schema.Literals(["unknown", "user", "assistant", "system", "critic", "discriminator", "developer", "tool"]) + .annotate({ + "description": + "The role of the message. One of `unknown`, `user`, `assistant`, `system`, `critic`, `discriminator`, `developer`, or `tool`." + }), + "content": Schema.Array( + Schema.Union([ + InputTextContent, + OutputTextContent, + TextContent, + SummaryTextContent, + ReasoningTextContent, + RefusalContent, + InputImageContent, + ComputerScreenshotContent, + InputFileContent + ], { mode: "oneOf" }).annotate({ "description": "A content part that makes up an input or output item." }) + ).annotate({ "description": "The content of the message" }), + "phase": Schema.optionalKey(Schema.Union([ + Schema.Literals(["commentary", "final_answer"]).annotate({ + "description": + "Labels an `assistant` message as intermediate commentary (`commentary`) or the final answer (`final_answer`). For models like `gpt-5.3-codex` and beyond, when sending follow-up requests, preserve and resend phase on all assistant messages — dropping it can degrade performance. Not used for user messages." + }), + Schema.Null + ])) +}).annotate({ "title": "Message", "description": "A message to or from the model." }) +export type EasyInputMessage = { + readonly "role": "user" | "assistant" | "system" | "developer" + readonly "content": string | InputMessageContentList + readonly "phase"?: MessagePhase | null + readonly "type"?: "message" +} +export const EasyInputMessage = Schema.Struct({ + "role": Schema.Literals(["user", "assistant", "system", "developer"]).annotate({ + "description": "The role of the message input. One of `user`, `assistant`, `system`, or\n`developer`.\n" + }), + "content": Schema.Union([ + Schema.String.annotate({ "title": "Text input", "description": "A text input to the model.\n" }), + InputMessageContentList + ], { mode: "oneOf" }).annotate({ + "description": + "Text, image, or audio input to the model, used to generate a response.\nCan also contain previous assistant responses.\n" + }), + "phase": Schema.optionalKey(Schema.Union([MessagePhase, Schema.Null])), + "type": Schema.optionalKey( + Schema.Literal("message").annotate({ "description": "The type of the message input. Always `message`.\n" }) + ) +}).annotate({ + "title": "Input message", + "description": + "A message input to the model with a role indicating instruction following\nhierarchy. Instructions given with the `developer` or `system` role take\nprecedence over instructions given with the `user` role. Messages with the\n`assistant` role are presumed to have been generated by the model in previous\ninteractions.\n" +}) +export type InputMessageResource = { + readonly "type": "message" + readonly "role": "user" | "system" | "developer" + readonly "status"?: "in_progress" | "completed" | "incomplete" + readonly "content": InputMessageContentList + readonly "id": string +} +export const InputMessageResource = Schema.Struct({ + "type": Schema.Literal("message").annotate({ + "description": "The type of the message input. Always set to `message`.\n" + }), + "role": Schema.Literals(["user", "system", "developer"]).annotate({ + "description": "The role of the message input. One of `user`, `system`, or `developer`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ), + "content": InputMessageContentList, + "id": Schema.String.annotate({ "description": "The unique ID of the message input.\n" }) +}).annotate({ + "title": "Input message", + "description": + "A message input to the model with a role indicating instruction following\nhierarchy. Instructions given with the `developer` or `system` role take\nprecedence over instructions given with the `user` role.\n" +}) +export type ComputerActionList = ReadonlyArray +export const ComputerActionList = Schema.Array(ComputerAction).annotate({ + "title": "Computer Action List", + "description": + "Flattened batched actions for `computer_use`. Each action includes an\n`type` discriminator and action-specific fields.\n" +}) +export type ThreadItem = + | UserMessageItem + | AssistantMessageItem + | WidgetMessageItem + | ClientToolCallItem + | TaskItem + | TaskGroupItem +export const ThreadItem = Schema.Union([ + UserMessageItem, + AssistantMessageItem, + WidgetMessageItem, + ClientToolCallItem, + TaskItem, + TaskGroupItem +], { mode: "oneOf" }).annotate({ "title": "The thread item" }) +export type ListAuditLogsResponse = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id"?: string | null + readonly "last_id"?: string | null + readonly "has_more": boolean +} +export const ListAuditLogsResponse = Schema.Struct({ + "object": Schema.Literal("list"), + "data": Schema.Array(AuditLog), + "first_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "last_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "has_more": Schema.Boolean +}) +export type ChatCompletionList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ChatCompletionList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ + "description": "The type of this object. It is always set to \"list\".\n" + }), + "data": Schema.Array(CreateChatCompletionResponse).annotate({ + "description": "An array of chat completion objects.\n" + }), + "first_id": Schema.String.annotate({ + "description": "The identifier of the first chat completion in the data array." + }), + "last_id": Schema.String.annotate({ "description": "The identifier of the last chat completion in the data array." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates whether there are more Chat Completions available." }) +}).annotate({ "title": "ChatCompletionList", "description": "An object representing a list of Chat Completions.\n" }) +export type CreateChatCompletionRequest = { + readonly "metadata"?: Metadata + readonly "top_logprobs"?: number + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "messages": ReadonlyArray + readonly "model": + | string + | "gpt-5.4" + | "gpt-5.4-mini" + | "gpt-5.4-nano" + | "gpt-5.4-mini-2026-03-17" + | "gpt-5.4-nano-2026-03-17" + | "gpt-5.3-chat-latest" + | "gpt-5.2" + | "gpt-5.2-2025-12-11" + | "gpt-5.2-chat-latest" + | "gpt-5.2-pro" + | "gpt-5.2-pro-2025-12-11" + | "gpt-5.1" + | "gpt-5.1-2025-11-13" + | "gpt-5.1-codex" + | "gpt-5.1-mini" + | "gpt-5.1-chat-latest" + | "gpt-5" + | "gpt-5-mini" + | "gpt-5-nano" + | "gpt-5-2025-08-07" + | "gpt-5-mini-2025-08-07" + | "gpt-5-nano-2025-08-07" + | "gpt-5-chat-latest" + | "gpt-4.1" + | "gpt-4.1-mini" + | "gpt-4.1-nano" + | "gpt-4.1-2025-04-14" + | "gpt-4.1-mini-2025-04-14" + | "gpt-4.1-nano-2025-04-14" + | "o4-mini" + | "o4-mini-2025-04-16" + | "o3" + | "o3-2025-04-16" + | "o3-mini" + | "o3-mini-2025-01-31" + | "o1" + | "o1-2024-12-17" + | "o1-preview" + | "o1-preview-2024-09-12" + | "o1-mini" + | "o1-mini-2024-09-12" + | "gpt-4o" + | "gpt-4o-2024-11-20" + | "gpt-4o-2024-08-06" + | "gpt-4o-2024-05-13" + | "gpt-4o-audio-preview" + | "gpt-4o-audio-preview-2024-10-01" + | "gpt-4o-audio-preview-2024-12-17" + | "gpt-4o-audio-preview-2025-06-03" + | "gpt-4o-mini-audio-preview" + | "gpt-4o-mini-audio-preview-2024-12-17" + | "gpt-4o-search-preview" + | "gpt-4o-mini-search-preview" + | "gpt-4o-search-preview-2025-03-11" + | "gpt-4o-mini-search-preview-2025-03-11" + | "chatgpt-4o-latest" + | "codex-mini-latest" + | "gpt-4o-mini" + | "gpt-4o-mini-2024-07-18" + | "gpt-4-turbo" + | "gpt-4-turbo-2024-04-09" + | "gpt-4-0125-preview" + | "gpt-4-turbo-preview" + | "gpt-4-1106-preview" + | "gpt-4-vision-preview" + | "gpt-4" + | "gpt-4-0314" + | "gpt-4-0613" + | "gpt-4-32k" + | "gpt-4-32k-0314" + | "gpt-4-32k-0613" + | "gpt-3.5-turbo" + | "gpt-3.5-turbo-16k" + | "gpt-3.5-turbo-0301" + | "gpt-3.5-turbo-0613" + | "gpt-3.5-turbo-1106" + | "gpt-3.5-turbo-0125" + | "gpt-3.5-turbo-16k-0613" + readonly "modalities"?: ResponseModalities + readonly "verbosity"?: Verbosity + readonly "reasoning_effort"?: ReasoningEffort + readonly "max_completion_tokens"?: number | null + readonly "frequency_penalty"?: number + readonly "presence_penalty"?: number + readonly "web_search_options"?: { + readonly "user_location"?: { readonly "type": "approximate"; readonly "approximate": WebSearchLocation } + readonly "search_context_size"?: WebSearchContextSize + } + readonly "response_format"?: ResponseFormatText | ResponseFormatJsonSchema | ResponseFormatJsonObject + readonly "audio"?: { + readonly "voice": VoiceIdsShared | { readonly "id": string } + readonly "format": "wav" | "aac" | "mp3" | "flac" | "opus" | "pcm16" + } + readonly "store"?: boolean | null + readonly "stream"?: boolean | null + readonly "stop"?: StopConfiguration + readonly "logit_bias"?: {} + readonly "logprobs"?: boolean | null + readonly "max_tokens"?: number | null + readonly "n"?: number + readonly "prediction"?: PredictionContent | null + readonly "seed"?: number + readonly "stream_options"?: ChatCompletionStreamOptions + readonly "tools"?: ReadonlyArray + readonly "tool_choice"?: ChatCompletionToolChoiceOption + readonly "parallel_tool_calls"?: ParallelToolCalls + readonly "function_call"?: "none" | "auto" | ChatCompletionFunctionCallOption + readonly "functions"?: ReadonlyArray +} +export const CreateChatCompletionRequest = Schema.Struct({ + "metadata": Schema.optionalKey(Metadata), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup( + [Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(20)], + { + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n`logprobs` must be set to `true` if this parameter is used.\n" + } + ) + ).check( + Schema.makeFilterGroup([ + Schema.isGreaterThanOrEqualTo(0), + Schema.isLessThanOrEqualTo(20), + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(20)], { + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }) + ], { + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }) + ) + ]) + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "messages": Schema.Array(ChatCompletionRequestMessage).annotate({ + "description": + "A list of messages comprising the conversation so far. Depending on the\n[model](/docs/models) you use, different message types (modalities) are\nsupported, like [text](/docs/guides/text-generation),\n[images](/docs/guides/vision), and [audio](/docs/guides/audio).\n" + }).check(Schema.isMinLength(1)), + "model": Schema.Union([ + Schema.String, + Schema.Literals([ + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5.4-nano", + "gpt-5.4-mini-2026-03-17", + "gpt-5.4-nano-2026-03-17", + "gpt-5.3-chat-latest", + "gpt-5.2", + "gpt-5.2-2025-12-11", + "gpt-5.2-chat-latest", + "gpt-5.2-pro", + "gpt-5.2-pro-2025-12-11", + "gpt-5.1", + "gpt-5.1-2025-11-13", + "gpt-5.1-codex", + "gpt-5.1-mini", + "gpt-5.1-chat-latest", + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-2025-08-07", + "gpt-5-mini-2025-08-07", + "gpt-5-nano-2025-08-07", + "gpt-5-chat-latest", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano-2025-04-14", + "o4-mini", + "o4-mini-2025-04-16", + "o3", + "o3-2025-04-16", + "o3-mini", + "o3-mini-2025-01-31", + "o1", + "o1-2024-12-17", + "o1-preview", + "o1-preview-2024-09-12", + "o1-mini", + "o1-mini-2024-09-12", + "gpt-4o", + "gpt-4o-2024-11-20", + "gpt-4o-2024-08-06", + "gpt-4o-2024-05-13", + "gpt-4o-audio-preview", + "gpt-4o-audio-preview-2024-10-01", + "gpt-4o-audio-preview-2024-12-17", + "gpt-4o-audio-preview-2025-06-03", + "gpt-4o-mini-audio-preview", + "gpt-4o-mini-audio-preview-2024-12-17", + "gpt-4o-search-preview", + "gpt-4o-mini-search-preview", + "gpt-4o-search-preview-2025-03-11", + "gpt-4o-mini-search-preview-2025-03-11", + "chatgpt-4o-latest", + "codex-mini-latest", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-0125-preview", + "gpt-4-turbo-preview", + "gpt-4-1106-preview", + "gpt-4-vision-preview", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-16k-0613" + ]) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "modalities": Schema.optionalKey(ResponseModalities), + "verbosity": Schema.optionalKey(Verbosity), + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }) + ), + "frequency_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(-2), Schema.isLessThanOrEqualTo(2)], { + "description": + "Number between -2.0 and 2.0. Positive values penalize new tokens based on\ntheir existing frequency in the text so far, decreasing the model's\nlikelihood to repeat the same line verbatim.\n" + }) + ) + ]) + ), + "presence_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(-2), Schema.isLessThanOrEqualTo(2)], { + "description": + "Number between -2.0 and 2.0. Positive values penalize new tokens based on\nwhether they appear in the text so far, increasing the model's likelihood\nto talk about new topics.\n" + }) + ) + ]) + ), + "web_search_options": Schema.optionalKey( + Schema.Struct({ + "user_location": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("approximate").annotate({ + "description": "The type of location approximation. Always `approximate`.\n" + }), + "approximate": WebSearchLocation + }).annotate({ "description": "Approximate location parameters for the search.\n" }) + ]) + ), + "search_context_size": Schema.optionalKey(WebSearchContextSize) + }).annotate({ + "title": "Web search", + "description": + "This tool searches the web for relevant results to use in a response.\nLearn more about the [web search tool](/docs/guides/tools-web-search?api-mode=chat).\n" + }) + ), + "response_format": Schema.optionalKey( + Schema.Union([ResponseFormatText, ResponseFormatJsonSchema, ResponseFormatJsonObject], { mode: "oneOf" }).annotate({ + "description": + "An object specifying the format that the model must output.\n\nSetting to `{ \"type\": \"json_schema\", \"json_schema\": {...} }` enables\nStructured Outputs which ensures the model will match your supplied JSON\nschema. Learn more in the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n\nSetting to `{ \"type\": \"json_object\" }` enables the older JSON mode, which\nensures the message the model generates is valid JSON. Using `json_schema`\nis preferred for models that support it.\n" + }) + ), + "audio": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "voice": Schema.Union([ + VoiceIdsShared, + Schema.Struct({ "id": Schema.String.annotate({ "description": "The custom voice ID, e.g. `voice_1234`." }) }) + .annotate({ "description": "Custom voice reference." }) + ]).annotate({ + "title": "Voice", + "description": + "The voice the model uses to respond. Supported built-in voices are\n`alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`,\n`sage`, `shimmer`, `marin`, and `cedar`. You may also provide a\ncustom voice object with an `id`, for example `{ \"id\": \"voice_1234\" }`.\n" + }), + "format": Schema.Literals(["wav", "aac", "mp3", "flac", "opus", "pcm16"]).annotate({ + "description": "Specifies the output audio format. Must be one of `wav`, `mp3`, `flac`,\n`opus`, or `pcm16`.\n" + }) + }).annotate({ + "description": + "Parameters for audio output. Required when audio output is requested with\n`modalities: [\"audio\"]`. [Learn more](/docs/guides/audio).\n" + }) + ])), + "store": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether or not to store the output of this chat completion request for\nuse in our [model distillation](/docs/guides/distillation) or\n[evals](/docs/guides/evals) products.\n\nSupports text and image inputs. Note: image inputs over 8MB will be dropped.\n" + }) + ), + "stream": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "If set to true, the model response data will be streamed to the client\nas it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).\nSee the [Streaming section below](/docs/api-reference/chat/streaming)\nfor more information, along with the [streaming responses](/docs/guides/streaming-responses)\nguide for more information on how to handle the streaming events.\n" + }) + ), + "stop": Schema.optionalKey(StopConfiguration), + "logit_bias": Schema.optionalKey(Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Modify the likelihood of specified tokens appearing in the completion.\n\nAccepts a JSON object that maps tokens (specified by their token ID in the\ntokenizer) to an associated bias value from -100 to 100. Mathematically,\nthe bias is added to the logits generated by the model prior to sampling.\nThe exact effect will vary per model, but values between -1 and 1 should\ndecrease or increase likelihood of selection; values like -100 or 100\nshould result in a ban or exclusive selection of the relevant token.\n" + }) + ])), + "logprobs": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to return log probabilities of the output tokens or not. If true,\nreturns the log probabilities of each output token returned in the\n`content` of `message`.\n" + }) + ), + "max_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt()), Schema.Null]).annotate({ + "description": + "The maximum number of [tokens](/tokenizer) that can be generated in the\nchat completion. This value can be used to control\n[costs](https://openai.com/api/pricing/) for text generated via API.\n\nThis value is now deprecated in favor of `max_completion_tokens`, and is\nnot compatible with [o-series models](/docs/guides/reasoning).\n" + }) + ), + "n": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([Schema.isFinite(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(128)], { + "description": + "How many chat completion choices to generate for each input message. Note that you will be charged based on the number of generated tokens across all of the choices. Keep `n` as `1` to minimize costs." + }) + ) + ]) + ), + "prediction": Schema.optionalKey(Schema.Union([ + Schema.Union([PredictionContent], { mode: "oneOf" }).annotate({ + "description": + "Configuration for a [Predicted Output](/docs/guides/predicted-outputs),\nwhich can greatly improve response times when large parts of the model\nresponse are known ahead of time. This is most common when you are\nregenerating a file with only minor changes to most of the content.\n" + }), + Schema.Null + ])), + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([ + Schema.isFinite(), + Schema.isGreaterThanOrEqualTo(-9223372036854776000), + Schema.isLessThanOrEqualTo(9223372036854776000) + ], { + "description": + "This feature is in Beta.\nIf specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result.\nDeterminism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend.\n" + }) + ) + ]) + ), + "stream_options": Schema.optionalKey(ChatCompletionStreamOptions), + "tools": Schema.optionalKey( + Schema.Array(Schema.Union([ChatCompletionTool, CustomToolChatCompletions], { mode: "oneOf" })).annotate({ + "description": + "A list of tools the model may call. You can provide either\n[custom tools](/docs/guides/function-calling#custom-tools) or\n[function tools](/docs/guides/function-calling).\n" + }) + ), + "tool_choice": Schema.optionalKey(ChatCompletionToolChoiceOption), + "parallel_tool_calls": Schema.optionalKey(ParallelToolCalls), + "function_call": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["none", "auto"]).annotate({ + "description": + "`none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function.\n" + }), + ChatCompletionFunctionCallOption + ], { mode: "oneOf" }).annotate({ + "description": + "Deprecated in favor of `tool_choice`.\n\nControls which (if any) function is called by the model.\n\n`none` means the model will not call a function and instead generates a\nmessage.\n\n`auto` means the model can pick between generating a message or calling a\nfunction.\n\nSpecifying a particular function via `{\"name\": \"my_function\"}` forces the\nmodel to call that function.\n\n`none` is the default when no functions are present. `auto` is the default\nif functions are present.\n" + }) + ), + "functions": Schema.optionalKey( + Schema.Array(ChatCompletionFunctions).annotate({ + "description": "Deprecated in favor of `tools`.\n\nA list of functions the model may generate JSON inputs for.\n" + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(128)) + ) +}) +export type ListRunStepsResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean + readonly [x: string]: unknown +} +export const ListRunStepsResponse = Schema.StructWithRest( + Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(RunStepObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean + }), + [Schema.Record(Schema.String, Schema.Json)] +) +export type RunStepStreamEvent = + | { readonly "event": "thread.run.step.created"; readonly "data": RunStepObject } + | { readonly "event": "thread.run.step.in_progress"; readonly "data": RunStepObject } + | { readonly "event": "thread.run.step.delta"; readonly "data": RunStepDeltaObject } + | { readonly "event": "thread.run.step.completed"; readonly "data": RunStepObject } + | { readonly "event": "thread.run.step.failed"; readonly "data": RunStepObject } + | { readonly "event": "thread.run.step.cancelled"; readonly "data": RunStepObject } + | { readonly "event": "thread.run.step.expired"; readonly "data": RunStepObject } +export const RunStepStreamEvent = Schema.Union([ + Schema.Struct({ "event": Schema.Literal("thread.run.step.created"), "data": RunStepObject }).annotate({ + "description": "Occurs when a [run step](/docs/api-reference/run-steps/step-object) is created." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.in_progress"), "data": RunStepObject }).annotate({ + "description": + "Occurs when a [run step](/docs/api-reference/run-steps/step-object) moves to an `in_progress` state." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.delta"), "data": RunStepDeltaObject }).annotate({ + "description": "Occurs when parts of a [run step](/docs/api-reference/run-steps/step-object) are being streamed." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.completed"), "data": RunStepObject }).annotate({ + "description": "Occurs when a [run step](/docs/api-reference/run-steps/step-object) is completed." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.failed"), "data": RunStepObject }).annotate({ + "description": "Occurs when a [run step](/docs/api-reference/run-steps/step-object) fails." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.cancelled"), "data": RunStepObject }).annotate({ + "description": "Occurs when a [run step](/docs/api-reference/run-steps/step-object) is cancelled." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.step.expired"), "data": RunStepObject }).annotate({ + "description": "Occurs when a [run step](/docs/api-reference/run-steps/step-object) expires." + }) +], { mode: "oneOf" }) +export type RealtimeServerEventResponseCreated = { + readonly "event_id": string + readonly "type": "response.created" + readonly "response": RealtimeResponse +} +export const RealtimeServerEventResponseCreated = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.created").annotate({ "description": "The event type, must be `response.created`." }), + "response": RealtimeResponse +}).annotate({ + "description": + "Returned when a new Response is created. The first event of response creation,\nwhere the response is in an initial state of `in_progress`.\n" +}) +export type RealtimeServerEventResponseDone = { + readonly "event_id": string + readonly "type": "response.done" + readonly "response": RealtimeResponse +} +export const RealtimeServerEventResponseDone = Schema.Struct({ + "event_id": Schema.String.annotate({ "description": "The unique ID of the server event." }), + "type": Schema.Literal("response.done").annotate({ "description": "The event type, must be `response.done`." }), + "response": RealtimeResponse +}).annotate({ + "description": + "Returned when a Response is done streaming. Always emitted, no matter the \nfinal state. The Response object included in the `response.done` event will \ninclude all output Items in the Response but will omit the raw audio data.\n\nClients should check the `status` field of the Response to determine if it was successful\n(`completed`) or if there was another outcome: `cancelled`, `failed`, or `incomplete`.\n\nA response will contain all output items that were generated during the response, excluding\nany audio content.\n" +}) +export type RealtimeClientEventResponseCreate = { + readonly "event_id"?: string + readonly "type": "response.create" + readonly "response"?: RealtimeResponseCreateParams +} +export const RealtimeClientEventResponseCreate = Schema.Struct({ + "event_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional client-generated ID used to identify this event." }).check( + Schema.isMaxLength(512) + ) + ), + "type": Schema.Literal("response.create").annotate({ "description": "The event type, must be `response.create`." }), + "response": Schema.optionalKey(RealtimeResponseCreateParams) +}).annotate({ + "description": + "This event instructs the server to create a Response, which means triggering \nmodel inference. When in Server VAD mode, the server will create Responses \nautomatically.\n\nA Response will include at least one Item, and may have two, in which case \nthe second will be a function call. These Items will be appended to the \nconversation history by default.\n\nThe server will respond with a `response.created` event, events for Items \nand content created, and finally a `response.done` event to indicate the \nResponse is complete.\n\nThe `response.create` event includes inference configuration like \n`instructions` and `tools`. If these are set, they will override the Session's \nconfiguration for this Response only.\n\nResponses can be created out-of-band of the default Conversation, meaning that they can\nhave arbitrary input, and it's possible to disable writing the output to the Conversation.\nOnly one Response can write to the default Conversation at a time, but otherwise multiple\nResponses can be created in parallel. The `metadata` field is a good way to disambiguate\nmultiple simultaneous Responses.\n\nClients can set `conversation` to `none` to create a Response that does not write to the default\nConversation. Arbitrary input can be provided with the `input` field, which is an array accepting\nraw Items and references to existing Items.\n" +}) +export type ListAssistantsResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ListAssistantsResponse = Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(AssistantObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean +}) +export type ListRunsResponse = { + readonly "object": string + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const ListRunsResponse = Schema.Struct({ + "object": Schema.String, + "data": Schema.Array(RunObject), + "first_id": Schema.String, + "last_id": Schema.String, + "has_more": Schema.Boolean +}) +export type RunStreamEvent = + | { readonly "event": "thread.run.created"; readonly "data": RunObject } + | { readonly "event": "thread.run.queued"; readonly "data": RunObject } + | { readonly "event": "thread.run.in_progress"; readonly "data": RunObject } + | { readonly "event": "thread.run.requires_action"; readonly "data": RunObject } + | { readonly "event": "thread.run.completed"; readonly "data": RunObject } + | { readonly "event": "thread.run.incomplete"; readonly "data": RunObject } + | { readonly "event": "thread.run.failed"; readonly "data": RunObject } + | { readonly "event": "thread.run.cancelling"; readonly "data": RunObject } + | { readonly "event": "thread.run.cancelled"; readonly "data": RunObject } + | { readonly "event": "thread.run.expired"; readonly "data": RunObject } +export const RunStreamEvent = Schema.Union([ + Schema.Struct({ "event": Schema.Literal("thread.run.created"), "data": RunObject }).annotate({ + "description": "Occurs when a new [run](/docs/api-reference/runs/object) is created." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.queued"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) moves to a `queued` status." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.in_progress"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) moves to an `in_progress` status." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.requires_action"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) moves to a `requires_action` status." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.completed"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) is completed." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.incomplete"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) ends with status `incomplete`." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.failed"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) fails." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.cancelling"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) moves to a `cancelling` status." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.cancelled"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) is cancelled." + }), + Schema.Struct({ "event": Schema.Literal("thread.run.expired"), "data": RunObject }).annotate({ + "description": "Occurs when a [run](/docs/api-reference/runs/object) expires." + }) +], { mode: "oneOf" }) +export type Tool = + | FunctionTool + | FileSearchTool + | ComputerTool + | ComputerUsePreviewTool + | WebSearchTool + | MCPTool + | CodeInterpreterTool + | ImageGenTool + | LocalShellToolParam + | FunctionShellToolParam + | CustomToolParam + | NamespaceToolParam + | ToolSearchToolParam + | WebSearchPreviewTool + | ApplyPatchToolParam +export const Tool = Schema.Union([ + FunctionTool, + FileSearchTool, + ComputerTool, + ComputerUsePreviewTool, + WebSearchTool, + MCPTool, + CodeInterpreterTool, + ImageGenTool, + LocalShellToolParam, + FunctionShellToolParam, + CustomToolParam, + NamespaceToolParam, + ToolSearchToolParam, + WebSearchPreviewTool, + ApplyPatchToolParam +], { mode: "oneOf" }).annotate({ "description": "A tool that can be used to generate a response.\n" }) +export type CreateEvalItem = { readonly "role": string; readonly "content": string } | { + readonly "role": "user" | "assistant" | "system" | "developer" + readonly "content": EvalItemContent + readonly "type"?: "message" +} +export const CreateEvalItem = Schema.Union([ + Schema.Struct({ + "role": Schema.String.annotate({ + "description": "The role of the message (e.g. \"system\", \"assistant\", \"user\")." + }), + "content": Schema.String.annotate({ "description": "The content of the message." }) + }).annotate({ + "title": "CreateEvalItem", + "description": + "A chat message that makes up the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }), + Schema.Struct({ + "role": Schema.Literals(["user", "assistant", "system", "developer"]).annotate({ + "description": "The role of the message input. One of `user`, `assistant`, `system`, or\n`developer`.\n" + }), + "content": EvalItemContent, + "type": Schema.optionalKey( + Schema.Literal("message").annotate({ "description": "The type of the message input. Always `message`.\n" }) + ) + }).annotate({ + "title": "CreateEvalItem", + "description": + "A chat message that makes up the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }) +], { mode: "oneOf" }) +export type EvalItem = { + readonly "role": "user" | "assistant" | "system" | "developer" + readonly "content": EvalItemContent + readonly "type"?: "message" +} +export const EvalItem = Schema.Struct({ + "role": Schema.Literals(["user", "assistant", "system", "developer"]).annotate({ + "description": "The role of the message input. One of `user`, `assistant`, `system`, or\n`developer`.\n" + }), + "content": EvalItemContent, + "type": Schema.optionalKey( + Schema.Literal("message").annotate({ "description": "The type of the message input. Always `message`.\n" }) + ) +}).annotate({ + "title": "Eval message object", + "description": + "A message input to the model with a role indicating instruction following\nhierarchy. Instructions given with the `developer` or `system` role take\nprecedence over instructions given with the `user` role. Messages with the\n`assistant` role are presumed to have been generated by the model in previous\ninteractions.\n" +}) +export type OutputMessage = { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray + readonly "phase"?: MessagePhase | null + readonly "status": "in_progress" | "completed" | "incomplete" +} +export const OutputMessage = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the output message.\n" }), + "type": Schema.Literal("message").annotate({ "description": "The type of the output message. Always `message`.\n" }), + "role": Schema.Literal("assistant").annotate({ + "description": "The role of the output message. Always `assistant`.\n" + }), + "content": Schema.Array(OutputMessageContent).annotate({ "description": "The content of the output message.\n" }), + "phase": Schema.optionalKey(Schema.Union([MessagePhase, Schema.Null])), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or\n`incomplete`. Populated when input items are returned via API.\n" + }) +}).annotate({ "title": "Output message", "description": "An output message from the model.\n" }) +export type ComputerToolCall = { + readonly "type": "computer_call" + readonly "id": string + readonly "call_id": string + readonly "action"?: ComputerAction + readonly "actions"?: ComputerActionList + readonly "pending_safety_checks": ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" +} +export const ComputerToolCall = Schema.Struct({ + "type": Schema.Literal("computer_call").annotate({ + "description": "The type of the computer call. Always `computer_call`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the computer call." }), + "call_id": Schema.String.annotate({ + "description": "An identifier used when responding to the tool call with output.\n" + }), + "action": Schema.optionalKey(ComputerAction), + "actions": Schema.optionalKey(ComputerActionList), + "pending_safety_checks": Schema.Array(ComputerCallSafetyCheckParam).annotate({ + "description": "The pending safety checks for the computer call.\n" + }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) +}).annotate({ + "title": "Computer tool call", + "description": + "A tool call to a computer use tool. See the\n[computer use guide](/docs/guides/tools-computer-use) for more information.\n" +}) +export type ThreadItemListResource = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "last_id": string | null + readonly "has_more": boolean +} +export const ThreadItemListResource = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(ThreadItem).annotate({ "description": "A list of items" }), + "first_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the first item in the list." }), + Schema.Null + ]), + "last_id": Schema.Union([ + Schema.String.annotate({ "description": "The ID of the last item in the list." }), + Schema.Null + ]), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }) +}).annotate({ + "title": "Thread Items", + "description": "A paginated list of thread items rendered for the ChatKit API." +}) +export type InputItem = + | EasyInputMessage + | { + readonly "type"?: "message" + readonly "role": "user" | "system" | "developer" + readonly "status"?: "in_progress" | "completed" | "incomplete" + readonly "content": InputMessageContentList + } + | { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray + readonly "phase"?: MessagePhase | null + readonly "status": "in_progress" | "completed" | "incomplete" + } + | { + readonly "id": string + readonly "type": "file_search_call" + readonly "status": "in_progress" | "searching" | "completed" | "incomplete" | "failed" + readonly "queries": ReadonlyArray + readonly "results"?: + | ReadonlyArray< + { + readonly "file_id"?: string + readonly "text"?: string + readonly "filename"?: string + readonly "attributes"?: VectorStoreFileAttributes + readonly "score"?: number + } + > + | null + } + | { + readonly "type": "computer_call" + readonly "id": string + readonly "call_id": string + readonly "action"?: ComputerAction + readonly "actions"?: ComputerActionList + readonly "pending_safety_checks": ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + } + | { + readonly "id"?: string | null + readonly "call_id": string + readonly "type": "computer_call_output" + readonly "output": ComputerScreenshotImage + readonly "acknowledged_safety_checks"?: ReadonlyArray | null + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + } + | { + readonly "id": string + readonly "type": "web_search_call" + readonly "status": "in_progress" | "searching" | "completed" | "failed" + readonly "action": + | { + readonly "type": "search" + readonly "query"?: string + readonly "queries"?: ReadonlyArray + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "type": "find_in_page"; readonly "url": string; readonly "pattern": string } + } + | { + readonly "id"?: string + readonly "type": "function_call" + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "arguments": string + readonly "status"?: "in_progress" | "completed" | "incomplete" + } + | { + readonly "id"?: string | null + readonly "call_id": string + readonly "type": "function_call_output" + readonly "output": + | string + | ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + } + | { + readonly "id"?: string | null + readonly "call_id"?: string | null + readonly "type": "tool_search_call" + readonly "execution"?: "server" | "client" + readonly "arguments": {} + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + } + | { + readonly "id"?: string | null + readonly "call_id"?: string | null + readonly "type": "tool_search_output" + readonly "execution"?: "server" | "client" + readonly "tools": ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + } + | { + readonly "type": "reasoning" + readonly "id": string + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + readonly "content"?: ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" + } + | { readonly "id"?: string | null; readonly "type": "compaction"; readonly "encrypted_content": string } + | { + readonly "type": "image_generation_call" + readonly "id": string + readonly "status": "in_progress" | "completed" | "generating" | "failed" + readonly "result": string | null + } + | { + readonly "type": "code_interpreter_call" + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" | "interpreting" | "failed" + readonly "container_id": string + readonly "code": string | null + readonly "outputs": ReadonlyArray | null + } + | { + readonly "type": "local_shell_call" + readonly "id": string + readonly "call_id": string + readonly "action": LocalShellExecAction + readonly "status": "in_progress" | "completed" | "incomplete" + } + | { + readonly "type": "local_shell_call_output" + readonly "id": string + readonly "output": string + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + readonly "call_id": unknown + } + | { + readonly "id"?: string | null + readonly "call_id": string + readonly "type": "shell_call" + readonly "action": { + readonly "commands": ReadonlyArray + readonly "timeout_ms"?: number | null + readonly "max_output_length"?: number | null + } + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + readonly "environment"?: LocalEnvironmentParam | ContainerReferenceParam | null + } + | { + readonly "id"?: string | null + readonly "call_id": string + readonly "type": "shell_call_output" + readonly "output": ReadonlyArray + readonly "status"?: "in_progress" | "completed" | "incomplete" | null + readonly "max_output_length"?: number | null + } + | { + readonly "type": "apply_patch_call" + readonly "id"?: string | null + readonly "call_id": string + readonly "status": "in_progress" | "completed" + readonly "operation": + | ApplyPatchCreateFileOperationParam + | ApplyPatchDeleteFileOperationParam + | ApplyPatchUpdateFileOperationParam + } + | { + readonly "type": "apply_patch_call_output" + readonly "id"?: string | null + readonly "call_id": string + readonly "status": "completed" | "failed" + readonly "output"?: string | null + } + | { + readonly "type": "mcp_list_tools" + readonly "id": string + readonly "server_label": string + readonly "tools": ReadonlyArray + readonly "error"?: string | null + } + | { + readonly "type": "mcp_approval_request" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string + } + | { + readonly "type": "mcp_approval_response" + readonly "id"?: string | null + readonly "approval_request_id": string + readonly "approve": boolean + readonly "reason"?: string | null + readonly "request_id": unknown + } + | { + readonly "type": "mcp_call" + readonly "id": string + readonly "server_label": string + readonly "name": string + readonly "arguments": string + readonly "output"?: string | null + readonly "error"?: string | null + readonly "status"?: "in_progress" | "completed" | "incomplete" | "calling" | "failed" + readonly "approval_request_id"?: string | null + } + | { + readonly "type": "custom_tool_call_output" + readonly "id"?: string + readonly "call_id": string + readonly "output": string | ReadonlyArray + } + | { + readonly "type": "custom_tool_call" + readonly "id"?: string + readonly "call_id": string + readonly "namespace"?: string + readonly "name": string + readonly "input": string + } + | CompactionTriggerItemParam + | ItemReferenceParam +export const InputItem = Schema.Union([ + EasyInputMessage, + Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey( + Schema.Literal("message").annotate({ + "description": "The type of the message input. Always set to `message`.\n" + }) + ), + "role": Schema.Literals(["user", "system", "developer"]).annotate({ + "description": "The role of the message input. One of `user`, `system`, or `developer`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ), + "content": InputMessageContentList + }).annotate({ "title": "Input message", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the output message.\n" }), + "type": Schema.Literal("message").annotate({ + "description": "The type of the output message. Always `message`.\n" + }), + "role": Schema.Literal("assistant").annotate({ + "description": "The role of the output message. Always `assistant`.\n" + }), + "content": Schema.Array(OutputMessageContent).annotate({ "description": "The content of the output message.\n" }), + "phase": Schema.optionalKey(Schema.Union([MessagePhase, Schema.Null])), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or\n`incomplete`. Populated when input items are returned via API.\n" + }) + }).annotate({ "title": "Output message", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the file search tool call.\n" }), + "type": Schema.Literal("file_search_call").annotate({ + "description": "The type of the file search tool call. Always `file_search_call`.\n" + }), + "status": Schema.Literals(["in_progress", "searching", "completed", "incomplete", "failed"]).annotate({ + "description": + "The status of the file search tool call. One of `in_progress`,\n`searching`, `incomplete` or `failed`,\n" + }), + "queries": Schema.Array(Schema.String).annotate({ "description": "The queries used to search for files.\n" }), + "results": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the file.\n" })), + "text": Schema.optionalKey( + Schema.String.annotate({ "description": "The text that was retrieved from the file.\n" }) + ), + "filename": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the file.\n" })), + "attributes": Schema.optionalKey(VectorStoreFileAttributes), + "score": Schema.optionalKey( + Schema.Number.annotate({ + "description": "The relevance score of the file - a value between 0 and 1.\n", + "format": "float" + }).check(Schema.isFinite()) + ) + })).annotate({ "description": "The results of the file search tool call.\n" }), + Schema.Null + ])) + }).annotate({ "title": "File search tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("computer_call").annotate({ + "description": "The type of the computer call. Always `computer_call`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the computer call." }), + "call_id": Schema.String.annotate({ + "description": "An identifier used when responding to the tool call with output.\n" + }), + "action": Schema.optionalKey(ComputerAction), + "actions": Schema.optionalKey(ComputerActionList), + "pending_safety_checks": Schema.Array(ComputerCallSafetyCheckParam).annotate({ + "description": "The pending safety checks for the computer call.\n" + }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + }).annotate({ "title": "Computer tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The ID of the computer tool call output." }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ "description": "The ID of the computer tool call that produced the output." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "type": Schema.Literal("computer_call_output").annotate({ + "description": "The type of the computer tool call output. Always `computer_call_output`." + }), + "output": ComputerScreenshotImage, + "acknowledged_safety_checks": Schema.optionalKey( + Schema.Union([ + Schema.Array(ComputerCallSafetyCheckParam).annotate({ + "description": "The safety checks reported by the API that have been acknowledged by the developer." + }), + Schema.Null + ]) + ), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the message input. One of `in_progress`, `completed`, or `incomplete`. Populated when input items are returned via API." + }), + Schema.Null + ]) + ) + }).annotate({ "title": "Computer tool call output", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique ID of the web search tool call.\n" }), + "type": Schema.Literal("web_search_call").annotate({ + "description": "The type of the web search tool call. Always `web_search_call`.\n" + }), + "status": Schema.Literals(["in_progress", "searching", "completed", "failed"]).annotate({ + "description": "The status of the web search tool call.\n" + }), + "action": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("search").annotate({ "description": "The action type.\n" }), + "query": Schema.optionalKey(Schema.String.annotate({ "description": "[DEPRECATED] The search query.\n" })), + "queries": Schema.optionalKey( + Schema.Array(Schema.String.annotate({ "description": "A search query.\n" })).annotate({ + "title": "Search queries", + "description": "The search queries.\n" + }) + ), + "sources": Schema.optionalKey( + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("url").annotate({ "description": "The type of source. Always `url`.\n" }), + "url": Schema.String.annotate({ "description": "The URL of the source.\n", "format": "uri" }) + }).annotate({ "title": "Web search source", "description": "A source used in the search.\n" }) + ).annotate({ "title": "Web search sources", "description": "The sources used in the search.\n" }) + ) + }).annotate({ + "title": "Search action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }), + Schema.Struct({ + "type": Schema.Literal("open_page").annotate({ "description": "The action type. Always `open_page`.\n" }), + "url": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "format": "uri" }), Schema.Null]).annotate({ + "description": "The URL opened by the model.\n" + }) + ) + }).annotate({ + "title": "Open page action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }), + Schema.Struct({ + "type": Schema.Literal("find_in_page").annotate({ "description": "The action type.\n" }), + "url": Schema.String.annotate({ + "description": "The URL of the page searched for the pattern.\n", + "format": "uri" + }), + "pattern": Schema.String.annotate({ "description": "The pattern or text to search for within the page.\n" }) + }).annotate({ + "title": "Find action", + "description": + "An object describing the specific action taken in this web search call.\nIncludes details on how the model used the web (search, open_page, find_in_page).\n" + }) + ], { mode: "oneOf" }) + }).annotate({ "title": "Web search tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String.annotate({ "description": "The unique ID of the function tool call.\n" })), + "type": Schema.Literal("function_call").annotate({ + "description": "The type of the function tool call. Always `function_call`.\n" + }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model.\n" + }), + "namespace": Schema.optionalKey( + Schema.String.annotate({ "description": "The namespace of the function to run.\n" }) + ), + "name": Schema.String.annotate({ "description": "The name of the function to run.\n" }), + "arguments": Schema.String.annotate({ + "description": "A JSON string of the arguments to pass to the function.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ) + }).annotate({ "title": "Function tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the function tool call output. Populated when this item is returned via API." + }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the function tool call generated by the model." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "type": Schema.Literal("function_call_output").annotate({ + "description": "The type of the function tool call output. Always `function_call_output`." + }), + "output": Schema.Union([ + Schema.String.annotate({ "description": "A JSON string of the output of the function tool call." }).check( + Schema.isMaxLength(10485760) + ), + Schema.Array( + Schema.Union([InputTextContentParam, InputImageContentParamAutoParam, InputFileContentParam], { + mode: "oneOf" + }).annotate({ "description": "A piece of message content, such as text, an image, or a file." }) + ).annotate({ "description": "An array of content outputs (text, image, file) for the function tool call." }) + ], { mode: "oneOf" }).annotate({ "description": "Text, image, or file output of the function tool call." }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or `incomplete`. Populated when items are returned via API." + }), + Schema.Null + ]) + ) + }).annotate({ "title": "Function tool call output", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of this tool search call." }), + Schema.Null + ]) + ), + "call_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of the tool search call generated by the model." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + Schema.Null + ]) + ), + "type": Schema.Literal("tool_search_call").annotate({ + "description": "The item type. Always `tool_search_call`." + }), + "execution": Schema.optionalKey( + Schema.Literals(["server", "client"]).annotate({ + "description": "Whether tool search was executed by the server or by the client." + }) + ), + "arguments": Schema.Struct({}).annotate({ "description": "The arguments supplied to the tool search call." }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the tool search call." + }), + Schema.Null + ]) + ) + }).annotate({ "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of this tool search output." }), + Schema.Null + ]) + ), + "call_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of the tool search call generated by the model." }) + .check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + Schema.Null + ]) + ), + "type": Schema.Literal("tool_search_output").annotate({ + "description": "The item type. Always `tool_search_output`." + }), + "execution": Schema.optionalKey( + Schema.Literals(["server", "client"]).annotate({ + "description": "Whether tool search was executed by the server or by the client." + }) + ), + "tools": Schema.Array(Tool).annotate({ + "description": "The loaded tool definitions returned by the tool search output." + }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the tool search output." + }), + Schema.Null + ]) + ) + }).annotate({ "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("reasoning").annotate({ "description": "The type of the object. Always `reasoning`.\n" }), + "id": Schema.String.annotate({ "description": "The unique identifier of the reasoning content.\n" }), + "encrypted_content": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The encrypted content of the reasoning item - populated when a response is\ngenerated with `reasoning.encrypted_content` in the `include` parameter.\n" + }), + Schema.Null + ]) + ), + "summary": Schema.Array(SummaryTextContent).annotate({ "description": "Reasoning summary content.\n" }), + "content": Schema.optionalKey( + Schema.Array(ReasoningTextContent).annotate({ "description": "Reasoning text content.\n" }) + ), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": + "The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n" + }) + ) + }).annotate({ "title": "Reasoning", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The ID of the compaction item." }), Schema.Null]) + ), + "type": Schema.Literal("compaction").annotate({ "description": "The type of the item. Always `compaction`." }), + "encrypted_content": Schema.String.annotate({ "description": "The encrypted content of the compaction summary." }) + .check(Schema.isMaxLength(10485760)) + }).annotate({ "title": "Compaction item", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("image_generation_call").annotate({ + "description": "The type of the image generation call. Always `image_generation_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the image generation call.\n" }), + "status": Schema.Literals(["in_progress", "completed", "generating", "failed"]).annotate({ + "description": "The status of the image generation call.\n" + }), + "result": Schema.Union([ + Schema.String.annotate({ "description": "The generated image encoded in base64.\n" }), + Schema.Null + ]) + }).annotate({ "title": "Image generation call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("code_interpreter_call").annotate({ + "description": "The type of the code interpreter tool call. Always `code_interpreter_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the code interpreter tool call.\n" }), + "status": Schema.Literals(["in_progress", "completed", "incomplete", "interpreting", "failed"]).annotate({ + "description": + "The status of the code interpreter tool call. Valid values are `in_progress`, `completed`, `incomplete`, `interpreting`, and `failed`.\n" + }), + "container_id": Schema.String.annotate({ "description": "The ID of the container used to run the code.\n" }), + "code": Schema.Union([ + Schema.String.annotate({ "description": "The code to run, or null if not available.\n" }), + Schema.Null + ]), + "outputs": Schema.Union([ + Schema.Array(Schema.Union([CodeInterpreterOutputLogs, CodeInterpreterOutputImage], { mode: "oneOf" })).annotate( + { + "description": + "The outputs generated by the code interpreter, such as logs or images.\nCan be null if no outputs are available.\n" + } + ), + Schema.Null + ]) + }).annotate({ + "title": "Code interpreter tool call", + "description": "Content item used to generate a response.\n" + }), + Schema.Struct({ + "type": Schema.Literal("local_shell_call").annotate({ + "description": "The type of the local shell call. Always `local_shell_call`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the local shell call.\n" }), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the local shell tool call generated by the model.\n" + }), + "action": LocalShellExecAction, + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the local shell call.\n" + }) + }).annotate({ "title": "Local shell call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("local_shell_call_output").annotate({ + "description": "The type of the local shell tool call output. Always `local_shell_call_output`.\n" + }), + "id": Schema.String.annotate({ + "description": "The unique ID of the local shell tool call generated by the model.\n" + }), + "output": Schema.String.annotate({ + "description": "A JSON string of the output of the local shell tool call.\n" + }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the item. One of `in_progress`, `completed`, or `incomplete`.\n" + }), + Schema.Null + ]) + ), + "call_id": Schema.Unknown + }).annotate({ "title": "Local shell call output", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The unique ID of the shell tool call. Populated when this item is returned via API." + }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the shell tool call generated by the model." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "type": Schema.Literal("shell_call").annotate({ "description": "The type of the item. Always `shell_call`." }), + "action": Schema.Struct({ + "commands": Schema.Array(Schema.String).annotate({ + "description": "Ordered shell commands for the execution environment to run." + }), + "timeout_ms": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "Maximum wall-clock time in milliseconds to allow the shell commands to run." + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "max_output_length": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "Maximum number of UTF-8 characters to capture from combined stdout and stderr output." + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ + "title": "Shell action", + "description": "The shell commands and limits that describe how to run the tool call." + }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "title": "Shell call status", + "description": "The status of the shell call. One of `in_progress`, `completed`, or `incomplete`." + }), + Schema.Null + ]) + ), + "environment": Schema.optionalKey( + Schema.Union([ + Schema.Union([LocalEnvironmentParam, ContainerReferenceParam], { mode: "oneOf" }).annotate({ + "description": "The environment to execute the shell commands in." + }), + Schema.Null + ]) + ) + }).annotate({ "title": "Shell tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The unique ID of the shell tool call output. Populated when this item is returned via API." + }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the shell tool call generated by the model." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "type": Schema.Literal("shell_call_output").annotate({ + "description": "The type of the item. Always `shell_call_output`." + }), + "output": Schema.Array(FunctionShellCallOutputContentParam).annotate({ + "description": "Captured chunks of stdout and stderr output, along with their associated outcomes." + }), + "status": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "title": "Shell call status", + "description": "The status of the shell call output." + }), + Schema.Null + ]) + ), + "max_output_length": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of UTF-8 characters captured for this shell call's combined output." + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "Shell tool call output", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("apply_patch_call").annotate({ + "description": "The type of the item. Always `apply_patch_call`." + }), + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call. Populated when this item is returned via API." + }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call generated by the model." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "status": Schema.Literals(["in_progress", "completed"]).annotate({ + "title": "Apply patch call status", + "description": "The status of the apply patch tool call. One of `in_progress` or `completed`." + }), + "operation": Schema.Union([ + ApplyPatchCreateFileOperationParam, + ApplyPatchDeleteFileOperationParam, + ApplyPatchUpdateFileOperationParam + ], { mode: "oneOf" }).annotate({ + "title": "Apply patch operation", + "description": "The specific create, delete, or update instruction for the apply_patch tool call." + }) + }).annotate({ "title": "Apply patch tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("apply_patch_call_output").annotate({ + "description": "The type of the item. Always `apply_patch_call_output`." + }), + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the apply patch tool call output. Populated when this item is returned via API." + }), + Schema.Null + ]) + ), + "call_id": Schema.String.annotate({ + "description": "The unique ID of the apply patch tool call generated by the model." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(64)), + "status": Schema.Literals(["completed", "failed"]).annotate({ + "title": "Apply patch call output status", + "description": "The status of the apply patch tool call output. One of `completed` or `failed`." + }), + "output": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": "Optional human-readable log text from the apply patch tool (e.g., patch results or errors)." + }).check(Schema.isMaxLength(10485760)), + Schema.Null + ]) + ) + }).annotate({ + "title": "Apply patch tool call output", + "description": "Content item used to generate a response.\n" + }), + Schema.Struct({ + "type": Schema.Literal("mcp_list_tools").annotate({ + "description": "The type of the item. Always `mcp_list_tools`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the list.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server.\n" }), + "tools": Schema.Array(MCPListToolsTool).annotate({ "description": "The tools available on the server.\n" }), + "error": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "Error message if the server could not list tools.\n" }), + Schema.Null + ]) + ) + }).annotate({ "title": "MCP list tools", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("mcp_approval_request").annotate({ + "description": "The type of the item. Always `mcp_approval_request`.\n" + }), + "id": Schema.String.annotate({ "description": "The unique ID of the approval request.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server making the request.\n" }), + "name": Schema.String.annotate({ "description": "The name of the tool to run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of arguments for the tool.\n" }) + }).annotate({ "title": "MCP approval request", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("mcp_approval_response").annotate({ + "description": "The type of the item. Always `mcp_approval_response`.\n" + }), + "id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of the approval response\n" }), + Schema.Null + ]) + ), + "approval_request_id": Schema.String.annotate({ + "description": "The ID of the approval request being answered.\n" + }), + "approve": Schema.Boolean.annotate({ "description": "Whether the request was approved.\n" }), + "reason": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "Optional reason for the decision.\n" }), Schema.Null]) + ), + "request_id": Schema.Unknown + }).annotate({ "title": "MCP approval response", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("mcp_call").annotate({ "description": "The type of the item. Always `mcp_call`.\n" }), + "id": Schema.String.annotate({ "description": "The unique ID of the tool call.\n" }), + "server_label": Schema.String.annotate({ "description": "The label of the MCP server running the tool.\n" }), + "name": Schema.String.annotate({ "description": "The name of the tool that was run.\n" }), + "arguments": Schema.String.annotate({ "description": "A JSON string of the arguments passed to the tool.\n" }), + "output": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "The output from the tool call.\n" }), Schema.Null]) + ), + "error": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "The error from the tool call, if any.\n" }), + Schema.Null + ]) + ), + "status": Schema.optionalKey( + Schema.Literals(["in_progress", "completed", "incomplete", "calling", "failed"]).annotate({ + "description": + "The status of the tool call. One of `in_progress`, `completed`, `incomplete`, `calling`, or `failed`.\n" + }) + ), + "approval_request_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "Unique identifier for the MCP tool call approval request.\nInclude this value in a subsequent `mcp_approval_response` input to approve or reject the corresponding tool call.\n" + }), + Schema.Null + ]) + ) + }).annotate({ "title": "MCP tool call", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("custom_tool_call_output").annotate({ + "description": "The type of the custom tool call output. Always `custom_tool_call_output`.\n" + }), + "id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The unique ID of the custom tool call output in the OpenAI platform.\n" + }) + ), + "call_id": Schema.String.annotate({ + "description": "The call ID, used to map this custom tool call output to a custom tool call.\n" + }), + "output": Schema.Union([ + Schema.String.annotate({ + "title": "string output", + "description": "A string of the output of the custom tool call.\n" + }), + Schema.Array(FunctionAndCustomToolCallOutput).annotate({ + "title": "output content list", + "description": "Text, image, or file output of the custom tool call.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "The output from the custom tool call generated by your code.\nCan be a string or an list of output content.\n" + }) + }).annotate({ "title": "Custom tool call output", "description": "Content item used to generate a response.\n" }), + Schema.Struct({ + "type": Schema.Literal("custom_tool_call").annotate({ + "description": "The type of the custom tool call. Always `custom_tool_call`.\n" + }), + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "The unique ID of the custom tool call in the OpenAI platform.\n" }) + ), + "call_id": Schema.String.annotate({ + "description": "An identifier used to map this custom tool call to a tool call output.\n" + }), + "namespace": Schema.optionalKey( + Schema.String.annotate({ "description": "The namespace of the custom tool being called.\n" }) + ), + "name": Schema.String.annotate({ "description": "The name of the custom tool being called.\n" }), + "input": Schema.String.annotate({ "description": "The input for the custom tool call generated by the model.\n" }) + }).annotate({ "title": "Custom tool call", "description": "Content item used to generate a response.\n" }) + ], { mode: "oneOf" }).annotate({ + "title": "Item", + "description": + "An item representing part of the context for the response to be\ngenerated by the model. Can contain text, images, and audio inputs,\nas well as previous assistant responses and tool call outputs.\n" + }), + CompactionTriggerItemParam, + ItemReferenceParam +], { mode: "oneOf" }) +export type ToolsArray = ReadonlyArray +export const ToolsArray = Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" +}) +export type ToolSearchOutput = { + readonly "type": "tool_search_output" + readonly "id": string + readonly "call_id": string | null + readonly "execution": "server" | "client" + readonly "tools": ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "created_by"?: string +} +export const ToolSearchOutput = Schema.Struct({ + "type": Schema.Literal("tool_search_output").annotate({ + "description": "The type of the item. Always `tool_search_output`." + }), + "id": Schema.String.annotate({ "description": "The unique ID of the tool search output item." }), + "call_id": Schema.Union([ + Schema.String.annotate({ "description": "The unique ID of the tool search call generated by the model." }), + Schema.Null + ]), + "execution": Schema.Literals(["server", "client"]).annotate({ + "description": "Whether tool search was executed by the server or by the client." + }), + "tools": Schema.Array(Tool).annotate({ "description": "The loaded tool definitions returned by tool search." }), + "status": Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "The status of the tool search output item that was recorded." + }), + "created_by": Schema.optionalKey( + Schema.String.annotate({ "description": "The identifier of the actor that created the item." }) + ) +}) +export type CreateEvalLabelModelGrader = { + readonly "type": "label_model" + readonly "name": string + readonly "model": string + readonly "input": ReadonlyArray + readonly "labels": ReadonlyArray + readonly "passing_labels": ReadonlyArray +} +export const CreateEvalLabelModelGrader = Schema.Struct({ + "type": Schema.Literal("label_model").annotate({ "description": "The object type, which is always `label_model`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ + "description": "The model to use for the evaluation. Must support structured outputs." + }), + "input": Schema.Array(CreateEvalItem).annotate({ + "description": + "A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }), + "labels": Schema.Array(Schema.String).annotate({ + "description": "The labels to classify to each item in the evaluation." + }), + "passing_labels": Schema.Array(Schema.String).annotate({ + "description": "The labels that indicate a passing result. Must be a subset of labels." + }) +}).annotate({ + "title": "LabelModelGrader", + "description": "A LabelModelGrader object which uses a model to assign labels to each item\nin the evaluation.\n" +}) +export type CreateEvalRunRequest = { + readonly "name"?: string + readonly "metadata"?: Metadata + readonly "data_source": { + readonly "type": "jsonl" + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource + } | { + readonly "type": "completions" + readonly "input_messages"?: { + readonly "type": "template" + readonly "template": ReadonlyArray + } | { readonly "type": "item_reference"; readonly "item_reference": string } + readonly "sampling_params"?: { + readonly "reasoning_effort"?: ReasoningEffort + readonly "temperature"?: number + readonly "max_completion_tokens"?: number + readonly "top_p"?: number + readonly "seed"?: number + readonly "response_format"?: ResponseFormatText | ResponseFormatJsonSchema | ResponseFormatJsonObject + readonly "tools"?: ReadonlyArray + } + readonly "model"?: string + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource | EvalStoredCompletionsSource + } | { + readonly "type": "responses" + readonly "input_messages"?: { + readonly "type": "template" + readonly "template": ReadonlyArray<{ readonly "role": string; readonly "content": string } | EvalItem> + } | { readonly "type": "item_reference"; readonly "item_reference": string } + readonly "sampling_params"?: { + readonly "reasoning_effort"?: ReasoningEffort + readonly "temperature"?: number + readonly "max_completion_tokens"?: number + readonly "top_p"?: number + readonly "seed"?: number + readonly "tools"?: ReadonlyArray + readonly "text"?: { readonly "format"?: TextResponseFormatConfiguration } + } + readonly "model"?: string + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource | EvalResponsesSource + } +} +export const CreateEvalRunRequest = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the run." })), + "metadata": Schema.optionalKey(Metadata), + "data_source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("jsonl").annotate({ "description": "The type of data source. Always `jsonl`." }), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource], { mode: "oneOf" }).annotate({ + "description": "Determines what populates the `item` namespace in the data source." + }) + }).annotate({ "title": "JsonlRunDataSource", "description": "Details about the run's data source." }), + Schema.Struct({ + "type": Schema.Literal("completions").annotate({ + "description": "The type of run data source. Always `completions`." + }), + "input_messages": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("template").annotate({ + "description": "The type of input messages. Always `template`." + }), + "template": Schema.Array(Schema.Union([EasyInputMessage, EvalItem], { mode: "oneOf" })).annotate({ + "description": + "A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }) + }).annotate({ "title": "TemplateInputMessages" }), + Schema.Struct({ + "type": Schema.Literal("item_reference").annotate({ + "description": "The type of input messages. Always `item_reference`." + }), + "item_reference": Schema.String.annotate({ + "description": "A reference to a variable in the `item` namespace. Ie, \"item.input_trajectory\"" + }) + }).annotate({ "title": "ItemReferenceInputMessages" }) + ], { mode: "oneOf" }).annotate({ + "description": + "Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace." + }) + ), + "sampling_params": Schema.optionalKey(Schema.Struct({ + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs." }).check( + Schema.isFinite() + ) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum number of tokens in the generated output." }).check( + Schema.isInt() + ) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens." + }).check(Schema.isFinite()) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling." }) + .check(Schema.isInt()) + ), + "response_format": Schema.optionalKey( + Schema.Union([ResponseFormatText, ResponseFormatJsonSchema, ResponseFormatJsonObject], { mode: "oneOf" }) + .annotate({ + "description": + "An object specifying the format that the model must output.\n\nSetting to `{ \"type\": \"json_schema\", \"json_schema\": {...} }` enables\nStructured Outputs which ensures the model will match your supplied JSON\nschema. Learn more in the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n\nSetting to `{ \"type\": \"json_object\" }` enables the older JSON mode, which\nensures the message the model generates is valid JSON. Using `json_schema`\nis preferred for models that support it.\n" + }) + ), + "tools": Schema.optionalKey( + Schema.Array(ChatCompletionTool).annotate({ + "description": + "A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported.\n" + }) + ) + })), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name of the model to use for generating completions (e.g. \"o3-mini\")." + }) + ), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalStoredCompletionsSource], { + mode: "oneOf" + }).annotate({ "description": "Determines what populates the `item` namespace in this run's data source." }) + }).annotate({ "title": "CompletionsRunDataSource", "description": "Details about the run's data source." }), + Schema.Struct({ + "type": Schema.Literal("responses").annotate({ + "description": "The type of run data source. Always `responses`." + }), + "input_messages": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("template").annotate({ + "description": "The type of input messages. Always `template`." + }), + "template": Schema.Array( + Schema.Union([ + Schema.Struct({ + "role": Schema.String.annotate({ + "description": "The role of the message (e.g. \"system\", \"assistant\", \"user\")." + }), + "content": Schema.String.annotate({ "description": "The content of the message." }) + }).annotate({ "title": "ChatMessage" }), + EvalItem + ], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }) + }).annotate({ "title": "InputMessagesTemplate" }), + Schema.Struct({ + "type": Schema.Literal("item_reference").annotate({ + "description": "The type of input messages. Always `item_reference`." + }), + "item_reference": Schema.String.annotate({ + "description": "A reference to a variable in the `item` namespace. Ie, \"item.name\"" + }) + }).annotate({ "title": "InputMessagesItemReference" }) + ], { mode: "oneOf" }).annotate({ + "description": + "Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace." + }) + ), + "sampling_params": Schema.optionalKey(Schema.Struct({ + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs." }).check( + Schema.isFinite() + ) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum number of tokens in the generated output." }).check( + Schema.isInt() + ) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens." + }).check(Schema.isFinite()) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling." }) + .check(Schema.isInt()) + ), + "tools": Schema.optionalKey( + Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nThe two categories of tools you can provide the model are:\n\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code. Learn more about\n [function calling](/docs/guides/function-calling).\n" + }) + ), + "text": Schema.optionalKey( + Schema.Struct({ "format": Schema.optionalKey(TextResponseFormatConfiguration) }).annotate({ + "description": + "Configuration options for a text response from the model. Can be plain\ntext or structured JSON data. Learn more:\n- [Text inputs and outputs](/docs/guides/text)\n- [Structured Outputs](/docs/guides/structured-outputs)\n" + }) + ) + })), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name of the model to use for generating completions (e.g. \"o3-mini\")." + }) + ), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalResponsesSource], { + mode: "oneOf" + }).annotate({ "description": "Determines what populates the `item` namespace in this run's data source." }) + }).annotate({ "title": "ResponsesRunDataSource", "description": "Details about the run's data source." }) + ], { mode: "oneOf" }) +}).annotate({ "title": "CreateEvalRunRequest" }) +export type EvalGraderLabelModel = { + readonly "type": "label_model" + readonly "name": string + readonly "model": string + readonly "input": ReadonlyArray + readonly "labels": ReadonlyArray + readonly "passing_labels": ReadonlyArray +} +export const EvalGraderLabelModel = Schema.Struct({ + "type": Schema.Literal("label_model").annotate({ "description": "The object type, which is always `label_model`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ + "description": "The model to use for the evaluation. Must support structured outputs." + }), + "input": Schema.Array(EvalItem), + "labels": Schema.Array(Schema.String).annotate({ + "description": "The labels to assign to each item in the evaluation." + }), + "passing_labels": Schema.Array(Schema.String).annotate({ + "description": "The labels that indicate a passing result. Must be a subset of labels." + }) +}).annotate({ + "title": "LabelModelGrader", + "description": "A LabelModelGrader object which uses a model to assign labels to each item\nin the evaluation.\n" +}) +export type EvalGraderScoreModel = { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray + readonly "pass_threshold"?: number +} +export const EvalGraderScoreModel = Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ "description": "The object type, which is always `score_model`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ), + "pass_threshold": Schema.optionalKey( + Schema.Number.annotate({ "description": "The threshold for the score." }).check(Schema.isFinite()) + ) +}).annotate({ + "title": "ScoreModelGrader", + "description": "A ScoreModelGrader object that uses a model to assign a score to the input.\n" +}) +export type EvalRun = { + readonly "object": "eval.run" + readonly "id": string + readonly "eval_id": string + readonly "status": string + readonly "model": string + readonly "name": string + readonly "created_at": number + readonly "report_url": string + readonly "result_counts": { + readonly "total": number + readonly "errored": number + readonly "failed": number + readonly "passed": number + } + readonly "per_model_usage": ReadonlyArray< + { + readonly "model_name": string + readonly "invocation_count": number + readonly "prompt_tokens": number + readonly "completion_tokens": number + readonly "total_tokens": number + readonly "cached_tokens": number + } + > + readonly "per_testing_criteria_results": ReadonlyArray< + { readonly "testing_criteria": string; readonly "passed": number; readonly "failed": number } + > + readonly "data_source": { + readonly "type": "jsonl" + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource + } | { + readonly "type": "completions" + readonly "input_messages"?: { + readonly "type": "template" + readonly "template": ReadonlyArray + } | { readonly "type": "item_reference"; readonly "item_reference": string } + readonly "sampling_params"?: { + readonly "reasoning_effort"?: ReasoningEffort + readonly "temperature"?: number + readonly "max_completion_tokens"?: number + readonly "top_p"?: number + readonly "seed"?: number + readonly "response_format"?: ResponseFormatText | ResponseFormatJsonSchema | ResponseFormatJsonObject + readonly "tools"?: ReadonlyArray + } + readonly "model"?: string + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource | EvalStoredCompletionsSource + } | { + readonly "type": "responses" + readonly "input_messages"?: { + readonly "type": "template" + readonly "template": ReadonlyArray<{ readonly "role": string; readonly "content": string } | EvalItem> + } | { readonly "type": "item_reference"; readonly "item_reference": string } + readonly "sampling_params"?: { + readonly "reasoning_effort"?: ReasoningEffort + readonly "temperature"?: number + readonly "max_completion_tokens"?: number + readonly "top_p"?: number + readonly "seed"?: number + readonly "tools"?: ReadonlyArray + readonly "text"?: { readonly "format"?: TextResponseFormatConfiguration } + } + readonly "model"?: string + readonly "source": EvalJsonlFileContentSource | EvalJsonlFileIdSource | EvalResponsesSource + } + readonly "metadata": Metadata + readonly "error": EvalApiError +} +export const EvalRun = Schema.Struct({ + "object": Schema.Literal("eval.run").annotate({ "description": "The type of the object. Always \"eval.run\"." }), + "id": Schema.String.annotate({ "description": "Unique identifier for the evaluation run." }), + "eval_id": Schema.String.annotate({ "description": "The identifier of the associated evaluation." }), + "status": Schema.String.annotate({ "description": "The status of the evaluation run." }), + "model": Schema.String.annotate({ "description": "The model that is evaluated, if applicable." }), + "name": Schema.String.annotate({ "description": "The name of the evaluation run." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the evaluation run was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "report_url": Schema.String.annotate({ + "description": "The URL to the rendered evaluation run report on the UI dashboard.", + "format": "uri" + }), + "result_counts": Schema.Struct({ + "total": Schema.Number.annotate({ "description": "Total number of executed output items." }).check(Schema.isInt()), + "errored": Schema.Number.annotate({ "description": "Number of output items that resulted in an error." }).check( + Schema.isInt() + ), + "failed": Schema.Number.annotate({ "description": "Number of output items that failed to pass the evaluation." }) + .check(Schema.isInt()), + "passed": Schema.Number.annotate({ "description": "Number of output items that passed the evaluation." }).check( + Schema.isInt() + ) + }).annotate({ "description": "Counters summarizing the outcomes of the evaluation run." }), + "per_model_usage": Schema.Array(Schema.Struct({ + "model_name": Schema.String.annotate({ "description": "The name of the model." }), + "invocation_count": Schema.Number.annotate({ "description": "The number of invocations." }).check(Schema.isInt()), + "prompt_tokens": Schema.Number.annotate({ "description": "The number of prompt tokens used." }).check( + Schema.isInt() + ), + "completion_tokens": Schema.Number.annotate({ "description": "The number of completion tokens generated." }).check( + Schema.isInt() + ), + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used." }).check(Schema.isInt()), + "cached_tokens": Schema.Number.annotate({ "description": "The number of tokens retrieved from cache." }).check( + Schema.isInt() + ) + })).annotate({ "description": "Usage statistics for each model during the evaluation run." }), + "per_testing_criteria_results": Schema.Array(Schema.Struct({ + "testing_criteria": Schema.String.annotate({ "description": "A description of the testing criteria." }), + "passed": Schema.Number.annotate({ "description": "Number of tests passed for this criteria." }).check( + Schema.isInt() + ), + "failed": Schema.Number.annotate({ "description": "Number of tests failed for this criteria." }).check( + Schema.isInt() + ) + })).annotate({ "description": "Results per testing criteria applied during the evaluation run." }), + "data_source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("jsonl").annotate({ "description": "The type of data source. Always `jsonl`." }), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource], { mode: "oneOf" }).annotate({ + "description": "Determines what populates the `item` namespace in the data source." + }) + }).annotate({ "title": "JsonlRunDataSource", "description": "Information about the run's data source." }), + Schema.Struct({ + "type": Schema.Literal("completions").annotate({ + "description": "The type of run data source. Always `completions`." + }), + "input_messages": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("template").annotate({ + "description": "The type of input messages. Always `template`." + }), + "template": Schema.Array(Schema.Union([EasyInputMessage, EvalItem], { mode: "oneOf" })).annotate({ + "description": + "A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }) + }).annotate({ "title": "TemplateInputMessages" }), + Schema.Struct({ + "type": Schema.Literal("item_reference").annotate({ + "description": "The type of input messages. Always `item_reference`." + }), + "item_reference": Schema.String.annotate({ + "description": "A reference to a variable in the `item` namespace. Ie, \"item.input_trajectory\"" + }) + }).annotate({ "title": "ItemReferenceInputMessages" }) + ], { mode: "oneOf" }).annotate({ + "description": + "Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace." + }) + ), + "sampling_params": Schema.optionalKey(Schema.Struct({ + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs." }).check( + Schema.isFinite() + ) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum number of tokens in the generated output." }).check( + Schema.isInt() + ) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens." + }).check(Schema.isFinite()) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling." }) + .check(Schema.isInt()) + ), + "response_format": Schema.optionalKey( + Schema.Union([ResponseFormatText, ResponseFormatJsonSchema, ResponseFormatJsonObject], { mode: "oneOf" }) + .annotate({ + "description": + "An object specifying the format that the model must output.\n\nSetting to `{ \"type\": \"json_schema\", \"json_schema\": {...} }` enables\nStructured Outputs which ensures the model will match your supplied JSON\nschema. Learn more in the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n\nSetting to `{ \"type\": \"json_object\" }` enables the older JSON mode, which\nensures the message the model generates is valid JSON. Using `json_schema`\nis preferred for models that support it.\n" + }) + ), + "tools": Schema.optionalKey( + Schema.Array(ChatCompletionTool).annotate({ + "description": + "A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported.\n" + }) + ) + })), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name of the model to use for generating completions (e.g. \"o3-mini\")." + }) + ), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalStoredCompletionsSource], { + mode: "oneOf" + }).annotate({ "description": "Determines what populates the `item` namespace in this run's data source." }) + }).annotate({ "title": "CompletionsRunDataSource", "description": "Information about the run's data source." }), + Schema.Struct({ + "type": Schema.Literal("responses").annotate({ + "description": "The type of run data source. Always `responses`." + }), + "input_messages": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("template").annotate({ + "description": "The type of input messages. Always `template`." + }), + "template": Schema.Array( + Schema.Union([ + Schema.Struct({ + "role": Schema.String.annotate({ + "description": "The role of the message (e.g. \"system\", \"assistant\", \"user\")." + }), + "content": Schema.String.annotate({ "description": "The content of the message." }) + }).annotate({ "title": "ChatMessage" }), + EvalItem + ], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of chat messages forming the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}." + }) + }).annotate({ "title": "InputMessagesTemplate" }), + Schema.Struct({ + "type": Schema.Literal("item_reference").annotate({ + "description": "The type of input messages. Always `item_reference`." + }), + "item_reference": Schema.String.annotate({ + "description": "A reference to a variable in the `item` namespace. Ie, \"item.name\"" + }) + }).annotate({ "title": "InputMessagesItemReference" }) + ], { mode: "oneOf" }).annotate({ + "description": + "Used when sampling from a model. Dictates the structure of the messages passed into the model. Can either be a reference to a prebuilt trajectory (ie, `item.input_trajectory`), or a template with variable references to the `item` namespace." + }) + ), + "sampling_params": Schema.optionalKey(Schema.Struct({ + "reasoning_effort": Schema.optionalKey(ReasoningEffort), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs." }).check( + Schema.isFinite() + ) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "The maximum number of tokens in the generated output." }).check( + Schema.isInt() + ) + ), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens." + }).check(Schema.isFinite()) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling." }) + .check(Schema.isInt()) + ), + "tools": Schema.optionalKey( + Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nThe two categories of tools you can provide the model are:\n\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code. Learn more about\n [function calling](/docs/guides/function-calling).\n" + }) + ), + "text": Schema.optionalKey( + Schema.Struct({ "format": Schema.optionalKey(TextResponseFormatConfiguration) }).annotate({ + "description": + "Configuration options for a text response from the model. Can be plain\ntext or structured JSON data. Learn more:\n- [Text inputs and outputs](/docs/guides/text)\n- [Structured Outputs](/docs/guides/structured-outputs)\n" + }) + ) + })), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name of the model to use for generating completions (e.g. \"o3-mini\")." + }) + ), + "source": Schema.Union([EvalJsonlFileContentSource, EvalJsonlFileIdSource, EvalResponsesSource], { + mode: "oneOf" + }).annotate({ "description": "Determines what populates the `item` namespace in this run's data source." }) + }).annotate({ "title": "ResponsesRunDataSource", "description": "Information about the run's data source." }) + ], { mode: "oneOf" }), + "metadata": Metadata, + "error": EvalApiError +}).annotate({ "title": "EvalRun", "description": "A schema representing an evaluation run.\n" }) +export type GraderLabelModel = { + readonly "type": "label_model" + readonly "name": string + readonly "model": string + readonly "input": ReadonlyArray + readonly "labels": ReadonlyArray + readonly "passing_labels": ReadonlyArray +} +export const GraderLabelModel = Schema.Struct({ + "type": Schema.Literal("label_model").annotate({ "description": "The object type, which is always `label_model`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ + "description": "The model to use for the evaluation. Must support structured outputs." + }), + "input": Schema.Array(EvalItem), + "labels": Schema.Array(Schema.String).annotate({ + "description": "The labels to assign to each item in the evaluation." + }), + "passing_labels": Schema.Array(Schema.String).annotate({ + "description": "The labels that indicate a passing result. Must be a subset of labels." + }) +}).annotate({ + "title": "LabelModelGrader", + "description": "A LabelModelGrader object which uses a model to assign labels to each item\nin the evaluation.\n" +}) +export type GraderScoreModel = { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray +} +export const GraderScoreModel = Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ "description": "The object type, which is always `score_model`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ) +}).annotate({ + "title": "ScoreModelGrader", + "description": "A ScoreModelGrader object that uses a model to assign a score to the input.\n" +}) +export type InputParam = string | ReadonlyArray +export const InputParam = Schema.Union([ + Schema.String.annotate({ + "title": "Text input", + "description": "A text input to the model, equivalent to a text input with the\n`user` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) +], { mode: "oneOf" }).annotate({ + "description": + "Text, image, or file inputs to the model, used to generate a response.\n\nLearn more:\n- [Text inputs and outputs](/docs/guides/text)\n- [Image inputs](/docs/guides/images)\n- [File inputs](/docs/guides/pdf-files)\n- [Conversation state](/docs/guides/conversation-state)\n- [Function calling](/docs/guides/function-calling)\n" +}) +export type CreateConversationBody = { + readonly "metadata"?: {} | null | null + readonly "items"?: ReadonlyArray | null +} +export const CreateConversationBody = Schema.Struct({ + "metadata": Schema.optionalKey(Schema.Union([ + Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard.\n Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters." + }), + Schema.Null + ])), + "items": Schema.optionalKey( + Schema.Union([ + Schema.Array(InputItem).annotate({ + "description": "Initial items to include in the conversation context. You may add up to 20 items at a time." + }).check(Schema.isMaxLength(20)), + Schema.Null + ]) + ) +}) +export type TokenCountsBody = { + readonly "model"?: string | null + readonly "input"?: string | ReadonlyArray | null + readonly "previous_response_id"?: string | null + readonly "tools"?: ReadonlyArray | null + readonly "text"?: ResponseTextParam | null + readonly "reasoning"?: { + readonly "effort"?: ReasoningEffort + readonly "summary"?: "auto" | "concise" | "detailed" | null + readonly "generate_summary"?: "auto" | "concise" | "detailed" | null + } | null + readonly "truncation"?: "auto" | "disabled" + readonly "instructions"?: string | null + readonly "conversation"?: ConversationParam | null + readonly "tool_choice"?: + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + | null + readonly "parallel_tool_calls"?: boolean | null +} +export const TokenCountsBody = Schema.Struct({ + "model": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the [model guide](/docs/models) to browse and compare available models." + }), + Schema.Null + ]) + ), + "input": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the `user` role." + }).check(Schema.isMaxLength(10485760)), + Schema.Array(InputItem).annotate({ + "description": "A list of one or many input items to the model, containing different content types." + }) + ], { mode: "oneOf" }).annotate({ + "description": "Text, image, or file inputs to the model, used to generate a response" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to create multi-turn conversations. Learn more about [conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`." + }), + Schema.Null + ]) + ), + "tools": Schema.optionalKey( + Schema.Union([ + Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You can specify which tool to use by setting the `tool_choice` parameter." + }), + Schema.Null + ]) + ), + "text": Schema.optionalKey(Schema.Union([ResponseTextParam, Schema.Null])), + "reasoning": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "effort": Schema.optionalKey(ReasoningEffort), + "summary": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "concise", "detailed"]).annotate({ + "description": + "A summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n\n`concise` is supported for `computer-use-preview` models and all reasoning models after `gpt-5`.\n" + }), + Schema.Null + ])), + "generate_summary": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["auto", "concise", "detailed"]).annotate({ + "description": + "**Deprecated:** use `summary` instead.\n\nA summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n" + }), + Schema.Null + ]) + ) + }).annotate({ + "title": "Reasoning", + "description": + "**gpt-5 and o-series models only** Configuration options for [reasoning models](https://platform.openai.com/docs/guides/reasoning)." + }), + Schema.Null + ])), + "truncation": Schema.optionalKey( + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response. - `auto`: If the input to this Response exceeds the model's context window size, the model will truncate the response to fit the context window by dropping items from the beginning of the conversation. - `disabled` (default): If the input size will exceed the context window size for a model, the request will fail with a 400 error." + }) + ), + "instructions": Schema.optionalKey(Schema.Union([ + Schema.String.annotate({ + "description": + "A system (or developer) message inserted into the model's context.\nWhen used along with `previous_response_id`, the instructions from a previous response will not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses." + }), + Schema.Null + ])), + "conversation": Schema.optionalKey(Schema.Union([ConversationParam, Schema.Null])), + "tool_choice": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ "description": "Controls which tool the model should use, if any." }), + Schema.Null + ]) + ), + "parallel_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ "description": "Whether to allow the model to run tool calls in parallel." }), + Schema.Null + ]) + ) +}) +export type CompactResponseMethodPublicBody = { + readonly "model": ModelIdsCompaction + readonly "input"?: string | ReadonlyArray | null + readonly "previous_response_id"?: string | null + readonly "instructions"?: string | null + readonly "prompt_cache_key"?: string | null + readonly "prompt_cache_retention"?: "in_memory" | "in-memory" | "24h" | null + readonly "service_tier"?: "auto" | "default" | "flex" | "priority" | null +} +export const CompactResponseMethodPublicBody = Schema.Struct({ + "model": ModelIdsCompaction, + "input": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the `user` role." + }).check(Schema.isMaxLength(10485760)), + Schema.Array(InputItem).annotate({ + "description": "A list of one or many input items to the model, containing different content types." + }) + ], { mode: "oneOf" }).annotate({ + "description": "Text, image, or file inputs to the model, used to generate a response" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to create multi-turn conversations. Learn more about [conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`." + }), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey(Schema.Union([ + Schema.String.annotate({ + "description": + "A system (or developer) message inserted into the model's context.\nWhen used along with `previous_response_id`, the instructions from a previous response will not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses." + }), + Schema.Null + ])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ "description": "A key to use when reading from or writing to the prompt cache." }).check( + Schema.isMaxLength(64) + ), + Schema.Null + ]) + ), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in-memory", "24h"]).annotate({ + "description": "How long to retain a prompt cache entry created by this request." + }), + Schema.Null + ]) + ), + "service_tier": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["auto", "default", "flex", "priority"]).annotate({ + "description": "The service tier to use for this request." + }), + Schema.Null + ]) + ) +}) +export type ConversationItem = + | Message + | FunctionToolCallResource + | FunctionToolCallOutputResource + | FileSearchToolCall + | WebSearchToolCall + | ImageGenToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | ToolSearchCall + | ToolSearchOutput + | ReasoningItem + | CompactionBody + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | MCPToolCall + | CustomToolCall + | CustomToolCallOutput +export const ConversationItem = Schema.Union([ + Message, + FunctionToolCallResource, + FunctionToolCallOutputResource, + FileSearchToolCall, + WebSearchToolCall, + ImageGenToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ToolSearchCall, + ToolSearchOutput, + ReasoningItem, + CompactionBody, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + MCPToolCall, + CustomToolCall, + CustomToolCallOutput +], { mode: "oneOf" }).annotate({ + "title": "Conversation item", + "description": + "A single item within a conversation. The set of possible types are the same as the `output` type of a [Response object](/docs/api-reference/responses/object#responses/object-output)." +}) +export type ItemResource = + | InputMessageResource + | OutputMessage + | FileSearchToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | WebSearchToolCall + | FunctionToolCallResource + | FunctionToolCallOutputResource + | ToolSearchCall + | ToolSearchOutput + | ReasoningItem + | CompactionBody + | ImageGenToolCall + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | MCPToolCall + | CustomToolCallResource + | CustomToolCallOutputResource +export const ItemResource = Schema.Union([ + InputMessageResource, + OutputMessage, + FileSearchToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + WebSearchToolCall, + FunctionToolCallResource, + FunctionToolCallOutputResource, + ToolSearchCall, + ToolSearchOutput, + ReasoningItem, + CompactionBody, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + MCPToolCall, + CustomToolCallResource, + CustomToolCallOutputResource +], { mode: "oneOf" }).annotate({ "description": "Content item used to generate a response.\n" }) +export type OutputItem = + | OutputMessage + | FileSearchToolCall + | FunctionToolCall + | FunctionToolCallOutputResource + | WebSearchToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | ReasoningItem + | ToolSearchCall + | ToolSearchOutput + | CompactionBody + | ImageGenToolCall + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPToolCall + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | CustomToolCall + | CustomToolCallOutputResource +export const OutputItem = Schema.Union([ + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + FunctionToolCallOutputResource, + WebSearchToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ReasoningItem, + ToolSearchCall, + ToolSearchOutput, + CompactionBody, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPToolCall, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + CustomToolCall, + CustomToolCallOutputResource +], { mode: "oneOf" }) +export type ResponseOutputItemAddedEvent = { + readonly "type": "response.output_item.added" + readonly "output_index": number + readonly "sequence_number": number + readonly "item": + | OutputMessage + | FileSearchToolCall + | FunctionToolCall + | FunctionToolCallOutputResource + | WebSearchToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | ReasoningItem + | ToolSearchCall + | ToolSearchOutput + | CompactionBody + | ImageGenToolCall + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPToolCall + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | CustomToolCall + | CustomToolCallOutputResource +} +export const ResponseOutputItemAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.output_item.added").annotate({ + "description": "The type of the event. Always `response.output_item.added`.\n" + }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that was added.\n" }).check( + Schema.isInt() + ), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ), + "item": Schema.Union([ + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + FunctionToolCallOutputResource, + WebSearchToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ReasoningItem, + ToolSearchCall, + ToolSearchOutput, + CompactionBody, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPToolCall, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + CustomToolCall, + CustomToolCallOutputResource + ], { mode: "oneOf" }).annotate({ "description": "The output item that was added.\n" }) +}).annotate({ "description": "Emitted when a new output item is added." }) +export type ResponseOutputItemDoneEvent = { + readonly "type": "response.output_item.done" + readonly "output_index": number + readonly "sequence_number": number + readonly "item": + | OutputMessage + | FileSearchToolCall + | FunctionToolCall + | FunctionToolCallOutputResource + | WebSearchToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | ReasoningItem + | ToolSearchCall + | ToolSearchOutput + | CompactionBody + | ImageGenToolCall + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPToolCall + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | CustomToolCall + | CustomToolCallOutputResource +} +export const ResponseOutputItemDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.output_item.done").annotate({ + "description": "The type of the event. Always `response.output_item.done`.\n" + }), + "output_index": Schema.Number.annotate({ "description": "The index of the output item that was marked done.\n" }) + .check(Schema.isInt()), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event.\n" }).check( + Schema.isInt() + ), + "item": Schema.Union([ + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + FunctionToolCallOutputResource, + WebSearchToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ReasoningItem, + ToolSearchCall, + ToolSearchOutput, + CompactionBody, + ImageGenToolCall, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPToolCall, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + CustomToolCall, + CustomToolCallOutputResource + ], { mode: "oneOf" }).annotate({ "description": "The output item that was marked done.\n" }) +}).annotate({ "description": "Emitted when an output item is marked done." }) +export type ItemField = + | Message + | FunctionToolCall + | ToolSearchCall + | ToolSearchOutput + | FunctionToolCallOutput + | FileSearchToolCall + | WebSearchToolCall + | ImageGenToolCall + | ComputerToolCall + | ComputerToolCallOutputResource + | ReasoningItem + | CompactionBody + | CodeInterpreterToolCall + | LocalShellToolCall + | LocalShellToolCallOutput + | FunctionShellCall + | FunctionShellCallOutput + | ApplyPatchToolCall + | ApplyPatchToolCallOutput + | MCPListTools + | MCPApprovalRequest + | MCPApprovalResponseResource + | MCPToolCall + | CustomToolCall + | CustomToolCallOutput +export const ItemField = Schema.Union([ + Message, + FunctionToolCall, + ToolSearchCall, + ToolSearchOutput, + FunctionToolCallOutput, + FileSearchToolCall, + WebSearchToolCall, + ImageGenToolCall, + ComputerToolCall, + ComputerToolCallOutputResource, + ReasoningItem, + CompactionBody, + CodeInterpreterToolCall, + LocalShellToolCall, + LocalShellToolCallOutput, + FunctionShellCall, + FunctionShellCallOutput, + ApplyPatchToolCall, + ApplyPatchToolCallOutput, + MCPListTools, + MCPApprovalRequest, + MCPApprovalResponseResource, + MCPToolCall, + CustomToolCall, + CustomToolCallOutput +], { mode: "oneOf" }).annotate({ + "description": "An item representing a message, tool call, tool output, reasoning, or other response element." +}) +export type CreateEvalRequest = { + readonly "name"?: string + readonly "metadata"?: Metadata + readonly "data_source_config": + | { readonly "type": "custom"; readonly "item_schema": {}; readonly "include_sample_schema"?: boolean } + | { readonly "type": "logs"; readonly "metadata"?: {} } + | { readonly "type": "stored_completions"; readonly "metadata"?: {} } + readonly "testing_criteria": ReadonlyArray< + | CreateEvalLabelModelGrader + | EvalGraderStringCheck + | EvalGraderTextSimilarity + | EvalGraderPython + | EvalGraderScoreModel + > +} +export const CreateEvalRequest = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "The name of the evaluation." })), + "metadata": Schema.optionalKey(Metadata), + "data_source_config": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "The type of data source. Always `custom`." }), + "item_schema": Schema.Struct({}).annotate({ "description": "The json schema for each row in the data source." }), + "include_sample_schema": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether the eval should expect you to populate the sample namespace (ie, by generating responses off of your data source)" + }) + ) + }).annotate({ + "title": "CustomDataSourceConfig", + "description": + "The configuration for the data source used for the evaluation runs. Dictates the schema of the data used in the evaluation." + }), + Schema.Struct({ + "type": Schema.Literal("logs").annotate({ "description": "The type of data source. Always `logs`." }), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Metadata filters for the logs data source." }) + ) + }).annotate({ + "title": "LogsDataSourceConfig", + "description": + "The configuration for the data source used for the evaluation runs. Dictates the schema of the data used in the evaluation." + }), + Schema.Struct({ + "type": Schema.Literal("stored_completions").annotate({ + "description": "The type of data source. Always `stored_completions`." + }), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Metadata filters for the stored completions data source." }) + ) + }).annotate({ + "title": "StoredCompletionsDataSourceConfig", + "description": + "The configuration for the data source used for the evaluation runs. Dictates the schema of the data used in the evaluation." + }) + ], { mode: "oneOf" }), + "testing_criteria": Schema.Array( + Schema.Union([ + CreateEvalLabelModelGrader, + EvalGraderStringCheck, + EvalGraderTextSimilarity, + EvalGraderPython, + EvalGraderScoreModel + ], { mode: "oneOf" }) + ).annotate({ + "description": + "A list of graders for all eval runs in this group. Graders can reference variables in the data source using double curly braces notation, like `{{item.variable_name}}`. To reference the model's output, use the `sample` namespace (ie, `{{sample.output_text}}`)." + }) +}).annotate({ "title": "CreateEvalRequest" }) +export type Eval = { + readonly "object": "eval" + readonly "id": string + readonly "name": string + readonly "data_source_config": { readonly "type": "custom"; readonly "schema": {} } | { + readonly "type": "logs" + readonly "metadata"?: Metadata + readonly "schema": {} + } | { readonly "type": "stored_completions"; readonly "metadata"?: Metadata; readonly "schema": {} } + readonly "testing_criteria": ReadonlyArray< + EvalGraderLabelModel | EvalGraderStringCheck | EvalGraderTextSimilarity | EvalGraderPython | EvalGraderScoreModel + > + readonly "created_at": number + readonly "metadata": Metadata +} +export const Eval = Schema.Struct({ + "object": Schema.Literal("eval").annotate({ "description": "The object type." }), + "id": Schema.String.annotate({ "description": "Unique identifier for the evaluation." }), + "name": Schema.String.annotate({ "description": "The name of the evaluation." }), + "data_source_config": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("custom").annotate({ "description": "The type of data source. Always `custom`." }), + "schema": Schema.Struct({}).annotate({ + "description": + "The json schema for the run data source items.\nLearn how to build JSON schemas [here](https://json-schema.org/).\n" + }) + }).annotate({ + "title": "CustomDataSourceConfig", + "description": "Configuration of data sources used in runs of the evaluation." + }), + Schema.Struct({ + "type": Schema.Literal("logs").annotate({ "description": "The type of data source. Always `logs`." }), + "metadata": Schema.optionalKey(Metadata), + "schema": Schema.Struct({}).annotate({ + "description": + "The json schema for the run data source items.\nLearn how to build JSON schemas [here](https://json-schema.org/).\n" + }) + }).annotate({ + "title": "LogsDataSourceConfig", + "description": "Configuration of data sources used in runs of the evaluation." + }), + Schema.Struct({ + "type": Schema.Literal("stored_completions").annotate({ + "description": "The type of data source. Always `stored_completions`." + }), + "metadata": Schema.optionalKey(Metadata), + "schema": Schema.Struct({}).annotate({ + "description": + "The json schema for the run data source items.\nLearn how to build JSON schemas [here](https://json-schema.org/).\n" + }) + }).annotate({ + "title": "StoredCompletionsDataSourceConfig", + "description": "Configuration of data sources used in runs of the evaluation." + }) + ], { mode: "oneOf" }), + "testing_criteria": Schema.Array( + Schema.Union([ + EvalGraderLabelModel, + EvalGraderStringCheck, + EvalGraderTextSimilarity, + EvalGraderPython, + EvalGraderScoreModel + ], { mode: "oneOf" }) + ).annotate({ "description": "A list of testing criteria." }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the eval was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "metadata": Metadata +}).annotate({ + "title": "Eval", + "description": + "An Eval object with a data source config and testing criteria.\nAn Eval represents a task to be done for your LLM integration.\nLike:\n - Improve the quality of my chatbot\n - See how well my chatbot handles customer support\n - Check if o4-mini is better at my usecase than gpt-4o\n" +}) +export type EvalRunList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const EvalRunList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ + "description": "The type of this object. It is always set to \"list\".\n" + }), + "data": Schema.Array(EvalRun).annotate({ "description": "An array of eval run objects.\n" }), + "first_id": Schema.String.annotate({ "description": "The identifier of the first eval run in the data array." }), + "last_id": Schema.String.annotate({ "description": "The identifier of the last eval run in the data array." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates whether there are more evals available." }) +}).annotate({ "title": "EvalRunList", "description": "An object representing a list of runs for an evaluation.\n" }) +export type FineTuneReinforcementMethod = { + readonly "grader": + | { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" + } + | { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" + } + | { readonly "type": "python"; readonly "name": string; readonly "source": string; readonly "image_tag"?: string } + | { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray + } + | { + readonly "type": "multi" + readonly "name": string + readonly "graders": GraderStringCheck | GraderTextSimilarity | GraderPython | GraderScoreModel | GraderLabelModel + readonly "calculate_output": string + } + readonly "hyperparameters"?: FineTuneReinforcementHyperparameters +} +export const FineTuneReinforcementMethod = Schema.Struct({ + "grader": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) + }).annotate({ "title": "StringCheckGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }) + }).annotate({ "title": "TextSimilarityGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ) + }).annotate({ "title": "PythonGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ + "description": "The object type, which is always `score_model`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ) + }).annotate({ "title": "ScoreModelGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("multi").annotate({ "description": "The object type, which is always `multi`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "graders": Schema.Union([ + GraderStringCheck, + GraderTextSimilarity, + GraderPython, + GraderScoreModel, + GraderLabelModel + ], { mode: "oneOf" }), + "calculate_output": Schema.String.annotate({ + "description": "A formula to calculate the output based on grader results." + }) + }).annotate({ "title": "MultiGrader", "description": "The grader used for the fine-tuning job." }) + ], { mode: "oneOf" }), + "hyperparameters": Schema.optionalKey(FineTuneReinforcementHyperparameters) +}).annotate({ "description": "Configuration for the reinforcement fine-tuning method." }) +export type RunGraderRequest = { + readonly "grader": + | { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" + } + | { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" + } + | { readonly "type": "python"; readonly "name": string; readonly "source": string; readonly "image_tag"?: string } + | { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray + } + | { + readonly "type": "multi" + readonly "name": string + readonly "graders": GraderStringCheck | GraderTextSimilarity | GraderPython | GraderScoreModel | GraderLabelModel + readonly "calculate_output": string + } + readonly "item"?: {} + readonly "model_sample": string +} +export const RunGraderRequest = Schema.Struct({ + "grader": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) + }).annotate({ "title": "StringCheckGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }) + }).annotate({ "title": "TextSimilarityGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ) + }).annotate({ "title": "PythonGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ + "description": "The object type, which is always `score_model`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ) + }).annotate({ "title": "ScoreModelGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("multi").annotate({ "description": "The object type, which is always `multi`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "graders": Schema.Union([ + GraderStringCheck, + GraderTextSimilarity, + GraderPython, + GraderScoreModel, + GraderLabelModel + ], { mode: "oneOf" }), + "calculate_output": Schema.String.annotate({ + "description": "A formula to calculate the output based on grader results." + }) + }).annotate({ "title": "MultiGrader", "description": "The grader used for the fine-tuning job." }) + ], { mode: "oneOf" }), + "item": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "The dataset item provided to the grader. This will be used to populate \nthe `item` namespace. See [the guide](/docs/guides/graders) for more details. \n" + }) + ), + "model_sample": Schema.String.annotate({ + "description": + "The model sample to be evaluated. This value will be used to populate \nthe `sample` namespace. See [the guide](/docs/guides/graders) for more details.\nThe `output_json` variable will be populated if the model sample is a \nvalid JSON string.\n \n" + }) +}).annotate({ "title": "RunGraderRequest" }) +export type ValidateGraderRequest = { + readonly "grader": + | { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" + } + | { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" + } + | { readonly "type": "python"; readonly "name": string; readonly "source": string; readonly "image_tag"?: string } + | { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray + } + | { + readonly "type": "multi" + readonly "name": string + readonly "graders": GraderStringCheck | GraderTextSimilarity | GraderPython | GraderScoreModel | GraderLabelModel + readonly "calculate_output": string + } +} +export const ValidateGraderRequest = Schema.Struct({ + "grader": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) + }).annotate({ "title": "StringCheckGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }) + }).annotate({ "title": "TextSimilarityGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ) + }).annotate({ "title": "PythonGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ + "description": "The object type, which is always `score_model`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ) + }).annotate({ "title": "ScoreModelGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("multi").annotate({ "description": "The object type, which is always `multi`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "graders": Schema.Union([ + GraderStringCheck, + GraderTextSimilarity, + GraderPython, + GraderScoreModel, + GraderLabelModel + ], { mode: "oneOf" }), + "calculate_output": Schema.String.annotate({ + "description": "A formula to calculate the output based on grader results." + }) + }).annotate({ "title": "MultiGrader", "description": "The grader used for the fine-tuning job." }) + ], { mode: "oneOf" }) +}).annotate({ "title": "ValidateGraderRequest" }) +export type ValidateGraderResponse = { + readonly "grader"?: + | { + readonly "type": "string_check" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "operation": "eq" | "ne" | "like" | "ilike" + } + | { + readonly "type": "text_similarity" + readonly "name": string + readonly "input": string + readonly "reference": string + readonly "evaluation_metric": + | "cosine" + | "fuzzy_match" + | "bleu" + | "gleu" + | "meteor" + | "rouge_1" + | "rouge_2" + | "rouge_3" + | "rouge_4" + | "rouge_5" + | "rouge_l" + } + | { readonly "type": "python"; readonly "name": string; readonly "source": string; readonly "image_tag"?: string } + | { + readonly "type": "score_model" + readonly "name": string + readonly "model": string + readonly "sampling_params"?: { + readonly "seed"?: number | null + readonly "top_p"?: number | null + readonly "temperature"?: number | null + readonly "max_completions_tokens"?: number | null + readonly "reasoning_effort"?: ReasoningEffort + } + readonly "input": ReadonlyArray + readonly "range"?: ReadonlyArray + } + | { + readonly "type": "multi" + readonly "name": string + readonly "graders": GraderStringCheck | GraderTextSimilarity | GraderPython | GraderScoreModel | GraderLabelModel + readonly "calculate_output": string + } +} +export const ValidateGraderResponse = Schema.Struct({ + "grader": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("string_check").annotate({ + "description": "The object type, which is always `string_check`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The input text. This may include template strings." }), + "reference": Schema.String.annotate({ "description": "The reference text. This may include template strings." }), + "operation": Schema.Literals(["eq", "ne", "like", "ilike"]).annotate({ + "description": "The string check operation to perform. One of `eq`, `ne`, `like`, or `ilike`." + }) + }).annotate({ "title": "StringCheckGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("text_similarity").annotate({ "description": "The type of grader." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "input": Schema.String.annotate({ "description": "The text being graded." }), + "reference": Schema.String.annotate({ "description": "The text being graded against." }), + "evaluation_metric": Schema.Literals([ + "cosine", + "fuzzy_match", + "bleu", + "gleu", + "meteor", + "rouge_1", + "rouge_2", + "rouge_3", + "rouge_4", + "rouge_5", + "rouge_l" + ]).annotate({ + "description": + "The evaluation metric to use. One of `cosine`, `fuzzy_match`, `bleu`, \n`gleu`, `meteor`, `rouge_1`, `rouge_2`, `rouge_3`, `rouge_4`, `rouge_5`, \nor `rouge_l`.\n" + }) + }).annotate({ "title": "TextSimilarityGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("python").annotate({ "description": "The object type, which is always `python`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "source": Schema.String.annotate({ "description": "The source code of the python script." }), + "image_tag": Schema.optionalKey( + Schema.String.annotate({ "description": "The image tag to use for the python script." }) + ) + }).annotate({ "title": "PythonGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("score_model").annotate({ + "description": "The object type, which is always `score_model`." + }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "model": Schema.String.annotate({ "description": "The model to use for the evaluation." }), + "sampling_params": Schema.optionalKey( + Schema.Struct({ + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A seed value to initialize the randomness, during sampling.\n" }) + .check(Schema.isInt()), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "An alternative to temperature for nucleus sampling; 1.0 includes all tokens.\n" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ "description": "A higher temperature increases randomness in the outputs.\n" }) + .check(Schema.isFinite()), + Schema.Null + ]) + ), + "max_completions_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": "The maximum number of tokens the grader model may generate in its response.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null + ]) + ), + "reasoning_effort": Schema.optionalKey(ReasoningEffort) + }).annotate({ "description": "The sampling parameters for the model." }) + ), + "input": Schema.Array(EvalItem).annotate({ + "description": + "The input messages evaluated by the grader. Supports text, output text, input image, and input audio content blocks, and may include template strings.\n" + }), + "range": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isFinite())).annotate({ + "description": "The range of the score. Defaults to `[0, 1]`." + }) + ) + }).annotate({ "title": "ScoreModelGrader", "description": "The grader used for the fine-tuning job." }), + Schema.Struct({ + "type": Schema.Literal("multi").annotate({ "description": "The object type, which is always `multi`." }), + "name": Schema.String.annotate({ "description": "The name of the grader." }), + "graders": Schema.Union([ + GraderStringCheck, + GraderTextSimilarity, + GraderPython, + GraderScoreModel, + GraderLabelModel + ], { mode: "oneOf" }), + "calculate_output": Schema.String.annotate({ + "description": "A formula to calculate the output based on grader results." + }) + }).annotate({ "title": "MultiGrader", "description": "The grader used for the fine-tuning job." }) + ], { mode: "oneOf" })) +}).annotate({ "title": "ValidateGraderResponse" }) +export type CreateResponse = { + readonly "metadata"?: Metadata + readonly "top_logprobs"?: number + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model"?: + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools"?: ToolsArray + readonly "tool_choice"?: ToolChoiceParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "input"?: InputParam + readonly "include"?: ReadonlyArray | null + readonly "parallel_tool_calls"?: boolean | null + readonly "store"?: boolean | null + readonly "instructions"?: string | null + readonly "stream"?: boolean | null + readonly "stream_options"?: ResponseStreamOptions + readonly "conversation"?: ConversationParam | null + readonly "context_management"?: ReadonlyArray | null + readonly "max_output_tokens"?: number | null +} +export const CreateResponse = Schema.Struct({ + "metadata": Schema.optionalKey(Metadata), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(20)], { + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }) + ) + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }) + ), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.optionalKey(ToolsArray), + "tool_choice": Schema.optionalKey(ToolChoiceParam), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "input": Schema.optionalKey(InputParam), + "include": Schema.optionalKey(Schema.Union([ + Schema.Array(IncludeEnum).annotate({ + "description": + "Specify additional output data to include in the model response. Currently supported values are:\n- `web_search_call.action.sources`: Include the sources of the web search tool call.\n- `code_interpreter_call.outputs`: Includes the outputs of python code execution in code interpreter tool call items.\n- `computer_call_output.output.image_url`: Include image urls from the computer call output.\n- `file_search_call.results`: Include the search results of the file search tool call.\n- `message.input_image.image_url`: Include image urls from the input message.\n- `message.output_text.logprobs`: Include logprobs with assistant messages.\n- `reasoning.encrypted_content`: Includes an encrypted version of reasoning tokens in reasoning item outputs. This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly (like when the `store` parameter is set to `false`, or when an organization is enrolled in the zero data retention program)." + }), + Schema.Null + ])), + "parallel_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ "description": "Whether to allow the model to run tool calls in parallel.\n" }), + Schema.Null + ]) + ), + "store": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "Whether to store the generated model response for later retrieval via\nAPI.\n" + }), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey(Schema.Union([ + Schema.String.annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ])), + "stream": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "If set to true, the model response data will be streamed to the client\nas it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).\nSee the [Streaming section below](/docs/api-reference/responses-streaming)\nfor more information.\n" + }), + Schema.Null + ])), + "stream_options": Schema.optionalKey(ResponseStreamOptions), + "conversation": Schema.optionalKey(Schema.Union([ConversationParam, Schema.Null])), + "context_management": Schema.optionalKey( + Schema.Union([ + Schema.Array(ContextManagementParam).annotate({ + "description": "Context management configuration for this request.\n" + }).check(Schema.isMinLength(1)), + Schema.Null + ]) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(16)), + Schema.Null + ]) + ) +}) +export type ResponsesClientEventResponseCreate = { + readonly "type": "response.create" + readonly "metadata"?: Metadata + readonly "top_logprobs"?: number + readonly "temperature"?: number | null + readonly "top_p"?: number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model"?: + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools"?: ToolsArray + readonly "tool_choice"?: ToolChoiceParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "input"?: InputParam + readonly "include"?: ReadonlyArray | null + readonly "parallel_tool_calls"?: boolean | null + readonly "store"?: boolean | null + readonly "instructions"?: string | null + readonly "stream"?: boolean | null + readonly "stream_options"?: ResponseStreamOptions + readonly "conversation"?: ConversationParam | null + readonly "context_management"?: ReadonlyArray | null + readonly "max_output_tokens"?: number | null +} +export const ResponsesClientEventResponseCreate = Schema.Struct({ + "type": Schema.Literal("response.create").annotate({ + "description": "The type of the client event. Always `response.create`.\n" + }), + "metadata": Schema.optionalKey(Metadata), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)).check( + Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(20)], { + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }) + ) + ]) + ), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]) + ), + "top_p": Schema.optionalKey(Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ])), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.optionalKey( + Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }) + ), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.optionalKey(ToolsArray), + "tool_choice": Schema.optionalKey(ToolChoiceParam), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "input": Schema.optionalKey(InputParam), + "include": Schema.optionalKey(Schema.Union([ + Schema.Array(IncludeEnum).annotate({ + "description": + "Specify additional output data to include in the model response. Currently supported values are:\n- `web_search_call.action.sources`: Include the sources of the web search tool call.\n- `code_interpreter_call.outputs`: Includes the outputs of python code execution in code interpreter tool call items.\n- `computer_call_output.output.image_url`: Include image urls from the computer call output.\n- `file_search_call.results`: Include the search results of the file search tool call.\n- `message.input_image.image_url`: Include image urls from the input message.\n- `message.output_text.logprobs`: Include logprobs with assistant messages.\n- `reasoning.encrypted_content`: Includes an encrypted version of reasoning tokens in reasoning item outputs. This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly (like when the `store` parameter is set to `false`, or when an organization is enrolled in the zero data retention program)." + }), + Schema.Null + ])), + "parallel_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ "description": "Whether to allow the model to run tool calls in parallel.\n" }), + Schema.Null + ]) + ), + "store": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "Whether to store the generated model response for later retrieval via\nAPI.\n" + }), + Schema.Null + ]) + ), + "instructions": Schema.optionalKey(Schema.Union([ + Schema.String.annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ])), + "stream": Schema.optionalKey(Schema.Union([ + Schema.Boolean.annotate({ + "description": + "If set to true, the model response data will be streamed to the client\nas it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).\nSee the [Streaming section below](/docs/api-reference/responses-streaming)\nfor more information.\n" + }), + Schema.Null + ])), + "stream_options": Schema.optionalKey(ResponseStreamOptions), + "conversation": Schema.optionalKey(Schema.Union([ConversationParam, Schema.Null])), + "context_management": Schema.optionalKey( + Schema.Union([ + Schema.Array(ContextManagementParam).annotate({ + "description": "Context management configuration for this request.\n" + }).check(Schema.isMinLength(1)), + Schema.Null + ]) + ), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(16)), + Schema.Null + ]) + ) +}).annotate({ + "description": + "Client event for creating a response over a persistent WebSocket connection.\nThis payload uses the same top-level fields as `POST /v1/responses`.\n\nNotes:\n- `stream` is implicit over WebSocket and should not be sent.\n- `background` is not supported over WebSocket.\n" +}) +export type ConversationItemList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "first_id": string + readonly "last_id": string +} +export const ConversationItemList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(ConversationItem).annotate({ "description": "A list of conversation items." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }), + "first_id": Schema.String.annotate({ "description": "The ID of the first item in the list." }), + "last_id": Schema.String.annotate({ "description": "The ID of the last item in the list." }) +}).annotate({ "title": "The conversation item list", "description": "A list of Conversation items." }) +export type ResponseItemList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "first_id": string + readonly "last_id": string +} +export const ResponseItemList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ "description": "The type of object returned, must be `list`." }), + "data": Schema.Array(ItemResource).annotate({ "description": "A list of items used to generate this response." }), + "has_more": Schema.Boolean.annotate({ "description": "Whether there are more items available." }), + "first_id": Schema.String.annotate({ "description": "The ID of the first item in the list." }), + "last_id": Schema.String.annotate({ "description": "The ID of the last item in the list." }) +}).annotate({ "description": "A list of Response items." }) +export type Response = { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null +} +export const Response = Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) +}).annotate({ "title": "The response object" }) +export type ResponseCompletedEvent = { + readonly "type": "response.completed" + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } + readonly "sequence_number": number +} +export const ResponseCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.completed").annotate({ + "description": "The type of the event. Always `response.completed`.\n" + }), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "Properties of the completed response.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number for this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when the model response is complete." }) +export type ResponseCreatedEvent = { + readonly "type": "response.created" + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } + readonly "sequence_number": number +} +export const ResponseCreatedEvent = Schema.Struct({ + "type": Schema.Literal("response.created").annotate({ + "description": "The type of the event. Always `response.created`.\n" + }), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "The response that was created.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number for this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "An event that is emitted when a response is created.\n" }) +export type ResponseFailedEvent = { + readonly "type": "response.failed" + readonly "sequence_number": number + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } +} +export const ResponseFailedEvent = Schema.Struct({ + "type": Schema.Literal("response.failed").annotate({ + "description": "The type of the event. Always `response.failed`.\n" + }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "The response that failed.\n" }) +}).annotate({ "description": "An event that is emitted when a response fails.\n" }) +export type ResponseInProgressEvent = { + readonly "type": "response.in_progress" + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } + readonly "sequence_number": number +} +export const ResponseInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.in_progress").annotate({ + "description": "The type of the event. Always `response.in_progress`.\n" + }), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "The response that is in progress.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "Emitted when the response is in progress." }) +export type ResponseIncompleteEvent = { + readonly "type": "response.incomplete" + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } + readonly "sequence_number": number +} +export const ResponseIncompleteEvent = Schema.Struct({ + "type": Schema.Literal("response.incomplete").annotate({ + "description": "The type of the event. Always `response.incomplete`.\n" + }), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "The response that was incomplete.\n" }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number of this event." }).check( + Schema.isInt() + ) +}).annotate({ "description": "An event that is emitted when a response finishes as incomplete.\n" }) +export type ResponseQueuedEvent = { + readonly "type": "response.queued" + readonly "response": { + readonly "metadata": {} | null + readonly "top_logprobs"?: number | null + readonly "temperature": number | null + readonly "top_p": number | null + readonly "user"?: string | null + readonly "safety_identifier"?: string + readonly "prompt_cache_key"?: string | null + readonly "service_tier"?: ServiceTier + readonly "prompt_cache_retention"?: "in_memory" | "in_memory" | "24h" | null + readonly "previous_response_id"?: string | null + readonly "model": + | ModelIdsShared + | "o1-pro" + | "o1-pro-2025-03-19" + | "o3-pro" + | "o3-pro-2025-06-10" + | "o3-deep-research" + | "o3-deep-research-2025-06-26" + | "o4-mini-deep-research" + | "o4-mini-deep-research-2025-06-26" + | "computer-use-preview" + | "computer-use-preview-2025-03-11" + | "gpt-5-codex" + | "gpt-5-pro" + | "gpt-5-pro-2025-10-06" + | "gpt-5.1-codex-max" + readonly "reasoning"?: Reasoning | null + readonly "background"?: boolean | null + readonly "max_tool_calls"?: number | null + readonly "text"?: ResponseTextParam + readonly "tools": ReadonlyArray + readonly "tool_choice": + | ToolChoiceOptions + | ToolChoiceAllowed + | ToolChoiceTypes + | ToolChoiceFunction + | ToolChoiceMCP + | ToolChoiceCustom + | SpecificApplyPatchParam + | SpecificFunctionShellParam + readonly "prompt"?: Prompt + readonly "truncation"?: "auto" | "disabled" | null + readonly "id": string + readonly "object": "response" + readonly "status"?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" + readonly "created_at": number + readonly "completed_at"?: number | null + readonly "error": ResponseError + readonly "incomplete_details": { readonly "reason"?: "max_output_tokens" | "content_filter" } | null + readonly "output": ReadonlyArray + readonly "instructions": string | ReadonlyArray | null + readonly "output_text"?: string | null + readonly "usage"?: ResponseUsage | null + readonly "parallel_tool_calls": boolean + readonly "conversation"?: Conversation_2 | null + readonly "max_output_tokens"?: number | null + } + readonly "sequence_number": number +} +export const ResponseQueuedEvent = Schema.Struct({ + "type": Schema.Literal("response.queued").annotate({ + "description": "The type of the event. Always 'response.queued'." + }), + "response": Schema.Struct({ + "metadata": Schema.Union([ + Schema.Struct({}).annotate({ + "description": + "Set of 16 key-value pairs that can be attached to an object. This can be\nuseful for storing additional information about the object in a structured\nformat, and querying for objects via API or the dashboard.\n\nKeys are strings with a maximum length of 64 characters. Values are strings\nwith a maximum length of 512 characters.\n" + }), + Schema.Null + ]), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An integer between 0 and 20 specifying the maximum number of most likely\ntokens to return at each token position, each with an associated log\nprobability. In some cases, the number of returned tokens may be fewer than\nrequested.\n" + }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)), + Schema.Null + ]) + ), + "temperature": Schema.Union([ + Schema.Number.annotate({ + "description": + "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or `top_p` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)), + Schema.Null + ]), + "top_p": Schema.Union([ + Schema.Number.annotate({ + "description": + "An alternative to sampling with temperature, called nucleus sampling,\nwhere the model considers the results of the tokens with top_p probability\nmass. So 0.1 means only the tokens comprising the top 10% probability mass\nare considered.\n\nWe generally recommend altering this or `temperature` but not both.\n" + }).check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)), + Schema.Null + ]), + "user": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "This field is being replaced by `safety_identifier` and `prompt_cache_key`. Use `prompt_cache_key` instead to maintain caching optimizations.\nA stable identifier for your end-users.\nUsed to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }) + ), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String.check(Schema.isMaxLength(64, { + "description": + "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.\nThe IDs should be a string that uniquely identifies each user, with a maximum length of 64 characters. We recommend hashing their username or email address, in order to avoid sending us any identifying information. [Learn more](/docs/guides/safety-best-practices#safety-identifiers).\n" + }))])), + "prompt_cache_key": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the `user` field. [Learn more](/docs/guides/prompt-caching).\n" + }) + ), + "service_tier": Schema.optionalKey(ServiceTier), + "prompt_cache_retention": Schema.optionalKey( + Schema.Union([ + Schema.Literals(["in_memory", "in_memory", "24h"]).annotate({ + "description": + "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](/docs/guides/prompt-caching#prompt-cache-retention).\n" + }), + Schema.Null + ]) + ), + "previous_response_id": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about\n[conversation state](/docs/guides/conversation-state). Cannot be used in conjunction with `conversation`.\n" + }), + Schema.Null + ]) + ), + "model": Schema.Union([ + ModelIdsShared, + Schema.Literals([ + "o1-pro", + "o1-pro-2025-03-19", + "o3-pro", + "o3-pro-2025-06-10", + "o3-deep-research", + "o3-deep-research-2025-06-26", + "o4-mini-deep-research", + "o4-mini-deep-research-2025-06-26", + "computer-use-preview", + "computer-use-preview-2025-03-11", + "gpt-5-codex", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5.1-codex-max" + ]).annotate({ "title": "ResponsesOnlyModel" }) + ]).annotate({ + "description": + "Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n" + }), + "reasoning": Schema.optionalKey(Schema.Union([Reasoning, Schema.Null])), + "background": Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + "description": + "Whether to run the model response in the background.\n[Learn more](/docs/guides/background).\n" + }), + Schema.Null + ]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "text": Schema.optionalKey(ResponseTextParam), + "tools": Schema.Array(Tool).annotate({ + "description": + "An array of tools the model may call while generating a response. You\ncan specify which tool to use by setting the `tool_choice` parameter.\n\nWe support the following categories of tools:\n- **Built-in tools**: Tools that are provided by OpenAI that extend the\n model's capabilities, like [web search](/docs/guides/tools-web-search)\n or [file search](/docs/guides/tools-file-search). Learn more about\n [built-in tools](/docs/guides/tools).\n- **MCP Tools**: Integrations with third-party systems via custom MCP servers\n or predefined connectors such as Google Drive and SharePoint. Learn more about\n [MCP Tools](/docs/guides/tools-connectors-mcp).\n- **Function calls (custom tools)**: Functions that are defined by you,\n enabling the model to call your own code with strongly typed arguments\n and outputs. Learn more about\n [function calling](/docs/guides/function-calling). You can also use\n custom tools to call your own code.\n" + }), + "tool_choice": Schema.Union([ + ToolChoiceOptions, + ToolChoiceAllowed, + ToolChoiceTypes, + ToolChoiceFunction, + ToolChoiceMCP, + ToolChoiceCustom, + SpecificApplyPatchParam, + SpecificFunctionShellParam + ], { mode: "oneOf" }).annotate({ + "description": + "How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n" + }), + "prompt": Schema.optionalKey(Prompt), + "truncation": Schema.optionalKey(Schema.Union([ + Schema.Literals(["auto", "disabled"]).annotate({ + "description": + "The truncation strategy to use for the model response.\n- `auto`: If the input to this Response exceeds\n the model's context window size, the model will truncate the\n response to fit the context window by dropping items from the beginning of the conversation.\n- `disabled` (default): If the input size will exceed the context window\n size for a model, the request will fail with a 400 error.\n" + }), + Schema.Null + ])), + "id": Schema.String.annotate({ "description": "Unique identifier for this Response.\n" }), + "object": Schema.Literal("response").annotate({ + "description": "The object type of this resource - always set to `response`.\n" + }), + "status": Schema.optionalKey( + Schema.Literals(["completed", "failed", "in_progress", "cancelled", "queued", "incomplete"]).annotate({ + "description": + "The status of the response generation. One of `completed`, `failed`,\n`in_progress`, `cancelled`, `queued`, or `incomplete`.\n" + }) + ), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) of when this Response was created.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + "completed_at": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "Unix timestamp (in seconds) of when this Response was completed.\nOnly present when the status is `completed`.\n", + "format": "unixtime" + }).check(Schema.isFinite()), + Schema.Null + ]) + ), + "error": ResponseError, + "incomplete_details": Schema.Union([ + Schema.Struct({ + "reason": Schema.optionalKey( + Schema.Literals(["max_output_tokens", "content_filter"]).annotate({ + "description": "The reason why the response is incomplete." + }) + ) + }).annotate({ "description": "Details about why the response is incomplete.\n" }), + Schema.Null + ]), + "output": Schema.Array(OutputItem).annotate({ + "description": + "An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and\n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n" + }), + "instructions": Schema.Union([ + Schema.Union([ + Schema.String.annotate({ + "description": "A text input to the model, equivalent to a text input with the\n`developer` role.\n" + }), + Schema.Array(InputItem).annotate({ + "title": "Input item list", + "description": "A list of one or many input items to the model, containing\ndifferent content types.\n" + }) + ], { mode: "oneOf" }).annotate({ + "description": + "A system (or developer) message inserted into the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n" + }), + Schema.Null + ]), + "output_text": Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + "description": + "SDK-only convenience property that contains the aggregated text output\nfrom all `output_text` items in the `output` array, if any are present.\nSupported in the Python and JavaScript SDKs.\n" + }), + Schema.Null + ]) + ), + "usage": Schema.optionalKey(Schema.Union([ResponseUsage, Schema.Null])), + "parallel_tool_calls": Schema.Boolean.annotate({ + "description": "Whether to allow the model to run tool calls in parallel.\n" + }), + "conversation": Schema.optionalKey(Schema.Union([Conversation_2, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n" + }).check(Schema.isInt()), + Schema.Null + ]) + ) + }).annotate({ "title": "The response object", "description": "The full response object that is queued." }), + "sequence_number": Schema.Number.annotate({ "description": "The sequence number for this event." }).check( + Schema.isInt() + ) +}).annotate({ + "title": "ResponseQueuedEvent", + "description": "Emitted when a response is queued and waiting to be processed.\n" +}) +export type CompactResource = { + readonly "id": string + readonly "object": "response.compaction" + readonly "output": ReadonlyArray + readonly "created_at": number + readonly "usage": { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number + } +} +export const CompactResource = Schema.Struct({ + "id": Schema.String.annotate({ "description": "The unique identifier for the compacted response." }), + "object": Schema.Literal("response.compaction").annotate({ + "description": "The object type. Always `response.compaction`." + }), + "output": Schema.Array(ItemField).annotate({ "description": "The compacted list of output items." }), + "created_at": Schema.Number.annotate({ + "description": "Unix timestamp (in seconds) when the compacted conversation was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "usage": Schema.Struct({ + "input_tokens": Schema.Number.annotate({ "description": "The number of input tokens." }).check(Schema.isInt()), + "input_tokens_details": Schema.Struct({ + "cached_tokens": Schema.Number.annotate({ + "description": + "The number of tokens that were retrieved from the cache. \n[More on prompt caching](/docs/guides/prompt-caching).\n" + }).check(Schema.isInt()) + }).annotate({ "description": "A detailed breakdown of the input tokens." }), + "output_tokens": Schema.Number.annotate({ "description": "The number of output tokens." }).check(Schema.isInt()), + "output_tokens_details": Schema.Struct({ + "reasoning_tokens": Schema.Number.annotate({ "description": "The number of reasoning tokens." }).check( + Schema.isInt() + ) + }).annotate({ "description": "A detailed breakdown of the output tokens." }), + "total_tokens": Schema.Number.annotate({ "description": "The total number of tokens used." }).check(Schema.isInt()) + }).annotate({ + "description": "Token accounting for the compaction pass, including cached, reasoning, and total tokens." + }) +}).annotate({ "title": "The compacted response object" }) +export type EvalList = { + readonly "object": "list" + readonly "data": ReadonlyArray + readonly "first_id": string + readonly "last_id": string + readonly "has_more": boolean +} +export const EvalList = Schema.Struct({ + "object": Schema.Literal("list").annotate({ + "description": "The type of this object. It is always set to \"list\".\n" + }), + "data": Schema.Array(Eval).annotate({ "description": "An array of eval objects.\n" }), + "first_id": Schema.String.annotate({ "description": "The identifier of the first eval in the data array." }), + "last_id": Schema.String.annotate({ "description": "The identifier of the last eval in the data array." }), + "has_more": Schema.Boolean.annotate({ "description": "Indicates whether there are more evals available." }) +}).annotate({ "title": "EvalList", "description": "An object representing a list of evals.\n" }) +export type FineTuneMethod = { + readonly "type": "supervised" | "dpo" | "reinforcement" + readonly "supervised"?: FineTuneSupervisedMethod + readonly "dpo"?: FineTuneDPOMethod + readonly "reinforcement"?: FineTuneReinforcementMethod +} +export const FineTuneMethod = Schema.Struct({ + "type": Schema.Literals(["supervised", "dpo", "reinforcement"]).annotate({ + "description": "The type of method. Is either `supervised`, `dpo`, or `reinforcement`." + }), + "supervised": Schema.optionalKey(FineTuneSupervisedMethod), + "dpo": Schema.optionalKey(FineTuneDPOMethod), + "reinforcement": Schema.optionalKey(FineTuneReinforcementMethod) +}).annotate({ "description": "The method used for fine-tuning." }) +export type ResponseStreamEvent = + | ResponseAudioDeltaEvent + | ResponseAudioDoneEvent + | ResponseAudioTranscriptDeltaEvent + | ResponseAudioTranscriptDoneEvent + | ResponseCodeInterpreterCallCodeDeltaEvent + | ResponseCodeInterpreterCallCodeDoneEvent + | ResponseCodeInterpreterCallCompletedEvent + | ResponseCodeInterpreterCallInProgressEvent + | ResponseCodeInterpreterCallInterpretingEvent + | ResponseCompletedEvent + | ResponseContentPartAddedEvent + | ResponseContentPartDoneEvent + | ResponseCreatedEvent + | ResponseErrorEvent + | ResponseFileSearchCallCompletedEvent + | ResponseFileSearchCallInProgressEvent + | ResponseFileSearchCallSearchingEvent + | ResponseFunctionCallArgumentsDeltaEvent + | ResponseFunctionCallArgumentsDoneEvent + | ResponseInProgressEvent + | ResponseFailedEvent + | ResponseIncompleteEvent + | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent + | ResponseReasoningSummaryPartAddedEvent + | ResponseReasoningSummaryPartDoneEvent + | ResponseReasoningSummaryTextDeltaEvent + | ResponseReasoningSummaryTextDoneEvent + | ResponseReasoningTextDeltaEvent + | ResponseReasoningTextDoneEvent + | ResponseRefusalDeltaEvent + | ResponseRefusalDoneEvent + | ResponseTextDeltaEvent + | ResponseTextDoneEvent + | ResponseWebSearchCallCompletedEvent + | ResponseWebSearchCallInProgressEvent + | ResponseWebSearchCallSearchingEvent + | ResponseImageGenCallCompletedEvent + | ResponseImageGenCallGeneratingEvent + | ResponseImageGenCallInProgressEvent + | ResponseImageGenCallPartialImageEvent + | ResponseMCPCallArgumentsDeltaEvent + | ResponseMCPCallArgumentsDoneEvent + | ResponseMCPCallCompletedEvent + | ResponseMCPCallFailedEvent + | ResponseMCPCallInProgressEvent + | ResponseMCPListToolsCompletedEvent + | ResponseMCPListToolsFailedEvent + | ResponseMCPListToolsInProgressEvent + | ResponseOutputTextAnnotationAddedEvent + | ResponseQueuedEvent + | ResponseCustomToolCallInputDeltaEvent + | ResponseCustomToolCallInputDoneEvent + | ResponseKeepAliveEvent + | ResponseApplyPatchCallOperationDiffDeltaEvent + | ResponseApplyPatchCallOperationDiffDoneEvent +export const ResponseStreamEvent = Schema.Union([ + ResponseAudioDeltaEvent, + ResponseAudioDoneEvent, + ResponseAudioTranscriptDeltaEvent, + ResponseAudioTranscriptDoneEvent, + ResponseCodeInterpreterCallCodeDeltaEvent, + ResponseCodeInterpreterCallCodeDoneEvent, + ResponseCodeInterpreterCallCompletedEvent, + ResponseCodeInterpreterCallInProgressEvent, + ResponseCodeInterpreterCallInterpretingEvent, + ResponseCompletedEvent, + ResponseContentPartAddedEvent, + ResponseContentPartDoneEvent, + ResponseCreatedEvent, + ResponseErrorEvent, + ResponseFileSearchCallCompletedEvent, + ResponseFileSearchCallInProgressEvent, + ResponseFileSearchCallSearchingEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, + ResponseInProgressEvent, + ResponseFailedEvent, + ResponseIncompleteEvent, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseReasoningSummaryPartAddedEvent, + ResponseReasoningSummaryPartDoneEvent, + ResponseReasoningSummaryTextDeltaEvent, + ResponseReasoningSummaryTextDoneEvent, + ResponseReasoningTextDeltaEvent, + ResponseReasoningTextDoneEvent, + ResponseRefusalDeltaEvent, + ResponseRefusalDoneEvent, + ResponseTextDeltaEvent, + ResponseTextDoneEvent, + ResponseWebSearchCallCompletedEvent, + ResponseWebSearchCallInProgressEvent, + ResponseWebSearchCallSearchingEvent, + ResponseImageGenCallCompletedEvent, + ResponseImageGenCallGeneratingEvent, + ResponseImageGenCallInProgressEvent, + ResponseImageGenCallPartialImageEvent, + ResponseMCPCallArgumentsDeltaEvent, + ResponseMCPCallArgumentsDoneEvent, + ResponseMCPCallCompletedEvent, + ResponseMCPCallFailedEvent, + ResponseMCPCallInProgressEvent, + ResponseMCPListToolsCompletedEvent, + ResponseMCPListToolsFailedEvent, + ResponseMCPListToolsInProgressEvent, + ResponseOutputTextAnnotationAddedEvent, + ResponseQueuedEvent, + ResponseCustomToolCallInputDeltaEvent, + ResponseCustomToolCallInputDoneEvent, + ResponseKeepAliveEvent, + ResponseApplyPatchCallOperationDiffDeltaEvent, + ResponseApplyPatchCallOperationDiffDoneEvent +]) +export type CreateFineTuningJobRequest = { + readonly "model": string | "babbage-002" | "davinci-002" | "gpt-3.5-turbo" | "gpt-4o-mini" + readonly "training_file": string + readonly "hyperparameters"?: { + readonly "batch_size"?: "auto" | number + readonly "learning_rate_multiplier"?: "auto" | number + readonly "n_epochs"?: "auto" | number + } + readonly "suffix"?: string + readonly "validation_file"?: string | null + readonly "integrations"?: ReadonlyArray< + { + readonly "type": "wandb" + readonly "wandb": { + readonly "project": string + readonly "name"?: string | null + readonly "entity"?: string | null + readonly "tags"?: ReadonlyArray + } + } + > + readonly "seed"?: number + readonly "method"?: FineTuneMethod + readonly "metadata"?: Metadata +} +export const CreateFineTuningJobRequest = Schema.Struct({ + "model": Schema.Union([ + Schema.String, + Schema.Literals(["babbage-002", "davinci-002", "gpt-3.5-turbo", "gpt-4o-mini"]) + ]).annotate({ + "description": + "The name of the model to fine-tune. You can select one of the\n[supported models](/docs/guides/fine-tuning#which-models-can-be-fine-tuned).\n" + }), + "training_file": Schema.String.annotate({ + "description": + "The ID of an uploaded file that contains training data.\n\nSee [upload file](/docs/api-reference/files/create) for how to upload a file.\n\nYour dataset must be formatted as a JSONL file. Additionally, you must upload your file with the purpose `fine-tune`.\n\nThe contents of the file should differ depending on if the model uses the [chat](/docs/api-reference/fine-tuning/chat-input), [completions](/docs/api-reference/fine-tuning/completions-input) format, or if the fine-tuning method uses the [preference](/docs/api-reference/fine-tuning/preference-input) format.\n\nSee the [fine-tuning guide](/docs/guides/model-optimization) for more details.\n" + }), + "hyperparameters": Schema.optionalKey( + Schema.Struct({ + "batch_size": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check( + Schema.isLessThanOrEqualTo(256) + ) + ], { mode: "oneOf" }).annotate({ + "description": + "Number of examples in each batch. A larger batch size means that model parameters\nare updated less frequently, but with lower variance.\n" + }) + ), + "learning_rate_multiplier": Schema.optionalKey( + Schema.Union([Schema.Literal("auto"), Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(0))], { + mode: "oneOf" + }).annotate({ + "description": + "Scaling factor for the learning rate. A smaller learning rate may be useful to avoid\noverfitting.\n" + }) + ), + "n_epochs": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check( + Schema.isLessThanOrEqualTo(50) + ) + ], { mode: "oneOf" }).annotate({ + "description": + "The number of epochs to train the model for. An epoch refers to one full cycle\nthrough the training dataset.\n" + }) + ) + }).annotate({ + "description": + "The hyperparameters used for the fine-tuning job.\nThis value is now deprecated in favor of `method`, and should be passed in under the `method` parameter.\n" + }) + ), + "suffix": Schema.optionalKey( + Schema.Union([ + Schema.String.check( + Schema.makeFilterGroup([Schema.isMinLength(1), Schema.isMaxLength(64)], { + "description": + "A string of up to 64 characters that will be added to your fine-tuned model name.\n\nFor example, a `suffix` of \"custom-model-name\" would produce a model name like `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`.\n" + }) + ) + ]) + ), + "validation_file": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The ID of an uploaded file that contains validation data.\n\nIf you provide this file, the data is used to generate validation\nmetrics periodically during fine-tuning. These metrics can be viewed in\nthe fine-tuning results file.\nThe same data should not be present in both train and validation files.\n\nYour dataset must be formatted as a JSONL file. You must upload your file with the purpose `fine-tune`.\n\nSee the [fine-tuning guide](/docs/guides/model-optimization) for more details.\n" + }) + ), + "integrations": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Struct({ + "type": Schema.Literal("wandb").annotate({ + "description": + "The type of integration to enable. Currently, only \"wandb\" (Weights and Biases) is supported.\n" + }), + "wandb": Schema.Struct({ + "project": Schema.String.annotate({ + "description": "The name of the project that the new run will be created under.\n" + }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A display name to set for the run. If not set, we will use the Job ID as the name.\n" + }) + ), + "entity": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The entity to use for the run. This allows you to set the team or username of the WandB user that you would\nlike associated with the run. If not set, the default entity for the registered WandB API key is used.\n" + }) + ), + "tags": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some\ndefault tags are generated by OpenAI: \"openai/finetune\", \"openai/{base-model}\", \"openai/{ftjob-abcdef}\".\n" + }) + ) + }).annotate({ + "description": + "The settings for your integration with Weights and Biases. This payload specifies the project that\nmetrics will be sent to. Optionally, you can set an explicit display name for your run, add tags\nto your run, and set a default entity (team, username, etc) to be associated with your run.\n" + }) + })).annotate({ "description": "A list of integrations to enable for your fine-tuning job." }) + ])), + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check( + Schema.makeFilterGroup([ + Schema.isFinite(), + Schema.isGreaterThanOrEqualTo(0), + Schema.isLessThanOrEqualTo(2147483647) + ], { + "description": + "The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases.\nIf a seed is not specified, one will be generated for you.\n" + }) + ) + ]) + ), + "method": Schema.optionalKey(FineTuneMethod), + "metadata": Schema.optionalKey(Metadata) +}) +export type FineTuningJob = { + readonly "id": string + readonly "created_at": number + readonly "error": { readonly "code": string; readonly "message": string; readonly "param": string | null } | null + readonly "fine_tuned_model": string | null + readonly "finished_at": number | null + readonly "hyperparameters": { + readonly "batch_size"?: "auto" | number | null + readonly "learning_rate_multiplier"?: "auto" | number + readonly "n_epochs"?: "auto" | number + } + readonly "model": string + readonly "object": "fine_tuning.job" + readonly "organization_id": string + readonly "result_files": ReadonlyArray + readonly "status": "validating_files" | "queued" | "running" | "succeeded" | "failed" | "cancelled" + readonly "trained_tokens": number | null + readonly "training_file": string + readonly "validation_file": string | null + readonly "integrations"?: ReadonlyArray | null + readonly "seed": number + readonly "estimated_finish"?: number | null + readonly "method"?: FineTuneMethod + readonly "metadata"?: Metadata +} +export const FineTuningJob = Schema.Struct({ + "id": Schema.String.annotate({ + "description": "The object identifier, which can be referenced in the API endpoints." + }), + "created_at": Schema.Number.annotate({ + "description": "The Unix timestamp (in seconds) for when the fine-tuning job was created.", + "format": "unixtime" + }).check(Schema.isInt()), + "error": Schema.Union([ + Schema.Struct({ + "code": Schema.String.annotate({ "description": "A machine-readable error code." }), + "message": Schema.String.annotate({ "description": "A human-readable error message." }), + "param": Schema.Union([ + Schema.String.annotate({ + "description": + "The parameter that was invalid, usually `training_file` or `validation_file`. This field will be null if the failure was not parameter-specific." + }), + Schema.Null + ]) + }).annotate({ + "description": + "For fine-tuning jobs that have `failed`, this will contain more information on the cause of the failure." + }), + Schema.Null + ]), + "fine_tuned_model": Schema.Union([ + Schema.String.annotate({ + "description": + "The name of the fine-tuned model that is being created. The value will be null if the fine-tuning job is still running." + }), + Schema.Null + ]), + "finished_at": Schema.Union([ + Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) for when the fine-tuning job was finished. The value will be null if the fine-tuning job is still running.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]), + "hyperparameters": Schema.Struct({ + "batch_size": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check( + Schema.isLessThanOrEqualTo(256) + ) + ], { mode: "oneOf" }).annotate({ + "description": + "Number of examples in each batch. A larger batch size means that model parameters\nare updated less frequently, but with lower variance.\n" + }), + Schema.Null + ]) + ), + "learning_rate_multiplier": Schema.optionalKey( + Schema.Union([Schema.Literal("auto"), Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(0))], { + mode: "oneOf" + }).annotate({ + "description": + "Scaling factor for the learning rate. A smaller learning rate may be useful to avoid\noverfitting.\n" + }) + ), + "n_epochs": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1)).check( + Schema.isLessThanOrEqualTo(50) + ) + ], { mode: "oneOf" }).annotate({ + "description": + "The number of epochs to train the model for. An epoch refers to one full cycle\nthrough the training dataset.\n" + }) + ) + }).annotate({ + "description": + "The hyperparameters used for the fine-tuning job. This value will only be returned when running `supervised` jobs." + }), + "model": Schema.String.annotate({ "description": "The base model that is being fine-tuned." }), + "object": Schema.Literal("fine_tuning.job").annotate({ + "description": "The object type, which is always \"fine_tuning.job\"." + }), + "organization_id": Schema.String.annotate({ "description": "The organization that owns the fine-tuning job." }), + "result_files": Schema.Array(Schema.String).annotate({ + "description": + "The compiled results file ID(s) for the fine-tuning job. You can retrieve the results with the [Files API](/docs/api-reference/files/retrieve-contents)." + }), + "status": Schema.Literals(["validating_files", "queued", "running", "succeeded", "failed", "cancelled"]).annotate({ + "description": + "The current status of the fine-tuning job, which can be either `validating_files`, `queued`, `running`, `succeeded`, `failed`, or `cancelled`." + }), + "trained_tokens": Schema.Union([ + Schema.Number.annotate({ + "description": + "The total number of billable tokens processed by this fine-tuning job. The value will be null if the fine-tuning job is still running." + }).check(Schema.isInt()), + Schema.Null + ]), + "training_file": Schema.String.annotate({ + "description": + "The file ID used for training. You can retrieve the training data with the [Files API](/docs/api-reference/files/retrieve-contents)." + }), + "validation_file": Schema.Union([ + Schema.String.annotate({ + "description": + "The file ID used for validation. You can retrieve the validation results with the [Files API](/docs/api-reference/files/retrieve-contents)." + }), + Schema.Null + ]), + "integrations": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Union([FineTuningIntegration], { mode: "oneOf" })).annotate({ + "description": "A list of integrations to enable for this fine-tuning job." + }).check(Schema.isMaxLength(5)), + Schema.Null + ]) + ), + "seed": Schema.Number.annotate({ "description": "The seed used for the fine-tuning job." }).check(Schema.isInt()), + "estimated_finish": Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + "description": + "The Unix timestamp (in seconds) for when the fine-tuning job is estimated to finish. The value will be null if the fine-tuning job is not running.", + "format": "unixtime" + }).check(Schema.isInt()), + Schema.Null + ]) + ), + "method": Schema.optionalKey(FineTuneMethod), + "metadata": Schema.optionalKey(Metadata) +}).annotate({ + "title": "FineTuningJob", + "description": "The `fine_tuning.job` object represents a fine-tuning job that has been created through the API.\n" +}) +export type ListPaginatedFineTuningJobsResponse = { + readonly "data": ReadonlyArray + readonly "has_more": boolean + readonly "object": "list" +} +export const ListPaginatedFineTuningJobsResponse = Schema.Struct({ + "data": Schema.Array(FineTuningJob), + "has_more": Schema.Boolean, + "object": Schema.Literal("list") +}) +// schemas +export type ListAssistantsParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string +} +export const ListAssistantsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String) +}) +export type ListAssistants200 = ListAssistantsResponse +export const ListAssistants200 = ListAssistantsResponse +export type CreateAssistantRequestJson = CreateAssistantRequest +export const CreateAssistantRequestJson = CreateAssistantRequest +export type CreateAssistant200 = AssistantObject +export const CreateAssistant200 = AssistantObject +export type GetAssistant200 = AssistantObject +export const GetAssistant200 = AssistantObject +export type ModifyAssistantRequestJson = ModifyAssistantRequest +export const ModifyAssistantRequestJson = ModifyAssistantRequest +export type ModifyAssistant200 = AssistantObject +export const ModifyAssistant200 = AssistantObject +export type DeleteAssistant200 = DeleteAssistantResponse +export const DeleteAssistant200 = DeleteAssistantResponse +export type CreateSpeechRequestJson = CreateSpeechRequest +export const CreateSpeechRequestJson = CreateSpeechRequest +export type CreateSpeech200Sse = CreateSpeechResponseStreamEvent +export const CreateSpeech200Sse = CreateSpeechResponseStreamEvent +export type CreateTranscriptionRequestFormData = CreateTranscriptionRequest +export const CreateTranscriptionRequestFormData = CreateTranscriptionRequest +export type CreateTranscription200 = + | CreateTranscriptionResponseJson + | CreateTranscriptionResponseDiarizedJson + | CreateTranscriptionResponseVerboseJson +export const CreateTranscription200 = Schema.Union([ + CreateTranscriptionResponseJson, + CreateTranscriptionResponseDiarizedJson, + CreateTranscriptionResponseVerboseJson +], { mode: "oneOf" }) +export type CreateTranscription200Sse = CreateTranscriptionResponseStreamEvent +export const CreateTranscription200Sse = CreateTranscriptionResponseStreamEvent +export type CreateTranslationRequestFormData = CreateTranslationRequest +export const CreateTranslationRequestFormData = CreateTranslationRequest +export type CreateTranslation200 = CreateTranslationResponseJson | CreateTranslationResponseVerboseJson +export const CreateTranslation200 = Schema.Union( + [CreateTranslationResponseJson, CreateTranslationResponseVerboseJson], + { mode: "oneOf" } +) +export type ListVoiceConsentsParams = { readonly "after"?: string; readonly "limit"?: number } +export const ListVoiceConsentsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())) +}) +export type ListVoiceConsents200 = VoiceConsentListResource +export const ListVoiceConsents200 = VoiceConsentListResource +export type CreateVoiceConsentRequestFormData = CreateVoiceConsentRequest +export const CreateVoiceConsentRequestFormData = CreateVoiceConsentRequest +export type CreateVoiceConsent200 = VoiceConsentResource +export const CreateVoiceConsent200 = VoiceConsentResource +export type GetVoiceConsent200 = VoiceConsentResource +export const GetVoiceConsent200 = VoiceConsentResource +export type UpdateVoiceConsentRequestJson = UpdateVoiceConsentRequest +export const UpdateVoiceConsentRequestJson = UpdateVoiceConsentRequest +export type UpdateVoiceConsent200 = VoiceConsentResource +export const UpdateVoiceConsent200 = VoiceConsentResource +export type DeleteVoiceConsent200 = VoiceConsentDeletedResource +export const DeleteVoiceConsent200 = VoiceConsentDeletedResource +export type CreateVoiceRequestFormData = CreateVoiceRequest +export const CreateVoiceRequestFormData = CreateVoiceRequest +export type CreateVoice200 = VoiceResource +export const CreateVoice200 = VoiceResource +export type ListBatchesParams = { readonly "after"?: string; readonly "limit"?: number } +export const ListBatchesParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())) +}) +export type ListBatches200 = ListBatchesResponse +export const ListBatches200 = ListBatchesResponse +export type CreateBatchRequestJson = { + readonly "input_file_id": string + readonly "endpoint": + | "/v1/responses" + | "/v1/chat/completions" + | "/v1/embeddings" + | "/v1/completions" + | "/v1/moderations" + | "/v1/images/generations" + | "/v1/images/edits" + | "/v1/videos" + readonly "completion_window": "24h" + readonly "metadata"?: Metadata + readonly "output_expires_after"?: BatchFileExpirationAfter +} +export const CreateBatchRequestJson = Schema.Struct({ + "input_file_id": Schema.String.annotate({ + "description": + "The ID of an uploaded file that contains requests for the new batch.\n\nSee [upload file](/docs/api-reference/files/create) for how to upload a file.\n\nYour input file must be formatted as a [JSONL file](/docs/api-reference/batch/request-input), and must be uploaded with the purpose `batch`. The file can contain up to 50,000 requests, and can be up to 200 MB in size.\n" + }), + "endpoint": Schema.Literals([ + "/v1/responses", + "/v1/chat/completions", + "/v1/embeddings", + "/v1/completions", + "/v1/moderations", + "/v1/images/generations", + "/v1/images/edits", + "/v1/videos" + ]).annotate({ + "description": + "The endpoint to be used for all requests in the batch. Currently `/v1/responses`, `/v1/chat/completions`, `/v1/embeddings`, `/v1/completions`, `/v1/moderations`, `/v1/images/generations`, `/v1/images/edits`, and `/v1/videos` are supported. Note that `/v1/embeddings` batches are also restricted to a maximum of 50,000 embedding inputs across all requests in the batch." + }), + "completion_window": Schema.Literal("24h").annotate({ + "description": "The time frame within which the batch should be processed. Currently only `24h` is supported." + }), + "metadata": Schema.optionalKey(Metadata), + "output_expires_after": Schema.optionalKey(BatchFileExpirationAfter) +}) +export type CreateBatch200 = Batch +export const CreateBatch200 = Batch +export type RetrieveBatch200 = Batch +export const RetrieveBatch200 = Batch +export type CancelBatch200 = Batch +export const CancelBatch200 = Batch +export type ListChatCompletionsParams = { + readonly "model"?: string + readonly "metadata"?: Metadata + readonly "after"?: string + readonly "limit"?: number + readonly "order"?: "asc" | "desc" +} +export const ListChatCompletionsParams = Schema.Struct({ + "model": Schema.optionalKey(Schema.String), + "metadata": Schema.optionalKey(Metadata), + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListChatCompletions200 = ChatCompletionList +export const ListChatCompletions200 = ChatCompletionList +export type CreateChatCompletionRequestJson = CreateChatCompletionRequest +export const CreateChatCompletionRequestJson = CreateChatCompletionRequest +export type CreateChatCompletion200 = CreateChatCompletionResponse +export const CreateChatCompletion200 = CreateChatCompletionResponse +export type CreateChatCompletion200Sse = CreateChatCompletionStreamResponse +export const CreateChatCompletion200Sse = CreateChatCompletionStreamResponse +export type GetChatCompletion200 = CreateChatCompletionResponse +export const GetChatCompletion200 = CreateChatCompletionResponse +export type UpdateChatCompletionRequestJson = { readonly "metadata": Metadata } +export const UpdateChatCompletionRequestJson = Schema.Struct({ "metadata": Metadata }) +export type UpdateChatCompletion200 = CreateChatCompletionResponse +export const UpdateChatCompletion200 = CreateChatCompletionResponse +export type DeleteChatCompletion200 = ChatCompletionDeleted +export const DeleteChatCompletion200 = ChatCompletionDeleted +export type GetChatCompletionMessagesParams = { + readonly "after"?: string + readonly "limit"?: number + readonly "order"?: "asc" | "desc" +} +export const GetChatCompletionMessagesParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type GetChatCompletionMessages200 = ChatCompletionMessageList +export const GetChatCompletionMessages200 = ChatCompletionMessageList +export type CreateCompletionRequestJson = CreateCompletionRequest +export const CreateCompletionRequestJson = CreateCompletionRequest +export type CreateCompletion200 = CreateCompletionResponse +export const CreateCompletion200 = CreateCompletionResponse +export type ListContainersParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "name"?: string +} +export const ListContainersParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "name": Schema.optionalKey(Schema.String) +}) +export type ListContainers200 = ContainerListResource +export const ListContainers200 = ContainerListResource +export type CreateContainerRequestJson = CreateContainerBody +export const CreateContainerRequestJson = CreateContainerBody +export type CreateContainer200 = ContainerResource +export const CreateContainer200 = ContainerResource +export type RetrieveContainer200 = ContainerResource +export const RetrieveContainer200 = ContainerResource +export type ListContainerFilesParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string +} +export const ListContainerFilesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String) +}) +export type ListContainerFiles200 = ContainerFileListResource +export const ListContainerFiles200 = ContainerFileListResource +export type CreateContainerFileRequestJson = CreateContainerFileBody +export const CreateContainerFileRequestJson = CreateContainerFileBody +export type CreateContainerFileRequestFormData = CreateContainerFileBody +export const CreateContainerFileRequestFormData = CreateContainerFileBody +export type CreateContainerFile200 = ContainerFileResource +export const CreateContainerFile200 = ContainerFileResource +export type RetrieveContainerFile200 = ContainerFileResource +export const RetrieveContainerFile200 = ContainerFileResource +export type ListConversationItemsParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "include"?: ReadonlyArray +} +export const ListConversationItemsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "include": Schema.optionalKey(Schema.Array(IncludeEnum)) +}) +export type ListConversationItems200 = ConversationItemList +export const ListConversationItems200 = ConversationItemList +export type CreateConversationItemsParams = { readonly "include"?: ReadonlyArray } +export const CreateConversationItemsParams = Schema.Struct({ "include": Schema.optionalKey(Schema.Array(IncludeEnum)) }) +export type CreateConversationItemsRequestJson = { + readonly "items": ReadonlyArray + readonly [x: string]: unknown +} +export const CreateConversationItemsRequestJson = Schema.StructWithRest( + Schema.Struct({ + "items": Schema.Array(InputItem).annotate({ + "description": "The items to add to the conversation. You may add up to 20 items at a time.\n" + }).check(Schema.isMaxLength(20)) + }), + [Schema.Record(Schema.String, Schema.Unknown)] +) +export type CreateConversationItems200 = ConversationItemList +export const CreateConversationItems200 = ConversationItemList +export type GetConversationItemParams = { readonly "include"?: ReadonlyArray } +export const GetConversationItemParams = Schema.Struct({ "include": Schema.optionalKey(Schema.Array(IncludeEnum)) }) +export type GetConversationItem200 = ConversationItem +export const GetConversationItem200 = ConversationItem +export type DeleteConversationItem200 = ConversationResource +export const DeleteConversationItem200 = ConversationResource +export type CreateEmbeddingRequestJson = CreateEmbeddingRequest +export const CreateEmbeddingRequestJson = CreateEmbeddingRequest +export type CreateEmbedding200 = CreateEmbeddingResponse +export const CreateEmbedding200 = CreateEmbeddingResponse +export type ListEvalsParams = { + readonly "after"?: string + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "order_by"?: "created_at" | "updated_at" +} +export const ListEvalsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "order_by": Schema.optionalKey(Schema.Literals(["created_at", "updated_at"])) +}) +export type ListEvals200 = EvalList +export const ListEvals200 = EvalList +export type CreateEvalRequestJson = CreateEvalRequest +export const CreateEvalRequestJson = CreateEvalRequest +export type CreateEval201 = Eval +export const CreateEval201 = Eval +export type GetEval200 = Eval +export const GetEval200 = Eval +export type UpdateEvalRequestJson = { readonly "name"?: string; readonly "metadata"?: Metadata } +export const UpdateEvalRequestJson = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Rename the evaluation." })), + "metadata": Schema.optionalKey(Metadata) +}) +export type UpdateEval200 = Eval +export const UpdateEval200 = Eval +export type DeleteEval200 = { readonly "object": string; readonly "deleted": boolean; readonly "eval_id": string } +export const DeleteEval200 = Schema.Struct({ + "object": Schema.String, + "deleted": Schema.Boolean, + "eval_id": Schema.String +}) +export type DeleteEval404 = Error +export const DeleteEval404 = Error +export type GetEvalRunsParams = { + readonly "after"?: string + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "status"?: "queued" | "in_progress" | "completed" | "canceled" | "failed" +} +export const GetEvalRunsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "status": Schema.optionalKey(Schema.Literals(["queued", "in_progress", "completed", "canceled", "failed"])) +}) +export type GetEvalRuns200 = EvalRunList +export const GetEvalRuns200 = EvalRunList +export type CreateEvalRunRequestJson = CreateEvalRunRequest +export const CreateEvalRunRequestJson = CreateEvalRunRequest +export type CreateEvalRun201 = EvalRun +export const CreateEvalRun201 = EvalRun +export type CreateEvalRun400 = Error +export const CreateEvalRun400 = Error +export type GetEvalRun200 = EvalRun +export const GetEvalRun200 = EvalRun +export type CancelEvalRun200 = EvalRun +export const CancelEvalRun200 = EvalRun +export type DeleteEvalRun200 = { readonly "object"?: string; readonly "deleted"?: boolean; readonly "run_id"?: string } +export const DeleteEvalRun200 = Schema.Struct({ + "object": Schema.optionalKey(Schema.String), + "deleted": Schema.optionalKey(Schema.Boolean), + "run_id": Schema.optionalKey(Schema.String) +}) +export type DeleteEvalRun404 = Error +export const DeleteEvalRun404 = Error +export type GetEvalRunOutputItemsParams = { + readonly "after"?: string + readonly "limit"?: number + readonly "status"?: "fail" | "pass" + readonly "order"?: "asc" | "desc" +} +export const GetEvalRunOutputItemsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "status": Schema.optionalKey(Schema.Literals(["fail", "pass"])), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type GetEvalRunOutputItems200 = EvalRunOutputItemList +export const GetEvalRunOutputItems200 = EvalRunOutputItemList +export type GetEvalRunOutputItem200 = EvalRunOutputItem +export const GetEvalRunOutputItem200 = EvalRunOutputItem +export type ListFilesParams = { + readonly "purpose"?: string + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string +} +export const ListFilesParams = Schema.Struct({ + "purpose": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String) +}) +export type ListFiles200 = ListFilesResponse +export const ListFiles200 = ListFilesResponse +export type CreateFileRequestFormData = CreateFileRequest +export const CreateFileRequestFormData = CreateFileRequest +export type CreateFile200 = OpenAIFile +export const CreateFile200 = OpenAIFile +export type RetrieveFile200 = OpenAIFile +export const RetrieveFile200 = OpenAIFile +export type DeleteFile200 = DeleteFileResponse +export const DeleteFile200 = DeleteFileResponse +export type DownloadFile200 = string +export const DownloadFile200 = Schema.String +export type RunGraderRequestJson = RunGraderRequest +export const RunGraderRequestJson = RunGraderRequest +export type RunGrader200 = RunGraderResponse +export const RunGrader200 = RunGraderResponse +export type ValidateGraderRequestJson = ValidateGraderRequest +export const ValidateGraderRequestJson = ValidateGraderRequest +export type ValidateGrader200 = ValidateGraderResponse +export const ValidateGrader200 = ValidateGraderResponse +export type ListFineTuningCheckpointPermissionsParams = { + readonly "project_id"?: string + readonly "after"?: string + readonly "limit"?: number + readonly "order"?: "ascending" | "descending" +} +export const ListFineTuningCheckpointPermissionsParams = Schema.Struct({ + "project_id": Schema.optionalKey(Schema.String), + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["ascending", "descending"])) +}) +export type ListFineTuningCheckpointPermissions200 = ListFineTuningCheckpointPermissionResponse +export const ListFineTuningCheckpointPermissions200 = ListFineTuningCheckpointPermissionResponse +export type CreateFineTuningCheckpointPermissionRequestJson = CreateFineTuningCheckpointPermissionRequest +export const CreateFineTuningCheckpointPermissionRequestJson = CreateFineTuningCheckpointPermissionRequest +export type CreateFineTuningCheckpointPermission200 = ListFineTuningCheckpointPermissionResponse +export const CreateFineTuningCheckpointPermission200 = ListFineTuningCheckpointPermissionResponse +export type DeleteFineTuningCheckpointPermission200 = DeleteFineTuningCheckpointPermissionResponse +export const DeleteFineTuningCheckpointPermission200 = DeleteFineTuningCheckpointPermissionResponse +export type ListPaginatedFineTuningJobsParams = { + readonly "after"?: string + readonly "limit"?: number + readonly "metadata"?: {} +} +export const ListPaginatedFineTuningJobsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "metadata": Schema.optionalKey(Schema.Union([Schema.Struct({})])) +}) +export type ListPaginatedFineTuningJobs200 = ListPaginatedFineTuningJobsResponse +export const ListPaginatedFineTuningJobs200 = ListPaginatedFineTuningJobsResponse +export type CreateFineTuningJobRequestJson = CreateFineTuningJobRequest +export const CreateFineTuningJobRequestJson = CreateFineTuningJobRequest +export type CreateFineTuningJob200 = FineTuningJob +export const CreateFineTuningJob200 = FineTuningJob +export type RetrieveFineTuningJob200 = FineTuningJob +export const RetrieveFineTuningJob200 = FineTuningJob +export type CancelFineTuningJob200 = FineTuningJob +export const CancelFineTuningJob200 = FineTuningJob +export type ListFineTuningJobCheckpointsParams = { readonly "after"?: string; readonly "limit"?: number } +export const ListFineTuningJobCheckpointsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())) +}) +export type ListFineTuningJobCheckpoints200 = ListFineTuningJobCheckpointsResponse +export const ListFineTuningJobCheckpoints200 = ListFineTuningJobCheckpointsResponse +export type ListFineTuningEventsParams = { readonly "after"?: string; readonly "limit"?: number } +export const ListFineTuningEventsParams = Schema.Struct({ + "after": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())) +}) +export type ListFineTuningEvents200 = ListFineTuningJobEventsResponse +export const ListFineTuningEvents200 = ListFineTuningJobEventsResponse +export type PauseFineTuningJob200 = FineTuningJob +export const PauseFineTuningJob200 = FineTuningJob +export type ResumeFineTuningJob200 = FineTuningJob +export const ResumeFineTuningJob200 = FineTuningJob +export type CreateImageEditRequestJson = EditImageBodyJsonParam +export const CreateImageEditRequestJson = EditImageBodyJsonParam +export type CreateImageEditRequestFormData = CreateImageEditRequest +export const CreateImageEditRequestFormData = CreateImageEditRequest +export type CreateImageEdit200 = ImagesResponse +export const CreateImageEdit200 = ImagesResponse +export type CreateImageEdit200Sse = ImageEditStreamEvent +export const CreateImageEdit200Sse = ImageEditStreamEvent +export type CreateImageRequestJson = CreateImageRequest +export const CreateImageRequestJson = CreateImageRequest +export type CreateImage200 = ImagesResponse +export const CreateImage200 = ImagesResponse +export type CreateImage200Sse = ImageGenStreamEvent +export const CreateImage200Sse = ImageGenStreamEvent +export type CreateImageVariationRequestFormData = CreateImageVariationRequest +export const CreateImageVariationRequestFormData = CreateImageVariationRequest +export type CreateImageVariation200 = ImagesResponse +export const CreateImageVariation200 = ImagesResponse +export type ListModels200 = ListModelsResponse +export const ListModels200 = ListModelsResponse +export type RetrieveModel200 = Model +export const RetrieveModel200 = Model +export type DeleteModel200 = DeleteModelResponse +export const DeleteModel200 = DeleteModelResponse +export type CreateModerationRequestJson = CreateModerationRequest +export const CreateModerationRequestJson = CreateModerationRequest +export type CreateModeration200 = CreateModerationResponse +export const CreateModeration200 = CreateModerationResponse +export type AdminApiKeysListParams = { + readonly "after"?: string | null + readonly "order"?: "asc" | "desc" + readonly "limit"?: number +} +export const AdminApiKeysListParams = Schema.Struct({ + "after": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Return keys with IDs that come after this ID in the pagination order." + }) + ), + "order": Schema.optionalKey( + Schema.Literals(["asc", "desc"]).annotate({ + "description": "Order results by creation time, ascending or descending." + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of keys to return." }).check(Schema.isInt()) + ) +}) +export type AdminApiKeysList200 = ApiKeyList +export const AdminApiKeysList200 = ApiKeyList +export type AdminApiKeysCreateRequestJson = { readonly "name": string } +export const AdminApiKeysCreateRequestJson = Schema.Struct({ "name": Schema.String }) +export type AdminApiKeysCreate200 = AdminApiKeyCreateResponse +export const AdminApiKeysCreate200 = AdminApiKeyCreateResponse +export type AdminApiKeysGet200 = AdminApiKey +export const AdminApiKeysGet200 = AdminApiKey +export type AdminApiKeysDelete200 = { + readonly "id": string + readonly "object": "organization.admin_api_key.deleted" + readonly "deleted": boolean +} +export const AdminApiKeysDelete200 = Schema.Struct({ + "id": Schema.String, + "object": Schema.Literal("organization.admin_api_key.deleted"), + "deleted": Schema.Boolean +}) +export type ListAuditLogsParams = { + readonly "effective_at[gt]"?: number + readonly "effective_at[gte]"?: number + readonly "effective_at[lt]"?: number + readonly "effective_at[lte]"?: number + readonly "project_ids[]"?: ReadonlyArray + readonly "event_types[]"?: ReadonlyArray + readonly "actor_ids[]"?: ReadonlyArray + readonly "actor_emails[]"?: ReadonlyArray + readonly "resource_ids[]"?: ReadonlyArray + readonly "limit"?: number + readonly "after"?: string + readonly "before"?: string +} +export const ListAuditLogsParams = Schema.Struct({ + "effective_at[gt]": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Return only events whose `effective_at` (Unix seconds) is greater than this value." + }).check(Schema.isInt()) + ), + "effective_at[gte]": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Return only events whose `effective_at` (Unix seconds) is greater than or equal to this value." + }).check(Schema.isInt()) + ), + "effective_at[lt]": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Return only events whose `effective_at` (Unix seconds) is less than this value." + }).check(Schema.isInt()) + ), + "effective_at[lte]": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Return only events whose `effective_at` (Unix seconds) is less than or equal to this value." + }).check(Schema.isInt()) + ), + "project_ids[]": Schema.optionalKey(Schema.Array(Schema.String)), + "event_types[]": Schema.optionalKey(Schema.Array(AuditLogEventType)), + "actor_ids[]": Schema.optionalKey(Schema.Array(Schema.String)), + "actor_emails[]": Schema.optionalKey(Schema.Array(Schema.String)), + "resource_ids[]": Schema.optionalKey(Schema.Array(Schema.String)), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String) +}) +export type ListAuditLogs200 = ListAuditLogsResponse +export const ListAuditLogs200 = ListAuditLogsResponse +export type ListOrganizationCertificatesParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListOrganizationCertificatesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListOrganizationCertificates200 = ListCertificatesResponse +export const ListOrganizationCertificates200 = ListCertificatesResponse +export type UploadCertificateRequestJson = UploadCertificateRequest +export const UploadCertificateRequestJson = UploadCertificateRequest +export type UploadCertificate200 = Certificate +export const UploadCertificate200 = Certificate +export type ActivateOrganizationCertificatesRequestJson = ToggleCertificatesRequest +export const ActivateOrganizationCertificatesRequestJson = ToggleCertificatesRequest +export type ActivateOrganizationCertificates200 = OrganizationCertificateActivationResponse +export const ActivateOrganizationCertificates200 = OrganizationCertificateActivationResponse +export type DeactivateOrganizationCertificatesRequestJson = ToggleCertificatesRequest +export const DeactivateOrganizationCertificatesRequestJson = ToggleCertificatesRequest +export type DeactivateOrganizationCertificates200 = OrganizationCertificateDeactivationResponse +export const DeactivateOrganizationCertificates200 = OrganizationCertificateDeactivationResponse +export type GetCertificateParams = { readonly "include"?: ReadonlyArray<"content"> } +export const GetCertificateParams = Schema.Struct({ + "include": Schema.optionalKey(Schema.Array(Schema.Literal("content"))) +}) +export type GetCertificate200 = Certificate +export const GetCertificate200 = Certificate +export type ModifyCertificateRequestJson = ModifyCertificateRequest +export const ModifyCertificateRequestJson = ModifyCertificateRequest +export type ModifyCertificate200 = Certificate +export const ModifyCertificate200 = Certificate +export type DeleteCertificate200 = DeleteCertificateResponse +export const DeleteCertificate200 = DeleteCertificateResponse +export type UsageCostsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1d" + readonly "project_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "line_item" | "api_key_id"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageCostsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literal("1d")), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literals(["project_id", "line_item", "api_key_id"]))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageCosts200 = UsageResponse +export const UsageCosts200 = UsageResponse +export type ListGroupsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListGroupsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListGroups200 = GroupListResource +export const ListGroups200 = GroupListResource +export type CreateGroupRequestJson = CreateGroupBody +export const CreateGroupRequestJson = CreateGroupBody +export type CreateGroup200 = GroupResponse +export const CreateGroup200 = GroupResponse +export type UpdateGroupRequestJson = UpdateGroupBody +export const UpdateGroupRequestJson = UpdateGroupBody +export type UpdateGroup200 = GroupResourceWithSuccess +export const UpdateGroup200 = GroupResourceWithSuccess +export type DeleteGroup200 = GroupDeletedResource +export const DeleteGroup200 = GroupDeletedResource +export type ListGroupRoleAssignmentsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListGroupRoleAssignmentsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListGroupRoleAssignments200 = RoleListResource +export const ListGroupRoleAssignments200 = RoleListResource +export type AssignGroupRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export const AssignGroupRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export type AssignGroupRole200 = GroupRoleAssignment +export const AssignGroupRole200 = GroupRoleAssignment +export type UnassignGroupRole200 = DeletedRoleAssignmentResource +export const UnassignGroupRole200 = DeletedRoleAssignmentResource +export type ListGroupUsersParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListGroupUsersParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListGroupUsers200 = UserListResource +export const ListGroupUsers200 = UserListResource +export type AddGroupUserRequestJson = CreateGroupUserBody +export const AddGroupUserRequestJson = CreateGroupUserBody +export type AddGroupUser200 = GroupUserAssignment +export const AddGroupUser200 = GroupUserAssignment +export type RemoveGroupUser200 = GroupUserDeletedResource +export const RemoveGroupUser200 = GroupUserDeletedResource +export type ListInvitesParams = { readonly "limit"?: number; readonly "after"?: string } +export const ListInvitesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String) +}) +export type ListInvites200 = InviteListResponse +export const ListInvites200 = InviteListResponse +export type InviteUserRequestJson = InviteRequest +export const InviteUserRequestJson = InviteRequest +export type InviteUser200 = Invite +export const InviteUser200 = Invite +export type RetrieveInvite200 = Invite +export const RetrieveInvite200 = Invite +export type DeleteInvite200 = InviteDeleteResponse +export const DeleteInvite200 = InviteDeleteResponse +export type ListProjectsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "include_archived"?: boolean +} +export const ListProjectsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "include_archived": Schema.optionalKey(Schema.Boolean) +}) +export type ListProjects200 = ProjectListResponse +export const ListProjects200 = ProjectListResponse +export type CreateProjectRequestJson = ProjectCreateRequest +export const CreateProjectRequestJson = ProjectCreateRequest +export type CreateProject200 = Project +export const CreateProject200 = Project +export type RetrieveProject200 = Project +export const RetrieveProject200 = Project +export type ModifyProjectRequestJson = ProjectUpdateRequest +export const ModifyProjectRequestJson = ProjectUpdateRequest +export type ModifyProject200 = Project +export const ModifyProject200 = Project +export type ModifyProject400 = ErrorResponse +export const ModifyProject400 = ErrorResponse +export type ListProjectApiKeysParams = { readonly "limit"?: number; readonly "after"?: string } +export const ListProjectApiKeysParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String) +}) +export type ListProjectApiKeys200 = ProjectApiKeyListResponse +export const ListProjectApiKeys200 = ProjectApiKeyListResponse +export type RetrieveProjectApiKey200 = ProjectApiKey +export const RetrieveProjectApiKey200 = ProjectApiKey +export type DeleteProjectApiKey200 = ProjectApiKeyDeleteResponse +export const DeleteProjectApiKey200 = ProjectApiKeyDeleteResponse +export type DeleteProjectApiKey400 = ErrorResponse +export const DeleteProjectApiKey400 = ErrorResponse +export type ArchiveProject200 = Project +export const ArchiveProject200 = Project +export type ListProjectCertificatesParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListProjectCertificatesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListProjectCertificates200 = ListProjectCertificatesResponse +export const ListProjectCertificates200 = ListProjectCertificatesResponse +export type ActivateProjectCertificatesRequestJson = ToggleCertificatesRequest +export const ActivateProjectCertificatesRequestJson = ToggleCertificatesRequest +export type ActivateProjectCertificates200 = OrganizationProjectCertificateActivationResponse +export const ActivateProjectCertificates200 = OrganizationProjectCertificateActivationResponse +export type DeactivateProjectCertificatesRequestJson = ToggleCertificatesRequest +export const DeactivateProjectCertificatesRequestJson = ToggleCertificatesRequest +export type DeactivateProjectCertificates200 = OrganizationProjectCertificateDeactivationResponse +export const DeactivateProjectCertificates200 = OrganizationProjectCertificateDeactivationResponse +export type ListProjectGroupsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListProjectGroupsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListProjectGroups200 = ProjectGroupListResource +export const ListProjectGroups200 = ProjectGroupListResource +export type AddProjectGroupRequestJson = InviteProjectGroupBody +export const AddProjectGroupRequestJson = InviteProjectGroupBody +export type AddProjectGroup200 = ProjectGroup +export const AddProjectGroup200 = ProjectGroup +export type RemoveProjectGroup200 = ProjectGroupDeletedResource +export const RemoveProjectGroup200 = ProjectGroupDeletedResource +export type RetrieveProjectHostedToolPermissions200 = ProjectHostedToolPermissions +export const RetrieveProjectHostedToolPermissions200 = ProjectHostedToolPermissions +export type UpdateProjectHostedToolPermissionsRequestJson = ProjectHostedToolPermissionsUpdateRequest +export const UpdateProjectHostedToolPermissionsRequestJson = ProjectHostedToolPermissionsUpdateRequest +export type UpdateProjectHostedToolPermissions200 = ProjectHostedToolPermissions +export const UpdateProjectHostedToolPermissions200 = ProjectHostedToolPermissions +export type RetrieveProjectModelPermissions200 = ProjectModelPermissions +export const RetrieveProjectModelPermissions200 = ProjectModelPermissions +export type UpdateProjectModelPermissionsRequestJson = ProjectModelPermissionsUpdateRequest +export const UpdateProjectModelPermissionsRequestJson = ProjectModelPermissionsUpdateRequest +export type UpdateProjectModelPermissions200 = ProjectModelPermissions +export const UpdateProjectModelPermissions200 = ProjectModelPermissions +export type DeleteProjectModelPermissions200 = ProjectModelPermissionsDeleteResponse +export const DeleteProjectModelPermissions200 = ProjectModelPermissionsDeleteResponse +export type ListProjectRateLimitsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "before"?: string +} +export const ListProjectRateLimitsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String) +}) +export type ListProjectRateLimits200 = ProjectRateLimitListResponse +export const ListProjectRateLimits200 = ProjectRateLimitListResponse +export type UpdateProjectRateLimitsRequestJson = ProjectRateLimitUpdateRequest +export const UpdateProjectRateLimitsRequestJson = ProjectRateLimitUpdateRequest +export type UpdateProjectRateLimits200 = ProjectRateLimit +export const UpdateProjectRateLimits200 = ProjectRateLimit +export type UpdateProjectRateLimits400 = ErrorResponse +export const UpdateProjectRateLimits400 = ErrorResponse +export type ListProjectServiceAccountsParams = { readonly "limit"?: number; readonly "after"?: string } +export const ListProjectServiceAccountsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String) +}) +export type ListProjectServiceAccounts200 = ProjectServiceAccountListResponse +export const ListProjectServiceAccounts200 = ProjectServiceAccountListResponse +export type ListProjectServiceAccounts400 = ErrorResponse +export const ListProjectServiceAccounts400 = ErrorResponse +export type CreateProjectServiceAccountRequestJson = ProjectServiceAccountCreateRequest +export const CreateProjectServiceAccountRequestJson = ProjectServiceAccountCreateRequest +export type CreateProjectServiceAccount200 = ProjectServiceAccountCreateResponse +export const CreateProjectServiceAccount200 = ProjectServiceAccountCreateResponse +export type CreateProjectServiceAccount400 = ErrorResponse +export const CreateProjectServiceAccount400 = ErrorResponse +export type RetrieveProjectServiceAccount200 = ProjectServiceAccount +export const RetrieveProjectServiceAccount200 = ProjectServiceAccount +export type DeleteProjectServiceAccount200 = ProjectServiceAccountDeleteResponse +export const DeleteProjectServiceAccount200 = ProjectServiceAccountDeleteResponse +export type ListProjectUsersParams = { readonly "limit"?: number; readonly "after"?: string } +export const ListProjectUsersParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String) +}) +export type ListProjectUsers200 = ProjectUserListResponse +export const ListProjectUsers200 = ProjectUserListResponse +export type ListProjectUsers400 = ErrorResponse +export const ListProjectUsers400 = ErrorResponse +export type CreateProjectUserRequestJson = ProjectUserCreateRequest +export const CreateProjectUserRequestJson = ProjectUserCreateRequest +export type CreateProjectUser200 = ProjectUser +export const CreateProjectUser200 = ProjectUser +export type CreateProjectUser400 = ErrorResponse +export const CreateProjectUser400 = ErrorResponse +export type RetrieveProjectUser200 = ProjectUser +export const RetrieveProjectUser200 = ProjectUser +export type ModifyProjectUserRequestJson = ProjectUserUpdateRequest +export const ModifyProjectUserRequestJson = ProjectUserUpdateRequest +export type ModifyProjectUser200 = ProjectUser +export const ModifyProjectUser200 = ProjectUser +export type ModifyProjectUser400 = ErrorResponse +export const ModifyProjectUser400 = ErrorResponse +export type DeleteProjectUser200 = ProjectUserDeleteResponse +export const DeleteProjectUser200 = ProjectUserDeleteResponse +export type DeleteProjectUser400 = ErrorResponse +export const DeleteProjectUser400 = ErrorResponse +export type ListRolesParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListRolesParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListRoles200 = PublicRoleListResource +export const ListRoles200 = PublicRoleListResource +export type CreateRoleRequestJson = PublicCreateOrganizationRoleBody +export const CreateRoleRequestJson = PublicCreateOrganizationRoleBody +export type CreateRole200 = Role +export const CreateRole200 = Role +export type UpdateRoleRequestJson = PublicUpdateOrganizationRoleBody +export const UpdateRoleRequestJson = PublicUpdateOrganizationRoleBody +export type UpdateRole200 = Role +export const UpdateRole200 = Role +export type DeleteRole200 = RoleDeletedResource +export const DeleteRole200 = RoleDeletedResource +export type UsageAudioSpeechesParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageAudioSpeechesParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model"]))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageAudioSpeeches200 = UsageResponse +export const UsageAudioSpeeches200 = UsageResponse +export type UsageAudioTranscriptionsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageAudioTranscriptionsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model"]))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageAudioTranscriptions200 = UsageResponse +export const UsageAudioTranscriptions200 = UsageResponse +export type UsageCodeInterpreterSessionsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageCodeInterpreterSessionsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literal("project_id"))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageCodeInterpreterSessions200 = UsageResponse +export const UsageCodeInterpreterSessions200 = UsageResponse +export type UsageCompletionsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "batch"?: boolean + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model" | "batch" | "service_tier"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageCompletionsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "batch": Schema.optionalKey(Schema.Boolean), + "group_by": Schema.optionalKey( + Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model", "batch", "service_tier"])) + ), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageCompletions200 = UsageResponse +export const UsageCompletions200 = UsageResponse +export type UsageEmbeddingsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageEmbeddingsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model"]))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageEmbeddings200 = UsageResponse +export const UsageEmbeddings200 = UsageResponse +export type UsageFileSearchCallsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "vector_store_ids"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "vector_store_id"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageFileSearchCallsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "vector_store_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey( + Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "vector_store_id"])) + ), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageFileSearchCalls200 = UsageResponse +export const UsageFileSearchCalls200 = UsageResponse +export type UsageImagesParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "sources"?: ReadonlyArray<"image.generation" | "image.edit" | "image.variation"> + readonly "sizes"?: ReadonlyArray<"256x256" | "512x512" | "1024x1024" | "1792x1792" | "1024x1792"> + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model" | "size" | "source"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageImagesParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "sources": Schema.optionalKey(Schema.Array(Schema.Literals(["image.generation", "image.edit", "image.variation"]))), + "sizes": Schema.optionalKey( + Schema.Array(Schema.Literals(["256x256", "512x512", "1024x1024", "1792x1792", "1024x1792"])) + ), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey( + Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model", "size", "source"])) + ), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageImages200 = UsageResponse +export const UsageImages200 = UsageResponse +export type UsageModerationsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageModerationsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model"]))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageModerations200 = UsageResponse +export const UsageModerations200 = UsageResponse +export type UsageVectorStoresParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "group_by"?: ReadonlyArray<"project_id"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageVectorStoresParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "group_by": Schema.optionalKey(Schema.Array(Schema.Literal("project_id"))), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageVectorStores200 = UsageResponse +export const UsageVectorStores200 = UsageResponse +export type UsageWebSearchCallsParams = { + readonly "start_time": number + readonly "end_time"?: number + readonly "bucket_width"?: "1m" | "1h" | "1d" + readonly "project_ids"?: ReadonlyArray + readonly "user_ids"?: ReadonlyArray + readonly "api_key_ids"?: ReadonlyArray + readonly "models"?: ReadonlyArray + readonly "context_levels"?: ReadonlyArray<"low" | "medium" | "high"> + readonly "group_by"?: ReadonlyArray<"project_id" | "user_id" | "api_key_id" | "model" | "context_level"> + readonly "limit"?: number + readonly "page"?: string +} +export const UsageWebSearchCallsParams = Schema.Struct({ + "start_time": Schema.Number.check(Schema.isInt()), + "end_time": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "bucket_width": Schema.optionalKey(Schema.Literals(["1m", "1h", "1d"])), + "project_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "user_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "api_key_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "context_levels": Schema.optionalKey(Schema.Array(Schema.Literals(["low", "medium", "high"]))), + "group_by": Schema.optionalKey( + Schema.Array(Schema.Literals(["project_id", "user_id", "api_key_id", "model", "context_level"])) + ), + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "page": Schema.optionalKey(Schema.String) +}) +export type UsageWebSearchCalls200 = UsageResponse +export const UsageWebSearchCalls200 = UsageResponse +export type ListUsersParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "emails"?: ReadonlyArray +} +export const ListUsersParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "after": Schema.optionalKey(Schema.String), + "emails": Schema.optionalKey(Schema.Array(Schema.String)) +}) +export type ListUsers200 = UserListResponse +export const ListUsers200 = UserListResponse +export type RetrieveUser200 = User +export const RetrieveUser200 = User +export type ModifyUserRequestJson = UserRoleUpdateRequest +export const ModifyUserRequestJson = UserRoleUpdateRequest +export type ModifyUser200 = User +export const ModifyUser200 = User +export type DeleteUser200 = UserDeleteResponse +export const DeleteUser200 = UserDeleteResponse +export type ListUserRoleAssignmentsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListUserRoleAssignmentsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListUserRoleAssignments200 = RoleListResource +export const ListUserRoleAssignments200 = RoleListResource +export type AssignUserRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export const AssignUserRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export type AssignUserRole200 = UserRoleAssignment +export const AssignUserRole200 = UserRoleAssignment +export type UnassignUserRole200 = DeletedRoleAssignmentResource +export const UnassignUserRole200 = DeletedRoleAssignmentResource +export type ListProjectGroupRoleAssignmentsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListProjectGroupRoleAssignmentsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListProjectGroupRoleAssignments200 = RoleListResource +export const ListProjectGroupRoleAssignments200 = RoleListResource +export type AssignProjectGroupRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export const AssignProjectGroupRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export type AssignProjectGroupRole200 = GroupRoleAssignment +export const AssignProjectGroupRole200 = GroupRoleAssignment +export type UnassignProjectGroupRole200 = DeletedRoleAssignmentResource +export const UnassignProjectGroupRole200 = DeletedRoleAssignmentResource +export type ListProjectRolesParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListProjectRolesParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListProjectRoles200 = PublicRoleListResource +export const ListProjectRoles200 = PublicRoleListResource +export type CreateProjectRoleRequestJson = PublicCreateOrganizationRoleBody +export const CreateProjectRoleRequestJson = PublicCreateOrganizationRoleBody +export type CreateProjectRole200 = Role +export const CreateProjectRole200 = Role +export type UpdateProjectRoleRequestJson = PublicUpdateOrganizationRoleBody +export const UpdateProjectRoleRequestJson = PublicUpdateOrganizationRoleBody +export type UpdateProjectRole200 = Role +export const UpdateProjectRole200 = Role +export type DeleteProjectRole200 = RoleDeletedResource +export const DeleteProjectRole200 = RoleDeletedResource +export type ListProjectUserRoleAssignmentsParams = { + readonly "limit"?: number + readonly "after"?: string + readonly "order"?: "asc" | "desc" +} +export const ListProjectUserRoleAssignmentsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1000)) + ), + "after": Schema.optionalKey(Schema.String), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])) +}) +export type ListProjectUserRoleAssignments200 = RoleListResource +export const ListProjectUserRoleAssignments200 = RoleListResource +export type AssignProjectUserRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export const AssignProjectUserRoleRequestJson = PublicAssignOrganizationGroupRoleBody +export type AssignProjectUserRole200 = UserRoleAssignment +export const AssignProjectUserRole200 = UserRoleAssignment +export type UnassignProjectUserRole200 = DeletedRoleAssignmentResource +export const UnassignProjectUserRole200 = DeletedRoleAssignmentResource +export type CreateRealtimeCallRequestFormData = RealtimeCallCreateRequest +export const CreateRealtimeCallRequestFormData = RealtimeCallCreateRequest +export type AcceptRealtimeCallRequestJson = RealtimeSessionCreateRequestGA +export const AcceptRealtimeCallRequestJson = RealtimeSessionCreateRequestGA +export type ReferRealtimeCallRequestJson = RealtimeCallReferRequest +export const ReferRealtimeCallRequestJson = RealtimeCallReferRequest +export type RejectRealtimeCallRequestJson = RealtimeCallRejectRequest +export const RejectRealtimeCallRequestJson = RealtimeCallRejectRequest +export type CreateRealtimeClientSecretRequestJson = RealtimeCreateClientSecretRequest +export const CreateRealtimeClientSecretRequestJson = RealtimeCreateClientSecretRequest +export type CreateRealtimeClientSecret200 = RealtimeCreateClientSecretResponse +export const CreateRealtimeClientSecret200 = RealtimeCreateClientSecretResponse +export type CreateRealtimeSessionRequestJson = RealtimeSessionCreateRequest +export const CreateRealtimeSessionRequestJson = RealtimeSessionCreateRequest +export type CreateRealtimeSession200 = RealtimeSessionCreateResponse +export const CreateRealtimeSession200 = RealtimeSessionCreateResponse +export type CreateRealtimeTranscriptionSessionRequestJson = RealtimeTranscriptionSessionCreateRequest +export const CreateRealtimeTranscriptionSessionRequestJson = RealtimeTranscriptionSessionCreateRequest +export type CreateRealtimeTranscriptionSession200 = RealtimeTranscriptionSessionCreateResponse +export const CreateRealtimeTranscriptionSession200 = RealtimeTranscriptionSessionCreateResponse +export type CreateRealtimeTranslationClientSecretRequestJson = RealtimeTranslationClientSecretCreateRequest +export const CreateRealtimeTranslationClientSecretRequestJson = RealtimeTranslationClientSecretCreateRequest +export type CreateRealtimeTranslationClientSecret200 = RealtimeTranslationClientSecretCreateResponse +export const CreateRealtimeTranslationClientSecret200 = RealtimeTranslationClientSecretCreateResponse +export type CreateResponseRequestJson = CreateResponse +export const CreateResponseRequestJson = CreateResponse +export type CreateResponse200 = Response +export const CreateResponse200 = Response +export type CreateResponse200Sse = ResponseStreamEvent +export const CreateResponse200Sse = ResponseStreamEvent +export type GetResponseParams = { + readonly "include"?: ReadonlyArray + readonly "stream"?: boolean + readonly "starting_after"?: number + readonly "include_obfuscation"?: boolean +} +export const GetResponseParams = Schema.Struct({ + "include": Schema.optionalKey(Schema.Array(IncludeEnum)), + "stream": Schema.optionalKey(Schema.Boolean), + "starting_after": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "include_obfuscation": Schema.optionalKey(Schema.Boolean) +}) +export type GetResponse200 = Response +export const GetResponse200 = Response +export type DeleteResponse404 = Error +export const DeleteResponse404 = Error +export type CancelResponse200 = Response +export const CancelResponse200 = Response +export type CancelResponse404 = Error +export const CancelResponse404 = Error +export type ListInputItemsParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "include"?: ReadonlyArray +} +export const ListInputItemsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "include": Schema.optionalKey(Schema.Array(IncludeEnum)) +}) +export type ListInputItems200 = ResponseItemList +export const ListInputItems200 = ResponseItemList +export type CreateThreadRequestJson = CreateThreadRequest +export const CreateThreadRequestJson = CreateThreadRequest +export type CreateThread200 = ThreadObject +export const CreateThread200 = ThreadObject +export type CreateThreadAndRunRequestJson = CreateThreadAndRunRequest +export const CreateThreadAndRunRequestJson = CreateThreadAndRunRequest +export type CreateThreadAndRun200 = RunObject +export const CreateThreadAndRun200 = RunObject +export type GetThread200 = ThreadObject +export const GetThread200 = ThreadObject +export type ModifyThreadRequestJson = ModifyThreadRequest +export const ModifyThreadRequestJson = ModifyThreadRequest +export type ModifyThread200 = ThreadObject +export const ModifyThread200 = ThreadObject +export type DeleteThread200 = DeleteThreadResponse +export const DeleteThread200 = DeleteThreadResponse +export type ListMessagesParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string + readonly "run_id"?: string +} +export const ListMessagesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String), + "run_id": Schema.optionalKey(Schema.String) +}) +export type ListMessages200 = ListMessagesResponse +export const ListMessages200 = ListMessagesResponse +export type CreateMessageRequestJson = CreateMessageRequest +export const CreateMessageRequestJson = CreateMessageRequest +export type CreateMessage200 = MessageObject +export const CreateMessage200 = MessageObject +export type GetMessage200 = MessageObject +export const GetMessage200 = MessageObject +export type ModifyMessageRequestJson = ModifyMessageRequest +export const ModifyMessageRequestJson = ModifyMessageRequest +export type ModifyMessage200 = MessageObject +export const ModifyMessage200 = MessageObject +export type DeleteMessage200 = DeleteMessageResponse +export const DeleteMessage200 = DeleteMessageResponse +export type ListRunsParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string +} +export const ListRunsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String) +}) +export type ListRuns200 = ListRunsResponse +export const ListRuns200 = ListRunsResponse +export type CreateRunParams = { + readonly "include[]"?: ReadonlyArray<"step_details.tool_calls[*].file_search.results[*].content"> +} +export const CreateRunParams = Schema.Struct({ + "include[]": Schema.optionalKey( + Schema.Array(Schema.Literal("step_details.tool_calls[*].file_search.results[*].content")) + ) +}) +export type CreateRunRequestJson = CreateRunRequest +export const CreateRunRequestJson = CreateRunRequest +export type CreateRun200 = RunObject +export const CreateRun200 = RunObject +export type GetRun200 = RunObject +export const GetRun200 = RunObject +export type ModifyRunRequestJson = ModifyRunRequest +export const ModifyRunRequestJson = ModifyRunRequest +export type ModifyRun200 = RunObject +export const ModifyRun200 = RunObject +export type CancelRun200 = RunObject +export const CancelRun200 = RunObject +export type ListRunStepsParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string + readonly "include[]"?: ReadonlyArray<"step_details.tool_calls[*].file_search.results[*].content"> +} +export const ListRunStepsParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String), + "include[]": Schema.optionalKey( + Schema.Array(Schema.Literal("step_details.tool_calls[*].file_search.results[*].content")) + ) +}) +export type ListRunSteps200 = ListRunStepsResponse +export const ListRunSteps200 = ListRunStepsResponse +export type GetRunStepParams = { + readonly "include[]"?: ReadonlyArray<"step_details.tool_calls[*].file_search.results[*].content"> +} +export const GetRunStepParams = Schema.Struct({ + "include[]": Schema.optionalKey( + Schema.Array(Schema.Literal("step_details.tool_calls[*].file_search.results[*].content")) + ) +}) +export type GetRunStep200 = RunStepObject +export const GetRunStep200 = RunStepObject +export type SubmitToolOuputsToRunRequestJson = SubmitToolOutputsRunRequest +export const SubmitToolOuputsToRunRequestJson = SubmitToolOutputsRunRequest +export type SubmitToolOuputsToRun200 = RunObject +export const SubmitToolOuputsToRun200 = RunObject +export type CreateUploadRequestJson = CreateUploadRequest +export const CreateUploadRequestJson = CreateUploadRequest +export type CreateUpload200 = Upload +export const CreateUpload200 = Upload +export type CancelUpload200 = Upload +export const CancelUpload200 = Upload +export type CompleteUploadRequestJson = CompleteUploadRequest +export const CompleteUploadRequestJson = CompleteUploadRequest +export type CompleteUpload200 = Upload +export const CompleteUpload200 = Upload +export type AddUploadPartRequestFormData = AddUploadPartRequest +export const AddUploadPartRequestFormData = AddUploadPartRequest +export type AddUploadPart200 = UploadPart +export const AddUploadPart200 = UploadPart +export type ListVectorStoresParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string +} +export const ListVectorStoresParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String) +}) +export type ListVectorStores200 = ListVectorStoresResponse +export const ListVectorStores200 = ListVectorStoresResponse +export type CreateVectorStoreRequestJson = CreateVectorStoreRequest +export const CreateVectorStoreRequestJson = CreateVectorStoreRequest +export type CreateVectorStore200 = VectorStoreObject +export const CreateVectorStore200 = VectorStoreObject +export type GetVectorStore200 = VectorStoreObject +export const GetVectorStore200 = VectorStoreObject +export type ModifyVectorStoreRequestJson = UpdateVectorStoreRequest +export const ModifyVectorStoreRequestJson = UpdateVectorStoreRequest +export type ModifyVectorStore200 = VectorStoreObject +export const ModifyVectorStore200 = VectorStoreObject +export type DeleteVectorStore200 = DeleteVectorStoreResponse +export const DeleteVectorStore200 = DeleteVectorStoreResponse +export type CreateVectorStoreFileBatchRequestJson = CreateVectorStoreFileBatchRequest +export const CreateVectorStoreFileBatchRequestJson = CreateVectorStoreFileBatchRequest +export type CreateVectorStoreFileBatch200 = VectorStoreFileBatchObject +export const CreateVectorStoreFileBatch200 = VectorStoreFileBatchObject +export type GetVectorStoreFileBatch200 = VectorStoreFileBatchObject +export const GetVectorStoreFileBatch200 = VectorStoreFileBatchObject +export type CancelVectorStoreFileBatch200 = VectorStoreFileBatchObject +export const CancelVectorStoreFileBatch200 = VectorStoreFileBatchObject +export type ListFilesInVectorStoreBatchParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string + readonly "filter"?: "in_progress" | "completed" | "failed" | "cancelled" +} +export const ListFilesInVectorStoreBatchParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String), + "filter": Schema.optionalKey(Schema.Literals(["in_progress", "completed", "failed", "cancelled"])) +}) +export type ListFilesInVectorStoreBatch200 = ListVectorStoreFilesResponse +export const ListFilesInVectorStoreBatch200 = ListVectorStoreFilesResponse +export type ListVectorStoreFilesParams = { + readonly "limit"?: number + readonly "order"?: "asc" | "desc" + readonly "after"?: string + readonly "before"?: string + readonly "filter"?: "in_progress" | "completed" | "failed" | "cancelled" +} +export const ListVectorStoreFilesParams = Schema.Struct({ + "limit": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "order": Schema.optionalKey(Schema.Literals(["asc", "desc"])), + "after": Schema.optionalKey(Schema.String), + "before": Schema.optionalKey(Schema.String), + "filter": Schema.optionalKey(Schema.Literals(["in_progress", "completed", "failed", "cancelled"])) +}) +export type ListVectorStoreFiles200 = ListVectorStoreFilesResponse +export const ListVectorStoreFiles200 = ListVectorStoreFilesResponse +export type CreateVectorStoreFileRequestJson = CreateVectorStoreFileRequest +export const CreateVectorStoreFileRequestJson = CreateVectorStoreFileRequest +export type CreateVectorStoreFile200 = VectorStoreFileObject +export const CreateVectorStoreFile200 = VectorStoreFileObject +export type GetVectorStoreFile200 = VectorStoreFileObject +export const GetVectorStoreFile200 = VectorStoreFileObject +export type UpdateVectorStoreFileAttributesRequestJson = UpdateVectorStoreFileAttributesRequest +export const UpdateVectorStoreFileAttributesRequestJson = UpdateVectorStoreFileAttributesRequest +export type UpdateVectorStoreFileAttributes200 = VectorStoreFileObject +export const UpdateVectorStoreFileAttributes200 = VectorStoreFileObject +export type DeleteVectorStoreFile200 = DeleteVectorStoreFileResponse +export const DeleteVectorStoreFile200 = DeleteVectorStoreFileResponse +export type RetrieveVectorStoreFileContent200 = VectorStoreFileContentResponse +export const RetrieveVectorStoreFileContent200 = VectorStoreFileContentResponse +export type SearchVectorStoreRequestJson = VectorStoreSearchRequest +export const SearchVectorStoreRequestJson = VectorStoreSearchRequest +export type SearchVectorStore200 = VectorStoreSearchResultsPage +export const SearchVectorStore200 = VectorStoreSearchResultsPage +export type CreateConversationRequestJson = CreateConversationBody +export const CreateConversationRequestJson = CreateConversationBody +export type CreateConversation200 = ConversationResource +export const CreateConversation200 = ConversationResource +export type GetConversation200 = ConversationResource +export const GetConversation200 = ConversationResource +export type UpdateConversationRequestJson = UpdateConversationBody +export const UpdateConversationRequestJson = UpdateConversationBody +export type UpdateConversation200 = ConversationResource +export const UpdateConversation200 = ConversationResource +export type DeleteConversation200 = DeletedConversationResource +export const DeleteConversation200 = DeletedConversationResource +export type ListVideosParams = { readonly "limit"?: number; readonly "order"?: OrderEnum; readonly "after"?: string } +export const ListVideosParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "order": Schema.optionalKey(OrderEnum), + "after": Schema.optionalKey( + Schema.String.annotate({ "description": "Identifier for the last item from the previous pagination request" }) + ) +}) +export type ListVideos200 = VideoListResource +export const ListVideos200 = VideoListResource +export type CreateVideoRequestJson = CreateVideoJsonBody +export const CreateVideoRequestJson = CreateVideoJsonBody +export type CreateVideoRequestFormData = CreateVideoMultipartBody +export const CreateVideoRequestFormData = CreateVideoMultipartBody +export type CreateVideo200 = VideoResource +export const CreateVideo200 = VideoResource +export type CreateVideoCharacterRequestFormData = CreateVideoCharacterBody +export const CreateVideoCharacterRequestFormData = CreateVideoCharacterBody +export type CreateVideoCharacter200 = VideoCharacterResource +export const CreateVideoCharacter200 = VideoCharacterResource +export type GetVideoCharacter200 = VideoCharacterResource +export const GetVideoCharacter200 = VideoCharacterResource +export type CreateVideoEditRequestJson = CreateVideoEditJsonBody +export const CreateVideoEditRequestJson = CreateVideoEditJsonBody +export type CreateVideoEditRequestFormData = CreateVideoEditMultipartBody +export const CreateVideoEditRequestFormData = CreateVideoEditMultipartBody +export type CreateVideoEdit200 = VideoResource +export const CreateVideoEdit200 = VideoResource +export type CreateVideoExtendRequestJson = CreateVideoExtendJsonBody +export const CreateVideoExtendRequestJson = CreateVideoExtendJsonBody +export type CreateVideoExtendRequestFormData = CreateVideoExtendMultipartBody +export const CreateVideoExtendRequestFormData = CreateVideoExtendMultipartBody +export type CreateVideoExtend200 = VideoResource +export const CreateVideoExtend200 = VideoResource +export type GetVideo200 = VideoResource +export const GetVideo200 = VideoResource +export type DeleteVideo200 = DeletedVideoResource +export const DeleteVideo200 = DeletedVideoResource +export type RetrieveVideoContentParams = { readonly "variant"?: VideoContentVariant } +export const RetrieveVideoContentParams = Schema.Struct({ "variant": Schema.optionalKey(VideoContentVariant) }) +export type RetrieveVideoContent200 = string +export const RetrieveVideoContent200 = Schema.String +export type CreateVideoRemixRequestJson = CreateVideoRemixBody +export const CreateVideoRemixRequestJson = CreateVideoRemixBody +export type CreateVideoRemixRequestFormData = CreateVideoRemixBody +export const CreateVideoRemixRequestFormData = CreateVideoRemixBody +export type CreateVideoRemix200 = VideoResource +export const CreateVideoRemix200 = VideoResource +export type GetinputtokencountsRequestJson = TokenCountsBody +export const GetinputtokencountsRequestJson = TokenCountsBody +export type GetinputtokencountsRequestFormUrlEncoded = TokenCountsBody +export const GetinputtokencountsRequestFormUrlEncoded = TokenCountsBody +export type Getinputtokencounts200 = TokenCountsResource +export const Getinputtokencounts200 = TokenCountsResource +export type CompactconversationRequestJson = CompactResponseMethodPublicBody +export const CompactconversationRequestJson = CompactResponseMethodPublicBody +export type CompactconversationRequestFormUrlEncoded = CompactResponseMethodPublicBody +export const CompactconversationRequestFormUrlEncoded = CompactResponseMethodPublicBody +export type Compactconversation200 = CompactResource +export const Compactconversation200 = CompactResource +export type ListSkillsParams = { readonly "limit"?: number; readonly "order"?: OrderEnum; readonly "after"?: string } +export const ListSkillsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "order": Schema.optionalKey(OrderEnum), + "after": Schema.optionalKey( + Schema.String.annotate({ "description": "Identifier for the last item from the previous pagination request" }) + ) +}) +export type ListSkills200 = SkillListResource +export const ListSkills200 = SkillListResource +export type CreateSkillRequestJson = CreateSkillBody +export const CreateSkillRequestJson = CreateSkillBody +export type CreateSkillRequestFormData = CreateSkillBody +export const CreateSkillRequestFormData = CreateSkillBody +export type CreateSkill200 = SkillResource +export const CreateSkill200 = SkillResource +export type GetSkill200 = SkillResource +export const GetSkill200 = SkillResource +export type UpdateSkillDefaultVersionRequestJson = SetDefaultSkillVersionBody +export const UpdateSkillDefaultVersionRequestJson = SetDefaultSkillVersionBody +export type UpdateSkillDefaultVersionRequestFormUrlEncoded = SetDefaultSkillVersionBody +export const UpdateSkillDefaultVersionRequestFormUrlEncoded = SetDefaultSkillVersionBody +export type UpdateSkillDefaultVersion200 = SkillResource +export const UpdateSkillDefaultVersion200 = SkillResource +export type DeleteSkill200 = DeletedSkillResource +export const DeleteSkill200 = DeletedSkillResource +export type GetSkillContent200 = string +export const GetSkillContent200 = Schema.String +export type ListSkillVersionsParams = { + readonly "limit"?: number + readonly "order"?: OrderEnum + readonly "after"?: string +} +export const ListSkillVersionsParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "order": Schema.optionalKey(OrderEnum), + "after": Schema.optionalKey(Schema.String) +}) +export type ListSkillVersions200 = SkillVersionListResource +export const ListSkillVersions200 = SkillVersionListResource +export type CreateSkillVersionRequestJson = CreateSkillVersionBody +export const CreateSkillVersionRequestJson = CreateSkillVersionBody +export type CreateSkillVersionRequestFormData = CreateSkillVersionBody +export const CreateSkillVersionRequestFormData = CreateSkillVersionBody +export type CreateSkillVersion200 = SkillVersionResource +export const CreateSkillVersion200 = SkillVersionResource +export type GetSkillVersion200 = SkillVersionResource +export const GetSkillVersion200 = SkillVersionResource +export type DeleteSkillVersion200 = DeletedSkillVersionResource +export const DeleteSkillVersion200 = DeletedSkillVersionResource +export type GetSkillVersionContent200 = string +export const GetSkillVersionContent200 = Schema.String +export type CancelChatSessionMethod200 = ChatSessionResource +export const CancelChatSessionMethod200 = ChatSessionResource +export type CreateChatSessionMethodRequestJson = CreateChatSessionBody +export const CreateChatSessionMethodRequestJson = CreateChatSessionBody +export type CreateChatSessionMethod200 = ChatSessionResource +export const CreateChatSessionMethod200 = ChatSessionResource +export type ListThreadItemsMethodParams = { + readonly "limit"?: number + readonly "order"?: OrderEnum + readonly "after"?: string + readonly "before"?: string +} +export const ListThreadItemsMethodParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "order": Schema.optionalKey(OrderEnum), + "after": Schema.optionalKey( + Schema.String.annotate({ + "description": "List items created after this thread item ID. Defaults to null for the first page." + }) + ), + "before": Schema.optionalKey( + Schema.String.annotate({ + "description": "List items created before this thread item ID. Defaults to null for the newest results." + }) + ) +}) +export type ListThreadItemsMethod200 = ThreadItemListResource +export const ListThreadItemsMethod200 = ThreadItemListResource +export type GetThreadMethod200 = ThreadResource +export const GetThreadMethod200 = ThreadResource +export type DeleteThreadMethod200 = DeletedThreadResource +export const DeleteThreadMethod200 = DeletedThreadResource +export type ListThreadsMethodParams = { + readonly "limit"?: number + readonly "order"?: OrderEnum + readonly "after"?: string + readonly "before"?: string + readonly "user"?: string +} +export const ListThreadsMethodParams = Schema.Struct({ + "limit": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(100)) + ), + "order": Schema.optionalKey(OrderEnum), + "after": Schema.optionalKey( + Schema.String.annotate({ + "description": "List items created after this thread item ID. Defaults to null for the first page." + }) + ), + "before": Schema.optionalKey( + Schema.String.annotate({ + "description": "List items created before this thread item ID. Defaults to null for the newest results." + }) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter threads that belong to this user identifier. Defaults to null to return all users." + }).check(Schema.isMinLength(1)).check(Schema.isMaxLength(512)) + ) +}) +export type ListThreadsMethod200 = ThreadListResource +export const ListThreadsMethod200 = ThreadListResource + +export interface OperationConfig { + /** + * Whether or not the response should be included in the value returned from + * an operation. + * + * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, + * where `A` is the success type of the operation. + * + * If set to `false`, only the success type of the operation will be returned. + */ + readonly includeResponse?: boolean | undefined +} + +/** + * A utility type which optionally includes the response in the return result + * of an operation based upon the value of the `includeResponse` configuration + * option. + */ +export type WithOptionalResponse = Config extends { + readonly includeResponse: true +} ? [A, HttpClientResponse.HttpClientResponse] : + A + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): OpenAiClient => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description) + }) + }) + ) + ) + const withResponse = (config: Config | undefined) => + ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { + const withOptionalResponse = ( + config?.includeResponse + ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) + : (response: HttpClientResponse.HttpClientResponse) => f(response) + ) as any + return options?.transformClient + ? (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + withOptionalResponse + ) + : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + } + const sseRequest = < + Type, + DecodingServices + >( + schema: Schema.ConstraintDecoder + ) => + ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + DecodingServices + > => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) + ) + const binaryRequest = ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap + ) + const decodeSuccess = + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(OpenAiClientError(tag, cause, response)) + ) + return { + httpClient, + "listAssistants": (options) => + HttpClientRequest.get(`/assistants`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListAssistants200), + orElse: unexpectedStatus + })) + ), + "createAssistant": (options) => + HttpClientRequest.post(`/assistants`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateAssistant200), + orElse: unexpectedStatus + })) + ), + "getAssistant": (assistantId, options) => + HttpClientRequest.get(`/assistants/${assistantId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetAssistant200), + orElse: unexpectedStatus + })) + ), + "modifyAssistant": (assistantId, options) => + HttpClientRequest.post(`/assistants/${assistantId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyAssistant200), + orElse: unexpectedStatus + })) + ), + "deleteAssistant": (assistantId, options) => + HttpClientRequest.delete(`/assistants/${assistantId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteAssistant200), + orElse: unexpectedStatus + })) + ), + "createSpeech": (options) => + HttpClientRequest.post(`/audio/speech`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "createSpeechSse": (options) => + HttpClientRequest.post(`/audio/speech`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateSpeech200Sse) + ), + "createSpeechStream": (options) => + HttpClientRequest.post(`/audio/speech`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + binaryRequest + ), + "createTranscription": (options) => + HttpClientRequest.post(`/audio/transcriptions`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateTranscription200), + orElse: unexpectedStatus + })) + ), + "createTranscriptionSse": (options) => + HttpClientRequest.post(`/audio/transcriptions`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + sseRequest(CreateTranscription200Sse) + ), + "createTranslation": (options) => + HttpClientRequest.post(`/audio/translations`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateTranslation200), + orElse: unexpectedStatus + })) + ), + "listVoiceConsents": (options) => + HttpClientRequest.get(`/audio/voice_consents`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVoiceConsents200), + orElse: unexpectedStatus + })) + ), + "createVoiceConsent": (options) => + HttpClientRequest.post(`/audio/voice_consents`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVoiceConsent200), + orElse: unexpectedStatus + })) + ), + "getVoiceConsent": (consentId, options) => + HttpClientRequest.get(`/audio/voice_consents/${consentId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVoiceConsent200), + orElse: unexpectedStatus + })) + ), + "updateVoiceConsent": (consentId, options) => + HttpClientRequest.post(`/audio/voice_consents/${consentId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateVoiceConsent200), + orElse: unexpectedStatus + })) + ), + "deleteVoiceConsent": (consentId, options) => + HttpClientRequest.delete(`/audio/voice_consents/${consentId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVoiceConsent200), + orElse: unexpectedStatus + })) + ), + "createVoice": (options) => + HttpClientRequest.post(`/audio/voices`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVoice200), + orElse: unexpectedStatus + })) + ), + "listBatches": (options) => + HttpClientRequest.get(`/batches`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListBatches200), + orElse: unexpectedStatus + })) + ), + "createBatch": (options) => + HttpClientRequest.post(`/batches`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateBatch200), + orElse: unexpectedStatus + })) + ), + "retrieveBatch": (batchId, options) => + HttpClientRequest.get(`/batches/${batchId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveBatch200), + orElse: unexpectedStatus + })) + ), + "cancelBatch": (batchId, options) => + HttpClientRequest.post(`/batches/${batchId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelBatch200), + orElse: unexpectedStatus + })) + ), + "listChatCompletions": (options) => + HttpClientRequest.get(`/chat/completions`).pipe( + HttpClientRequest.setUrlParams({ + "model": options?.params?.["model"] as any, + "metadata": options?.params?.["metadata"] as any, + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListChatCompletions200), + orElse: unexpectedStatus + })) + ), + "createChatCompletion": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateChatCompletion200), + orElse: unexpectedStatus + })) + ), + "createChatCompletionSse": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateChatCompletion200Sse) + ), + "getChatCompletion": (completionId, options) => + HttpClientRequest.get(`/chat/completions/${completionId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetChatCompletion200), + orElse: unexpectedStatus + })) + ), + "updateChatCompletion": (completionId, options) => + HttpClientRequest.post(`/chat/completions/${completionId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateChatCompletion200), + orElse: unexpectedStatus + })) + ), + "deleteChatCompletion": (completionId, options) => + HttpClientRequest.delete(`/chat/completions/${completionId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteChatCompletion200), + orElse: unexpectedStatus + })) + ), + "getChatCompletionMessages": (completionId, options) => + HttpClientRequest.get(`/chat/completions/${completionId}/messages`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetChatCompletionMessages200), + orElse: unexpectedStatus + })) + ), + "createCompletion": (options) => + HttpClientRequest.post(`/completions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCompletion200), + orElse: unexpectedStatus + })) + ), + "ListContainers": (options) => + HttpClientRequest.get(`/containers`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "name": options?.params?.["name"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListContainers200), + orElse: unexpectedStatus + })) + ), + "CreateContainer": (options) => + HttpClientRequest.post(`/containers`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateContainer200), + orElse: unexpectedStatus + })) + ), + "RetrieveContainer": (containerId, options) => + HttpClientRequest.get(`/containers/${containerId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveContainer200), + orElse: unexpectedStatus + })) + ), + "DeleteContainer": (containerId, options) => + HttpClientRequest.delete(`/containers/${containerId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "ListContainerFiles": (containerId, options) => + HttpClientRequest.get(`/containers/${containerId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListContainerFiles200), + orElse: unexpectedStatus + })) + ), + "CreateContainerFile": (containerId, options) => + HttpClientRequest.post(`/containers/${containerId}/files`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateContainerFile200), + orElse: unexpectedStatus + })) + ), + "RetrieveContainerFile": (containerId, fileId, options) => + HttpClientRequest.get(`/containers/${containerId}/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveContainerFile200), + orElse: unexpectedStatus + })) + ), + "DeleteContainerFile": (containerId, fileId, options) => + HttpClientRequest.delete(`/containers/${containerId}/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "RetrieveContainerFileContent": (containerId, fileId, options) => + HttpClientRequest.get(`/containers/${containerId}/files/${fileId}/content`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "listConversationItems": (conversationId, options) => + HttpClientRequest.get(`/conversations/${conversationId}/items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "include": options?.params?.["include"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListConversationItems200), + orElse: unexpectedStatus + })) + ), + "createConversationItems": (conversationId, options) => + HttpClientRequest.post(`/conversations/${conversationId}/items`).pipe( + HttpClientRequest.setUrlParams({ "include": options.params?.["include"] as any }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateConversationItems200), + orElse: unexpectedStatus + })) + ), + "getConversationItem": (conversationId, itemId, options) => + HttpClientRequest.get(`/conversations/${conversationId}/items/${itemId}`).pipe( + HttpClientRequest.setUrlParams({ "include": options?.params?.["include"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetConversationItem200), + orElse: unexpectedStatus + })) + ), + "deleteConversationItem": (conversationId, itemId, options) => + HttpClientRequest.delete(`/conversations/${conversationId}/items/${itemId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteConversationItem200), + orElse: unexpectedStatus + })) + ), + "createEmbedding": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEmbedding200), + orElse: unexpectedStatus + })) + ), + "listEvals": (options) => + HttpClientRequest.get(`/evals`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "order_by": options?.params?.["order_by"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEvals200), + orElse: unexpectedStatus + })) + ), + "createEval": (options) => + HttpClientRequest.post(`/evals`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEval201), + orElse: unexpectedStatus + })) + ), + "getEval": (evalId, options) => + HttpClientRequest.get(`/evals/${evalId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetEval200), + orElse: unexpectedStatus + })) + ), + "updateEval": (evalId, options) => + HttpClientRequest.post(`/evals/${evalId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateEval200), + orElse: unexpectedStatus + })) + ), + "deleteEval": (evalId, options) => + HttpClientRequest.delete(`/evals/${evalId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteEval200), + "404": decodeError("DeleteEval404", DeleteEval404), + orElse: unexpectedStatus + })) + ), + "getEvalRuns": (evalId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "status": options?.params?.["status"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetEvalRuns200), + orElse: unexpectedStatus + })) + ), + "createEvalRun": (evalId, options) => + HttpClientRequest.post(`/evals/${evalId}/runs`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEvalRun201), + "400": decodeError("CreateEvalRun400", CreateEvalRun400), + orElse: unexpectedStatus + })) + ), + "getEvalRun": (evalId, runId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetEvalRun200), + orElse: unexpectedStatus + })) + ), + "cancelEvalRun": (evalId, runId, options) => + HttpClientRequest.post(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelEvalRun200), + orElse: unexpectedStatus + })) + ), + "deleteEvalRun": (evalId, runId, options) => + HttpClientRequest.delete(`/evals/${evalId}/runs/${runId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteEvalRun200), + "404": decodeError("DeleteEvalRun404", DeleteEvalRun404), + orElse: unexpectedStatus + })) + ), + "getEvalRunOutputItems": (evalId, runId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}/output_items`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "status": options?.params?.["status"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetEvalRunOutputItems200), + orElse: unexpectedStatus + })) + ), + "getEvalRunOutputItem": (evalId, runId, outputItemId, options) => + HttpClientRequest.get(`/evals/${evalId}/runs/${runId}/output_items/${outputItemId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetEvalRunOutputItem200), + orElse: unexpectedStatus + })) + ), + "listFiles": (options) => + HttpClientRequest.get(`/files`).pipe( + HttpClientRequest.setUrlParams({ + "purpose": options?.params?.["purpose"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFiles200), + orElse: unexpectedStatus + })) + ), + "createFile": (options) => + HttpClientRequest.post(`/files`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateFile200), + orElse: unexpectedStatus + })) + ), + "retrieveFile": (fileId, options) => + HttpClientRequest.get(`/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveFile200), + orElse: unexpectedStatus + })) + ), + "deleteFile": (fileId, options) => + HttpClientRequest.delete(`/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFile200), + orElse: unexpectedStatus + })) + ), + "downloadFile": (fileId, options) => + HttpClientRequest.get(`/files/${fileId}/content`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DownloadFile200), + orElse: unexpectedStatus + })) + ), + "runGrader": (options) => + HttpClientRequest.post(`/fine_tuning/alpha/graders/run`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RunGrader200), + orElse: unexpectedStatus + })) + ), + "validateGrader": (options) => + HttpClientRequest.post(`/fine_tuning/alpha/graders/validate`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ValidateGrader200), + orElse: unexpectedStatus + })) + ), + "listFineTuningCheckpointPermissions": (fineTunedModelCheckpoint, options) => + HttpClientRequest.get(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions`).pipe( + HttpClientRequest.setUrlParams({ + "project_id": options?.params?.["project_id"] as any, + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningCheckpointPermissions200), + orElse: unexpectedStatus + })) + ), + "createFineTuningCheckpointPermission": (fineTunedModelCheckpoint, options) => + HttpClientRequest.post(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateFineTuningCheckpointPermission200), + orElse: unexpectedStatus + })) + ), + "deleteFineTuningCheckpointPermission": (fineTunedModelCheckpoint, permissionId, options) => + HttpClientRequest.delete(`/fine_tuning/checkpoints/${fineTunedModelCheckpoint}/permissions/${permissionId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFineTuningCheckpointPermission200), + orElse: unexpectedStatus + })) + ), + "listPaginatedFineTuningJobs": (options) => + HttpClientRequest.get(`/fine_tuning/jobs`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any, + "metadata": options?.params?.["metadata"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPaginatedFineTuningJobs200), + orElse: unexpectedStatus + })) + ), + "createFineTuningJob": (options) => + HttpClientRequest.post(`/fine_tuning/jobs`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateFineTuningJob200), + orElse: unexpectedStatus + })) + ), + "retrieveFineTuningJob": (fineTuningJobId, options) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveFineTuningJob200), + orElse: unexpectedStatus + })) + ), + "cancelFineTuningJob": (fineTuningJobId, options) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelFineTuningJob200), + orElse: unexpectedStatus + })) + ), + "listFineTuningJobCheckpoints": (fineTuningJobId, options) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}/checkpoints`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningJobCheckpoints200), + orElse: unexpectedStatus + })) + ), + "listFineTuningEvents": (fineTuningJobId, options) => + HttpClientRequest.get(`/fine_tuning/jobs/${fineTuningJobId}/events`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFineTuningEvents200), + orElse: unexpectedStatus + })) + ), + "pauseFineTuningJob": (fineTuningJobId, options) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/pause`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(PauseFineTuningJob200), + orElse: unexpectedStatus + })) + ), + "resumeFineTuningJob": (fineTuningJobId, options) => + HttpClientRequest.post(`/fine_tuning/jobs/${fineTuningJobId}/resume`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ResumeFineTuningJob200), + orElse: unexpectedStatus + })) + ), + "createImageEdit": (options) => + HttpClientRequest.post(`/images/edits`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateImageEdit200), + orElse: unexpectedStatus + })) + ), + "createImageEditSse": (options) => + HttpClientRequest.post(`/images/edits`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + sseRequest(CreateImageEdit200Sse) + ), + "createImage": (options) => + HttpClientRequest.post(`/images/generations`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateImage200), + orElse: unexpectedStatus + })) + ), + "createImageSse": (options) => + HttpClientRequest.post(`/images/generations`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateImage200Sse) + ), + "createImageVariation": (options) => + HttpClientRequest.post(`/images/variations`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateImageVariation200), + orElse: unexpectedStatus + })) + ), + "listModels": (options) => + HttpClientRequest.get(`/models`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModels200), + orElse: unexpectedStatus + })) + ), + "retrieveModel": (model, options) => + HttpClientRequest.get(`/models/${model}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveModel200), + orElse: unexpectedStatus + })) + ), + "deleteModel": (model, options) => + HttpClientRequest.delete(`/models/${model}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteModel200), + orElse: unexpectedStatus + })) + ), + "createModeration": (options) => + HttpClientRequest.post(`/moderations`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateModeration200), + orElse: unexpectedStatus + })) + ), + "adminApiKeysList": (options) => + HttpClientRequest.get(`/organization/admin_api_keys`).pipe( + HttpClientRequest.setUrlParams({ + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKeysList200), + orElse: unexpectedStatus + })) + ), + "adminApiKeysCreate": (options) => + HttpClientRequest.post(`/organization/admin_api_keys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKeysCreate200), + orElse: unexpectedStatus + })) + ), + "adminApiKeysGet": (keyId, options) => + HttpClientRequest.get(`/organization/admin_api_keys/${keyId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKeysGet200), + orElse: unexpectedStatus + })) + ), + "adminApiKeysDelete": (keyId, options) => + HttpClientRequest.delete(`/organization/admin_api_keys/${keyId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AdminApiKeysDelete200), + orElse: unexpectedStatus + })) + ), + "listAuditLogs": (options) => + HttpClientRequest.get(`/organization/audit_logs`).pipe( + HttpClientRequest.setUrlParams({ + "effective_at[gt]": options?.params?.["effective_at[gt]"] as any, + "effective_at[gte]": options?.params?.["effective_at[gte]"] as any, + "effective_at[lt]": options?.params?.["effective_at[lt]"] as any, + "effective_at[lte]": options?.params?.["effective_at[lte]"] as any, + "project_ids[]": options?.params?.["project_ids[]"] as any, + "event_types[]": options?.params?.["event_types[]"] as any, + "actor_ids[]": options?.params?.["actor_ids[]"] as any, + "actor_emails[]": options?.params?.["actor_emails[]"] as any, + "resource_ids[]": options?.params?.["resource_ids[]"] as any, + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListAuditLogs200), + orElse: unexpectedStatus + })) + ), + "listOrganizationCertificates": (options) => + HttpClientRequest.get(`/organization/certificates`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListOrganizationCertificates200), + orElse: unexpectedStatus + })) + ), + "uploadCertificate": (options) => + HttpClientRequest.post(`/organization/certificates`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UploadCertificate200), + orElse: unexpectedStatus + })) + ), + "activateOrganizationCertificates": (options) => + HttpClientRequest.post(`/organization/certificates/activate`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ActivateOrganizationCertificates200), + orElse: unexpectedStatus + })) + ), + "deactivateOrganizationCertificates": (options) => + HttpClientRequest.post(`/organization/certificates/deactivate`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeactivateOrganizationCertificates200), + orElse: unexpectedStatus + })) + ), + "getCertificate": (certificateId, options) => + HttpClientRequest.get(`/organization/certificates/${certificateId}`).pipe( + HttpClientRequest.setUrlParams({ "include": options?.params?.["include"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCertificate200), + orElse: unexpectedStatus + })) + ), + "modifyCertificate": (certificateId, options) => + HttpClientRequest.post(`/organization/certificates/${certificateId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyCertificate200), + orElse: unexpectedStatus + })) + ), + "deleteCertificate": (certificateId, options) => + HttpClientRequest.delete(`/organization/certificates/${certificateId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteCertificate200), + orElse: unexpectedStatus + })) + ), + "usageCosts": (options) => + HttpClientRequest.get(`/organization/costs`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageCosts200), + orElse: unexpectedStatus + })) + ), + "listGroups": (options) => + HttpClientRequest.get(`/organization/groups`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGroups200), + orElse: unexpectedStatus + })) + ), + "createGroup": (options) => + HttpClientRequest.post(`/organization/groups`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateGroup200), + orElse: unexpectedStatus + })) + ), + "updateGroup": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateGroup200), + orElse: unexpectedStatus + })) + ), + "deleteGroup": (groupId, options) => + HttpClientRequest.delete(`/organization/groups/${groupId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteGroup200), + orElse: unexpectedStatus + })) + ), + "listGroupRoleAssignments": (groupId, options) => + HttpClientRequest.get(`/organization/groups/${groupId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGroupRoleAssignments200), + orElse: unexpectedStatus + })) + ), + "assignGroupRole": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssignGroupRole200), + orElse: unexpectedStatus + })) + ), + "unassignGroupRole": (groupId, roleId, options) => + HttpClientRequest.delete(`/organization/groups/${groupId}/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UnassignGroupRole200), + orElse: unexpectedStatus + })) + ), + "listGroupUsers": (groupId, options) => + HttpClientRequest.get(`/organization/groups/${groupId}/users`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGroupUsers200), + orElse: unexpectedStatus + })) + ), + "addGroupUser": (groupId, options) => + HttpClientRequest.post(`/organization/groups/${groupId}/users`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AddGroupUser200), + orElse: unexpectedStatus + })) + ), + "removeGroupUser": (groupId, userId, options) => + HttpClientRequest.delete(`/organization/groups/${groupId}/users/${userId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RemoveGroupUser200), + orElse: unexpectedStatus + })) + ), + "listInvites": (options) => + HttpClientRequest.get(`/organization/invites`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListInvites200), + orElse: unexpectedStatus + })) + ), + "inviteUser": (options) => + HttpClientRequest.post(`/organization/invites`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(InviteUser200), + orElse: unexpectedStatus + })) + ), + "retrieveInvite": (inviteId, options) => + HttpClientRequest.get(`/organization/invites/${inviteId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveInvite200), + orElse: unexpectedStatus + })) + ), + "deleteInvite": (inviteId, options) => + HttpClientRequest.delete(`/organization/invites/${inviteId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteInvite200), + orElse: unexpectedStatus + })) + ), + "listProjects": (options) => + HttpClientRequest.get(`/organization/projects`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "include_archived": options?.params?.["include_archived"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjects200), + orElse: unexpectedStatus + })) + ), + "createProject": (options) => + HttpClientRequest.post(`/organization/projects`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateProject200), + orElse: unexpectedStatus + })) + ), + "retrieveProject": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProject200), + orElse: unexpectedStatus + })) + ), + "modifyProject": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyProject200), + "400": decodeError("ModifyProject400", ModifyProject400), + orElse: unexpectedStatus + })) + ), + "listProjectApiKeys": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/api_keys`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectApiKeys200), + orElse: unexpectedStatus + })) + ), + "retrieveProjectApiKey": (projectId, apiKeyId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/api_keys/${apiKeyId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProjectApiKey200), + orElse: unexpectedStatus + })) + ), + "deleteProjectApiKey": (projectId, apiKeyId, options) => + HttpClientRequest.delete(`/organization/projects/${projectId}/api_keys/${apiKeyId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProjectApiKey200), + "400": decodeError("DeleteProjectApiKey400", DeleteProjectApiKey400), + orElse: unexpectedStatus + })) + ), + "archiveProject": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/archive`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ArchiveProject200), + orElse: unexpectedStatus + })) + ), + "listProjectCertificates": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/certificates`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectCertificates200), + orElse: unexpectedStatus + })) + ), + "activateProjectCertificates": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/certificates/activate`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ActivateProjectCertificates200), + orElse: unexpectedStatus + })) + ), + "deactivateProjectCertificates": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/certificates/deactivate`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeactivateProjectCertificates200), + orElse: unexpectedStatus + })) + ), + "listProjectGroups": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/groups`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectGroups200), + orElse: unexpectedStatus + })) + ), + "addProjectGroup": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/groups`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AddProjectGroup200), + orElse: unexpectedStatus + })) + ), + "removeProjectGroup": (projectId, groupId, options) => + HttpClientRequest.delete(`/organization/projects/${projectId}/groups/${groupId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RemoveProjectGroup200), + orElse: unexpectedStatus + })) + ), + "retrieveProjectHostedToolPermissions": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/hosted_tool_permissions`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProjectHostedToolPermissions200), + orElse: unexpectedStatus + })) + ), + "updateProjectHostedToolPermissions": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/hosted_tool_permissions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateProjectHostedToolPermissions200), + orElse: unexpectedStatus + })) + ), + "retrieveProjectModelPermissions": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/model_permissions`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProjectModelPermissions200), + orElse: unexpectedStatus + })) + ), + "updateProjectModelPermissions": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/model_permissions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateProjectModelPermissions200), + orElse: unexpectedStatus + })) + ), + "deleteProjectModelPermissions": (projectId, options) => + HttpClientRequest.delete(`/organization/projects/${projectId}/model_permissions`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProjectModelPermissions200), + orElse: unexpectedStatus + })) + ), + "listProjectRateLimits": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/rate_limits`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectRateLimits200), + orElse: unexpectedStatus + })) + ), + "updateProjectRateLimits": (projectId, rateLimitId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/rate_limits/${rateLimitId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateProjectRateLimits200), + "400": decodeError("UpdateProjectRateLimits400", UpdateProjectRateLimits400), + orElse: unexpectedStatus + })) + ), + "listProjectServiceAccounts": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/service_accounts`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectServiceAccounts200), + "400": decodeError("ListProjectServiceAccounts400", ListProjectServiceAccounts400), + orElse: unexpectedStatus + })) + ), + "createProjectServiceAccount": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/service_accounts`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateProjectServiceAccount200), + "400": decodeError("CreateProjectServiceAccount400", CreateProjectServiceAccount400), + orElse: unexpectedStatus + })) + ), + "retrieveProjectServiceAccount": (projectId, serviceAccountId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/service_accounts/${serviceAccountId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProjectServiceAccount200), + orElse: unexpectedStatus + })) + ), + "deleteProjectServiceAccount": (projectId, serviceAccountId, options) => + HttpClientRequest.delete(`/organization/projects/${projectId}/service_accounts/${serviceAccountId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProjectServiceAccount200), + orElse: unexpectedStatus + })) + ), + "listProjectUsers": (projectId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/users`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectUsers200), + "400": decodeError("ListProjectUsers400", ListProjectUsers400), + orElse: unexpectedStatus + })) + ), + "createProjectUser": (projectId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/users`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateProjectUser200), + "400": decodeError("CreateProjectUser400", CreateProjectUser400), + orElse: unexpectedStatus + })) + ), + "retrieveProjectUser": (projectId, userId, options) => + HttpClientRequest.get(`/organization/projects/${projectId}/users/${userId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveProjectUser200), + orElse: unexpectedStatus + })) + ), + "modifyProjectUser": (projectId, userId, options) => + HttpClientRequest.post(`/organization/projects/${projectId}/users/${userId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyProjectUser200), + "400": decodeError("ModifyProjectUser400", ModifyProjectUser400), + orElse: unexpectedStatus + })) + ), + "deleteProjectUser": (projectId, userId, options) => + HttpClientRequest.delete(`/organization/projects/${projectId}/users/${userId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProjectUser200), + "400": decodeError("DeleteProjectUser400", DeleteProjectUser400), + orElse: unexpectedStatus + })) + ), + "listRoles": (options) => + HttpClientRequest.get(`/organization/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListRoles200), + orElse: unexpectedStatus + })) + ), + "createRole": (options) => + HttpClientRequest.post(`/organization/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRole200), + orElse: unexpectedStatus + })) + ), + "updateRole": (roleId, options) => + HttpClientRequest.post(`/organization/roles/${roleId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateRole200), + orElse: unexpectedStatus + })) + ), + "deleteRole": (roleId, options) => + HttpClientRequest.delete(`/organization/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteRole200), + orElse: unexpectedStatus + })) + ), + "usageAudioSpeeches": (options) => + HttpClientRequest.get(`/organization/usage/audio_speeches`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageAudioSpeeches200), + orElse: unexpectedStatus + })) + ), + "usageAudioTranscriptions": (options) => + HttpClientRequest.get(`/organization/usage/audio_transcriptions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageAudioTranscriptions200), + orElse: unexpectedStatus + })) + ), + "usageCodeInterpreterSessions": (options) => + HttpClientRequest.get(`/organization/usage/code_interpreter_sessions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageCodeInterpreterSessions200), + orElse: unexpectedStatus + })) + ), + "usageCompletions": (options) => + HttpClientRequest.get(`/organization/usage/completions`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "batch": options.params["batch"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageCompletions200), + orElse: unexpectedStatus + })) + ), + "usageEmbeddings": (options) => + HttpClientRequest.get(`/organization/usage/embeddings`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageEmbeddings200), + orElse: unexpectedStatus + })) + ), + "usageFileSearchCalls": (options) => + HttpClientRequest.get(`/organization/usage/file_search_calls`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "vector_store_ids": options.params["vector_store_ids"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageFileSearchCalls200), + orElse: unexpectedStatus + })) + ), + "usageImages": (options) => + HttpClientRequest.get(`/organization/usage/images`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "sources": options.params["sources"] as any, + "sizes": options.params["sizes"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageImages200), + orElse: unexpectedStatus + })) + ), + "usageModerations": (options) => + HttpClientRequest.get(`/organization/usage/moderations`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageModerations200), + orElse: unexpectedStatus + })) + ), + "usageVectorStores": (options) => + HttpClientRequest.get(`/organization/usage/vector_stores`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageVectorStores200), + orElse: unexpectedStatus + })) + ), + "usageWebSearchCalls": (options) => + HttpClientRequest.get(`/organization/usage/web_search_calls`).pipe( + HttpClientRequest.setUrlParams({ + "start_time": options.params["start_time"] as any, + "end_time": options.params["end_time"] as any, + "bucket_width": options.params["bucket_width"] as any, + "project_ids": options.params["project_ids"] as any, + "user_ids": options.params["user_ids"] as any, + "api_key_ids": options.params["api_key_ids"] as any, + "models": options.params["models"] as any, + "context_levels": options.params["context_levels"] as any, + "group_by": options.params["group_by"] as any, + "limit": options.params["limit"] as any, + "page": options.params["page"] as any + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UsageWebSearchCalls200), + orElse: unexpectedStatus + })) + ), + "listUsers": (options) => + HttpClientRequest.get(`/organization/users`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "emails": options?.params?.["emails"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListUsers200), + orElse: unexpectedStatus + })) + ), + "retrieveUser": (userId, options) => + HttpClientRequest.get(`/organization/users/${userId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveUser200), + orElse: unexpectedStatus + })) + ), + "modifyUser": (userId, options) => + HttpClientRequest.post(`/organization/users/${userId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyUser200), + orElse: unexpectedStatus + })) + ), + "deleteUser": (userId, options) => + HttpClientRequest.delete(`/organization/users/${userId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteUser200), + orElse: unexpectedStatus + })) + ), + "listUserRoleAssignments": (userId, options) => + HttpClientRequest.get(`/organization/users/${userId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListUserRoleAssignments200), + orElse: unexpectedStatus + })) + ), + "assignUserRole": (userId, options) => + HttpClientRequest.post(`/organization/users/${userId}/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssignUserRole200), + orElse: unexpectedStatus + })) + ), + "unassignUserRole": (userId, roleId, options) => + HttpClientRequest.delete(`/organization/users/${userId}/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UnassignUserRole200), + orElse: unexpectedStatus + })) + ), + "listProjectGroupRoleAssignments": (projectId, groupId, options) => + HttpClientRequest.get(`/projects/${projectId}/groups/${groupId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectGroupRoleAssignments200), + orElse: unexpectedStatus + })) + ), + "assignProjectGroupRole": (projectId, groupId, options) => + HttpClientRequest.post(`/projects/${projectId}/groups/${groupId}/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssignProjectGroupRole200), + orElse: unexpectedStatus + })) + ), + "unassignProjectGroupRole": (projectId, groupId, roleId, options) => + HttpClientRequest.delete(`/projects/${projectId}/groups/${groupId}/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UnassignProjectGroupRole200), + orElse: unexpectedStatus + })) + ), + "listProjectRoles": (projectId, options) => + HttpClientRequest.get(`/projects/${projectId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectRoles200), + orElse: unexpectedStatus + })) + ), + "createProjectRole": (projectId, options) => + HttpClientRequest.post(`/projects/${projectId}/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateProjectRole200), + orElse: unexpectedStatus + })) + ), + "updateProjectRole": (projectId, roleId, options) => + HttpClientRequest.post(`/projects/${projectId}/roles/${roleId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateProjectRole200), + orElse: unexpectedStatus + })) + ), + "deleteProjectRole": (projectId, roleId, options) => + HttpClientRequest.delete(`/projects/${projectId}/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProjectRole200), + orElse: unexpectedStatus + })) + ), + "listProjectUserRoleAssignments": (projectId, userId, options) => + HttpClientRequest.get(`/projects/${projectId}/users/${userId}/roles`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "after": options?.params?.["after"] as any, + "order": options?.params?.["order"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProjectUserRoleAssignments200), + orElse: unexpectedStatus + })) + ), + "assignProjectUserRole": (projectId, userId, options) => + HttpClientRequest.post(`/projects/${projectId}/users/${userId}/roles`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AssignProjectUserRole200), + orElse: unexpectedStatus + })) + ), + "unassignProjectUserRole": (projectId, userId, roleId, options) => + HttpClientRequest.delete(`/projects/${projectId}/users/${userId}/roles/${roleId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UnassignProjectUserRole200), + orElse: unexpectedStatus + })) + ), + "createRealtimeCall": (options) => + HttpClientRequest.post(`/realtime/calls`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + orElse: unexpectedStatus + })) + ), + "acceptRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/accept`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "hangupRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/hangup`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "referRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/refer`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "rejectRealtimeCall": (callId, options) => + HttpClientRequest.post(`/realtime/calls/${callId}/reject`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "createRealtimeClientSecret": (options) => + HttpClientRequest.post(`/realtime/client_secrets`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRealtimeClientSecret200), + orElse: unexpectedStatus + })) + ), + "createRealtimeSession": (options) => + HttpClientRequest.post(`/realtime/sessions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRealtimeSession200), + orElse: unexpectedStatus + })) + ), + "createRealtimeTranscriptionSession": (options) => + HttpClientRequest.post(`/realtime/transcription_sessions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRealtimeTranscriptionSession200), + orElse: unexpectedStatus + })) + ), + "createRealtimeTranslationClientSecret": (options) => + HttpClientRequest.post(`/realtime/translations/client_secrets`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRealtimeTranslationClientSecret200), + orElse: unexpectedStatus + })) + ), + "createResponse": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateResponse200), + orElse: unexpectedStatus + })) + ), + "createResponseSse": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateResponse200Sse) + ), + "getResponse": (responseId, options) => + HttpClientRequest.get(`/responses/${responseId}`).pipe( + HttpClientRequest.setUrlParams({ + "include": options?.params?.["include"] as any, + "stream": options?.params?.["stream"] as any, + "starting_after": options?.params?.["starting_after"] as any, + "include_obfuscation": options?.params?.["include_obfuscation"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetResponse200), + orElse: unexpectedStatus + })) + ), + "deleteResponse": (responseId, options) => + HttpClientRequest.delete(`/responses/${responseId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "404": decodeError("DeleteResponse404", DeleteResponse404), + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "cancelResponse": (responseId, options) => + HttpClientRequest.post(`/responses/${responseId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelResponse200), + "404": decodeError("CancelResponse404", CancelResponse404), + orElse: unexpectedStatus + })) + ), + "listInputItems": (responseId, options) => + HttpClientRequest.get(`/responses/${responseId}/input_items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "include": options?.params?.["include"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListInputItems200), + orElse: unexpectedStatus + })) + ), + "createThread": (options) => + HttpClientRequest.post(`/threads`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateThread200), + orElse: unexpectedStatus + })) + ), + "createThreadAndRun": (options) => + HttpClientRequest.post(`/threads/runs`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateThreadAndRun200), + orElse: unexpectedStatus + })) + ), + "getThread": (threadId, options) => + HttpClientRequest.get(`/threads/${threadId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetThread200), + orElse: unexpectedStatus + })) + ), + "modifyThread": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyThread200), + orElse: unexpectedStatus + })) + ), + "deleteThread": (threadId, options) => + HttpClientRequest.delete(`/threads/${threadId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteThread200), + orElse: unexpectedStatus + })) + ), + "listMessages": (threadId, options) => + HttpClientRequest.get(`/threads/${threadId}/messages`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any, + "run_id": options?.params?.["run_id"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMessages200), + orElse: unexpectedStatus + })) + ), + "createMessage": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}/messages`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateMessage200), + orElse: unexpectedStatus + })) + ), + "getMessage": (threadId, messageId, options) => + HttpClientRequest.get(`/threads/${threadId}/messages/${messageId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetMessage200), + orElse: unexpectedStatus + })) + ), + "modifyMessage": (threadId, messageId, options) => + HttpClientRequest.post(`/threads/${threadId}/messages/${messageId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyMessage200), + orElse: unexpectedStatus + })) + ), + "deleteMessage": (threadId, messageId, options) => + HttpClientRequest.delete(`/threads/${threadId}/messages/${messageId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteMessage200), + orElse: unexpectedStatus + })) + ), + "listRuns": (threadId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListRuns200), + orElse: unexpectedStatus + })) + ), + "createRun": (threadId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs`).pipe( + HttpClientRequest.setUrlParams({ "include[]": options.params?.["include[]"] as any }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRun200), + orElse: unexpectedStatus + })) + ), + "getRun": (threadId, runId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetRun200), + orElse: unexpectedStatus + })) + ), + "modifyRun": (threadId, runId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyRun200), + orElse: unexpectedStatus + })) + ), + "cancelRun": (threadId, runId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelRun200), + orElse: unexpectedStatus + })) + ), + "listRunSteps": (threadId, runId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}/steps`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any, + "include[]": options?.params?.["include[]"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListRunSteps200), + orElse: unexpectedStatus + })) + ), + "getRunStep": (threadId, runId, stepId, options) => + HttpClientRequest.get(`/threads/${threadId}/runs/${runId}/steps/${stepId}`).pipe( + HttpClientRequest.setUrlParams({ "include[]": options?.params?.["include[]"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetRunStep200), + orElse: unexpectedStatus + })) + ), + "submitToolOuputsToRun": (threadId, runId, options) => + HttpClientRequest.post(`/threads/${threadId}/runs/${runId}/submit_tool_outputs`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SubmitToolOuputsToRun200), + orElse: unexpectedStatus + })) + ), + "createUpload": (options) => + HttpClientRequest.post(`/uploads`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateUpload200), + orElse: unexpectedStatus + })) + ), + "cancelUpload": (uploadId, options) => + HttpClientRequest.post(`/uploads/${uploadId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelUpload200), + orElse: unexpectedStatus + })) + ), + "completeUpload": (uploadId, options) => + HttpClientRequest.post(`/uploads/${uploadId}/complete`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CompleteUpload200), + orElse: unexpectedStatus + })) + ), + "addUploadPart": (uploadId, options) => + HttpClientRequest.post(`/uploads/${uploadId}/parts`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(AddUploadPart200), + orElse: unexpectedStatus + })) + ), + "listVectorStores": (options) => + HttpClientRequest.get(`/vector_stores`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVectorStores200), + orElse: unexpectedStatus + })) + ), + "createVectorStore": (options) => + HttpClientRequest.post(`/vector_stores`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVectorStore200), + orElse: unexpectedStatus + })) + ), + "getVectorStore": (vectorStoreId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVectorStore200), + orElse: unexpectedStatus + })) + ), + "modifyVectorStore": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ModifyVectorStore200), + orElse: unexpectedStatus + })) + ), + "deleteVectorStore": (vectorStoreId, options) => + HttpClientRequest.delete(`/vector_stores/${vectorStoreId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVectorStore200), + orElse: unexpectedStatus + })) + ), + "createVectorStoreFileBatch": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/file_batches`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVectorStoreFileBatch200), + orElse: unexpectedStatus + })) + ), + "getVectorStoreFileBatch": (vectorStoreId, batchId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/file_batches/${batchId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVectorStoreFileBatch200), + orElse: unexpectedStatus + })) + ), + "cancelVectorStoreFileBatch": (vectorStoreId, batchId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/file_batches/${batchId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelVectorStoreFileBatch200), + orElse: unexpectedStatus + })) + ), + "listFilesInVectorStoreBatch": (vectorStoreId, batchId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/file_batches/${batchId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any, + "filter": options?.params?.["filter"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFilesInVectorStoreBatch200), + orElse: unexpectedStatus + })) + ), + "listVectorStoreFiles": (vectorStoreId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any, + "filter": options?.params?.["filter"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVectorStoreFiles200), + orElse: unexpectedStatus + })) + ), + "createVectorStoreFile": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/files`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVectorStoreFile200), + orElse: unexpectedStatus + })) + ), + "getVectorStoreFile": (vectorStoreId, fileId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVectorStoreFile200), + orElse: unexpectedStatus + })) + ), + "updateVectorStoreFileAttributes": (vectorStoreId, fileId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateVectorStoreFileAttributes200), + orElse: unexpectedStatus + })) + ), + "deleteVectorStoreFile": (vectorStoreId, fileId, options) => + HttpClientRequest.delete(`/vector_stores/${vectorStoreId}/files/${fileId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVectorStoreFile200), + orElse: unexpectedStatus + })) + ), + "retrieveVectorStoreFileContent": (vectorStoreId, fileId, options) => + HttpClientRequest.get(`/vector_stores/${vectorStoreId}/files/${fileId}/content`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveVectorStoreFileContent200), + orElse: unexpectedStatus + })) + ), + "searchVectorStore": (vectorStoreId, options) => + HttpClientRequest.post(`/vector_stores/${vectorStoreId}/search`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SearchVectorStore200), + orElse: unexpectedStatus + })) + ), + "createConversation": (options) => + HttpClientRequest.post(`/conversations`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateConversation200), + orElse: unexpectedStatus + })) + ), + "getConversation": (conversationId, options) => + HttpClientRequest.get(`/conversations/${conversationId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetConversation200), + orElse: unexpectedStatus + })) + ), + "updateConversation": (conversationId, options) => + HttpClientRequest.post(`/conversations/${conversationId}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateConversation200), + orElse: unexpectedStatus + })) + ), + "deleteConversation": (conversationId, options) => + HttpClientRequest.delete(`/conversations/${conversationId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteConversation200), + orElse: unexpectedStatus + })) + ), + "ListVideos": (options) => + HttpClientRequest.get(`/videos`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVideos200), + orElse: unexpectedStatus + })) + ), + "createVideo": (options) => + HttpClientRequest.post(`/videos`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideo200), + orElse: unexpectedStatus + })) + ), + "CreateVideoCharacter": (options) => + HttpClientRequest.post(`/videos/characters`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideoCharacter200), + orElse: unexpectedStatus + })) + ), + "GetVideoCharacter": (characterId, options) => + HttpClientRequest.get(`/videos/characters/${characterId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVideoCharacter200), + orElse: unexpectedStatus + })) + ), + "CreateVideoEdit": (options) => + HttpClientRequest.post(`/videos/edits`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideoEdit200), + orElse: unexpectedStatus + })) + ), + "CreateVideoExtend": (options) => + HttpClientRequest.post(`/videos/extensions`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideoExtend200), + orElse: unexpectedStatus + })) + ), + "GetVideo": (videoId, options) => + HttpClientRequest.get(`/videos/${videoId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVideo200), + orElse: unexpectedStatus + })) + ), + "DeleteVideo": (videoId, options) => + HttpClientRequest.delete(`/videos/${videoId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteVideo200), + orElse: unexpectedStatus + })) + ), + "RetrieveVideoContent": (videoId, options) => + HttpClientRequest.get(`/videos/${videoId}/content`).pipe( + HttpClientRequest.setUrlParams({ "variant": options?.params?.["variant"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(RetrieveVideoContent200), + orElse: unexpectedStatus + })) + ), + "CreateVideoRemix": (videoId, options) => + HttpClientRequest.post(`/videos/${videoId}/remix`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideoRemix200), + orElse: unexpectedStatus + })) + ), + "Getinputtokencounts": (options) => + HttpClientRequest.post(`/responses/input_tokens`).pipe( + HttpClientRequest.bodyUrlParams(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Getinputtokencounts200), + orElse: unexpectedStatus + })) + ), + "Compactconversation": (options) => + HttpClientRequest.post(`/responses/compact`).pipe( + HttpClientRequest.bodyUrlParams(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(Compactconversation200), + orElse: unexpectedStatus + })) + ), + "ListSkills": (options) => + HttpClientRequest.get(`/skills`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkills200), + orElse: unexpectedStatus + })) + ), + "CreateSkill": (options) => + HttpClientRequest.post(`/skills`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkill200), + orElse: unexpectedStatus + })) + ), + "GetSkill": (skillId, options) => + HttpClientRequest.get(`/skills/${skillId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkill200), + orElse: unexpectedStatus + })) + ), + "UpdateSkillDefaultVersion": (skillId, options) => + HttpClientRequest.post(`/skills/${skillId}`).pipe( + HttpClientRequest.bodyUrlParams(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateSkillDefaultVersion200), + orElse: unexpectedStatus + })) + ), + "DeleteSkill": (skillId, options) => + HttpClientRequest.delete(`/skills/${skillId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkill200), + orElse: unexpectedStatus + })) + ), + "GetSkillContent": (skillId, options) => + HttpClientRequest.get(`/skills/${skillId}/content`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillContent200), + orElse: unexpectedStatus + })) + ), + "ListSkillVersions": (skillId, options) => + HttpClientRequest.get(`/skills/${skillId}/versions`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListSkillVersions200), + orElse: unexpectedStatus + })) + ), + "CreateSkillVersion": (skillId, options) => + HttpClientRequest.post(`/skills/${skillId}/versions`).pipe( + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateSkillVersion200), + orElse: unexpectedStatus + })) + ), + "GetSkillVersion": (skillId, version, options) => + HttpClientRequest.get(`/skills/${skillId}/versions/${version}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillVersion200), + orElse: unexpectedStatus + })) + ), + "DeleteSkillVersion": (skillId, version, options) => + HttpClientRequest.delete(`/skills/${skillId}/versions/${version}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteSkillVersion200), + orElse: unexpectedStatus + })) + ), + "GetSkillVersionContent": (skillId, version, options) => + HttpClientRequest.get(`/skills/${skillId}/versions/${version}/content`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetSkillVersionContent200), + orElse: unexpectedStatus + })) + ), + "CancelChatSessionMethod": (sessionId, options) => + HttpClientRequest.post(`/chatkit/sessions/${sessionId}/cancel`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CancelChatSessionMethod200), + orElse: unexpectedStatus + })) + ), + "CreateChatSessionMethod": (options) => + HttpClientRequest.post(`/chatkit/sessions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateChatSessionMethod200), + orElse: unexpectedStatus + })) + ), + "ListThreadItemsMethod": (threadId, options) => + HttpClientRequest.get(`/chatkit/threads/${threadId}/items`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListThreadItemsMethod200), + orElse: unexpectedStatus + })) + ), + "GetThreadMethod": (threadId, options) => + HttpClientRequest.get(`/chatkit/threads/${threadId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetThreadMethod200), + orElse: unexpectedStatus + })) + ), + "DeleteThreadMethod": (threadId, options) => + HttpClientRequest.delete(`/chatkit/threads/${threadId}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteThreadMethod200), + orElse: unexpectedStatus + })) + ), + "ListThreadsMethod": (options) => + HttpClientRequest.get(`/chatkit/threads`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "order": options?.params?.["order"] as any, + "after": options?.params?.["after"] as any, + "before": options?.params?.["before"] as any, + "user": options?.params?.["user"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListThreadsMethod200), + orElse: unexpectedStatus + })) + ) + } +} + +export interface OpenAiClient { + readonly httpClient: HttpClient.HttpClient + /** + * Returns a list of assistants. + */ + readonly "listAssistants": ( + options: + | { readonly params?: typeof ListAssistantsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create an assistant with a model and instructions. + */ + readonly "createAssistant": ( + options: { readonly payload: typeof CreateAssistantRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves an assistant. + */ + readonly "getAssistant": ( + assistantId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies an assistant. + */ + readonly "modifyAssistant": ( + assistantId: string, + options: { readonly payload: typeof ModifyAssistantRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete an assistant. + */ + readonly "deleteAssistant": ( + assistantId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Generates audio from the input text. + * + * Returns the audio file content, or a stream of audio events. + */ + readonly "createSpeech": ( + options: { readonly payload: typeof CreateSpeechRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Generates audio from the input text. + * + * Returns the audio file content, or a stream of audio events. + */ + readonly "createSpeechSse": ( + options: { readonly payload: typeof CreateSpeechRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateSpeech200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateSpeech200Sse.DecodingServices + > + /** + * Generates audio from the input text. + * + * Returns the audio file content, or a stream of audio events. + */ + readonly "createSpeechStream": ( + options: { readonly payload: typeof CreateSpeechRequestJson.Encoded } + ) => Stream.Stream + /** + * Transcribes audio into the input language. + * + * Returns a transcription object in `json`, `diarized_json`, or `verbose_json` + * format, or a stream of transcript events. + */ + readonly "createTranscription": ( + options: { + readonly payload: typeof CreateTranscriptionRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Transcribes audio into the input language. + * + * Returns a transcription object in `json`, `diarized_json`, or `verbose_json` + * format, or a stream of transcript events. + */ + readonly "createTranscriptionSse": ( + options: { readonly payload: typeof CreateTranscriptionRequestFormData.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateTranscription200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateTranscription200Sse.DecodingServices + > + /** + * Translates audio into English. + */ + readonly "createTranslation": ( + options: { readonly payload: typeof CreateTranslationRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List consent recordings available to your organization for creating custom voices. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers. + */ + readonly "listVoiceConsents": ( + options: { + readonly params?: typeof ListVoiceConsentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Upload a consent recording that authorizes creation of a custom voice. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices) for requirements and best practices. Custom voices are limited to eligible customers. + */ + readonly "createVoiceConsent": ( + options: { + readonly payload: typeof CreateVoiceConsentRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieve consent recording metadata used for creating custom voices. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers. + */ + readonly "getVoiceConsent": ( + consentId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Update consent recording metadata used for creating custom voices. This endpoint updates metadata only and does not replace the underlying audio. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers. + */ + readonly "updateVoiceConsent": ( + consentId: string, + options: { readonly payload: typeof UpdateVoiceConsentRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a consent recording that was uploaded for creating custom voices. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices). Custom voices are limited to eligible customers. + */ + readonly "deleteVoiceConsent": ( + consentId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a custom voice you can use for audio output (for example, in Text-to-Speech and the Realtime API). This requires an audio sample and a previously uploaded consent recording. + * + * See the [custom voices guide](/docs/guides/text-to-speech#custom-voices) for requirements and best practices. Custom voices are limited to eligible customers. + */ + readonly "createVoice": ( + options: { readonly payload: typeof CreateVoiceRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List your organization's batches. + */ + readonly "listBatches": ( + options: + | { readonly params?: typeof ListBatchesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates and executes a batch from an uploaded file of requests + */ + readonly "createBatch": ( + options: { readonly payload: typeof CreateBatchRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a batch. + */ + readonly "retrieveBatch": ( + batchId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file. + */ + readonly "cancelBatch": ( + batchId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List stored Chat Completions. Only Chat Completions that have been stored + * with the `store` parameter set to `true` will be returned. + */ + readonly "listChatCompletions": ( + options: { + readonly params?: typeof ListChatCompletionsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * **Starting a new project?** We recommend trying [Responses](/docs/api-reference/responses) + * to take advantage of the latest OpenAI platform features. Compare + * [Chat Completions with Responses](/docs/guides/responses-vs-chat-completions?api-mode=responses). + * + * --- + * + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](/docs/guides/text-generation), [vision](/docs/guides/vision), + * and [audio](/docs/guides/audio) guides. + * + * Parameter support can differ depending on the model used to generate the + * response, particularly for newer reasoning models. Parameters that are only + * supported for reasoning models are noted below. For the current state of + * unsupported parameters in reasoning models, + * [refer to the reasoning guide](/docs/guides/reasoning). + * + * Returns a chat completion object, or a streamed sequence of chat completion + * chunk objects if the request is streamed. + */ + readonly "createChatCompletion": ( + options: { readonly payload: typeof CreateChatCompletionRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * **Starting a new project?** We recommend trying [Responses](/docs/api-reference/responses) + * to take advantage of the latest OpenAI platform features. Compare + * [Chat Completions with Responses](/docs/guides/responses-vs-chat-completions?api-mode=responses). + * + * --- + * + * Creates a model response for the given chat conversation. Learn more in the + * [text generation](/docs/guides/text-generation), [vision](/docs/guides/vision), + * and [audio](/docs/guides/audio) guides. + * + * Parameter support can differ depending on the model used to generate the + * response, particularly for newer reasoning models. Parameters that are only + * supported for reasoning models are noted below. For the current state of + * unsupported parameters in reasoning models, + * [refer to the reasoning guide](/docs/guides/reasoning). + * + * Returns a chat completion object, or a streamed sequence of chat completion + * chunk objects if the request is streamed. + */ + readonly "createChatCompletionSse": ( + options: { readonly payload: typeof CreateChatCompletionRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateChatCompletion200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateChatCompletion200Sse.DecodingServices + > + /** + * Get a stored chat completion. Only Chat Completions that have been created + * with the `store` parameter set to `true` will be returned. + */ + readonly "getChatCompletion": ( + completionId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modify a stored chat completion. Only Chat Completions that have been + * created with the `store` parameter set to `true` can be modified. Currently, + * the only supported modification is to update the `metadata` field. + */ + readonly "updateChatCompletion": ( + completionId: string, + options: { readonly payload: typeof UpdateChatCompletionRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a stored chat completion. Only Chat Completions that have been + * created with the `store` parameter set to `true` can be deleted. + */ + readonly "deleteChatCompletion": ( + completionId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get the messages in a stored chat completion. Only Chat Completions that + * have been created with the `store` parameter set to `true` will be + * returned. + */ + readonly "getChatCompletionMessages": ( + completionId: string, + options: { + readonly params?: typeof GetChatCompletionMessagesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a completion for the provided prompt and parameters. + * + * Returns a completion object, or a sequence of completion objects if the request is streamed. + */ + readonly "createCompletion": ( + options: { readonly payload: typeof CreateCompletionRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists containers. + */ + readonly "ListContainers": ( + options: + | { readonly params?: typeof ListContainersParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a container. + */ + readonly "CreateContainer": ( + options: { readonly payload: typeof CreateContainerRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a container. + */ + readonly "RetrieveContainer": ( + containerId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a container. + */ + readonly "DeleteContainer": ( + containerId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Lists container files. + */ + readonly "ListContainerFiles": ( + containerId: string, + options: { + readonly params?: typeof ListContainerFilesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a container file. + */ + readonly "CreateContainerFile": ( + containerId: string, + options: { + readonly payload: typeof CreateContainerFileRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a container file. + */ + readonly "RetrieveContainerFile": ( + containerId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a container file. + */ + readonly "DeleteContainerFile": ( + containerId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Retrieves a container file content. + */ + readonly "RetrieveContainerFileContent": ( + containerId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * List all items for a conversation with the given ID. + */ + readonly "listConversationItems": ( + conversationId: string, + options: { + readonly params?: typeof ListConversationItemsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create items in a conversation with the given ID. + */ + readonly "createConversationItems": ( + conversationId: string, + options: { + readonly params?: typeof CreateConversationItemsParams.Encoded | undefined + readonly payload: typeof CreateConversationItemsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get a single item from a conversation with the given IDs. + */ + readonly "getConversationItem": ( + conversationId: string, + itemId: string, + options: { + readonly params?: typeof GetConversationItemParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete an item from a conversation with the given IDs. + */ + readonly "deleteConversationItem": ( + conversationId: string, + itemId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates an embedding vector representing the input text. + */ + readonly "createEmbedding": ( + options: { readonly payload: typeof CreateEmbeddingRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List evaluations for a project. + */ + readonly "listEvals": ( + options: + | { readonly params?: typeof ListEvalsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create the structure of an evaluation that can be used to test a model's performance. + * An evaluation is a set of testing criteria and the config for a data source, which dictates the schema of the data used in the evaluation. After creating an evaluation, you can run it on different models and model parameters. We support several types of graders and datasources. + * For more information, see the [Evals guide](/docs/guides/evals). + */ + readonly "createEval": ( + options: { readonly payload: typeof CreateEvalRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get an evaluation by ID. + */ + readonly "getEval": ( + evalId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Update certain properties of an evaluation. + */ + readonly "updateEval": ( + evalId: string, + options: { readonly payload: typeof UpdateEvalRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete an evaluation. + */ + readonly "deleteEval": ( + evalId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | OpenAiClientError<"DeleteEval404", typeof DeleteEval404.Type> + > + /** + * Get a list of runs for an evaluation. + */ + readonly "getEvalRuns": ( + evalId: string, + options: + | { readonly params?: typeof GetEvalRunsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Kicks off a new run for a given evaluation, specifying the data source, and what model configuration to use to test. The datasource will be validated against the schema specified in the config of the evaluation. + */ + readonly "createEvalRun": ( + evalId: string, + options: { readonly payload: typeof CreateEvalRunRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | OpenAiClientError<"CreateEvalRun400", typeof CreateEvalRun400.Type> + > + /** + * Get an evaluation run by ID. + */ + readonly "getEvalRun": ( + evalId: string, + runId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancel an ongoing evaluation run. + */ + readonly "cancelEvalRun": ( + evalId: string, + runId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete an eval run. + */ + readonly "deleteEvalRun": ( + evalId: string, + runId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | OpenAiClientError<"DeleteEvalRun404", typeof DeleteEvalRun404.Type> + > + /** + * Get a list of output items for an evaluation run. + */ + readonly "getEvalRunOutputItems": ( + evalId: string, + runId: string, + options: { + readonly params?: typeof GetEvalRunOutputItemsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get an evaluation run output item by ID. + */ + readonly "getEvalRunOutputItem": ( + evalId: string, + runId: string, + outputItemId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of files. + */ + readonly "listFiles": ( + options: + | { readonly params?: typeof ListFilesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Upload a file that can be used across various endpoints. Individual files + * can be up to 512 MB, and each project can store up to 2.5 TB of files in + * total. There is no organization-wide storage limit. Uploads to this + * endpoint are rate-limited to 1,000 requests per minute per authenticated + * user. + * + * - The Assistants API supports files up to 2 million tokens and of specific + * file types. See the [Assistants Tools guide](/docs/assistants/tools) for + * details. + * - The Fine-tuning API only supports `.jsonl` files. The input also has + * certain required formats for fine-tuning + * [chat](/docs/api-reference/fine-tuning/chat-input) or + * [completions](/docs/api-reference/fine-tuning/completions-input) models. + * - The Batch API only supports `.jsonl` files up to 200 MB in size. The input + * also has a specific required + * [format](/docs/api-reference/batch/request-input). + * - For Retrieval or `file_search` ingestion, upload files here first. If + * you need to attach multiple uploaded files to the same vector store, use + * [`/vector_stores/{vector_store_id}/file_batches`](/docs/api-reference/vector-stores-file-batches/createBatch) + * instead of attaching them one by one. Vector store attachment has separate + * limits from file upload, including 2,000 attached files per minute per + * organization. + * + * Please [contact us](https://help.openai.com/) if you need to increase these + * storage limits. + */ + readonly "createFile": ( + options: { readonly payload: typeof CreateFileRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns information about a specific file. + */ + readonly "retrieveFile": ( + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a file and remove it from all vector stores. + */ + readonly "deleteFile": ( + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns the contents of the specified file. + */ + readonly "downloadFile": ( + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Run a grader. + */ + readonly "runGrader": ( + options: { readonly payload: typeof RunGraderRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Validate a grader. + */ + readonly "validateGrader": ( + options: { readonly payload: typeof ValidateGraderRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). + * + * Organization owners can use this endpoint to view all permissions for a fine-tuned model checkpoint. + */ + readonly "listFineTuningCheckpointPermissions": ( + fineTunedModelCheckpoint: string, + options: { + readonly params?: typeof ListFineTuningCheckpointPermissionsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * **NOTE:** Calling this endpoint requires an [admin API key](../admin-api-keys). + * + * This enables organization owners to share fine-tuned models with other projects in their organization. + */ + readonly "createFineTuningCheckpointPermission": ( + fineTunedModelCheckpoint: string, + options: { + readonly payload: typeof CreateFineTuningCheckpointPermissionRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). + * + * Organization owners can use this endpoint to delete a permission for a fine-tuned model checkpoint. + */ + readonly "deleteFineTuningCheckpointPermission": ( + fineTunedModelCheckpoint: string, + permissionId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List your organization's fine-tuning jobs + */ + readonly "listPaginatedFineTuningJobs": ( + options: { + readonly params?: typeof ListPaginatedFineTuningJobsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a fine-tuning job which begins the process of creating a new model from a given dataset. + * + * Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete. + * + * [Learn more about fine-tuning](/docs/guides/model-optimization) + */ + readonly "createFineTuningJob": ( + options: { readonly payload: typeof CreateFineTuningJobRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get info about a fine-tuning job. + * + * [Learn more about fine-tuning](/docs/guides/model-optimization) + */ + readonly "retrieveFineTuningJob": ( + fineTuningJobId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Immediately cancel a fine-tune job. + */ + readonly "cancelFineTuningJob": ( + fineTuningJobId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List checkpoints for a fine-tuning job. + */ + readonly "listFineTuningJobCheckpoints": ( + fineTuningJobId: string, + options: { + readonly params?: typeof ListFineTuningJobCheckpointsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get status updates for a fine-tuning job. + */ + readonly "listFineTuningEvents": ( + fineTuningJobId: string, + options: { + readonly params?: typeof ListFineTuningEventsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Pause a fine-tune job. + */ + readonly "pauseFineTuningJob": ( + fineTuningJobId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Resume a fine-tune job. + */ + readonly "resumeFineTuningJob": ( + fineTuningJobId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * You can call this endpoint with either: + * + * - `multipart/form-data`: use binary uploads via `image` (and optional `mask`). + * - `application/json`: use `images` (and optional `mask`) as references with either `image_url` or `file_id`. + * + * Note that JSON requests use `images` (array) instead of the multipart `image` field. + */ + readonly "createImageEdit": ( + options: { readonly payload: typeof CreateImageEditRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * You can call this endpoint with either: + * + * - `multipart/form-data`: use binary uploads via `image` (and optional `mask`). + * - `application/json`: use `images` (and optional `mask`) as references with either `image_url` or `file_id`. + * + * Note that JSON requests use `images` (array) instead of the multipart `image` field. + */ + readonly "createImageEditSse": ( + options: { readonly payload: typeof CreateImageEditRequestFormData.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateImageEdit200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateImageEdit200Sse.DecodingServices + > + /** + * Creates an image given a prompt. [Learn more](/docs/guides/images). + */ + readonly "createImage": ( + options: { readonly payload: typeof CreateImageRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates an image given a prompt. [Learn more](/docs/guides/images). + */ + readonly "createImageSse": ( + options: { readonly payload: typeof CreateImageRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateImage200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateImage200Sse.DecodingServices + > + /** + * Creates a variation of a given image. This endpoint only supports `dall-e-2`. + */ + readonly "createImageVariation": ( + options: { + readonly payload: typeof CreateImageVariationRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the currently available models, and provides basic information about each one such as the owner and availability. + */ + readonly "listModels": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a model instance, providing basic information about the model such as the owner and permissioning. + */ + readonly "retrieveModel": ( + model: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. + */ + readonly "deleteModel": ( + model: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Classifies if text and/or image inputs are potentially harmful. Learn + * more in the [moderation guide](/docs/guides/moderation). + */ + readonly "createModeration": ( + options: { readonly payload: typeof CreateModerationRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieve a paginated list of organization admin API keys. + */ + readonly "adminApiKeysList": ( + options: { + readonly params?: typeof AdminApiKeysListParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new admin-level API key for the organization. + */ + readonly "adminApiKeysCreate": ( + options: { readonly payload: typeof AdminApiKeysCreateRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get details for a specific organization API key by its ID. + */ + readonly "adminApiKeysGet": ( + keyId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete the specified admin API key. + */ + readonly "adminApiKeysDelete": ( + keyId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List user actions and configuration changes within this organization. + */ + readonly "listAuditLogs": ( + options: + | { readonly params?: typeof ListAuditLogsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List uploaded certificates for this organization. + */ + readonly "listOrganizationCertificates": ( + options: { + readonly params?: typeof ListOrganizationCertificatesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Upload a certificate to the organization. This does **not** automatically activate the certificate. + * + * Organizations can upload up to 50 certificates. + */ + readonly "uploadCertificate": ( + options: { readonly payload: typeof UploadCertificateRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Activate certificates at the organization level. + * + * You can atomically and idempotently activate up to 10 certificates at a time. + */ + readonly "activateOrganizationCertificates": ( + options: { + readonly payload: typeof ActivateOrganizationCertificatesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deactivate certificates at the organization level. + * + * You can atomically and idempotently deactivate up to 10 certificates at a time. + */ + readonly "deactivateOrganizationCertificates": ( + options: { + readonly payload: typeof DeactivateOrganizationCertificatesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get a certificate that has been uploaded to the organization. + * + * You can get a certificate regardless of whether it is active or not. + */ + readonly "getCertificate": ( + certificateId: string, + options: + | { readonly params?: typeof GetCertificateParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modify a certificate. Note that only the name can be modified. + */ + readonly "modifyCertificate": ( + certificateId: string, + options: { readonly payload: typeof ModifyCertificateRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a certificate from the organization. + * + * The certificate must be inactive for the organization and all projects. + */ + readonly "deleteCertificate": ( + certificateId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get costs details for the organization. + */ + readonly "usageCosts": ( + options: { readonly params: typeof UsageCostsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists all groups in the organization. + */ + readonly "listGroups": ( + options: + | { readonly params?: typeof ListGroupsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a new group in the organization. + */ + readonly "createGroup": ( + options: { readonly payload: typeof CreateGroupRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates a group's information. + */ + readonly "updateGroup": ( + groupId: string, + options: { readonly payload: typeof UpdateGroupRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a group from the organization. + */ + readonly "deleteGroup": ( + groupId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the organization roles assigned to a group within the organization. + */ + readonly "listGroupRoleAssignments": ( + groupId: string, + options: { + readonly params?: typeof ListGroupRoleAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Assigns an organization role to a group within the organization. + */ + readonly "assignGroupRole": ( + groupId: string, + options: { readonly payload: typeof AssignGroupRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Unassigns an organization role from a group within the organization. + */ + readonly "unassignGroupRole": ( + groupId: string, + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the users assigned to a group. + */ + readonly "listGroupUsers": ( + groupId: string, + options: + | { readonly params?: typeof ListGroupUsersParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Adds a user to a group. + */ + readonly "addGroupUser": ( + groupId: string, + options: { readonly payload: typeof AddGroupUserRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Removes a user from a group. + */ + readonly "removeGroupUser": ( + groupId: string, + userId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of invites in the organization. + */ + readonly "listInvites": ( + options: + | { readonly params?: typeof ListInvitesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization. + */ + readonly "inviteUser": ( + options: { readonly payload: typeof InviteUserRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves an invite. + */ + readonly "retrieveInvite": ( + inviteId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete an invite. If the invite has already been accepted, it cannot be deleted. + */ + readonly "deleteInvite": ( + inviteId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of projects. + */ + readonly "listProjects": ( + options: + | { readonly params?: typeof ListProjectsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new project in the organization. Projects can be created and archived, but cannot be deleted. + */ + readonly "createProject": ( + options: { readonly payload: typeof CreateProjectRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a project. + */ + readonly "retrieveProject": ( + projectId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a project in the organization. + */ + readonly "modifyProject": ( + projectId: string, + options: { readonly payload: typeof ModifyProjectRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError | OpenAiClientError<"ModifyProject400", typeof ModifyProject400.Type> + > + /** + * Returns a list of API keys in the project. + */ + readonly "listProjectApiKeys": ( + projectId: string, + options: { + readonly params?: typeof ListProjectApiKeysParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves an API key in the project. + */ + readonly "retrieveProjectApiKey": ( + projectId: string, + apiKeyId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes an API key from the project. + * + * Returns confirmation of the key deletion, or an error if the key belonged to + * a service account. + */ + readonly "deleteProjectApiKey": ( + projectId: string, + apiKeyId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"DeleteProjectApiKey400", typeof DeleteProjectApiKey400.Type> + > + /** + * Archives a project in the organization. Archived projects cannot be used or updated. + */ + readonly "archiveProject": ( + projectId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List certificates for this project. + */ + readonly "listProjectCertificates": ( + projectId: string, + options: { + readonly params?: typeof ListProjectCertificatesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Activate certificates at the project level. + * + * You can atomically and idempotently activate up to 10 certificates at a time. + */ + readonly "activateProjectCertificates": ( + projectId: string, + options: { + readonly payload: typeof ActivateProjectCertificatesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deactivate certificates at the project level. You can atomically and + * idempotently deactivate up to 10 certificates at a time. + */ + readonly "deactivateProjectCertificates": ( + projectId: string, + options: { + readonly payload: typeof DeactivateProjectCertificatesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the groups that have access to a project. + */ + readonly "listProjectGroups": ( + projectId: string, + options: { + readonly params?: typeof ListProjectGroupsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Grants a group access to a project. + */ + readonly "addProjectGroup": ( + projectId: string, + options: { readonly payload: typeof AddProjectGroupRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Revokes a group's access to a project. + */ + readonly "removeProjectGroup": ( + projectId: string, + groupId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns hosted tool permissions for a project. + */ + readonly "retrieveProjectHostedToolPermissions": ( + projectId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates hosted tool permissions for a project. + */ + readonly "updateProjectHostedToolPermissions": ( + projectId: string, + options: { + readonly payload: typeof UpdateProjectHostedToolPermissionsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns model permissions for a project. + */ + readonly "retrieveProjectModelPermissions": ( + projectId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates model permissions for a project. + */ + readonly "updateProjectModelPermissions": ( + projectId: string, + options: { + readonly payload: typeof UpdateProjectModelPermissionsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes model permissions for a project. + */ + readonly "deleteProjectModelPermissions": ( + projectId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns the rate limits per model for a project. + */ + readonly "listProjectRateLimits": ( + projectId: string, + options: { + readonly params?: typeof ListProjectRateLimitsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates a project rate limit. + */ + readonly "updateProjectRateLimits": ( + projectId: string, + rateLimitId: string, + options: { + readonly payload: typeof UpdateProjectRateLimitsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"UpdateProjectRateLimits400", typeof UpdateProjectRateLimits400.Type> + > + /** + * Returns a list of service accounts in the project. + */ + readonly "listProjectServiceAccounts": ( + projectId: string, + options: { + readonly params?: typeof ListProjectServiceAccountsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"ListProjectServiceAccounts400", typeof ListProjectServiceAccounts400.Type> + > + /** + * Creates a new service account in the project. This also returns an unredacted API key for the service account. + */ + readonly "createProjectServiceAccount": ( + projectId: string, + options: { + readonly payload: typeof CreateProjectServiceAccountRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"CreateProjectServiceAccount400", typeof CreateProjectServiceAccount400.Type> + > + /** + * Retrieves a service account in the project. + */ + readonly "retrieveProjectServiceAccount": ( + projectId: string, + serviceAccountId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a service account from the project. + * + * Returns confirmation of service account deletion, or an error if the project + * is archived (archived projects have no service accounts). + */ + readonly "deleteProjectServiceAccount": ( + projectId: string, + serviceAccountId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of users in the project. + */ + readonly "listProjectUsers": ( + projectId: string, + options: { + readonly params?: typeof ListProjectUsersParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"ListProjectUsers400", typeof ListProjectUsers400.Type> + > + /** + * Adds a user to the project. Users must already be members of the organization to be added to a project. + */ + readonly "createProjectUser": ( + projectId: string, + options: { readonly payload: typeof CreateProjectUserRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"CreateProjectUser400", typeof CreateProjectUser400.Type> + > + /** + * Retrieves a user in the project. + */ + readonly "retrieveProjectUser": ( + projectId: string, + userId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a user's role in the project. + */ + readonly "modifyProjectUser": ( + projectId: string, + userId: string, + options: { readonly payload: typeof ModifyProjectUserRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"ModifyProjectUser400", typeof ModifyProjectUser400.Type> + > + /** + * Deletes a user from the project. + * + * Returns confirmation of project user deletion, or an error if the project is + * archived (archived projects have no users). + */ + readonly "deleteProjectUser": ( + projectId: string, + userId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"DeleteProjectUser400", typeof DeleteProjectUser400.Type> + > + /** + * Lists the roles configured for the organization. + */ + readonly "listRoles": ( + options: + | { readonly params?: typeof ListRolesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a custom role for the organization. + */ + readonly "createRole": ( + options: { readonly payload: typeof CreateRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates an existing organization role. + */ + readonly "updateRole": ( + roleId: string, + options: { readonly payload: typeof UpdateRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a custom role from the organization. + */ + readonly "deleteRole": ( + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get audio speeches usage details for the organization. + */ + readonly "usageAudioSpeeches": ( + options: { readonly params: typeof UsageAudioSpeechesParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get audio transcriptions usage details for the organization. + */ + readonly "usageAudioTranscriptions": ( + options: { readonly params: typeof UsageAudioTranscriptionsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get code interpreter sessions usage details for the organization. + */ + readonly "usageCodeInterpreterSessions": ( + options: { + readonly params: typeof UsageCodeInterpreterSessionsParams.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get completions usage details for the organization. + */ + readonly "usageCompletions": ( + options: { readonly params: typeof UsageCompletionsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get embeddings usage details for the organization. + */ + readonly "usageEmbeddings": ( + options: { readonly params: typeof UsageEmbeddingsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get file search calls usage details for the organization. + */ + readonly "usageFileSearchCalls": ( + options: { readonly params: typeof UsageFileSearchCallsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get images usage details for the organization. + */ + readonly "usageImages": ( + options: { readonly params: typeof UsageImagesParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get moderations usage details for the organization. + */ + readonly "usageModerations": ( + options: { readonly params: typeof UsageModerationsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get vector stores usage details for the organization. + */ + readonly "usageVectorStores": ( + options: { readonly params: typeof UsageVectorStoresParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get web search calls usage details for the organization. + */ + readonly "usageWebSearchCalls": ( + options: { readonly params: typeof UsageWebSearchCallsParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists all of the users in the organization. + */ + readonly "listUsers": ( + options: + | { readonly params?: typeof ListUsersParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a user by their identifier. + */ + readonly "retrieveUser": ( + userId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a user's role in the organization. + */ + readonly "modifyUser": ( + userId: string, + options: { readonly payload: typeof ModifyUserRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a user from the organization. + */ + readonly "deleteUser": ( + userId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the organization roles assigned to a user within the organization. + */ + readonly "listUserRoleAssignments": ( + userId: string, + options: { + readonly params?: typeof ListUserRoleAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Assigns an organization role to a user within the organization. + */ + readonly "assignUserRole": ( + userId: string, + options: { readonly payload: typeof AssignUserRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Unassigns an organization role from a user within the organization. + */ + readonly "unassignUserRole": ( + userId: string, + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the project roles assigned to a group within a project. + */ + readonly "listProjectGroupRoleAssignments": ( + projectId: string, + groupId: string, + options: { + readonly params?: typeof ListProjectGroupRoleAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Assigns a project role to a group within a project. + */ + readonly "assignProjectGroupRole": ( + projectId: string, + groupId: string, + options: { + readonly payload: typeof AssignProjectGroupRoleRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Unassigns a project role from a group within a project. + */ + readonly "unassignProjectGroupRole": ( + projectId: string, + groupId: string, + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the roles configured for a project. + */ + readonly "listProjectRoles": ( + projectId: string, + options: { + readonly params?: typeof ListProjectRolesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a custom role for a project. + */ + readonly "createProjectRole": ( + projectId: string, + options: { readonly payload: typeof CreateProjectRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Updates an existing project role. + */ + readonly "updateProjectRole": ( + projectId: string, + roleId: string, + options: { readonly payload: typeof UpdateProjectRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a custom role from a project. + */ + readonly "deleteProjectRole": ( + projectId: string, + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Lists the project roles assigned to a user within a project. + */ + readonly "listProjectUserRoleAssignments": ( + projectId: string, + userId: string, + options: { + readonly params?: typeof ListProjectUserRoleAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Assigns a project role to a user within a project. + */ + readonly "assignProjectUserRole": ( + projectId: string, + userId: string, + options: { readonly payload: typeof AssignProjectUserRoleRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Unassigns a project role from a user within a project. + */ + readonly "unassignProjectUserRole": ( + projectId: string, + userId: string, + roleId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new Realtime API call over WebRTC and receive the SDP answer needed + * to complete the peer connection. + */ + readonly "createRealtimeCall": ( + options: { + readonly payload: typeof CreateRealtimeCallRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Accept an incoming SIP call and configure the realtime session that will + * handle it. + */ + readonly "acceptRealtimeCall": ( + callId: string, + options: { readonly payload: typeof AcceptRealtimeCallRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * End an active Realtime API call, whether it was initiated over SIP or + * WebRTC. + */ + readonly "hangupRealtimeCall": ( + callId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Transfer an active SIP call to a new destination using the SIP REFER verb. + */ + readonly "referRealtimeCall": ( + callId: string, + options: { readonly payload: typeof ReferRealtimeCallRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Decline an incoming SIP call by returning a SIP status code to the caller. + */ + readonly "rejectRealtimeCall": ( + callId: string, + options: { readonly payload: typeof RejectRealtimeCallRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Create a Realtime client secret with an associated session configuration. + * + * Client secrets are short-lived tokens that can be passed to a client app, + * such as a web frontend or mobile client, which grants access to the Realtime API without + * leaking your main API key. You can configure a custom TTL for each client secret. + * + * You can also attach session configuration options to the client secret, which will be + * applied to any sessions created using that client secret, but these can also be overridden + * by the client connection. + * + * [Learn more about authentication with client secrets over WebRTC](/docs/guides/realtime-webrtc). + * + * Returns the created client secret and the effective session object. The client secret is a string that looks like `ek_1234`. + */ + readonly "createRealtimeClientSecret": ( + options: { + readonly payload: typeof CreateRealtimeClientSecretRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create an ephemeral API token for use in client-side applications with the + * Realtime API. Can be configured with the same session parameters as the + * `session.update` client event. + * + * It responds with a session object, plus a `client_secret` key which contains + * a usable ephemeral API token that can be used to authenticate browser clients + * for the Realtime API. + * + * Returns the created Realtime session object, plus an ephemeral key. + */ + readonly "createRealtimeSession": ( + options: { readonly payload: typeof CreateRealtimeSessionRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create an ephemeral API token for use in client-side applications with the + * Realtime API specifically for realtime transcriptions. + * Can be configured with the same session parameters as the `transcription_session.update` client event. + * + * It responds with a session object, plus a `client_secret` key which contains + * a usable ephemeral API token that can be used to authenticate browser clients + * for the Realtime API. + * + * Returns the created Realtime transcription session object, plus an ephemeral key. + */ + readonly "createRealtimeTranscriptionSession": ( + options: { + readonly payload: typeof CreateRealtimeTranscriptionSessionRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a Realtime translation client secret with an associated translation session configuration. + * + * Client secrets are short-lived tokens that can be passed to a client app, + * such as a web frontend or mobile client, which grants access to the Realtime + * Translation API without leaking your main API key. You can configure a custom + * TTL for each client secret. + * + * Returns the created client secret and the effective translation session object. + * The client secret is a string that looks like `ek_1234`. + */ + readonly "createRealtimeTranslationClientSecret": ( + options: { + readonly payload: typeof CreateRealtimeTranslationClientSecretRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a model response. Provide [text](/docs/guides/text) or + * [image](/docs/guides/images) inputs to generate [text](/docs/guides/text) + * or [JSON](/docs/guides/structured-outputs) outputs. Have the model call + * your own [custom code](/docs/guides/function-calling) or use built-in + * [tools](/docs/guides/tools) like [web search](/docs/guides/tools-web-search) + * or [file search](/docs/guides/tools-file-search) to use your own data + * as input for the model's response. + */ + readonly "createResponse": ( + options: { readonly payload: typeof CreateResponseRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates a model response. Provide [text](/docs/guides/text) or + * [image](/docs/guides/images) inputs to generate [text](/docs/guides/text) + * or [JSON](/docs/guides/structured-outputs) outputs. Have the model call + * your own [custom code](/docs/guides/function-calling) or use built-in + * [tools](/docs/guides/tools) like [web search](/docs/guides/tools-web-search) + * or [file search](/docs/guides/tools-file-search) to use your own data + * as input for the model's response. + */ + readonly "createResponseSse": ( + options: { readonly payload: typeof CreateResponseRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateResponse200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateResponse200Sse.DecodingServices + > + /** + * Retrieves a model response with the given ID. + */ + readonly "getResponse": ( + responseId: string, + options: + | { readonly params?: typeof GetResponseParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a model response with the given ID. + */ + readonly "deleteResponse": ( + responseId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"DeleteResponse404", typeof DeleteResponse404.Type> + > + /** + * Cancels a model response with the given ID. Only responses created with + * the `background` parameter set to `true` can be cancelled. + * [Learn more](/docs/guides/background). + */ + readonly "cancelResponse": ( + responseId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenAiClientError<"CancelResponse404", typeof CancelResponse404.Type> + > + /** + * Returns a list of input items for a given response. + */ + readonly "listInputItems": ( + responseId: string, + options: + | { readonly params?: typeof ListInputItemsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a thread. + */ + readonly "createThread": ( + options: { readonly payload: typeof CreateThreadRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a thread and run it in one request. + */ + readonly "createThreadAndRun": ( + options: { readonly payload: typeof CreateThreadAndRunRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a thread. + */ + readonly "getThread": ( + threadId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a thread. + */ + readonly "modifyThread": ( + threadId: string, + options: { readonly payload: typeof ModifyThreadRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a thread. + */ + readonly "deleteThread": ( + threadId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of messages for a given thread. + */ + readonly "listMessages": ( + threadId: string, + options: + | { readonly params?: typeof ListMessagesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a message. + */ + readonly "createMessage": ( + threadId: string, + options: { readonly payload: typeof CreateMessageRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieve a message. + */ + readonly "getMessage": ( + threadId: string, + messageId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a message. + */ + readonly "modifyMessage": ( + threadId: string, + messageId: string, + options: { readonly payload: typeof ModifyMessageRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Deletes a message. + */ + readonly "deleteMessage": ( + threadId: string, + messageId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of runs belonging to a thread. + */ + readonly "listRuns": ( + threadId: string, + options: + | { readonly params?: typeof ListRunsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a run. + */ + readonly "createRun": ( + threadId: string, + options: { + readonly params?: typeof CreateRunParams.Encoded | undefined + readonly payload: typeof CreateRunRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a run. + */ + readonly "getRun": ( + threadId: string, + runId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** + * Modifies a run. + */ + readonly "modifyRun": ( + threadId: string, + runId: string, + options: { readonly payload: typeof ModifyRunRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancels a run that is `in_progress`. + */ + readonly "cancelRun": ( + threadId: string, + runId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of run steps belonging to a run. + */ + readonly "listRunSteps": ( + threadId: string, + runId: string, + options: + | { readonly params?: typeof ListRunStepsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a run step. + */ + readonly "getRunStep": ( + threadId: string, + runId: string, + stepId: string, + options: + | { readonly params?: typeof GetRunStepParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. + */ + readonly "submitToolOuputsToRun": ( + threadId: string, + runId: string, + options: { readonly payload: typeof SubmitToolOuputsToRunRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Creates an intermediate [Upload](/docs/api-reference/uploads/object) object + * that you can add [Parts](/docs/api-reference/uploads/part-object) to. + * Currently, an Upload can accept at most 8 GB in total and expires after an + * hour after you create it. + * + * Once you complete the Upload, we will create a + * [File](/docs/api-reference/files/object) object that contains all the parts + * you uploaded. This File is usable in the rest of our platform as a regular + * File object. + * + * For certain `purpose` values, the correct `mime_type` must be specified. + * Please refer to documentation for the + * [supported MIME types for your use case](/docs/assistants/tools/file-search#supported-files). + * + * For guidance on the proper filename extensions for each purpose, please + * follow the documentation on [creating a + * File](/docs/api-reference/files/create). + * + * Returns the Upload object with status `pending`. + */ + readonly "createUpload": ( + options: { readonly payload: typeof CreateUploadRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancels the Upload. No Parts may be added after an Upload is cancelled. + * + * Returns the Upload object with status `cancelled`. + */ + readonly "cancelUpload": ( + uploadId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Completes the [Upload](/docs/api-reference/uploads/object). + * + * Within the returned Upload object, there is a nested [File](/docs/api-reference/files/object) object that is ready to use in the rest of the platform. + * + * You can specify the order of the Parts by passing in an ordered list of the Part IDs. + * + * The number of bytes uploaded upon completion must match the number of bytes initially specified when creating the Upload object. No Parts may be added after an Upload is completed. + * Returns the Upload object with status `completed`, including an additional `file` property containing the created usable File object. + */ + readonly "completeUpload": ( + uploadId: string, + options: { readonly payload: typeof CompleteUploadRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Adds a [Part](/docs/api-reference/uploads/part-object) to an [Upload](/docs/api-reference/uploads/object) object. A Part represents a chunk of bytes from the file you are trying to upload. + * + * Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8 GB. + * + * It is possible to add multiple Parts in parallel. You can decide the intended order of the Parts when you [complete the Upload](/docs/api-reference/uploads/complete). + */ + readonly "addUploadPart": ( + uploadId: string, + options: { readonly payload: typeof AddUploadPartRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of vector stores. + */ + readonly "listVectorStores": ( + options: { + readonly params?: typeof ListVectorStoresParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a vector store. + */ + readonly "createVectorStore": ( + options: { readonly payload: typeof CreateVectorStoreRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a vector store. + */ + readonly "getVectorStore": ( + vectorStoreId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Modifies a vector store. + */ + readonly "modifyVectorStore": ( + vectorStoreId: string, + options: { readonly payload: typeof ModifyVectorStoreRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a vector store. + */ + readonly "deleteVectorStore": ( + vectorStoreId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * The maximum number of files in a single batch request is 2000. + * Vector store file attach requests are rate limited per vector store (300 requests per minute across both this endpoint and `/vector_stores/{vector_store_id}/files`). + * For ingesting multiple files into the same vector store, this batch endpoint is recommended. + */ + readonly "createVectorStoreFileBatch": ( + vectorStoreId: string, + options: { + readonly payload: typeof CreateVectorStoreFileBatchRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a vector store file batch. + */ + readonly "getVectorStoreFileBatch": ( + vectorStoreId: string, + batchId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancel a vector store file batch. This attempts to cancel the processing of files in this batch as soon as possible. + */ + readonly "cancelVectorStoreFileBatch": ( + vectorStoreId: string, + batchId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of vector store files in a batch. + */ + readonly "listFilesInVectorStoreBatch": ( + vectorStoreId: string, + batchId: string, + options: { + readonly params?: typeof ListFilesInVectorStoreBatchParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns a list of vector store files. + */ + readonly "listVectorStoreFiles": ( + vectorStoreId: string, + options: { + readonly params?: typeof ListVectorStoreFilesParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * This endpoint is subject to a per-vector-store write rate limit of 300 requests per minute, shared with `/vector_stores/{vector_store_id}/file_batches`. + * For uploading multiple files to the same vector store, use the file batches endpoint to reduce request volume. + */ + readonly "createVectorStoreFile": ( + vectorStoreId: string, + options: { readonly payload: typeof CreateVectorStoreFileRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieves a vector store file. + */ + readonly "getVectorStoreFile": ( + vectorStoreId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Update attributes on a vector store file. + */ + readonly "updateVectorStoreFileAttributes": ( + vectorStoreId: string, + fileId: string, + options: { + readonly payload: typeof UpdateVectorStoreFileAttributesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a vector store file. This will remove the file from the vector store but the file itself will not be deleted. To delete the file, use the [delete file](/docs/api-reference/files/delete) endpoint. + */ + readonly "deleteVectorStoreFile": ( + vectorStoreId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieve the parsed contents of a vector store file. + */ + readonly "retrieveVectorStoreFileContent": ( + vectorStoreId: string, + fileId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Search a vector store for relevant chunks based on a query and file attributes filter. + */ + readonly "searchVectorStore": ( + vectorStoreId: string, + options: { readonly payload: typeof SearchVectorStoreRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a conversation. + */ + readonly "createConversation": ( + options: { readonly payload: typeof CreateConversationRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get a conversation + */ + readonly "getConversation": ( + conversationId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Update a conversation + */ + readonly "updateConversation": ( + conversationId: string, + options: { readonly payload: typeof UpdateConversationRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a conversation. Items in the conversation will not be deleted. + */ + readonly "deleteConversation": ( + conversationId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List recently generated videos for the current project. + */ + readonly "ListVideos": ( + options: + | { readonly params?: typeof ListVideosParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new video generation job from a prompt and optional reference assets. + */ + readonly "createVideo": ( + options: { readonly payload: typeof CreateVideoRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a character from an uploaded video. + */ + readonly "CreateVideoCharacter": ( + options: { + readonly payload: typeof CreateVideoCharacterRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Fetch a character. + */ + readonly "GetVideoCharacter": ( + characterId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new video generation job by editing a source video or existing generated video. + */ + readonly "CreateVideoEdit": ( + options: { readonly payload: typeof CreateVideoEditRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create an extension of a completed video. + */ + readonly "CreateVideoExtend": ( + options: { readonly payload: typeof CreateVideoExtendRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Fetch the latest metadata for a generated video. + */ + readonly "GetVideo": ( + videoId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Permanently delete a completed or failed video and its stored assets. + */ + readonly "DeleteVideo": ( + videoId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Download the generated video bytes or a derived preview asset. + * + * Streams the rendered video content for the specified video job. + */ + readonly "RetrieveVideoContent": ( + videoId: string, + options: { + readonly params?: typeof RetrieveVideoContentParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a remix of a completed video using a refreshed prompt. + */ + readonly "CreateVideoRemix": ( + videoId: string, + options: { readonly payload: typeof CreateVideoRemixRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Returns input token counts of the request. + * + * Returns an object with `object` set to `response.input_tokens` and an `input_tokens` count. + */ + readonly "Getinputtokencounts": ( + options: { + readonly payload: typeof GetinputtokencountsRequestFormUrlEncoded.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Compact a conversation. Returns a compacted response object. + * + * Learn when and how to compact long-running conversations in the [conversation state guide](/docs/guides/conversation-state#managing-the-context-window). For ZDR-compatible compaction details, see [Compaction (advanced)](/docs/guides/conversation-state#compaction-advanced). + */ + readonly "Compactconversation": ( + options: { + readonly payload: typeof CompactconversationRequestFormUrlEncoded.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List all skills for the current project. + */ + readonly "ListSkills": ( + options: + | { readonly params?: typeof ListSkillsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new skill. + */ + readonly "CreateSkill": ( + options: { readonly payload: typeof CreateSkillRequestFormData.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get a skill by its ID. + */ + readonly "GetSkill": ( + skillId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Update the default version pointer for a skill. + */ + readonly "UpdateSkillDefaultVersion": ( + skillId: string, + options: { + readonly payload: typeof UpdateSkillDefaultVersionRequestFormUrlEncoded.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a skill by its ID. + */ + readonly "DeleteSkill": ( + skillId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Download a skill zip bundle by its ID. + */ + readonly "GetSkillContent": ( + skillId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List skill versions for a skill. + */ + readonly "ListSkillVersions": ( + skillId: string, + options: { + readonly params?: typeof ListSkillVersionsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a new immutable skill version. + */ + readonly "CreateSkillVersion": ( + skillId: string, + options: { + readonly payload: typeof CreateSkillVersionRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Get a specific skill version. + */ + readonly "GetSkillVersion": ( + skillId: string, + version: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a skill version. + */ + readonly "DeleteSkillVersion": ( + skillId: string, + version: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Download a skill version zip bundle. + */ + readonly "GetSkillVersionContent": ( + skillId: string, + version: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Cancel an active ChatKit session and return its most recent metadata. + * + * Cancelling prevents new requests from using the issued client secret. + */ + readonly "CancelChatSessionMethod": ( + sessionId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Create a ChatKit session. + */ + readonly "CreateChatSessionMethod": ( + options: { + readonly payload: typeof CreateChatSessionMethodRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List items that belong to a ChatKit thread. + */ + readonly "ListThreadItemsMethod": ( + threadId: string, + options: { + readonly params?: typeof ListThreadItemsMethodParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Retrieve a ChatKit thread by its identifier. + */ + readonly "GetThreadMethod": ( + threadId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * Delete a ChatKit thread along with its items and stored attachments. + */ + readonly "DeleteThreadMethod": ( + threadId: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > + /** + * List ChatKit threads with optional pagination and user filters. + */ + readonly "ListThreadsMethod": ( + options: { + readonly params?: typeof ListThreadsMethodParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + HttpClientError.HttpClientError | SchemaError + > +} + +export interface OpenAiClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class OpenAiClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const OpenAiClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): OpenAiClientError => + new OpenAiClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/openai/src/OpenAiClient.ts b/repos/effect/packages/ai/openai/src/OpenAiClient.ts new file mode 100644 index 0000000..a1774b6 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiClient.ts @@ -0,0 +1,730 @@ +/** + * The `OpenAiClient` module defines the low-level Effect service used by the + * OpenAI integration for Responses API and embedding requests. It builds a + * configured HTTP client with authentication and OpenAI organization or project + * headers, exposes helpers for non-streaming responses, SSE response streams, + * WebSocket response streams, and embeddings, and maps transport or decoding + * failures into `AiError`. + * + * @since 4.0.0 + */ +import * as Array from "effect/Array" +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Function from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Queue from "effect/Queue" +import * as RcRef from "effect/RcRef" +import * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as AiError from "effect/unstable/ai/AiError" +import * as ResponseIdTracker from "effect/unstable/ai/ResponseIdTracker" +import * as Sse from "effect/unstable/encoding/Sse" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Socket from "effect/unstable/socket/Socket" +import * as Errors from "./internal/errors.ts" +import { OpenAiConfig } from "./OpenAiConfig.ts" +import * as OpenAiSchema from "./OpenAiSchema.ts" + +// ============================================================================= +// Service Interface +// ============================================================================= + +/** + * Effect service interface for the handwritten OpenAI client. + * + * **Details** + * + * Provides the configured HTTP client plus helpers for Responses API calls, streaming Responses events, and embeddings. Transport and schema decoding failures are mapped to `AiError`. + * + * @category models + * @since 4.0.0 + */ +export interface Service { + /** + * The transformed HTTP client used by this service. + */ + readonly client: HttpClient.HttpClient + + /** + * Create a response using the OpenAI responses endpoint. + */ + readonly createResponse: ( + options: typeof OpenAiSchema.CreateResponse.Encoded + ) => Effect.Effect< + readonly [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > + + /** + * Create a streaming response using the OpenAI responses endpoint. + */ + readonly createResponseStream: ( + options: Omit + ) => Effect.Effect< + readonly [ + response: HttpClientResponse.HttpClientResponse, + stream: Stream.Stream + ], + AiError.AiError + > + + /** + * Create embeddings using the OpenAI embeddings endpoint. + */ + readonly createEmbedding: ( + options: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded + ) => Effect.Effect +} + +// ============================================================================= +// Service Identifier +// ============================================================================= + +/** + * Service tag for the OpenAI client. + * + * **When to use** + * + * Use when accessing or providing the OpenAI client service through Effect's + * context. + * + * @see {@link make} for constructing an OpenAI client effectfully + * @see {@link layer} for providing a client from explicit options + * @see {@link layerConfig} for providing a client from `Config` + * + * @category services + * @since 4.0.0 + */ +export class OpenAiClient extends Context.Service()( + "@effect/ai-openai/OpenAiClient" +) {} + +// ============================================================================= +// Options +// ============================================================================= + +/** + * Options for configuring the OpenAI client. + * + * @category options + * @since 4.0.0 + */ +export type Options = { + /** + * The OpenAI API key. + */ + readonly apiKey?: Redacted.Redacted | undefined + + /** + * The base URL for the OpenAI API. + * + * @default "https://api.openai.com/v1" + */ + readonly apiUrl?: string | undefined + + /** + * Optional organization ID for multi-org accounts. + */ + readonly organizationId?: Redacted.Redacted | undefined + + /** + * Optional project ID for project-scoped requests. + */ + readonly projectId?: Redacted.Redacted | undefined + + /** + * Optional transformer for the HTTP client. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +// ============================================================================= +// Constructor +// ============================================================================= + +const RedactedOpenAiHeaders = { + OpenAiOrganization: "OpenAI-Organization", + OpenAiProject: "OpenAI-Project" +} + +/** + * Creates an OpenAI client service with the given options. + * + * **When to use** + * + * Use when you need the OpenAI client service value inside an effect. + * + * **Details** + * + * The returned service uses the current `HttpClient`, prepends `apiUrl` or + * `https://api.openai.com/v1`, adds the bearer token and optional OpenAI + * organization/project headers, accepts JSON responses, filters for successful + * HTTP statuses, and applies `transformClient` when provided. + * + * **Gotchas** + * + * A scoped `OpenAiConfig.withClientTransform` is applied when request helpers + * run, after the `transformClient` option supplied to `make`. + * + * @see {@link layer} for providing this client from explicit options + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*( + options: Options + ): Effect.fn.Return { + const baseClient = yield* HttpClient.HttpClient + const apiUrl = options.apiUrl ?? "https://api.openai.com/v1" + + const httpClient = baseClient.pipe( + HttpClient.mapRequest(Function.flow( + HttpClientRequest.prependUrl(apiUrl), + options.apiKey + ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey)) + : identity, + options.organizationId + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiOrganization, + Redacted.value(options.organizationId) + ) + : identity, + options.projectId + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiProject, + Redacted.value(options.projectId) + ) + : identity, + HttpClientRequest.acceptJson + )), + HttpClient.filterStatusOk, + options.transformClient + ? options.transformClient + : identity + ) + + const resolveHttpClient = Effect.map( + OpenAiConfig.getOrUndefined, + (config) => + Predicate.isNotUndefined(config?.transformClient) + ? config.transformClient(httpClient) + : httpClient + ) + + const decodeResponse = HttpClientResponse.schemaBodyJson(OpenAiSchema.Response) + + const createResponse = ( + payload: typeof OpenAiSchema.CreateResponse.Encoded + ): Effect.Effect< + [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > => + Effect.flatMap(resolveHttpClient, (client) => + client.execute( + HttpClientRequest.post("/responses", { + body: HttpBody.jsonUnsafe(payload) + }) + ).pipe( + Effect.flatMap((response) => + decodeResponse(response).pipe( + Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [ + body, + response + ]) + ) + ), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) + }) + )) + + const buildResponseStream = ( + response: HttpClientResponse.HttpClientResponse + ): [ + HttpClientResponse.HttpClientResponse, + Stream.Stream + ] => { + const stream = response.stream.pipe( + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)), + Stream.takeUntil((event) => + event.data.type === "response.completed" || + event.data.type === "response.incomplete" + ), + Stream.map((event) => event.data), + Stream.catchTags({ + // TODO: handle SSE retries + Retry: (error) => Stream.die(error), + HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")), + SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createResponseStream")) + }) + ) + return [response, stream] + } + + const createResponseStream: Service["createResponseStream"] = (payload) => + Effect.contextWith((services) => { + const socket = Context.getOrUndefined(services, OpenAiSocket) + if (socket) return socket.createResponseStream(payload) + return Effect.flatMap(resolveHttpClient, (client) => + client.execute( + HttpClientRequest.post("/responses", { + body: HttpBody.jsonUnsafe({ ...payload, stream: true }) + }) + ).pipe( + Effect.map(buildResponseStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createResponseStream") + ) + )) + }) + + const decodeEmbedding = HttpClientResponse.schemaBodyJson(OpenAiSchema.CreateEmbeddingResponse) + + const createEmbedding = ( + payload: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded + ): Effect.Effect => + Effect.flatMap(resolveHttpClient, (client) => + client.execute( + HttpClientRequest.post("/embeddings", { + body: HttpBody.jsonUnsafe(payload) + }) + ).pipe( + Effect.flatMap(decodeEmbedding), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) + }) + )) + + return OpenAiClient.of({ + client: httpClient, + createResponse, + createResponseStream, + createEmbedding + }) + }, + Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) + ) +) + +// ============================================================================= +// Layers +// ============================================================================= + +/** + * Creates a layer for the OpenAI client with the given options. + * + * **When to use** + * + * Use when you already have explicit `Options` values, such as an API key or + * custom API URL, and want to provide `OpenAiClient` as a `Layer`. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(OpenAiClient, make(options)) + +/** + * Creates a layer for the OpenAI client from provided `Config` values. + * + * **When to use** + * + * Use when you need client settings for OpenAI-compatible APIs to be read from + * Effect `Config` values while providing `OpenAiClient` as a `Layer`. + * + * **Details** + * + * Only config values supplied in `options` are loaded. Omitted fields are + * passed to `make` as `undefined`, and `transformClient` is forwarded as a + * plain option. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layer} for providing the client from already-resolved options + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = (options?: { + /** + * The config value to load for the API key. + */ + readonly apiKey?: Config.Config | undefined> | undefined + + /** + * The config value to load for the API URL. + */ + readonly apiUrl?: Config.Config | undefined + + /** + * The config value to load for the organization ID. + */ + readonly organizationId?: Config.Config | undefined> | undefined + + /** + * The config value to load for the project ID. + */ + readonly projectId?: Config.Config | undefined> | undefined + + /** + * Optional transformer for the HTTP client. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + OpenAiClient, + Effect.gen(function*() { + const apiKey = Predicate.isNotUndefined(options?.apiKey) + ? yield* options.apiKey : + undefined + const apiUrl = Predicate.isNotUndefined(options?.apiUrl) + ? yield* options.apiUrl : + undefined + const organizationId = Predicate.isNotUndefined(options?.organizationId) + ? yield* options.organizationId + : undefined + const projectId = Predicate.isNotUndefined(options?.projectId) + ? yield* options.projectId : + undefined + return yield* make({ + apiKey, + apiUrl, + organizationId, + projectId, + transformClient: options?.transformClient + }) + }) + ) + +// ============================================================================= +// Websocket mode +// ============================================================================= + +/** + * Response stream event emitted by the OpenAI Responses API. + * + * @category Events + * @since 4.0.0 + */ +export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type + +/** + * Service for creating OpenAI response streams over a WebSocket connection. + * + * **When to use** + * + * Use when you need direct access to the WebSocket-backed response streaming + * service rather than wrapping an effect with WebSocket mode. + * + * **Details** + * + * `createResponseStream` sends a `response.create` message over the WebSocket + * connection and returns an HTTP response together with a stream of + * `ResponseStreamEvent` values. + * + * **Gotchas** + * + * WebSocket response streams are serialized to one request at a time by the + * shared socket service. + * + * @see {@link withWebSocketMode} for enabling WebSocket mode for one effect + * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer + * + * @category Websocket mode + * @since 4.0.0 + */ +export class OpenAiSocket extends Context.Service + ) => Effect.Effect< + readonly [ + response: HttpClientResponse.HttpClientResponse, + stream: Stream.Stream + ], + AiError.AiError + > +}>()("@effect/ai-openai/OpenAiClient/OpenAiSocket") {} + +const makeSocket = Effect.gen(function*() { + const client = yield* OpenAiClient + const tracker = yield* ResponseIdTracker.make + const socketScope = yield* Effect.scope + const makeRequest = Effect.flatMap( + OpenAiConfig.getOrUndefined, + (config) => { + const httpClient = Predicate.isNotUndefined(config?.transformClient) + ? config.transformClient(client.client) + : client.client + return Effect.orDie(httpClient.preprocess(HttpClientRequest.post("/responses"))) + } + ) + const makeWebSocket = yield* Socket.WebSocketConstructor + + const decoder = new TextDecoder() + + const queueRef: RcRef.RcRef< + { + readonly send: (message: typeof OpenAiSchema.CreateResponse.Encoded) => Effect.Effect + readonly incoming: Queue.Dequeue + } + > = yield* RcRef.make({ + idleTimeToLive: 60_000, + acquire: Effect.gen(function*() { + const scope = yield* Effect.scope + const request = yield* makeRequest + const socket = yield* Socket.makeWebSocket(request.url.replace(/^http/, "ws")).pipe( + Effect.provideService(Socket.WebSocketConstructor, (url) => + makeWebSocket(url, { + headers: request.headers + } as any)) + ) + const write = yield* socket.writer + + yield* Scope.addFinalizerExit(scope, () => { + tracker.clearUnsafe() + return Effect.void + }) + + const incoming = yield* Queue.unbounded() + const send = (message: typeof OpenAiSchema.CreateResponse.Encoded) => + write(JSON.stringify({ + type: "response.create", + ...message + })).pipe( + Effect.mapError((_error) => + AiError.make({ + module: "OpenAiClient", + method: "createResponseStream", + reason: new AiError.NetworkError({ + reason: "TransportError", + request: { + method: "POST", + url: request.url, + urlParams: [], + hash: undefined, + headers: request.headers + }, + description: "Failed to send message over WebSocket" + }) + }) + ) + ) + + yield* socket.runRaw((msg) => { + const text = typeof msg === "string" ? msg : decoder.decode(msg) + try { + const event = decodeEvent(text) + if (event.type === "error" && "status" in event) { + const status = Number(event.status) + const error = "error" in event ? event.error as typeof ErrorEvent.Type.error : event + const json = JSON.stringify(error) + return Effect.fail( + AiError.make({ + module: "OpenAiClient", + method: "createResponseStream", + reason: AiError.reasonFromHttpStatus({ + description: json, + status: isNaN(status) ? errorTypeToStatus[error.type] ?? 500 : status, + metadata: error as any, + http: { + body: json, + request: { + method: "POST", + url: request.url, + urlParams: [], + hash: undefined, + headers: request.headers + } + } + }) + }) + ) + } + Queue.offerUnsafe(incoming, event) + } catch {} + }).pipe( + Effect.catchTag("SocketError", (error) => + AiError.make({ + module: "OpenAiClient", + method: "createResponseStream", + reason: new AiError.NetworkError({ + reason: "TransportError", + request: { + method: "POST", + url: request.url, + urlParams: [], + hash: undefined, + headers: request.headers + }, + description: error.message + }) + })), + Effect.catchCause((cause) => Queue.failCause(incoming, cause)), + Effect.ensuring(Effect.forkIn(RcRef.invalidate(queueRef), socketScope, { + startImmediately: true + })), + Effect.forkScoped({ startImmediately: true }) + ) + + return { send, incoming } as const + }) + }) + + // Prime the websocket + yield* Effect.scoped(RcRef.get(queueRef)) + + // Websocket mode only allows one request at a time + const semaphore = Semaphore.makeUnsafe(1) + const request = yield* makeRequest + + return OpenAiSocket.context({ + createResponseStream(options) { + const stream = Stream.unwrap(Effect.gen(function*() { + const scope = yield* Effect.scope + yield* Effect.acquireRelease( + semaphore.take(1), + () => semaphore.release(1), + { interruptible: true } + ) + const { send, incoming } = yield* RcRef.get(queueRef) + let done = false + + yield* Scope.addFinalizerExit( + scope, + () => done ? Effect.void : RcRef.invalidate(queueRef) + ) + + yield* send(options).pipe( + Effect.forkScoped({ startImmediately: true }) + ) + + return Stream.fromQueue(incoming).pipe( + Stream.takeUntil((e) => { + done = e.type === "response.completed" || e.type === "response.incomplete" + return done + }) + ) + })) + + return Effect.succeed([ + HttpClientResponse.fromWeb(request, new Response()), + stream + ]) + } + }).pipe( + Context.add(ResponseIdTracker.ResponseIdTracker, tracker) + ) +}) + +const ErrorEvent = Schema.Struct({ + type: Schema.Literal("error"), + status: Schema.Number.pipe( + Schema.withDecodingDefault(Effect.succeed(500)) + ), + error: Schema.Struct({ + type: Schema.String, + message: Schema.String + }) +}) + +const errorTypeToStatus: Record = { + invalid_request_error: 400, + invalid_api_key_error: 401, + insufficient_quota_error: 429, + rate_limit_error: 429, + service_unavailable_error: 503 +} + +const AllEvents = Schema.Union([ErrorEvent, OpenAiSchema.ResponseStreamEvent]) +const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents)) + +/** + * Uses OpenAI's WebSocket mode for response streams within the provided effect. + * + * **When to use** + * + * Use to enable WebSocket mode around one effect that creates OpenAI response + * streams. + * + * **Gotchas** + * + * This only works with the following WebSocket constructor layers: + * + * - `NodeSocket.layerWebSocketConstructorWS` + * - `BunSocket.layerWebSocketConstructor` + * + * These constructor layers support the non-standard options needed to set the + * Authorization header. + * + * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer + * @see {@link OpenAiSocket} for direct access to the WebSocket-backed streaming service + * + * @category Websocket mode + * @since 4.0.0 + */ +export const withWebSocketMode = ( + effect: Effect.Effect +): Effect.Effect< + A, + E, + Exclude | OpenAiClient | Socket.WebSocketConstructor +> => + Effect.scopedWith((scope) => + Effect.flatMap( + Scope.provide(makeSocket, scope), + (services) => Effect.provideContext(effect, services) + ) + ) + +/** + * Uses OpenAI's websocket mode for all responses that use the Layer. + * + * **When to use** + * + * Use to provide WebSocket mode through layer composition for effects that use + * OpenAI response streaming. + * + * **Gotchas** + * + * This only works with the following WebSocket constructor layers: + * + * - `NodeSocket.layerWebSocketConstructorWS` + * - `BunSocket.layerWebSocketConstructor` + * + * These constructor layers support the non-standard options needed to set the + * Authorization header. + * + * @see {@link withWebSocketMode} for enabling WebSocket mode around a single effect + * + * @category Websocket mode + * @since 4.0.0 + */ +export const layerWebSocketMode: Layer.Layer< + OpenAiSocket | ResponseIdTracker.ResponseIdTracker, + never, + OpenAiClient | Socket.WebSocketConstructor +> = Layer.effectContext(makeSocket) diff --git a/repos/effect/packages/ai/openai/src/OpenAiClientGenerated.ts b/repos/effect/packages/ai/openai/src/OpenAiClientGenerated.ts new file mode 100644 index 0000000..1b2c9b0 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiClientGenerated.ts @@ -0,0 +1,202 @@ +/** + * @since 4.0.0 + */ +import * as Array from "effect/Array" +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Function from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Redacted from "effect/Redacted" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as Generated from "./Generated.ts" +import { OpenAiConfig } from "./OpenAiConfig.ts" + +// ============================================================================= +// Service Identifier +// ============================================================================= + +/** + * Service identifier for the generated OpenAI client. + * + * @since 4.0.0 + * @category service + */ +export class OpenAiClientGenerated extends Context.Service()( + "@effect/ai-openai/OpenAiClientGenerated" +) {} + +// ============================================================================= +// Options +// ============================================================================= + +/** + * Options for configuring the generated OpenAI client. + * + * @since 4.0.0 + * @category options + */ +export type Options = { + /** + * The OpenAI API key. + */ + readonly apiKey?: Redacted.Redacted | undefined + + /** + * The base URL for the OpenAI API. + * + * @default "https://api.openai.com/v1" + */ + readonly apiUrl?: string | undefined + + /** + * Optional organization ID for multi-org accounts. + */ + readonly organizationId?: Redacted.Redacted | undefined + + /** + * Optional project ID for project-scoped requests. + */ + readonly projectId?: Redacted.Redacted | undefined + + /** + * Optional transformer for the HTTP client. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +const RedactedOpenAiHeaders = { + OpenAiOrganization: "OpenAI-Organization", + OpenAiProject: "OpenAI-Project" +} + +// ============================================================================= +// Constructor +// ============================================================================= + +/** + * Creates a generated OpenAI client service with the given options. + * + * @since 4.0.0 + * @category constructors + */ +export const make = Effect.fnUntraced( + function*(options: Options): Effect.fn.Return { + const baseClient = yield* HttpClient.HttpClient + const apiUrl = options.apiUrl ?? "https://api.openai.com/v1" + + const httpClient = baseClient.pipe( + HttpClient.mapRequest(Function.flow( + HttpClientRequest.prependUrl(apiUrl), + options.apiKey + ? HttpClientRequest.bearerToken(Redacted.value(options.apiKey)) + : identity, + options.organizationId + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiOrganization, + Redacted.value(options.organizationId) + ) + : identity, + options.projectId + ? HttpClientRequest.setHeader( + RedactedOpenAiHeaders.OpenAiProject, + Redacted.value(options.projectId) + ) + : identity, + HttpClientRequest.acceptJson + )), + options.transformClient + ? options.transformClient + : identity + ) + + return Generated.make(httpClient, { + transformClient: Effect.fnUntraced(function*(client) { + const config = yield* OpenAiConfig.getOrUndefined + if (Predicate.isNotUndefined(config?.transformClient)) { + return config.transformClient(client) + } + return client + }) + }) + }, + Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) + ) +) + +// ============================================================================= +// Layers +// ============================================================================= + +/** + * Creates a layer for the generated OpenAI client with the given options. + * + * @since 4.0.0 + * @category layers + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(OpenAiClientGenerated, make(options)) + +/** + * Creates a layer for the generated OpenAI client, loading the requisite + * configuration via Effect's `Config` module. + * + * @since 4.0.0 + * @category layers + */ +export const layerConfig = (options?: { + /** + * The config value to load for the API key. + */ + readonly apiKey?: Config.Config | undefined> | undefined + + /** + * The config value to load for the API URL. + */ + readonly apiUrl?: Config.Config | undefined + + /** + * The config value to load for the organization ID. + */ + readonly organizationId?: Config.Config | undefined> | undefined + + /** + * The config value to load for the project ID. + */ + readonly projectId?: Config.Config | undefined> | undefined + + /** + * Optional transformer for the HTTP client. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + OpenAiClientGenerated, + Effect.gen(function*() { + const apiKey = Predicate.isNotUndefined(options?.apiKey) + ? yield* options.apiKey : + undefined + const apiUrl = Predicate.isNotUndefined(options?.apiUrl) + ? yield* options.apiUrl : + undefined + const organizationId = Predicate.isNotUndefined(options?.organizationId) + ? yield* options.organizationId + : undefined + const projectId = Predicate.isNotUndefined(options?.projectId) + ? yield* options.projectId : + undefined + return yield* make({ + apiKey, + apiUrl, + organizationId, + projectId, + transformClient: options?.transformClient + }) + }) + ) diff --git a/repos/effect/packages/ai/openai/src/OpenAiConfig.ts b/repos/effect/packages/ai/openai/src/OpenAiConfig.ts new file mode 100644 index 0000000..ea0b05c --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiConfig.ts @@ -0,0 +1,93 @@ +/** + * The `OpenAiConfig` module lets a workflow temporarily customize the HTTP + * client used by `@effect/ai-openai` request helpers. OpenAI client, language + * model, and embedding code read this scoped transform when they execute + * provider calls. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * Context service for scoped OpenAI configuration used by provider operations. + * + * **When to use** + * + * Use to provide scoped OpenAI client configuration, such as an HTTP client + * transform, to OpenAI provider operations without passing it through each call. + * + * @see {@link withClientTransform} for scoping an HTTP client transformation + * + * @category services + * @since 4.0.0 + */ +export class OpenAiConfig extends Context.Service< + OpenAiConfig, + OpenAiConfig.Service +>()("@effect/ai-openai/OpenAiConfig") { + /** + * Gets the configured OpenAI service from the current context when present. + * + * @since 4.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.mapUnsafe.get(OpenAiConfig.key) + ) +} + +/** + * Types used by the `OpenAiConfig` context service. + * + * @since 4.0.0 + */ +export declare namespace OpenAiConfig { + /** + * Configuration values read by OpenAI provider operations when executing + * requests. + * + * @category models + * @since 4.0.0 + */ + export interface Service { + readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined + } +} + +/** + * Provides a scoped transform for the OpenAI HTTP client used by provider + * operations. + * + * **When to use** + * + * Use when you need temporary OpenAI HTTP client customization for a single + * effect or workflow without rebuilding the client layer. + * + * **Details** + * + * Supports both data-first and data-last forms. The transform is stored in the + * scoped `OpenAiConfig` service and read by OpenAI provider operations while + * running the supplied effect. + * + * **Gotchas** + * + * If a transform is already present in the scoped config, this helper replaces + * it. Compose transforms manually when both should apply. + * + * @category configuration + * @since 4.0.0 + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + transformClient: (client: HttpClient) => HttpClient +) => + Effect.flatMap( + OpenAiConfig.getOrUndefined, + (config) => Effect.provideService(self, OpenAiConfig, { ...config, transformClient }) + )) diff --git a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts new file mode 100644 index 0000000..7644378 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts @@ -0,0 +1,258 @@ +/** + * The `OpenAiEmbeddingModel` module provides the OpenAI implementation of + * Effect AI's `EmbeddingModel` service. It sends embedding requests through + * `OpenAiClient`, exposes constructors for layers and `AiModel` values, + * supports scoped request configuration overrides, and checks that OpenAI + * returns one numeric vector for each requested input. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import type { Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import * as EmbeddingModel from "effect/unstable/ai/EmbeddingModel" +import * as AiModel from "effect/unstable/ai/Model" +import { OpenAiClient } from "./OpenAiClient.ts" +import type * as OpenAiSchema from "./OpenAiSchema.ts" + +/** + * Model identifiers supported by OpenAI's embeddings API. + * + * @category models + * @since 4.0.0 + */ +export type Model = "text-embedding-ada-002" | "text-embedding-3-small" | "text-embedding-3-large" + +/** + * Context service for OpenAI embedding model configuration. + * + * **When to use** + * + * Use when you need scoped OpenAI request defaults or overrides for embedding + * requests from Effect context. + * + * **Details** + * + * The service stores the OpenAI create-embedding request payload without + * `input`, carrying options such as `model`, `dimensions`, `encoding_format`, + * and `user`. + * + * @see {@link withConfigOverride} for scoping embedding request overrides + * + * @category services + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + Simplify< + & Partial< + Omit< + typeof OpenAiSchema.CreateEmbeddingRequest.Encoded, + "input" + > + > + & { + readonly [x: string]: unknown + } + > +>()("@effect/ai-openai/OpenAiEmbeddingModel/Config") {} + +/** + * Creates an `AiModel` for an OpenAI embedding model with its configured vector dimensions. + * + * **When to use** + * + * Use to provide an OpenAI `EmbeddingModel` and its `Dimensions` service to an + * Effect program. + * + * @see {@link layer} for providing only the embedding model service + * @see {@link withConfigOverride} for scoped request configuration overrides + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: (string & {}) | Model, + options: { + readonly dimensions: number + readonly config?: Omit + } +): AiModel.Model<"openai", EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions, OpenAiClient> => + AiModel.make( + "openai", + model, + Layer.merge( + layer({ + model, + config: { + ...options.config, + dimensions: options.dimensions + } + }), + Layer.succeed(EmbeddingModel.Dimensions, options.dimensions) + ) + ) + +/** + * Creates an OpenAI embedding model service. + * + * **When to use** + * + * Use to construct the `EmbeddingModel.Service` effectfully when + * `OpenAiClient` is already available in the environment. + * + * **Details** + * + * The `model` option is sent with each embedding request. Constructor `config` + * supplies create-embedding request fields other than `model` and `input`, and + * scoped overrides from `withConfigOverride` are merged last for each request. + * + * **Gotchas** + * + * The service expects numeric embedding vectors. It fails with + * `InvalidOutputError` when the provider returns base64 embeddings, + * out-of-range indexes, duplicate indexes, or an unexpected number of + * embeddings. + * + * @see {@link layer} for providing the embedding model service as a layer + * @see {@link model} for creating an `AiModel` that also provides dimensions + * @see {@link withConfigOverride} for scoped request configuration overrides + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Effect.fn.Return { + const client = yield* OpenAiClient + + const makeConfig = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + return yield* EmbeddingModel.make({ + embedMany: Effect.fnUntraced(function*({ inputs }) { + const config = yield* makeConfig + const response = yield* client.createEmbedding({ ...config, input: inputs }) + return yield* mapProviderResponse(inputs.length, response) + }) + }) +}) + +/** + * Creates a layer for the OpenAI embedding model. + * + * **When to use** + * + * Use when composing application layers and you want OpenAI to satisfy + * `EmbeddingModel.EmbeddingModel` while supplying `OpenAiClient` from another + * layer. + * + * **Gotchas** + * + * Use the default floating-point embedding format. The service expects numeric + * vectors and fails with `InvalidOutputError` if OpenAI returns base64 + * embeddings. + * + * @see {@link make} for constructing the embedding model service effectfully + * @see {@link model} for creating an `AiModel` that also provides embedding dimensions + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Layer.Layer => + Layer.effect(EmbeddingModel.EmbeddingModel, make(options)) + +/** + * Provides config overrides for OpenAI embedding model operations. + * + * **When to use** + * + * Use when you need scoped OpenAI embedding request defaults for a single + * effect or workflow without rebuilding the embedding model service. + * + * **Details** + * + * Supports both data-first and data-last forms. Existing scoped config is read + * first, then the provided overrides are applied so override fields take + * precedence. + * + * @see {@link Config} for the scoped embedding request configuration service + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +const mapProviderResponse = ( + inputLength: number, + response: typeof OpenAiSchema.CreateEmbeddingResponse.Type +): Effect.Effect => { + if (response.data.length !== inputLength) { + return Effect.fail( + invalidOutput("Provider returned " + response.data.length + " embeddings but expected " + inputLength) + ) + } + + const results = new Array>(inputLength) + const seen = new Set() + + for (const entry of response.data) { + if (!Number.isInteger(entry.index) || entry.index < 0 || entry.index >= inputLength) { + return Effect.fail(invalidOutput("Provider returned invalid embedding index: " + entry.index)) + } + if (seen.has(entry.index)) { + return Effect.fail(invalidOutput("Provider returned duplicate embedding index: " + entry.index)) + } + if (!Array.isArray(entry.embedding)) { + return Effect.fail(invalidOutput("Provider returned non-vector embedding at index " + entry.index)) + } + + seen.add(entry.index) + results[entry.index] = [...entry.embedding] + } + + if (seen.size !== inputLength) { + return Effect.fail( + invalidOutput("Provider returned embeddings for " + seen.size + " inputs but expected " + inputLength) + ) + } + + return Effect.succeed({ + results, + usage: { + inputTokens: response.usage?.prompt_tokens + } + }) +} + +const invalidOutput = (description: string): AiError.AiError => + AiError.make({ + module: "OpenAiEmbeddingModel", + method: "embedMany", + reason: new AiError.InvalidOutputError({ description }) + }) diff --git a/repos/effect/packages/ai/openai/src/OpenAiError.ts b/repos/effect/packages/ai/openai/src/OpenAiError.ts new file mode 100644 index 0000000..cb0ccc2 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiError.ts @@ -0,0 +1,242 @@ +/** + * OpenAI error metadata augmentation. + * + * Provides OpenAI-specific metadata fields for AI error types through module + * augmentation, enabling typed access to OpenAI error details. + * + * @since 4.0.0 + */ + +/** + * OpenAI-specific error metadata fields. + * + * @category models + * @since 4.0.0 + */ +export type OpenAiErrorMetadata = { + /** + * The OpenAI error code returned by the API. + */ + readonly errorCode: string | null + /** + * The OpenAI error type returned by the API. + */ + readonly errorType: string | null + /** + * The unique request ID for debugging with OpenAI support. + */ + readonly requestId: string | null +} + +/** + * OpenAI-specific rate limit metadata fields. + * + * **Details** + * + * Extends base error metadata with rate limit specific information from + * OpenAI's rate limit headers. + * + * @category models + * @since 4.0.0 + */ +export type OpenAiRateLimitMetadata = OpenAiErrorMetadata & { + /** + * The rate limit type (e.g. "requests", "tokens"). + */ + readonly limit: string | null + /** + * Number of remaining requests in the current window. + */ + readonly remaining: number | null + /** + * Time until the request rate limit resets. + */ + readonly resetRequests: string | null + /** + * Time until the token rate limit resets. + */ + readonly resetTokens: string | null +} + +declare module "effect/unstable/ai/AiError" { + /** + * OpenAI metadata attached to `RateLimitError` values. + * + * **Details** + * + * Captures OpenAI error details together with rate limit header information + * from responses where the provider rejected the request because a limit was + * reached. + * + * @category configuration + * @since 4.0.0 + */ + export interface RateLimitErrorMetadata { + /** + * OpenAI-specific details for the rate limit response. + */ + readonly openai?: OpenAiRateLimitMetadata | null + } + + /** + * OpenAI metadata attached to `QuotaExhaustedError` values. + * + * **Details** + * + * Preserves provider error details for failures caused by exhausted account, + * billing, or usage quota. + * + * @category configuration + * @since 4.0.0 + */ + export interface QuotaExhaustedErrorMetadata { + /** + * OpenAI-specific details for the quota exhaustion response. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `AuthenticationError` values. + * + * **Details** + * + * Preserves provider error details for failed API key, authorization, or + * permission checks. + * + * @category configuration + * @since 4.0.0 + */ + export interface AuthenticationErrorMetadata { + /** + * OpenAI-specific details for the authentication failure. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `ContentPolicyError` values. + * + * **Details** + * + * Preserves provider error details when OpenAI rejects input or output because + * it violates a content policy. + * + * @category configuration + * @since 4.0.0 + */ + export interface ContentPolicyErrorMetadata { + /** + * OpenAI-specific details for the content policy response. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `InvalidRequestError` values. + * + * **Details** + * + * Preserves provider error details for malformed requests, unsupported + * parameters, or other request validation failures reported by OpenAI. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidRequestErrorMetadata { + /** + * OpenAI-specific details for the invalid request response. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `InternalProviderError` values. + * + * **Details** + * + * Preserves provider error details for OpenAI-side failures such as transient + * server errors. + * + * @category configuration + * @since 4.0.0 + */ + export interface InternalProviderErrorMetadata { + /** + * OpenAI-specific details for the internal provider response. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `InvalidOutputError` values. + * + * **Details** + * + * Preserves provider error details when an OpenAI response cannot be parsed or + * validated as the expected output. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidOutputErrorMetadata { + /** + * OpenAI-specific details for the invalid output response. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `StructuredOutputError` values. + * + * **Details** + * + * Preserves provider error details when OpenAI returns content that does not + * satisfy the requested structured output schema. + * + * @category configuration + * @since 4.0.0 + */ + export interface StructuredOutputErrorMetadata { + /** + * OpenAI-specific details for the structured output failure. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `UnsupportedSchemaError` values. + * + * **Details** + * + * Preserves provider error details when an unsupported schema failure is + * associated with an OpenAI response. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnsupportedSchemaErrorMetadata { + /** + * OpenAI-specific details for the unsupported schema failure. + */ + readonly openai?: OpenAiErrorMetadata | null + } + + /** + * OpenAI metadata attached to `UnknownError` values. + * + * **Details** + * + * Preserves provider error details for OpenAI failures that do not map cleanly + * to a more specific AI error category. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnknownErrorMetadata { + /** + * OpenAI-specific details for the unclassified provider failure. + */ + readonly openai?: OpenAiErrorMetadata | null + } +} diff --git a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts new file mode 100644 index 0000000..7930d3d --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -0,0 +1,3133 @@ +/** + * The `OpenAiLanguageModel` module provides the OpenAI Responses API + * implementation of Effect AI's `LanguageModel` service. It translates Effect + * AI prompts, files, tools, structured output requests, reasoning metadata, and + * provider options into OpenAI response requests, then converts OpenAI + * non-streaming or streaming response results back into Effect AI response + * content and metadata. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AST from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { DeepMutable, Mutable, Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import * as IdGenerator from "effect/unstable/ai/IdGenerator" +import * as LanguageModel from "effect/unstable/ai/LanguageModel" +import * as AiModel from "effect/unstable/ai/Model" +import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" +import type * as Prompt from "effect/unstable/ai/Prompt" +import type * as Response from "effect/unstable/ai/Response" +import * as Tool from "effect/unstable/ai/Tool" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Generated from "./Generated.ts" +import * as InternalUtilities from "./internal/utilities.ts" +import { OpenAiClient } from "./OpenAiClient.ts" +import type * as OpenAiSchema from "./OpenAiSchema.ts" +import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" +import type * as OpenAiTool from "./OpenAiTool.ts" + +const ResponseModelIds = Generated.ModelIdsResponses.members[1] +const SharedModelIds = Generated.ModelIdsShared.members[1] + +/** + * OpenAI model identifiers supported by the Responses API language model. + * + * @category models + * @since 4.0.0 + */ +export type Model = typeof ResponseModelIds.Encoded | typeof SharedModelIds.Encoded + +/** + * Image detail level for vision requests. + */ +type ImageDetail = "auto" | "low" | "high" + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Context service for OpenAI language model configuration. + * + * **When to use** + * + * Use when you need to provide OpenAI Responses API request defaults through + * Effect context for language model operations. + * + * **Details** + * + * Config values are merged with the config object passed to `model`, `make`, or + * `layer`, with scoped context values taking precedence. + * + * @see {@link withConfigOverride} for scoping language model request overrides + * + * @category services + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + Simplify< + & Partial< + Omit< + typeof OpenAiSchema.CreateResponse.Encoded, + "input" | "tools" | "tool_choice" | "stream" | "text" + > + > + & { + /** + * File ID prefixes used to identify file IDs in Responses API. + * When undefined, all file data is treated as base64 content. + * + * Examples: + * - OpenAI: ['file-'] for IDs like 'file-abc123' + * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123' + */ + readonly fileIdPrefixes?: ReadonlyArray | undefined + /** + * Configuration options for a text response from the model. + */ + readonly text?: { + /** + * Constrains the verbosity of the model's response. Lower values will + * result in more concise responses, while higher values will result in + * more verbose responses. + * + * Defaults to `"medium"`. + */ + readonly verbosity?: "low" | "medium" | "high" | undefined + } | undefined + /** + * Whether to use strict JSON schema validation. + * + * Defaults to `true`. + */ + readonly strictJsonSchema?: boolean | undefined + } + > +>()("@effect/ai-openai/OpenAiLanguageModel/Config") {} + +// ============================================================================= +// Provider Options / Metadata +// ============================================================================= + +declare module "effect/unstable/ai/Prompt" { + /** + * OpenAI-specific options for file prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface FilePartOptions extends ProviderOptions { + /** + * Provider-specific file options for the OpenAI Responses API. + */ + readonly openai?: { + /** + * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`. + */ + readonly imageDetail?: ImageDetail | null + } | null + } + + /** + * OpenAI-specific options for reasoning prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ReasoningPartOptions extends ProviderOptions { + /** + * Provider-specific reasoning options for the OpenAI Responses API. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The encrypted content of the reasoning item - populated when a response + * is generated with `reasoning.encrypted_content` in the `include` + * parameter. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI-specific options for assistant tool-call prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ToolCallPartOptions extends ProviderOptions { + /** + * Provider-specific tool-call options for the OpenAI Responses API. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status of item. + */ + readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null + /** + * The ID of the approval request. + */ + readonly approvalRequestId?: string | null + } | null + } + + /** + * OpenAI-specific options for tool-result prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface ToolResultPartOptions extends ProviderOptions { + /** + * Provider-specific tool-result options for the OpenAI Responses API. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status of item. + */ + readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null + /** + * The ID of the approval request. + */ + readonly approvalId?: string | null + } | null + } + + /** + * OpenAI-specific options for text prompt parts. + * + * @category request + * @since 4.0.0 + */ + export interface TextPartOptions extends ProviderOptions { + /** + * Provider-specific text options for the OpenAI Responses API. + */ + readonly openai?: { + /** + * The ID of the item to reference. + */ + readonly itemId?: string | null + /** + * The status of item. + */ + readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null + /** + * A list of annotations that apply to the output text. + */ + readonly annotations?: ReadonlyArray | null + } | null + } +} + +declare module "effect/unstable/ai/Response" { + /** + * OpenAI metadata attached to a complete text response part. + * + * @category response + * @since 4.0.0 + */ + export interface TextPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the text part. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the text part. + */ + readonly itemId?: string | null + /** + * If the model emits a refusal content part, the refusal explanation + * from the model will be contained in the metadata of an empty text + * part. + */ + readonly refusal?: string | null + /** + * The status of item. + */ + readonly status?: typeof OpenAiSchema.MessageStatus.Encoded | null + /** + * The text content part annotations. + */ + readonly annotations?: ReadonlyArray | null + } + } + + /** + * OpenAI metadata emitted when a streamed text part starts. + * + * @category response + * @since 4.0.0 + */ + export interface TextStartPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed text start. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the streamed text part. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI metadata emitted when a streamed text part ends. + * + * @category response + * @since 4.0.0 + */ + export interface TextEndPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed text end. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the streamed text part. + */ + readonly itemId?: string | null + /** + * The annotations collected for the completed streamed text part. + */ + readonly annotations?: ReadonlyArray | null + } | null + } + + /** + * OpenAI metadata attached to a complete reasoning response part. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the reasoning part. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI metadata emitted when a streamed reasoning part starts. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningStartPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning start. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string | null + } | null + } + + /** + * OpenAI metadata emitted for a streamed reasoning delta. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning delta. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI metadata emitted when a streamed reasoning part ends. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningEndPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning end. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the reasoning part. + */ + readonly itemId?: string | null + /** + * Encrypted reasoning content that can be sent back in later requests. + */ + readonly encryptedContent?: string + } | null + } + + /** + * OpenAI metadata attached to tool-call response parts. + * + * @category response + * @since 4.0.0 + */ + export interface ToolCallPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the tool call. + */ + readonly openai?: { + /** + * The OpenAI item ID associated with the tool call. + */ + readonly itemId?: string | null + } | null + } + + /** + * OpenAI metadata attached to document source citations. + * + * @category response + * @since 4.0.0 + */ + export interface DocumentSourcePartMetadata extends ProviderMetadata { + /** + * Provider-specific citation metadata for the OpenAI Responses API. + */ + readonly openai?: + | { + /** + * Identifies a citation to an uploaded file. + */ + readonly type: "file_citation" + /** + * The index of the file in the list of files. + */ + readonly index: number + /** + * The ID of the file. + */ + readonly fileId: string + } + | { + /** + * Identifies a citation to a generated file path. + */ + readonly type: "file_path" + /** + * The index of the file in the list of files. + */ + readonly index: number + /** + * The ID of the file. + */ + readonly fileId: string + } + | { + /** + * Identifies a citation to a file inside a container. + */ + readonly type: "container_file_citation" + /** + * The ID of the file. + */ + readonly fileId: string + /** + * The ID of the container file. + */ + readonly containerId: string + } + | null + } + + /** + * OpenAI metadata attached to URL source citations. + * + * @category response + * @since 4.0.0 + */ + export interface UrlSourcePartMetadata extends ProviderMetadata { + /** + * Provider-specific URL citation metadata for the OpenAI Responses API. + */ + readonly openai?: { + /** + * Identifies a citation to a URL. + */ + readonly type: "url_citation" + /** + * The index of the first character of the URL citation in the message. + */ + readonly startIndex: number + /** + * The index of the last character of the URL citation in the message. + */ + readonly endIndex: number + } | null + } + + /** + * OpenAI metadata attached to finish response parts. + * + * @category response + * @since 4.0.0 + */ + export interface FinishPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned when generation finishes. + */ + readonly openai?: { + /** + * The service tier reported by OpenAI for the response. + */ + readonly serviceTier?: "default" | "auto" | "flex" | "scale" | "priority" | null + } | null + } +} + +// ============================================================================= +// Language Model +// ============================================================================= + +/** + * Creates an OpenAI model descriptor that can be provided with + * `Effect.provide`. + * + * **When to use** + * + * Use when you want an OpenAI language model value that carries provider and + * model metadata and can be supplied directly to an Effect program. + * + * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly + * @see {@link make} for constructing the language model service effectfully + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: (string & {}) | Model, + config?: Omit +): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenAiClient> => + AiModel.make("openai", model, layer({ model, config })) + +// TODO +// /** +// * @since 4.0.0 +// * @category constructors +// */ +// export const modelWithTokenizer = ( +// model: (string & {}) | Model, +// config?: Omit +// ): AiModel.Model<"openai", LanguageModel.LanguageModel | Tokenizer.Tokenizer, OpenAiClient> => +// AiModel.make("openai", model, layerWithTokenizer({ model, config })) + +/** + * Creates an OpenAI `LanguageModel` service from a model identifier and + * optional request defaults. + * + * **When to use** + * + * Use to construct an OpenAI Responses API language model service backed by + * `OpenAiClient`. + * + * **Details** + * + * The returned effect requires `OpenAiClient`. Request defaults from the + * `config` option are merged with any `Config` service in the context, with + * context values taking precedence. The service supports both `generateText` + * and `streamText`. + * + * @see {@link layer} for providing the service as a `Layer` + * @see {@link model} for creating a model descriptor for `Effect.provide` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Effect.fn.Return { + const client = yield* OpenAiClient + + const makeConfig = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + const makeRequest = Effect.fnUntraced( + function*>({ config, options, toolNameMapper }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return { + const include = new Set() + const capabilities = getModelCapabilities(config.model as string) + const messages = yield* prepareMessages({ + config, + options, + capabilities, + include, + toolNameMapper + }) + const { toolChoice, tools } = yield* prepareTools({ + config, + options, + toolNameMapper + }) + const responseFormat = yield* prepareResponseFormat({ + config, + options + }) + const { fileIdPrefixes: _fip, strictJsonSchema: _sjs, ...apiConfig } = config + const request: Mutable = { + ...apiConfig, + input: messages, + include: include.size > 0 ? Array.from(include) : undefined, + text: { + verbosity: config.text?.verbosity ?? undefined, + format: responseFormat + } + } + if (tools) request.tools = tools + if (toolChoice) request.tool_choice = toolChoice + if (options.previousResponseId) request.previous_response_id = options.previousResponseId + return request + } + ) + + return yield* LanguageModel.make({ + codecTransformer: toCodecOpenAI, + generateText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request) + const [rawResponse, response] = yield* client.createResponse(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse({ + options, + rawResponse, + response, + toolNameMapper + }) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const toolNameMapper = new Tool.NameMapper(options.tools) + const request = yield* makeRequest({ config, options, toolNameMapper }) + annotateRequest(options.span, request) + const [response, stream] = yield* client.createResponseStream(request) + return yield* makeStreamResponse({ + stream, + response, + config, + options, + toolNameMapper + }) + }, + (effect, options) => + effect.pipe( + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * Creates a layer that provides the OpenAI `LanguageModel.LanguageModel` + * service. + * + * **When to use** + * + * Use when composing application layers and you want OpenAI to satisfy + * `LanguageModel.LanguageModel` while supplying `OpenAiClient` from another + * layer. + * + * **Details** + * + * The `config` option supplies request defaults for the selected model. Scoped + * values from `withConfigOverride` are merged when each request is built and + * take precedence over these defaults. + * + * @see {@link make} for constructing the language model service effectfully + * @see {@link model} for creating a model descriptor for `Effect.provide` + * @see {@link withConfigOverride} for scoped request configuration overrides + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make(options)) + +/** + * Provides scoped config overrides for OpenAI language model operations. + * + * **When to use** + * + * Use to apply OpenAI Responses API config overrides around one or more + * language model operations without changing the defaults passed to `model`, + * `make`, or `layer`. + * + * **Details** + * + * The override is dual, so it can be used in pipe form or as + * `withConfigOverride(effect, overrides)`. Overrides are merged with any + * existing `Config` service in the current context, and the override values take + * precedence. + * + * @see {@link Config} for the scoped configuration service consumed by this function + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const getSystemMessageMode = (model: string): "system" | "developer" => + model.startsWith("o") || + model.startsWith("gpt-5") || + model.startsWith("codex-") || + model.startsWith("computer-use") + ? "developer" + : "system" + +const prepareMessages = Effect.fnUntraced( + function*>({ + config, + options, + capabilities, + include, + toolNameMapper + }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly include: Set + readonly capabilities: ModelCapabilities + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return, AiError.AiError> { + const processedApprovalIds = new Set() + + const hasConversation = Predicate.isNotNullish(config.conversation) + + // Provider-Defined Tools + const applyPatchTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiApplyPatch" + ) + const codeInterpreterTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiCodeInterpreter" + ) + const shellTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiFunctionShell" + ) + const localShellTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiLocalShell" + ) + const webSearchTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiWebSearch" + ) + const webSearchPreviewTool = options.tools.find((tool): tool is ReturnType => + Tool.isProviderDefined(tool) && tool.name === "OpenAiWebSearchPreview" + ) + + // Handle Included Features + if (Predicate.isNotUndefined(config.top_logprobs)) { + include.add("message.output_text.logprobs") + } + if (config.store === false && capabilities.isReasoningModel) { + include.add("reasoning.encrypted_content") + } + if (codeInterpreterTool) { + include.add("code_interpreter_call.outputs") + } + if (webSearchTool || webSearchPreviewTool) { + include.add("web_search_call.action.sources") + } + + const messages: Array = [] + const prompt = options.incrementalPrompt ?? options.prompt + + for (const message of prompt.content) { + switch (message.role) { + case "system": { + messages.push({ + role: getSystemMessageMode(config.model as string), + content: message.content + }) + break + } + + case "user": { + const content: Array = [] + + for (let index = 0; index < message.content.length; index++) { + const part = message.content[index] + + switch (part.type) { + case "text": { + content.push({ type: "input_text", text: part.text }) + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const detail = getImageDetail(part) + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_image", file_id: part.data, detail }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_image", image_url: part.data.toString(), detail }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const imageUrl = `data:${mediaType};base64,${base64}` + content.push({ type: "input_image", image_url: imageUrl, detail }) + } + } else if (part.mediaType === "application/pdf") { + if (typeof part.data === "string" && isFileId(part.data, config)) { + content.push({ type: "input_file", file_id: part.data }) + } + + if (part.data instanceof URL) { + content.push({ type: "input_file", file_url: part.data.toString() }) + } + + if (part.data instanceof Uint8Array) { + const base64 = Encoding.encodeBase64(part.data) + const fileName = part.fileName ?? `part-${index}.pdf` + const fileData = `data:application/pdf;base64,${base64}` + content.push({ type: "input_file", filename: fileName, file_data: fileData }) + } + } else { + return yield* AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidRequestError({ + description: `Detected unsupported media type for file: '${part.mediaType}'` + }) + }) + } + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const reasoningMessages: Record> = {} + + for (const part of message.content) { + switch (part.type) { + case "text": { + const id = getItemId(part) + + // When in conversation mode, skip items that already exist in the + // conversation context to avoid "Duplicate item found" errors + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (config.store === true && Predicate.isNotNull(id)) { + messages.push({ type: "item_reference", id }) + break + } + + messages.push({ + id: id!, + type: "message", + role: "assistant", + status: part.options.openai?.status ?? "completed", + content: [{ + type: "output_text", + text: part.text, + annotations: part.options.openai?.annotations ?? [], + logprobs: [] + }] + }) + + break + } + + case "reasoning": { + const id = getItemId(part) + const encryptedContent = getEncryptedContent(part) + + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (Predicate.isNotNull(id)) { + const message = reasoningMessages[id] + + if (config.store === true) { + // Use item references to refer to reasoning (single reference) + // when the first part is encountered + if (Predicate.isUndefined(message)) { + messages.push({ type: "item_reference", id }) + + // Store unused reasoning message to mark its id as used + reasoningMessages[id] = { + type: "reasoning", + id, + summary: [] + } + } + } else { + const summaryParts: Array = [] + + if (part.text.length > 0) { + summaryParts.push({ type: "summary_text", text: part.text }) + } + + if (Predicate.isUndefined(message)) { + reasoningMessages[id] = { + type: "reasoning", + id, + summary: summaryParts, + ...(Predicate.isNotNull(encryptedContent) + ? { encrypted_content: encryptedContent } + : undefined) + } + + messages.push(reasoningMessages[id]) + } else { + message.summary.push(...summaryParts) + + // Update encrypted content to enable setting it in the + // last summary part + if (Predicate.isNotNull(encryptedContent)) { + message.encrypted_content = encryptedContent + } + } + } + } + + break + } + + case "tool-call": { + const id = getItemId(part) + const status = getStatus(part) + + if (hasConversation && Predicate.isNotNull(id)) { + break + } + + if (config.store && Predicate.isNotNull(id)) { + messages.push({ type: "item_reference", id }) + break + } + + if (part.providerExecuted) { + break + } + + const toolName = toolNameMapper.getProviderName(part.name) + + if (Predicate.isNotUndefined(localShellTool) && toolName === "local_shell") { + const params = yield* Schema.decodeUnknownEffect(localShellTool.parametersSchema)(part.params).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareMessages", + reason: new AiError.ToolParameterValidationError({ + toolName: "local_shell", + toolParams: part.params as Schema.Json, + description: error.message + }) + }) + ) + ) + + messages.push({ + id: id!, + type: "local_shell_call", + call_id: part.id, + status: status ?? "completed", + action: params.action + }) + + break + } + + if (Predicate.isNotUndefined(shellTool) && toolName === "shell") { + const params = yield* Schema.decodeUnknownEffect(shellTool.parametersSchema)(part.params).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareMessages", + reason: new AiError.ToolParameterValidationError({ + toolName: "shell", + toolParams: part.params as Schema.Json, + description: error.message + }) + }) + ) + ) + + messages.push({ + id: id!, + type: "shell_call", + call_id: part.id, + status: status ?? "completed", + action: params.action + }) + + break + } + + messages.push({ + type: "function_call", + name: toolName, + call_id: part.id, + arguments: JSON.stringify(part.params), + ...(Predicate.isNotNull(id) ? { id } : {}), + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + + break + } + + // Assistant tool-result parts are always provider executed + case "tool-result": { + // Skip execution denied results - these have no corresponding + // item in OpenAI's store + if ( + Predicate.hasProperty(part.result, "type") && + part.result.type === "execution-denied" + ) { + break + } + + if (hasConversation) { + break + } + + if (config.store === true) { + const id = getItemId(part) ?? part.id + messages.push({ type: "item_reference", id }) + } + } + } + } + + break + } + + case "tool": { + for (const part of message.content) { + if (part.type === "tool-approval-response") { + if (processedApprovalIds.has(part.approvalId)) { + continue + } + + processedApprovalIds.add(part.approvalId) + + if (config.store === true) { + messages.push({ type: "item_reference", id: part.approvalId }) + } + + messages.push({ + type: "mcp_approval_response", + approval_request_id: part.approvalId, + approve: part.approved + } as any) + + continue + } + + // Skip execution-denied results that already have an approvalId - + // this indicates that the part was already handled via tool-approval-response + if ( + Predicate.hasProperty(part.result, "type") && + part.result.type === "execution-denied" + ) { + if (Predicate.isNotNullish(part.options.openai?.approvalId)) { + continue + } + } + + const id = getItemId(part) ?? part.id + const status = getStatus(part) + const toolName = toolNameMapper.getProviderName(part.name) + + if (Predicate.isNotUndefined(applyPatchTool) && toolName === "apply_patch") { + messages.push({ + id, + type: "apply_patch_call_output", + call_id: part.id, + ...(part.result as any) + }) + } + + if (Predicate.isNotUndefined(shellTool) && toolName === "shell") { + messages.push({ + id, + type: "shell_call_output", + call_id: part.id, + output: part.result as any, + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + } + + if (Predicate.isNotUndefined(localShellTool) && toolName === "local_shell") { + messages.push({ + id, + type: "local_shell_call_output", + call_id: part.id, + output: part.result as any, + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + } + + messages.push({ + type: "function_call_output", + call_id: part.id, + output: JSON.stringify(part.result), + ...(Predicate.isNotNull(status) ? { status } : {}) + }) + } + + break + } + } + } + + return messages + } +) + +// ============================================================================= +// HTTP Details +// ============================================================================= + +const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +const buildHttpResponseDetails = ( + response: HttpClientResponse.HttpClientResponse +): typeof Response.HttpResponseDetails.Type => ({ + status: response.status, + headers: Redactable.redact(response.headers) as Record +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type + +type KnownResponseStreamEventType = + | "response.created" + | "response.completed" + | "response.incomplete" + | "response.failed" + | "response.output_item.added" + | "response.output_item.done" + | "response.output_text.delta" + | "response.output_text.annotation.added" + | "response.reasoning_summary_part.added" + | "response.reasoning_summary_part.done" + | "response.reasoning_summary_text.delta" + | "response.function_call_arguments.delta" + | "response.function_call_arguments.done" + | "response.code_interpreter_call_code.delta" + | "response.code_interpreter_call_code.done" + | "response.apply_patch_call_operation_diff.delta" + | "response.apply_patch_call_operation_diff.done" + | "response.image_generation_call.partial_image" + | "error" + +type KnownResponseStreamEvent = Extract + +const knownResponseStreamEventTypes = new Set([ + "response.created", + "response.completed", + "response.incomplete", + "response.failed", + "response.output_item.added", + "response.output_item.done", + "response.output_text.delta", + "response.output_text.annotation.added", + "response.reasoning_summary_part.added", + "response.reasoning_summary_part.done", + "response.reasoning_summary_text.delta", + "response.function_call_arguments.delta", + "response.function_call_arguments.done", + "response.code_interpreter_call_code.delta", + "response.code_interpreter_call_code.done", + "response.apply_patch_call_operation_diff.delta", + "response.apply_patch_call_operation_diff.done", + "response.image_generation_call.partial_image", + "error" +]) + +const isKnownResponseStreamEvent = ( + event: ResponseStreamEvent +): event is KnownResponseStreamEvent => knownResponseStreamEventTypes.has(event.type as KnownResponseStreamEventType) + +const makeResponse = Effect.fnUntraced( + function*>({ + options, + rawResponse, + response, + toolNameMapper + }: { + readonly options: LanguageModel.ProviderOptions + readonly rawResponse: OpenAiSchema.Response + readonly response: HttpClientResponse.HttpClientResponse + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Array, + AiError.AiError, + IdGenerator.IdGenerator + > { + const idGenerator = yield* IdGenerator.IdGenerator + + const approvalRequests = getApprovalRequestIdMapping(options.prompt) + + const webSearchTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + (tool.name === "OpenAiWebSearch" || + tool.name === "OpenAiWebSearchPreview") + ) as Tool.AnyProviderDefined | undefined + + let hasToolCalls = false + const parts: Array = [] + + const createdAt = new Date(rawResponse.created_at * 1000) + parts.push({ + type: "response-metadata", + id: rawResponse.id, + modelId: rawResponse.model as string, + timestamp: DateTime.formatIso(DateTime.fromDateUnsafe(createdAt)), + request: buildHttpRequestDetails(response.request) + }) + + for (const part of rawResponse.output) { + switch (part.type) { + case "apply_patch_call": { + const toolName = toolNameMapper.getCustomName("apply_patch") + parts.push({ + type: "tool-call", + id: part.call_id, + name: toolName, + params: { call_id: part.call_id, operation: part.operation }, + metadata: { openai: makeItemIdMetadata(part.id) } + }) + break + } + + case "code_interpreter_call": { + const toolName = toolNameMapper.getCustomName("code_interpreter") + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: { code: part.code, container_id: part.container_id }, + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: part.id, + name: toolName, + isFailure: false, + result: { outputs: part.outputs }, + providerExecuted: true + }) + break + } + + case "file_search_call": { + const toolName = toolNameMapper.getCustomName("file_search") + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: {}, + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: part.id, + name: toolName, + isFailure: false, + result: { + status: part.status, + queries: part.queries, + results: part.results ?? null + }, + providerExecuted: true + }) + break + } + + case "function_call": { + hasToolCalls = true + + const toolName = part.name + + const toolParams = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(part.arguments), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams: {}, + description: `Faled to securely JSON parse tool parameters: ${cause}` + }) + }) + }) + + const params = yield* transformToolCallParams(options.tools, part.name, toolParams) + + parts.push({ + type: "tool-call", + id: part.call_id, + name: toolName, + params, + metadata: { openai: makeItemIdMetadata(part.id) } + }) + break + } + + case "image_generation_call": { + const toolName = toolNameMapper.getCustomName("image_generation") + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: {}, + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: part.id, + name: toolName, + isFailure: false, + result: { result: part.result } + }) + break + } + + case "local_shell_call": { + const toolName = toolNameMapper.getCustomName("local_shell") + parts.push({ + type: "tool-call", + id: part.call_id, + name: toolName, + params: { action: part.action }, + metadata: { openai: makeItemIdMetadata(part.id) } + }) + break + } + + case "mcp_call": { + const toolId = Predicate.isNotNullish(part.approval_request_id) + ? (approvalRequests.get(part.approval_request_id) ?? part.id) + : part.id + + const { toolName, params } = yield* normalizeMcpToolCall({ + toolNameMapper, + toolParams: part.arguments, + method: "makeResponse" + }) + + parts.push({ + type: "tool-call", + id: toolId, + name: toolName, + params, + providerExecuted: true + }) + + parts.push({ + type: "tool-result", + id: toolId, + name: toolName, + isFailure: false, + providerExecuted: true, + result: { + type: "mcp_call", + name: part.name, + arguments: part.arguments, + server_label: part.server_label, + ...(Predicate.isNotNullish(part.output) ? { output: part.output } : undefined), + ...(Predicate.isNotNullish(part.error) ? { error: part.error } : undefined) + }, + metadata: { openai: makeItemIdMetadata(part.id) } + }) + + break + } + + case "mcp_list_tools": { + // Skip + break + } + + case "mcp_approval_request": { + const approvalRequestId = (part as any).approval_request_id ?? part.id + const toolId = yield* idGenerator.generateId() + + const { toolName, params } = yield* normalizeMcpToolCall({ + toolNameMapper, + toolParams: part.arguments, + method: "makeResponse" + }) + + parts.push({ + type: "tool-call", + id: toolId, + name: toolName, + params, + providerExecuted: true + }) + + parts.push({ + type: "tool-approval-request", + toolCallId: toolId, + approvalId: approvalRequestId + }) + + break + } + + case "message": { + for (const contentPart of part.content) { + switch (contentPart.type) { + case "output_text": { + const annotations = contentPart.annotations.length > 0 + ? { annotations: contentPart.annotations as any } + : undefined + + parts.push({ + type: "text", + text: contentPart.text, + metadata: { + openai: { + ...makeItemIdMetadata(part.id), + ...annotations + } + } + }) + for (const annotation of contentPart.annotations) { + if (annotation.type === "container_file_citation") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: annotation.filename, + fileName: annotation.filename, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + containerId: annotation.container_id + } + } + }) + } + if (annotation.type === "file_citation") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: annotation.filename, + fileName: annotation.filename, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + index: annotation.index + } + } + }) + } + if (annotation.type === "file_path") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "application/octet-stream", + title: annotation.file_id, + fileName: annotation.file_id, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + index: annotation.index + } + } + }) + } + if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: yield* idGenerator.generateId(), + url: annotation.url, + title: annotation.title, + metadata: { + openai: { + type: annotation.type, + startIndex: annotation.start_index, + endIndex: annotation.end_index + } + } + }) + } + } + break + } + case "refusal": { + parts.push({ + type: "text", + text: "", + metadata: { openai: { refusal: contentPart.refusal } } + }) + break + } + } + } + break + } + + case "reasoning": { + const metadata = { + openai: { + ...makeItemIdMetadata(part.id), + ...makeEncryptedContentMetadata(part.encrypted_content) + } + } + // If there are no summary parts, we have to add an empty one to + // propagate the part identifier and encrypted content + if (part.summary.length === 0) { + parts.push({ type: "reasoning", text: "", metadata }) + } else { + for (const summary of part.summary) { + parts.push({ type: "reasoning", text: summary.text, metadata }) + } + } + break + } + + case "shell_call": { + const toolName = toolNameMapper.getCustomName("shell") + parts.push({ + type: "tool-call", + id: part.call_id, + name: toolName, + params: { action: part.action }, + metadata: { openai: makeItemIdMetadata(part.id) } + }) + break + } + + case "web_search_call": { + const toolName = toolNameMapper.getCustomName( + webSearchTool?.name ?? "web_search" + ) + parts.push({ + type: "tool-call", + id: part.id, + name: toolName, + params: {}, + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: part.id, + name: toolName, + isFailure: false, + result: { action: part.action, status: part.status }, + providerExecuted: true + }) + break + } + } + } + + const finishReason = InternalUtilities.resolveFinishReason( + rawResponse.incomplete_details?.reason, + hasToolCalls + ) + + parts.push({ + type: "finish", + reason: finishReason, + usage: getUsage(rawResponse.usage), + response: buildHttpResponseDetails(response), + ...toServiceTier(rawResponse.service_tier) + }) + + return parts + } +) + +const makeStreamResponse = Effect.fnUntraced( + function*>({ + stream, + response, + config, + options, + toolNameMapper + }: { + readonly config: typeof Config.Service + readonly stream: Stream.Stream + readonly response: HttpClientResponse.HttpClientResponse + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper + }): Effect.fn.Return< + Stream.Stream, + AiError.AiError, + IdGenerator.IdGenerator + > { + const idGenerator = yield* IdGenerator.IdGenerator + + const approvalRequests = getApprovalRequestIdMapping(options.prompt) + const streamApprovalRequests = new Map() + + let hasToolCalls = false + + // Track annotations for current message to include in text-end metadata + const activeAnnotations: Array = [] + + type ReasoningSummaryPartStatus = "active" | "can-conclude" | "concluded" + type ReasoningPart = { + encryptedContent: string | undefined + summaryParts: Record + } + + // Track active reasoning items with state machine for proper concluding logic + const activeReasoning: Record = {} + + const getOrCreateReasoningPart = ( + itemId: string, + encryptedContent?: string | null + ): ReasoningPart => { + const activePart = activeReasoning[itemId] + if (Predicate.isNotUndefined(activePart)) { + if (Predicate.isNotNullish(encryptedContent)) { + activePart.encryptedContent = encryptedContent + } + return activePart + } + + const reasoningPart: ReasoningPart = { + encryptedContent: Predicate.isNotNullish(encryptedContent) ? encryptedContent : undefined, + summaryParts: {} + } + activeReasoning[itemId] = reasoningPart + return reasoningPart + } + + // Track active tool calls with optional provider-specific state + const activeToolCalls: Record = {} + + const webSearchTool = options.tools.find((tool) => + Tool.isProviderDefined(tool) && + (tool.name === "OpenAiWebSearch" || + tool.name === "OpenAiWebSearchPreview") + ) as ReturnType | ReturnType | undefined + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if (!isKnownResponseStreamEvent(event)) { + return parts + } + + switch (event.type) { + case "response.created": { + const createdAt = new Date(event.response.created_at * 1000) + parts.push({ + type: "response-metadata", + id: event.response.id, + modelId: event.response.model, + timestamp: DateTime.formatIso(DateTime.fromDateUnsafe(createdAt)), + request: buildHttpRequestDetails(response.request) + }) + break + } + + case "error": { + parts.push({ type: "error", error: event }) + break + } + + case "response.completed": + case "response.incomplete": + case "response.failed": { + parts.push({ + type: "finish", + reason: InternalUtilities.resolveFinishReason( + event.response.incomplete_details?.reason, + hasToolCalls + ), + usage: getUsage(event.response.usage), + response: buildHttpResponseDetails(response), + ...toServiceTier(event.response.service_tier) + }) + break + } + + case "response.output_item.added": { + switch (event.item.type) { + case "apply_patch_call": { + const toolId = event.item.call_id + const toolName = toolNameMapper.getCustomName("apply_patch") + const operation = event.item.operation + activeToolCalls[event.output_index] = { + id: toolId, + name: toolName, + applyPatch: { + hasDiff: operation.type !== "delete_file", + endEmitted: operation.type === "delete_file" + } + } + parts.push({ + type: "tool-params-start", + id: toolId, + name: toolName + }) + + if (operation.type === "delete_file") { + parts.push({ + type: "tool-params-delta", + id: toolId, + delta: JSON.stringify({ + call_id: toolId, + operation: operation + }) + }) + parts.push({ + type: "tool-params-end", + id: toolId + }) + } else { + parts.push({ + type: "tool-params-delta", + id: toolId, + delta: `{"call_id":"${InternalUtilities.escapeJSONDelta(toolId)}",` + + `"operation":{"type":"${InternalUtilities.escapeJSONDelta(operation.type)}",` + + `"path":"${InternalUtilities.escapeJSONDelta(operation.path)}","diff":"` + }) + } + break + } + + case "code_interpreter_call": { + const toolName = toolNameMapper.getCustomName("code_interpreter") + activeToolCalls[event.output_index] = { + id: event.item.id, + name: toolName, + codeInterpreter: { containerId: event.item.container_id } + } + parts.push({ + type: "tool-params-start", + id: event.item.id, + name: toolName, + providerExecuted: true + }) + parts.push({ + type: "tool-params-delta", + id: event.item.id, + delta: `{"containerId":"${event.item.container_id}","code":"` + }) + break + } + + case "computer_call": { + const toolName = toolNameMapper.getCustomName("computer_use") + activeToolCalls[event.output_index] = { + id: event.item.id, + name: toolName + } + parts.push({ + type: "tool-params-start", + id: event.item.id, + name: toolName, + providerExecuted: true + }) + break + } + + case "file_search_call": { + const toolName = toolNameMapper.getCustomName("file_search") + parts.push({ + type: "tool-call", + id: event.item.id, + name: toolName, + params: {}, + providerExecuted: true + }) + break + } + + case "function_call": { + activeToolCalls[event.output_index] = { + id: event.item.call_id, + name: event.item.name, + functionCall: { emitted: false } + } + parts.push({ + type: "tool-params-start", + id: event.item.call_id, + name: event.item.name + }) + break + } + + case "image_generation_call": { + const toolName = toolNameMapper.getCustomName("image_generation") + parts.push({ + type: "tool-call", + id: event.item.id, + name: toolName, + params: {}, + providerExecuted: true + }) + break + } + + case "mcp_call": + case "mcp_list_tools": + case "mcp_approval_request": { + // We emit MCP tool call / approvals on `output_item.done` to facilitate: + // - Aliasing tool call identifiers when an approval request id exists + // - Emit a proper tool-approval-request part for MCP approvals + break + } + + case "message": { + // Clear annotations for new message + activeAnnotations.length = 0 + parts.push({ + type: "text-start", + id: event.item.id, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + break + } + + case "reasoning": { + const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content) + if (Predicate.isUndefined(reasoningPart.summaryParts[0])) { + reasoningPart.summaryParts[0] = "active" + parts.push({ + type: "reasoning-start", + id: `${event.item.id}:0`, + metadata: { + openai: { + ...makeItemIdMetadata(event.item.id), + ...makeEncryptedContentMetadata(reasoningPart.encryptedContent) + } + } + }) + } + break + } + + case "shell_call": { + const toolName = toolNameMapper.getCustomName("shell") + activeToolCalls[event.output_index] = { + id: event.item.id ?? event.item.call_id, + name: toolName + } + break + } + + case "web_search_call": { + const toolName = toolNameMapper.getCustomName( + webSearchTool?.providerName ?? "web_search" + ) + activeToolCalls[event.output_index] = { + id: event.item.id, + name: toolName + } + parts.push({ + type: "tool-params-start", + id: event.item.id, + name: webSearchTool?.name ?? "OpenAiWebSearch", + providerExecuted: true + }) + parts.push({ + type: "tool-params-end", + id: event.item.id + }) + parts.push({ + type: "tool-call", + id: event.item.id, + name: toolName, + params: {}, + providerExecuted: true + }) + break + } + } + + break + } + + case "response.output_item.done": { + switch (event.item.type) { + case "apply_patch_call": { + const toolCall = activeToolCalls[event.output_index] + if ( + Predicate.isNotUndefined(toolCall.applyPatch) && + !toolCall.applyPatch.endEmitted && + event.item.operation.type !== "delete_file" + ) { + if (!toolCall.applyPatch.hasDiff) { + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: InternalUtilities.escapeJSONDelta(event.item.operation.diff ?? "") + }) + } + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: `"}}` + }) + parts.push({ + type: "tool-params-end", + id: toolCall.id + }) + toolCall.applyPatch.endEmitted = true + } + // Emit the final tool call with the complete diff when the status is completed + if (Predicate.isNotUndefined(toolCall) && event.item.status === "completed") { + const toolName = toolNameMapper.getCustomName("apply_patch") + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolName, + params: { call_id: event.item.call_id, operation: event.item.operation }, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + } + delete activeToolCalls[event.output_index] + break + } + + case "code_interpreter_call": { + delete activeToolCalls[event.output_index] + const toolName = toolNameMapper.getCustomName("code_interpreter") + parts.push({ + type: "tool-result", + id: event.item.id, + name: toolName, + isFailure: false, + result: { outputs: event.item.outputs }, + providerExecuted: true + }) + break + } + + case "computer_call": { + delete activeToolCalls[event.output_index] + const toolName = toolNameMapper.getCustomName("computer_use") + parts.push({ + type: "tool-params-end", + id: event.item.id + }) + parts.push({ + type: "tool-call", + id: event.item.id, + name: toolName, + params: {}, + providerExecuted: true + }) + parts.push({ + type: "tool-result", + id: event.item.id, + name: toolName, + isFailure: false, + result: { status: event.item.status ?? "completed" } + }) + break + } + + case "file_search_call": { + delete activeToolCalls[event.output_index] + const toolName = toolNameMapper.getCustomName("file_search") + const results = Predicate.isNotNullish(event.item.results) + ? { results: event.item.results } + : undefined + parts.push({ + type: "tool-result", + id: event.item.id, + name: toolName, + isFailure: false, + result: { ...results, status: event.item.status, queries: event.item.queries }, + providerExecuted: true + }) + break + } + + case "function_call": { + const toolCall = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCall?.functionCall?.emitted) && toolCall.functionCall.emitted) { + delete activeToolCalls[event.output_index] + break + } + delete activeToolCalls[event.output_index] + + hasToolCalls = true + + const toolName = event.item.name + const toolArgs = event.item.arguments + + const toolParams = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolArgs), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeStreamResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams: {}, + description: `Failed securely JSON parse tool parameters: ${cause}` + }) + }) + }) + + const params = yield* transformToolCallParams(options.tools, toolName, toolParams) + + parts.push({ + type: "tool-params-end", + id: event.item.call_id + }) + + parts.push({ + type: "tool-call", + id: event.item.call_id, + name: toolName, + params, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + + break + } + + case "image_generation_call": { + const toolName = toolNameMapper.getCustomName("image_generation") + parts.push({ + type: "tool-result", + id: event.item.id, + name: toolName, + isFailure: false, + result: { result: event.item.result }, + providerExecuted: true + }) + break + } + + case "local_shell_call": { + const toolName = toolNameMapper.getCustomName("local_shell") + parts.push({ + type: "tool-call", + id: event.item.call_id, + name: toolName, + params: { action: event.item.action }, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + break + } + + case "mcp_call": { + const approvalRequestId = event.item.approval_request_id + + // Track approval with our own tool call identifiers + const toolId = Predicate.isNotNullish(approvalRequestId) + ? (streamApprovalRequests.get(approvalRequestId) ?? approvalRequests.get(approvalRequestId) ?? + event.item.id) + : event.item.id + + const { toolName, params } = yield* normalizeMcpToolCall({ + toolNameMapper, + toolParams: event.item.arguments, + method: "makeStreamResponse" + }) + + parts.push({ + type: "tool-call", + id: toolId, + name: toolName, + params, + providerExecuted: true + }) + + parts.push({ + type: "tool-result", + id: toolId, + name: toolName, + isFailure: false, + providerExecuted: true, + result: { + type: "mcp_call", + name: event.item.name, + arguments: event.item.arguments, + server_label: event.item.server_label, + ...(Predicate.isNotNullish(event.item.output) ? { output: event.item.output } : undefined), + ...(Predicate.isNotNullish(event.item.error) ? { error: event.item.error } : undefined) + }, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + + break + } + + case "mcp_list_tools": { + // Skip + break + } + + case "mcp_approval_request": { + const toolId = yield* idGenerator.generateId() + const approvalRequestId = (event.item as any).approval_request_id ?? event.item.id + streamApprovalRequests.set(approvalRequestId, toolId) + const { toolName, params } = yield* normalizeMcpToolCall({ + toolNameMapper, + toolParams: event.item.arguments, + method: "makeStreamResponse" + }) + parts.push({ + type: "tool-call", + id: toolId, + name: toolName, + params, + providerExecuted: true + }) + parts.push({ + type: "tool-approval-request", + approvalId: approvalRequestId, + toolCallId: toolId + }) + break + } + + case "message": { + const annotations = activeAnnotations.length > 0 + ? { annotations: activeAnnotations.slice() } + : undefined + parts.push({ + type: "text-end", + id: event.item.id, + metadata: { openai: { ...annotations, ...makeItemIdMetadata(event.item.id) } } + }) + break + } + + case "reasoning": { + const reasoningPart = getOrCreateReasoningPart(event.item.id, event.item.encrypted_content) + for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) { + if (status === "active" || status === "can-conclude") { + parts.push({ + type: "reasoning-end", + id: `${event.item.id}:${summaryIndex}`, + metadata: { + openai: { + ...makeItemIdMetadata(event.item.id), + ...makeEncryptedContentMetadata(reasoningPart.encryptedContent) + } + } + }) + } + } + delete activeReasoning[event.item.id] + break + } + + case "shell_call": { + delete activeToolCalls[event.output_index] + const toolName = toolNameMapper.getCustomName("shell") + parts.push({ + type: "tool-call", + id: event.item.id ?? event.item.call_id, + name: toolName, + params: { action: event.item.action }, + metadata: { openai: makeItemIdMetadata(event.item.id) } + }) + break + } + + case "web_search_call": { + delete activeToolCalls[event.output_index] + const toolName = toolNameMapper.getCustomName( + webSearchTool?.name ?? "web_search" + ) + parts.push({ + type: "tool-result", + id: event.item.id, + name: toolName, + isFailure: false, + result: { action: event.item.action, status: event.item.status }, + providerExecuted: true + }) + break + } + } + + break + } + + case "response.output_text.delta": { + parts.push({ + type: "text-delta", + id: event.item_id, + delta: event.delta + }) + break + } + + case "response.output_text.annotation.added": { + const annotation = event.annotation as typeof OpenAiSchema.Annotation.Encoded + // Track annotation for text-end metadata + activeAnnotations.push(annotation) + if (annotation.type === "container_file_citation") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: annotation.filename, + fileName: annotation.filename, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + containerId: annotation.container_id + } + } + }) + } else if (annotation.type === "file_citation") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "text/plain", + title: annotation.filename, + fileName: annotation.filename, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + index: annotation.index + } + } + }) + } else if (annotation.type === "file_path") { + parts.push({ + type: "source", + sourceType: "document", + id: yield* idGenerator.generateId(), + mediaType: "application/octet-stream", + title: annotation.file_id, + fileName: annotation.file_id, + metadata: { + openai: { + type: annotation.type, + fileId: annotation.file_id, + index: annotation.index + } + } + }) + } else if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: yield* idGenerator.generateId(), + url: annotation.url, + title: annotation.title, + metadata: { + openai: { + type: annotation.type, + startIndex: annotation.start_index, + endIndex: annotation.end_index + } + } + }) + } + break + } + + case "response.function_call_arguments.delta": { + const toolCallPart = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCallPart)) { + parts.push({ + type: "tool-params-delta", + id: toolCallPart.id, + delta: event.delta + }) + } + break + } + + case "response.function_call_arguments.done": { + const toolCall = activeToolCalls[event.output_index] + if ( + Predicate.isNotUndefined(toolCall?.functionCall) && + !toolCall.functionCall.emitted + ) { + hasToolCalls = true + + const toolParams = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(event.arguments), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeStreamResponse", + reason: new AiError.ToolParameterValidationError({ + toolName: toolCall.name, + toolParams: {}, + description: `Failed securely JSON parse tool parameters: ${cause}` + }) + }) + }) + + const params = yield* transformToolCallParams(options.tools, toolCall.name, toolParams) + + parts.push({ + type: "tool-params-end", + id: toolCall.id + }) + + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolCall.name, + params, + metadata: { openai: makeItemIdMetadata(event.item_id) } + }) + + toolCall.functionCall.emitted = true + } + break + } + + case "response.apply_patch_call_operation_diff.delta": { + const toolCall = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCall?.applyPatch)) { + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: InternalUtilities.escapeJSONDelta(event.delta) + }) + toolCall.applyPatch.hasDiff = true + } + break + } + + case "response.apply_patch_call_operation_diff.done": { + const toolCall = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCall?.applyPatch) && !toolCall.applyPatch.endEmitted) { + if (!toolCall.applyPatch.hasDiff && Predicate.isNotUndefined(event.delta)) { + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: InternalUtilities.escapeJSONDelta(event.delta) + }) + toolCall.applyPatch.hasDiff = true + } + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: `"}}` + }) + parts.push({ + type: "tool-params-end", + id: toolCall.id + }) + toolCall.applyPatch.endEmitted = true + } + break + } + + case "response.code_interpreter_call_code.delta": { + const toolCall = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCall)) { + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: InternalUtilities.escapeJSONDelta(event.delta) + }) + } + break + } + + case "response.code_interpreter_call_code.done": { + const toolCall = activeToolCalls[event.output_index] + if (Predicate.isNotUndefined(toolCall) && Predicate.isNotUndefined(toolCall.codeInterpreter)) { + const toolName = toolNameMapper.getCustomName("code_interpreter") + parts.push({ + type: "tool-params-delta", + id: toolCall.id, + delta: "\"}" + }) + parts.push({ type: "tool-params-end", id: toolCall.id }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolName, + params: { + code: event.code, + container_id: toolCall.codeInterpreter.containerId + }, + providerExecuted: true + }) + } + break + } + + case "response.image_generation_call.partial_image": { + const toolName = toolNameMapper.getCustomName("image_generation") + parts.push({ + type: "tool-result", + id: event.item_id, + name: toolName, + isFailure: false, + providerExecuted: false, + result: { result: event.partial_image_b64 }, + preliminary: true + }) + break + } + + case "response.reasoning_summary_part.added": { + const reasoningPart = getOrCreateReasoningPart(event.item_id) + if (event.summary_index > 0) { + // Conclude all can-conclude parts before starting new one + for (const [summaryIndex, status] of Object.entries(reasoningPart.summaryParts)) { + if (status === "can-conclude") { + parts.push({ + type: "reasoning-end", + id: `${event.item_id}:${summaryIndex}`, + metadata: { + openai: { + ...makeItemIdMetadata(event.item_id), + ...makeEncryptedContentMetadata(reasoningPart.encryptedContent) + } + } + }) + reasoningPart.summaryParts[Number(summaryIndex)] = "concluded" + } + } + } + + if (Predicate.isUndefined(reasoningPart.summaryParts[event.summary_index])) { + reasoningPart.summaryParts[event.summary_index] = "active" + parts.push({ + type: "reasoning-start", + id: `${event.item_id}:${event.summary_index}`, + metadata: { + openai: { + ...makeItemIdMetadata(event.item_id), + ...makeEncryptedContentMetadata(reasoningPart.encryptedContent) + } + } + }) + } + break + } + + case "response.reasoning_summary_text.delta": { + parts.push({ + type: "reasoning-delta", + id: `${event.item_id}:${event.summary_index}`, + delta: event.delta, + metadata: { openai: makeItemIdMetadata(event.item_id) } + }) + break + } + + case "response.reasoning_summary_part.done": { + const reasoningPart = getOrCreateReasoningPart(event.item_id) + // When OpenAI stores message data, we can immediately conclude the + // reasoning part given that we do not need the encrypted content + if (config.store === true) { + parts.push({ + type: "reasoning-end", + id: `${event.item_id}:${event.summary_index}`, + metadata: { openai: makeItemIdMetadata(event.item_id) } + }) + // Mark the summary part concluded + reasoningPart.summaryParts[event.summary_index] = "concluded" + } else { + // Mark the summary part as can-conclude given we still need a + // final summary part with the encrypted content + reasoningPart.summaryParts[event.summary_index] = "can-conclude" + } + break + } + } + + return parts + })), + Stream.flattenIterable + ) + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof OpenAiSchema.CreateResponse.Encoded +): void => { + addGenAIAnnotations(span, { + system: "openai", + operation: { name: "chat" }, + request: { + model: request.model as string, + temperature: request.temperature as number | undefined, + topP: request.top_p as number | undefined, + maxTokens: request.max_output_tokens as number | undefined + }, + openai: { + request: { + responseFormat: (request.text as any)?.format?.type, + serviceTier: request.service_tier as string | undefined + } + } + }) +} + +const annotateResponse = (span: Span, response: OpenAiSchema.Response): void => { + const finishReason = response.incomplete_details?.reason as string | undefined + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model as string, + finishReasons: Predicate.isNotUndefined(finishReason) ? [finishReason] : undefined + }, + usage: { + inputTokens: response.usage?.input_tokens as number | undefined, + outputTokens: response.usage?.output_tokens as number | undefined + }, + openai: { + response: { + serviceTier: response.service_tier as string | undefined + } + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + const serviceTier = (part.metadata as any)?.openai?.serviceTier as string | undefined + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens.total, + outputTokens: part.usage.outputTokens.total + }, + openai: { + response: { serviceTier } + } + }) + } +} + +// ============================================================================= +// Tool Conversion +// ============================================================================= + +type OpenAiToolChoice = typeof OpenAiSchema.CreateResponse.Encoded["tool_choice"] + +const prepareTools = Effect.fnUntraced(function*>({ + config, + options, + toolNameMapper +}: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly toolNameMapper: Tool.NameMapper +}): Effect.fn.Return<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: OpenAiToolChoice | undefined +}, AiError.AiError> { + // Return immediately if no tools are in the toolkit + if (options.tools.length === 0) { + return { tools: undefined, toolChoice: undefined } + } + + const tools: Array = [] + let toolChoice: OpenAiToolChoice | undefined = undefined + + // Filter the incoming tools down to the set of allowed tools as indicated by + // the tool choice. This must be done here given that there is no tool name + // in OpenAI's provider-defined tools, so there would be no way to perform + // this filter otherwise + let allowedTools = options.tools + if (typeof options.toolChoice === "object" && "oneOf" in options.toolChoice) { + const allowedToolNames = new Set(options.toolChoice.oneOf) + allowedTools = options.tools.filter((tool) => allowedToolNames.has(tool.name)) + toolChoice = options.toolChoice.mode === "required" ? "required" : "auto" + } + + // Convert the tools in the toolkit to the provider-defined format + for (const tool of allowedTools) { + if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) { + const strict = Tool.getStrictMode(tool) ?? config.strictJsonSchema ?? true + const description = Tool.getDescription(tool) + const parameters = yield* tryToolJsonSchema(tool, "prepareTools") + tools.push({ + type: "function", + name: tool.name, + parameters, + strict, + ...(Predicate.isNotUndefined(description) ? { description } : undefined) + }) + } + + if (Tool.isProviderDefined(tool)) { + const openAiTool = tool as OpenAiTool.OpenAiTool + switch (openAiTool.name) { + case "OpenAiApplyPatch": { + tools.push({ type: "apply_patch" }) + break + } + case "OpenAiCodeInterpreter": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "code_interpreter" + }) + break + } + case "OpenAiFileSearch": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "file_search" + }) + break + } + case "OpenAiShell": { + tools.push({ type: "shell" }) + break + } + case "OpenAiImageGeneration": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "image_generation" + }) + break + } + case "OpenAiLocalShell": { + tools.push({ type: "local_shell" }) + break + } + case "OpenAiMcp": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "mcp" + }) + break + } + case "OpenAiWebSearch": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "web_search" + }) + break + } + case "OpenAiWebSearchPreview": { + const args = yield* Schema.decodeUnknownEffect(openAiTool.argsSchema)(tool.args).pipe( + Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.ToolConfigurationError({ + toolName: openAiTool.name, + description: error.message + }) + }) + ) + ) + tools.push({ + ...args, + type: "web_search_preview" + }) + break + } + default: { + return yield* AiError.make({ + module: "OpenAiLanguageModel", + method: "prepareTools", + reason: new AiError.InvalidRequestError({ + description: `Unknown provider-defined tool '${tool.name}'` + }) + }) + } + } + } + } + + if (options.toolChoice === "auto" || options.toolChoice === "none" || options.toolChoice === "required") { + toolChoice = options.toolChoice + } + + if (typeof options.toolChoice === "object" && "tool" in options.toolChoice) { + const toolName = toolNameMapper.getProviderName(options.toolChoice.tool) + const providerNames = toolNameMapper.providerNames + if (providerNames.includes(toolName)) { + toolChoice = { type: toolName as any } + } else { + toolChoice = { type: "function", name: options.toolChoice.tool } + } + } + + return { tools, toolChoice } +}) + +// ============================================================================= +// Utilities +// ============================================================================= + +const isFileId = (data: string, config: typeof Config.Service): boolean => + config.fileIdPrefixes != null && config.fileIdPrefixes.some((prefix) => data.startsWith(prefix)) + +const getItemId = ( + part: + | Prompt.TextPart + | Prompt.ReasoningPart + | Prompt.ToolCallPart + | Prompt.ToolResultPart +): string | null => part.options.openai?.itemId ?? null +const getStatus = ( + part: + | Prompt.TextPart + | Prompt.ToolCallPart + | Prompt.ToolResultPart +): typeof OpenAiSchema.MessageStatus.Encoded | null => part.options.openai?.status ?? null +const getEncryptedContent = ( + part: Prompt.ReasoningPart +): string | null => part.options.openai?.encryptedContent ?? null + +const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto" + +const makeItemIdMetadata = (itemId: string | undefined) => Predicate.isNotUndefined(itemId) ? { itemId } : {} + +const makeEncryptedContentMetadata = (encryptedContent: string | null | undefined) => + Predicate.isNotNullish(encryptedContent) ? { encryptedContent } : undefined + +const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError => + AiError.make({ + module: "OpenAiLanguageModel", + method, + reason: new AiError.UnsupportedSchemaError({ + description: error instanceof Error ? error.message : String(error) + }) + }) + +const tryCodecTransform = (schema: S, method: string) => + Effect.try({ + try: () => toCodecOpenAI(schema), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const tryJsonSchema = (schema: S, method: string) => + Effect.try({ + try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const tryToolJsonSchema = (tool: T, method: string) => + Effect.try({ + try: () => Tool.getJsonSchema(tool, { transformer: toCodecOpenAI }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const prepareResponseFormat = Effect.fnUntraced(function*({ config, options }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions +}): Effect.fn.Return { + if (options.responseFormat.type === "json") { + const name = options.responseFormat.objectName + const schema = options.responseFormat.schema + const jsonSchema = yield* tryJsonSchema(schema, "prepareResponseFormat") + return { + type: "json_schema", + name, + description: AST.resolveDescription(schema.ast) ?? "Response with a JSON object", + schema: jsonSchema, + strict: config.strictJsonSchema ?? true + } + } + return { type: "text" } +}) + +interface ModelCapabilities { + readonly isReasoningModel: boolean + readonly systemMessageMode: "remove" | "system" | "developer" + readonly supportsFlexProcessing: boolean + readonly supportsPriorityProcessing: boolean + /** + * Allow temperature, topP, logProbs when reasoningEffort is none. + */ + readonly supportsNonReasoningParameters: boolean +} + +const getModelCapabilities = (modelId: string): ModelCapabilities => { + const supportsFlexProcessing = modelId.startsWith("o3") || + modelId.startsWith("o4-mini") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) + + const supportsPriorityProcessing = modelId.startsWith("gpt-4") || + modelId.startsWith("gpt-5-mini") || + (modelId.startsWith("gpt-5") && + !modelId.startsWith("gpt-5-nano") && + !modelId.startsWith("gpt-5-chat")) || + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") + + // Use allowlist approach: only known reasoning models should use 'developer' role + // This prevents issues with fine-tuned models, third-party models, and custom models + const isReasoningModel = modelId.startsWith("o1") || + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") || + modelId.startsWith("codex-mini") || + modelId.startsWith("computer-use-preview") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) + + // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility + // GPT-5.1 and GPT-5.2 support temperature, topP, logProbs when reasoningEffort is none + const supportsNonReasoningParameters = modelId.startsWith("gpt-5.1") || modelId.startsWith("gpt-5.2") + + const systemMessageMode = isReasoningModel ? "developer" : "system" + + return { + supportsFlexProcessing, + supportsPriorityProcessing, + isReasoningModel, + systemMessageMode, + supportsNonReasoningParameters + } +} + +const getApprovalRequestIdMapping = (prompt: Prompt.Prompt): ReadonlyMap => { + const mapping = new Map() + + for (const message of prompt.content) { + if (message.role !== "assistant") { + continue + } + + for (const part of message.content) { + if (part.type !== "tool-call") { + continue + } + + const approvalRequestId = part.options.openai?.approvalRequestId + + if (Predicate.isNotNullish(approvalRequestId)) { + mapping.set(approvalRequestId, part.id) + } + } + } + + return mapping +} + +const normalizeMcpToolCall = Effect.fnUntraced(function*>({ + toolNameMapper, + toolParams, + method +}: { + readonly toolNameMapper: Tool.NameMapper + readonly toolParams: unknown + readonly method: string +}): Effect.fn.Return<{ + readonly toolName: string + readonly params: unknown +}, AiError.AiError> { + const toolName = toolNameMapper.getCustomName("mcp") + + if (typeof toolParams !== "string") { + return { toolName, params: toolParams } + } + + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + AiError.make({ + module: "OpenAiLanguageModel", + method, + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams, + description: `Failed to securely JSON parse tool parameters: ${cause}` + }) + }) + }) + + return { toolName, params } +}) + +const getUsage = (usage: OpenAiSchema.ResponseUsage | null | undefined): Response.Usage => { + if (Predicate.isNullish(usage)) { + return { + inputTokens: { + uncached: undefined, + total: undefined, + cacheRead: undefined, + cacheWrite: undefined + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined + } + } + } + + const inputTokens = usage.input_tokens + const outputTokens = usage.output_tokens + const cachedTokens = getUsageTokenDetail(usage.input_tokens_details, "cached_tokens") + const reasoningTokens = getUsageTokenDetail(usage.output_tokens_details, "reasoning_tokens") + + return { + inputTokens: { + uncached: inputTokens - cachedTokens, + total: inputTokens, + cacheRead: cachedTokens, + cacheWrite: undefined + }, + outputTokens: { + total: outputTokens, + text: outputTokens - reasoningTokens, + reasoning: reasoningTokens + } + } +} + +type ServiceTier = "default" | "auto" | "flex" | "scale" | "priority" | null + +const toServiceTier = (value: string | undefined): { + readonly metadata: { + readonly openai: { + readonly serviceTier: ServiceTier + } + } +} | undefined => { + switch (value) { + case "default": + case "auto": + case "flex": + case "scale": + case "priority": + return { metadata: { openai: { serviceTier: value } } } + default: + return undefined + } +} + +const getUsageTokenDetail = (details: unknown, key: string): number => + Predicate.hasProperty(details, key) && typeof details[key] === "number" ? details[key] : 0 + +const transformToolCallParams = Effect.fnUntraced(function*>( + tools: Tools, + toolName: string, + toolParams: unknown +): Effect.fn.Return { + const tool = tools.find((tool) => tool.name === toolName) + + if (Predicate.isUndefined(tool)) { + return yield* AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolNotFoundError({ + toolName, + availableTools: tools.map((tool) => tool.name) + }) + }) + } + + const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse") + + const transform = Schema.decodeEffect(codec) + + return yield* ( + transform(toolParams) as Effect.Effect + ).pipe(Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams, + description: error.issue.toString() + }) + }) + )) +}) diff --git a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts new file mode 100644 index 0000000..b93188d --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiSchema.ts @@ -0,0 +1,1286 @@ +/** + * The `OpenAiSchema` module defines the request, response, streaming, and + * embedding schemas used by the handwritten OpenAI client. These schemas are + * the transport boundary for JSON sent to and decoded from the Responses and + * embeddings endpoints. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" + +const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown) + +const JsonObject = Schema.Record(Schema.String, Schema.Unknown) + +const MessageRole = Schema.Literals(["system", "developer", "user", "assistant"]) + +const ImageDetail = Schema.Literals(["low", "high", "auto"]) + +/** + * Schema for optional `include` values supported by the local handwritten + * Responses client schema. + * + * **Details** + * + * These values request additional response fields such as image URLs, encrypted + * reasoning content, output logprobs, code interpreter outputs, or web search + * sources. This schema enumerates the include values supported by this client + * path. + * + * @category schemas + * @since 4.0.0 + */ +export const IncludeEnum = Schema.Literals([ + "message.input_image.image_url", + "reasoning.encrypted_content", + "message.output_text.logprobs", + "code_interpreter_call.outputs", + "web_search_call.action.sources" +]) + +/** + * Type of optional `include` values accepted by OpenAI Responses requests. + * + * @category models + * @since 4.0.0 + */ +export type IncludeEnum = typeof IncludeEnum.Type + +/** + * Schema for lifecycle statuses shared by messages, reasoning items, and tool calls. + * + * **Details** + * + * Accepted values are `"in_progress"`, `"completed"`, and `"incomplete"`. + * This item-level status is used by message, reasoning, and tool-call shapes. + * + * @category schemas + * @since 4.0.0 + */ +export const MessageStatus = Schema.Literals(["in_progress", "completed", "incomplete"]) + +/** + * Lifecycle status shared by messages, reasoning items, and tool calls. + * + * **Details** + * + * Accepted values are `"in_progress"`, `"completed"`, and `"incomplete"`. + * + * @category models + * @since 4.0.0 + */ +export type MessageStatus = typeof MessageStatus.Type + +const InputTextContent = Schema.Struct({ + type: Schema.Literal("input_text"), + text: Schema.String +}) + +const InputImageContent = Schema.Struct({ + type: Schema.Literal("input_image"), + image_url: Schema.optionalKey(Schema.NullOr(Schema.String)), + file_id: Schema.optionalKey(Schema.NullOr(Schema.String)), + detail: Schema.optionalKey(Schema.NullOr(ImageDetail)) +}) + +const InputFileContent = Schema.Struct({ + type: Schema.Literal("input_file"), + file_id: Schema.optionalKey(Schema.NullOr(Schema.String)), + filename: Schema.optionalKey(Schema.String), + file_url: Schema.optionalKey(Schema.String), + file_data: Schema.optionalKey(Schema.String) +}) + +/** + * Schema for content blocks accepted in OpenAI Responses input messages. + * + * **Details** + * + * Accepted block variants are `input_text`, `input_image`, and `input_file`. + * + * @see {@link InputItem} for request input item shapes that can contain these content blocks + * + * @category schemas + * @since 4.0.0 + */ +export const InputContent = Schema.Union([ + InputTextContent, + InputImageContent, + InputFileContent +]) + +/** + * Content block accepted in OpenAI Responses input messages. + * + * **Details** + * + * Accepted block variants are `input_text`, `input_image`, and `input_file`. + * + * @category models + * @since 4.0.0 + */ +export type InputContent = typeof InputContent.Type + +/** + * Schema for a text block containing a model-provided reasoning summary. + * + * **Details** + * + * The decoded shape is `type: "summary_text"` plus `text` containing the + * reasoning summary text. + * + * @see {@link ReasoningItem} for reasoning output items that contain summary text blocks + * + * @category schemas + * @since 4.0.0 + */ +export const SummaryTextContent = Schema.Struct({ + type: Schema.Literal("summary_text"), + text: Schema.String +}) + +/** + * Text content block used for model-provided reasoning summaries. + * + * @category models + * @since 4.0.0 + */ +export type SummaryTextContent = typeof SummaryTextContent.Type + +const ReasoningTextContent = Schema.Struct({ + type: Schema.Literal("reasoning_text"), + text: Schema.String +}) + +const RefusalContent = Schema.Struct({ + type: Schema.Literal("refusal"), + refusal: Schema.String +}) + +const TextContent = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String +}) + +const ComputerScreenshotContent = Schema.Struct({ + type: Schema.Literal("computer_screenshot"), + image_url: Schema.NullOr(Schema.String), + file_id: Schema.NullOr(Schema.String) +}) + +const FileCitationAnnotation = Schema.Struct({ + type: Schema.Literal("file_citation"), + file_id: Schema.String, + index: Schema.Number, + filename: Schema.String +}) + +const UrlCitationAnnotation = Schema.Struct({ + type: Schema.Literal("url_citation"), + url: Schema.String, + start_index: Schema.Number, + end_index: Schema.Number, + title: Schema.String +}) + +const ContainerFileCitationAnnotation = Schema.Struct({ + type: Schema.Literal("container_file_citation"), + container_id: Schema.String, + file_id: Schema.String, + start_index: Schema.Number, + end_index: Schema.Number, + filename: Schema.String +}) + +const FilePathAnnotation = Schema.Struct({ + type: Schema.Literal("file_path"), + file_id: Schema.String, + index: Schema.Number +}) + +/** + * Schema for citation and file-path annotations attached to output text content. + * + * **Details** + * + * Accepts annotation objects discriminated by `type`: `file_citation`, + * `url_citation`, `container_file_citation`, or `file_path`. + * + * @category schemas + * @since 4.0.0 + */ +export const Annotation = Schema.Union([ + FileCitationAnnotation, + UrlCitationAnnotation, + ContainerFileCitationAnnotation, + FilePathAnnotation +]) + +/** + * Citation or file-path annotation attached to output text content. + * + * **Details** + * + * Accepted annotation variants are `file_citation`, `url_citation`, + * `container_file_citation`, and `file_path`. + * + * @category models + * @since 4.0.0 + */ +export type Annotation = typeof Annotation.Type + +const OutputTextContent = Schema.Struct({ + type: Schema.Literal("output_text"), + text: Schema.String, + annotations: Schema.Array(Annotation), + logprobs: Schema.optionalKey(Schema.Array(Schema.Unknown)) +}) + +const OutputMessageContent = Schema.Union([ + InputTextContent, + OutputTextContent, + TextContent, + SummaryTextContent, + ReasoningTextContent, + RefusalContent, + InputImageContent, + ComputerScreenshotContent, + InputFileContent +]) + +const OutputMessage = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("message"), + role: Schema.Literal("assistant"), + content: Schema.Array(OutputMessageContent), + status: MessageStatus +}) + +/** + * Schema for a reasoning output item containing encrypted content, summaries, and optional reasoning text. + * + * **When to use** + * + * Use when decoding or encoding OpenAI Responses reasoning items that may be + * carried into later request input. + * + * **Details** + * + * Reasoning items represent model reasoning content. `summary` is required, + * while `content` and `status` are optional. + * + * **Gotchas** + * + * `encrypted_content` is populated only when `reasoning.encrypted_content` is + * requested through `include`. + * + * @see {@link InputItem} for request input items that can carry reasoning items + * @see {@link IncludeEnum} for requesting encrypted reasoning content + * + * @category schemas + * @since 4.0.0 + */ +export const ReasoningItem = Schema.Struct({ + type: Schema.Literal("reasoning"), + id: Schema.String, + encrypted_content: Schema.optionalKey(Schema.NullOr(Schema.String)), + summary: Schema.Array(SummaryTextContent), + content: Schema.optionalKey(Schema.Array(ReasoningTextContent)), + status: Schema.optionalKey(MessageStatus) +}) + +/** + * Reasoning output item containing encrypted content, summaries, and optional reasoning text. + * + * **When to use** + * + * Use when typing OpenAI Responses reasoning items that may be carried into + * later request input. + * + * **Details** + * + * Reasoning items represent model reasoning content. `summary` is required, + * while `content` and `status` are optional. + * + * **Gotchas** + * + * `encrypted_content` is populated only when `reasoning.encrypted_content` is + * requested through `include`. + * + * @category models + * @since 4.0.0 + */ +export type ReasoningItem = typeof ReasoningItem.Type + +const FunctionCall = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("function_call"), + call_id: Schema.String, + name: Schema.String, + arguments: Schema.String, + status: Schema.optionalKey(MessageStatus) +}) + +const FunctionCallOutput = Schema.Struct({ + id: Schema.optionalKey(Schema.NullOr(Schema.String)), + type: Schema.Literal("function_call_output"), + call_id: Schema.String, + output: Schema.Union([ + Schema.String, + Schema.Array(InputContent) + ]), + status: Schema.optionalKey(Schema.NullOr(MessageStatus)) +}) + +const ItemReference = Schema.Struct({ + type: Schema.Literal("item_reference"), + id: Schema.String +}) + +const LocalShellCall = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("local_shell_call"), + call_id: Schema.String, + action: Schema.Unknown, + status: Schema.optionalKey(MessageStatus) +}) + +const LocalShellCallOutput = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("local_shell_call_output"), + call_id: Schema.String, + output: Schema.Unknown, + status: Schema.optionalKey(MessageStatus) +}) + +const ShellCall = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("shell_call"), + call_id: Schema.String, + action: Schema.Unknown, + status: Schema.optionalKey(MessageStatus) +}) + +const ShellCallOutput = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("shell_call_output"), + call_id: Schema.String, + output: Schema.Unknown, + status: Schema.optionalKey(MessageStatus) +}) + +const ApplyPatchCallOutput = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + type: Schema.Literal("apply_patch_call_output"), + call_id: Schema.String, + status: Schema.optionalKey(MessageStatus), + output: Schema.optionalKey(Schema.Unknown) +}) + +const McpApprovalResponse = Schema.Struct({ + type: Schema.Literal("mcp_approval_response"), + approval_request_id: Schema.String, + approve: Schema.Boolean +}) + +const RequestMessageItem = Schema.Struct({ + type: Schema.optionalKey(Schema.Literal("message")), + role: MessageRole, + status: Schema.optionalKey(MessageStatus), + content: Schema.Union([ + Schema.String, + Schema.Array(InputContent) + ]) +}) + +/** + * Schema for item shapes accepted by an OpenAI Responses request `input` field. + * + * **When to use** + * + * Use when validating structured `CreateResponse.input` array items. + * + * **Details** + * + * Accepted item families include request/output messages, function call and + * function call output, reasoning items, item references, shell and local shell + * calls and outputs, apply-patch output, and MCP approval responses. + * + * @see {@link CreateResponse} for the request schema that consumes input items + * @see {@link InputContent} for content blocks inside message items + * + * @category schemas + * @since 4.0.0 + */ +export const InputItem = Schema.Union([ + RequestMessageItem, + OutputMessage, + FunctionCall, + FunctionCallOutput, + ReasoningItem, + ItemReference, + LocalShellCall, + LocalShellCallOutput, + ShellCall, + ShellCallOutput, + ApplyPatchCallOutput, + McpApprovalResponse +]) + +/** + * Item shape accepted by an OpenAI Responses request `input` field. + * + * **When to use** + * + * Use when typing structured `CreateResponse.input` array items. + * + * **Details** + * + * Accepted item families include request/output messages, function call and + * function call output, reasoning items, item references, shell and local shell + * calls and outputs, apply-patch output, and MCP approval responses. + * + * @category models + * @since 4.0.0 + */ +export type InputItem = typeof InputItem.Type + +const FunctionTool = Schema.Struct({ + type: Schema.Literal("function"), + name: Schema.String, + description: Schema.optionalKey(Schema.NullOr(Schema.String)), + parameters: Schema.optionalKey(Schema.NullOr(JsonObject)), + strict: Schema.optionalKey(Schema.NullOr(Schema.Boolean)) +}) + +const CustomTool = Schema.Struct({ + type: Schema.Literal("custom"), + name: Schema.String, + description: Schema.optionalKey(Schema.String), + format: Schema.optionalKey(Schema.Unknown) +}) + +const ProviderDefinedTool = Schema.StructWithRest( + Schema.Struct({ + type: Schema.Literals([ + "apply_patch", + "code_interpreter", + "file_search", + "image_generation", + "local_shell", + "mcp", + "shell", + "web_search", + "web_search_preview" + ]) + }), + [UnknownRecord] +) + +/** + * Schema for tool definitions that can be supplied to an OpenAI Responses request. + * + * **When to use** + * + * Use when validating or encoding the `tools` array for a Responses request, + * including provider-defined tool records with provider-specific fields. + * + * **Details** + * + * Accepted variants are function tools, custom tools, and provider-defined + * OpenAI tools. Provider-defined `type` literals include `apply_patch`, + * `code_interpreter`, `file_search`, `image_generation`, `local_shell`, `mcp`, + * `shell`, `web_search`, and `web_search_preview`. + * + * **Gotchas** + * + * Provider-defined tools use `Schema.StructWithRest`, so this schema checks the + * provider tool `type` and permits additional provider fields rather than fully + * validating every provider-specific tool payload. + * + * @see {@link ToolChoice} for selecting whether and which tools the model may call + * @see {@link CreateResponse} for the request schema that consumes tools + * + * @category schemas + * @since 4.0.0 + */ +export const Tool = Schema.Union([ + FunctionTool, + CustomTool, + ProviderDefinedTool +]) + +/** + * Tool definition that can be supplied to an OpenAI Responses request. + * + * @category models + * @since 4.0.0 + */ +export type Tool = typeof Tool.Type + +/** + * Schema for selecting whether and which tools the model may call in a Responses request. + * + * **When to use** + * + * Use when validating or encoding the `tool_choice` field that constrains model + * tool use separately from the tool definitions themselves. + * + * **Details** + * + * Accepted forms are `"none"`, `"auto"`, `"required"`, an allowed-tools set, + * a named function or custom tool, or a provider-defined tool choice. + * + * @see {@link Tool} for tool definitions referenced by tool choices + * @see {@link CreateResponse} for the request schema that consumes `tool_choice` + * + * @category schemas + * @since 4.0.0 + */ +export const ToolChoice = Schema.Union([ + Schema.Literals(["none", "auto", "required"]), + Schema.Struct({ + type: Schema.Literal("allowed_tools"), + mode: Schema.Literals(["auto", "required"]), + tools: Schema.Array(JsonObject) + }), + Schema.Struct({ + type: Schema.Literal("function"), + name: Schema.String + }), + Schema.Struct({ + type: Schema.Literal("custom"), + name: Schema.String + }), + Schema.StructWithRest( + Schema.Struct({ + type: Schema.Literals([ + "apply_patch", + "code_interpreter", + "file_search", + "image_generation", + "local_shell", + "mcp", + "shell", + "web_search", + "web_search_preview" + ]) + }), + [UnknownRecord] + ) +]) + +/** + * Tool selection mode or named tool choice for a Responses request. + * + * **Details** + * + * Accepted forms are `"none"`, `"auto"`, `"required"`, an allowed-tools set, + * a named function or custom tool, or a provider-defined tool choice. + * + * @category models + * @since 4.0.0 + */ +export type ToolChoice = typeof ToolChoice.Type + +/** + * Schema for text output format configuration, including plain text, JSON object, and JSON Schema responses. + * + * **When to use** + * + * Use when validating or encoding the `text.format` setting for a Responses + * request, especially when choosing structured JSON Schema output. + * + * **Details** + * + * Accepted variants are `text`, `json_schema`, and `json_object`. + * + * **Gotchas** + * + * `json_object` is the older JSON mode. Prefer `json_schema` for models that + * support it. + * + * @see {@link CreateResponse} for the request schema that consumes text format configuration + * + * @category schemas + * @since 4.0.0 + */ +export const TextResponseFormatConfiguration = Schema.Union([ + Schema.Struct({ type: Schema.Literal("text") }), + Schema.Struct({ + type: Schema.Literal("json_schema"), + description: Schema.optionalKey(Schema.String), + name: Schema.String, + schema: JsonObject, + strict: Schema.optionalKey(Schema.NullOr(Schema.Boolean)) + }), + Schema.Struct({ type: Schema.Literal("json_object") }) +]) + +/** + * Text output format configuration for plain text, JSON object, or JSON Schema responses. + * + * @category models + * @since 4.0.0 + */ +export type TextResponseFormatConfiguration = typeof TextResponseFormatConfiguration.Type + +/** + * Schema for request options used to create an OpenAI Responses API response. + * + * **When to use** + * + * Use to validate or encode payloads sent to the OpenAI Responses API. + * + * **Details** + * + * Validates the Responses API request payload, including input content, model + * selection, instructions, reasoning options, text output format, tools, + * `tool_choice`, streaming, storage, response continuation, sampling options, + * and optional response fields requested through `include`. + * + * **Gotchas** + * + * When `stream` is `true`, the API returns stream events instead of a single + * response object. + * + * @see {@link Response} for decoded non-streaming response objects + * @see {@link ResponseStreamEvent} for decoded streaming event objects + * + * @category schemas + * @since 4.0.0 + */ +export const CreateResponse = Schema.Struct({ + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + top_logprobs: Schema.optional(Schema.Number), + temperature: Schema.optional(Schema.Number), + top_p: Schema.optional(Schema.Number), + user: Schema.optional(Schema.String), + service_tier: Schema.optional(Schema.String), + previous_response_id: Schema.optional(Schema.String), + model: Schema.optional(Schema.String), + reasoning: Schema.optional(Schema.Struct({ + effort: Schema.optional(Schema.Literals(["none", "minimal", "low", "medium", "high", "xhigh"])), + + summary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])), + generate_summary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])) + })), + background: Schema.optional(Schema.Boolean), + max_output_tokens: Schema.optional(Schema.Number), + max_tool_calls: Schema.optional(Schema.Number), + text: Schema.optional( + Schema.Struct({ + format: Schema.optional(TextResponseFormatConfiguration), + verbosity: Schema.optional(Schema.Literals(["low", "medium", "high"])) + }) + ), + tools: Schema.optional(Schema.Array(Tool)), + tool_choice: Schema.optional(ToolChoice), + truncation: Schema.optional(Schema.Literals(["auto", "disabled"])), + input: Schema.optional( + Schema.Union([ + Schema.String, + Schema.Array(InputItem) + ]) + ), + include: Schema.optional(Schema.Array(IncludeEnum)), + store: Schema.optional(Schema.Boolean), + instructions: Schema.optional(Schema.String), + stream: Schema.optional(Schema.Boolean), + conversation: Schema.optional(Schema.String), + modalities: Schema.optional(Schema.Array(Schema.Literals(["text", "audio"]))), + seed: Schema.optional(Schema.Number) +}) + +/** + * Request options used to create an OpenAI Responses API response. + * + * @category options + * @since 4.0.0 + */ +export type CreateResponse = typeof CreateResponse.Type + +/** + * Schema for token accounting reported on OpenAI Responses API response objects. + * + * **Details** + * + * The required counters are `input_tokens`, `output_tokens`, and + * `total_tokens`. Provider-specific token detail objects are preserved through + * `input_tokens_details`, `output_tokens_details`, and additional fields. + * + * @category schemas + * @since 4.0.0 + */ +export const ResponseUsage = Schema.StructWithRest( + Schema.Struct({ + input_tokens: Schema.Number, + output_tokens: Schema.Number, + total_tokens: Schema.Number, + input_tokens_details: Schema.optionalKey(Schema.Unknown), + output_tokens_details: Schema.optionalKey(Schema.Unknown) + }), + [UnknownRecord] +) + +/** + * Token accounting reported on OpenAI Responses API response objects. + * + * **Details** + * + * Includes total input, output, and combined token counts, with provider-specific + * token detail fields preserved when present. + * + * @category models + * @since 4.0.0 + */ +export type ResponseUsage = typeof ResponseUsage.Type + +const ApplyPatchOperation = Schema.Struct({ + type: Schema.String, + path: Schema.String, + diff: Schema.optionalKey(Schema.String) +}) + +const ApplyPatchCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("apply_patch_call"), + call_id: Schema.String, + operation: ApplyPatchOperation, + status: Schema.optionalKey(MessageStatus) +}) + +const CodeInterpreterCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("code_interpreter_call"), + code: Schema.optionalKey(Schema.String), + container_id: Schema.String, + outputs: Schema.optionalKey(Schema.Array(Schema.Unknown)), + status: Schema.optionalKey(MessageStatus) +}) + +const ComputerCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("computer_call"), + status: Schema.optionalKey(MessageStatus) +}) + +const FileSearchCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("file_search_call"), + status: Schema.optionalKey(Schema.String), + queries: Schema.optionalKey(Schema.Array(Schema.String)), + results: Schema.optionalKey(Schema.NullOr(Schema.Unknown)) +}) + +const ImageGenerationCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("image_generation_call"), + result: Schema.optionalKey(Schema.String), + status: Schema.optionalKey(MessageStatus) +}) + +const McpCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("mcp_call"), + approval_request_id: Schema.optionalKey(Schema.NullOr(Schema.String)), + name: Schema.String, + arguments: Schema.Unknown, + output: Schema.optionalKey(Schema.Unknown), + error: Schema.optionalKey(Schema.Unknown), + server_label: Schema.optionalKey(Schema.NullOr(Schema.String)) +}) + +const McpListTools = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("mcp_list_tools") +}) + +const McpApprovalRequest = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("mcp_approval_request"), + approval_request_id: Schema.optionalKey(Schema.String), + name: Schema.String, + arguments: Schema.Unknown +}) + +const WebSearchCall = Schema.Struct({ + id: Schema.String, + type: Schema.Literal("web_search_call"), + action: Schema.optionalKey(Schema.Unknown), + status: Schema.optionalKey(Schema.String) +}) + +const OutputItem = Schema.Union([ + ApplyPatchCall, + CodeInterpreterCall, + ComputerCall, + FileSearchCall, + FunctionCall, + ImageGenerationCall, + LocalShellCall, + McpCall, + McpListTools, + McpApprovalRequest, + OutputMessage, + ReasoningItem, + ShellCall, + WebSearchCall +]) + +/** + * Schema for an OpenAI Responses API response object. + * + * **When to use** + * + * Use to decode non-streaming OpenAI Responses API responses. + * + * **Details** + * + * Response objects include the response id, model, creation time, output items, + * optional token usage, optional incomplete details, and optional service tier. + * + * @see {@link CreateResponse} for the request schema that creates responses + * @see {@link ResponseUsage} for token accounting on responses + * @see {@link ResponseStreamEvent} for streaming response events + * + * @category schemas + * @since 4.0.0 + */ +export const Response = Schema.Struct({ + id: Schema.String, + object: Schema.optionalKey(Schema.Literal("response")), + model: Schema.String, + created_at: Schema.Number, + output: Schema.Array(OutputItem).pipe( + Schema.withDecodingDefault(Effect.succeed([])) + ), + usage: Schema.optionalKey(Schema.NullOr(ResponseUsage)), + incomplete_details: Schema.optionalKey( + Schema.NullOr( + Schema.Struct({ + reason: Schema.optionalKey(Schema.Literals(["max_output_tokens", "content_filter"])) + }) + ) + ), + service_tier: Schema.optionalKey(Schema.String) +}) + +/** + * OpenAI Responses API response object. + * + * **When to use** + * + * Use when typing non-streaming OpenAI Responses API responses. + * + * **Details** + * + * Response objects include metadata, output items, optional token usage, and + * optional incomplete details. + * + * @category models + * @since 4.0.0 + */ +export type Response = typeof Response.Type + +const ResponseCreatedEvent = Schema.Struct({ + type: Schema.Literal("response.created"), + response: Response, + sequence_number: Schema.Number +}) + +const ResponseCompletedEvent = Schema.Struct({ + type: Schema.Literal("response.completed"), + response: Response, + sequence_number: Schema.Number +}) + +const ResponseIncompleteEvent = Schema.Struct({ + type: Schema.Literal("response.incomplete"), + response: Response, + sequence_number: Schema.Number +}) + +const ResponseFailedEvent = Schema.Struct({ + type: Schema.Literal("response.failed"), + response: Response, + sequence_number: Schema.Number +}) + +const ResponseOutputItemAddedEvent = Schema.Struct({ + type: Schema.Literal("response.output_item.added"), + output_index: Schema.Number, + sequence_number: Schema.Number, + item: OutputItem +}) + +const ResponseOutputItemDoneEvent = Schema.Struct({ + type: Schema.Literal("response.output_item.done"), + output_index: Schema.Number, + sequence_number: Schema.Number, + item: OutputItem +}) + +const ResponseOutputTextDeltaEvent = Schema.Struct({ + type: Schema.Literal("response.output_text.delta"), + item_id: Schema.String, + output_index: Schema.Number, + content_index: Schema.Number, + delta: Schema.String, + sequence_number: Schema.Number, + logprobs: Schema.optionalKey(Schema.Array(Schema.Unknown)) +}) + +const ResponseOutputTextAnnotationAddedEvent = Schema.Struct({ + type: Schema.Literal("response.output_text.annotation.added"), + item_id: Schema.String, + output_index: Schema.Number, + content_index: Schema.Number, + annotation_index: Schema.Number, + sequence_number: Schema.Number, + annotation: Annotation +}) + +const ResponseReasoningSummaryPartAddedEvent = Schema.Struct({ + type: Schema.Literal("response.reasoning_summary_part.added"), + item_id: Schema.String, + output_index: Schema.Number, + summary_index: Schema.Number, + sequence_number: Schema.Number, + part: SummaryTextContent +}) + +const ResponseReasoningSummaryPartDoneEvent = Schema.Struct({ + type: Schema.Literal("response.reasoning_summary_part.done"), + item_id: Schema.String, + output_index: Schema.Number, + summary_index: Schema.Number, + sequence_number: Schema.Number, + part: SummaryTextContent +}) + +const ResponseReasoningSummaryTextDeltaEvent = Schema.Struct({ + type: Schema.Literal("response.reasoning_summary_text.delta"), + item_id: Schema.String, + output_index: Schema.Number, + summary_index: Schema.Number, + delta: Schema.String, + sequence_number: Schema.Number +}) + +const ResponseFunctionCallArgumentsDeltaEvent = Schema.Struct({ + type: Schema.Literal("response.function_call_arguments.delta"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + delta: Schema.String +}) + +const ResponseFunctionCallArgumentsDoneEvent = Schema.Struct({ + type: Schema.Literal("response.function_call_arguments.done"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + arguments: Schema.String +}) + +const ResponseCodeInterpreterCallCodeDeltaEvent = Schema.Struct({ + type: Schema.Literal("response.code_interpreter_call_code.delta"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + delta: Schema.String +}) + +const ResponseCodeInterpreterCallCodeDoneEvent = Schema.Struct({ + type: Schema.Literal("response.code_interpreter_call_code.done"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + code: Schema.String +}) + +const ResponseApplyPatchCallOperationDiffDeltaEvent = Schema.Struct({ + type: Schema.Literal("response.apply_patch_call_operation_diff.delta"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + delta: Schema.String +}) + +const ResponseApplyPatchCallOperationDiffDoneEvent = Schema.Struct({ + type: Schema.Literal("response.apply_patch_call_operation_diff.done"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + delta: Schema.optionalKey(Schema.String) +}) + +const ResponseImageGenerationCallPartialImageEvent = Schema.Struct({ + type: Schema.Literal("response.image_generation_call.partial_image"), + item_id: Schema.String, + output_index: Schema.Number, + sequence_number: Schema.Number, + partial_image_b64: Schema.String +}) + +const ResponseErrorEvent = Schema.Struct({ + type: Schema.Literal("error"), + code: Schema.NullOr(Schema.String), + message: Schema.String, + param: Schema.NullOr(Schema.String), + sequence_number: Schema.Number, + status: Schema.optionalKey(Schema.Number) +}) + +const knownResponseStreamEventTypes = new Set([ + "response.created", + "response.completed", + "response.incomplete", + "response.failed", + "response.output_item.added", + "response.output_item.done", + "response.output_text.delta", + "response.output_text.annotation.added", + "response.reasoning_summary_part.added", + "response.reasoning_summary_part.done", + "response.reasoning_summary_text.delta", + "response.function_call_arguments.delta", + "response.function_call_arguments.done", + "response.code_interpreter_call_code.delta", + "response.code_interpreter_call_code.done", + "response.apply_patch_call_operation_diff.delta", + "response.apply_patch_call_operation_diff.done", + "response.image_generation_call.partial_image", + "error" +]) + +/** + * Fallback event shape for future or provider-specific response stream events. + * + * @category models + * @since 4.0.0 + */ +export type UnknownResponseStreamEvent = { + readonly type: string + readonly [key: string]: unknown +} + +const UnknownResponseStreamEvent = Schema.declare( + (value): value is UnknownResponseStreamEvent => + Predicate.hasProperty(value, "type") && + typeof value.type === "string" && + !knownResponseStreamEventTypes.has(value.type), + { + identifier: "UnknownResponseStreamEvent", + description: "Fallback for unknown future stream events" + } +) + +/** + * Schema for server-sent event shapes emitted by OpenAI Responses API streams. + * + * **When to use** + * + * Use to decode events from a streaming OpenAI Responses API request. + * + * **Details** + * + * Known event variants include response lifecycle events, output item events, + * text and reasoning deltas, tool-call deltas, partial image events, and error + * events. + * + * **Gotchas** + * + * Future event types decode through the fallback only when their `type` is not + * one of the known event types. Malformed known events still fail to decode. + * + * @see {@link Response} for complete response objects carried by lifecycle events + * @see {@link UnknownResponseStreamEvent} for the fallback shape for future event types + * + * @category schemas + * @since 4.0.0 + */ +export const ResponseStreamEvent = Schema.Union([ + ResponseCreatedEvent, + ResponseCompletedEvent, + ResponseIncompleteEvent, + ResponseFailedEvent, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseOutputTextDeltaEvent, + ResponseOutputTextAnnotationAddedEvent, + ResponseReasoningSummaryPartAddedEvent, + ResponseReasoningSummaryPartDoneEvent, + ResponseReasoningSummaryTextDeltaEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, + ResponseCodeInterpreterCallCodeDeltaEvent, + ResponseCodeInterpreterCallCodeDoneEvent, + ResponseApplyPatchCallOperationDiffDeltaEvent, + ResponseApplyPatchCallOperationDiffDoneEvent, + ResponseImageGenerationCallPartialImageEvent, + ResponseErrorEvent, + UnknownResponseStreamEvent +]) + +/** + * Server-sent event shape emitted by OpenAI Responses API streams. + * + * **When to use** + * + * Use when typing events from a streaming OpenAI Responses API request. + * + * **Details** + * + * Includes known response stream events plus a fallback shape for unknown future + * event types. + * + * @category models + * @since 4.0.0 + */ +export type ResponseStreamEvent = typeof ResponseStreamEvent.Type + +/** + * Schema for one embedding item returned by the OpenAI embeddings API. + * + * **When to use** + * + * Use when validating individual embedding entries at the OpenAI client boundary + * before assuming the embedding payload is a numeric vector. + * + * **Details** + * + * An embedding item contains its `index`, optional `object` marker, and an + * `embedding` represented either as a numeric vector or as a string. + * + * **Gotchas** + * + * Callers that need numeric vectors must account for string embeddings, such as + * base64-encoded embeddings returned for string encoding formats. + * + * @category schemas + * @since 4.0.0 + */ +export const Embedding = Schema.Struct({ + embedding: Schema.Union([ + Schema.Array(Schema.Number), + Schema.String + ]), + index: Schema.Number, + object: Schema.optionalKey(Schema.String) +}) + +/** + * One embedding item returned by the OpenAI embeddings API. + * + * **Details** + * + * Contains the item index and embedding payload. The embedding payload may be a + * numeric vector or a string. + * + * @category models + * @since 4.0.0 + */ +export type Embedding = typeof Embedding.Type + +/** + * Schema for the request payload sent to the OpenAI embeddings endpoint. + * + * **When to use** + * + * Use when validating or encoding embeddings requests before sending them to + * OpenAI, while leaving model-specific limits to the provider. + * + * **Details** + * + * Requires `input` and `model`. `input` may be a string, an array of strings, + * a token array, or an array of token arrays. Optional fields configure the + * embedding encoding format, requested dimensions, and user identifier. + * + * **Gotchas** + * + * This schema validates the transport shape, but OpenAI still enforces + * provider-side constraints such as non-empty input, integer token ids, input + * size limits, positive dimensions, and model-specific dimension support. + * + * @category schemas + * @since 4.0.0 + */ +export const CreateEmbeddingRequest = Schema.Struct({ + input: Schema.Union([ + Schema.String, + Schema.Array(Schema.String), + Schema.Array(Schema.Number), + Schema.Array(Schema.Array(Schema.Number)) + ]), + model: Schema.String, + encoding_format: Schema.optionalKey(Schema.Literals(["float", "base64"])), + dimensions: Schema.optionalKey(Schema.Number), + user: Schema.optionalKey(Schema.String) +}) + +/** + * Request payload sent to the OpenAI embeddings endpoint. + * + * @category models + * @since 4.0.0 + */ +export type CreateEmbeddingRequest = typeof CreateEmbeddingRequest.Type + +/** + * Schema for a successful response payload returned by the OpenAI embeddings endpoint. + * + * **When to use** + * + * Use when you need to validate embeddings responses at an OpenAI client + * boundary before trusting item shapes, especially when numeric and string + * embeddings are both allowed. + * + * **Details** + * + * The response contains an array of `Embedding` items, the model name, an + * optional `object: "list"` marker, and optional token usage counts for prompt + * and total tokens. + * + * **Gotchas** + * + * Each `Embedding` may contain either a numeric vector or a string embedding. + * Callers that require numeric vectors must account for string embeddings. + * + * @see {@link CreateEmbeddingRequest} for the request schema sent to the embeddings endpoint + * @see {@link Embedding} for individual embedding items in the response + * + * @category schemas + * @since 4.0.0 + */ +export const CreateEmbeddingResponse = Schema.Struct({ + data: Schema.Array(Embedding), + model: Schema.String, + object: Schema.optionalKey(Schema.Literal("list")), + usage: Schema.optionalKey( + Schema.Struct({ + prompt_tokens: Schema.Number, + total_tokens: Schema.Number + }) + ) +}) + +/** + * Successful response payload returned by the OpenAI embeddings endpoint. + * + * **When to use** + * + * Use when typing successful OpenAI embeddings responses. + * + * **Details** + * + * Contains embedding items, the model name, optional list marker, and optional + * token usage counts. + * + * @category models + * @since 4.0.0 + */ +export type CreateEmbeddingResponse = typeof CreateEmbeddingResponse.Type diff --git a/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts b/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts new file mode 100644 index 0000000..84b7533 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts @@ -0,0 +1,162 @@ +/** + * The `OpenAiTelemetry` module defines OpenAI-specific telemetry attributes + * and a helper for adding them to a tracing span. It keeps the standard GenAI + * telemetry attributes and adds request and response metadata, such as response + * format, service tier, and system fingerprint, under the `gen_ai.openai.*` + * OpenTelemetry namespaces. + * + * @since 4.0.0 + */ +import { dual } from "effect/Function" +import * as String from "effect/String" +import type { Span } from "effect/Tracer" +import type { Simplify } from "effect/Types" +import * as Telemetry from "effect/unstable/ai/Telemetry" + +/** + * The attributes used to describe telemetry in the context of Generative + * Artificial Intelligence (GenAI) Models requests and responses. + * + * **Details** + * + * These attributes follow the OpenTelemetry generative AI semantic + * conventions: + * https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/ + * + * @category models + * @since 4.0.0 + */ +export type OpenAiTelemetryAttributes = Simplify< + & Telemetry.GenAITelemetryAttributes + & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix +> + +/** + * All telemetry attributes which are part of the GenAI specification, + * including the OpenAI-specific attributes. + * + * @category models + * @since 4.0.0 + */ +export type AllAttributes = Telemetry.AllAttributes & RequestAttributes & ResponseAttributes + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.request`. + * + * @category models + * @since 4.0.0 + */ +export interface RequestAttributes { + /** + * The response format that is requested. + */ + readonly responseFormat?: (string & {}) | WellKnownResponseFormat | null | undefined + /** + * The service tier requested. May be a specific tier, `default`, or `auto`. + */ + readonly serviceTier?: (string & {}) | WellKnownServiceTier | null | undefined +} + +/** + * Telemetry attributes which are part of the GenAI specification and are + * namespaced by `gen_ai.openai.response`. + * + * @category models + * @since 4.0.0 + */ +export interface ResponseAttributes { + /** + * The service tier used for the response. + */ + readonly serviceTier?: string | null | undefined + /** + * A fingerprint to track any eventual change in the Generative AI + * environment. + */ + readonly systemFingerprint?: string | null | undefined +} + +/** + * The `gen_ai.openai.request.response_format` attribute has the following + * list of well-known values. + * + * **Details** + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @category models + * @since 4.0.0 + */ +export type WellKnownResponseFormat = "json_object" | "json_schema" | "text" + +/** + * The `gen_ai.openai.request.service_tier` attribute has the following + * list of well-known values. + * + * **Details** + * + * If one of them applies, then the respective value **MUST** be used; + * otherwise, a custom value **MAY** be used. + * + * @category models + * @since 4.0.0 + */ +export type WellKnownServiceTier = "auto" | "default" + +/** + * Options accepted by `addGenAIAnnotations`, combining standard GenAI + * telemetry attributes with optional OpenAI request and response attributes. + * + * @category options + * @since 4.0.0 + */ +export type OpenAiTelemetryAttributeOptions = Telemetry.GenAITelemetryAttributeOptions & { + openai?: { + request?: RequestAttributes | undefined + response?: ResponseAttributes | undefined + } | undefined +} + +const addOpenAiRequestAttributes = Telemetry.addSpanAttributes("gen_ai.openai.request", String.camelToSnake)< + RequestAttributes +> +const addOpenAiResponseAttributes = Telemetry.addSpanAttributes("gen_ai.openai.response", String.camelToSnake)< + ResponseAttributes +> + +/** + * Applies the specified OpenAI GenAI telemetry attributes to the provided + * `Span`. + * + * **When to use** + * + * Use to annotate an existing OpenTelemetry span with standard GenAI attributes + * plus OpenAI-specific request and response metadata. + * + * **Gotchas** + * + * Mutates the supplied `Span` in place. + * + * @see {@link OpenAiTelemetryAttributeOptions} for the accepted telemetry attributes + * @see {@link Telemetry.addGenAIAnnotations} for the provider-neutral annotation helper + * + * @category tracing + * @since 4.0.0 + */ +export const addGenAIAnnotations: { + (options: OpenAiTelemetryAttributeOptions): (span: Span) => void + (span: Span, options: OpenAiTelemetryAttributeOptions): void +} = dual(2, (span: Span, options: OpenAiTelemetryAttributeOptions) => { + Telemetry.addGenAIAnnotations(span, options) + if (options.openai != null) { + if (options.openai.request != null) { + addOpenAiRequestAttributes(span, options.openai.request) + } + if (options.openai.response != null) { + addOpenAiResponseAttributes(span, options.openai.response) + } + } +}) diff --git a/repos/effect/packages/ai/openai/src/OpenAiTool.ts b/repos/effect/packages/ai/openai/src/OpenAiTool.ts new file mode 100644 index 0000000..7d80c57 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/OpenAiTool.ts @@ -0,0 +1,342 @@ +/** + * The `OpenAiTool` module defines OpenAI provider tools for Effect AI language + * model requests. It exposes typed descriptors for tools such as Apply Patch, + * Code Interpreter, File Search, Image Generation, MCP, Web Search, and + * shell-like local tools, including their provider names, configuration + * arguments, call parameters, success schemas, and handler requirements. + * + * @since 4.0.0 + */ +import * as Schema from "effect/Schema" +import * as Tool from "effect/unstable/ai/Tool" +import * as Generated from "./Generated.ts" + +/** + * Union of all OpenAI provider-defined tools. + * + * @category models + * @since 4.0.0 + */ +export type OpenAiTool = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + +/** + * Defines the OpenAI Apply Patch tool that allows the model to apply diffs by creating, + * deleting, or updating files. This local tool runs in your environment and + * requires a handler to execute file operations. + * + * **When to use** + * + * Use when you want an OpenAI model to request structured file edits as create, + * delete, or update operations that your application executes through a local + * handler. + * + * @category tools + * @since 4.0.0 + */ +export const ApplyPatch = Tool.providerDefined({ + id: "openai.apply_patch", + customName: "OpenAiApplyPatch", + providerName: "apply_patch", + requiresHandler: true, + parameters: Schema.Struct({ + call_id: Generated.ApplyPatchToolCall.fields.call_id, + operation: Generated.ApplyPatchToolCall.fields.operation + }), + success: Schema.Struct({ + status: Generated.ApplyPatchToolCallOutput.fields.status, + output: Generated.ApplyPatchToolCallOutput.fields.output + }) +}) + +/** + * Defines the OpenAI Code Interpreter tool that allows the model to execute Python code in + * a sandboxed environment. + * + * **When to use** + * + * Use to enable OpenAI-hosted Python execution for a model response. + * + * **Details** + * + * The tool is configured with a `container` argument. Successful tool calls + * expose `outputs`, which may contain logs or generated images, or `null` when + * no outputs are available. + * + * @category tools + * @since 4.0.0 + */ +export const CodeInterpreter = Tool.providerDefined({ + id: "openai.code_interpreter", + customName: "OpenAiCodeInterpreter", + providerName: "code_interpreter", + args: Schema.Struct({ + container: Generated.CodeInterpreterTool.fields.container + }), + parameters: Schema.Struct({ + code: Generated.CodeInterpreterToolCall.fields.code, + container_id: Generated.CodeInterpreterToolCall.fields.container_id + }), + success: Schema.Struct({ + outputs: Generated.CodeInterpreterToolCall.fields.outputs + }) +}) + +/** + * Defines the OpenAI File Search tool that enables the model to search through uploaded + * files and vector stores. + * + * **When to use** + * + * Use to let an OpenAI model search uploaded files through one or more vector + * stores. + * + * **Details** + * + * The tool requires `vector_store_ids` and accepts optional `filters`, + * `max_num_results`, and `ranking_options`. Successful tool calls expose the + * search `status`, generated `queries`, and optional `results`. + * + * @category tools + * @since 4.0.0 + */ +export const FileSearch = Tool.providerDefined({ + id: "openai.file_search", + customName: "OpenAiFileSearch", + providerName: "file_search", + args: Schema.Struct({ + filters: Generated.FileSearchTool.fields.filters, + max_num_results: Generated.FileSearchTool.fields.max_num_results, + ranking_options: Generated.FileSearchTool.fields.ranking_options, + vector_store_ids: Generated.FileSearchTool.fields.vector_store_ids + }), + success: Schema.Struct({ + status: Generated.FileSearchToolCall.fields.status, + queries: Generated.FileSearchToolCall.fields.queries, + results: Generated.FileSearchToolCall.fields.results + }) +}) + +/** + * Defines the OpenAI Image Generation tool that enables the model to generate images using + * the GPT image models. + * + * **When to use** + * + * Use to enable OpenAI provider-defined image generation through a language + * model response. + * + * **Details** + * + * The tool configures the `image_generation` provider tool, including model, + * size, quality, output format, moderation, background, input-image options, + * and partial image settings. Successful tool calls expose `result` as base64 + * image data or `null`. + * + * @category tools + * @since 4.0.0 + */ +export const ImageGeneration = Tool.providerDefined({ + id: "openai.image_generation", + customName: "OpenAiImageGeneration", + providerName: "image_generation", + args: Schema.Struct({ + background: Generated.ImageGenTool.fields.background, + input_fidelity: Generated.ImageGenTool.fields.input_fidelity, + input_image_mask: Generated.ImageGenTool.fields.input_image_mask, + model: Generated.ImageGenTool.fields.model, + moderation: Generated.ImageGenTool.fields.moderation, + output_compression: Generated.ImageGenTool.fields.output_compression, + output_format: Generated.ImageGenTool.fields.output_format, + partial_images: Generated.ImageGenTool.fields.partial_images, + quality: Generated.ImageGenTool.fields.quality, + size: Generated.ImageGenTool.fields.size + }), + success: Schema.Struct({ + result: Generated.ImageGenToolCall.fields.result + }) +}) + +/** + * Defines the OpenAI Local Shell tool that enables the model to run a command with a local + * shell. This local tool runs in your environment and requires a handler to + * execute commands. + * + * **When to use** + * + * Use to let an OpenAI model request local shell commands that your application + * executes through a handler. + * + * **Details** + * + * The tool exposes a provider-defined `local_shell` call. It is marked as + * handler-required, so applications must provide the command execution policy + * and implementation. + * + * @category tools + * @since 4.0.0 + */ +export const LocalShell = Tool.providerDefined({ + id: "openai.local_shell", + customName: "OpenAiLocalShell", + providerName: "local_shell", + requiresHandler: true, + parameters: Schema.Struct({ + action: Generated.LocalShellToolCall.fields.action + }), + success: Schema.Struct({ + output: Generated.LocalShellToolCallOutput.fields.output + }) +}) + +/** + * Defines the OpenAI MCP tool that gives the model access to additional tools via remote + * Model Context Protocol (MCP) servers. + * + * **When to use** + * + * Use to let an OpenAI model call tools exposed by a remote MCP server. + * + * **Details** + * + * The tool accepts MCP server configuration such as allowed tools, + * authorization, connector id, approval requirements, server metadata, and + * server URL. Tool call results include the called tool name, arguments, output, + * error, and server label. + * + * **Gotchas** + * + * This schema leaves both `server_url` and `connector_id` optional, but OpenAI + * may require a server URL or connector id for a usable MCP tool configuration. + * + * @category tools + * @since 4.0.0 + */ +export const Mcp = Tool.providerDefined({ + id: "openai.mcp", + customName: "OpenAiMcp", + providerName: "mcp", + args: Schema.Struct({ + allowed_tools: Generated.MCPTool.fields.allowed_tools, + authorization: Generated.MCPTool.fields.authorization, + connector_id: Generated.MCPTool.fields.connector_id, + require_approval: Generated.MCPTool.fields.require_approval, + server_description: Generated.MCPTool.fields.server_description, + server_label: Generated.MCPTool.fields.server_label, + server_url: Generated.MCPTool.fields.server_url + }), + parameters: Schema.Unknown, + success: Schema.Struct({ + type: Generated.MCPToolCall.fields.type, + name: Generated.MCPToolCall.fields.name, + arguments: Generated.MCPToolCall.fields.arguments, + output: Generated.MCPToolCall.fields.output, + error: Generated.MCPToolCall.fields.error, + server_label: Generated.MCPToolCall.fields.server_label + }) +}) + +/** + * Defines the OpenAI shell tool for model-requested command execution. + * + * **When to use** + * + * Use to let an OpenAI model request shell commands that your application + * executes through a handler. + * + * **Details** + * + * The tool exposes a provider-defined `shell` call. It is marked as + * handler-required, so applications must provide the command execution policy + * and implementation. + * + * @category tools + * @since 4.0.0 + */ +export const Shell = Tool.providerDefined({ + id: "openai.shell", + customName: "OpenAiShell", + providerName: "shell", + requiresHandler: true, + parameters: Schema.Struct({ + action: Generated.FunctionShellCall.fields.action + }), + success: Schema.Struct({ + output: Generated.FunctionShellCallOutput.fields.output + }) +}) + +/** + * Defines the OpenAI Web Search tool that enables the model to search the web for + * information. + * + * **When to use** + * + * Use to enable OpenAI provider-defined web search for a model response. + * + * **Details** + * + * The tool accepts optional filters, user location, and search context size. + * Successful calls expose the performed search action and status. + * + * @see {@link WebSearchPreview} for the preview web search provider tool + * + * @category tools + * @since 4.0.0 + */ +export const WebSearch = Tool.providerDefined({ + id: "openai.web_search", + customName: "OpenAiWebSearch", + providerName: "web_search", + args: Schema.Struct({ + filters: Generated.WebSearchTool.fields.filters, + user_location: Generated.WebSearchTool.fields.user_location, + search_context_size: Generated.WebSearchTool.fields.search_context_size + }), + parameters: Schema.Struct({ + action: Generated.WebSearchToolCall.fields.action + }), + success: Schema.Struct({ + action: Generated.WebSearchToolCall.fields.action, + status: Generated.WebSearchToolCall.fields.status + }) +}) + +/** + * Defines the OpenAI preview Web Search tool for model responses. + * + * **When to use** + * + * Use to enable the preview OpenAI web search provider tool. + * + * **Details** + * + * The preview tool accepts optional user location and search context size, then + * exposes the performed search action and status in successful calls. + * + * @see {@link WebSearch} for the stable web search provider tool + * + * @category tools + * @since 4.0.0 + */ +export const WebSearchPreview = Tool.providerDefined({ + id: "openai.web_search_preview", + customName: "OpenAiWebSearchPreview", + providerName: "web_search_preview", + args: Schema.Struct({ + user_location: Generated.WebSearchPreviewTool.fields.user_location, + search_context_size: Generated.WebSearchPreviewTool.fields.search_context_size + }), + success: Schema.Struct({ + action: Generated.WebSearchToolCall.fields.action, + status: Generated.WebSearchToolCall.fields.status + }) +}) diff --git a/repos/effect/packages/ai/openai/src/index.ts b/repos/effect/packages/ai/openai/src/index.ts new file mode 100644 index 0000000..b1758b6 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/index.ts @@ -0,0 +1,55 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 1.0.0 + */ +export * as Generated from "./Generated.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiClient from "./OpenAiClient.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiClientGenerated from "./OpenAiClientGenerated.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiConfig from "./OpenAiConfig.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiError from "./OpenAiError.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiLanguageModel from "./OpenAiLanguageModel.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiSchema from "./OpenAiSchema.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiTelemetry from "./OpenAiTelemetry.ts" + +/** + * @since 4.0.0 + */ +export * as OpenAiTool from "./OpenAiTool.ts" diff --git a/repos/effect/packages/ai/openai/src/internal/errors.ts b/repos/effect/packages/ai/openai/src/internal/errors.ts new file mode 100644 index 0000000..a1be0c9 --- /dev/null +++ b/repos/effect/packages/ai/openai/src/internal/errors.ts @@ -0,0 +1,349 @@ +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import type * as Response from "effect/unstable/ai/Response" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type { OpenAiErrorMetadata } from "../OpenAiError.ts" + +// ============================================================================= +// OpenAI Error Body Schema +// ============================================================================= + +/** @internal */ +export const OpenAiErrorBody = Schema.Struct({ + error: Schema.Struct({ + message: Schema.String, + type: Schema.optional(Schema.NullOr(Schema.String)), + param: Schema.optional(Schema.NullOr(Schema.String)), + code: Schema.optional(Schema.NullOr(Schema.String)) + }) +}) + +// ============================================================================= +// Error Mappers +// ============================================================================= + +/** @internal */ +export const mapSchemaError = dual< + (method: string) => (error: Schema.SchemaError) => AiError.AiError, + (error: Schema.SchemaError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenAiClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + })) + +/** @internal */ +export const mapHttpClientError = dual< + (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, + (error: HttpClientError.HttpClientError, method: string) => Effect.Effect +>(2, (error, method) => { + const reason = error.reason + switch (reason._tag) { + case "TransportError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "TransportError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "EncodeError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "EncodeError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "InvalidUrlError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.NetworkError({ + reason: "InvalidUrlError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "StatusCodeError": { + return mapStatusCodeError(reason, method) + } + case "DecodeError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Failed to decode response" + }) + })) + } + case "EmptyBodyError": { + return Effect.fail(AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Response body was empty" + }) + })) + } + } +}) + +/** @internal */ +const mapStatusCodeError = Effect.fnUntraced(function*( + error: HttpClientError.StatusCodeError, + method: string +) { + const { request, response, description } = error + const status = response.status + const headers = response.headers as Record + const requestId = headers["x-request-id"] + + // Try to get the actual response body. The description from filterStatusOk + // is often just "non 2xx status code", so try reading from response.text + let body: string | undefined = description + if (!description || !description.startsWith("{")) { + const responseBody = yield* Effect.option(response.text) + if (Option.isSome(responseBody) && responseBody.value) { + body = responseBody.value + } + } + + // Try to parse the body as JSON to extract error details + let json: unknown = undefined + // @effect-diagnostics effect/tryCatchInEffectGen:off + try { + json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined + } catch { + json = undefined + } + const decoded = Schema.decodeUnknownOption(OpenAiErrorBody)(json) + + const reason = mapStatusCodeToReason({ + status, + headers, + message: Option.isSome(decoded) ? decoded.value.error.message : undefined, + http: buildHttpContext({ request, response, body }), + metadata: { + errorCode: Option.isSome(decoded) ? decoded.value.error.code ?? null : null, + errorType: Option.isSome(decoded) ? decoded.value.error.type ?? null : null, + requestId: requestId ?? null + } + }) + + return yield* AiError.make({ module: "OpenAiClient", method, reason }) +}) + +// ============================================================================= +// Rate Limits +// ============================================================================= + +/** @internal */ +export const parseRateLimitHeaders = (headers: Record) => { + const retryAfterRaw = headers["retry-after"] + let retryAfter: Duration.Duration | undefined + if (Predicate.isNotUndefined(retryAfterRaw)) { + const parsed = Number.parse(retryAfterRaw) + if (Option.isSome(parsed)) { + retryAfter = Duration.seconds(parsed.value) + } + } + const remainingRaw = headers["x-ratelimit-remaining-requests"] + const remaining = Predicate.isNotUndefined(remainingRaw) + ? Option.getOrNull(Number.parse(remainingRaw)) + : null + return { + retryAfter, + limit: headers["x-ratelimit-limit-requests"] ?? null, + remaining, + resetRequests: headers["x-ratelimit-reset-requests"] ?? null, + resetTokens: headers["x-ratelimit-reset-tokens"] ?? null + } +} + +// ============================================================================= +// HTTP Context +// ============================================================================= + +/** @internal */ +export const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +/** @internal */ +export const buildHttpContext = (params: { + readonly request: HttpClientRequest.HttpClientRequest + readonly response?: HttpClientResponse.HttpClientResponse + readonly body?: string | undefined +}): typeof AiError.HttpContext.Type => ({ + request: buildHttpRequestDetails(params.request), + response: Predicate.isNotUndefined(params.response) + ? { + status: params.response.status, + headers: Redactable.redact(params.response.headers) as Record + } + : undefined, + body: params.body +}) + +// ============================================================================= +// HTTP Status Code +// ============================================================================= + +const buildInvalidRequestDescription = (params: { + readonly status: number + readonly message: string | undefined + readonly method: string + readonly url: string + readonly errorCode: string | null + readonly errorType: string | null + readonly requestId: string | null + readonly body: string | undefined +}): string => { + const parts: Array = [] + + // Primary message or status description + if (params.message) { + parts.push(params.message) + } else { + parts.push(`HTTP ${params.status}`) + } + + // Request context + parts.push(`(${params.method} ${params.url})`) + + // Error code/type if available + if (params.errorCode) { + parts.push(`[code: ${params.errorCode}]`) + } else if (params.errorType) { + parts.push(`[type: ${params.errorType}]`) + } + + // Request ID for debugging + if (params.requestId) { + parts.push(`[requestId: ${params.requestId}]`) + } + + // If no message and we have body, show truncated body + if (!params.message && params.body) { + const truncated = params.body.length > 200 + ? params.body.slice(0, 200) + "..." + : params.body + parts.push(`Response: ${truncated}`) + } + + return parts.join(" ") +} + +/** @internal */ +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { + readonly status: number + readonly headers: Record + readonly message: string | undefined + readonly metadata: OpenAiErrorMetadata + readonly http: typeof AiError.HttpContext.Type +}): AiError.AiErrorReason => { + const invalidRequestDescription = buildInvalidRequestDescription({ + status, + message, + method: http.request.method, + url: http.request.url, + errorCode: metadata.errorCode, + errorType: metadata.errorType, + requestId: metadata.requestId, + body: http.body + }) + + switch (status) { + case 400: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 401: + return new AiError.AuthenticationError({ + kind: "InvalidKey", + metadata, + http + }) + case 403: + return new AiError.AuthenticationError({ + kind: "InsufficientPermissions", + metadata, + http + }) + case 404: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 409: + case 422: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openai: metadata }, + http + }) + case 429: { + // Best-effort detection: OpenAI returns insufficient_quota for billing/quota issues + if ( + metadata.errorCode === "insufficient_quota" || + metadata.errorType === "insufficient_quota" + ) { + return new AiError.QuotaExhaustedError({ + metadata: { openai: metadata }, + http + }) + } + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + return new AiError.RateLimitError({ + retryAfter, + metadata: { + openai: { + ...metadata, + ...rateLimitMetadata + } + }, + http + }) + } + default: + if (status >= 500) { + return new AiError.InternalProviderError({ + description: message ?? "Server error", + metadata, + http + }) + } + return new AiError.UnknownError({ + description: message, + metadata, + http + }) + } +} diff --git a/repos/effect/packages/ai/openai/src/internal/utilities.ts b/repos/effect/packages/ai/openai/src/internal/utilities.ts new file mode 100644 index 0000000..1aca7da --- /dev/null +++ b/repos/effect/packages/ai/openai/src/internal/utilities.ts @@ -0,0 +1,27 @@ +import type * as Response from "effect/unstable/ai/Response" + +const finishReasonMap: Record = { + content_filter: "content-filter", + function_call: "tool-calls", + length: "length", + stop: "stop", + tool_calls: "tool-calls" +} + +/** @internal */ +export const escapeJSONDelta = (delta: string): string => JSON.stringify(delta).slice(1, -1) + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string | null | undefined, + hasToolCalls: boolean +): Response.FinishReason => { + if (finishReason == null) { + return hasToolCalls ? "tool-calls" : "stop" + } + const reason = finishReasonMap[finishReason] + if (reason == null) { + return hasToolCalls ? "tool-calls" : "unknown" + } + return reason +} diff --git a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts new file mode 100644 index 0000000..d9069fd --- /dev/null +++ b/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts @@ -0,0 +1,798 @@ +import type * as Generated from "@effect/ai-openai/Generated" +import * as Errors from "@effect/ai-openai/internal/errors" +import * as OpenAiClient from "@effect/ai-openai/OpenAiClient" +import * as OpenAiClientGenerated from "@effect/ai-openai/OpenAiClientGenerated" +import * as OpenAiConfig from "@effect/ai-openai/OpenAiConfig" +import { assert, describe, it } from "@effect/vitest" +import { Config, ConfigProvider, Effect, Layer, Redacted, Schema, Stream } from "effect" +import type * as AiError from "effect/unstable/ai/AiError" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" + +// ============================================================================= +// Mock Helpers +// ============================================================================= + +const makeMockResponse = (options: { + readonly status: number + readonly body: unknown + readonly request?: HttpClientRequest.HttpClientRequest +}): HttpClientResponse.HttpClientResponse => { + // Always use a plain request for the response to avoid Redacted headers in error contexts + const request = HttpClientRequest.get(options.request?.url ?? "/") + const json = JSON.stringify(options.body) + return HttpClientResponse.fromWeb( + request, + new Response(json, { + status: options.status, + headers: { "content-type": "application/json" } + }) + ) +} + +const makeMockStreamResponse = (options: { + readonly events: ReadonlyArray + readonly request?: HttpClientRequest.HttpClientRequest +}): HttpClientResponse.HttpClientResponse => { + const request = HttpClientRequest.get(options.request?.url ?? "/") + const body = options.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) +} + +const makeMockHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +): HttpClient.HttpClient => + HttpClient.makeWith( + (effect) => + Effect.flatMap(effect, handler) as Effect.Effect< + HttpClientResponse.HttpClientResponse, + HttpClientError.HttpClientError, + never + >, + Effect.succeed + ) + +const makeResponseBody = ( + overrides: Partial = {} +): typeof Generated.Response.Encoded => ({ + id: "resp_test123", + object: "response", + created_at: 1, + model: "gpt-4o-mini", + status: "completed", + output: [], + metadata: null, + temperature: null, + top_p: null, + tools: [], + tool_choice: "auto", + error: null, + incomplete_details: null, + instructions: null, + parallel_tool_calls: false, + ...overrides +}) + +// ============================================================================= +// Tests +// ============================================================================= + +describe("OpenAiClient", () => { + describe("make", () => { + it.effect("sets Bearer token from apiKey", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("sk-test-12345") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + // Call method and ignore response parsing errors - we only care about the request + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + const authHeader = capturedRequest!.headers["authorization"] + assert.strictEqual(authHeader, "Bearer sk-test-12345") + })) + + it.effect("prepends default URL", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.isTrue(capturedRequest!.url.startsWith("https://api.openai.com/v1")) + })) + + it.effect("uses custom apiUrl when provided", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + apiUrl: "https://custom.api.com/v2" + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.isTrue(capturedRequest!.url.startsWith("https://custom.api.com/v2")) + })) + + it.effect("sets OpenAI-Organization header when organizationId provided", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + organizationId: Redacted.make("org-12345") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["openai-organization"], "org-12345") + })) + + it.effect("sets OpenAI-Project header when projectId provided", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + projectId: Redacted.make("proj-67890") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["openai-project"], "proj-67890") + })) + + it.effect("applies transformClient option", () => + Effect.gen(function*() { + let transformApplied = false + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + transformApplied = true + return client + } + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + assert.isTrue(transformApplied) + })) + + it.effect("exposes transformed HttpClient via client field", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => + client.pipe(HttpClient.mapRequest(HttpClientRequest.setHeader("x-client-field", "enabled"))) + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.client.execute(HttpClientRequest.get("/responses")).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.isTrue(capturedRequest!.url.startsWith("https://api.openai.com/v1")) + assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer test-key") + assert.strictEqual(capturedRequest!.headers["x-client-field"], "enabled") + })) + + it.effect("applies OpenAiConfig transformClient after options transformClient", () => + Effect.gen(function*() { + let optionsTransformApplied = false + let configTransformApplied = false + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) + }) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + optionsTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) + ) + } + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ + model: "gpt-4o", + input: "test" + }).pipe( + OpenAiConfig.withClientTransform((client) => { + configTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "config")) + ) + }) + ) + + assert.isTrue(optionsTransformApplied) + assert.isTrue(configTransformApplied) + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["x-openai-transform"], "config") + })) + }) + + describe("OpenAiClientGenerated", () => { + it.effect("sets Bearer token from apiKey", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) + }) + + const client = yield* OpenAiClientGenerated.make({ + apiKey: Redacted.make("sk-generated-test") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ + payload: { + model: "gpt-4o", + input: "test" + } + }) + + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer sk-generated-test") + })) + + it.effect("prepends custom apiUrl", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) + }) + + const client = yield* OpenAiClientGenerated.make({ + apiKey: Redacted.make("test-key"), + apiUrl: "https://generated.example.test/v2" + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ + payload: { + model: "gpt-4o", + input: "test" + } + }) + + assert.isDefined(capturedRequest) + assert.isTrue(capturedRequest!.url.startsWith("https://generated.example.test/v2")) + })) + + it.effect("applies OpenAiConfig transformClient after options transformClient", () => + Effect.gen(function*() { + let optionsTransformApplied = false + let configTransformApplied = false + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const mockClient = makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) + }) + + const client = yield* OpenAiClientGenerated.make({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + optionsTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) + ) + } + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + yield* client.createResponse({ + payload: { + model: "gpt-4o", + input: "test" + } + }).pipe( + OpenAiConfig.withClientTransform((client) => { + configTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "config")) + ) + }) + ) + + assert.isTrue(optionsTransformApplied) + assert.isTrue(configTransformApplied) + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["x-openai-transform"], "config") + })) + }) + + describe("layer", () => { + it.effect("creates working service", () => { + const HttpClientLayer = Layer.succeed( + HttpClient.HttpClient, + makeMockHttpClient(() => Effect.succeed(makeMockResponse({ status: 200, body: {} }))) + ) + + const MainLayer = OpenAiClient.layer({ + apiKey: Redacted.make("test-key") + }).pipe(Layer.provide(HttpClientLayer)) + + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + assert.isNotNull(client.client) + }).pipe(Effect.provide(MainLayer)) + }) + + it.effect("layerConfig loads from Config", () => { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const HttpClientLayer = Layer.succeed( + HttpClient.HttpClient, + makeMockHttpClient((request) => { + capturedRequest = request + return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) + }) + ) + + const configProvider = ConfigProvider.fromEnv({ + env: { + MY_API_KEY: "sk-config-key", + MY_API_URL: "https://config.api.com/v1" + } + }) + + // Use explicit config values to test the layerConfig mechanism + // Provide explicit configs that won't fail for optional fields + const MainLayer = OpenAiClient.layerConfig({ + apiKey: Config.redacted("MY_API_KEY"), + apiUrl: Config.string("MY_API_URL") + }).pipe( + Layer.provide(HttpClientLayer), + Layer.provide(ConfigProvider.layer(configProvider)) + ) + + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + + assert.isDefined(capturedRequest) + assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer sk-config-key") + assert.isTrue(capturedRequest!.url.startsWith("https://config.api.com/v1")) + }).pipe(Effect.provide(MainLayer)) + }) + }) + + describe("error mapping", () => { + it.effect("maps TransportError to NetworkError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient(() => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + request: HttpClientRequest.get("/"), + cause: new Error("Connection refused") + }) + }) + ) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.module, "OpenAiClient") + assert.strictEqual(result.method, "createResponse") + assert.strictEqual(result.reason._tag, "NetworkError") + })) + + it.effect("maps 400 status to InvalidRequestError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 400, + body: { error: { message: "Bad request" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.module, "OpenAiClient") + assert.strictEqual(result.method, "createResponse") + assert.strictEqual(result.reason._tag, "InvalidRequestError") + })) + + it.effect("maps 401 status to AuthenticationError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 401, + body: { error: { message: "Invalid API key" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "AuthenticationError") + assert.strictEqual((result.reason as AiError.AuthenticationError).kind, "InvalidKey") + })) + + it.effect("maps 403 status to AuthenticationError with InsufficientPermissions", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 403, + body: { error: { message: "Access denied" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "AuthenticationError") + assert.strictEqual((result.reason as AiError.AuthenticationError).kind, "InsufficientPermissions") + })) + + it.effect("maps 429 status to RateLimitError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 429, + body: { error: { message: "Rate limit exceeded", type: "rate_limit_error", code: null } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "RateLimitError") + assert.isTrue(result.isRetryable) + })) + + it.effect("maps 429 with insufficient_quota code to QuotaExhaustedError", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 429, + body: { + error: { + message: "You exceeded your current quota", + type: "insufficient_quota", + code: "insufficient_quota" + } + }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "QuotaExhaustedError") + assert.isFalse(result.isRetryable) + })) + + it("mapStatusCodeToReason detects insufficient_quota as QuotaExhaustedError", () => { + const http = { + request: { + method: "POST" as const, + url: "https://api.openai.com", + urlParams: [], + hash: undefined, + headers: {} + } + } + const reason = Errors.mapStatusCodeToReason({ + status: 429, + headers: {}, + message: "You exceeded your current quota", + metadata: { + errorCode: "insufficient_quota", + errorType: "insufficient_quota", + requestId: null + }, + http + }) + assert.strictEqual(reason._tag, "QuotaExhaustedError") + }) + + it("OpenAiErrorBody decodes error with type and code", () => { + const json = { + error: { + message: "You exceeded your current quota", + type: "insufficient_quota", + code: "insufficient_quota" + } + } + const decoded = Schema.decodeUnknownSync(Errors.OpenAiErrorBody)(json) + assert.strictEqual(decoded.error.message, "You exceeded your current quota") + assert.strictEqual(decoded.error.type, "insufficient_quota") + assert.strictEqual(decoded.error.code, "insufficient_quota") + }) + + it.effect("maps 5xx status to InternalProviderError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 500, + body: { error: { message: "Internal server error" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "InternalProviderError") + assert.isTrue(result.isRetryable) + })) + + it.effect("maps schema error to InvalidOutputError reason", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 200, + body: { invalid: "response" }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.method, "createResponse") + assert.strictEqual(result.reason._tag, "InvalidOutputError") + })) + }) + + describe("createEmbedding", () => { + it.effect("maps 400 error to AiError", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 400, + body: { error: { message: "Invalid model" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createEmbedding({ + model: "invalid-model", + input: "test" + }).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.method, "createEmbedding") + assert.strictEqual(result.reason._tag, "InvalidRequestError") + })) + + it.effect("maps 429 error to RateLimitError", () => + Effect.gen(function*() { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 429, + body: { error: { message: "Rate limit exceeded" } }, + request + })) + ) + + const client = yield* OpenAiClient.make({ + apiKey: Redacted.make("test-key") + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + + const result = yield* client.createEmbedding({ + model: "text-embedding-ada-002", + input: "test" + }).pipe(Effect.flip) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "RateLimitError") + })) + }) + + describe("createResponseStream", () => { + it.effect("accepts keepalive stream events", () => { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockStreamResponse({ + request, + events: [ + { + type: "response.created", + sequence_number: 1, + response: makeResponseBody({ + id: "resp_stream", + status: "in_progress" + }) + }, + { + type: "keepalive", + sequence_number: 2 + }, + { + type: "response.completed", + sequence_number: 3, + response: makeResponseBody({ + id: "resp_stream" + }) + } + ] + })) + ) + + const HttpClientLayer = Layer.succeed(HttpClient.HttpClient, mockClient) + + const MainLayer = OpenAiClient.layer({ + apiKey: Redacted.make("test-key") + }).pipe(Layer.provide(HttpClientLayer)) + + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + + const [_, stream] = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }) + + const events = yield* Stream.runCollect(stream) + const parts = globalThis.Array.from(events) + + assert.strictEqual(parts.length, 3) + const keepAlive = parts[1] + assert.strictEqual(keepAlive.type, "keepalive") + if (keepAlive.type === "keepalive" && "sequence_number" in keepAlive) { + assert.strictEqual(keepAlive.sequence_number, 2) + } + + const completed = parts[2] + assert.isTrue(typeof completed === "object" && completed !== null && "type" in completed) + if (typeof completed === "object" && completed !== null && "type" in completed) { + assert.strictEqual(completed.type, "response.completed") + if ( + completed.type === "response.completed" && + "response" in completed && + typeof completed.response === "object" && + completed.response !== null && + "id" in completed.response + ) { + assert.strictEqual(completed.response.id, "resp_stream") + } + } + }).pipe(Effect.provide(MainLayer)) + }) + + it.effect("maps HTTP error before stream starts", () => { + const mockClient = makeMockHttpClient((request) => + Effect.succeed(makeMockResponse({ + status: 500, + body: { error: { message: "Server error" } }, + request + })) + ) + + const HttpClientLayer = Layer.succeed(HttpClient.HttpClient, mockClient) + + const MainLayer = OpenAiClient.layer({ + apiKey: Redacted.make("test-key") + }).pipe(Layer.provide(HttpClientLayer)) + + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + + const result = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }).pipe( + Effect.andThen(([_, stream]) => Stream.runDrain(stream)), + Effect.flip + ) + + assert.strictEqual(result._tag, "AiError") + assert.strictEqual(result.reason._tag, "InternalProviderError") + }).pipe(Effect.provide(MainLayer)) + }) + }) +}) diff --git a/repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts b/repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts new file mode 100644 index 0000000..5bf0889 --- /dev/null +++ b/repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts @@ -0,0 +1,289 @@ +import { OpenAiClient, OpenAiEmbeddingModel } from "@effect/ai-openai" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Redacted } from "effect" +import { EmbeddingModel } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenAiEmbeddingModel", () => { + it.effect("model provides dimensions service", () => + Effect.gen(function*() { + const dimensions = yield* EmbeddingModel.Dimensions + assert.strictEqual(dimensions, 1536) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.model("text-embedding-3-small", { dimensions: 1536 })), + Effect.provideService(OpenAiClient.OpenAiClient, noopOpenAiClient) + )) + + it.effect("reorders embeddings by provider index", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + data: [ + { index: 1, embedding: [20, 21], object: "embedding" }, + { index: 0, embedding: [10, 11], object: "embedding" } + ], + model: "text-embedding-3-small", + object: "list", + usage: { + prompt_tokens: 7, + total_tokens: 7 + } + })) + }) + )) + ) + + const response = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["first", "second"]) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.deepStrictEqual(response.embeddings.map((embedding) => embedding.vector), [[10, 11], [20, 21]]) + assert.strictEqual(response.usage.inputTokens, 7) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.model, "text-embedding-3-small") + assert.deepStrictEqual(requestBody.input, ["first", "second"]) + })) + + it.effect("merges config and applies withConfigOverride precedence", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + data: [{ index: 0, embedding: [1, 2, 3], object: "embedding" }], + model: "override-model", + object: "list", + usage: { + prompt_tokens: 3, + total_tokens: 3 + } + })) + }) + )) + ) + + yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + yield* model.embed("hello") + }).pipe( + OpenAiEmbeddingModel.withConfigOverride({ + model: "override-model", + dimensions: 1024, + user: "request-user" + }), + Effect.provide(OpenAiEmbeddingModel.layer({ + model: "base-model", + config: { + dimensions: 256, + user: "provider-user" + } + })), + Effect.provide(clientLayer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.strictEqual(requestBody.model, "override-model") + assert.strictEqual(requestBody.dimensions, 1024) + assert.strictEqual(requestBody.user, "request-user") + assert.deepStrictEqual(requestBody.input, ["hello"]) + })) + + it.effect("fails with InvalidOutputError when provider returns duplicate indices", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [ + { index: 0, embedding: [1], object: "embedding" }, + { index: 0, embedding: [2], object: "embedding" } + ], + model: "text-embedding-3-small", + object: "list", + usage: { + prompt_tokens: 2, + total_tokens: 2 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["a", "b"]).pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when provider returns out-of-range indices", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [ + { index: 0, embedding: [1], object: "embedding" }, + { index: 2, embedding: [2], object: "embedding" } + ], + model: "text-embedding-3-small", + object: "list", + usage: { + prompt_tokens: 2, + total_tokens: 2 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["a", "b"]).pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when provider returns wrong embedding count", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [{ index: 0, embedding: [1], object: "embedding" }], + model: "text-embedding-3-small", + object: "list", + usage: { + prompt_tokens: 1, + total_tokens: 1 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embedMany(["a", "b"]).pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) + + it.effect("fails with InvalidOutputError when provider returns base64 embeddings", () => + Effect.gen(function*() { + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse(request, { + data: [{ index: 0, embedding: "AQID", object: "embedding" }], + model: "text-embedding-3-small", + object: "list", + usage: { + prompt_tokens: 1, + total_tokens: 1 + } + })) + ) + )) + ) + + const error = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* model.embed("a").pipe(Effect.flip) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.strictEqual(error._tag, "AiError") + assert.strictEqual(error.reason._tag, "InvalidOutputError") + })) +}) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json" + } + }) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const noopOpenAiClient: OpenAiClient.Service = { + client: undefined as unknown as OpenAiClient.Service["client"], + createResponse: () => Effect.die(new Error("noop")), + createResponseStream: () => Effect.die(new Error("noop")), + createEmbedding: () => Effect.die(new Error("noop")) +} diff --git a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts new file mode 100644 index 0000000..d85cb86 --- /dev/null +++ b/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -0,0 +1,1528 @@ +import { Generated, OpenAiClient, OpenAiLanguageModel, OpenAiTool } from "@effect/ai-openai" +import { assert, describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Context, Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" +import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenAiLanguageModel", () => { + describe("make", () => { + it.effect("sends correct model in request", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "test" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")) + ) + + const metadata = result.content.find((part) => part.type === "response-metadata") + + strictEqual(metadata?.modelId, "gpt-4o-mini") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("sends custom model string in request", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ prompt: "test" }).pipe( + Effect.provide(OpenAiLanguageModel.model("ft:gpt-4o-mini:custom")) + ) + + const metadata = result.content.find((part) => part.type === "response-metadata") + strictEqual(metadata?.modelId, "ft:gpt-4o-mini:custom") + }).pipe(Effect.provide(makeTestLayer({ body: { model: "ft:gpt-4o-mini:custom" as any } })))) + }) + + describe("generateText", () => { + describe("message preparation", () => { + describe("system messages", () => { + it.effect("uses system role for standard models", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const systemMessage = body.input.find((m: any) => m.role === "system") + assert.isDefined(systemMessage) + strictEqual(systemMessage.content, "You are a helpful assistant") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("uses developer role for reasoning models", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("o1"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const devMessage = body.input.find((m: any) => m.role === "developer") + assert.isDefined(devMessage) + strictEqual(devMessage.content, "You are a helpful assistant") + }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } })))) + + it.effect("uses developer role for gpt-5 models", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-5"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const devMessage = body.input.find((m: any) => m.role === "developer") + assert.isDefined(devMessage) + }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5" } })))) + + it.effect("uses developer role for o3 models", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("o3-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const devMessage = body.input.find((m: any) => m.role === "developer") + assert.isDefined(devMessage) + }).pipe(Effect.provide(makeTestLayer({ body: { model: "o3-mini" } })))) + }) + + describe("user messages", () => { + it.effect("converts text parts to input_text", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Hello world" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + assert.isDefined(userMessage) + deepStrictEqual(userMessage.content, [{ type: "input_text", text: "Hello world" }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles image URLs", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "image/png", + data: new URL("https://example.com/image.png") + }) + ] + }]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "input_image", + image_url: "https://example.com/image.png", + detail: "auto" + }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles image with custom detail level", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "image/png", + data: new URL("https://example.com/image.png"), + options: { openai: { imageDetail: "high" } } + }) + ] + }]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + strictEqual(userMessage.content[0].detail, "high") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles image file IDs with configured prefixes", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "image/png", + data: "file-abc123" + }) + ] + }]) + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { + fileIdPrefixes: ["file-"] + })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "input_image", + file_id: "file-abc123", + detail: "auto" + }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles image base64 data", () => + Effect.gen(function*() { + const imageData = new Uint8Array([137, 80, 78, 71]) // PNG magic bytes + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "image/png", + data: imageData + }) + ] + }]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + assert.isTrue(userMessage.content[0].image_url.startsWith("data:image/png;base64,")) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles PDF URLs", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "application/pdf", + data: new URL("https://example.com/document.pdf") + }) + ] + }]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "input_file", + file_url: "https://example.com/document.pdf" + }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles PDF base64 data with filename", () => + Effect.gen(function*() { + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "application/pdf", + data: pdfData, + fileName: "document.pdf" + }) + ] + }]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.input.find((m: any) => m.role === "user") + strictEqual(userMessage.content[0].type, "input_file") + strictEqual(userMessage.content[0].filename, "document.pdf") + assert.isTrue(userMessage.content[0].file_data.startsWith("data:application/pdf;base64,")) + }).pipe(Effect.provide(makeTestLayer()))) + }) + + describe("assistant messages", () => { + it.effect("converts text parts to message output", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [Prompt.textPart({ text: "Hi there!" })] + }, + { role: "user", content: "How are you?" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const assistantMessage = body.input.find((m: any) => m.type === "message" && m.role === "assistant") + assert.isDefined(assistantMessage) + strictEqual(assistantMessage.content[0].type, "output_text") + strictEqual(assistantMessage.content[0].text, "Hi there!") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("converts reasoning parts", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Think step by step" }, + { + role: "assistant", + content: [ + Prompt.reasoningPart({ + text: "Let me think...", + options: { openai: { itemId: "reasoning_123" } } + }) + ] + }, + { role: "user", content: "Continue" } + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("o1"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const reasoningItem = body.input.find((m: any) => m.type === "reasoning") + assert.isDefined(reasoningItem) + strictEqual(reasoningItem.id, "reasoning_123") + }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } })))) + + it.effect("converts tool call parts to function_call", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "call_abc", + name: "TestTool", + params: { input: "test" }, + providerExecuted: false + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolResultPart({ + id: "call_abc", + name: "TestTool", + isFailure: false, + result: { output: "result" } + }) + ] + } + ]), + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const functionCall = body.input.find((m: any) => m.type === "function_call") + assert.isDefined(functionCall) + strictEqual(functionCall.name, "TestTool") + strictEqual(functionCall.call_id, "call_abc") + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + }) + + describe("tool messages", () => { + it.effect("converts tool results to function_call_output", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Use the tool" }, + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "call_abc", + name: "TestTool", + params: { input: "test" }, + providerExecuted: false + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolResultPart({ + id: "call_abc", + name: "TestTool", + isFailure: false, + result: { output: "result" } + }) + ] + } + ]), + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const toolOutput = body.input.find((m: any) => m.type === "function_call_output") + assert.isDefined(toolOutput) + strictEqual(toolOutput.call_id, "call_abc") + strictEqual(toolOutput.output, JSON.stringify({ output: "result" })) + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + }) + }) + + describe("tool preparation", () => { + it.effect("converts user-defined tools to function type", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "function") + assert.isDefined(tool) + strictEqual(tool.name, "TestTool") + strictEqual(tool.description, "A test tool") + strictEqual(tool.strict, true) + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("empty object on properties for empty parameters", () => + Effect.gen(function*() { + const EmptyTool = Tool.make("EmptyParamsTool", { + description: "Empty params tool", + parameters: Tool.EmptyParams, + success: Schema.String + }) + const toolkit = Toolkit.make(EmptyTool) + const toolkitLayer = toolkit.toLayer({ + EmptyParamsTool: () => Effect.succeed("ok") + }) + + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe(Effect.provide([OpenAiLanguageModel.model("gpt-4o-mini"), toolkitLayer])) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "function" && t.name === "EmptyParamsTool") + assert.isDefined(tool) + deepStrictEqual(tool.parameters, { + type: "object", + properties: {}, + additionalProperties: false + }) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("converts dynamic tools to function type", () => + Effect.gen(function*() { + const inputSchema = { + type: "object", + properties: { + query: { type: "string" }, + limit: { type: "number" } + }, + required: ["query"], + additionalProperties: false + } as const + + const DynamicTool = Tool.dynamic("DynamicTool", { + description: "A dynamic tool", + parameters: inputSchema + }) + + yield* LanguageModel.generateText({ + prompt: "Use the dynamic tool", + toolkit: Toolkit.make(DynamicTool), + disableToolCallResolution: true + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((entry: any) => entry.type === "function" && entry.name === "DynamicTool") + assert.isDefined(tool) + strictEqual(tool.description, "A dynamic tool") + deepStrictEqual(tool.parameters, inputSchema) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("empty object on properties for empty parameters", () => + Effect.gen(function*() { + const EmptyTool = Tool.make("EmptyParamsTool", { + description: "Empty params tool", + parameters: Tool.EmptyParams, + success: Schema.String + }) + const toolkit = Toolkit.make(EmptyTool) + const toolkitLayer = toolkit.toLayer({ + EmptyParamsTool: () => Effect.succeed("ok") + }) + + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit + }).pipe(Effect.provide([OpenAiLanguageModel.model("gpt-4o-mini"), toolkitLayer])) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "function" && t.name === "EmptyParamsTool") + assert.isDefined(tool) + deepStrictEqual(tool.parameters, { + type: "object", + properties: {}, + additionalProperties: false + }) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("handles tool choice auto", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit, + toolChoice: "auto" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.tool_choice, "auto") + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("handles tool choice none", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit, + toolChoice: "none" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.tool_choice, "none") + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("handles tool choice required", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit, + toolChoice: "required" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.tool_choice, "required") + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("handles specific tool choice", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit, + toolChoice: { tool: "TestTool" } + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + deepStrictEqual(body.tool_choice, { type: "function", name: "TestTool" }) + }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("adds code_interpreter tool", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.CodeInterpreter({ container: { type: "auto" } })) + + yield* LanguageModel.generateText({ + prompt: "Run some code", + toolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "code_interpreter") + assert.isDefined(tool) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("adds web_search tool", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.WebSearch({})) + + yield* LanguageModel.generateText({ + prompt: "Search the web", + toolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "web_search") + assert.isDefined(tool) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("adds file_search tool with vector store IDs", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.FileSearch({ + vector_store_ids: ["vs_123"] + })) + + yield* LanguageModel.generateText({ + prompt: "Search files", + toolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const tool = body.tools?.find((t: any) => t.type === "file_search") + assert.isDefined(tool) + deepStrictEqual(tool.vector_store_ids, ["vs_123"]) + }).pipe(Effect.provide(makeTestLayer()))) + }) + + describe("response format", () => { + it.effect("uses text format by default", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.text?.format?.type, "text") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("uses json_schema format for structured output", () => + Effect.gen(function*() { + yield* LanguageModel.generateObject({ + prompt: "Give me a person", + schema: Schema.Struct({ + name: Schema.String, + age: Schema.Number + }) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.text?.format?.type, "json_schema") + strictEqual(body.text?.format?.strict, true) + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [makeTextOutput(JSON.stringify({ name: "John", age: 30 }))] + } + })))) + }) + + describe("response handling", () => { + it.effect("extracts text from output_text", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + strictEqual(result.text, "Hello, world!") + }).pipe(Effect.provide(makeTestLayer({ + body: { output: [makeTextOutput("Hello, world!")] } + })))) + + it.effect("extracts multiple text parts", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const textParts = result.content.filter((p) => p.type === "text") + strictEqual(textParts.length, 2) + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [ + makeTextOutput("First"), + makeTextOutput("Second", { id: "msg_456" }) + ] + } + })))) + + it.effect("handles refusal content", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Do something bad" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const textPart = result.content.find((p) => p.type === "text") + strictEqual(textPart?.text, "") + strictEqual(textPart?.metadata?.openai?.refusal, "I cannot do that") + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [{ + type: "message", + id: "msg_123", + role: "assistant", + status: "completed", + content: [{ type: "refusal", refusal: "I cannot do that" }] + }] + } + })))) + + it.effect("parses function call arguments", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const toolCall = result.content.find((p) => p.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "TestTool") + deepStrictEqual(toolCall.params, { input: "hello" }) + } + }).pipe( + Effect.provide([ + makeTestLayer({ + body: { output: [makeFunctionCall("TestTool", { input: "hello" })] } + }), + TestToolkitLayer + ]) + )) + + it.effect("uses canonical OpenAiMcp name for mcp_call", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Use MCP", + toolkit: McpToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "OpenAiMcp") + deepStrictEqual(toolCall.params, { packageName: "effect" }) + } + + const toolResult = result.content.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + if (toolResult?.type === "tool-result") { + strictEqual(toolResult.name, "OpenAiMcp") + strictEqual(toolResult.result.name, "CheckPackage") + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [makeMcpCall("CheckPackage", { packageName: "effect" })] + } + })))) + + it.effect("uses canonical OpenAiMcp name for mcp_approval_request", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Use MCP", + toolkit: McpToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "OpenAiMcp") + deepStrictEqual(toolCall.params, { packageName: "effect" }) + } + + const approvalRequest = result.content.find((part) => part.type === "tool-approval-request") + assert.isDefined(approvalRequest) + if (toolCall?.type === "tool-call" && approvalRequest?.type === "tool-approval-request") { + strictEqual(approvalRequest.toolCallId, toolCall.id) + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [makeMcpApprovalRequest("CheckPackage", { packageName: "effect" })] + } + })))) + + it.effect("extracts reasoning parts", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Think about this" + }).pipe(Effect.provide(OpenAiLanguageModel.model("o1"))) + + const reasoningParts = result.content.filter((p) => p.type === "reasoning") + strictEqual(reasoningParts.length, 2) + if (reasoningParts[0]?.type === "reasoning") { + strictEqual(reasoningParts[0].text, "First thought") + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + model: "o1", + output: [makeReasoningOutput(["First thought", "Second thought"])] + } + })))) + + it.effect("extracts usage information", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const finishPart = result.content.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type === "finish") { + deepStrictEqual(finishPart.usage.inputTokens, { + uncached: 10, + total: 10, + cacheRead: 0, + cacheWrite: undefined + }) + deepStrictEqual(finishPart.usage.outputTokens, { total: 20, text: 20, reasoning: 0 }) + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [makeTextOutput("Hello")], + usage: makeUsage() + } + })))) + + it.effect("determines finish reason from incomplete_details", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const finishPart = result.content.find((p) => p.type === "finish") + if (finishPart?.type === "finish") { + strictEqual(finishPart.reason, "content-filter") + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [makeTextOutput("Hello")], + incomplete_details: { reason: "content_filter" } + } + })))) + + it.effect("defaults finish reason to stop", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const finishPart = result.content.find((p) => p.type === "finish") + if (finishPart?.type === "finish") { + strictEqual(finishPart.reason, "stop") + } + }).pipe(Effect.provide(makeTestLayer({ + body: { output: [makeTextOutput("Hello")] } + })))) + + it.effect("sets finish reason to tool-calls when has tool calls", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Use the tool", + toolkit: TestToolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const finishPart = result.content.find((p) => p.type === "finish") + if (finishPart?.type === "finish") { + strictEqual(finishPart.reason, "tool-calls") + } + }).pipe( + Effect.provide([ + makeTestLayer({ + body: { output: [makeFunctionCall("TestTool", { input: "test" })] } + }), + TestToolkitLayer + ]) + )) + + it.effect("extracts url citations as source parts", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const sourcePart = result.content.find((p) => p.type === "source") + assert.isDefined(sourcePart) + if (sourcePart?.type === "source" && sourcePart.sourceType === "url") { + strictEqual(sourcePart.url.href, "https://example.com/") + strictEqual(sourcePart.title, "Example") + } + }).pipe(Effect.provide(makeTestLayer({ + body: { + output: [{ + type: "message", + id: "msg_123", + role: "assistant", + status: "completed", + content: [{ + type: "output_text", + text: "Check this out", + annotations: [{ + type: "url_citation", + url: "https://example.com", + title: "Example", + start_index: 0, + end_index: 14 + }], + logprobs: [] + }] + }] + } + })))) + }) + }) + + describe("streamText", () => { + it.effect("emits valid apply_patch tool params JSON for update_file diffs", () => + Effect.gen(function*() { + const diff = "@@ -1 +1 @@\n-old\n+new\n" + const outputItem = { + type: "apply_patch_call", + id: "patch_item_1", + call_id: "patch_call_1", + status: "in_progress", + operation: { + type: "update_file", + path: "src/example.ts", + diff + } + } as const + + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_patch_stream", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.added", + output_index: 0, + sequence_number: 2, + item: outputItem + }, + { + type: "response.apply_patch_call_operation_diff.delta", + sequence_number: 3, + output_index: 0, + item_id: outputItem.id, + delta: diff + }, + { + type: "response.apply_patch_call_operation_diff.done", + sequence_number: 4, + output_index: 0, + item_id: outputItem.id + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Update src/example.ts", + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)) + ) + + const parts = globalThis.Array.from(partsChunk) + const params = decodeToolParamsFromStream(parts, outputItem.call_id) + + deepStrictEqual(params, { + call_id: outputItem.call_id, + operation: { + type: "update_file", + path: "src/example.ts", + diff + } + }) + })) + + it.effect("emits tool call from function_call_arguments.done when output_item.done is missing", () => + Effect.gen(function*() { + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_function_call_done", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.added", + sequence_number: 2, + output_index: 0, + item: { + type: "function_call", + id: "fc_1", + call_id: "call_1", + name: "TestTool", + arguments: "", + status: "in_progress" + } + }, + { + type: "response.function_call_arguments.delta", + sequence_number: 3, + output_index: 0, + item_id: "fc_1", + delta: "{\"input\":\"hel" + }, + { + type: "response.function_call_arguments.done", + sequence_number: 4, + output_index: 0, + item_id: "fc_1", + name: "TestTool", + arguments: "{\"input\":\"hello\"}" + }, + { + type: "response.completed", + sequence_number: 5, + response: makeDefaultResponse({ + id: "resp_function_call_done", + status: "completed", + output: [] + }) + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Use the test tool", + toolkit: TestToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)), + Effect.provide(TestToolkitLayer) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolCalls = parts.filter((part) => part.type === "tool-call" && part.id === "call_1") + strictEqual(toolCalls.length, 1) + const toolCall = toolCalls[0] + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "TestTool") + deepStrictEqual(toolCall.params, { input: "hello" }) + } + + const toolParamsEnd = parts.find((part) => part.type === "tool-params-end" && part.id === "call_1") + assert.isDefined(toolParamsEnd) + })) + + it.effect("handles reasoning summary events when reasoning state is missing", () => + Effect.gen(function*() { + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_reasoning_missing_state", + status: "in_progress", + output: [] + }) + }, + { + type: "response.reasoning_summary_part.added", + sequence_number: 2, + output_index: 0, + item_id: "rs_missing", + summary_index: 1 + }, + { + type: "response.reasoning_summary_text.delta", + sequence_number: 3, + output_index: 0, + item_id: "rs_missing", + summary_index: 1, + delta: "thinking" + }, + { + type: "response.reasoning_summary_part.done", + sequence_number: 4, + output_index: 0, + item_id: "rs_missing", + summary_index: 1 + }, + { + type: "response.output_item.done", + sequence_number: 5, + output_index: 0, + item: makeReasoningOutput(["thinking"], { id: "rs_missing" }) + }, + { + type: "response.output_item.done", + sequence_number: 6, + output_index: 1, + item: makeReasoningOutput([], { id: "rs_done_only" }) + }, + { + type: "response.completed", + sequence_number: 7, + response: makeDefaultResponse({ + id: "resp_reasoning_missing_state", + status: "completed", + output: [] + }) + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "reason" + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)) + ) + + const parts = globalThis.Array.from(partsChunk) + assert.isDefined(parts.find((part) => part.type === "reasoning-start" && part.id === "rs_missing:1")) + assert.isDefined(parts.find((part) => part.type === "reasoning-end" && part.id === "rs_missing:1")) + assert.isDefined(parts.find((part) => part.type === "finish")) + })) + + it.effect("uses canonical OpenAiMcp name for streamed mcp_call", () => + Effect.gen(function*() { + const outputItem = makeMcpCall("CheckPackage", { packageName: "effect" }, { id: "mcp_call_1" }) + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_mcp_stream", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.done", + output_index: 0, + sequence_number: 2, + item: outputItem + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Use MCP", + toolkit: McpToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "OpenAiMcp") + deepStrictEqual(toolCall.params, { packageName: "effect" }) + } + + const toolResult = parts.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + if (toolResult?.type === "tool-result") { + strictEqual(toolResult.name, "OpenAiMcp") + strictEqual(toolResult.result.name, "CheckPackage") + } + })) + + it.effect("uses canonical OpenAiMcp name for streamed mcp_approval_request", () => + Effect.gen(function*() { + const outputItem = makeMcpApprovalRequest("CheckPackage", { packageName: "effect" }, { id: "approval_1" }) + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ + id: "resp_mcp_approval_stream", + status: "in_progress", + output: [] + }) + }, + { + type: "response.output_item.done", + output_index: 0, + sequence_number: 2, + item: outputItem + } + ] as unknown as ReadonlyArray + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "Use MCP", + toolkit: McpToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)) + ) + + const parts = globalThis.Array.from(partsChunk) + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + strictEqual(toolCall.name, "OpenAiMcp") + deepStrictEqual(toolCall.params, { packageName: "effect" }) + } + + const approvalRequest = parts.find((part) => part.type === "tool-approval-request") + assert.isDefined(approvalRequest) + if (toolCall?.type === "tool-call" && approvalRequest?.type === "tool-approval-request") { + strictEqual(approvalRequest.toolCallId, toolCall.id) + } + })) + + it.effect("pre-resolves denied OpenAiMcp approvals without lookup failure", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "mcp_tool_call_1", + name: "OpenAiMcp", + params: { packageName: "effect" }, + providerExecuted: true + }), + Prompt.makePart("tool-approval-request", { + approvalId: "approval_1", + toolCallId: "mcp_tool_call_1" + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolApprovalResponsePart({ + approvalId: "approval_1", + approved: false, + reason: "Denied" + }) + ] + }, + { role: "user", content: "Continue" } + ]), + toolkit: McpToolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeTestLayer({ + body: { + output: [makeTextOutput("Handled denied MCP approval")] + } + })) + ) + + strictEqual(result.text, "Handled denied MCP approval") + })) + }) + + describe("withConfigOverride", () => { + it.effect("merges config overrides", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ prompt: "test" }).pipe( + OpenAiLanguageModel.withConfigOverride({ temperature: 0.5 }), + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.temperature, 0.5) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("override takes precedence", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ prompt: "test" }).pipe( + OpenAiLanguageModel.withConfigOverride({ temperature: 0.9 }), + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { temperature: 0.5 })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.temperature, 0.9) + }).pipe(Effect.provide(makeTestLayer()))) + }) + + describe("config", () => { + it.effect("does not leak library-only fields into request body", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ prompt: "test" }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { + fileIdPrefixes: ["file-"], + strictJsonSchema: false, + temperature: 0.5 + })) + ) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + strictEqual(body.fileIdPrefixes, undefined) + strictEqual(body.strictJsonSchema, undefined) + strictEqual(body.temperature, 0.5) + }).pipe(Effect.provide(makeTestLayer()))) + }) +}) + +// ============================================================================= +// Test Infrastructure +// ============================================================================= + +class MockOpenAiResponse extends Context.Service | undefined +}>()("MockOpenAiResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = Effect.service(MockHttpClient).pipe( + Effect.flatMap((client) => client.requests) + ) +} + +const encodeResponse = Schema.encodeEffect(Generated.Response) + +const makeHttpClient = Effect.gen(function*() { + const capturedRequests = yield* Ref.make>([]) + const response = yield* MockOpenAiResponse + const body = yield* Effect.orDie(encodeResponse(response.body)) + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(capturedRequests, Array.append(request)) + return HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + headers: response.headers ?? {}, + status: response.status + }) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) })) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClient) + +const makeStreamTestLayer = (events: ReadonlyArray) => { + const response = HttpClientResponse.fromWeb( + HttpClientRequest.get("https://api.openai.com/v1/responses"), + new Response("", { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) + + return Layer.succeed( + OpenAiClient.OpenAiClient, + OpenAiClient.OpenAiClient.of({ + client: undefined as any, + createResponse: () => Effect.die(new Error("unexpected createResponse call")), + createResponseStream: () => Effect.succeed([response, Stream.fromIterable(events)]), + createEmbedding: () => Effect.die(new Error("unexpected createEmbedding call")) + }) + ) +} + +const makeDefaultResponse = ( + overrides: Partial = {} +): Generated.Response => ({ + id: "resp_test123", + object: "response", + created_at: Math.floor(Date.now() / 1000), + model: "gpt-4o-mini", + status: "completed", + output: [], + metadata: null, + temperature: null, + top_p: null, + tools: [], + tool_choice: "auto", + error: null, + incomplete_details: null, + instructions: null, + parallel_tool_calls: false, + ...overrides +}) + +const makeTestLayer = (options: { + readonly body?: Partial + readonly status?: number + readonly headers?: Record +} = {}) => + OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { + body: makeDefaultResponse(options.body), + status: options.status ?? 200, + headers: options.headers ?? {} + })) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) + +const decodeToolParamsFromStream = ( + parts: ReadonlyArray, + toolCallId: string +): Record => { + const start = parts.find((part) => part.type === "tool-params-start" && part.id === toolCallId) + const end = parts.find((part) => part.type === "tool-params-end" && part.id === toolCallId) + assert.isDefined(start) + assert.isDefined(end) + + const deltas = parts + .filter((part) => part.type === "tool-params-delta" && part.id === toolCallId) + .map((part) => part.delta) + .join("") + + return JSON.parse(deltas) as Record +} + +const makeTextOutput = ( + text: string, + overrides: Partial = {} +): Generated.OutputMessage => ({ + type: "message", + id: "msg_123", + role: "assistant" as const, + status: "completed", + content: [{ type: "output_text", text, annotations: [], logprobs: [] }], + ...overrides +}) + +const makeFunctionCall = ( + name: string, + args: Record, + overrides: Partial = {} +): Generated.FunctionToolCall => ({ + type: "function_call", + id: "fc_123", + call_id: "call_123", + name, + arguments: JSON.stringify(args), + status: "completed", + ...overrides +}) + +const makeMcpCall = ( + name: string, + args: Record, + overrides: Partial = {} +): Generated.MCPToolCall => ({ + type: "mcp_call", + id: "mcp_call_123", + server_label: "npm", + name, + arguments: JSON.stringify(args), + output: "ok", + status: "completed", + ...overrides +}) + +const makeMcpApprovalRequest = ( + name: string, + args: Record, + overrides: Partial & { readonly approval_request_id?: string } = {} +): Generated.MCPApprovalRequest => ({ + type: "mcp_approval_request", + id: "approval_123", + server_label: "npm", + name, + arguments: JSON.stringify(args), + ...overrides +}) + +const makeReasoningOutput = ( + summaries: Array, + overrides: Partial = {} +): Generated.ReasoningItem => ({ + type: "reasoning", + id: "rs_123", + summary: summaries.map((text) => ({ type: "summary_text", text })), + encrypted_content: null, + ...overrides +}) + +const makeUsage = ( + overrides: Partial = {} +): Generated.ResponseUsage => ({ + input_tokens: 10, + output_tokens: 20, + total_tokens: 30, + input_tokens_details: { cached_tokens: 0 }, + output_tokens_details: { reasoning_tokens: 0 }, + ...overrides +}) + +const TestTool = Tool.make("TestTool", { + description: "A test tool", + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }) +}) + +const TestToolkit = Toolkit.make(TestTool) + +const McpToolkit = Toolkit.make(OpenAiTool.Mcp({ + server_label: "npm", + server_url: "https://example.com/mcp", + require_approval: "never" +})) + +const TestToolkitLayer = TestToolkit.toLayer({ + TestTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` }) +}) diff --git a/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts b/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts new file mode 100644 index 0000000..70a30f2 --- /dev/null +++ b/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts @@ -0,0 +1,273 @@ +import * as OpenAiSchema from "@effect/ai-openai/OpenAiSchema" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Schema, Stream } from "effect" +import * as Sse from "effect/unstable/encoding/Sse" + +const makeResponse = (overrides: Record = {}) => ({ + id: "resp_123", + object: "response", + model: "gpt-4o-mini", + status: "completed", + created_at: 1, + output: [], + ...overrides +}) + +describe("OpenAiSchema", () => { + it("decodes a representative response payload", () => { + const decoded = Schema.decodeUnknownSync(OpenAiSchema.Response)({ + ...makeResponse(), + output: [ + { + id: "msg_1", + type: "message", + role: "assistant", + status: "completed", + content: [ + { + type: "output_text", + text: "hello", + annotations: [ + { + type: "url_citation", + url: "https://example.com", + start_index: 0, + end_index: 5, + title: "example" + } + ] + }, + { + type: "refusal", + refusal: "cannot comply" + } + ] + }, + { + id: "fc_1", + type: "function_call", + call_id: "call_1", + name: "lookup", + arguments: "{}", + status: "completed" + }, + { + id: "reasoning_1", + type: "reasoning", + summary: [ + { + type: "summary_text", + text: "thinking" + } + ] + } + ], + usage: { + input_tokens: 10, + output_tokens: 20, + total_tokens: 30 + } + }) + + assert.strictEqual(decoded.id, "resp_123") + assert.strictEqual(decoded.output.length, 3) + assert.strictEqual(decoded.output[0].type, "message") + if (decoded.output[0].type === "message") { + assert.strictEqual(decoded.output[0].content[0].type, "output_text") + } + }) + + it("decodes required stream events", () => { + const response = makeResponse({ status: "in_progress" }) + const applyPatchItem = { + id: "ap_1", + type: "apply_patch_call", + call_id: "call_ap", + operation: { + type: "update_file", + path: "README.md", + diff: "@@" + } + } + + const events = [ + { type: "response.created", sequence_number: 1, response }, + { + type: "response.completed", + sequence_number: 2, + response: makeResponse() + }, + { type: "response.incomplete", sequence_number: 3, response: makeResponse({ status: "incomplete" }) }, + { type: "response.failed", sequence_number: 4, response: makeResponse({ status: "failed" }) }, + { type: "response.output_item.added", sequence_number: 5, output_index: 0, item: applyPatchItem }, + { type: "response.output_item.done", sequence_number: 6, output_index: 0, item: applyPatchItem }, + { + type: "response.output_text.delta", + sequence_number: 7, + item_id: "msg_1", + output_index: 0, + content_index: 0, + delta: "hel" + }, + { + type: "response.output_text.annotation.added", + sequence_number: 8, + item_id: "msg_1", + output_index: 0, + content_index: 0, + annotation_index: 0, + annotation: { + type: "file_path", + file_id: "file_1", + index: 0 + } + }, + { + type: "response.reasoning_summary_part.added", + sequence_number: 9, + item_id: "reasoning_1", + output_index: 1, + summary_index: 0, + part: { type: "summary_text", text: "thinking" } + }, + { + type: "response.reasoning_summary_part.done", + sequence_number: 10, + item_id: "reasoning_1", + output_index: 1, + summary_index: 0, + part: { type: "summary_text", text: "thinking" } + }, + { + type: "response.reasoning_summary_text.delta", + sequence_number: 11, + item_id: "reasoning_1", + output_index: 1, + summary_index: 0, + delta: "..." + }, + { + type: "response.function_call_arguments.delta", + sequence_number: 12, + item_id: "fc_1", + output_index: 2, + delta: "{" + }, + { + type: "response.function_call_arguments.done", + sequence_number: 13, + item_id: "fc_1", + output_index: 2, + arguments: "{}" + }, + { + type: "response.code_interpreter_call_code.delta", + sequence_number: 14, + item_id: "code_1", + output_index: 3, + delta: "print" + }, + { + type: "response.code_interpreter_call_code.done", + sequence_number: 15, + item_id: "code_1", + output_index: 3, + code: "print('ok')" + }, + { + type: "response.apply_patch_call_operation_diff.delta", + sequence_number: 16, + item_id: "ap_1", + output_index: 4, + delta: "@@" + }, + { + type: "response.apply_patch_call_operation_diff.done", + sequence_number: 17, + item_id: "ap_1", + output_index: 4, + delta: "@@" + }, + { + type: "response.image_generation_call.partial_image", + sequence_number: 18, + item_id: "img_1", + output_index: 5, + partial_image_b64: "AQID" + } + ] + + for (const event of events) { + const decoded = Schema.decodeUnknownSync(OpenAiSchema.ResponseStreamEvent)(event) + assert.strictEqual(decoded.type, event.type) + } + }) + + it.effect("keeps keepalive and unknown events tolerant in SSE decoding", () => + Effect.gen(function*() { + const sseBody = [ + { + type: "response.created", + sequence_number: 1, + response: makeResponse({ status: "in_progress" }) + }, + { + type: "keepalive", + sequence_number: 2, + heartbeat: true + }, + { + type: "provider.future_event", + sequence_number: 3, + nested: { ok: true } + }, + { + type: "response.completed", + sequence_number: 4, + response: makeResponse({ status: "completed" }) + } + ].map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + + const events = yield* Stream.fromIterable([sseBody]).pipe( + Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)), + Stream.map((event) => event.data), + Stream.runCollect + ) + + const decoded = globalThis.Array.from(events) + assert.strictEqual(decoded.length, 4) + assert.strictEqual(decoded[1].type, "keepalive") + assert.strictEqual(decoded[2].type, "provider.future_event") + if (decoded[1].type === "keepalive") { + assert.strictEqual(decoded[1].sequence_number, 2) + } + })) + + it.effect("does not silently decode malformed known events as unknown", () => + Effect.gen(function*() { + const malformed = yield* Schema.decodeUnknownEffect(OpenAiSchema.ResponseStreamEvent)({ + type: "response.completed", + sequence_number: 1 + }).pipe(Effect.flip) + + assert.isDefined(malformed) + })) + + it("decodes embedding response variants (numeric + string/base64)", () => { + const numeric = Schema.decodeUnknownSync(OpenAiSchema.CreateEmbeddingResponse)({ + object: "list", + model: "text-embedding-3-small", + data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2] }], + usage: { prompt_tokens: 2, total_tokens: 2 } + }) + const base64 = Schema.decodeUnknownSync(OpenAiSchema.CreateEmbeddingResponse)({ + object: "list", + model: "text-embedding-3-small", + data: [{ object: "embedding", index: 0, embedding: "AQID" }], + usage: { prompt_tokens: 2, total_tokens: 2 } + }) + + assert.strictEqual(numeric.data[0].embedding[0], 0.1) + assert.strictEqual(base64.data[0].embedding, "AQID") + }) +}) diff --git a/repos/effect/packages/ai/openai/tsconfig.json b/repos/effect/packages/ai/openai/tsconfig.json new file mode 100644 index 0000000..19a2f5d --- /dev/null +++ b/repos/effect/packages/ai/openai/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/ai/openai/vitest.config.ts b/repos/effect/packages/ai/openai/vitest.config.ts new file mode 100644 index 0000000..c8a52c1 --- /dev/null +++ b/repos/effect/packages/ai/openai/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/ai/openrouter/CHANGELOG.md b/repos/effect/packages/ai/openrouter/CHANGELOG.md new file mode 100644 index 0000000..34b490b --- /dev/null +++ b/repos/effect/packages/ai/openrouter/CHANGELOG.md @@ -0,0 +1,725 @@ +# @effect/ai-openrouter + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- [#2379](https://github.com/Effect-TS/effect-smol/pull/2379) [`717d1c8`](https://github.com/Effect-TS/effect-smol/commit/717d1c8b160d4de631b6b7938abcad9e3472c3d7) Thanks @walln! - Convert audio file parts in prompts into OpenRouter `input_audio` content blocks. + + Previously, every non-image file part was converted into a generic `file` content block. OpenRouter only accepts audio as base64-encoded `input_audio` content parts, so audio attachments were rejected or mishandled by the upstream provider. + + Audio file parts with a recognized `mediaType` (aac, aiff, flac, m4a, mp3, ogg, pcm16, pcm24, and wav) are now converted into `input_audio` blocks. Unsupported audio media types and `URL` audio data fail with an `AiError` since OpenRouter requires base64-encoded audio data and does not fetch audio URLs. + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- [#1871](https://github.com/Effect-TS/effect-smol/pull/1871) [`977386d`](https://github.com/Effect-TS/effect-smol/commit/977386da5e2e8aac2c07e99175673e0b5771191b) Thanks @IMax153! - Fix HTTP Referer header name in the `OpenRouterClient` + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- [#1529](https://github.com/Effect-TS/effect-smol/pull/1529) [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8) Thanks @tim-smart! - Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. + +- [#1528](https://github.com/Effect-TS/effect-smol/pull/1528) [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34) Thanks @tim-smart! - Add `Model.ModelName` and provide it from AI model constructors. + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- [#1502](https://github.com/Effect-TS/effect-smol/pull/1502) [`285b7e6`](https://github.com/Effect-TS/effect-smol/commit/285b7e667167566d5788367d5155b19c79f1bf22) Thanks @tim-smart! - allow undefined for ai config + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- [#1354](https://github.com/Effect-TS/effect-smol/pull/1354) [`b94962c`](https://github.com/Effect-TS/effect-smol/commit/b94962c249d46cf96cdf2e41188dc9feda41536a) Thanks @IMax153! - Fix the generated schemas for ai providers + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- [#1306](https://github.com/Effect-TS/effect-smol/pull/1306) [`c9fb5a5`](https://github.com/Effect-TS/effect-smol/commit/c9fb5a5bfb1c331c91d592323f4027b72a3bc0b4) Thanks @Leka74! - Fix sparse array crash in `streamText` tool call handling. + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/ai/openrouter/codegen.yml b/repos/effect/packages/ai/openrouter/codegen.yml new file mode 100644 index 0000000..b556693 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/codegen.yml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=../../tools/ai-codegen/codegen.schema.json +spec: https://openrouter.ai/openapi.yaml +output: src/Generated.ts +name: OpenRouterClient +header: | + /** + * @since 1.0.0 + */ +excludeAnnotations: + - default +disableAdditionalProperties: true +patches: + # Replace OpenResponsesStreamEvent with a flat oneOf of $refs. + # The original uses allOf wrappers that inline OpenResponsesNonStreamingResponse + # into each variant, producing lines over 1M chars that crash dprint. + - '[{"op":"replace","path":"/components/schemas/OpenResponsesStreamEvent","value":{"oneOf":[{"$ref":"#/components/schemas/OpenResponsesCreatedEvent"},{"$ref":"#/components/schemas/OpenResponsesInProgressEvent"},{"$ref":"#/components/schemas/OpenResponsesCompletedEvent"},{"$ref":"#/components/schemas/OpenResponsesIncompleteEvent"},{"$ref":"#/components/schemas/OpenResponsesFailedEvent"},{"$ref":"#/components/schemas/OpenResponsesErrorEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputItemAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputItemDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesContentPartAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesContentPartDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesTextDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesTextDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesRefusalDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesRefusalDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputTextAnnotationAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesFunctionCallArgumentsDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesFunctionCallArgumentsDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryPartAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryPartDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryTextDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryTextDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallInProgress"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallGenerating"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallPartialImage"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallCompleted"}]}}]' + # Fix AssistantMessage images to include type:"image_url" discriminator and nullable + - '[{"op":"replace","path":"/components/schemas/AssistantMessage/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' + # Add images to ChatStreamingMessageChunk (streaming delta) + - '[{"op":"add","path":"/components/schemas/ChatStreamingMessageChunk/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' + # Add annotations to AssistantMessage (non-streaming) + - '[{"op":"add","path":"/components/schemas/AssistantMessage/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' + # Add annotations to ChatStreamingMessageChunk (streaming delta) + - '[{"op":"add","path":"/components/schemas/ChatStreamingMessageChunk/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' + # Make tool call delta fields nullable (models like kimi-k2.5, minimax-m2.5 send null) + - '[{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/id","value":{"anyOf":[{"type":"string"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/type","value":{"anyOf":[{"type":"string","const":"function"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/function/properties/name","value":{"anyOf":[{"type":"string"},{"type":"null"}]}}]' + # Make finish_reason optional (only present on final streaming chunk) + - '[{"op":"remove","path":"/components/schemas/ChatStreamingChoice/required/1"}]' +replacements: + # Schema.Unknown doesn't work with Schema.toCodecJson (used by HttpClientResponse.schemaBodyJson) + # Replace with Schema.Json which properly handles arbitrary JSON values + - from: "Schema.Record(Schema.String, Schema.Unknown)" + to: "Schema.Record(Schema.String, Schema.Json)" + - from: "{ readonly [x: string]: unknown }" + to: "{ readonly [x: string]: Schema.Json }" diff --git a/repos/effect/packages/ai/openrouter/docgen.json b/repos/effect/packages/ai/openrouter/docgen.json new file mode 100644 index 0000000..0dd5cf8 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/docgen.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openrouter/src/", + "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/ai/openrouter/package.json b/repos/effect/packages/ai/openrouter/package.json new file mode 100644 index 0000000..8047803 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/package.json @@ -0,0 +1,66 @@ +{ + "name": "@effect/ai-openrouter", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "An OpenRouter provider integration for Effect AI SDK", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/ai/openrouter" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "ai", + "openrouter" + ], + "keywords": [ + "typescript", + "ai", + "openrouter" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + } +} diff --git a/repos/effect/packages/ai/openrouter/src/Generated.ts b/repos/effect/packages/ai/openrouter/src/Generated.ts new file mode 100644 index 0000000..c015b44 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/Generated.ts @@ -0,0 +1,10028 @@ +/** + * @since 4.0.0 + */ + +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { SchemaError } from "effect/Schema" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as Sse from "effect/unstable/encoding/Sse" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +// non-recursive definitions +export type OpenAIResponsesResponseStatus = + | "completed" + | "incomplete" + | "in_progress" + | "failed" + | "cancelled" + | "queued" +export const OpenAIResponsesResponseStatus = Schema.Literals([ + "completed", + "incomplete", + "in_progress", + "failed", + "cancelled", + "queued" +]) +export type FileCitation = { + readonly "type": "file_citation" + readonly "file_id": string + readonly "filename": string + readonly "index": number +} +export const FileCitation = Schema.Struct({ + "type": Schema.Literal("file_citation"), + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) +}) +export type URLCitation = { + readonly "type": "url_citation" + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number +} +export const URLCitation = Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()) +}) +export type FilePath = { readonly "type": "file_path"; readonly "file_id": string; readonly "index": number } +export const FilePath = Schema.Struct({ + "type": Schema.Literal("file_path"), + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) +}) +export type OpenAIResponsesRefusalContent = { readonly "type": "refusal"; readonly "refusal": string } +export const OpenAIResponsesRefusalContent = Schema.Struct({ + "type": Schema.Literal("refusal"), + "refusal": Schema.String +}) +export type ReasoningTextContent = { readonly "type": "reasoning_text"; readonly "text": string } +export const ReasoningTextContent = Schema.Struct({ "type": Schema.Literal("reasoning_text"), "text": Schema.String }) +export type ReasoningSummaryText = { readonly "type": "summary_text"; readonly "text": string } +export const ReasoningSummaryText = Schema.Struct({ "type": Schema.Literal("summary_text"), "text": Schema.String }) +export type OutputItemFunctionCall = { + readonly "type": "function_call" + readonly "id"?: string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" +} +export const OutputItemFunctionCall = Schema.Struct({ + "type": Schema.Literal("function_call"), + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) +}) +export type ResponsesOutputItemFunctionCall = { + readonly "type": "function_call" + readonly "id"?: string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" +} +export const ResponsesOutputItemFunctionCall = Schema.Struct({ + "type": Schema.Literal("function_call"), + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) +}) +export type WebSearchStatus = "completed" | "searching" | "in_progress" | "failed" +export const WebSearchStatus = Schema.Literals(["completed", "searching", "in_progress", "failed"]) +export type ImageGenerationStatus = "in_progress" | "completed" | "generating" | "failed" +export const ImageGenerationStatus = Schema.Literals(["in_progress", "completed", "generating", "failed"]) +export type ResponsesErrorField = { + readonly "code": + | "server_error" + | "rate_limit_exceeded" + | "invalid_prompt" + | "vector_store_timeout" + | "invalid_image" + | "invalid_image_format" + | "invalid_base64_image" + | "invalid_image_url" + | "image_too_large" + | "image_too_small" + | "image_parse_error" + | "image_content_policy_violation" + | "invalid_image_mode" + | "image_file_too_large" + | "unsupported_image_media_type" + | "empty_image_file" + | "failed_to_download_image" + | "image_file_not_found" + readonly "message": string +} +export const ResponsesErrorField = Schema.Struct({ + "code": Schema.Literals([ + "server_error", + "rate_limit_exceeded", + "invalid_prompt", + "vector_store_timeout", + "invalid_image", + "invalid_image_format", + "invalid_base64_image", + "invalid_image_url", + "image_too_large", + "image_too_small", + "image_parse_error", + "image_content_policy_violation", + "invalid_image_mode", + "image_file_too_large", + "unsupported_image_media_type", + "empty_image_file", + "failed_to_download_image", + "image_file_not_found" + ]), + "message": Schema.String +}).annotate({ "description": "Error information returned from the API" }) +export type OpenAIResponsesIncompleteDetails = { readonly "reason"?: "max_output_tokens" | "content_filter" } +export const OpenAIResponsesIncompleteDetails = Schema.Struct({ + "reason": Schema.optionalKey(Schema.Literals(["max_output_tokens", "content_filter"])) +}) +export type OpenAIResponsesUsage = { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number +} +export const OpenAIResponsesUsage = Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isFinite()), + "input_tokens_details": Schema.Struct({ "cached_tokens": Schema.Number.check(Schema.isFinite()) }), + "output_tokens": Schema.Number.check(Schema.isFinite()), + "output_tokens_details": Schema.Struct({ "reasoning_tokens": Schema.Number.check(Schema.isFinite()) }), + "total_tokens": Schema.Number.check(Schema.isFinite()) +}) +export type ResponseInputText = { readonly "type": "input_text"; readonly "text": string } +export const ResponseInputText = Schema.Struct({ "type": Schema.Literal("input_text"), "text": Schema.String }) + .annotate({ "description": "Text input content item" }) +export type ResponseInputImage = { + readonly "type": "input_image" + readonly "detail": "auto" | "high" | "low" + readonly "image_url"?: string +} +export const ResponseInputImage = Schema.Struct({ + "type": Schema.Literal("input_image"), + "detail": Schema.Literals(["auto", "high", "low"]), + "image_url": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Image input content item" }) +export type ResponseInputFile = { + readonly "type": "input_file" + readonly "file_id"?: string + readonly "file_data"?: string + readonly "filename"?: string + readonly "file_url"?: string +} +export const ResponseInputFile = Schema.Struct({ + "type": Schema.Literal("input_file"), + "file_id": Schema.optionalKey(Schema.String), + "file_data": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "file_url": Schema.optionalKey(Schema.String) +}).annotate({ "description": "File input content item" }) +export type ResponseInputAudio = { + readonly "type": "input_audio" + readonly "input_audio": { readonly "data": string; readonly "format": "mp3" | "wav" } +} +export const ResponseInputAudio = Schema.Struct({ + "type": Schema.Literal("input_audio"), + "input_audio": Schema.Struct({ "data": Schema.String, "format": Schema.Literals(["mp3", "wav"]) }) +}).annotate({ "description": "Audio input content item" }) +export type ToolCallStatus = "in_progress" | "completed" | "incomplete" +export const ToolCallStatus = Schema.Literals(["in_progress", "completed", "incomplete"]) +export type OpenResponsesRequestMetadata = {} +export const OpenResponsesRequestMetadata = Schema.Struct({}).annotate({ + "description": + "Metadata key-value pairs for the request. Keys must be ≤64 characters and cannot contain brackets. Values must be ≤512 characters. Maximum 16 pairs allowed." +}) +export type ResponsesSearchContextSize = "low" | "medium" | "high" +export const ResponsesSearchContextSize = Schema.Literals(["low", "medium", "high"]).annotate({ + "description": "Size of the search context for web search tools" +}) +export type WebSearchPreviewToolUserLocation = { + readonly "type": "approximate" + readonly "city"?: string + readonly "country"?: string + readonly "region"?: string + readonly "timezone"?: string +} +export const WebSearchPreviewToolUserLocation = Schema.Struct({ + "type": Schema.Literal("approximate"), + "city": Schema.optionalKey(Schema.String), + "country": Schema.optionalKey(Schema.String), + "region": Schema.optionalKey(Schema.String), + "timezone": Schema.optionalKey(Schema.String) +}) +export type ResponsesWebSearchUserLocation = { + readonly "type"?: "approximate" + readonly "city"?: string + readonly "country"?: string + readonly "region"?: string + readonly "timezone"?: string +} +export const ResponsesWebSearchUserLocation = Schema.Struct({ + "type": Schema.optionalKey(Schema.Literal("approximate")), + "city": Schema.optionalKey(Schema.String), + "country": Schema.optionalKey(Schema.String), + "region": Schema.optionalKey(Schema.String), + "timezone": Schema.optionalKey(Schema.String) +}).annotate({ "description": "User location information for web search" }) +export type OpenAIResponsesToolChoice = "auto" | "none" | "required" | { + readonly "type": "function" + readonly "name": string +} | { readonly "type": "web_search_preview_2025_03_11" | "web_search_preview" } +export const OpenAIResponsesToolChoice = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("none"), + Schema.Literal("required"), + Schema.Struct({ "type": Schema.Literal("function"), "name": Schema.String }), + Schema.Struct({ "type": Schema.Literals(["web_search_preview_2025_03_11", "web_search_preview"]) }) +]) +export type OpenAIResponsesPrompt = { readonly "id": string; readonly "variables"?: {} } +export const OpenAIResponsesPrompt = Schema.Struct({ + "id": Schema.String, + "variables": Schema.optionalKey(Schema.Struct({})) +}) +export type OpenAIResponsesReasoningEffort = "xhigh" | "high" | "medium" | "low" | "minimal" | "none" +export const OpenAIResponsesReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low", "minimal", "none"]) +export type ReasoningSummaryVerbosity = "auto" | "concise" | "detailed" +export const ReasoningSummaryVerbosity = Schema.Literals(["auto", "concise", "detailed"]) +export type OpenAIResponsesServiceTier = "auto" | "default" | "flex" | "priority" | "scale" +export const OpenAIResponsesServiceTier = Schema.Literals(["auto", "default", "flex", "priority", "scale"]) +export type OpenAIResponsesTruncation = "auto" | "disabled" +export const OpenAIResponsesTruncation = Schema.Literals(["auto", "disabled"]) +export type ResponsesFormatText = { readonly "type": "text" } +export const ResponsesFormatText = Schema.Struct({ "type": Schema.Literal("text") }).annotate({ + "description": "Plain text response format" +}) +export type ResponsesFormatJSONObject = { readonly "type": "json_object" } +export const ResponsesFormatJSONObject = Schema.Struct({ "type": Schema.Literal("json_object") }).annotate({ + "description": "JSON object response format" +}) +export type ResponsesFormatTextJSONSchemaConfig = { + readonly "type": "json_schema" + readonly "name": string + readonly "description"?: string + readonly "strict"?: boolean + readonly "schema": {} +} +export const ResponsesFormatTextJSONSchemaConfig = Schema.Struct({ + "type": Schema.Literal("json_schema"), + "name": Schema.String, + "description": Schema.optionalKey(Schema.String), + "strict": Schema.optionalKey(Schema.Boolean), + "schema": Schema.Struct({}) +}).annotate({ "description": "JSON schema constrained response format" }) +export type OpenResponsesErrorEvent = { + readonly "type": "error" + readonly "code": string + readonly "message": string + readonly "param": string + readonly "sequence_number": number +} +export const OpenResponsesErrorEvent = Schema.Struct({ + "type": Schema.Literal("error"), + "code": Schema.String, + "message": Schema.String, + "param": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when an error occurs during streaming" }) +export type OpenResponsesTopLogprobs = { readonly "token"?: string; readonly "logprob"?: number } +export const OpenResponsesTopLogprobs = Schema.Struct({ + "token": Schema.optionalKey(Schema.String), + "logprob": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) +}).annotate({ "description": "Alternative token with its log probability" }) +export type OpenResponsesRefusalDeltaEvent = { + readonly "type": "response.refusal.delta" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const OpenResponsesRefusalDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.refusal.delta"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a refusal delta is streamed" }) +export type OpenResponsesRefusalDoneEvent = { + readonly "type": "response.refusal.done" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "refusal": string + readonly "sequence_number": number +} +export const OpenResponsesRefusalDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.refusal.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "refusal": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when refusal streaming is complete" }) +export type OpenResponsesFunctionCallArgumentsDeltaEvent = { + readonly "type": "response.function_call_arguments.delta" + readonly "item_id": string + readonly "output_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const OpenResponsesFunctionCallArgumentsDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.function_call_arguments.delta"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when function call arguments are being streamed" }) +export type OpenResponsesFunctionCallArgumentsDoneEvent = { + readonly "type": "response.function_call_arguments.done" + readonly "item_id": string + readonly "output_index": number + readonly "name": string + readonly "arguments": string + readonly "sequence_number": number +} +export const OpenResponsesFunctionCallArgumentsDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.function_call_arguments.done"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "name": Schema.String, + "arguments": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when function call arguments streaming is complete" }) +export type OpenResponsesReasoningDeltaEvent = { + readonly "type": "response.reasoning_text.delta" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const OpenResponsesReasoningDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_text.delta"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when reasoning text delta is streamed" }) +export type OpenResponsesReasoningDoneEvent = { + readonly "type": "response.reasoning_text.done" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "text": string + readonly "sequence_number": number +} +export const OpenResponsesReasoningDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_text.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "text": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when reasoning text streaming is complete" }) +export type OpenResponsesReasoningSummaryTextDeltaEvent = { + readonly "type": "response.reasoning_summary_text.delta" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const OpenResponsesReasoningSummaryTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_text.delta"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "summary_index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when reasoning summary text delta is streamed" }) +export type OpenResponsesReasoningSummaryTextDoneEvent = { + readonly "type": "response.reasoning_summary_text.done" + readonly "item_id": string + readonly "output_index": number + readonly "summary_index": number + readonly "text": string + readonly "sequence_number": number +} +export const OpenResponsesReasoningSummaryTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_text.done"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "summary_index": Schema.Number.check(Schema.isFinite()), + "text": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when reasoning summary text streaming is complete" }) +export type OpenResponsesImageGenCallInProgress = { + readonly "type": "response.image_generation_call.in_progress" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const OpenResponsesImageGenCallInProgress = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.in_progress"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Image generation call in progress" }) +export type OpenResponsesImageGenCallGenerating = { + readonly "type": "response.image_generation_call.generating" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const OpenResponsesImageGenCallGenerating = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.generating"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Image generation call is generating" }) +export type OpenResponsesImageGenCallPartialImage = { + readonly "type": "response.image_generation_call.partial_image" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "partial_image_b64": string + readonly "partial_image_index": number +} +export const OpenResponsesImageGenCallPartialImage = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.partial_image"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "sequence_number": Schema.Number.check(Schema.isFinite()), + "partial_image_b64": Schema.String, + "partial_image_index": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Image generation call with partial image" }) +export type OpenResponsesImageGenCallCompleted = { + readonly "type": "response.image_generation_call.completed" + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number +} +export const OpenResponsesImageGenCallCompleted = Schema.Struct({ + "type": Schema.Literal("response.image_generation_call.completed"), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isFinite()), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Image generation call completed" }) +export type BadRequestResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const BadRequestResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for BadRequestResponse" }) +export type UnauthorizedResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const UnauthorizedResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for UnauthorizedResponse" }) +export type PaymentRequiredResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const PaymentRequiredResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for PaymentRequiredResponse" }) +export type NotFoundResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const NotFoundResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for NotFoundResponse" }) +export type RequestTimeoutResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const RequestTimeoutResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for RequestTimeoutResponse" }) +export type PayloadTooLargeResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const PayloadTooLargeResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for PayloadTooLargeResponse" }) +export type UnprocessableEntityResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const UnprocessableEntityResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for UnprocessableEntityResponse" }) +export type TooManyRequestsResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const TooManyRequestsResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for TooManyRequestsResponse" }) +export type InternalServerResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const InternalServerResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for InternalServerResponse" }) +export type BadGatewayResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const BadGatewayResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for BadGatewayResponse" }) +export type ServiceUnavailableResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const ServiceUnavailableResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for ServiceUnavailableResponse" }) +export type EdgeNetworkTimeoutResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const EdgeNetworkTimeoutResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for EdgeNetworkTimeoutResponse" }) +export type ProviderOverloadedResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const ProviderOverloadedResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for ProviderOverloadedResponse" }) +export type ResponseInputVideo = { readonly "type": "input_video"; readonly "video_url": string } +export const ResponseInputVideo = Schema.Struct({ + "type": Schema.Literal("input_video"), + "video_url": Schema.String.annotate({ + "description": "A base64 data URL or remote URL that resolves to a video file" + }) +}).annotate({ "description": "Video input content item" }) +export type ResponsesOutputModality = "text" | "image" +export const ResponsesOutputModality = Schema.Literals(["text", "image"]) +export type OpenAIResponsesIncludable = + | "file_search_call.results" + | "message.input_image.image_url" + | "computer_call_output.output.image_url" + | "reasoning.encrypted_content" + | "code_interpreter_call.outputs" +export const OpenAIResponsesIncludable = Schema.Literals([ + "file_search_call.results", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "reasoning.encrypted_content", + "code_interpreter_call.outputs" +]) +export type DataCollection = "deny" | "allow" +export const DataCollection = Schema.Literals(["deny", "allow"]).annotate({ + "description": + "Data collection setting. If no available model provider meets the requirement, your request will return an error.\n- allow: (default) allow providers which store user data non-transiently and may train on it\n\n- deny: use only providers which do not collect user data." +}) +export type ProviderName = + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Amazon Bedrock" + | "Amazon Nova" + | "Anthropic" + | "Arcee AI" + | "AtlasCloud" + | "Avian" + | "Azure" + | "BaseTen" + | "BytePlus" + | "Black Forest Labs" + | "Cerebras" + | "Chutes" + | "Cirrascale" + | "Clarifai" + | "Cloudflare" + | "Cohere" + | "Crusoe" + | "DeepInfra" + | "DeepSeek" + | "Featherless" + | "Fireworks" + | "Friendli" + | "GMICloud" + | "Google" + | "Google AI Studio" + | "Groq" + | "Hyperbolic" + | "Inception" + | "Inceptron" + | "InferenceNet" + | "Infermatic" + | "Io Net" + | "Inflection" + | "Liquid" + | "Mara" + | "Mancer 2" + | "Minimax" + | "ModelRun" + | "Mistral" + | "Modular" + | "Moonshot AI" + | "Morph" + | "NCompass" + | "Nebius" + | "NextBit" + | "Novita" + | "Nvidia" + | "OpenAI" + | "OpenInference" + | "Parasail" + | "Perplexity" + | "Phala" + | "Relace" + | "SambaNova" + | "Seed" + | "SiliconFlow" + | "Sourceful" + | "StepFun" + | "Stealth" + | "StreamLake" + | "Switchpoint" + | "Together" + | "Upstage" + | "Venice" + | "WandB" + | "Xiaomi" + | "xAI" + | "Z.AI" + | "FakeProvider" +export const ProviderName = Schema.Literals([ + "AI21", + "AionLabs", + "Alibaba", + "Ambient", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "Crusoe", + "DeepInfra", + "DeepSeek", + "Featherless", + "Fireworks", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "Hyperbolic", + "Inception", + "Inceptron", + "InferenceNet", + "Infermatic", + "Io Net", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Perplexity", + "Phala", + "Relace", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "StepFun", + "Stealth", + "StreamLake", + "Switchpoint", + "Together", + "Upstage", + "Venice", + "WandB", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" +]) +export type Quantization = "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown" +export const Quantization = Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]) +export type ProviderSort = "price" | "throughput" | "latency" +export const ProviderSort = Schema.Literals(["price", "throughput", "latency"]) +export type BigNumberUnion = string +export const BigNumberUnion = Schema.String.annotate({ "description": "Price per million prompt tokens" }) +export type PercentileThroughputCutoffs = { + readonly "p50"?: number + readonly "p75"?: number + readonly "p90"?: number + readonly "p99"?: number +} +export const PercentileThroughputCutoffs = Schema.Struct({ + "p50": Schema.optionalKey( + Schema.Number.annotate({ "description": "Minimum p50 throughput (tokens/sec)" }).check(Schema.isFinite()) + ), + "p75": Schema.optionalKey( + Schema.Number.annotate({ "description": "Minimum p75 throughput (tokens/sec)" }).check(Schema.isFinite()) + ), + "p90": Schema.optionalKey( + Schema.Number.annotate({ "description": "Minimum p90 throughput (tokens/sec)" }).check(Schema.isFinite()) + ), + "p99": Schema.optionalKey( + Schema.Number.annotate({ "description": "Minimum p99 throughput (tokens/sec)" }).check(Schema.isFinite()) + ) +}).annotate({ + "description": + "Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred." +}) +export type PercentileLatencyCutoffs = { + readonly "p50"?: number + readonly "p75"?: number + readonly "p90"?: number + readonly "p99"?: number +} +export const PercentileLatencyCutoffs = Schema.Struct({ + "p50": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum p50 latency (seconds)" }).check(Schema.isFinite()) + ), + "p75": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum p75 latency (seconds)" }).check(Schema.isFinite()) + ), + "p90": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum p90 latency (seconds)" }).check(Schema.isFinite()) + ), + "p99": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum p99 latency (seconds)" }).check(Schema.isFinite()) + ) +}).annotate({ + "description": "Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred." +}) +export type WebSearchEngine = "native" | "exa" +export const WebSearchEngine = Schema.Literals(["native", "exa"]).annotate({ + "description": "The search engine to use for web search." +}) +export type PDFParserEngine = "mistral-ocr" | "pdf-text" | "native" +export const PDFParserEngine = Schema.Literals(["mistral-ocr", "pdf-text", "native"]).annotate({ + "description": "The engine to use for parsing PDF files." +}) +export type AnthropicMessagesResponse = { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray< + | { + readonly "type": "text" + readonly "text": string + readonly "citations": ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + readonly "file_id": string + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + readonly "file_id": string + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + readonly "file_id": string + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + } + | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } + | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } + | { readonly "type": "redacted_thinking"; readonly "data": string } + | { + readonly "type": "server_tool_use" + readonly "id": string + readonly "name": "web_search" + readonly "input"?: unknown + } + | { + readonly "type": "web_search_tool_result" + readonly "tool_use_id": string + readonly "content": + | ReadonlyArray< + { + readonly "type": "web_search_result" + readonly "encrypted_content": string + readonly "page_age": string + readonly "title": string + readonly "url": string + } + > + | { + readonly "type": "web_search_tool_result_error" + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + } + } + > + readonly "model": string + readonly "stop_reason": "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" + readonly "stop_sequence": string + readonly "usage": { + readonly "input_tokens": number + readonly "output_tokens": number + readonly "cache_creation_input_tokens": number + readonly "cache_read_input_tokens": number + readonly "cache_creation": { + readonly "ephemeral_5m_input_tokens": number + readonly "ephemeral_1h_input_tokens": number + } + readonly "inference_geo": string + readonly "server_tool_use": { readonly "web_search_requests": number } + readonly "service_tier": "standard" | "priority" | "batch" + } +} +export const AnthropicMessagesResponse = Schema.Struct({ + "id": Schema.String, + "type": Schema.Literal("message"), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" })) + }), + Schema.Struct({ + "type": Schema.Literal("tool_use"), + "id": Schema.String, + "name": Schema.String, + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), + Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + Schema.Struct({ + "type": Schema.Literal("server_tool_use"), + "id": Schema.String, + "name": Schema.Literal("web_search"), + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result"), + "tool_use_id": Schema.String, + "content": Schema.Union([ + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("web_search_result"), + "encrypted_content": Schema.String, + "page_age": Schema.String, + "title": Schema.String, + "url": Schema.String + }) + ), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result_error"), + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ]) + }) + ]) + }) + ], { mode: "oneOf" })), + "model": Schema.String, + "stop_reason": Schema.Literals(["end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"]), + "stop_sequence": Schema.String, + "usage": Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isFinite()), + "output_tokens": Schema.Number.check(Schema.isFinite()), + "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), + "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), + "cache_creation": Schema.Struct({ + "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isFinite()), + "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isFinite()) + }), + "inference_geo": Schema.String, + "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }), + "service_tier": Schema.Literals(["standard", "priority", "batch"]) + }) +}).annotate({ "description": "Non-streaming response from the Anthropic Messages API with OpenRouter extensions" }) +export type AnthropicMessagesStreamEvent = + | { + readonly "type": "message_start" + readonly "message": { + readonly "id": string + readonly "type": "message" + readonly "role": "assistant" + readonly "content": ReadonlyArray< + | { + readonly "type": "text" + readonly "text": string + readonly "citations": ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + readonly "file_id": string + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + readonly "file_id": string + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + readonly "file_id": string + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + } + | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } + | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } + | { readonly "type": "redacted_thinking"; readonly "data": string } + | { + readonly "type": "server_tool_use" + readonly "id": string + readonly "name": "web_search" + readonly "input"?: unknown + } + | { + readonly "type": "web_search_tool_result" + readonly "tool_use_id": string + readonly "content": + | ReadonlyArray< + { + readonly "type": "web_search_result" + readonly "encrypted_content": string + readonly "page_age": string + readonly "title": string + readonly "url": string + } + > + | { + readonly "type": "web_search_tool_result_error" + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + } + } + > + readonly "model": string + readonly "stop_reason": unknown + readonly "stop_sequence": unknown + readonly "usage": { + readonly "input_tokens": number + readonly "output_tokens": number + readonly "cache_creation_input_tokens": number + readonly "cache_read_input_tokens": number + readonly "cache_creation": { + readonly "ephemeral_5m_input_tokens": number + readonly "ephemeral_1h_input_tokens": number + } + readonly "inference_geo": string + readonly "server_tool_use": { readonly "web_search_requests": number } + readonly "service_tier": "standard" | "priority" | "batch" + } + } + } + | { + readonly "type": "message_delta" + readonly "delta": { + readonly "stop_reason": "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" + readonly "stop_sequence": string + } + readonly "usage": { + readonly "input_tokens": number + readonly "output_tokens": number + readonly "cache_creation_input_tokens": number + readonly "cache_read_input_tokens": number + readonly "server_tool_use": { readonly "web_search_requests": number } + } + } + | { readonly "type": "message_stop" } + | { + readonly "type": "content_block_start" + readonly "index": number + readonly "content_block": + | { + readonly "type": "text" + readonly "text": string + readonly "citations": ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + readonly "file_id": string + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + readonly "file_id": string + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + readonly "file_id": string + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + } + | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } + | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } + | { readonly "type": "redacted_thinking"; readonly "data": string } + | { + readonly "type": "server_tool_use" + readonly "id": string + readonly "name": "web_search" + readonly "input"?: unknown + } + | { + readonly "type": "web_search_tool_result" + readonly "tool_use_id": string + readonly "content": + | ReadonlyArray< + { + readonly "type": "web_search_result" + readonly "encrypted_content": string + readonly "page_age": string + readonly "title": string + readonly "url": string + } + > + | { + readonly "type": "web_search_tool_result_error" + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + } + } + } + | { + readonly "type": "content_block_delta" + readonly "index": number + readonly "delta": + | { readonly "type": "text_delta"; readonly "text": string } + | { readonly "type": "input_json_delta"; readonly "partial_json": string } + | { readonly "type": "thinking_delta"; readonly "thinking": string } + | { readonly "type": "signature_delta"; readonly "signature": string } + | { + readonly "type": "citations_delta" + readonly "citation": { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + readonly "file_id": string + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + readonly "file_id": string + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + readonly "file_id": string + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + } + } + | { readonly "type": "content_block_stop"; readonly "index": number } + | { readonly "type": "ping" } + | { readonly "type": "error"; readonly "error": { readonly "type": string; readonly "message": string } } +export const AnthropicMessagesStreamEvent = Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("message_start"), + "message": Schema.Struct({ + "id": Schema.String, + "type": Schema.Literal("message"), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" })) + }), + Schema.Struct({ + "type": Schema.Literal("tool_use"), + "id": Schema.String, + "name": Schema.String, + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), + Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + Schema.Struct({ + "type": Schema.Literal("server_tool_use"), + "id": Schema.String, + "name": Schema.Literal("web_search"), + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result"), + "tool_use_id": Schema.String, + "content": Schema.Union([ + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("web_search_result"), + "encrypted_content": Schema.String, + "page_age": Schema.String, + "title": Schema.String, + "url": Schema.String + }) + ), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result_error"), + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ]) + }) + ]) + }) + ], { mode: "oneOf" })), + "model": Schema.String, + "stop_reason": Schema.Unknown, + "stop_sequence": Schema.Unknown, + "usage": Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isFinite()), + "output_tokens": Schema.Number.check(Schema.isFinite()), + "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), + "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), + "cache_creation": Schema.Struct({ + "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isFinite()), + "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isFinite()) + }), + "inference_geo": Schema.String, + "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }), + "service_tier": Schema.Literals(["standard", "priority", "batch"]) + }) + }) + }), + Schema.Struct({ + "type": Schema.Literal("message_delta"), + "delta": Schema.Struct({ + "stop_reason": Schema.Literals(["end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"]), + "stop_sequence": Schema.String + }), + "usage": Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isFinite()), + "output_tokens": Schema.Number.check(Schema.isFinite()), + "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), + "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), + "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }) + }) + }), + Schema.Struct({ "type": Schema.Literal("message_stop") }), + Schema.Struct({ + "type": Schema.Literal("content_block_start"), + "index": Schema.Number.check(Schema.isFinite()), + "content_block": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" })) + }), + Schema.Struct({ + "type": Schema.Literal("tool_use"), + "id": Schema.String, + "name": Schema.String, + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), + Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + Schema.Struct({ + "type": Schema.Literal("server_tool_use"), + "id": Schema.String, + "name": Schema.Literal("web_search"), + "input": Schema.optionalKey(Schema.Unknown) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result"), + "tool_use_id": Schema.String, + "content": Schema.Union([ + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("web_search_result"), + "encrypted_content": Schema.String, + "page_age": Schema.String, + "title": Schema.String, + "url": Schema.String + }) + ), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result_error"), + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ]) + }) + ]) + }) + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_delta"), + "index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.Union([ + Schema.Struct({ "type": Schema.Literal("text_delta"), "text": Schema.String }), + Schema.Struct({ "type": Schema.Literal("input_json_delta"), "partial_json": Schema.String }), + Schema.Struct({ "type": Schema.Literal("thinking_delta"), "thinking": Schema.String }), + Schema.Struct({ "type": Schema.Literal("signature_delta"), "signature": Schema.String }), + Schema.Struct({ + "type": Schema.Literal("citations_delta"), + "citation": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }) + }) + ], { mode: "oneOf" }) + }), + Schema.Struct({ "type": Schema.Literal("content_block_stop"), "index": Schema.Number.check(Schema.isFinite()) }), + Schema.Struct({ "type": Schema.Literal("ping") }), + Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) + }) +], { mode: "oneOf" }).annotate({ "description": "Union of all possible streaming events" }) +export type OpenRouterAnthropicMessageParam = { + readonly "role": "user" | "assistant" + readonly "content": + | string + | ReadonlyArray< + | { + readonly "type": "text" + readonly "text": string + readonly "citations"?: ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "image" + readonly "source": { + readonly "type": "base64" + readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" + readonly "data": string + } | { readonly "type": "url"; readonly "url": string } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "document" + readonly "source": + | { readonly "type": "base64"; readonly "media_type": "application/pdf"; readonly "data": string } + | { readonly "type": "text"; readonly "media_type": "text/plain"; readonly "data": string } + | { + readonly "type": "content" + readonly "content": + | string + | ReadonlyArray< + { + readonly "type": "text" + readonly "text": string + readonly "citations"?: ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } | { + readonly "type": "image" + readonly "source": { + readonly "type": "base64" + readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" + readonly "data": string + } | { readonly "type": "url"; readonly "url": string } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > + } + | { readonly "type": "url"; readonly "url": string } + readonly "citations"?: { readonly "enabled"?: boolean } + readonly "context"?: string + readonly "title"?: string + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "tool_use" + readonly "id": string + readonly "name": string + readonly "input"?: unknown + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "tool_result" + readonly "tool_use_id": string + readonly "content"?: + | string + | ReadonlyArray< + { + readonly "type": "text" + readonly "text": string + readonly "citations"?: ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } | { + readonly "type": "image" + readonly "source": { + readonly "type": "base64" + readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" + readonly "data": string + } | { readonly "type": "url"; readonly "url": string } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > + readonly "is_error"?: boolean + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } + | { readonly "type": "redacted_thinking"; readonly "data": string } + | { + readonly "type": "server_tool_use" + readonly "id": string + readonly "name": "web_search" + readonly "input"?: unknown + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "web_search_tool_result" + readonly "tool_use_id": string + readonly "content": + | ReadonlyArray< + { + readonly "type": "web_search_result" + readonly "encrypted_content": string + readonly "title": string + readonly "url": string + readonly "page_age"?: string + } + > + | { + readonly "type": "web_search_tool_result_error" + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + | { + readonly "type": "search_result" + readonly "source": string + readonly "title": string + readonly "content": ReadonlyArray< + { + readonly "type": "text" + readonly "text": string + readonly "citations"?: ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > + readonly "citations"?: { readonly "enabled"?: boolean } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > +} +export const OpenRouterAnthropicMessageParam = Schema.Struct({ + "role": Schema.Literals(["user", "assistant"]), + "content": Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }))), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("image"), + "source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("base64"), + "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), + "data": Schema.String + }), + Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) + ], { mode: "oneOf" }), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("document"), + "source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("base64"), + "media_type": Schema.Literal("application/pdf"), + "data": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("text"), + "media_type": Schema.Literal("text/plain"), + "data": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("content"), + "content": Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }))), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("image"), + "source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("base64"), + "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), + "data": Schema.String + }), + Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) + ], { mode: "oneOf" }), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }) + ], { mode: "oneOf" })) + ]) + }), + Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) + ], { mode: "oneOf" }), + "citations": Schema.optionalKey(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) })), + "context": Schema.optionalKey(Schema.String), + "title": Schema.optionalKey(Schema.String), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("tool_use"), + "id": Schema.String, + "name": Schema.String, + "input": Schema.optionalKey(Schema.Unknown), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("tool_result"), + "tool_use_id": Schema.String, + "content": Schema.optionalKey(Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }))), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("image"), + "source": Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("base64"), + "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), + "data": Schema.String + }), + Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) + ], { mode: "oneOf" }), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }) + ])) + ])), + "is_error": Schema.optionalKey(Schema.Boolean), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), + Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + Schema.Struct({ + "type": Schema.Literal("server_tool_use"), + "id": Schema.String, + "name": Schema.Literal("web_search"), + "input": Schema.optionalKey(Schema.Unknown), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result"), + "tool_use_id": Schema.String, + "content": Schema.Union([ + Schema.Array( + Schema.Struct({ + "type": Schema.Literal("web_search_result"), + "encrypted_content": Schema.String, + "title": Schema.String, + "url": Schema.String, + "page_age": Schema.optionalKey(Schema.String) + }) + ), + Schema.Struct({ + "type": Schema.Literal("web_search_tool_result_error"), + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ]) + }) + ]), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("search_result"), + "source": Schema.String, + "title": Schema.String, + "content": Schema.Array(Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }))), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + })), + "citations": Schema.optionalKey(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) })), + "cache_control": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) + }) + ) + }) + ], { mode: "oneOf" })) + ]) +}).annotate({ "description": "Anthropic message with OpenRouter extensions" }) +export type AnthropicOutputConfig = { readonly "effort"?: "low" | "medium" | "high" | "max" } +export const AnthropicOutputConfig = Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Literals(["low", "medium", "high", "max"]).annotate({ + "description": + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer. Valid values are `low`, `medium`, `high`, or `max`." + }) + ) +}).annotate({ + "description": + "Configuration for controlling output behavior. Currently supports the effort parameter for Claude Opus 4.5." +}) +export type ActivityItem = { + readonly "date": string + readonly "model": string + readonly "model_permaslug": string + readonly "endpoint_id": string + readonly "provider_name": string + readonly "usage": number + readonly "byok_usage_inference": number + readonly "requests": number + readonly "prompt_tokens": number + readonly "completion_tokens": number + readonly "reasoning_tokens": number +} +export const ActivityItem = Schema.Struct({ + "date": Schema.String.annotate({ "description": "Date of the activity (YYYY-MM-DD format)" }), + "model": Schema.String.annotate({ "description": "Model slug (e.g., \"openai/gpt-4.1\")" }), + "model_permaslug": Schema.String.annotate({ "description": "Model permaslug (e.g., \"openai/gpt-4.1-2025-04-14\")" }), + "endpoint_id": Schema.String.annotate({ "description": "Unique identifier for the endpoint" }), + "provider_name": Schema.String.annotate({ "description": "Name of the provider serving this endpoint" }), + "usage": Schema.Number.annotate({ "description": "Total cost in USD (OpenRouter credits spent)" }).check( + Schema.isFinite() + ), + "byok_usage_inference": Schema.Number.annotate({ + "description": "BYOK inference cost in USD (external credits spent)" + }).check(Schema.isFinite()), + "requests": Schema.Number.annotate({ "description": "Number of requests made" }).check(Schema.isFinite()), + "prompt_tokens": Schema.Number.annotate({ "description": "Total prompt tokens used" }).check(Schema.isFinite()), + "completion_tokens": Schema.Number.annotate({ "description": "Total completion tokens generated" }).check( + Schema.isFinite() + ), + "reasoning_tokens": Schema.Number.annotate({ "description": "Total reasoning tokens used" }).check(Schema.isFinite()) +}) +export type ForbiddenResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: {} +} +export const ForbiddenResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt()), + "message": Schema.String, + "metadata": Schema.optionalKey(Schema.Struct({})) +}).annotate({ "description": "Error data for ForbiddenResponse" }) +export type CreateChargeRequest = { + readonly "amount": number + readonly "sender": string + readonly "chain_id": 1 | 137 | 8453 +} +export const CreateChargeRequest = Schema.Struct({ + "amount": Schema.Number.check(Schema.isFinite()), + "sender": Schema.String, + "chain_id": Schema.Literals([1, 137, 8453]) +}).annotate({ "description": "Create a Coinbase charge for crypto payment" }) +export type PublicPricing = { + readonly "prompt": string + readonly "completion": string + readonly "request"?: string + readonly "image"?: string + readonly "image_token"?: string + readonly "image_output"?: string + readonly "audio"?: string + readonly "audio_output"?: string + readonly "input_audio_cache"?: string + readonly "web_search"?: string + readonly "internal_reasoning"?: string + readonly "input_cache_read"?: string + readonly "input_cache_write"?: string + readonly "discount"?: number +} +export const PublicPricing = Schema.Struct({ + "prompt": Schema.String.annotate({ "description": "A number or string value representing a large number" }), + "completion": Schema.String.annotate({ "description": "A number or string value representing a large number" }), + "request": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image_token": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image_output": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "audio": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "audio_output": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_audio_cache": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "web_search": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "internal_reasoning": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_cache_read": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_cache_write": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "discount": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) +}).annotate({ "description": "Pricing information for the model" }) +export type ModelGroup = + | "Router" + | "Media" + | "Other" + | "GPT" + | "Claude" + | "Gemini" + | "Grok" + | "Cohere" + | "Nova" + | "Qwen" + | "Yi" + | "DeepSeek" + | "Mistral" + | "Llama2" + | "Llama3" + | "Llama4" + | "PaLM" + | "RWKV" + | "Qwen3" +export const ModelGroup = Schema.Literals([ + "Router", + "Media", + "Other", + "GPT", + "Claude", + "Gemini", + "Grok", + "Cohere", + "Nova", + "Qwen", + "Yi", + "DeepSeek", + "Mistral", + "Llama2", + "Llama3", + "Llama4", + "PaLM", + "RWKV", + "Qwen3" +]).annotate({ "description": "Tokenizer type used by the model" }) +export type InputModality = "text" | "image" | "file" | "audio" | "video" +export const InputModality = Schema.Literals(["text", "image", "file", "audio", "video"]) +export type OutputModality = "text" | "image" | "embeddings" | "audio" +export const OutputModality = Schema.Literals(["text", "image", "embeddings", "audio"]) +export type TopProviderInfo = { + readonly "context_length"?: number + readonly "max_completion_tokens"?: number + readonly "is_moderated": boolean +} +export const TopProviderInfo = Schema.Struct({ + "context_length": Schema.optionalKey( + Schema.Number.annotate({ "description": "Context length from the top provider" }).check(Schema.isFinite()) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum completion tokens from the top provider" }).check( + Schema.isFinite() + ) + ), + "is_moderated": Schema.Boolean.annotate({ "description": "Whether the top provider moderates content" }) +}).annotate({ "description": "Information about the top provider for this model" }) +export type PerRequestLimits = { readonly "prompt_tokens": number; readonly "completion_tokens": number } +export const PerRequestLimits = Schema.Struct({ + "prompt_tokens": Schema.Number.annotate({ "description": "Maximum prompt tokens per request" }).check( + Schema.isFinite() + ), + "completion_tokens": Schema.Number.annotate({ "description": "Maximum completion tokens per request" }).check( + Schema.isFinite() + ) +}).annotate({ "description": "Per-request token limits" }) +export type Parameter = + | "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 const Parameter = Schema.Literals([ + "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 type DefaultParameters = { + readonly "temperature"?: number + readonly "top_p"?: number + readonly "frequency_penalty"?: number +} +export const DefaultParameters = Schema.Struct({ + "temperature": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)) + ), + "top_p": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + ), + "frequency_penalty": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) + ) +}).annotate({ "description": "Default parameters for this model" }) +export type ModelsCountResponse = { readonly "data": { readonly "count": number } } +export const ModelsCountResponse = Schema.Struct({ + "data": Schema.Struct({ + "count": Schema.Number.annotate({ "description": "Total number of available models" }).check(Schema.isFinite()) + }).annotate({ "description": "Model count data" }) +}).annotate({ "description": "Model count data" }) +export type EndpointStatus = 0 | -1 | -2 | -3 | -5 | -10 +export const EndpointStatus = Schema.Literals([0, -1, -2, -3, -5, -10]) +export type PercentileStats = { + readonly "p50": number + readonly "p75": number + readonly "p90": number + readonly "p99": number +} +export const PercentileStats = Schema.Struct({ + "p50": Schema.Number.annotate({ "description": "Median (50th percentile)" }).check(Schema.isFinite()), + "p75": Schema.Number.annotate({ "description": "75th percentile" }).check(Schema.isFinite()), + "p90": Schema.Number.annotate({ "description": "90th percentile" }).check(Schema.isFinite()), + "p99": Schema.Number.annotate({ "description": "99th percentile" }).check(Schema.isFinite()) +}).annotate({ + "description": + "Latency percentiles in milliseconds over the last 30 minutes. Latency measures time to first token. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests." +}) +export type __schema5 = ReadonlyArray< + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Amazon Bedrock" + | "Amazon Nova" + | "Anthropic" + | "Arcee AI" + | "AtlasCloud" + | "Avian" + | "Azure" + | "BaseTen" + | "BytePlus" + | "Black Forest Labs" + | "Cerebras" + | "Chutes" + | "Cirrascale" + | "Clarifai" + | "Cloudflare" + | "Cohere" + | "Crusoe" + | "DeepInfra" + | "DeepSeek" + | "Featherless" + | "Fireworks" + | "Friendli" + | "GMICloud" + | "Google" + | "Google AI Studio" + | "Groq" + | "Hyperbolic" + | "Inception" + | "Inceptron" + | "InferenceNet" + | "Infermatic" + | "Io Net" + | "Inflection" + | "Liquid" + | "Mara" + | "Mancer 2" + | "Minimax" + | "ModelRun" + | "Mistral" + | "Modular" + | "Moonshot AI" + | "Morph" + | "NCompass" + | "Nebius" + | "NextBit" + | "Novita" + | "Nvidia" + | "OpenAI" + | "OpenInference" + | "Parasail" + | "Perplexity" + | "Phala" + | "Relace" + | "SambaNova" + | "Seed" + | "SiliconFlow" + | "Sourceful" + | "StepFun" + | "Stealth" + | "StreamLake" + | "Switchpoint" + | "Together" + | "Upstage" + | "Venice" + | "WandB" + | "Xiaomi" + | "xAI" + | "Z.AI" + | "FakeProvider" + | string +> +export const __schema5 = Schema.Array( + Schema.Union([ + Schema.Literals([ + "AI21", + "AionLabs", + "Alibaba", + "Ambient", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "Crusoe", + "DeepInfra", + "DeepSeek", + "Featherless", + "Fireworks", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "Hyperbolic", + "Inception", + "Inceptron", + "InferenceNet", + "Infermatic", + "Io Net", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Perplexity", + "Phala", + "Relace", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "StepFun", + "Stealth", + "StreamLake", + "Switchpoint", + "Together", + "Upstage", + "Venice", + "WandB", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" + ]), + Schema.String + ]) +) +export type __schema11 = number +export const __schema11 = Schema.Number.check(Schema.isFinite()) +export type __schema13 = unknown +export const __schema13 = Schema.Unknown +export type __schema21 = string | null +export const __schema21 = Schema.Union([Schema.String, Schema.Null]) +export type __schema22 = + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + | null +export const __schema22 = Schema.Union([ + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]), + Schema.Null +]) +export type ModelName = string +export const ModelName = Schema.String +export type ChatMessageContentItemImage = { + readonly "type": "image_url" + readonly "image_url": { readonly "url": string; readonly "detail"?: "auto" | "low" | "high" } +} +export const ChatMessageContentItemImage = Schema.Struct({ + "type": Schema.Literal("image_url"), + "image_url": Schema.Struct({ + "url": Schema.String, + "detail": Schema.optionalKey(Schema.Literals(["auto", "low", "high"])) + }) +}) +export type ChatMessageContentItemAudio = { + readonly "type": "input_audio" + readonly "input_audio": { readonly "data": string; readonly "format": string } +} +export const ChatMessageContentItemAudio = Schema.Struct({ + "type": Schema.Literal("input_audio"), + "input_audio": Schema.Struct({ "data": Schema.String, "format": Schema.String }) +}) +export type ChatMessageContentItemVideo = { + readonly "type": "input_video" + readonly "video_url": { readonly "url": string } +} | { readonly "type": "video_url"; readonly "video_url": { readonly "url": string } } +export const ChatMessageContentItemVideo = Schema.Union([ + Schema.Struct({ "type": Schema.Literal("input_video"), "video_url": Schema.Struct({ "url": Schema.String }) }), + Schema.Struct({ "type": Schema.Literal("video_url"), "video_url": Schema.Struct({ "url": Schema.String }) }) +], { mode: "oneOf" }) +export type ChatMessageToolCall = { + readonly "id": string + readonly "type": "function" + readonly "function": { readonly "name": string; readonly "arguments": string } +} +export const ChatMessageToolCall = Schema.Struct({ + "id": Schema.String, + "type": Schema.Literal("function"), + "function": Schema.Struct({ "name": Schema.String, "arguments": Schema.String }) +}) +export type ChatMessageTokenLogprob = { + readonly "token": string + readonly "logprob": number + readonly "bytes": ReadonlyArray | null + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "logprob": number; readonly "bytes": ReadonlyArray | null } + > +} +export const ChatMessageTokenLogprob = Schema.Struct({ + "token": Schema.String, + "logprob": Schema.Number.check(Schema.isFinite()), + "bytes": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.Null]), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "logprob": Schema.Number.check(Schema.isFinite()), + "bytes": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.Null]) + }) + ) +}) +export type ChatGenerationTokenUsage = { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly "total_tokens": number + readonly "completion_tokens_details"?: { + readonly "reasoning_tokens"?: number | null + readonly "audio_tokens"?: number | null + readonly "accepted_prediction_tokens"?: number | null + readonly "rejected_prediction_tokens"?: number | null + } | null + readonly "prompt_tokens_details"?: { + readonly "cached_tokens"?: number + readonly "cache_write_tokens"?: number + readonly "audio_tokens"?: number + readonly "video_tokens"?: number + } | null +} +export const ChatGenerationTokenUsage = Schema.Struct({ + "completion_tokens": Schema.Number.check(Schema.isFinite()), + "prompt_tokens": Schema.Number.check(Schema.isFinite()), + "total_tokens": Schema.Number.check(Schema.isFinite()), + "completion_tokens_details": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "reasoning_tokens": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "audio_tokens": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "accepted_prediction_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]) + ), + "rejected_prediction_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]) + ) + }), + Schema.Null + ])), + "prompt_tokens_details": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "cached_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "cache_write_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "audio_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "video_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }), + Schema.Null + ])) +}) +export type ChatCompletionFinishReason = "tool_calls" | "stop" | "length" | "content_filter" | "error" +export const ChatCompletionFinishReason = Schema.Literals(["tool_calls", "stop", "length", "content_filter", "error"]) +export type JSONSchemaConfig = { + readonly "name": string + readonly "description"?: string + readonly "schema"?: {} + readonly "strict"?: boolean | null +} +export const JSONSchemaConfig = Schema.Struct({ + "name": Schema.String.check(Schema.isMaxLength(64)), + "description": Schema.optionalKey(Schema.String), + "schema": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) +}) +export type ResponseFormatTextGrammar = { readonly "type": "grammar"; readonly "grammar": string } +export const ResponseFormatTextGrammar = Schema.Struct({ "type": Schema.Literal("grammar"), "grammar": Schema.String }) +export type ChatMessageContentItemCacheControl = { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } +export const ChatMessageContentItemCacheControl = Schema.Struct({ + "type": Schema.Literal("ephemeral"), + "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) +}) +export type NamedToolChoice = { readonly "type": "function"; readonly "function": { readonly "name": string } } +export const NamedToolChoice = Schema.Struct({ + "type": Schema.Literal("function"), + "function": Schema.Struct({ "name": Schema.String }) +}) +export type ChatStreamOptions = { readonly "include_usage"?: boolean } +export const ChatStreamOptions = Schema.Struct({ "include_usage": Schema.optionalKey(Schema.Boolean) }) +export type ChatStreamingMessageToolCall = { + readonly "index": number + readonly "id"?: string | null + readonly "type"?: "function" | null + readonly "function"?: { readonly "name"?: string | null; readonly "arguments"?: string } +} +export const ChatStreamingMessageToolCall = Schema.Struct({ + "index": Schema.Number.check(Schema.isFinite()), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.optionalKey(Schema.Union([Schema.Literal("function"), Schema.Null])), + "function": Schema.optionalKey( + Schema.Struct({ + "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "arguments": Schema.optionalKey(Schema.String) + }) + ) +}) +export type ChatError = { + readonly "error": { + readonly "code": string | number | null + readonly "message": string + readonly "param"?: string | null + readonly "type"?: string | null + } +} +export const ChatError = Schema.Struct({ + "error": Schema.Struct({ + "code": Schema.Union([Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())]), Schema.Null]), + "message": Schema.String, + "param": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }) +}) +export type OpenAIResponsesAnnotation = FileCitation | URLCitation | FilePath +export const OpenAIResponsesAnnotation = Schema.Union([FileCitation, URLCitation, FilePath]) +export type OutputItemReasoning = { + readonly "type": "reasoning" + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" +} +export const OutputItemReasoning = Schema.Struct({ + "type": Schema.Literal("reasoning"), + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) +}) +export type ResponsesOutputItemReasoning = { + readonly "type": "reasoning" + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" +} +export const ResponsesOutputItemReasoning = Schema.Struct({ + "type": Schema.Literal("reasoning"), + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ) +}).annotate({ "description": "An output item containing reasoning" }) +export type OpenResponsesReasoningSummaryPartAddedEvent = { + readonly "type": "response.reasoning_summary_part.added" + readonly "output_index": number + readonly "item_id": string + readonly "summary_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number +} +export const OpenResponsesReasoningSummaryPartAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_part.added"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "summary_index": Schema.Number.check(Schema.isFinite()), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a reasoning summary part is added" }) +export type OpenResponsesReasoningSummaryPartDoneEvent = { + readonly "type": "response.reasoning_summary_part.done" + readonly "output_index": number + readonly "item_id": string + readonly "summary_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number +} +export const OpenResponsesReasoningSummaryPartDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.reasoning_summary_part.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "summary_index": Schema.Number.check(Schema.isFinite()), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a reasoning summary part is complete" }) +export type OpenResponsesReasoning = { + readonly "type": "reasoning" + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" +} +export const OpenResponsesReasoning = Schema.Struct({ + "type": Schema.Literal("reasoning"), + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "signature": Schema.optionalKey(Schema.String), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]) + ) +}).annotate({ "description": "Reasoning output item with signature and format extensions" }) +export type OutputItemWebSearchCall = { + readonly "type": "web_search_call" + readonly "id": string + readonly "status": WebSearchStatus +} +export const OutputItemWebSearchCall = Schema.Struct({ + "type": Schema.Literal("web_search_call"), + "id": Schema.String, + "status": WebSearchStatus +}) +export type ResponsesWebSearchCallOutput = { + readonly "type": "web_search_call" + readonly "id": string + readonly "status": WebSearchStatus +} +export const ResponsesWebSearchCallOutput = Schema.Struct({ + "type": Schema.Literal("web_search_call"), + "id": Schema.String, + "status": WebSearchStatus +}) +export type OutputItemFileSearchCall = { + readonly "type": "file_search_call" + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus +} +export const OutputItemFileSearchCall = Schema.Struct({ + "type": Schema.Literal("file_search_call"), + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus +}) +export type ResponsesOutputItemFileSearchCall = { + readonly "type": "file_search_call" + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus +} +export const ResponsesOutputItemFileSearchCall = Schema.Struct({ + "type": Schema.Literal("file_search_call"), + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus +}) +export type OutputItemImageGenerationCall = { + readonly "type": "image_generation_call" + readonly "id": string + readonly "result"?: string + readonly "status": ImageGenerationStatus +} +export const OutputItemImageGenerationCall = Schema.Struct({ + "type": Schema.Literal("image_generation_call"), + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": ImageGenerationStatus +}) +export type ResponsesImageGenerationCall = { + readonly "type": "image_generation_call" + readonly "id": string + readonly "result"?: string + readonly "status": ImageGenerationStatus +} +export const ResponsesImageGenerationCall = Schema.Struct({ + "type": Schema.Literal("image_generation_call"), + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": ImageGenerationStatus +}) +export type OpenResponsesFunctionToolCall = { + readonly "type": "function_call" + readonly "call_id": string + readonly "name": string + readonly "arguments": string + readonly "id": string + readonly "status"?: ToolCallStatus +} +export const OpenResponsesFunctionToolCall = Schema.Struct({ + "type": Schema.Literal("function_call"), + "call_id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "id": Schema.String, + "status": Schema.optionalKey(ToolCallStatus) +}).annotate({ "description": "A function call initiated by the model" }) +export type OpenResponsesFunctionCallOutput = { + readonly "type": "function_call_output" + readonly "id"?: string + readonly "call_id": string + readonly "output": string + readonly "status"?: ToolCallStatus +} +export const OpenResponsesFunctionCallOutput = Schema.Struct({ + "type": Schema.Literal("function_call_output"), + "id": Schema.optionalKey(Schema.String), + "call_id": Schema.String, + "output": Schema.String, + "status": Schema.optionalKey(ToolCallStatus) +}).annotate({ "description": "The output from a function call execution" }) +export type OpenResponsesWebSearchPreviewTool = { + readonly "type": "web_search_preview" + readonly "search_context_size"?: ResponsesSearchContextSize + readonly "user_location"?: WebSearchPreviewToolUserLocation +} +export const OpenResponsesWebSearchPreviewTool = Schema.Struct({ + "type": Schema.Literal("web_search_preview"), + "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), + "user_location": Schema.optionalKey(WebSearchPreviewToolUserLocation) +}).annotate({ "description": "Web search preview tool configuration" }) +export type OpenResponsesWebSearchPreview20250311Tool = { + readonly "type": "web_search_preview_2025_03_11" + readonly "search_context_size"?: ResponsesSearchContextSize + readonly "user_location"?: WebSearchPreviewToolUserLocation +} +export const OpenResponsesWebSearchPreview20250311Tool = Schema.Struct({ + "type": Schema.Literal("web_search_preview_2025_03_11"), + "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), + "user_location": Schema.optionalKey(WebSearchPreviewToolUserLocation) +}).annotate({ "description": "Web search preview tool configuration (2025-03-11 version)" }) +export type OpenResponsesWebSearchTool = { + readonly "type": "web_search" + readonly "filters"?: { readonly "allowed_domains"?: ReadonlyArray } + readonly "search_context_size"?: ResponsesSearchContextSize + readonly "user_location"?: ResponsesWebSearchUserLocation +} +export const OpenResponsesWebSearchTool = Schema.Struct({ + "type": Schema.Literal("web_search"), + "filters": Schema.optionalKey(Schema.Struct({ "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)) })), + "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), + "user_location": Schema.optionalKey(ResponsesWebSearchUserLocation) +}).annotate({ "description": "Web search tool configuration" }) +export type OpenResponsesWebSearch20250826Tool = { + readonly "type": "web_search_2025_08_26" + readonly "filters"?: { readonly "allowed_domains"?: ReadonlyArray } + readonly "search_context_size"?: ResponsesSearchContextSize + readonly "user_location"?: ResponsesWebSearchUserLocation +} +export const OpenResponsesWebSearch20250826Tool = Schema.Struct({ + "type": Schema.Literal("web_search_2025_08_26"), + "filters": Schema.optionalKey(Schema.Struct({ "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)) })), + "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), + "user_location": Schema.optionalKey(ResponsesWebSearchUserLocation) +}).annotate({ "description": "Web search tool configuration (2025-08-26 version)" }) +export type OpenAIResponsesReasoningConfig = { + readonly "effort"?: OpenAIResponsesReasoningEffort + readonly "summary"?: ReasoningSummaryVerbosity +} +export const OpenAIResponsesReasoningConfig = Schema.Struct({ + "effort": Schema.optionalKey(OpenAIResponsesReasoningEffort), + "summary": Schema.optionalKey(ReasoningSummaryVerbosity) +}) +export type OpenResponsesReasoningConfig = { + readonly "effort"?: OpenAIResponsesReasoningEffort + readonly "summary"?: ReasoningSummaryVerbosity + readonly "max_tokens"?: number + readonly "enabled"?: boolean +} +export const OpenResponsesReasoningConfig = Schema.Struct({ + "effort": Schema.optionalKey(OpenAIResponsesReasoningEffort), + "summary": Schema.optionalKey(ReasoningSummaryVerbosity), + "max_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "enabled": Schema.optionalKey(Schema.Boolean) +}).annotate({ "description": "Configuration for reasoning mode in the response" }) +export type ResponseFormatTextConfig = + | ResponsesFormatText + | ResponsesFormatJSONObject + | ResponsesFormatTextJSONSchemaConfig +export const ResponseFormatTextConfig = Schema.Union([ + ResponsesFormatText, + ResponsesFormatJSONObject, + ResponsesFormatTextJSONSchemaConfig +]).annotate({ "description": "Text response format configuration" }) +export type OpenResponsesLogProbs = { + readonly "logprob": number + readonly "token": string + readonly "top_logprobs"?: ReadonlyArray +} +export const OpenResponsesLogProbs = Schema.Struct({ + "logprob": Schema.Number.check(Schema.isFinite()), + "token": Schema.String, + "top_logprobs": Schema.optionalKey(Schema.Array(OpenResponsesTopLogprobs)) +}).annotate({ "description": "Log probability information for a token" }) +export type BadRequestResponse = { readonly "error": BadRequestResponseErrorData; readonly "user_id"?: string } +export const BadRequestResponse = Schema.Struct({ + "error": BadRequestResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Bad Request - Invalid request parameters or malformed input" }) +export type UnauthorizedResponse = { readonly "error": UnauthorizedResponseErrorData; readonly "user_id"?: string } +export const UnauthorizedResponse = Schema.Struct({ + "error": UnauthorizedResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Unauthorized - Authentication required or invalid credentials" }) +export type PaymentRequiredResponse = { + readonly "error": PaymentRequiredResponseErrorData + readonly "user_id"?: string +} +export const PaymentRequiredResponse = Schema.Struct({ + "error": PaymentRequiredResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Payment Required - Insufficient credits or quota to complete request" }) +export type NotFoundResponse = { readonly "error": NotFoundResponseErrorData; readonly "user_id"?: string } +export const NotFoundResponse = Schema.Struct({ + "error": NotFoundResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Not Found - Resource does not exist" }) +export type RequestTimeoutResponse = { readonly "error": RequestTimeoutResponseErrorData; readonly "user_id"?: string } +export const RequestTimeoutResponse = Schema.Struct({ + "error": RequestTimeoutResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Request Timeout - Operation exceeded time limit" }) +export type PayloadTooLargeResponse = { + readonly "error": PayloadTooLargeResponseErrorData + readonly "user_id"?: string +} +export const PayloadTooLargeResponse = Schema.Struct({ + "error": PayloadTooLargeResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Payload Too Large - Request payload exceeds size limits" }) +export type UnprocessableEntityResponse = { + readonly "error": UnprocessableEntityResponseErrorData + readonly "user_id"?: string +} +export const UnprocessableEntityResponse = Schema.Struct({ + "error": UnprocessableEntityResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Unprocessable Entity - Semantic validation failure" }) +export type TooManyRequestsResponse = { + readonly "error": TooManyRequestsResponseErrorData + readonly "user_id"?: string +} +export const TooManyRequestsResponse = Schema.Struct({ + "error": TooManyRequestsResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Too Many Requests - Rate limit exceeded" }) +export type InternalServerResponse = { readonly "error": InternalServerResponseErrorData; readonly "user_id"?: string } +export const InternalServerResponse = Schema.Struct({ + "error": InternalServerResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Internal Server Error - Unexpected server error" }) +export type BadGatewayResponse = { readonly "error": BadGatewayResponseErrorData; readonly "user_id"?: string } +export const BadGatewayResponse = Schema.Struct({ + "error": BadGatewayResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Bad Gateway - Provider/upstream API failure" }) +export type ServiceUnavailableResponse = { + readonly "error": ServiceUnavailableResponseErrorData + readonly "user_id"?: string +} +export const ServiceUnavailableResponse = Schema.Struct({ + "error": ServiceUnavailableResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Service Unavailable - Service temporarily unavailable" }) +export type EdgeNetworkTimeoutResponse = { + readonly "error": EdgeNetworkTimeoutResponseErrorData + readonly "user_id"?: string +} +export const EdgeNetworkTimeoutResponse = Schema.Struct({ + "error": EdgeNetworkTimeoutResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Infrastructure Timeout - Provider request timed out at edge network" }) +export type ProviderOverloadedResponse = { + readonly "error": ProviderOverloadedResponseErrorData + readonly "user_id"?: string +} +export const ProviderOverloadedResponse = Schema.Struct({ + "error": ProviderOverloadedResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Provider Overloaded - Provider is temporarily overloaded" }) +export type OpenResponsesEasyInputMessage = { + readonly "type"?: "message" + readonly "role": "user" | "system" | "assistant" | "developer" + readonly "content": + | ReadonlyArray< + | ResponseInputText + | { readonly "type": "input_image"; readonly "detail": "auto" | "high" | "low"; readonly "image_url"?: string } + | ResponseInputFile + | ResponseInputAudio + | ResponseInputVideo + > + | string +} +export const OpenResponsesEasyInputMessage = Schema.Struct({ + "type": Schema.optionalKey(Schema.Literal("message")), + "role": Schema.Literals(["user", "system", "assistant", "developer"]), + "content": Schema.Union([ + Schema.Array( + Schema.Union([ + ResponseInputText, + Schema.Struct({ + "type": Schema.Literal("input_image"), + "detail": Schema.Literals(["auto", "high", "low"]), + "image_url": Schema.optionalKey(Schema.String) + }).annotate({ "description": "Image input content item" }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + ], { mode: "oneOf" }) + ), + Schema.String + ]) +}) +export type OpenResponsesInputMessageItem = { + readonly "id"?: string + readonly "type"?: "message" + readonly "role": "user" | "system" | "developer" + readonly "content": ReadonlyArray< + | ResponseInputText + | { readonly "type": "input_image"; readonly "detail": "auto" | "high" | "low"; readonly "image_url"?: string } + | ResponseInputFile + | ResponseInputAudio + | ResponseInputVideo + > +} +export const OpenResponsesInputMessageItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "type": Schema.optionalKey(Schema.Literal("message")), + "role": Schema.Literals(["user", "system", "developer"]), + "content": Schema.Array( + Schema.Union([ + ResponseInputText, + Schema.Struct({ + "type": Schema.Literal("input_image"), + "detail": Schema.Literals(["auto", "high", "low"]), + "image_url": Schema.optionalKey(Schema.String) + }).annotate({ "description": "Image input content item" }), + ResponseInputFile, + ResponseInputAudio, + ResponseInputVideo + ], { mode: "oneOf" }) + ) +}) +export type ProviderSortConfig = { readonly "by"?: ProviderSort | null; readonly "partition"?: "model" | "none" | null } +export const ProviderSortConfig = Schema.Struct({ + "by": Schema.optionalKey(Schema.Union([ProviderSort, Schema.Null])), + "partition": Schema.optionalKey(Schema.Union([Schema.Literals(["model", "none"]), Schema.Null])) +}) +export type PreferredMinThroughput = number | PercentileThroughputCutoffs | unknown +export const PreferredMinThroughput = Schema.Union([ + Schema.Number.check(Schema.isFinite()), + PercentileThroughputCutoffs, + Schema.Unknown +]).annotate({ + "description": + "Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold." +}) +export type PreferredMaxLatency = number | PercentileLatencyCutoffs | unknown +export const PreferredMaxLatency = Schema.Union([ + Schema.Number.check(Schema.isFinite()), + PercentileLatencyCutoffs, + Schema.Unknown +]).annotate({ + "description": + "Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold." +}) +export type PDFParserOptions = { readonly "engine"?: PDFParserEngine } +export const PDFParserOptions = Schema.Struct({ "engine": Schema.optionalKey(PDFParserEngine) }).annotate({ + "description": "Options for PDF parsing." +}) +export type ForbiddenResponse = { readonly "error": ForbiddenResponseErrorData; readonly "user_id"?: string } +export const ForbiddenResponse = Schema.Struct({ + "error": ForbiddenResponseErrorData, + "user_id": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Forbidden - Authentication successful but insufficient permissions" }) +export type ModelArchitecture = { + readonly "tokenizer"?: ModelGroup + readonly "instruct_type"?: + | "none" + | "airoboros" + | "alpaca" + | "alpaca-modif" + | "chatml" + | "claude" + | "code-llama" + | "gemma" + | "llama2" + | "llama3" + | "mistral" + | "nemotron" + | "neural" + | "openchat" + | "phi3" + | "rwkv" + | "vicuna" + | "zephyr" + | "deepseek-r1" + | "deepseek-v3.1" + | "qwq" + | "qwen3" + readonly "modality": string + readonly "input_modalities": ReadonlyArray + readonly "output_modalities": ReadonlyArray +} +export const ModelArchitecture = Schema.Struct({ + "tokenizer": Schema.optionalKey(ModelGroup), + "instruct_type": Schema.optionalKey( + Schema.Literals([ + "none", + "airoboros", + "alpaca", + "alpaca-modif", + "chatml", + "claude", + "code-llama", + "gemma", + "llama2", + "llama3", + "mistral", + "nemotron", + "neural", + "openchat", + "phi3", + "rwkv", + "vicuna", + "zephyr", + "deepseek-r1", + "deepseek-v3.1", + "qwq", + "qwen3" + ]).annotate({ "description": "Instruction format type" }) + ), + "modality": Schema.String.annotate({ "description": "Primary modality of the model" }), + "input_modalities": Schema.Array(InputModality).annotate({ "description": "Supported input modalities" }), + "output_modalities": Schema.Array(OutputModality).annotate({ "description": "Supported output modalities" }) +}).annotate({ "description": "Model architecture information" }) +export type PublicEndpoint = { + readonly "name": string + readonly "model_id": string + readonly "model_name": string + readonly "context_length": number + readonly "pricing": { + readonly "prompt": string + readonly "completion": string + readonly "request"?: string + readonly "image"?: string + readonly "image_token"?: string + readonly "image_output"?: string + readonly "audio"?: string + readonly "audio_output"?: string + readonly "input_audio_cache"?: string + readonly "web_search"?: string + readonly "internal_reasoning"?: string + readonly "input_cache_read"?: string + readonly "input_cache_write"?: string + readonly "discount"?: number + } + readonly "provider_name": ProviderName + readonly "tag": string + readonly "quantization": "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown" + readonly "max_completion_tokens": number + readonly "max_prompt_tokens": number + readonly "supported_parameters": ReadonlyArray + readonly "status"?: EndpointStatus + readonly "uptime_last_30m": number + readonly "supports_implicit_caching": boolean + readonly "latency_last_30m": PercentileStats + readonly "throughput_last_30m": { + readonly "p50": number + readonly "p75": number + readonly "p90": number + readonly "p99": number + } +} +export const PublicEndpoint = Schema.Struct({ + "name": Schema.String, + "model_id": Schema.String.annotate({ "description": "The unique identifier for the model (permaslug)" }), + "model_name": Schema.String, + "context_length": Schema.Number.check(Schema.isFinite()), + "pricing": Schema.Struct({ + "prompt": Schema.String.annotate({ "description": "A number or string value representing a large number" }), + "completion": Schema.String.annotate({ "description": "A number or string value representing a large number" }), + "request": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image_token": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "image_output": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "audio": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "audio_output": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_audio_cache": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "web_search": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "internal_reasoning": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_cache_read": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "input_cache_write": Schema.optionalKey( + Schema.String.annotate({ "description": "A number or string value representing a large number" }) + ), + "discount": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }), + "provider_name": ProviderName, + "tag": Schema.String, + "quantization": Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]), + "max_completion_tokens": Schema.Number.check(Schema.isFinite()), + "max_prompt_tokens": Schema.Number.check(Schema.isFinite()), + "supported_parameters": Schema.Array(Parameter), + "status": Schema.optionalKey(EndpointStatus), + "uptime_last_30m": Schema.Number.check(Schema.isFinite()), + "supports_implicit_caching": Schema.Boolean, + "latency_last_30m": PercentileStats, + "throughput_last_30m": Schema.Struct({ + "p50": Schema.Number.annotate({ "description": "Median (50th percentile)" }).check(Schema.isFinite()), + "p75": Schema.Number.annotate({ "description": "75th percentile" }).check(Schema.isFinite()), + "p90": Schema.Number.annotate({ "description": "90th percentile" }).check(Schema.isFinite()), + "p99": Schema.Number.annotate({ "description": "99th percentile" }).check(Schema.isFinite()) + }).annotate({ + "description": + "Throughput percentiles in tokens per second over the last 30 minutes. Throughput measures output token generation speed. Only visible when authenticated with an API key or cookie; returns null for unauthenticated requests." + }) +}).annotate({ "description": "Information about a specific model endpoint" }) +export type __schema20 = { + readonly "type": "reasoning.summary" + readonly "summary": string + readonly "id"?: __schema21 + readonly "format"?: __schema22 + readonly "index"?: __schema11 +} | { + readonly "type": "reasoning.encrypted" + readonly "data": string + readonly "id"?: __schema21 + readonly "format"?: __schema22 + readonly "index"?: __schema11 +} | { + readonly "type": "reasoning.text" + readonly "text"?: string | null + readonly "signature"?: string | null + readonly "id"?: __schema21 + readonly "format"?: __schema22 + readonly "index"?: __schema11 +} +export const __schema20 = Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("reasoning.summary"), + "summary": Schema.String, + "id": Schema.optionalKey(__schema21), + "format": Schema.optionalKey(__schema22), + "index": Schema.optionalKey(__schema11) + }), + Schema.Struct({ + "type": Schema.Literal("reasoning.encrypted"), + "data": Schema.String, + "id": Schema.optionalKey(__schema21), + "format": Schema.optionalKey(__schema22), + "index": Schema.optionalKey(__schema11) + }), + Schema.Struct({ + "type": Schema.Literal("reasoning.text"), + "text": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "signature": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.optionalKey(__schema21), + "format": Schema.optionalKey(__schema22), + "index": Schema.optionalKey(__schema11) + }) +], { mode: "oneOf" }) +export type __schema14 = __schema11 | ModelName | __schema13 +export const __schema14 = Schema.Union([__schema11, ModelName, __schema13]) +export type ChatMessageTokenLogprobs = { + readonly "content": ReadonlyArray | null + readonly "refusal": ReadonlyArray | null +} +export const ChatMessageTokenLogprobs = Schema.Struct({ + "content": Schema.Union([Schema.Array(ChatMessageTokenLogprob), Schema.Null]), + "refusal": Schema.Union([Schema.Array(ChatMessageTokenLogprob), Schema.Null]) +}) +export type __schema26 = ChatCompletionFinishReason | null +export const __schema26 = Schema.Union([ChatCompletionFinishReason, Schema.Null]) +export type ResponseFormatJSONSchema = { readonly "type": "json_schema"; readonly "json_schema": JSONSchemaConfig } +export const ResponseFormatJSONSchema = Schema.Struct({ + "type": Schema.Literal("json_schema"), + "json_schema": JSONSchemaConfig +}) +export type ChatMessageContentItemText = { + readonly "type": "text" + readonly "text": string + readonly "cache_control"?: ChatMessageContentItemCacheControl +} +export const ChatMessageContentItemText = Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "cache_control": Schema.optionalKey(ChatMessageContentItemCacheControl) +}) +export type ToolDefinitionJson = { + readonly "type": "function" + readonly "function": { + readonly "name": string + readonly "description"?: string + readonly "parameters"?: {} + readonly "strict"?: boolean | null + } + readonly "cache_control"?: ChatMessageContentItemCacheControl +} +export const ToolDefinitionJson = Schema.Struct({ + "type": Schema.Literal("function"), + "function": Schema.Struct({ + "name": Schema.String.check(Schema.isMaxLength(64)), + "description": Schema.optionalKey(Schema.String), + "parameters": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) + }), + "cache_control": Schema.optionalKey(ChatMessageContentItemCacheControl) +}) +export type ToolChoiceOption = "none" | "auto" | "required" | NamedToolChoice +export const ToolChoiceOption = Schema.Union([ + Schema.Literal("none"), + Schema.Literal("auto"), + Schema.Literal("required"), + NamedToolChoice +]) +export type ResponseOutputText = { + readonly "type": "output_text" + readonly "text": string + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > +} +export const ResponseOutputText = Schema.Struct({ + "type": Schema.Literal("output_text"), + "text": Schema.String, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))) +}) +export type OpenResponsesOutputTextAnnotationAddedEvent = { + readonly "type": "response.output_text.annotation.added" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "sequence_number": number + readonly "annotation_index": number + readonly "annotation": OpenAIResponsesAnnotation +} +export const OpenResponsesOutputTextAnnotationAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.annotation.added"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "sequence_number": Schema.Number.check(Schema.isFinite()), + "annotation_index": Schema.Number.check(Schema.isFinite()), + "annotation": OpenAIResponsesAnnotation +}).annotate({ "description": "Event emitted when a text annotation is added to output" }) +export type ResponseTextConfig = { + readonly "format"?: ResponseFormatTextConfig + readonly "verbosity"?: "high" | "low" | "medium" +} +export const ResponseTextConfig = Schema.Struct({ + "format": Schema.optionalKey(ResponseFormatTextConfig), + "verbosity": Schema.optionalKey(Schema.Literals(["high", "low", "medium"])) +}).annotate({ "description": "Text output configuration including format and verbosity" }) +export type OpenResponsesResponseText = { + readonly "format"?: ResponseFormatTextConfig + readonly "verbosity"?: "high" | "low" | "medium" +} +export const OpenResponsesResponseText = Schema.Struct({ + "format": Schema.optionalKey(ResponseFormatTextConfig), + "verbosity": Schema.optionalKey(Schema.Literals(["high", "low", "medium"])) +}).annotate({ "description": "Text output configuration including format and verbosity" }) +export type OpenResponsesTextDeltaEvent = { + readonly "type": "response.output_text.delta" + readonly "logprobs": ReadonlyArray + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "delta": string + readonly "sequence_number": number +} +export const OpenResponsesTextDeltaEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.delta"), + "logprobs": Schema.Array(OpenResponsesLogProbs), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "delta": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a text delta is streamed" }) +export type OpenResponsesTextDoneEvent = { + readonly "type": "response.output_text.done" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "text": string + readonly "sequence_number": number + readonly "logprobs": ReadonlyArray +} +export const OpenResponsesTextDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.output_text.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "text": Schema.String, + "sequence_number": Schema.Number.check(Schema.isFinite()), + "logprobs": Schema.Array(OpenResponsesLogProbs) +}).annotate({ "description": "Event emitted when text streaming is complete" }) +export type ProviderSortUnion = ProviderSort | ProviderSortConfig +export const ProviderSortUnion = Schema.Union([ProviderSort, ProviderSortConfig]) +export type ProviderPreferences = { + readonly "allow_fallbacks"?: boolean + readonly "require_parameters"?: boolean + readonly "data_collection"?: DataCollection + readonly "zdr"?: boolean + readonly "enforce_distillable_text"?: boolean + readonly "order"?: ReadonlyArray + readonly "only"?: ReadonlyArray + readonly "ignore"?: ReadonlyArray + readonly "quantizations"?: ReadonlyArray + readonly "sort"?: "price" | "price" | "throughput" | "throughput" | "latency" | "latency" + readonly "max_price"?: { + readonly "prompt"?: BigNumberUnion + readonly "completion"?: string + readonly "image"?: string + readonly "audio"?: string + readonly "request"?: string + } + readonly "preferred_min_throughput"?: PreferredMinThroughput + readonly "preferred_max_latency"?: PreferredMaxLatency +} +export const ProviderPreferences = Schema.Struct({ + "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + })), + "require_parameters": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest." + }) + ), + "data_collection": Schema.optionalKey(DataCollection), + "zdr": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." + }) + ), + "enforce_distillable_text": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." + }) + ), + "order": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message." + }) + ), + "only": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + }) + ), + "ignore": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + }) + ), + "quantizations": Schema.optionalKey( + Schema.Array(Quantization).annotate({ "description": "A list of quantization levels to filter the provider by." }) + ), + "sort": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Literal("price"), Schema.Literal("price")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }), + Schema.Union([Schema.Literal("throughput"), Schema.Literal("throughput")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }), + Schema.Union([Schema.Literal("latency"), Schema.Literal("latency")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }) + ]) + ), + "max_price": Schema.optionalKey( + Schema.Struct({ + "prompt": Schema.optionalKey(BigNumberUnion), + "completion": Schema.optionalKey( + Schema.String.annotate({ "description": "Price per million completion tokens" }) + ), + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) + }).annotate({ + "description": + "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." + }) + ), + "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), + "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) +}).annotate({ "description": "Provider routing preferences for the request." }) +export type AnthropicMessagesRequest = { + readonly "model": string + readonly "max_tokens": number + readonly "messages": ReadonlyArray + readonly "system"?: + | string + | ReadonlyArray< + { + readonly "type": "text" + readonly "text": string + readonly "citations"?: ReadonlyArray< + { + readonly "type": "char_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_char_index": number + readonly "end_char_index": number + } | { + readonly "type": "page_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_page_number": number + readonly "end_page_number": number + } | { + readonly "type": "content_block_location" + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string + readonly "start_block_index": number + readonly "end_block_index": number + } | { + readonly "type": "web_search_result_location" + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string + readonly "url": string + } | { + readonly "type": "search_result_location" + readonly "cited_text": string + readonly "search_result_index": number + readonly "source": string + readonly "title": string + readonly "start_block_index": number + readonly "end_block_index": number + } + > + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > + readonly "metadata"?: { readonly "user_id"?: string } + readonly "stop_sequences"?: ReadonlyArray + readonly "stream"?: boolean + readonly "temperature"?: number + readonly "top_p"?: number + readonly "top_k"?: number + readonly "tools"?: ReadonlyArray< + { + readonly "name": string + readonly "description"?: string + readonly "input_schema": { + readonly "type": "object" + readonly "properties"?: unknown + readonly "required"?: ReadonlyArray + } + readonly "type"?: "custom" + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } | { + readonly "type": "bash_20250124" + readonly "name": "bash" + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } | { + readonly "type": "text_editor_20250124" + readonly "name": "str_replace_editor" + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } | { + readonly "type": "web_search_20250305" + readonly "name": "web_search" + readonly "allowed_domains"?: ReadonlyArray + readonly "blocked_domains"?: ReadonlyArray + readonly "max_uses"?: number + readonly "user_location"?: { + readonly "type": "approximate" + readonly "city"?: string + readonly "country"?: string + readonly "region"?: string + readonly "timezone"?: string + } + readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } + } + > + readonly "tool_choice"?: + | { readonly "type": "auto"; readonly "disable_parallel_tool_use"?: boolean } + | { readonly "type": "any"; readonly "disable_parallel_tool_use"?: boolean } + | { readonly "type": "none" } + | { readonly "type": "tool"; readonly "name": string; readonly "disable_parallel_tool_use"?: boolean } + readonly "thinking"?: { readonly "type": "enabled"; readonly "budget_tokens": number } | { + readonly "type": "disabled" + } | { readonly "type": "adaptive" } + readonly "service_tier"?: "auto" | "standard_only" + readonly "provider"?: { + readonly "allow_fallbacks"?: boolean + readonly "require_parameters"?: boolean + readonly "data_collection"?: DataCollection + readonly "zdr"?: boolean + readonly "enforce_distillable_text"?: boolean + readonly "order"?: ReadonlyArray + readonly "only"?: ReadonlyArray + readonly "ignore"?: ReadonlyArray + readonly "quantizations"?: ReadonlyArray + readonly "sort"?: "price" | "price" | "throughput" | "throughput" | "latency" | "latency" + readonly "max_price"?: { + readonly "prompt"?: BigNumberUnion + readonly "completion"?: string + readonly "image"?: string + readonly "audio"?: string + readonly "request"?: string + } + readonly "preferred_min_throughput"?: PreferredMinThroughput + readonly "preferred_max_latency"?: PreferredMaxLatency + } + readonly "plugins"?: ReadonlyArray< + | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } + | { readonly "id": "moderation" } + | { + readonly "id": "web" + readonly "enabled"?: boolean + readonly "max_results"?: number + readonly "search_prompt"?: string + readonly "engine"?: WebSearchEngine + } + | { readonly "id": "file-parser"; readonly "enabled"?: boolean; readonly "pdf"?: PDFParserOptions } + | { readonly "id": "response-healing"; readonly "enabled"?: boolean } + > + readonly "route"?: "fallback" | "sort" + readonly "user"?: string + readonly "session_id"?: string + readonly "trace"?: { + readonly "trace_id"?: string + readonly "trace_name"?: string + readonly "span_name"?: string + readonly "generation_name"?: string + readonly "parent_span_id"?: string + } + readonly "models"?: ReadonlyArray + readonly "output_config"?: AnthropicOutputConfig +} +export const AnthropicMessagesRequest = Schema.Struct({ + "model": Schema.String, + "max_tokens": Schema.Number.check(Schema.isFinite()), + "messages": Schema.Array(OpenRouterAnthropicMessageParam), + "system": Schema.optionalKey(Schema.Union([ + Schema.String, + Schema.Array(Schema.Struct({ + "type": Schema.Literal("text"), + "text": Schema.String, + "citations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("char_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_char_index": Schema.Number.check(Schema.isFinite()), + "end_char_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("page_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_page_number": Schema.Number.check(Schema.isFinite()), + "end_page_number": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("content_block_location"), + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isFinite()), + "document_title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_result_location"), + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "type": Schema.Literal("search_result_location"), + "cited_text": Schema.String, + "search_result_index": Schema.Number.check(Schema.isFinite()), + "source": Schema.String, + "title": Schema.String, + "start_block_index": Schema.Number.check(Schema.isFinite()), + "end_block_index": Schema.Number.check(Schema.isFinite()) + }) + ], { mode: "oneOf" }))), + "cache_control": Schema.optionalKey( + Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) + ) + })) + ])), + "metadata": Schema.optionalKey(Schema.Struct({ "user_id": Schema.optionalKey(Schema.String) })), + "stop_sequences": Schema.optionalKey(Schema.Array(Schema.String)), + "stream": Schema.optionalKey(Schema.Boolean), + "temperature": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "top_p": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "top_k": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "tools": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "name": Schema.String, + "description": Schema.optionalKey(Schema.String), + "input_schema": Schema.Struct({ + "type": Schema.Literal("object"), + "properties": Schema.optionalKey(Schema.Unknown), + "required": Schema.optionalKey(Schema.Array(Schema.String)) + }), + "type": Schema.optionalKey(Schema.Literal("custom")), + "cache_control": Schema.optionalKey( + Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("bash_20250124"), + "name": Schema.Literal("bash"), + "cache_control": Schema.optionalKey( + Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("text_editor_20250124"), + "name": Schema.Literal("str_replace_editor"), + "cache_control": Schema.optionalKey( + Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_20250305"), + "name": Schema.Literal("web_search"), + "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)), + "blocked_domains": Schema.optionalKey(Schema.Array(Schema.String)), + "max_uses": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "user_location": Schema.optionalKey( + Schema.Struct({ + "type": Schema.Literal("approximate"), + "city": Schema.optionalKey(Schema.String), + "country": Schema.optionalKey(Schema.String), + "region": Schema.optionalKey(Schema.String), + "timezone": Schema.optionalKey(Schema.String) + }) + ), + "cache_control": Schema.optionalKey( + Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) + ) + }) + ], { mode: "oneOf" }))), + "tool_choice": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("auto"), + "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) + }), + Schema.Struct({ "type": Schema.Literal("any"), "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) }), + Schema.Struct({ "type": Schema.Literal("none") }), + Schema.Struct({ + "type": Schema.Literal("tool"), + "name": Schema.String, + "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) + }) + ], { mode: "oneOf" }) + ), + "thinking": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("enabled"), "budget_tokens": Schema.Number.check(Schema.isFinite()) }), + Schema.Struct({ "type": Schema.Literal("disabled") }), + Schema.Struct({ "type": Schema.Literal("adaptive") }) + ], { mode: "oneOf" }) + ), + "service_tier": Schema.optionalKey(Schema.Literals(["auto", "standard_only"])), + "provider": Schema.optionalKey( + Schema.Struct({ + "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + })), + "require_parameters": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest." + }) + ), + "data_collection": Schema.optionalKey(DataCollection), + "zdr": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." + }) + ), + "enforce_distillable_text": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." + }) + ), + "order": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message." + }) + ), + "only": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + }) + ), + "ignore": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + }) + ), + "quantizations": Schema.optionalKey( + Schema.Array(Quantization).annotate({ + "description": "A list of quantization levels to filter the provider by." + }) + ), + "sort": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Literal("price"), Schema.Literal("price")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }), + Schema.Union([Schema.Literal("throughput"), Schema.Literal("throughput")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }), + Schema.Union([Schema.Literal("latency"), Schema.Literal("latency")]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }) + ]) + ), + "max_price": Schema.optionalKey( + Schema.Struct({ + "prompt": Schema.optionalKey(BigNumberUnion), + "completion": Schema.optionalKey( + Schema.String.annotate({ "description": "Price per million completion tokens" }) + ), + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) + }).annotate({ + "description": + "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." + }) + ), + "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), + "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) + }).annotate({ + "description": "When multiple model providers are available, optionally indicate your routing preference." + }) + ), + "plugins": Schema.optionalKey( + Schema.Array(Schema.Union([ + Schema.Struct({ + "id": Schema.Literal("auto-router"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the auto-router plugin for this request. Defaults to true." + }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., \"anthropic/*\" matches all Anthropic models). When not specified, uses the default supported models list." + }) + ) + }), + Schema.Struct({ "id": Schema.Literal("moderation") }), + Schema.Struct({ + "id": Schema.Literal("web"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the web-search plugin for this request. Defaults to true." + }) + ), + "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "search_prompt": Schema.optionalKey(Schema.String), + "engine": Schema.optionalKey(WebSearchEngine) + }), + Schema.Struct({ + "id": Schema.Literal("file-parser"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the file-parser plugin for this request. Defaults to true." + }) + ), + "pdf": Schema.optionalKey(PDFParserOptions) + }), + Schema.Struct({ + "id": Schema.Literal("response-healing"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the response-healing plugin for this request. Defaults to true." + }) + ) + }) + ], { mode: "oneOf" })).annotate({ + "description": "Plugins you want to enable for this request, including their settings." + }) + ), + "route": Schema.optionalKey( + Schema.Literals(["fallback", "sort"]).annotate({ + "description": + "**DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: \"fallback\" (maps to \"model\"), \"sort\" (maps to \"none\")." + }) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters." + }).check(Schema.isMaxLength(128)) + ), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters." + }).check(Schema.isMaxLength(128)) + ), + "trace": Schema.optionalKey( + Schema.Struct({ + "trace_id": Schema.optionalKey(Schema.String), + "trace_name": Schema.optionalKey(Schema.String), + "span_name": Schema.optionalKey(Schema.String), + "generation_name": Schema.optionalKey(Schema.String), + "parent_span_id": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." + }) + ), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "output_config": Schema.optionalKey(AnthropicOutputConfig) +}).annotate({ "description": "Request schema for Anthropic Messages API endpoint" }) +export type Model = { + readonly "id": string + readonly "canonical_slug": string + readonly "hugging_face_id"?: string + readonly "name": string + readonly "created": number + readonly "description"?: string + readonly "pricing": PublicPricing + readonly "context_length": number + readonly "architecture": ModelArchitecture + readonly "top_provider": TopProviderInfo + readonly "per_request_limits": PerRequestLimits + readonly "supported_parameters": ReadonlyArray + readonly "default_parameters": DefaultParameters + readonly "expiration_date"?: string +} +export const Model = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), + "canonical_slug": Schema.String.annotate({ "description": "Canonical slug for the model" }), + "hugging_face_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Hugging Face model identifier, if applicable" }) + ), + "name": Schema.String.annotate({ "description": "Display name of the model" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( + Schema.isFinite() + ), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the model" })), + "pricing": PublicPricing, + "context_length": Schema.Number.annotate({ "description": "Maximum context length in tokens" }).check( + Schema.isFinite() + ), + "architecture": ModelArchitecture, + "top_provider": TopProviderInfo, + "per_request_limits": PerRequestLimits, + "supported_parameters": Schema.Array(Parameter).annotate({ + "description": "List of supported parameters for this model" + }), + "default_parameters": DefaultParameters, + "expiration_date": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration." + }) + ) +}).annotate({ "description": "Information about an AI model available on OpenRouter" }) +export type ListEndpointsResponse = { + readonly "id": string + readonly "name": string + readonly "created": number + readonly "description": string + readonly "architecture": { + readonly "tokenizer": + | "Router" + | "Media" + | "Other" + | "GPT" + | "Claude" + | "Gemini" + | "Grok" + | "Cohere" + | "Nova" + | "Qwen" + | "Yi" + | "DeepSeek" + | "Mistral" + | "Llama2" + | "Llama3" + | "Llama4" + | "PaLM" + | "RWKV" + | "Qwen3" + readonly "instruct_type": + | "none" + | "airoboros" + | "alpaca" + | "alpaca-modif" + | "chatml" + | "claude" + | "code-llama" + | "gemma" + | "llama2" + | "llama3" + | "mistral" + | "nemotron" + | "neural" + | "openchat" + | "phi3" + | "rwkv" + | "vicuna" + | "zephyr" + | "deepseek-r1" + | "deepseek-v3.1" + | "qwq" + | "qwen3" + readonly "modality": string + readonly "input_modalities": ReadonlyArray<"text" | "image" | "file" | "audio" | "video"> + readonly "output_modalities": ReadonlyArray<"text" | "image" | "embeddings" | "audio"> + } + readonly "endpoints": ReadonlyArray +} +export const ListEndpointsResponse = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), + "name": Schema.String.annotate({ "description": "Display name of the model" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( + Schema.isFinite() + ), + "description": Schema.String.annotate({ "description": "Description of the model" }), + "architecture": Schema.Struct({ + "tokenizer": Schema.Union([ + Schema.Literal("Router").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Media").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Other").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("GPT").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Claude").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Gemini").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Grok").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Cohere").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Nova").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Qwen").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Yi").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("DeepSeek").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Mistral").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Llama2").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Llama3").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Llama4").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("PaLM").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("RWKV").annotate({ "description": "Tokenizer type used by the model" }), + Schema.Literal("Qwen3").annotate({ "description": "Tokenizer type used by the model" }) + ]).annotate({ "description": "Tokenizer type used by the model" }), + "instruct_type": Schema.Union([ + Schema.Literal("none").annotate({ "description": "Instruction format type" }), + Schema.Literal("airoboros").annotate({ "description": "Instruction format type" }), + Schema.Literal("alpaca").annotate({ "description": "Instruction format type" }), + Schema.Literal("alpaca-modif").annotate({ "description": "Instruction format type" }), + Schema.Literal("chatml").annotate({ "description": "Instruction format type" }), + Schema.Literal("claude").annotate({ "description": "Instruction format type" }), + Schema.Literal("code-llama").annotate({ "description": "Instruction format type" }), + Schema.Literal("gemma").annotate({ "description": "Instruction format type" }), + Schema.Literal("llama2").annotate({ "description": "Instruction format type" }), + Schema.Literal("llama3").annotate({ "description": "Instruction format type" }), + Schema.Literal("mistral").annotate({ "description": "Instruction format type" }), + Schema.Literal("nemotron").annotate({ "description": "Instruction format type" }), + Schema.Literal("neural").annotate({ "description": "Instruction format type" }), + Schema.Literal("openchat").annotate({ "description": "Instruction format type" }), + Schema.Literal("phi3").annotate({ "description": "Instruction format type" }), + Schema.Literal("rwkv").annotate({ "description": "Instruction format type" }), + Schema.Literal("vicuna").annotate({ "description": "Instruction format type" }), + Schema.Literal("zephyr").annotate({ "description": "Instruction format type" }), + Schema.Literal("deepseek-r1").annotate({ "description": "Instruction format type" }), + Schema.Literal("deepseek-v3.1").annotate({ "description": "Instruction format type" }), + Schema.Literal("qwq").annotate({ "description": "Instruction format type" }), + Schema.Literal("qwen3").annotate({ "description": "Instruction format type" }) + ]).annotate({ "description": "Instruction format type" }), + "modality": Schema.String.annotate({ "description": "Primary modality of the model" }), + "input_modalities": Schema.Array( + Schema.Union([ + Schema.Literal("text"), + Schema.Literal("image"), + Schema.Literal("file"), + Schema.Literal("audio"), + Schema.Literal("video") + ]) + ).annotate({ "description": "Supported input modalities" }), + "output_modalities": Schema.Array( + Schema.Union([ + Schema.Literal("text"), + Schema.Literal("image"), + Schema.Literal("embeddings"), + Schema.Literal("audio") + ]) + ).annotate({ "description": "Supported output modalities" }) + }).annotate({ "description": "Model architecture information" }), + "endpoints": Schema.Array(PublicEndpoint).annotate({ "description": "List of available endpoints for this model" }) +}).annotate({ "description": "List of available endpoints for a model" }) +export type ChatStreamingMessageChunk = { + readonly "role"?: "assistant" + readonly "content"?: string | null + readonly "reasoning"?: string | null + readonly "refusal"?: string | null + readonly "tool_calls"?: ReadonlyArray + readonly "reasoning_details"?: ReadonlyArray<__schema20> + readonly "images"?: + | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> + | null + readonly "annotations"?: + | ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "url": string + readonly "title"?: string + readonly "start_index"?: number + readonly "end_index"?: number + readonly "content"?: string + } + } | { + readonly "type": "file_annotation" + readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } + } | { + readonly "type": "file" + readonly "file": { + readonly "hash": string + readonly "name": string + readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> + } + } + > + | null +} +export const ChatStreamingMessageChunk = Schema.Struct({ + "role": Schema.optionalKey(Schema.Literal("assistant")), + "content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "reasoning": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "refusal": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "tool_calls": Schema.optionalKey(Schema.Array(ChatStreamingMessageToolCall)), + "reasoning_details": Schema.optionalKey(Schema.Array(__schema20)), + "images": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) + ), + Schema.Null + ]) + ), + "annotations": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url_citation": Schema.Struct({ + "url": Schema.String, + "title": Schema.optionalKey(Schema.String), + "start_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "end_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "content": Schema.optionalKey(Schema.String) + }) + }), + Schema.Struct({ + "type": Schema.Literal("file_annotation"), + "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) + }), + Schema.Struct({ + "type": Schema.Literal("file"), + "file": Schema.Struct({ + "hash": Schema.String, + "name": Schema.String, + "content": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) + ) + }) + }) + ], { mode: "oneOf" })), + Schema.Null + ])) +}) +export type ChatMessageContentItem = + | ChatMessageContentItemText + | ChatMessageContentItemImage + | ChatMessageContentItemAudio + | ChatMessageContentItemVideo +export const ChatMessageContentItem = Schema.Union([ + ChatMessageContentItemText, + ChatMessageContentItemImage, + ChatMessageContentItemAudio, + ChatMessageContentItemVideo +], { mode: "oneOf" }) +export type SystemMessage = { + readonly "role": "system" + readonly "content": string | ReadonlyArray + readonly "name"?: string +} +export const SystemMessage = Schema.Struct({ + "role": Schema.Literal("system"), + "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItemText)]), + "name": Schema.optionalKey(Schema.String) +}) +export type DeveloperMessage = { + readonly "role": "developer" + readonly "content": string | ReadonlyArray + readonly "name"?: string +} +export const DeveloperMessage = Schema.Struct({ + "role": Schema.Literal("developer"), + "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItemText)]), + "name": Schema.optionalKey(Schema.String) +}) +export type OutputMessage = { + readonly "id": string + readonly "role": "assistant" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content": ReadonlyArray +} +export const OutputMessage = Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Literal("message"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) +}) +export type ResponsesOutputMessage = { + readonly "id": string + readonly "role": "assistant" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content": ReadonlyArray +} +export const ResponsesOutputMessage = Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Literal("message"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) +}).annotate({ "description": "An output message item" }) +export type OpenResponsesContentPartAddedEvent = { + readonly "type": "response.content_part.added" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "sequence_number": number +} +export const OpenResponsesContentPartAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.content_part.added"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a new content part is added to an output item" }) +export type OpenResponsesContentPartDoneEvent = { + readonly "type": "response.content_part.done" + readonly "output_index": number + readonly "item_id": string + readonly "content_index": number + readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "sequence_number": number +} +export const OpenResponsesContentPartDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.content_part.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item_id": Schema.String, + "content_index": Schema.Number.check(Schema.isFinite()), + "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a content part is complete" }) +export type ModelsListResponseData = ReadonlyArray +export const ModelsListResponseData = Schema.Array(Model).annotate({ "description": "List of available models" }) +export type ChatStreamingChoice = { + readonly "delta": ChatStreamingMessageChunk + readonly "finish_reason"?: __schema26 + readonly "index": number + readonly "logprobs"?: ChatMessageTokenLogprobs | null +} +export const ChatStreamingChoice = Schema.Struct({ + "delta": ChatStreamingMessageChunk, + "finish_reason": Schema.optionalKey(__schema26), + "index": Schema.Number.check(Schema.isFinite()), + "logprobs": Schema.optionalKey(Schema.Union([ChatMessageTokenLogprobs, Schema.Null])) +}) +export type UserMessage = { + readonly "role": "user" + readonly "content": string | ReadonlyArray + readonly "name"?: string +} +export const UserMessage = Schema.Struct({ + "role": Schema.Literal("user"), + "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), + "name": Schema.optionalKey(Schema.String) +}) +export type AssistantMessage = { + readonly "role": "assistant" + readonly "content"?: string | ReadonlyArray | null + readonly "name"?: string + readonly "tool_calls"?: ReadonlyArray + readonly "refusal"?: string | null + readonly "reasoning"?: string | null + readonly "reasoning_details"?: ReadonlyArray<__schema20> + readonly "images"?: + | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> + | null + readonly "annotations"?: + | ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "url": string + readonly "title"?: string + readonly "start_index"?: number + readonly "end_index"?: number + readonly "content"?: string + } + } | { + readonly "type": "file_annotation" + readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } + } | { + readonly "type": "file" + readonly "file": { + readonly "hash": string + readonly "name": string + readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> + } + } + > + | null +} +export const AssistantMessage = Schema.Struct({ + "role": Schema.Literal("assistant"), + "content": Schema.optionalKey( + Schema.Union([Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), Schema.Null]) + ), + "name": Schema.optionalKey(Schema.String), + "tool_calls": Schema.optionalKey(Schema.Array(ChatMessageToolCall)), + "refusal": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "reasoning": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "reasoning_details": Schema.optionalKey(Schema.Array(__schema20)), + "images": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) + ), + Schema.Null + ]) + ), + "annotations": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url_citation": Schema.Struct({ + "url": Schema.String, + "title": Schema.optionalKey(Schema.String), + "start_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "end_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "content": Schema.optionalKey(Schema.String) + }) + }), + Schema.Struct({ + "type": Schema.Literal("file_annotation"), + "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) + }), + Schema.Struct({ + "type": Schema.Literal("file"), + "file": Schema.Struct({ + "hash": Schema.String, + "name": Schema.String, + "content": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) + ) + }) + }) + ], { mode: "oneOf" })), + Schema.Null + ])) +}) +export type ToolResponseMessage = { + readonly "role": "tool" + readonly "content": string | ReadonlyArray + readonly "tool_call_id": string +} +export const ToolResponseMessage = Schema.Struct({ + "role": Schema.Literal("tool"), + "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), + "tool_call_id": Schema.String +}) +export type OpenAIResponsesInput = + | string + | ReadonlyArray< + | { + readonly "type"?: "message" + readonly "role": "user" | "system" | "assistant" | "developer" + readonly "content": + | ReadonlyArray + | string + } + | { + readonly "id": string + readonly "type"?: "message" + readonly "role": "user" | "system" | "developer" + readonly "content": ReadonlyArray + } + | { + readonly "type": "function_call_output" + readonly "id"?: string + readonly "call_id": string + readonly "output": string + readonly "status"?: ToolCallStatus + } + | { + readonly "type": "function_call" + readonly "call_id": string + readonly "name": string + readonly "arguments": string + readonly "id"?: string + readonly "status"?: ToolCallStatus + } + | OutputItemImageGenerationCall + | OutputMessage + > + | unknown +export const OpenAIResponsesInput = Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.optionalKey(Schema.Literal("message")), + "role": Schema.Literals(["user", "system", "assistant", "developer"]), + "content": Schema.Union([ + Schema.Array( + Schema.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio], { + mode: "oneOf" + }) + ), + Schema.String + ]) + }), + Schema.Struct({ + "id": Schema.String, + "type": Schema.optionalKey(Schema.Literal("message")), + "role": Schema.Literals(["user", "system", "developer"]), + "content": Schema.Array( + Schema.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio], { mode: "oneOf" }) + ) + }), + Schema.Struct({ + "type": Schema.Literal("function_call_output"), + "id": Schema.optionalKey(Schema.String), + "call_id": Schema.String, + "output": Schema.String, + "status": Schema.optionalKey(ToolCallStatus) + }), + Schema.Struct({ + "type": Schema.Literal("function_call"), + "call_id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "id": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey(ToolCallStatus) + }), + OutputItemImageGenerationCall, + OutputMessage + ])), + Schema.Unknown +]) +export type OpenResponsesOutputItemAddedEvent = { + readonly "type": "response.output_item.added" + readonly "output_index": number + readonly "item": + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + readonly "sequence_number": number +} +export const OpenResponsesOutputItemAddedEvent = Schema.Struct({ + "type": Schema.Literal("response.output_item.added"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item": Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall + ], { mode: "oneOf" }), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a new output item is added to the response" }) +export type OpenResponsesOutputItemDoneEvent = { + readonly "type": "response.output_item.done" + readonly "output_index": number + readonly "item": + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + readonly "sequence_number": number +} +export const OpenResponsesOutputItemDoneEvent = Schema.Struct({ + "type": Schema.Literal("response.output_item.done"), + "output_index": Schema.Number.check(Schema.isFinite()), + "item": Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall + ], { mode: "oneOf" }), + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when an output item is complete" }) +export type OpenResponsesInput = + | string + | ReadonlyArray< + | OpenResponsesReasoning + | OpenResponsesEasyInputMessage + | OpenResponsesInputMessageItem + | OpenResponsesFunctionToolCall + | OpenResponsesFunctionCallOutput + | ResponsesOutputMessage + | ResponsesOutputItemReasoning + | ResponsesOutputItemFunctionCall + | ResponsesWebSearchCallOutput + | ResponsesOutputItemFileSearchCall + | ResponsesImageGenerationCall + > +export const OpenResponsesInput = Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([ + OpenResponsesReasoning, + OpenResponsesEasyInputMessage, + OpenResponsesInputMessageItem, + OpenResponsesFunctionToolCall, + OpenResponsesFunctionCallOutput, + ResponsesOutputMessage, + ResponsesOutputItemReasoning, + ResponsesOutputItemFunctionCall, + ResponsesWebSearchCallOutput, + ResponsesOutputItemFileSearchCall, + ResponsesImageGenerationCall + ]) + ) +]).annotate({ "description": "Input for a response request - can be a string or array of items" }) +export type ModelsListResponse = { readonly "data": ModelsListResponseData } +export const ModelsListResponse = Schema.Struct({ "data": ModelsListResponseData }).annotate({ + "description": "List of available models" +}) +export type ChatStreamingResponseChunk = { + readonly "data": { + readonly "id": string + readonly "choices": ReadonlyArray + readonly "created": number + readonly "model": string + readonly "object": "chat.completion.chunk" + readonly "system_fingerprint"?: string | null + readonly "error"?: { readonly "message": string; readonly "code": number } + readonly "usage"?: ChatGenerationTokenUsage + } +} +export const ChatStreamingResponseChunk = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String, + "choices": Schema.Array(ChatStreamingChoice), + "created": Schema.Number.check(Schema.isFinite()), + "model": Schema.String, + "object": Schema.Literal("chat.completion.chunk"), + "system_fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "error": Schema.optionalKey( + Schema.Struct({ "message": Schema.String, "code": Schema.Number.check(Schema.isFinite()) }) + ), + "usage": Schema.optionalKey(ChatGenerationTokenUsage) + }) +}) +export type ChatResponseChoice = { + readonly "finish_reason": __schema26 + readonly "index": number + readonly "message": AssistantMessage + readonly "logprobs"?: ChatMessageTokenLogprobs | null +} +export const ChatResponseChoice = Schema.Struct({ + "finish_reason": __schema26, + "index": Schema.Number.check(Schema.isFinite()), + "message": AssistantMessage, + "logprobs": Schema.optionalKey(Schema.Union([ChatMessageTokenLogprobs, Schema.Null])) +}) +export type Message = SystemMessage | UserMessage | DeveloperMessage | AssistantMessage | ToolResponseMessage +export const Message = Schema.Union([ + SystemMessage, + UserMessage, + DeveloperMessage, + AssistantMessage, + ToolResponseMessage +], { mode: "oneOf" }) +export type OpenAIResponsesNonStreamingResponse = { + readonly "id": string + readonly "object": "response" + readonly "created_at": number + readonly "model": string + readonly "status": OpenAIResponsesResponseStatus + readonly "completed_at": number + readonly "output": ReadonlyArray< + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + > + readonly "user"?: string + readonly "output_text"?: string + readonly "prompt_cache_key"?: string + readonly "safety_identifier"?: string + readonly "error": ResponsesErrorField + readonly "incomplete_details": OpenAIResponsesIncompleteDetails + readonly "usage"?: OpenAIResponsesUsage + readonly "max_tool_calls"?: number + readonly "top_logprobs"?: number + readonly "max_output_tokens"?: number + readonly "temperature": number + readonly "top_p": number + readonly "presence_penalty": number + readonly "frequency_penalty": number + readonly "instructions": OpenAIResponsesInput + readonly "metadata": OpenResponsesRequestMetadata + readonly "tools": ReadonlyArray< + | { + readonly "type": "function" + readonly "name": string + readonly "description"?: string + readonly "strict"?: boolean + readonly "parameters": {} + } + | OpenResponsesWebSearchPreviewTool + | OpenResponsesWebSearchPreview20250311Tool + | OpenResponsesWebSearchTool + | OpenResponsesWebSearch20250826Tool + > + readonly "tool_choice": OpenAIResponsesToolChoice + readonly "parallel_tool_calls": boolean + readonly "prompt"?: OpenAIResponsesPrompt + readonly "background"?: boolean + readonly "previous_response_id"?: string + readonly "reasoning"?: OpenAIResponsesReasoningConfig + readonly "service_tier"?: OpenAIResponsesServiceTier + readonly "store"?: boolean + readonly "truncation"?: OpenAIResponsesTruncation + readonly "text"?: ResponseTextConfig +} +export const OpenAIResponsesNonStreamingResponse = Schema.Struct({ + "id": Schema.String, + "object": Schema.Literal("response"), + "created_at": Schema.Number.check(Schema.isFinite()), + "model": Schema.String, + "status": OpenAIResponsesResponseStatus, + "completed_at": Schema.Number.check(Schema.isFinite()), + "output": Schema.Array( + Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall + ], { mode: "oneOf" }) + ), + "user": Schema.optionalKey(Schema.String), + "output_text": Schema.optionalKey(Schema.String), + "prompt_cache_key": Schema.optionalKey(Schema.String), + "safety_identifier": Schema.optionalKey(Schema.String), + "error": ResponsesErrorField, + "incomplete_details": OpenAIResponsesIncompleteDetails, + "usage": Schema.optionalKey(OpenAIResponsesUsage), + "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "temperature": Schema.Number.check(Schema.isFinite()), + "top_p": Schema.Number.check(Schema.isFinite()), + "presence_penalty": Schema.Number.check(Schema.isFinite()), + "frequency_penalty": Schema.Number.check(Schema.isFinite()), + "instructions": OpenAIResponsesInput, + "metadata": OpenResponsesRequestMetadata, + "tools": Schema.Array( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("function"), + "name": Schema.String, + "description": Schema.optionalKey(Schema.String), + "strict": Schema.optionalKey(Schema.Boolean), + "parameters": Schema.Struct({}) + }).annotate({ "description": "Function tool definition" }), + OpenResponsesWebSearchPreviewTool, + OpenResponsesWebSearchPreview20250311Tool, + OpenResponsesWebSearchTool, + OpenResponsesWebSearch20250826Tool + ], { mode: "oneOf" }) + ), + "tool_choice": OpenAIResponsesToolChoice, + "parallel_tool_calls": Schema.Boolean, + "prompt": Schema.optionalKey(OpenAIResponsesPrompt), + "background": Schema.optionalKey(Schema.Boolean), + "previous_response_id": Schema.optionalKey(Schema.String), + "reasoning": Schema.optionalKey(OpenAIResponsesReasoningConfig), + "service_tier": Schema.optionalKey(OpenAIResponsesServiceTier), + "store": Schema.optionalKey(Schema.Boolean), + "truncation": Schema.optionalKey(OpenAIResponsesTruncation), + "text": Schema.optionalKey(ResponseTextConfig) +}) +export type OpenResponsesNonStreamingResponse = { + readonly "id": string + readonly "object": "response" + readonly "created_at": number + readonly "model": string + readonly "status": OpenAIResponsesResponseStatus + readonly "completed_at": number + readonly "output": ReadonlyArray< + { + readonly "id": string + readonly "role": "assistant" + readonly "type": "message" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content": ReadonlyArray< + { + readonly "type": "output_text" + readonly "text": string + readonly "annotations"?: ReadonlyArray< + { + readonly "type": "file_citation" + readonly "file_id": string + readonly "filename": string + readonly "index": number + } | { + readonly "type": never + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number + readonly "file_id": string + readonly "filename": string + readonly "index": number + } | { + readonly "type": never + readonly "file_id": string + readonly "index": number + readonly "filename": string + } | { + readonly "type": never + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number + } | { + readonly "type": "url_citation" + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number + } | { + readonly "type": never + readonly "file_id": string + readonly "index": number + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number + } | { + readonly "type": never + readonly "file_id": string + readonly "filename": string + readonly "index": number + } | { + readonly "type": never + readonly "url": string + readonly "title": string + readonly "start_index": number + readonly "end_index": number + readonly "file_id": string + readonly "index": number + } | { readonly "type": "file_path"; readonly "file_id": string; readonly "index": number } + > + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > + } | { + readonly "type": never + readonly "refusal": string + readonly "text": string + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > + } | { + readonly "type": never + readonly "text": string + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > + readonly "refusal": string + } | { readonly "type": "refusal"; readonly "refusal": string } + > + } | { + readonly "type": never + readonly "id": string + readonly "content": ReadonlyArray< + { + readonly "type": never + readonly "text": string + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > + } | { readonly "type": never; readonly "refusal": string; readonly "text": string } + > + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + readonly "role": "assistant" + } | { + readonly "type": never + readonly "id": string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "role": "assistant" + readonly "content": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "role": "assistant" + readonly "content": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "role": "assistant" + readonly "content": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "role": "assistant" + readonly "content": ReadonlyArray + } | { + readonly "id": string + readonly "role": "assistant" + readonly "type": never + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content": ReadonlyArray< + { + readonly "type": never + readonly "text": string + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "token": string + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "top_logprobs": ReadonlyArray< + { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + > + } + > + } | { readonly "type": never; readonly "refusal": string; readonly "text": string } + > + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + } | { + readonly "type": "reasoning" + readonly "id": string + readonly "content"?: ReadonlyArray<{ readonly "type": "reasoning_text"; readonly "text": string }> + readonly "summary": ReadonlyArray<{ readonly "type": "summary_text"; readonly "text": string }> + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + } | { + readonly "type": never + readonly "id": string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + } | { + readonly "type": never + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + } | { + readonly "type": never + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + } | { + readonly "type": never + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + } | { + readonly "id": string + readonly "role": "assistant" + readonly "type": never + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "content": ReadonlyArray + readonly "name": string + readonly "arguments": string + readonly "call_id": string + } | { + readonly "type": never + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + readonly "name": string + readonly "arguments": string + readonly "call_id": string + } | { + readonly "type": "function_call" + readonly "id"?: string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + } | { + readonly "type": never + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "name": string + readonly "arguments": string + readonly "call_id": string + } | { + readonly "type": never + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "name": string + readonly "arguments": string + readonly "call_id": string + } | { + readonly "type": never + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "name": string + readonly "arguments": string + readonly "call_id": string + } | { + readonly "id": string + readonly "role": "assistant" + readonly "type": never + readonly "status": "completed" | "in_progress" + readonly "content": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status": "completed" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + } | { + readonly "type": never + readonly "id": string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status": "completed" | "in_progress" + } | { + readonly "type": "web_search_call" + readonly "id": string + readonly "status": "completed" | "searching" | "in_progress" | "failed" + } | { + readonly "type": never + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "searching" | "in_progress" | "failed" + } | { + readonly "type": never + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "failed" + } | { + readonly "id": string + readonly "role": "assistant" + readonly "type": never + readonly "status": "completed" | "in_progress" + readonly "content": ReadonlyArray + readonly "queries": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status": "completed" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + readonly "queries": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status": "completed" | "in_progress" + readonly "queries": ReadonlyArray + } | { + readonly "type": never + readonly "id": string + readonly "status": "completed" | "searching" | "in_progress" | "failed" + readonly "queries": ReadonlyArray + } | { + readonly "type": "file_search_call" + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "searching" | "in_progress" | "failed" + } | { + readonly "type": never + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "failed" + readonly "queries": ReadonlyArray + } | { + readonly "id": string + readonly "role": "assistant" + readonly "type": never + readonly "status": "completed" | "in_progress" + readonly "content": ReadonlyArray + readonly "result"?: string + } | { + readonly "type": never + readonly "id": string + readonly "content"?: ReadonlyArray + readonly "summary": ReadonlyArray + readonly "encrypted_content"?: string + readonly "status": "completed" | "in_progress" + readonly "signature"?: string + readonly "format"?: + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + readonly "result"?: string + } | { + readonly "type": never + readonly "id": string + readonly "name": string + readonly "arguments": string + readonly "call_id": string + readonly "status": "completed" | "in_progress" + readonly "result"?: string + } | { + readonly "type": never + readonly "id": string + readonly "status": "completed" | "in_progress" | "failed" + readonly "result"?: string + } | { + readonly "type": never + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" | "failed" + readonly "result"?: string + } | { + readonly "type": "image_generation_call" + readonly "id": string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "generating" | "failed" + } + > + readonly "user"?: string + readonly "output_text"?: string + readonly "prompt_cache_key"?: string + readonly "safety_identifier"?: string + readonly "error": ResponsesErrorField + readonly "incomplete_details": OpenAIResponsesIncompleteDetails + readonly "usage"?: { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number + readonly "cost"?: number + readonly "is_byok"?: boolean + readonly "cost_details"?: { + readonly "upstream_inference_cost"?: number + readonly "upstream_inference_input_cost": number + readonly "upstream_inference_output_cost": number + } + } + readonly "max_tool_calls"?: number + readonly "top_logprobs"?: number + readonly "max_output_tokens"?: number + readonly "temperature": number + readonly "top_p": number + readonly "presence_penalty": number + readonly "frequency_penalty": number + readonly "instructions": OpenAIResponsesInput + readonly "metadata": OpenResponsesRequestMetadata + readonly "tools": ReadonlyArray< + | { + readonly "type": "function" + readonly "name": string + readonly "description"?: string + readonly "strict"?: boolean + readonly "parameters": {} + } + | OpenResponsesWebSearchPreviewTool + | OpenResponsesWebSearchPreview20250311Tool + | OpenResponsesWebSearchTool + | OpenResponsesWebSearch20250826Tool + > + readonly "tool_choice": OpenAIResponsesToolChoice + readonly "parallel_tool_calls": boolean + readonly "prompt"?: OpenAIResponsesPrompt + readonly "background"?: boolean + readonly "previous_response_id"?: string + readonly "reasoning"?: OpenAIResponsesReasoningConfig + readonly "service_tier"?: OpenAIResponsesServiceTier + readonly "store"?: boolean + readonly "truncation"?: OpenAIResponsesTruncation + readonly "text"?: ResponseTextConfig +} +export const OpenResponsesNonStreamingResponse = Schema.Struct({ + "id": Schema.String, + "object": Schema.Literal("response"), + "created_at": Schema.Number.check(Schema.isFinite()), + "model": Schema.String, + "status": OpenAIResponsesResponseStatus, + "completed_at": Schema.Number.check(Schema.isFinite()), + "output": Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Literal("message"), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "content": Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("output_text"), + "text": Schema.String, + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("file_citation"), + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Never, + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Never, + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isFinite()), + "filename": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isFinite()), + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Never, + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isFinite()), + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()) + }) + ]), + Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Never, + "url": Schema.String, + "title": Schema.String, + "start_index": Schema.Number.check(Schema.isFinite()), + "end_index": Schema.Number.check(Schema.isFinite()), + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) + }), + Schema.Struct({ + "type": Schema.Literal("file_path"), + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isFinite()) + }) + ]) + ]) + ) + ), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))) + }), + Schema.Struct({ + "type": Schema.Never, + "refusal": Schema.String, + "text": Schema.String, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))) + }) + ]), + Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "text": Schema.String, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))), + "refusal": Schema.String + }), + Schema.Struct({ "type": Schema.Literal("refusal"), "refusal": Schema.String }) + ]) + ])) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "content": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "text": Schema.String, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))) + }), + Schema.Struct({ "type": Schema.Never, "refusal": Schema.String, "text": Schema.String }) + ])), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "role": Schema.Literal("assistant"), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) + }) + ]).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Never, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "content": Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "text": Schema.String, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()), + "top_logprobs": Schema.Array( + Schema.Struct({ + "token": Schema.String, + "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), + "logprob": Schema.Number.check(Schema.isFinite()) + }) + ) + }))) + }), + Schema.Struct({ "type": Schema.Never, "refusal": Schema.String, "text": Schema.String }) + ])), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Literal("reasoning"), + "id": Schema.String, + "content": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("reasoning_text"), "text": Schema.String })) + ), + "summary": Schema.Array(Schema.Struct({ "type": Schema.Literal("summary_text"), "text": Schema.String })), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String) + }) + ]).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Never, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Literal("function_call"), + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String + }) + ]).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Never, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]) + }), + Schema.Struct({ + "type": Schema.Literal("web_search_call"), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]) + }) + ]).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Never, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ), + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]), + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "type": Schema.Literal("file_search_call"), + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]), + "queries": Schema.Array(Schema.String) + }) + ]).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "id": Schema.String, + "role": Schema.Literal("assistant"), + "type": Schema.Never, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "result": Schema.optionalKey(Schema.String) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "summary": Schema.Array(ReasoningSummaryText), + "encrypted_content": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "signature": Schema.optionalKey( + Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) + ), + "format": Schema.optionalKey( + Schema.Literals([ + "unknown", + "openai-responses-v1", + "azure-openai-responses-v1", + "xai-responses-v1", + "anthropic-claude-v1", + "google-gemini-v1" + ]).annotate({ "description": "The format of the reasoning content" }) + ), + "result": Schema.optionalKey(Schema.String) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "name": Schema.String, + "arguments": Schema.String, + "call_id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "result": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), + "result": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Never, + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), + "result": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "type": Schema.Literal("image_generation_call"), + "id": Schema.String, + "result": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("generating"), + Schema.Literal("failed") + ]) + }) + ]).annotate({ "description": "An output item from the response" }) + ])), + "user": Schema.optionalKey(Schema.String), + "output_text": Schema.optionalKey(Schema.String), + "prompt_cache_key": Schema.optionalKey(Schema.String), + "safety_identifier": Schema.optionalKey(Schema.String), + "error": ResponsesErrorField, + "incomplete_details": OpenAIResponsesIncompleteDetails, + "usage": Schema.optionalKey( + Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isFinite()), + "input_tokens_details": Schema.Struct({ "cached_tokens": Schema.Number.check(Schema.isFinite()) }), + "output_tokens": Schema.Number.check(Schema.isFinite()), + "output_tokens_details": Schema.Struct({ "reasoning_tokens": Schema.Number.check(Schema.isFinite()) }), + "total_tokens": Schema.Number.check(Schema.isFinite()), + "cost": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cost of the completion" }).check(Schema.isFinite()) + ), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Whether a request was made using a Bring Your Own Key configuration" + }) + ), + "cost_details": Schema.optionalKey( + Schema.Struct({ + "upstream_inference_cost": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "upstream_inference_input_cost": Schema.Number.check(Schema.isFinite()), + "upstream_inference_output_cost": Schema.Number.check(Schema.isFinite()) + }) + ) + }).annotate({ "description": "Token usage information for the response" }) + ), + "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "temperature": Schema.Number.check(Schema.isFinite()), + "top_p": Schema.Number.check(Schema.isFinite()), + "presence_penalty": Schema.Number.check(Schema.isFinite()), + "frequency_penalty": Schema.Number.check(Schema.isFinite()), + "instructions": OpenAIResponsesInput, + "metadata": OpenResponsesRequestMetadata, + "tools": Schema.Array( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("function"), + "name": Schema.String, + "description": Schema.optionalKey(Schema.String), + "strict": Schema.optionalKey(Schema.Boolean), + "parameters": Schema.Struct({}) + }).annotate({ "description": "Function tool definition" }), + OpenResponsesWebSearchPreviewTool, + OpenResponsesWebSearchPreview20250311Tool, + OpenResponsesWebSearchTool, + OpenResponsesWebSearch20250826Tool + ], { mode: "oneOf" }) + ), + "tool_choice": OpenAIResponsesToolChoice, + "parallel_tool_calls": Schema.Boolean, + "prompt": Schema.optionalKey(OpenAIResponsesPrompt), + "background": Schema.optionalKey(Schema.Boolean), + "previous_response_id": Schema.optionalKey(Schema.String), + "reasoning": Schema.optionalKey(OpenAIResponsesReasoningConfig), + "service_tier": Schema.optionalKey(OpenAIResponsesServiceTier), + "store": Schema.optionalKey(Schema.Boolean), + "truncation": Schema.optionalKey(OpenAIResponsesTruncation), + "text": Schema.optionalKey(ResponseTextConfig) +}).annotate({ "description": "Complete non-streaming response from the Responses API" }) +export type OpenResponsesRequest = { + readonly "input"?: OpenResponsesInput + readonly "instructions"?: string + readonly "metadata"?: OpenResponsesRequestMetadata + readonly "tools"?: ReadonlyArray< + | { + readonly "type": "function" + readonly "name": string + readonly "description"?: string + readonly "strict"?: boolean + readonly "parameters": {} + } + | OpenResponsesWebSearchPreviewTool + | OpenResponsesWebSearchPreview20250311Tool + | OpenResponsesWebSearchTool + | OpenResponsesWebSearch20250826Tool + > + readonly "tool_choice"?: OpenAIResponsesToolChoice + readonly "parallel_tool_calls"?: boolean + readonly "model"?: string + readonly "models"?: ReadonlyArray + readonly "text"?: OpenResponsesResponseText + readonly "reasoning"?: OpenResponsesReasoningConfig + readonly "max_output_tokens"?: number + readonly "temperature"?: number + readonly "top_p"?: number + readonly "top_logprobs"?: number + readonly "max_tool_calls"?: number + readonly "presence_penalty"?: number + readonly "frequency_penalty"?: number + readonly "top_k"?: number + readonly "image_config"?: {} + readonly "modalities"?: ReadonlyArray + readonly "prompt_cache_key"?: string + readonly "previous_response_id"?: string + readonly "prompt"?: OpenAIResponsesPrompt + readonly "include"?: ReadonlyArray + readonly "background"?: boolean + readonly "safety_identifier"?: string + readonly "store"?: false + readonly "service_tier"?: "auto" + readonly "truncation"?: "auto" | "disabled" + readonly "stream"?: boolean + readonly "provider"?: { + readonly "allow_fallbacks"?: boolean + readonly "require_parameters"?: boolean + readonly "data_collection"?: DataCollection + readonly "zdr"?: boolean + readonly "enforce_distillable_text"?: boolean + readonly "order"?: ReadonlyArray + readonly "only"?: ReadonlyArray + readonly "ignore"?: ReadonlyArray + readonly "quantizations"?: ReadonlyArray + readonly "sort"?: ProviderSort | ProviderSortConfig | unknown + readonly "max_price"?: { + readonly "prompt"?: BigNumberUnion + readonly "completion"?: string + readonly "image"?: string + readonly "audio"?: string + readonly "request"?: string + } + readonly "preferred_min_throughput"?: PreferredMinThroughput + readonly "preferred_max_latency"?: PreferredMaxLatency + } + readonly "plugins"?: ReadonlyArray< + | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } + | { readonly "id": "moderation" } + | { + readonly "id": "web" + readonly "enabled"?: boolean + readonly "max_results"?: number + readonly "search_prompt"?: string + readonly "engine"?: WebSearchEngine + } + | { readonly "id": "file-parser"; readonly "enabled"?: boolean; readonly "pdf"?: PDFParserOptions } + | { readonly "id": "response-healing"; readonly "enabled"?: boolean } + > + readonly "route"?: "fallback" | "sort" + readonly "user"?: string + readonly "session_id"?: string + readonly "trace"?: { + readonly "trace_id"?: string + readonly "trace_name"?: string + readonly "span_name"?: string + readonly "generation_name"?: string + readonly "parent_span_id"?: string + } +} +export const OpenResponsesRequest = Schema.Struct({ + "input": Schema.optionalKey(OpenResponsesInput), + "instructions": Schema.optionalKey(Schema.String), + "metadata": Schema.optionalKey(OpenResponsesRequestMetadata), + "tools": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("function"), + "name": Schema.String, + "description": Schema.optionalKey(Schema.String), + "strict": Schema.optionalKey(Schema.Boolean), + "parameters": Schema.Struct({}) + }).annotate({ "description": "Function tool definition" }), + OpenResponsesWebSearchPreviewTool, + OpenResponsesWebSearchPreview20250311Tool, + OpenResponsesWebSearchTool, + OpenResponsesWebSearch20250826Tool + ], { mode: "oneOf" }) + ) + ), + "tool_choice": Schema.optionalKey(OpenAIResponsesToolChoice), + "parallel_tool_calls": Schema.optionalKey(Schema.Boolean), + "model": Schema.optionalKey(Schema.String), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "text": Schema.optionalKey(OpenResponsesResponseText), + "reasoning": Schema.optionalKey(OpenResponsesReasoningConfig), + "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "temperature": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)) + ), + "top_p": Schema.optionalKey(Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0))), + "top_logprobs": Schema.optionalKey( + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)) + ), + "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isInt())), + "presence_penalty": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) + ), + "frequency_penalty": Schema.optionalKey( + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) + ), + "top_k": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "image_config": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details." + }) + ), + "modalities": Schema.optionalKey( + Schema.Array(ResponsesOutputModality).annotate({ + "description": "Output modalities for the response. Supported values are \"text\" and \"image\"." + }) + ), + "prompt_cache_key": Schema.optionalKey(Schema.String), + "previous_response_id": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey(OpenAIResponsesPrompt), + "include": Schema.optionalKey(Schema.Array(OpenAIResponsesIncludable)), + "background": Schema.optionalKey(Schema.Boolean), + "safety_identifier": Schema.optionalKey(Schema.String), + "store": Schema.optionalKey(Schema.Literal(false)), + "service_tier": Schema.optionalKey(Schema.Literal("auto")), + "truncation": Schema.optionalKey(Schema.Literals(["auto", "disabled"])), + "stream": Schema.optionalKey(Schema.Boolean), + "provider": Schema.optionalKey( + Schema.Struct({ + "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + })), + "require_parameters": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest." + }) + ), + "data_collection": Schema.optionalKey(DataCollection), + "zdr": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." + }) + ), + "enforce_distillable_text": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." + }) + ), + "order": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message." + }) + ), + "only": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + }) + ), + "ignore": Schema.optionalKey( + Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + }) + ), + "quantizations": Schema.optionalKey( + Schema.Array(Quantization).annotate({ + "description": "A list of quantization levels to filter the provider by." + }) + ), + "sort": Schema.optionalKey( + Schema.Union([ProviderSort, ProviderSortConfig, Schema.Unknown]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }) + ), + "max_price": Schema.optionalKey( + Schema.Struct({ + "prompt": Schema.optionalKey(BigNumberUnion), + "completion": Schema.optionalKey( + Schema.String.annotate({ "description": "Price per million completion tokens" }) + ), + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) + }).annotate({ + "description": + "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." + }) + ), + "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), + "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) + }).annotate({ + "description": "When multiple model providers are available, optionally indicate your routing preference." + }) + ), + "plugins": Schema.optionalKey( + Schema.Array(Schema.Union([ + Schema.Struct({ + "id": Schema.Literal("auto-router"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the auto-router plugin for this request. Defaults to true." + }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "List of model patterns to filter which models the auto-router can route between. Supports wildcards (e.g., \"anthropic/*\" matches all Anthropic models). When not specified, uses the default supported models list." + }) + ) + }), + Schema.Struct({ "id": Schema.Literal("moderation") }), + Schema.Struct({ + "id": Schema.Literal("web"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the web-search plugin for this request. Defaults to true." + }) + ), + "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "search_prompt": Schema.optionalKey(Schema.String), + "engine": Schema.optionalKey(WebSearchEngine) + }), + Schema.Struct({ + "id": Schema.Literal("file-parser"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the file-parser plugin for this request. Defaults to true." + }) + ), + "pdf": Schema.optionalKey(PDFParserOptions) + }), + Schema.Struct({ + "id": Schema.Literal("response-healing"), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the response-healing plugin for this request. Defaults to true." + }) + ) + }) + ], { mode: "oneOf" })).annotate({ + "description": "Plugins you want to enable for this request, including their settings." + }) + ), + "route": Schema.optionalKey( + Schema.Literals(["fallback", "sort"]).annotate({ + "description": + "**DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: \"fallback\" (maps to \"model\"), \"sort\" (maps to \"none\")." + }) + ), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier representing your end-user, which helps distinguish between different users of your app. This allows your app to identify specific users in case of abuse reports, preventing your entire app from being affected by the actions of individual users. Maximum of 128 characters." + }).check(Schema.isMaxLength(128)) + ), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters." + }).check(Schema.isMaxLength(128)) + ), + "trace": Schema.optionalKey( + Schema.Struct({ + "trace_id": Schema.optionalKey(Schema.String), + "trace_name": Schema.optionalKey(Schema.String), + "span_name": Schema.optionalKey(Schema.String), + "generation_name": Schema.optionalKey(Schema.String), + "parent_span_id": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." + }) + ) +}).annotate({ "description": "Request schema for Responses endpoint" }) +export type ChatGenerationParams = { + readonly "provider"?: { + readonly "allow_fallbacks"?: boolean | null + readonly "require_parameters"?: boolean | null + readonly "data_collection"?: "deny" | "allow" | null + readonly "zdr"?: boolean | null + readonly "enforce_distillable_text"?: boolean | null + readonly "order"?: __schema5 | null + readonly "only"?: __schema5 | null + readonly "ignore"?: __schema5 | null + readonly "quantizations"?: + | ReadonlyArray<"int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown"> + | null + readonly "sort"?: ProviderSortUnion | null + readonly "max_price"?: { + readonly "prompt"?: __schema11 | ModelName | __schema13 + readonly "completion"?: __schema11 | ModelName | __schema13 + readonly "image"?: __schema14 + readonly "audio"?: __schema14 + readonly "request"?: __schema14 + } + readonly "preferred_min_throughput"?: number | { + readonly "p50"?: number | null + readonly "p75"?: number | null + readonly "p90"?: number | null + readonly "p99"?: number | null + } | null + readonly "preferred_max_latency"?: number | { + readonly "p50"?: number | null + readonly "p75"?: number | null + readonly "p90"?: number | null + readonly "p99"?: number | null + } | null + } | null + readonly "plugins"?: ReadonlyArray< + | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } + | { readonly "id": "moderation" } + | { + readonly "id": "web" + readonly "enabled"?: boolean + readonly "max_results"?: number + readonly "search_prompt"?: string + readonly "engine"?: "native" | "exa" + } + | { + readonly "id": "file-parser" + readonly "enabled"?: boolean + readonly "pdf"?: { readonly "engine"?: "mistral-ocr" | "pdf-text" | "native" } + } + | { readonly "id": "response-healing"; readonly "enabled"?: boolean } + > + readonly "route"?: "fallback" | "sort" | null + readonly "user"?: string + readonly "session_id"?: string + readonly "trace"?: { + readonly "trace_id"?: string + readonly "trace_name"?: string + readonly "span_name"?: string + readonly "generation_name"?: string + readonly "parent_span_id"?: string + } + readonly "messages": ReadonlyArray + readonly "model"?: ModelName + readonly "models"?: ReadonlyArray + readonly "frequency_penalty"?: number | null + readonly "logit_bias"?: {} | null + readonly "logprobs"?: boolean | null + readonly "top_logprobs"?: number | null + readonly "max_completion_tokens"?: number | null + readonly "max_tokens"?: number | null + readonly "metadata"?: {} + readonly "presence_penalty"?: number | null + readonly "reasoning"?: { + readonly "effort"?: "xhigh" | "high" | "medium" | "low" | "minimal" | "none" | null + readonly "summary"?: ReasoningSummaryVerbosity | null + } + readonly "response_format"?: + | { readonly "type": "text" } + | { readonly "type": "json_object" } + | ResponseFormatJSONSchema + | ResponseFormatTextGrammar + | { readonly "type": "python" } + readonly "seed"?: number | null + readonly "stop"?: string | ReadonlyArray | null + readonly "stream"?: boolean + readonly "stream_options"?: ChatStreamOptions | null + readonly "temperature"?: number | null + readonly "parallel_tool_calls"?: boolean | null + readonly "tool_choice"?: ToolChoiceOption + readonly "tools"?: ReadonlyArray + readonly "top_p"?: number | null + readonly "debug"?: { readonly "echo_upstream_body"?: boolean } + readonly "image_config"?: {} + readonly "modalities"?: ReadonlyArray<"text" | "image"> +} +export const ChatGenerationParams = Schema.Struct({ + "provider": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "allow_fallbacks": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + }) + ), + "require_parameters": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to filter providers to only those that support the parameters you've provided. If this setting is omitted or set to false, then providers will receive only the parameters they support, and ignore the rest." + }) + ), + "data_collection": Schema.optionalKey( + Schema.Union([Schema.Literals(["deny", "allow"]), Schema.Null]).annotate({ + "description": + "Data collection setting. If no available model provider meets the requirement, your request will return an error.\n- allow: (default) allow providers which store user data non-transiently and may train on it\n\n- deny: use only providers which do not collect user data." + }) + ), + "zdr": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "enforce_distillable_text": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "order": Schema.optionalKey( + Schema.Union([__schema5, Schema.Null]).annotate({ + "description": + "An ordered list of provider slugs. The router will attempt to use the first provider in the subset of this list that supports your requested model, and fall back to the next if it is unavailable. If no providers are available, the request will fail with an error message." + }) + ), + "only": Schema.optionalKey( + Schema.Union([__schema5, Schema.Null]).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + }) + ), + "ignore": Schema.optionalKey( + Schema.Union([__schema5, Schema.Null]).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + }) + ), + "quantizations": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"])), + Schema.Null + ]).annotate({ "description": "A list of quantization levels to filter the provider by." }) + ), + "sort": Schema.optionalKey( + Schema.Union([ProviderSortUnion, Schema.Null]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }) + ), + "max_price": Schema.optionalKey( + Schema.Struct({ + "prompt": Schema.optionalKey(Schema.Union([__schema11, ModelName, __schema13])), + "completion": Schema.optionalKey(Schema.Union([__schema11, ModelName, __schema13])), + "image": Schema.optionalKey(__schema14), + "audio": Schema.optionalKey(__schema14), + "request": Schema.optionalKey(__schema14) + }).annotate({ + "description": + "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." + }) + ), + "preferred_min_throughput": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Number.check(Schema.isFinite()), + Schema.Struct({ + "p50": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p75": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p90": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p99": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])) + }) + ]), + Schema.Null + ]).annotate({ + "description": + "Preferred minimum throughput (in tokens per second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints below the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold." + }) + ), + "preferred_max_latency": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Number.check(Schema.isFinite()), + Schema.Struct({ + "p50": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p75": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p90": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), + "p99": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])) + }) + ]), + Schema.Null + ]).annotate({ + "description": + "Preferred maximum latency (in seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. Endpoints above the threshold(s) may still be used, but are deprioritized in routing. When using fallback models, this may cause a fallback model to be used instead of the primary model if it meets the threshold." + }) + ) + }), + Schema.Null + ]).annotate({ + "description": "When multiple model providers are available, optionally indicate your routing preference." + }) + ), + "plugins": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Schema.Struct({ + "id": Schema.Literal("auto-router"), + "enabled": Schema.optionalKey(Schema.Boolean), + "allowed_models": Schema.optionalKey(Schema.Array(Schema.String)) + }), + Schema.Struct({ "id": Schema.Literal("moderation") }), + Schema.Struct({ + "id": Schema.Literal("web"), + "enabled": Schema.optionalKey(Schema.Boolean), + "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "search_prompt": Schema.optionalKey(Schema.String), + "engine": Schema.optionalKey(Schema.Literals(["native", "exa"])) + }), + Schema.Struct({ + "id": Schema.Literal("file-parser"), + "enabled": Schema.optionalKey(Schema.Boolean), + "pdf": Schema.optionalKey( + Schema.Struct({ "engine": Schema.optionalKey(Schema.Literals(["mistral-ocr", "pdf-text", "native"])) }) + ) + }), + Schema.Struct({ "id": Schema.Literal("response-healing"), "enabled": Schema.optionalKey(Schema.Boolean) }) + ], { mode: "oneOf" }) + ).annotate({ "description": "Plugins you want to enable for this request, including their settings." }) + ), + "route": Schema.optionalKey(Schema.Union([Schema.Literals(["fallback", "sort"]), Schema.Null])), + "user": Schema.optionalKey(Schema.String), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow) for observability. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 128 characters." + }).check(Schema.isMaxLength(128)) + ), + "trace": Schema.optionalKey( + Schema.Struct({ + "trace_id": Schema.optionalKey(Schema.String), + "trace_name": Schema.optionalKey(Schema.String), + "span_name": Schema.optionalKey(Schema.String), + "generation_name": Schema.optionalKey(Schema.String), + "parent_span_id": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." + }) + ), + "messages": Schema.Array(Message).check(Schema.isMinLength(1)), + "model": Schema.optionalKey(ModelName), + "models": Schema.optionalKey(Schema.Array(ModelName)), + "frequency_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check( + Schema.isLessThanOrEqualTo(2) + ), + Schema.Null + ]) + ), + "logit_bias": Schema.optionalKey( + Schema.Union([Schema.Struct({}).check(Schema.isPropertyNames(Schema.String)), Schema.Null]) + ), + "logprobs": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "top_logprobs": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( + Schema.isLessThanOrEqualTo(20) + ), + Schema.Null + ]) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]) + ), + "max_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]) + ), + "metadata": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), + "presence_penalty": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check( + Schema.isLessThanOrEqualTo(2) + ), + Schema.Null + ]) + ), + "reasoning": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([Schema.Literals(["xhigh", "high", "medium", "low", "minimal", "none"]), Schema.Null]) + ), + "summary": Schema.optionalKey(Schema.Union([ReasoningSummaryVerbosity, Schema.Null])) + }) + ), + "response_format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("text") }), + Schema.Struct({ "type": Schema.Literal("json_object") }), + ResponseFormatJSONSchema, + ResponseFormatTextGrammar, + Schema.Struct({ "type": Schema.Literal("python") }) + ], { mode: "oneOf" }) + ), + "seed": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(-9007199254740991)).check( + Schema.isLessThanOrEqualTo(9007199254740991) + ), + Schema.Null + ]) + ), + "stop": Schema.optionalKey( + Schema.Union([Schema.Union([Schema.String, Schema.Array(ModelName).check(Schema.isMaxLength(4))]), Schema.Null]) + ), + "stream": Schema.optionalKey(Schema.Boolean), + "stream_options": Schema.optionalKey(Schema.Union([ChatStreamOptions, Schema.Null])), + "temperature": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( + Schema.isLessThanOrEqualTo(2) + ), + Schema.Null + ]) + ), + "parallel_tool_calls": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "tool_choice": Schema.optionalKey(ToolChoiceOption), + "tools": Schema.optionalKey(Schema.Array(ToolDefinitionJson)), + "top_p": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( + Schema.isLessThanOrEqualTo(1) + ), + Schema.Null + ]) + ), + "debug": Schema.optionalKey(Schema.Struct({ "echo_upstream_body": Schema.optionalKey(Schema.Boolean) })), + "image_config": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), + "modalities": Schema.optionalKey(Schema.Array(Schema.Literals(["text", "image"]))) +}) +export type OpenResponsesCreatedEvent = { + readonly "type": "response.created" + readonly "response": OpenAIResponsesNonStreamingResponse + readonly "sequence_number": number +} +export const OpenResponsesCreatedEvent = Schema.Struct({ + "type": Schema.Literal("response.created"), + "response": OpenAIResponsesNonStreamingResponse, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a response is created" }) +export type OpenResponsesInProgressEvent = { + readonly "type": "response.in_progress" + readonly "response": OpenAIResponsesNonStreamingResponse + readonly "sequence_number": number +} +export const OpenResponsesInProgressEvent = Schema.Struct({ + "type": Schema.Literal("response.in_progress"), + "response": OpenAIResponsesNonStreamingResponse, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a response is in progress" }) +export type OpenResponsesCompletedEvent = { + readonly "type": "response.completed" + readonly "response": OpenAIResponsesNonStreamingResponse + readonly "sequence_number": number +} +export const OpenResponsesCompletedEvent = Schema.Struct({ + "type": Schema.Literal("response.completed"), + "response": OpenAIResponsesNonStreamingResponse, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a response has completed successfully" }) +export type OpenResponsesIncompleteEvent = { + readonly "type": "response.incomplete" + readonly "response": OpenAIResponsesNonStreamingResponse + readonly "sequence_number": number +} +export const OpenResponsesIncompleteEvent = Schema.Struct({ + "type": Schema.Literal("response.incomplete"), + "response": OpenAIResponsesNonStreamingResponse, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a response is incomplete" }) +export type OpenResponsesFailedEvent = { + readonly "type": "response.failed" + readonly "response": OpenAIResponsesNonStreamingResponse + readonly "sequence_number": number +} +export const OpenResponsesFailedEvent = Schema.Struct({ + "type": Schema.Literal("response.failed"), + "response": OpenAIResponsesNonStreamingResponse, + "sequence_number": Schema.Number.check(Schema.isFinite()) +}).annotate({ "description": "Event emitted when a response has failed" }) +export type OpenResponsesStreamEvent = + | OpenResponsesCreatedEvent + | OpenResponsesInProgressEvent + | OpenResponsesCompletedEvent + | OpenResponsesIncompleteEvent + | OpenResponsesFailedEvent + | OpenResponsesErrorEvent + | OpenResponsesOutputItemAddedEvent + | OpenResponsesOutputItemDoneEvent + | OpenResponsesContentPartAddedEvent + | OpenResponsesContentPartDoneEvent + | OpenResponsesTextDeltaEvent + | OpenResponsesTextDoneEvent + | OpenResponsesRefusalDeltaEvent + | OpenResponsesRefusalDoneEvent + | OpenResponsesOutputTextAnnotationAddedEvent + | OpenResponsesFunctionCallArgumentsDeltaEvent + | OpenResponsesFunctionCallArgumentsDoneEvent + | OpenResponsesReasoningDeltaEvent + | OpenResponsesReasoningDoneEvent + | OpenResponsesReasoningSummaryPartAddedEvent + | OpenResponsesReasoningSummaryPartDoneEvent + | OpenResponsesReasoningSummaryTextDeltaEvent + | OpenResponsesReasoningSummaryTextDoneEvent + | OpenResponsesImageGenCallInProgress + | OpenResponsesImageGenCallGenerating + | OpenResponsesImageGenCallPartialImage + | OpenResponsesImageGenCallCompleted +export const OpenResponsesStreamEvent = Schema.Union([ + OpenResponsesCreatedEvent, + OpenResponsesInProgressEvent, + OpenResponsesCompletedEvent, + OpenResponsesIncompleteEvent, + OpenResponsesFailedEvent, + OpenResponsesErrorEvent, + OpenResponsesOutputItemAddedEvent, + OpenResponsesOutputItemDoneEvent, + OpenResponsesContentPartAddedEvent, + OpenResponsesContentPartDoneEvent, + OpenResponsesTextDeltaEvent, + OpenResponsesTextDoneEvent, + OpenResponsesRefusalDeltaEvent, + OpenResponsesRefusalDoneEvent, + OpenResponsesOutputTextAnnotationAddedEvent, + OpenResponsesFunctionCallArgumentsDeltaEvent, + OpenResponsesFunctionCallArgumentsDoneEvent, + OpenResponsesReasoningDeltaEvent, + OpenResponsesReasoningDoneEvent, + OpenResponsesReasoningSummaryPartAddedEvent, + OpenResponsesReasoningSummaryPartDoneEvent, + OpenResponsesReasoningSummaryTextDeltaEvent, + OpenResponsesReasoningSummaryTextDoneEvent, + OpenResponsesImageGenCallInProgress, + OpenResponsesImageGenCallGenerating, + OpenResponsesImageGenCallPartialImage, + OpenResponsesImageGenCallCompleted +], { mode: "oneOf" }) +// schemas +export type CreateResponsesRequestJson = OpenResponsesRequest +export const CreateResponsesRequestJson = OpenResponsesRequest +export type CreateResponses200 = OpenResponsesNonStreamingResponse +export const CreateResponses200 = OpenResponsesNonStreamingResponse +export type CreateResponses200Sse = { readonly "data": OpenResponsesStreamEvent } +export const CreateResponses200Sse = Schema.Struct({ "data": OpenResponsesStreamEvent }) +export type CreateResponses400 = BadRequestResponse +export const CreateResponses400 = BadRequestResponse +export type CreateResponses401 = UnauthorizedResponse +export const CreateResponses401 = UnauthorizedResponse +export type CreateResponses402 = PaymentRequiredResponse +export const CreateResponses402 = PaymentRequiredResponse +export type CreateResponses404 = NotFoundResponse +export const CreateResponses404 = NotFoundResponse +export type CreateResponses408 = RequestTimeoutResponse +export const CreateResponses408 = RequestTimeoutResponse +export type CreateResponses413 = PayloadTooLargeResponse +export const CreateResponses413 = PayloadTooLargeResponse +export type CreateResponses422 = UnprocessableEntityResponse +export const CreateResponses422 = UnprocessableEntityResponse +export type CreateResponses429 = TooManyRequestsResponse +export const CreateResponses429 = TooManyRequestsResponse +export type CreateResponses500 = InternalServerResponse +export const CreateResponses500 = InternalServerResponse +export type CreateResponses502 = BadGatewayResponse +export const CreateResponses502 = BadGatewayResponse +export type CreateResponses503 = ServiceUnavailableResponse +export const CreateResponses503 = ServiceUnavailableResponse +export type CreateResponses524 = EdgeNetworkTimeoutResponse +export const CreateResponses524 = EdgeNetworkTimeoutResponse +export type CreateResponses529 = ProviderOverloadedResponse +export const CreateResponses529 = ProviderOverloadedResponse +export type CreateMessagesRequestJson = AnthropicMessagesRequest +export const CreateMessagesRequestJson = AnthropicMessagesRequest +export type CreateMessages200 = AnthropicMessagesResponse +export const CreateMessages200 = AnthropicMessagesResponse +export type CreateMessages200Sse = { readonly "event": string; readonly "data": AnthropicMessagesStreamEvent } +export const CreateMessages200Sse = Schema.Struct({ "event": Schema.String, "data": AnthropicMessagesStreamEvent }) +export type CreateMessages400 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages400 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages401 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages401 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages403 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages403 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages404 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages404 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages429 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages429 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages500 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages500 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages503 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages503 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type CreateMessages529 = { + readonly "type": "error" + readonly "error": { readonly "type": string; readonly "message": string } +} +export const CreateMessages529 = Schema.Struct({ + "type": Schema.Literal("error"), + "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +}) +export type GetUserActivityParams = { readonly "date"?: string } +export const GetUserActivityParams = Schema.Struct({ + "date": Schema.optionalKey( + Schema.String.annotate({ "description": "Filter by a single UTC date in the last 30 days (YYYY-MM-DD format)." }) + ) +}) +export type GetUserActivity200 = { readonly "data": ReadonlyArray } +export const GetUserActivity200 = Schema.Struct({ + "data": Schema.Array(ActivityItem).annotate({ "description": "List of activity items" }) +}) +export type GetUserActivity400 = BadRequestResponse +export const GetUserActivity400 = BadRequestResponse +export type GetUserActivity401 = UnauthorizedResponse +export const GetUserActivity401 = UnauthorizedResponse +export type GetUserActivity403 = ForbiddenResponse +export const GetUserActivity403 = ForbiddenResponse +export type GetUserActivity500 = InternalServerResponse +export const GetUserActivity500 = InternalServerResponse +export type GetCredits200 = { readonly "data": { readonly "total_credits": number; readonly "total_usage": number } } +export const GetCredits200 = Schema.Struct({ + "data": Schema.Struct({ + "total_credits": Schema.Number.annotate({ "description": "Total credits purchased" }).check(Schema.isFinite()), + "total_usage": Schema.Number.annotate({ "description": "Total credits used" }).check(Schema.isFinite()) + }) +}).annotate({ "description": "Total credits purchased and used" }) +export type GetCredits401 = UnauthorizedResponse +export const GetCredits401 = UnauthorizedResponse +export type GetCredits403 = ForbiddenResponse +export const GetCredits403 = ForbiddenResponse +export type GetCredits500 = InternalServerResponse +export const GetCredits500 = InternalServerResponse +export type CreateCoinbaseChargeRequestJson = CreateChargeRequest +export const CreateCoinbaseChargeRequestJson = CreateChargeRequest +export type CreateCoinbaseCharge200 = { + readonly "data": { + readonly "id": string + readonly "created_at": string + readonly "expires_at": string + readonly "web3_data": { + readonly "transfer_intent": { + readonly "call_data": { + readonly "deadline": string + readonly "fee_amount": string + readonly "id": string + readonly "operator": string + readonly "prefix": string + readonly "recipient": string + readonly "recipient_amount": string + readonly "recipient_currency": string + readonly "refund_destination": string + readonly "signature": string + } + readonly "metadata": { + readonly "chain_id": number + readonly "contract_address": string + readonly "sender": string + } + } + } + } +} +export const CreateCoinbaseCharge200 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String, + "created_at": Schema.String, + "expires_at": Schema.String, + "web3_data": Schema.Struct({ + "transfer_intent": Schema.Struct({ + "call_data": Schema.Struct({ + "deadline": Schema.String, + "fee_amount": Schema.String, + "id": Schema.String, + "operator": Schema.String, + "prefix": Schema.String, + "recipient": Schema.String, + "recipient_amount": Schema.String, + "recipient_currency": Schema.String, + "refund_destination": Schema.String, + "signature": Schema.String + }), + "metadata": Schema.Struct({ + "chain_id": Schema.Number.check(Schema.isFinite()), + "contract_address": Schema.String, + "sender": Schema.String + }) + }) + }) + }) +}) +export type CreateCoinbaseCharge400 = BadRequestResponse +export const CreateCoinbaseCharge400 = BadRequestResponse +export type CreateCoinbaseCharge401 = UnauthorizedResponse +export const CreateCoinbaseCharge401 = UnauthorizedResponse +export type CreateCoinbaseCharge429 = TooManyRequestsResponse +export const CreateCoinbaseCharge429 = TooManyRequestsResponse +export type CreateCoinbaseCharge500 = InternalServerResponse +export const CreateCoinbaseCharge500 = InternalServerResponse +export type CreateEmbeddingsRequestJson = { + readonly "input": + | string + | ReadonlyArray + | ReadonlyArray + | ReadonlyArray> + | ReadonlyArray< + { + readonly "content": ReadonlyArray< + { readonly "type": "text"; readonly "text": string } | { + readonly "type": "image_url" + readonly "image_url": { readonly "url": string } + } + > + } + > + readonly "model": string + readonly "encoding_format"?: "float" | "base64" + readonly "dimensions"?: number + readonly "user"?: string + readonly "provider"?: ProviderPreferences + readonly "input_type"?: string +} +export const CreateEmbeddingsRequestJson = Schema.Struct({ + "input": Schema.Union([ + Schema.String, + Schema.Array(Schema.String), + Schema.Array(Schema.Number.check(Schema.isFinite())), + Schema.Array(Schema.Array(Schema.Number.check(Schema.isFinite()))), + Schema.Array( + Schema.Struct({ + "content": Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("text"), "text": Schema.String }), + Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) + ], { mode: "oneOf" }) + ) + }) + ) + ]), + "model": Schema.String, + "encoding_format": Schema.optionalKey(Schema.Literals(["float", "base64"])), + "dimensions": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), + "user": Schema.optionalKey(Schema.String), + "provider": Schema.optionalKey(ProviderPreferences), + "input_type": Schema.optionalKey(Schema.String) +}) +export type CreateEmbeddings200 = { + readonly "id"?: string + readonly "object": "list" + readonly "data": ReadonlyArray< + { readonly "object": "embedding"; readonly "embedding": ReadonlyArray | string; readonly "index"?: number } + > + readonly "model": string + readonly "usage"?: { readonly "prompt_tokens": number; readonly "total_tokens": number; readonly "cost"?: number } +} +export const CreateEmbeddings200 = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "object": Schema.Literal("list"), + "data": Schema.Array( + Schema.Struct({ + "object": Schema.Literal("embedding"), + "embedding": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.String]), + "index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }) + ), + "model": Schema.String, + "usage": Schema.optionalKey( + Schema.Struct({ + "prompt_tokens": Schema.Number.check(Schema.isFinite()), + "total_tokens": Schema.Number.check(Schema.isFinite()), + "cost": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) + }) + ) +}) +export type CreateEmbeddings200Sse = string +export const CreateEmbeddings200Sse = Schema.String.annotate({ + "description": "Not used for embeddings - embeddings do not support streaming" +}) +export type CreateEmbeddings400 = BadRequestResponse +export const CreateEmbeddings400 = BadRequestResponse +export type CreateEmbeddings401 = UnauthorizedResponse +export const CreateEmbeddings401 = UnauthorizedResponse +export type CreateEmbeddings402 = PaymentRequiredResponse +export const CreateEmbeddings402 = PaymentRequiredResponse +export type CreateEmbeddings404 = NotFoundResponse +export const CreateEmbeddings404 = NotFoundResponse +export type CreateEmbeddings429 = TooManyRequestsResponse +export const CreateEmbeddings429 = TooManyRequestsResponse +export type CreateEmbeddings500 = InternalServerResponse +export const CreateEmbeddings500 = InternalServerResponse +export type CreateEmbeddings502 = BadGatewayResponse +export const CreateEmbeddings502 = BadGatewayResponse +export type CreateEmbeddings503 = ServiceUnavailableResponse +export const CreateEmbeddings503 = ServiceUnavailableResponse +export type CreateEmbeddings524 = EdgeNetworkTimeoutResponse +export const CreateEmbeddings524 = EdgeNetworkTimeoutResponse +export type CreateEmbeddings529 = ProviderOverloadedResponse +export const CreateEmbeddings529 = ProviderOverloadedResponse +export type ListEmbeddingsModels200 = ModelsListResponse +export const ListEmbeddingsModels200 = ModelsListResponse +export type ListEmbeddingsModels400 = BadRequestResponse +export const ListEmbeddingsModels400 = BadRequestResponse +export type ListEmbeddingsModels500 = InternalServerResponse +export const ListEmbeddingsModels500 = InternalServerResponse +export type GetGenerationParams = { readonly "id": string } +export const GetGenerationParams = Schema.Struct({ "id": Schema.String.check(Schema.isMinLength(1)) }) +export type GetGeneration200 = { + readonly "data": { + readonly "id": string + readonly "upstream_id": string + readonly "total_cost": number + readonly "cache_discount": number + readonly "upstream_inference_cost": number + readonly "created_at": string + readonly "model": string + readonly "app_id": number + readonly "streamed": boolean + readonly "cancelled": boolean + readonly "provider_name": string + readonly "latency": number + readonly "moderation_latency": number + readonly "generation_time": number + readonly "finish_reason": string + readonly "tokens_prompt": number + readonly "tokens_completion": number + readonly "native_tokens_prompt": number + readonly "native_tokens_completion": number + readonly "native_tokens_completion_images": number + readonly "native_tokens_reasoning": number + readonly "native_tokens_cached": number + readonly "num_media_prompt": number + readonly "num_input_audio_prompt": number + readonly "num_media_completion": number + readonly "num_search_results": number + readonly "origin": string + readonly "usage": number + readonly "is_byok": boolean + readonly "native_finish_reason": string + readonly "external_user": string + readonly "api_type": "completions" | "embeddings" + readonly "router": string + readonly "provider_responses": ReadonlyArray< + { + readonly "id"?: string + readonly "endpoint_id"?: string + readonly "model_permaslug"?: string + readonly "provider_name"?: + | "AnyScale" + | "Atoma" + | "Cent-ML" + | "CrofAI" + | "Enfer" + | "GoPomelo" + | "HuggingFace" + | "Hyperbolic 2" + | "InoCloud" + | "Kluster" + | "Lambda" + | "Lepton" + | "Lynn 2" + | "Lynn" + | "Mancer" + | "Meta" + | "Modal" + | "Nineteen" + | "OctoAI" + | "Recursal" + | "Reflection" + | "Replicate" + | "SambaNova 2" + | "SF Compute" + | "Targon" + | "Together 2" + | "Ubicloud" + | "01.AI" + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Amazon Bedrock" + | "Amazon Nova" + | "Anthropic" + | "Arcee AI" + | "AtlasCloud" + | "Avian" + | "Azure" + | "BaseTen" + | "BytePlus" + | "Black Forest Labs" + | "Cerebras" + | "Chutes" + | "Cirrascale" + | "Clarifai" + | "Cloudflare" + | "Cohere" + | "Crusoe" + | "DeepInfra" + | "DeepSeek" + | "Featherless" + | "Fireworks" + | "Friendli" + | "GMICloud" + | "Google" + | "Google AI Studio" + | "Groq" + | "Hyperbolic" + | "Inception" + | "Inceptron" + | "InferenceNet" + | "Infermatic" + | "Io Net" + | "Inflection" + | "Liquid" + | "Mara" + | "Mancer 2" + | "Minimax" + | "ModelRun" + | "Mistral" + | "Modular" + | "Moonshot AI" + | "Morph" + | "NCompass" + | "Nebius" + | "NextBit" + | "Novita" + | "Nvidia" + | "OpenAI" + | "OpenInference" + | "Parasail" + | "Perplexity" + | "Phala" + | "Relace" + | "SambaNova" + | "Seed" + | "SiliconFlow" + | "Sourceful" + | "StepFun" + | "Stealth" + | "StreamLake" + | "Switchpoint" + | "Together" + | "Upstage" + | "Venice" + | "WandB" + | "Xiaomi" + | "xAI" + | "Z.AI" + | "FakeProvider" + readonly "status": number + readonly "latency"?: number + readonly "is_byok"?: boolean + } + > + } +} +export const GetGeneration200 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the generation" }), + "upstream_id": Schema.String.annotate({ "description": "Upstream provider's identifier for this generation" }), + "total_cost": Schema.Number.annotate({ "description": "Total cost of the generation in USD" }).check( + Schema.isFinite() + ), + "cache_discount": Schema.Number.annotate({ "description": "Discount applied due to caching" }).check( + Schema.isFinite() + ), + "upstream_inference_cost": Schema.Number.annotate({ "description": "Cost charged by the upstream provider" }).check( + Schema.isFinite() + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the generation was created" }), + "model": Schema.String.annotate({ "description": "Model used for the generation" }), + "app_id": Schema.Number.annotate({ "description": "ID of the app that made the request" }).check(Schema.isFinite()), + "streamed": Schema.Boolean.annotate({ "description": "Whether the response was streamed" }), + "cancelled": Schema.Boolean.annotate({ "description": "Whether the generation was cancelled" }), + "provider_name": Schema.String.annotate({ "description": "Name of the provider that served the request" }), + "latency": Schema.Number.annotate({ "description": "Total latency in milliseconds" }).check(Schema.isFinite()), + "moderation_latency": Schema.Number.annotate({ "description": "Moderation latency in milliseconds" }).check( + Schema.isFinite() + ), + "generation_time": Schema.Number.annotate({ "description": "Time taken for generation in milliseconds" }).check( + Schema.isFinite() + ), + "finish_reason": Schema.String.annotate({ "description": "Reason the generation finished" }), + "tokens_prompt": Schema.Number.annotate({ "description": "Number of tokens in the prompt" }).check( + Schema.isFinite() + ), + "tokens_completion": Schema.Number.annotate({ "description": "Number of tokens in the completion" }).check( + Schema.isFinite() + ), + "native_tokens_prompt": Schema.Number.annotate({ "description": "Native prompt tokens as reported by provider" }) + .check(Schema.isFinite()), + "native_tokens_completion": Schema.Number.annotate({ + "description": "Native completion tokens as reported by provider" + }).check(Schema.isFinite()), + "native_tokens_completion_images": Schema.Number.annotate({ + "description": "Native completion image tokens as reported by provider" + }).check(Schema.isFinite()), + "native_tokens_reasoning": Schema.Number.annotate({ + "description": "Native reasoning tokens as reported by provider" + }).check(Schema.isFinite()), + "native_tokens_cached": Schema.Number.annotate({ "description": "Native cached tokens as reported by provider" }) + .check(Schema.isFinite()), + "num_media_prompt": Schema.Number.annotate({ "description": "Number of media items in the prompt" }).check( + Schema.isFinite() + ), + "num_input_audio_prompt": Schema.Number.annotate({ "description": "Number of audio inputs in the prompt" }).check( + Schema.isFinite() + ), + "num_media_completion": Schema.Number.annotate({ "description": "Number of media items in the completion" }).check( + Schema.isFinite() + ), + "num_search_results": Schema.Number.annotate({ "description": "Number of search results included" }).check( + Schema.isFinite() + ), + "origin": Schema.String.annotate({ "description": "Origin URL of the request" }), + "usage": Schema.Number.annotate({ "description": "Usage amount in USD" }).check(Schema.isFinite()), + "is_byok": Schema.Boolean.annotate({ "description": "Whether this used bring-your-own-key" }), + "native_finish_reason": Schema.String.annotate({ "description": "Native finish reason as reported by provider" }), + "external_user": Schema.String.annotate({ "description": "External user identifier" }), + "api_type": Schema.Literals(["completions", "embeddings"]).annotate({ + "description": "Type of API used for the generation" + }), + "router": Schema.String.annotate({ "description": "Router used for the request (e.g., openrouter/auto)" }), + "provider_responses": Schema.Array(Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "endpoint_id": Schema.optionalKey(Schema.String), + "model_permaslug": Schema.optionalKey(Schema.String), + "provider_name": Schema.optionalKey( + Schema.Literals([ + "AnyScale", + "Atoma", + "Cent-ML", + "CrofAI", + "Enfer", + "GoPomelo", + "HuggingFace", + "Hyperbolic 2", + "InoCloud", + "Kluster", + "Lambda", + "Lepton", + "Lynn 2", + "Lynn", + "Mancer", + "Meta", + "Modal", + "Nineteen", + "OctoAI", + "Recursal", + "Reflection", + "Replicate", + "SambaNova 2", + "SF Compute", + "Targon", + "Together 2", + "Ubicloud", + "01.AI", + "AI21", + "AionLabs", + "Alibaba", + "Ambient", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "Crusoe", + "DeepInfra", + "DeepSeek", + "Featherless", + "Fireworks", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "Hyperbolic", + "Inception", + "Inceptron", + "InferenceNet", + "Infermatic", + "Io Net", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Perplexity", + "Phala", + "Relace", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "StepFun", + "Stealth", + "StreamLake", + "Switchpoint", + "Together", + "Upstage", + "Venice", + "WandB", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" + ]) + ), + "status": Schema.Number.check(Schema.isFinite()), + "latency": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), + "is_byok": Schema.optionalKey(Schema.Boolean) + })).annotate({ "description": "List of provider responses for this generation, including fallback attempts" }) + }).annotate({ "description": "Generation data" }) +}).annotate({ "description": "Generation response" }) +export type GetGeneration401 = UnauthorizedResponse +export const GetGeneration401 = UnauthorizedResponse +export type GetGeneration402 = PaymentRequiredResponse +export const GetGeneration402 = PaymentRequiredResponse +export type GetGeneration404 = NotFoundResponse +export const GetGeneration404 = NotFoundResponse +export type GetGeneration429 = TooManyRequestsResponse +export const GetGeneration429 = TooManyRequestsResponse +export type GetGeneration500 = InternalServerResponse +export const GetGeneration500 = InternalServerResponse +export type GetGeneration502 = BadGatewayResponse +export const GetGeneration502 = BadGatewayResponse +export type GetGeneration524 = EdgeNetworkTimeoutResponse +export const GetGeneration524 = EdgeNetworkTimeoutResponse +export type GetGeneration529 = ProviderOverloadedResponse +export const GetGeneration529 = ProviderOverloadedResponse +export type ListModelsCount200 = ModelsCountResponse +export const ListModelsCount200 = ModelsCountResponse +export type ListModelsCount500 = InternalServerResponse +export const ListModelsCount500 = InternalServerResponse +export type GetModelsParams = { + readonly "category"?: + | "programming" + | "roleplay" + | "marketing" + | "marketing/seo" + | "technology" + | "science" + | "translation" + | "legal" + | "finance" + | "health" + | "trivia" + | "academia" + readonly "supported_parameters"?: string +} +export const GetModelsParams = Schema.Struct({ + "category": Schema.optionalKey( + Schema.Literals([ + "programming", + "roleplay", + "marketing", + "marketing/seo", + "technology", + "science", + "translation", + "legal", + "finance", + "health", + "trivia", + "academia" + ]).annotate({ "description": "Filter models by use case category" }) + ), + "supported_parameters": Schema.optionalKey(Schema.String) +}) +export type GetModels200 = ModelsListResponse +export const GetModels200 = ModelsListResponse +export type GetModels400 = BadRequestResponse +export const GetModels400 = BadRequestResponse +export type GetModels500 = InternalServerResponse +export const GetModels500 = InternalServerResponse +export type ListModelsUser200 = ModelsListResponse +export const ListModelsUser200 = ModelsListResponse +export type ListModelsUser401 = UnauthorizedResponse +export const ListModelsUser401 = UnauthorizedResponse +export type ListModelsUser404 = NotFoundResponse +export const ListModelsUser404 = NotFoundResponse +export type ListModelsUser500 = InternalServerResponse +export const ListModelsUser500 = InternalServerResponse +export type ListEndpoints200 = { readonly "data": ListEndpointsResponse } +export const ListEndpoints200 = Schema.Struct({ "data": ListEndpointsResponse }) +export type ListEndpoints404 = NotFoundResponse +export const ListEndpoints404 = NotFoundResponse +export type ListEndpoints500 = InternalServerResponse +export const ListEndpoints500 = InternalServerResponse +export type ListEndpointsZdr200 = { readonly "data": ReadonlyArray } +export const ListEndpointsZdr200 = Schema.Struct({ "data": Schema.Array(PublicEndpoint) }) +export type ListEndpointsZdr500 = InternalServerResponse +export const ListEndpointsZdr500 = InternalServerResponse +export type ListProviders200 = { + readonly "data": ReadonlyArray< + { + readonly "name": string + readonly "slug": string + readonly "privacy_policy_url": string + readonly "terms_of_service_url"?: string + readonly "status_page_url"?: string + } + > +} +export const ListProviders200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "name": Schema.String.annotate({ "description": "Display name of the provider" }), + "slug": Schema.String.annotate({ "description": "URL-friendly identifier for the provider" }), + "privacy_policy_url": Schema.String.annotate({ "description": "URL to the provider's privacy policy" }), + "terms_of_service_url": Schema.optionalKey( + Schema.String.annotate({ "description": "URL to the provider's terms of service" }) + ), + "status_page_url": Schema.optionalKey( + Schema.String.annotate({ "description": "URL to the provider's status page" }) + ) + })) +}) +export type ListProviders500 = InternalServerResponse +export const ListProviders500 = InternalServerResponse +export type ListParams = { readonly "include_disabled"?: string; readonly "offset"?: string } +export const ListParams = Schema.Struct({ + "include_disabled": Schema.optionalKey( + Schema.String.annotate({ "description": "Whether to include disabled API keys in the response" }) + ), + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of API keys to skip for pagination" })) +}) +export type List200 = { + readonly "data": ReadonlyArray< + { + readonly "hash": string + readonly "name": string + readonly "label": string + readonly "disabled": boolean + readonly "limit": number + readonly "limit_remaining": number + readonly "limit_reset": string + readonly "include_byok_in_limit": boolean + readonly "usage": number + readonly "usage_daily": number + readonly "usage_weekly": number + readonly "usage_monthly": number + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_weekly": number + readonly "byok_usage_monthly": number + readonly "created_at": string + readonly "updated_at": string + readonly "expires_at"?: string + } + > +} +export const List200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( + Schema.isFinite() + ), + "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( + Schema.isFinite() + ), + "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) + .check(Schema.isFinite()), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month" + }).check(Schema.isFinite()), + "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day" + }).check(Schema.isFinite()), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month" + }).check(Schema.isFinite()), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ) + })).annotate({ "description": "List of API keys" }) +}) +export type List401 = UnauthorizedResponse +export const List401 = UnauthorizedResponse +export type List429 = TooManyRequestsResponse +export const List429 = TooManyRequestsResponse +export type List500 = InternalServerResponse +export const List500 = InternalServerResponse +export type CreateKeysRequestJson = { + readonly "name": string + readonly "limit"?: number + readonly "limit_reset"?: "daily" | "weekly" | "monthly" + readonly "include_byok_in_limit"?: boolean + readonly "expires_at"?: string +} +export const CreateKeysRequestJson = Schema.Struct({ + "name": Schema.String.annotate({ "description": "Name for the new API key" }).check(Schema.isMinLength(1)), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Optional spending limit for the API key in USD" }).check(Schema.isFinite()) + ), + "limit_reset": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": + "Type of limit reset for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday." + }) + ), + "include_byok_in_limit": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) + ), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected", + "format": "date-time" + }) + ) +}) +export type CreateKeys201 = { + readonly "data": { + readonly "hash": string + readonly "name": string + readonly "label": string + readonly "disabled": boolean + readonly "limit": number + readonly "limit_remaining": number + readonly "limit_reset": string + readonly "include_byok_in_limit": boolean + readonly "usage": number + readonly "usage_daily": number + readonly "usage_weekly": number + readonly "usage_monthly": number + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_weekly": number + readonly "byok_usage_monthly": number + readonly "created_at": string + readonly "updated_at": string + readonly "expires_at"?: string + } + readonly "key": string +} +export const CreateKeys201 = Schema.Struct({ + "data": Schema.Struct({ + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( + Schema.isFinite() + ), + "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( + Schema.isFinite() + ), + "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) + .check(Schema.isFinite()), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month" + }).check(Schema.isFinite()), + "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day" + }).check(Schema.isFinite()), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month" + }).check(Schema.isFinite()), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ) + }).annotate({ "description": "The created API key information" }), + "key": Schema.String.annotate({ "description": "The actual API key string (only shown once)" }) +}) +export type CreateKeys400 = BadRequestResponse +export const CreateKeys400 = BadRequestResponse +export type CreateKeys401 = UnauthorizedResponse +export const CreateKeys401 = UnauthorizedResponse +export type CreateKeys429 = TooManyRequestsResponse +export const CreateKeys429 = TooManyRequestsResponse +export type CreateKeys500 = InternalServerResponse +export const CreateKeys500 = InternalServerResponse +export type GetKey200 = { + readonly "data": { + readonly "hash": string + readonly "name": string + readonly "label": string + readonly "disabled": boolean + readonly "limit": number + readonly "limit_remaining": number + readonly "limit_reset": string + readonly "include_byok_in_limit": boolean + readonly "usage": number + readonly "usage_daily": number + readonly "usage_weekly": number + readonly "usage_monthly": number + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_weekly": number + readonly "byok_usage_monthly": number + readonly "created_at": string + readonly "updated_at": string + readonly "expires_at"?: string + } +} +export const GetKey200 = Schema.Struct({ + "data": Schema.Struct({ + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( + Schema.isFinite() + ), + "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( + Schema.isFinite() + ), + "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) + .check(Schema.isFinite()), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month" + }).check(Schema.isFinite()), + "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day" + }).check(Schema.isFinite()), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month" + }).check(Schema.isFinite()), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ) + }).annotate({ "description": "The API key information" }) +}) +export type GetKey401 = UnauthorizedResponse +export const GetKey401 = UnauthorizedResponse +export type GetKey404 = NotFoundResponse +export const GetKey404 = NotFoundResponse +export type GetKey429 = TooManyRequestsResponse +export const GetKey429 = TooManyRequestsResponse +export type GetKey500 = InternalServerResponse +export const GetKey500 = InternalServerResponse +export type DeleteKeys200 = { readonly "deleted": true } +export const DeleteKeys200 = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the API key was deleted" }) +}) +export type DeleteKeys401 = UnauthorizedResponse +export const DeleteKeys401 = UnauthorizedResponse +export type DeleteKeys404 = NotFoundResponse +export const DeleteKeys404 = NotFoundResponse +export type DeleteKeys429 = TooManyRequestsResponse +export const DeleteKeys429 = TooManyRequestsResponse +export type DeleteKeys500 = InternalServerResponse +export const DeleteKeys500 = InternalServerResponse +export type UpdateKeysRequestJson = { + readonly "name"?: string + readonly "disabled"?: boolean + readonly "limit"?: number + readonly "limit_reset"?: "daily" | "weekly" | "monthly" + readonly "include_byok_in_limit"?: boolean +} +export const UpdateKeysRequestJson = Schema.Struct({ + "name": Schema.optionalKey(Schema.String.annotate({ "description": "New name for the API key" })), + "disabled": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Whether to disable the API key" })), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "New spending limit for the API key in USD" }).check(Schema.isFinite()) + ), + "limit_reset": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": + "New limit reset type for the API key (daily, weekly, monthly, or null for no reset). Resets happen automatically at midnight UTC, and weeks are Monday through Sunday." + }) + ), + "include_byok_in_limit": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) + ) +}) +export type UpdateKeys200 = { + readonly "data": { + readonly "hash": string + readonly "name": string + readonly "label": string + readonly "disabled": boolean + readonly "limit": number + readonly "limit_remaining": number + readonly "limit_reset": string + readonly "include_byok_in_limit": boolean + readonly "usage": number + readonly "usage_daily": number + readonly "usage_weekly": number + readonly "usage_monthly": number + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_weekly": number + readonly "byok_usage_monthly": number + readonly "created_at": string + readonly "updated_at": string + readonly "expires_at"?: string + } +} +export const UpdateKeys200 = Schema.Struct({ + "data": Schema.Struct({ + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( + Schema.isFinite() + ), + "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( + Schema.isFinite() + ), + "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) + .check(Schema.isFinite()), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month" + }).check(Schema.isFinite()), + "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day" + }).check(Schema.isFinite()), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month" + }).check(Schema.isFinite()), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ) + }).annotate({ "description": "The updated API key information" }) +}) +export type UpdateKeys400 = BadRequestResponse +export const UpdateKeys400 = BadRequestResponse +export type UpdateKeys401 = UnauthorizedResponse +export const UpdateKeys401 = UnauthorizedResponse +export type UpdateKeys404 = NotFoundResponse +export const UpdateKeys404 = NotFoundResponse +export type UpdateKeys429 = TooManyRequestsResponse +export const UpdateKeys429 = TooManyRequestsResponse +export type UpdateKeys500 = InternalServerResponse +export const UpdateKeys500 = InternalServerResponse +export type ListGuardrailsParams = { readonly "offset"?: string; readonly "limit"?: string } +export const ListGuardrailsParams = Schema.Struct({ + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), + "limit": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) + ) +}) +export type ListGuardrails200 = { + readonly "data": ReadonlyArray< + { + readonly "id": string + readonly "name": string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean + readonly "created_at": string + readonly "updated_at"?: string + } + > + readonly "total_count": number +} +export const ListGuardrails200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), + "name": Schema.String.annotate({ "description": "Name of the guardrail" }), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), + "updated_at": Schema.optionalKey( + Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) + ) + })).annotate({ "description": "List of guardrails" }), + "total_count": Schema.Number.annotate({ "description": "Total number of guardrails" }).check(Schema.isFinite()) +}) +export type ListGuardrails401 = UnauthorizedResponse +export const ListGuardrails401 = UnauthorizedResponse +export type ListGuardrails500 = InternalServerResponse +export const ListGuardrails500 = InternalServerResponse +export type CreateGuardrailRequestJson = { + readonly "name": string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean +} +export const CreateGuardrailRequestJson = Schema.Struct({ + "name": Schema.String.annotate({ "description": "Name for the new guardrail" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(200) + ), + "description": Schema.optionalKey( + Schema.String.annotate({ "description": "Description of the guardrail" }).check(Schema.isMaxLength(1000)) + ), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }).check(Schema.isMinLength(1)) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "Array of model identifiers (slug or canonical_slug accepted)" + }).check(Schema.isMinLength(1)) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ) +}) +export type CreateGuardrail201 = { + readonly "data": { + readonly "id": string + readonly "name": string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean + readonly "created_at": string + readonly "updated_at"?: string + } +} +export const CreateGuardrail201 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), + "name": Schema.String.annotate({ "description": "Name of the guardrail" }), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), + "updated_at": Schema.optionalKey( + Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) + ) + }).annotate({ "description": "The created guardrail" }) +}) +export type CreateGuardrail400 = BadRequestResponse +export const CreateGuardrail400 = BadRequestResponse +export type CreateGuardrail401 = UnauthorizedResponse +export const CreateGuardrail401 = UnauthorizedResponse +export type CreateGuardrail500 = InternalServerResponse +export const CreateGuardrail500 = InternalServerResponse +export type GetGuardrail200 = { + readonly "data": { + readonly "id": string + readonly "name": string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean + readonly "created_at": string + readonly "updated_at"?: string + } +} +export const GetGuardrail200 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), + "name": Schema.String.annotate({ "description": "Name of the guardrail" }), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), + "updated_at": Schema.optionalKey( + Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) + ) + }).annotate({ "description": "The guardrail" }) +}) +export type GetGuardrail401 = UnauthorizedResponse +export const GetGuardrail401 = UnauthorizedResponse +export type GetGuardrail404 = NotFoundResponse +export const GetGuardrail404 = NotFoundResponse +export type GetGuardrail500 = InternalServerResponse +export const GetGuardrail500 = InternalServerResponse +export type DeleteGuardrail200 = { readonly "deleted": true } +export const DeleteGuardrail200 = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the guardrail was deleted" }) +}) +export type DeleteGuardrail401 = UnauthorizedResponse +export const DeleteGuardrail401 = UnauthorizedResponse +export type DeleteGuardrail404 = NotFoundResponse +export const DeleteGuardrail404 = NotFoundResponse +export type DeleteGuardrail500 = InternalServerResponse +export const DeleteGuardrail500 = InternalServerResponse +export type UpdateGuardrailRequestJson = { + readonly "name"?: string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean +} +export const UpdateGuardrailRequestJson = Schema.Struct({ + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "New name for the guardrail" }).check(Schema.isMinLength(1)).check( + Schema.isMaxLength(200) + ) + ), + "description": Schema.optionalKey( + Schema.String.annotate({ "description": "New description for the guardrail" }).check(Schema.isMaxLength(1000)) + ), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "New spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "New list of allowed provider IDs" }).check( + Schema.isMinLength(1) + ) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": "Array of model identifiers (slug or canonical_slug accepted)" + }).check(Schema.isMinLength(1)) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ) +}) +export type UpdateGuardrail200 = { + readonly "data": { + readonly "id": string + readonly "name": string + readonly "description"?: string + readonly "limit_usd"?: number + readonly "reset_interval"?: "daily" | "weekly" | "monthly" + readonly "allowed_providers"?: ReadonlyArray + readonly "allowed_models"?: ReadonlyArray + readonly "enforce_zdr"?: boolean + readonly "created_at": string + readonly "updated_at"?: string + } +} +export const UpdateGuardrail200 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), + "name": Schema.String.annotate({ "description": "Name of the guardrail" }), + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), + "limit_usd": Schema.optionalKey( + Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( + Schema.isGreaterThanOrEqualTo(0) + ) + ), + "reset_interval": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) + ), + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), + "updated_at": Schema.optionalKey( + Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) + ) + }).annotate({ "description": "The updated guardrail" }) +}) +export type UpdateGuardrail400 = BadRequestResponse +export const UpdateGuardrail400 = BadRequestResponse +export type UpdateGuardrail401 = UnauthorizedResponse +export const UpdateGuardrail401 = UnauthorizedResponse +export type UpdateGuardrail404 = NotFoundResponse +export const UpdateGuardrail404 = NotFoundResponse +export type UpdateGuardrail500 = InternalServerResponse +export const UpdateGuardrail500 = InternalServerResponse +export type ListKeyAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } +export const ListKeyAssignmentsParams = Schema.Struct({ + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), + "limit": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) + ) +}) +export type ListKeyAssignments200 = { + readonly "data": ReadonlyArray< + { + readonly "id": string + readonly "key_hash": string + readonly "guardrail_id": string + readonly "key_name": string + readonly "key_label": string + readonly "assigned_by": string + readonly "created_at": string + } + > + readonly "total_count": number +} +export const ListKeyAssignments200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "key_hash": Schema.String.annotate({ "description": "Hash of the assigned API key" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "key_name": Schema.String.annotate({ "description": "Name of the API key" }), + "key_label": Schema.String.annotate({ "description": "Label of the API key" }), + "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) + })).annotate({ "description": "List of key assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of key assignments for this guardrail" }).check( + Schema.isFinite() + ) +}) +export type ListKeyAssignments401 = UnauthorizedResponse +export const ListKeyAssignments401 = UnauthorizedResponse +export type ListKeyAssignments500 = InternalServerResponse +export const ListKeyAssignments500 = InternalServerResponse +export type ListMemberAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } +export const ListMemberAssignmentsParams = Schema.Struct({ + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), + "limit": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) + ) +}) +export type ListMemberAssignments200 = { + readonly "data": ReadonlyArray< + { + readonly "id": string + readonly "user_id": string + readonly "organization_id": string + readonly "guardrail_id": string + readonly "assigned_by": string + readonly "created_at": string + } + > + readonly "total_count": number +} +export const ListMemberAssignments200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "user_id": Schema.String.annotate({ "description": "Clerk user ID of the assigned member" }), + "organization_id": Schema.String.annotate({ "description": "Organization ID" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) + })).annotate({ "description": "List of member assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of member assignments" }).check( + Schema.isFinite() + ) +}) +export type ListMemberAssignments401 = UnauthorizedResponse +export const ListMemberAssignments401 = UnauthorizedResponse +export type ListMemberAssignments500 = InternalServerResponse +export const ListMemberAssignments500 = InternalServerResponse +export type ListGuardrailKeyAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } +export const ListGuardrailKeyAssignmentsParams = Schema.Struct({ + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), + "limit": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) + ) +}) +export type ListGuardrailKeyAssignments200 = { + readonly "data": ReadonlyArray< + { + readonly "id": string + readonly "key_hash": string + readonly "guardrail_id": string + readonly "key_name": string + readonly "key_label": string + readonly "assigned_by": string + readonly "created_at": string + } + > + readonly "total_count": number +} +export const ListGuardrailKeyAssignments200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "key_hash": Schema.String.annotate({ "description": "Hash of the assigned API key" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "key_name": Schema.String.annotate({ "description": "Name of the API key" }), + "key_label": Schema.String.annotate({ "description": "Label of the API key" }), + "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) + })).annotate({ "description": "List of key assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of key assignments for this guardrail" }).check( + Schema.isFinite() + ) +}) +export type ListGuardrailKeyAssignments401 = UnauthorizedResponse +export const ListGuardrailKeyAssignments401 = UnauthorizedResponse +export type ListGuardrailKeyAssignments404 = NotFoundResponse +export const ListGuardrailKeyAssignments404 = NotFoundResponse +export type ListGuardrailKeyAssignments500 = InternalServerResponse +export const ListGuardrailKeyAssignments500 = InternalServerResponse +export type BulkAssignKeysToGuardrailRequestJson = { readonly "key_hashes": ReadonlyArray } +export const BulkAssignKeysToGuardrailRequestJson = Schema.Struct({ + "key_hashes": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ + "description": "Array of API key hashes to assign to the guardrail" + }).check(Schema.isMinLength(1)) +}) +export type BulkAssignKeysToGuardrail200 = { readonly "assigned_count": number } +export const BulkAssignKeysToGuardrail200 = Schema.Struct({ + "assigned_count": Schema.Number.annotate({ "description": "Number of keys successfully assigned" }).check( + Schema.isFinite() + ) +}) +export type BulkAssignKeysToGuardrail400 = BadRequestResponse +export const BulkAssignKeysToGuardrail400 = BadRequestResponse +export type BulkAssignKeysToGuardrail401 = UnauthorizedResponse +export const BulkAssignKeysToGuardrail401 = UnauthorizedResponse +export type BulkAssignKeysToGuardrail404 = NotFoundResponse +export const BulkAssignKeysToGuardrail404 = NotFoundResponse +export type BulkAssignKeysToGuardrail500 = InternalServerResponse +export const BulkAssignKeysToGuardrail500 = InternalServerResponse +export type ListGuardrailMemberAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } +export const ListGuardrailMemberAssignmentsParams = Schema.Struct({ + "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), + "limit": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) + ) +}) +export type ListGuardrailMemberAssignments200 = { + readonly "data": ReadonlyArray< + { + readonly "id": string + readonly "user_id": string + readonly "organization_id": string + readonly "guardrail_id": string + readonly "assigned_by": string + readonly "created_at": string + } + > + readonly "total_count": number +} +export const ListGuardrailMemberAssignments200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "user_id": Schema.String.annotate({ "description": "Clerk user ID of the assigned member" }), + "organization_id": Schema.String.annotate({ "description": "Organization ID" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) + })).annotate({ "description": "List of member assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of member assignments" }).check( + Schema.isFinite() + ) +}) +export type ListGuardrailMemberAssignments401 = UnauthorizedResponse +export const ListGuardrailMemberAssignments401 = UnauthorizedResponse +export type ListGuardrailMemberAssignments404 = NotFoundResponse +export const ListGuardrailMemberAssignments404 = NotFoundResponse +export type ListGuardrailMemberAssignments500 = InternalServerResponse +export const ListGuardrailMemberAssignments500 = InternalServerResponse +export type BulkAssignMembersToGuardrailRequestJson = { readonly "member_user_ids": ReadonlyArray } +export const BulkAssignMembersToGuardrailRequestJson = Schema.Struct({ + "member_user_ids": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ + "description": "Array of member user IDs to assign to the guardrail" + }).check(Schema.isMinLength(1)) +}) +export type BulkAssignMembersToGuardrail200 = { readonly "assigned_count": number } +export const BulkAssignMembersToGuardrail200 = Schema.Struct({ + "assigned_count": Schema.Number.annotate({ "description": "Number of members successfully assigned" }).check( + Schema.isFinite() + ) +}) +export type BulkAssignMembersToGuardrail400 = BadRequestResponse +export const BulkAssignMembersToGuardrail400 = BadRequestResponse +export type BulkAssignMembersToGuardrail401 = UnauthorizedResponse +export const BulkAssignMembersToGuardrail401 = UnauthorizedResponse +export type BulkAssignMembersToGuardrail404 = NotFoundResponse +export const BulkAssignMembersToGuardrail404 = NotFoundResponse +export type BulkAssignMembersToGuardrail500 = InternalServerResponse +export const BulkAssignMembersToGuardrail500 = InternalServerResponse +export type BulkUnassignKeysFromGuardrailRequestJson = { readonly "key_hashes": ReadonlyArray } +export const BulkUnassignKeysFromGuardrailRequestJson = Schema.Struct({ + "key_hashes": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ + "description": "Array of API key hashes to unassign from the guardrail" + }).check(Schema.isMinLength(1)) +}) +export type BulkUnassignKeysFromGuardrail200 = { readonly "unassigned_count": number } +export const BulkUnassignKeysFromGuardrail200 = Schema.Struct({ + "unassigned_count": Schema.Number.annotate({ "description": "Number of keys successfully unassigned" }).check( + Schema.isFinite() + ) +}) +export type BulkUnassignKeysFromGuardrail400 = BadRequestResponse +export const BulkUnassignKeysFromGuardrail400 = BadRequestResponse +export type BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse +export const BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse +export type BulkUnassignKeysFromGuardrail404 = NotFoundResponse +export const BulkUnassignKeysFromGuardrail404 = NotFoundResponse +export type BulkUnassignKeysFromGuardrail500 = InternalServerResponse +export const BulkUnassignKeysFromGuardrail500 = InternalServerResponse +export type BulkUnassignMembersFromGuardrailRequestJson = { readonly "member_user_ids": ReadonlyArray } +export const BulkUnassignMembersFromGuardrailRequestJson = Schema.Struct({ + "member_user_ids": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ + "description": "Array of member user IDs to unassign from the guardrail" + }).check(Schema.isMinLength(1)) +}) +export type BulkUnassignMembersFromGuardrail200 = { readonly "unassigned_count": number } +export const BulkUnassignMembersFromGuardrail200 = Schema.Struct({ + "unassigned_count": Schema.Number.annotate({ "description": "Number of members successfully unassigned" }).check( + Schema.isFinite() + ) +}) +export type BulkUnassignMembersFromGuardrail400 = BadRequestResponse +export const BulkUnassignMembersFromGuardrail400 = BadRequestResponse +export type BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse +export const BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse +export type BulkUnassignMembersFromGuardrail404 = NotFoundResponse +export const BulkUnassignMembersFromGuardrail404 = NotFoundResponse +export type BulkUnassignMembersFromGuardrail500 = InternalServerResponse +export const BulkUnassignMembersFromGuardrail500 = InternalServerResponse +export type GetCurrentKey200 = { + readonly "data": { + readonly "label": string + readonly "limit": number + readonly "usage": number + readonly "usage_daily": number + readonly "usage_weekly": number + readonly "usage_monthly": number + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_weekly": number + readonly "byok_usage_monthly": number + readonly "is_free_tier": boolean + readonly "is_management_key": boolean + readonly "is_provisioning_key": boolean + readonly "limit_remaining": number + readonly "limit_reset": string + readonly "include_byok_in_limit": boolean + readonly "expires_at"?: string + readonly "rate_limit": { readonly "requests": number; readonly "interval": string; readonly "note": string } + } +} +export const GetCurrentKey200 = Schema.Struct({ + "data": Schema.Struct({ + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( + Schema.isFinite() + ), + "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) + .check(Schema.isFinite()), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month" + }).check(Schema.isFinite()), + "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( + Schema.isFinite() + ), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day" + }).check(Schema.isFinite()), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" + }).check(Schema.isFinite()), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month" + }).check(Schema.isFinite()), + "is_free_tier": Schema.Boolean.annotate({ "description": "Whether this is a free tier API key" }), + "is_management_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), + "is_provisioning_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), + "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( + Schema.isFinite() + ), + "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "rate_limit": Schema.Struct({ + "requests": Schema.Number.annotate({ "description": "Number of requests allowed per interval" }).check( + Schema.isFinite() + ), + "interval": Schema.String.annotate({ "description": "Rate limit interval" }), + "note": Schema.String.annotate({ "description": "Note about the rate limit" }) + }).annotate({ "description": "Legacy rate limit information about a key. Will always return -1." }) + }).annotate({ "description": "Current API key information" }) +}) +export type GetCurrentKey401 = UnauthorizedResponse +export const GetCurrentKey401 = UnauthorizedResponse +export type GetCurrentKey500 = InternalServerResponse +export const GetCurrentKey500 = InternalServerResponse +export type ExchangeAuthCodeForAPIKeyRequestJson = { + readonly "code": string + readonly "code_verifier"?: string + readonly "code_challenge_method"?: "S256" | "plain" +} +export const ExchangeAuthCodeForAPIKeyRequestJson = Schema.Struct({ + "code": Schema.String.annotate({ "description": "The authorization code received from the OAuth redirect" }), + "code_verifier": Schema.optionalKey( + Schema.String.annotate({ + "description": "The code verifier if code_challenge was used in the authorization request" + }) + ), + "code_challenge_method": Schema.optionalKey( + Schema.Literals(["S256", "plain"]).annotate({ "description": "The method used to generate the code challenge" }) + ) +}) +export type ExchangeAuthCodeForAPIKey200 = { readonly "key": string; readonly "user_id": string } +export const ExchangeAuthCodeForAPIKey200 = Schema.Struct({ + "key": Schema.String.annotate({ "description": "The API key to use for OpenRouter requests" }), + "user_id": Schema.String.annotate({ "description": "User ID associated with the API key" }) +}) +export type ExchangeAuthCodeForAPIKey400 = BadRequestResponse +export const ExchangeAuthCodeForAPIKey400 = BadRequestResponse +export type ExchangeAuthCodeForAPIKey403 = ForbiddenResponse +export const ExchangeAuthCodeForAPIKey403 = ForbiddenResponse +export type ExchangeAuthCodeForAPIKey500 = InternalServerResponse +export const ExchangeAuthCodeForAPIKey500 = InternalServerResponse +export type CreateAuthKeysCodeRequestJson = { + readonly "callback_url": string + readonly "code_challenge"?: string + readonly "code_challenge_method"?: "S256" | "plain" + readonly "limit"?: number + readonly "expires_at"?: string +} +export const CreateAuthKeysCodeRequestJson = Schema.Struct({ + "callback_url": Schema.String.annotate({ + "description": + "The callback URL to redirect to after authorization. Note, only https URLs on ports 443 and 3000 are allowed.", + "format": "uri" + }), + "code_challenge": Schema.optionalKey( + Schema.String.annotate({ "description": "PKCE code challenge for enhanced security" }) + ), + "code_challenge_method": Schema.optionalKey( + Schema.Literals(["S256", "plain"]).annotate({ "description": "The method used to generate the code challenge" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Credit limit for the API key to be created" }).check(Schema.isFinite()) + ), + "expires_at": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional expiration time for the API key to be created", + "format": "date-time" + }) + ) +}) +export type CreateAuthKeysCode200 = { + readonly "data": { readonly "id": string; readonly "app_id": number; readonly "created_at": string } +} +export const CreateAuthKeysCode200 = Schema.Struct({ + "data": Schema.Struct({ + "id": Schema.String.annotate({ "description": "The authorization code ID to use in the exchange request" }), + "app_id": Schema.Number.annotate({ "description": "The application ID associated with this auth code" }).check( + Schema.isFinite() + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the auth code was created" }) + }).annotate({ "description": "Auth code data" }) +}) +export type CreateAuthKeysCode400 = BadRequestResponse +export const CreateAuthKeysCode400 = BadRequestResponse +export type CreateAuthKeysCode401 = UnauthorizedResponse +export const CreateAuthKeysCode401 = UnauthorizedResponse +export type CreateAuthKeysCode500 = InternalServerResponse +export const CreateAuthKeysCode500 = InternalServerResponse +export type SendChatCompletionRequestRequestJson = ChatGenerationParams +export const SendChatCompletionRequestRequestJson = ChatGenerationParams +export type SendChatCompletionRequest200 = { + readonly "id": string + readonly "choices": ReadonlyArray + readonly "created": number + readonly "model": string + readonly "object": "chat.completion" + readonly "system_fingerprint"?: string | null + readonly "usage"?: ChatGenerationTokenUsage +} +export const SendChatCompletionRequest200 = Schema.Struct({ + "id": Schema.String, + "choices": Schema.Array(ChatResponseChoice), + "created": Schema.Number.check(Schema.isFinite()), + "model": Schema.String, + "object": Schema.Literal("chat.completion"), + "system_fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "usage": Schema.optionalKey(ChatGenerationTokenUsage) +}).annotate({ "description": "Chat completion response" }) +export type SendChatCompletionRequest200Sse = ChatStreamingResponseChunk +export const SendChatCompletionRequest200Sse = ChatStreamingResponseChunk +export type SendChatCompletionRequest400 = ChatError +export const SendChatCompletionRequest400 = ChatError +export type SendChatCompletionRequest401 = ChatError +export const SendChatCompletionRequest401 = ChatError +export type SendChatCompletionRequest429 = ChatError +export const SendChatCompletionRequest429 = ChatError +export type SendChatCompletionRequest500 = ChatError +export const SendChatCompletionRequest500 = ChatError + +export interface OperationConfig { + /** + * Whether or not the response should be included in the value returned from + * an operation. + * + * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, + * where `A` is the success type of the operation. + * + * If set to `false`, only the success type of the operation will be returned. + */ + readonly includeResponse?: boolean | undefined +} + +/** + * A utility type which optionally includes the response in the return result + * of an operation based upon the value of the `includeResponse` configuration + * option. + */ +export type WithOptionalResponse = Config extends { + readonly includeResponse: true +} ? [A, HttpClientResponse.HttpClientResponse] : + A + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): OpenRouterClient => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description) + }) + }) + ) + ) + const withResponse = (config: Config | undefined) => + ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { + const withOptionalResponse = ( + config?.includeResponse + ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) + : (response: HttpClientResponse.HttpClientResponse) => f(response) + ) as any + return options?.transformClient + ? (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + withOptionalResponse + ) + : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + } + const sseRequest = < + Type, + DecodingServices + >( + schema: Schema.ConstraintDecoder + ) => + ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + DecodingServices + > => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) + ) + const decodeSuccess = + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(OpenRouterClientError(tag, cause, response)) + ) + return { + httpClient, + "createResponses": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateResponses200), + "400": decodeError("CreateResponses400", CreateResponses400), + "401": decodeError("CreateResponses401", CreateResponses401), + "402": decodeError("CreateResponses402", CreateResponses402), + "404": decodeError("CreateResponses404", CreateResponses404), + "408": decodeError("CreateResponses408", CreateResponses408), + "413": decodeError("CreateResponses413", CreateResponses413), + "422": decodeError("CreateResponses422", CreateResponses422), + "429": decodeError("CreateResponses429", CreateResponses429), + "500": decodeError("CreateResponses500", CreateResponses500), + "502": decodeError("CreateResponses502", CreateResponses502), + "503": decodeError("CreateResponses503", CreateResponses503), + "524": decodeError("CreateResponses524", CreateResponses524), + "529": decodeError("CreateResponses529", CreateResponses529), + orElse: unexpectedStatus + })) + ), + "createResponsesSse": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateResponses200Sse) + ), + "createMessages": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateMessages200), + "400": decodeError("CreateMessages400", CreateMessages400), + "401": decodeError("CreateMessages401", CreateMessages401), + "403": decodeError("CreateMessages403", CreateMessages403), + "404": decodeError("CreateMessages404", CreateMessages404), + "429": decodeError("CreateMessages429", CreateMessages429), + "500": decodeError("CreateMessages500", CreateMessages500), + "503": decodeError("CreateMessages503", CreateMessages503), + "529": decodeError("CreateMessages529", CreateMessages529), + orElse: unexpectedStatus + })) + ), + "createMessagesSse": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateMessages200Sse) + ), + "getUserActivity": (options) => + HttpClientRequest.get(`/activity`).pipe( + HttpClientRequest.setUrlParams({ "date": options?.params?.["date"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetUserActivity200), + "400": decodeError("GetUserActivity400", GetUserActivity400), + "401": decodeError("GetUserActivity401", GetUserActivity401), + "403": decodeError("GetUserActivity403", GetUserActivity403), + "500": decodeError("GetUserActivity500", GetUserActivity500), + orElse: unexpectedStatus + })) + ), + "getCredits": (options) => + HttpClientRequest.get(`/credits`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCredits200), + "401": decodeError("GetCredits401", GetCredits401), + "403": decodeError("GetCredits403", GetCredits403), + "500": decodeError("GetCredits500", GetCredits500), + orElse: unexpectedStatus + })) + ), + "createCoinbaseCharge": (options) => + HttpClientRequest.post(`/credits/coinbase`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCoinbaseCharge200), + "400": decodeError("CreateCoinbaseCharge400", CreateCoinbaseCharge400), + "401": decodeError("CreateCoinbaseCharge401", CreateCoinbaseCharge401), + "429": decodeError("CreateCoinbaseCharge429", CreateCoinbaseCharge429), + "500": decodeError("CreateCoinbaseCharge500", CreateCoinbaseCharge500), + orElse: unexpectedStatus + })) + ), + "createEmbeddings": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEmbeddings200), + "400": decodeError("CreateEmbeddings400", CreateEmbeddings400), + "401": decodeError("CreateEmbeddings401", CreateEmbeddings401), + "402": decodeError("CreateEmbeddings402", CreateEmbeddings402), + "404": decodeError("CreateEmbeddings404", CreateEmbeddings404), + "429": decodeError("CreateEmbeddings429", CreateEmbeddings429), + "500": decodeError("CreateEmbeddings500", CreateEmbeddings500), + "502": decodeError("CreateEmbeddings502", CreateEmbeddings502), + "503": decodeError("CreateEmbeddings503", CreateEmbeddings503), + "524": decodeError("CreateEmbeddings524", CreateEmbeddings524), + "529": decodeError("CreateEmbeddings529", CreateEmbeddings529), + orElse: unexpectedStatus + })) + ), + "createEmbeddingsSse": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateEmbeddings200Sse) + ), + "listEmbeddingsModels": (options) => + HttpClientRequest.get(`/embeddings/models`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEmbeddingsModels200), + "400": decodeError("ListEmbeddingsModels400", ListEmbeddingsModels400), + "500": decodeError("ListEmbeddingsModels500", ListEmbeddingsModels500), + orElse: unexpectedStatus + })) + ), + "getGeneration": (options) => + HttpClientRequest.get(`/generation`).pipe( + HttpClientRequest.setUrlParams({ "id": options.params["id"] as any }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGeneration200), + "401": decodeError("GetGeneration401", GetGeneration401), + "402": decodeError("GetGeneration402", GetGeneration402), + "404": decodeError("GetGeneration404", GetGeneration404), + "429": decodeError("GetGeneration429", GetGeneration429), + "500": decodeError("GetGeneration500", GetGeneration500), + "502": decodeError("GetGeneration502", GetGeneration502), + "524": decodeError("GetGeneration524", GetGeneration524), + "529": decodeError("GetGeneration529", GetGeneration529), + orElse: unexpectedStatus + })) + ), + "listModelsCount": (options) => + HttpClientRequest.get(`/models/count`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModelsCount200), + "500": decodeError("ListModelsCount500", ListModelsCount500), + orElse: unexpectedStatus + })) + ), + "getModels": (options) => + HttpClientRequest.get(`/models`).pipe( + HttpClientRequest.setUrlParams({ + "category": options?.params?.["category"] as any, + "supported_parameters": options?.params?.["supported_parameters"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetModels200), + "400": decodeError("GetModels400", GetModels400), + "500": decodeError("GetModels500", GetModels500), + orElse: unexpectedStatus + })) + ), + "listModelsUser": (options) => + HttpClientRequest.get(`/models/user`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModelsUser200), + "401": decodeError("ListModelsUser401", ListModelsUser401), + "404": decodeError("ListModelsUser404", ListModelsUser404), + "500": decodeError("ListModelsUser500", ListModelsUser500), + orElse: unexpectedStatus + })) + ), + "listEndpoints": (author, slug, options) => + HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpoints200), + "404": decodeError("ListEndpoints404", ListEndpoints404), + "500": decodeError("ListEndpoints500", ListEndpoints500), + orElse: unexpectedStatus + })) + ), + "listEndpointsZdr": (options) => + HttpClientRequest.get(`/endpoints/zdr`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpointsZdr200), + "500": decodeError("ListEndpointsZdr500", ListEndpointsZdr500), + orElse: unexpectedStatus + })) + ), + "listProviders": (options) => + HttpClientRequest.get(`/providers`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProviders200), + "500": decodeError("ListProviders500", ListProviders500), + orElse: unexpectedStatus + })) + ), + "list": (options) => + HttpClientRequest.get(`/keys`).pipe( + HttpClientRequest.setUrlParams({ + "include_disabled": options?.params?.["include_disabled"] as any, + "offset": options?.params?.["offset"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(List200), + "401": decodeError("List401", List401), + "429": decodeError("List429", List429), + "500": decodeError("List500", List500), + orElse: unexpectedStatus + })) + ), + "createKeys": (options) => + HttpClientRequest.post(`/keys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateKeys201), + "400": decodeError("CreateKeys400", CreateKeys400), + "401": decodeError("CreateKeys401", CreateKeys401), + "429": decodeError("CreateKeys429", CreateKeys429), + "500": decodeError("CreateKeys500", CreateKeys500), + orElse: unexpectedStatus + })) + ), + "getKey": (hash, options) => + HttpClientRequest.get(`/keys/${hash}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetKey200), + "401": decodeError("GetKey401", GetKey401), + "404": decodeError("GetKey404", GetKey404), + "429": decodeError("GetKey429", GetKey429), + "500": decodeError("GetKey500", GetKey500), + orElse: unexpectedStatus + })) + ), + "deleteKeys": (hash, options) => + HttpClientRequest.delete(`/keys/${hash}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteKeys200), + "401": decodeError("DeleteKeys401", DeleteKeys401), + "404": decodeError("DeleteKeys404", DeleteKeys404), + "429": decodeError("DeleteKeys429", DeleteKeys429), + "500": decodeError("DeleteKeys500", DeleteKeys500), + orElse: unexpectedStatus + })) + ), + "updateKeys": (hash, options) => + HttpClientRequest.patch(`/keys/${hash}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateKeys200), + "400": decodeError("UpdateKeys400", UpdateKeys400), + "401": decodeError("UpdateKeys401", UpdateKeys401), + "404": decodeError("UpdateKeys404", UpdateKeys404), + "429": decodeError("UpdateKeys429", UpdateKeys429), + "500": decodeError("UpdateKeys500", UpdateKeys500), + orElse: unexpectedStatus + })) + ), + "listGuardrails": (options) => + HttpClientRequest.get(`/guardrails`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrails200), + "401": decodeError("ListGuardrails401", ListGuardrails401), + "500": decodeError("ListGuardrails500", ListGuardrails500), + orElse: unexpectedStatus + })) + ), + "createGuardrail": (options) => + HttpClientRequest.post(`/guardrails`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateGuardrail201), + "400": decodeError("CreateGuardrail400", CreateGuardrail400), + "401": decodeError("CreateGuardrail401", CreateGuardrail401), + "500": decodeError("CreateGuardrail500", CreateGuardrail500), + orElse: unexpectedStatus + })) + ), + "getGuardrail": (id, options) => + HttpClientRequest.get(`/guardrails/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGuardrail200), + "401": decodeError("GetGuardrail401", GetGuardrail401), + "404": decodeError("GetGuardrail404", GetGuardrail404), + "500": decodeError("GetGuardrail500", GetGuardrail500), + orElse: unexpectedStatus + })) + ), + "deleteGuardrail": (id, options) => + HttpClientRequest.delete(`/guardrails/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteGuardrail200), + "401": decodeError("DeleteGuardrail401", DeleteGuardrail401), + "404": decodeError("DeleteGuardrail404", DeleteGuardrail404), + "500": decodeError("DeleteGuardrail500", DeleteGuardrail500), + orElse: unexpectedStatus + })) + ), + "updateGuardrail": (id, options) => + HttpClientRequest.patch(`/guardrails/${id}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateGuardrail200), + "400": decodeError("UpdateGuardrail400", UpdateGuardrail400), + "401": decodeError("UpdateGuardrail401", UpdateGuardrail401), + "404": decodeError("UpdateGuardrail404", UpdateGuardrail404), + "500": decodeError("UpdateGuardrail500", UpdateGuardrail500), + orElse: unexpectedStatus + })) + ), + "listKeyAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListKeyAssignments200), + "401": decodeError("ListKeyAssignments401", ListKeyAssignments401), + "500": decodeError("ListKeyAssignments500", ListKeyAssignments500), + orElse: unexpectedStatus + })) + ), + "listMemberAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMemberAssignments200), + "401": decodeError("ListMemberAssignments401", ListMemberAssignments401), + "500": decodeError("ListMemberAssignments500", ListMemberAssignments500), + orElse: unexpectedStatus + })) + ), + "listGuardrailKeyAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailKeyAssignments200), + "401": decodeError("ListGuardrailKeyAssignments401", ListGuardrailKeyAssignments401), + "404": decodeError("ListGuardrailKeyAssignments404", ListGuardrailKeyAssignments404), + "500": decodeError("ListGuardrailKeyAssignments500", ListGuardrailKeyAssignments500), + orElse: unexpectedStatus + })) + ), + "bulkAssignKeysToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), + "400": decodeError("BulkAssignKeysToGuardrail400", BulkAssignKeysToGuardrail400), + "401": decodeError("BulkAssignKeysToGuardrail401", BulkAssignKeysToGuardrail401), + "404": decodeError("BulkAssignKeysToGuardrail404", BulkAssignKeysToGuardrail404), + "500": decodeError("BulkAssignKeysToGuardrail500", BulkAssignKeysToGuardrail500), + orElse: unexpectedStatus + })) + ), + "listGuardrailMemberAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailMemberAssignments200), + "401": decodeError("ListGuardrailMemberAssignments401", ListGuardrailMemberAssignments401), + "404": decodeError("ListGuardrailMemberAssignments404", ListGuardrailMemberAssignments404), + "500": decodeError("ListGuardrailMemberAssignments500", ListGuardrailMemberAssignments500), + orElse: unexpectedStatus + })) + ), + "bulkAssignMembersToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), + "400": decodeError("BulkAssignMembersToGuardrail400", BulkAssignMembersToGuardrail400), + "401": decodeError("BulkAssignMembersToGuardrail401", BulkAssignMembersToGuardrail401), + "404": decodeError("BulkAssignMembersToGuardrail404", BulkAssignMembersToGuardrail404), + "500": decodeError("BulkAssignMembersToGuardrail500", BulkAssignMembersToGuardrail500), + orElse: unexpectedStatus + })) + ), + "bulkUnassignKeysFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), + "400": decodeError("BulkUnassignKeysFromGuardrail400", BulkUnassignKeysFromGuardrail400), + "401": decodeError("BulkUnassignKeysFromGuardrail401", BulkUnassignKeysFromGuardrail401), + "404": decodeError("BulkUnassignKeysFromGuardrail404", BulkUnassignKeysFromGuardrail404), + "500": decodeError("BulkUnassignKeysFromGuardrail500", BulkUnassignKeysFromGuardrail500), + orElse: unexpectedStatus + })) + ), + "bulkUnassignMembersFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), + "400": decodeError("BulkUnassignMembersFromGuardrail400", BulkUnassignMembersFromGuardrail400), + "401": decodeError("BulkUnassignMembersFromGuardrail401", BulkUnassignMembersFromGuardrail401), + "404": decodeError("BulkUnassignMembersFromGuardrail404", BulkUnassignMembersFromGuardrail404), + "500": decodeError("BulkUnassignMembersFromGuardrail500", BulkUnassignMembersFromGuardrail500), + orElse: unexpectedStatus + })) + ), + "getCurrentKey": (options) => + HttpClientRequest.get(`/key`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCurrentKey200), + "401": decodeError("GetCurrentKey401", GetCurrentKey401), + "500": decodeError("GetCurrentKey500", GetCurrentKey500), + orElse: unexpectedStatus + })) + ), + "exchangeAuthCodeForAPIKey": (options) => + HttpClientRequest.post(`/auth/keys`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), + "400": decodeError("ExchangeAuthCodeForAPIKey400", ExchangeAuthCodeForAPIKey400), + "403": decodeError("ExchangeAuthCodeForAPIKey403", ExchangeAuthCodeForAPIKey403), + "500": decodeError("ExchangeAuthCodeForAPIKey500", ExchangeAuthCodeForAPIKey500), + orElse: unexpectedStatus + })) + ), + "createAuthKeysCode": (options) => + HttpClientRequest.post(`/auth/keys/code`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateAuthKeysCode200), + "400": decodeError("CreateAuthKeysCode400", CreateAuthKeysCode400), + "401": decodeError("CreateAuthKeysCode401", CreateAuthKeysCode401), + "500": decodeError("CreateAuthKeysCode500", CreateAuthKeysCode500), + orElse: unexpectedStatus + })) + ), + "sendChatCompletionRequest": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SendChatCompletionRequest200), + "400": decodeError("SendChatCompletionRequest400", SendChatCompletionRequest400), + "401": decodeError("SendChatCompletionRequest401", SendChatCompletionRequest401), + "429": decodeError("SendChatCompletionRequest429", SendChatCompletionRequest429), + "500": decodeError("SendChatCompletionRequest500", SendChatCompletionRequest500), + orElse: unexpectedStatus + })) + ), + "sendChatCompletionRequestSse": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(SendChatCompletionRequest200Sse) + ) + } +} + +export interface OpenRouterClient { + readonly httpClient: HttpClient.HttpClient + /** + * Creates a streaming or non-streaming response using OpenResponses API format + */ + readonly "createResponses": ( + options: { readonly payload: typeof CreateResponsesRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateResponses400", typeof CreateResponses400.Type> + | OpenRouterClientError<"CreateResponses401", typeof CreateResponses401.Type> + | OpenRouterClientError<"CreateResponses402", typeof CreateResponses402.Type> + | OpenRouterClientError<"CreateResponses404", typeof CreateResponses404.Type> + | OpenRouterClientError<"CreateResponses408", typeof CreateResponses408.Type> + | OpenRouterClientError<"CreateResponses413", typeof CreateResponses413.Type> + | OpenRouterClientError<"CreateResponses422", typeof CreateResponses422.Type> + | OpenRouterClientError<"CreateResponses429", typeof CreateResponses429.Type> + | OpenRouterClientError<"CreateResponses500", typeof CreateResponses500.Type> + | OpenRouterClientError<"CreateResponses502", typeof CreateResponses502.Type> + | OpenRouterClientError<"CreateResponses503", typeof CreateResponses503.Type> + | OpenRouterClientError<"CreateResponses524", typeof CreateResponses524.Type> + | OpenRouterClientError<"CreateResponses529", typeof CreateResponses529.Type> + > + /** + * Creates a streaming or non-streaming response using OpenResponses API format + */ + readonly "createResponsesSse": ( + options: { readonly payload: typeof CreateResponsesRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateResponses200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateResponses200Sse.DecodingServices + > + /** + * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. + */ + readonly "createMessages": ( + options: { readonly payload: typeof CreateMessagesRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateMessages400", typeof CreateMessages400.Type> + | OpenRouterClientError<"CreateMessages401", typeof CreateMessages401.Type> + | OpenRouterClientError<"CreateMessages403", typeof CreateMessages403.Type> + | OpenRouterClientError<"CreateMessages404", typeof CreateMessages404.Type> + | OpenRouterClientError<"CreateMessages429", typeof CreateMessages429.Type> + | OpenRouterClientError<"CreateMessages500", typeof CreateMessages500.Type> + | OpenRouterClientError<"CreateMessages503", typeof CreateMessages503.Type> + | OpenRouterClientError<"CreateMessages529", typeof CreateMessages529.Type> + > + /** + * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. + */ + readonly "createMessagesSse": ( + options: { readonly payload: typeof CreateMessagesRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateMessages200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateMessages200Sse.DecodingServices + > + /** + * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getUserActivity": ( + options: { + readonly params?: typeof GetUserActivityParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetUserActivity400", typeof GetUserActivity400.Type> + | OpenRouterClientError<"GetUserActivity401", typeof GetUserActivity401.Type> + | OpenRouterClientError<"GetUserActivity403", typeof GetUserActivity403.Type> + | OpenRouterClientError<"GetUserActivity500", typeof GetUserActivity500.Type> + > + /** + * Get total credits purchased and used for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getCredits": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetCredits401", typeof GetCredits401.Type> + | OpenRouterClientError<"GetCredits403", typeof GetCredits403.Type> + | OpenRouterClientError<"GetCredits500", typeof GetCredits500.Type> + > + /** + * Create a Coinbase charge for crypto payment + */ + readonly "createCoinbaseCharge": ( + options: { readonly payload: typeof CreateCoinbaseChargeRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateCoinbaseCharge400", typeof CreateCoinbaseCharge400.Type> + | OpenRouterClientError<"CreateCoinbaseCharge401", typeof CreateCoinbaseCharge401.Type> + | OpenRouterClientError<"CreateCoinbaseCharge429", typeof CreateCoinbaseCharge429.Type> + | OpenRouterClientError<"CreateCoinbaseCharge500", typeof CreateCoinbaseCharge500.Type> + > + /** + * Submits an embedding request to the embeddings router + */ + readonly "createEmbeddings": ( + options: { readonly payload: typeof CreateEmbeddingsRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateEmbeddings400", typeof CreateEmbeddings400.Type> + | OpenRouterClientError<"CreateEmbeddings401", typeof CreateEmbeddings401.Type> + | OpenRouterClientError<"CreateEmbeddings402", typeof CreateEmbeddings402.Type> + | OpenRouterClientError<"CreateEmbeddings404", typeof CreateEmbeddings404.Type> + | OpenRouterClientError<"CreateEmbeddings429", typeof CreateEmbeddings429.Type> + | OpenRouterClientError<"CreateEmbeddings500", typeof CreateEmbeddings500.Type> + | OpenRouterClientError<"CreateEmbeddings502", typeof CreateEmbeddings502.Type> + | OpenRouterClientError<"CreateEmbeddings503", typeof CreateEmbeddings503.Type> + | OpenRouterClientError<"CreateEmbeddings524", typeof CreateEmbeddings524.Type> + | OpenRouterClientError<"CreateEmbeddings529", typeof CreateEmbeddings529.Type> + > + /** + * Submits an embedding request to the embeddings router + */ + readonly "createEmbeddingsSse": ( + options: { readonly payload: typeof CreateEmbeddingsRequestJson.Encoded } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateEmbeddings200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof CreateEmbeddings200Sse.DecodingServices + > + /** + * Returns a list of all available embeddings models and their properties + */ + readonly "listEmbeddingsModels": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListEmbeddingsModels400", typeof ListEmbeddingsModels400.Type> + | OpenRouterClientError<"ListEmbeddingsModels500", typeof ListEmbeddingsModels500.Type> + > + /** + * Get request & usage metadata for a generation + */ + readonly "getGeneration": ( + options: { readonly params: typeof GetGenerationParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetGeneration401", typeof GetGeneration401.Type> + | OpenRouterClientError<"GetGeneration402", typeof GetGeneration402.Type> + | OpenRouterClientError<"GetGeneration404", typeof GetGeneration404.Type> + | OpenRouterClientError<"GetGeneration429", typeof GetGeneration429.Type> + | OpenRouterClientError<"GetGeneration500", typeof GetGeneration500.Type> + | OpenRouterClientError<"GetGeneration502", typeof GetGeneration502.Type> + | OpenRouterClientError<"GetGeneration524", typeof GetGeneration524.Type> + | OpenRouterClientError<"GetGeneration529", typeof GetGeneration529.Type> + > + /** + * Get total count of available models + */ + readonly "listModelsCount": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListModelsCount500", typeof ListModelsCount500.Type> + > + /** + * List all models and their properties + */ + readonly "getModels": ( + options: + | { readonly params?: typeof GetModelsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetModels400", typeof GetModels400.Type> + | OpenRouterClientError<"GetModels500", typeof GetModels500.Type> + > + /** + * List models filtered by user provider preferences, [privacy settings](https://openrouter.ai/docs/guides/privacy/logging), and [guardrails](https://openrouter.ai/docs/guides/features/guardrails). If requesting through `eu.openrouter.ai/api/v1/...` the results will be filtered to models that satisfy [EU in-region routing](https://openrouter.ai/docs/guides/privacy/logging#enterprise-eu-in-region-routing). + */ + readonly "listModelsUser": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListModelsUser401", typeof ListModelsUser401.Type> + | OpenRouterClientError<"ListModelsUser404", typeof ListModelsUser404.Type> + | OpenRouterClientError<"ListModelsUser500", typeof ListModelsUser500.Type> + > + /** + * List all endpoints for a model + */ + readonly "listEndpoints": ( + author: string, + slug: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListEndpoints404", typeof ListEndpoints404.Type> + | OpenRouterClientError<"ListEndpoints500", typeof ListEndpoints500.Type> + > + /** + * Preview the impact of ZDR on the available endpoints + */ + readonly "listEndpointsZdr": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListEndpointsZdr500", typeof ListEndpointsZdr500.Type> + > + /** + * List all providers + */ + readonly "listProviders": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListProviders500", typeof ListProviders500.Type> + > + /** + * List all API keys for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "list": ( + options: + | { readonly params?: typeof ListParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"List401", typeof List401.Type> + | OpenRouterClientError<"List429", typeof List429.Type> + | OpenRouterClientError<"List500", typeof List500.Type> + > + /** + * Create a new API key for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "createKeys": ( + options: { readonly payload: typeof CreateKeysRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateKeys400", typeof CreateKeys400.Type> + | OpenRouterClientError<"CreateKeys401", typeof CreateKeys401.Type> + | OpenRouterClientError<"CreateKeys429", typeof CreateKeys429.Type> + | OpenRouterClientError<"CreateKeys500", typeof CreateKeys500.Type> + > + /** + * Get a single API key by hash. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getKey": ( + hash: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetKey401", typeof GetKey401.Type> + | OpenRouterClientError<"GetKey404", typeof GetKey404.Type> + | OpenRouterClientError<"GetKey429", typeof GetKey429.Type> + | OpenRouterClientError<"GetKey500", typeof GetKey500.Type> + > + /** + * Delete an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteKeys": ( + hash: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteKeys401", typeof DeleteKeys401.Type> + | OpenRouterClientError<"DeleteKeys404", typeof DeleteKeys404.Type> + | OpenRouterClientError<"DeleteKeys429", typeof DeleteKeys429.Type> + | OpenRouterClientError<"DeleteKeys500", typeof DeleteKeys500.Type> + > + /** + * Update an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "updateKeys": ( + hash: string, + options: { readonly payload: typeof UpdateKeysRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UpdateKeys400", typeof UpdateKeys400.Type> + | OpenRouterClientError<"UpdateKeys401", typeof UpdateKeys401.Type> + | OpenRouterClientError<"UpdateKeys404", typeof UpdateKeys404.Type> + | OpenRouterClientError<"UpdateKeys429", typeof UpdateKeys429.Type> + | OpenRouterClientError<"UpdateKeys500", typeof UpdateKeys500.Type> + > + /** + * List all guardrails for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrails": ( + options: + | { readonly params?: typeof ListGuardrailsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrails401", typeof ListGuardrails401.Type> + | OpenRouterClientError<"ListGuardrails500", typeof ListGuardrails500.Type> + > + /** + * Create a new guardrail for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "createGuardrail": ( + options: { readonly payload: typeof CreateGuardrailRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateGuardrail400", typeof CreateGuardrail400.Type> + | OpenRouterClientError<"CreateGuardrail401", typeof CreateGuardrail401.Type> + | OpenRouterClientError<"CreateGuardrail500", typeof CreateGuardrail500.Type> + > + /** + * Get a single guardrail by ID. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getGuardrail": ( + id: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetGuardrail401", typeof GetGuardrail401.Type> + | OpenRouterClientError<"GetGuardrail404", typeof GetGuardrail404.Type> + | OpenRouterClientError<"GetGuardrail500", typeof GetGuardrail500.Type> + > + /** + * Delete an existing guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteGuardrail": ( + id: string, + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteGuardrail401", typeof DeleteGuardrail401.Type> + | OpenRouterClientError<"DeleteGuardrail404", typeof DeleteGuardrail404.Type> + | OpenRouterClientError<"DeleteGuardrail500", typeof DeleteGuardrail500.Type> + > + /** + * Update an existing guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "updateGuardrail": ( + id: string, + options: { readonly payload: typeof UpdateGuardrailRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UpdateGuardrail400", typeof UpdateGuardrail400.Type> + | OpenRouterClientError<"UpdateGuardrail401", typeof UpdateGuardrail401.Type> + | OpenRouterClientError<"UpdateGuardrail404", typeof UpdateGuardrail404.Type> + | OpenRouterClientError<"UpdateGuardrail500", typeof UpdateGuardrail500.Type> + > + /** + * List all API key guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listKeyAssignments": ( + options: { + readonly params?: typeof ListKeyAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListKeyAssignments401", typeof ListKeyAssignments401.Type> + | OpenRouterClientError<"ListKeyAssignments500", typeof ListKeyAssignments500.Type> + > + /** + * List all organization member guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listMemberAssignments": ( + options: { + readonly params?: typeof ListMemberAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListMemberAssignments401", typeof ListMemberAssignments401.Type> + | OpenRouterClientError<"ListMemberAssignments500", typeof ListMemberAssignments500.Type> + > + /** + * List all API key assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrailKeyAssignments": ( + id: string, + options: { + readonly params?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrailKeyAssignments401", typeof ListGuardrailKeyAssignments401.Type> + | OpenRouterClientError<"ListGuardrailKeyAssignments404", typeof ListGuardrailKeyAssignments404.Type> + | OpenRouterClientError<"ListGuardrailKeyAssignments500", typeof ListGuardrailKeyAssignments500.Type> + > + /** + * Assign multiple API keys to a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkAssignKeysToGuardrail": ( + id: string, + options: { + readonly payload: typeof BulkAssignKeysToGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkAssignKeysToGuardrail400", typeof BulkAssignKeysToGuardrail400.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail401", typeof BulkAssignKeysToGuardrail401.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail404", typeof BulkAssignKeysToGuardrail404.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail500", typeof BulkAssignKeysToGuardrail500.Type> + > + /** + * List all organization member assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrailMemberAssignments": ( + id: string, + options: { + readonly params?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrailMemberAssignments401", typeof ListGuardrailMemberAssignments401.Type> + | OpenRouterClientError<"ListGuardrailMemberAssignments404", typeof ListGuardrailMemberAssignments404.Type> + | OpenRouterClientError<"ListGuardrailMemberAssignments500", typeof ListGuardrailMemberAssignments500.Type> + > + /** + * Assign multiple organization members to a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkAssignMembersToGuardrail": ( + id: string, + options: { + readonly payload: typeof BulkAssignMembersToGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkAssignMembersToGuardrail400", typeof BulkAssignMembersToGuardrail400.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail401", typeof BulkAssignMembersToGuardrail401.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail404", typeof BulkAssignMembersToGuardrail404.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail500", typeof BulkAssignMembersToGuardrail500.Type> + > + /** + * Unassign multiple API keys from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkUnassignKeysFromGuardrail": ( + id: string, + options: { + readonly payload: typeof BulkUnassignKeysFromGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail400", typeof BulkUnassignKeysFromGuardrail400.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail401", typeof BulkUnassignKeysFromGuardrail401.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail404", typeof BulkUnassignKeysFromGuardrail404.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail500", typeof BulkUnassignKeysFromGuardrail500.Type> + > + /** + * Unassign multiple organization members from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkUnassignMembersFromGuardrail": ( + id: string, + options: { + readonly payload: typeof BulkUnassignMembersFromGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail400", typeof BulkUnassignMembersFromGuardrail400.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail401", typeof BulkUnassignMembersFromGuardrail401.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail404", typeof BulkUnassignMembersFromGuardrail404.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail500", typeof BulkUnassignMembersFromGuardrail500.Type> + > + /** + * Get information on the API key associated with the current authentication session + */ + readonly "getCurrentKey": ( + options: { readonly config?: Config | undefined } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetCurrentKey401", typeof GetCurrentKey401.Type> + | OpenRouterClientError<"GetCurrentKey500", typeof GetCurrentKey500.Type> + > + /** + * Exchange an authorization code from the PKCE flow for a user-controlled API key + */ + readonly "exchangeAuthCodeForAPIKey": ( + options: { + readonly payload: typeof ExchangeAuthCodeForAPIKeyRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey400", typeof ExchangeAuthCodeForAPIKey400.Type> + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey403", typeof ExchangeAuthCodeForAPIKey403.Type> + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey500", typeof ExchangeAuthCodeForAPIKey500.Type> + > + /** + * Create an authorization code for the PKCE flow to generate a user-controlled API key + */ + readonly "createAuthKeysCode": ( + options: { readonly payload: typeof CreateAuthKeysCodeRequestJson.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateAuthKeysCode400", typeof CreateAuthKeysCode400.Type> + | OpenRouterClientError<"CreateAuthKeysCode401", typeof CreateAuthKeysCode401.Type> + | OpenRouterClientError<"CreateAuthKeysCode500", typeof CreateAuthKeysCode500.Type> + > + /** + * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + */ + readonly "sendChatCompletionRequest": ( + options: { + readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"SendChatCompletionRequest400", typeof SendChatCompletionRequest400.Type> + | OpenRouterClientError<"SendChatCompletionRequest401", typeof SendChatCompletionRequest401.Type> + | OpenRouterClientError<"SendChatCompletionRequest429", typeof SendChatCompletionRequest429.Type> + | OpenRouterClientError<"SendChatCompletionRequest500", typeof SendChatCompletionRequest500.Type> + > + /** + * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + */ + readonly "sendChatCompletionRequestSse": ( + options: { readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded } + ) => Stream.Stream< + { + readonly event: string + readonly id: string | undefined + readonly data: typeof SendChatCompletionRequest200Sse.Type + }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + typeof SendChatCompletionRequest200Sse.DecodingServices + > +} + +export interface OpenRouterClientError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class OpenRouterClientErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const OpenRouterClientError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse +): OpenRouterClientError => + new OpenRouterClientErrorImpl({ + _tag: tag, + cause, + response, + request: response.request + }) as any diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts new file mode 100644 index 0000000..ec461a1 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts @@ -0,0 +1,357 @@ +/** + * The `OpenRouterClient` module provides an Effect service for calling + * OpenRouter's chat completions API. It wraps the generated OpenRouter HTTP + * client with Effect-native constructors, layers, authentication and optional + * site ranking headers, typed errors, and streaming support. + * + * @since 4.0.0 + */ +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import type * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import type * as AiError from "effect/unstable/ai/AiError" +import * as Sse from "effect/unstable/encoding/Sse" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as Generated from "./Generated.ts" +import * as Errors from "./internal/errors.ts" +import { OpenRouterConfig } from "./OpenRouterConfig.ts" + +// ============================================================================= +// Service Interface +// ============================================================================= + +/** + * The OpenRouter client service interface. + * + * **Details** + * + * Provides methods for interacting with OpenRouter's Chat Completions API, + * including both synchronous and streaming message creation. + * + * @category models + * @since 4.0.0 + */ +export interface Service { + readonly client: Generated.OpenRouterClient + + readonly createChatCompletion: ( + options: typeof Generated.ChatGenerationParams.Encoded + ) => Effect.Effect< + [body: typeof Generated.SendChatCompletionRequest200.Type, response: HttpClientResponse.HttpClientResponse], + AiError.AiError + > + + readonly createChatCompletionStream: ( + options: Omit + ) => Effect.Effect< + [ + response: HttpClientResponse.HttpClientResponse, + stream: Stream.Stream + ], + AiError.AiError + > +} + +/** + * Decoded `data` payload from an OpenRouter chat completion streaming chunk. + * + * **Details** + * + * The payload contains streamed choices, model metadata, optional usage, and may + * include an OpenRouter error object for a streamed response. + * + * @category models + * @since 4.0.0 + */ +export type ChatStreamingResponseChunkData = typeof Generated.ChatStreamingResponseChunk.fields.data.Type + +// ============================================================================= +// Service Identifier +// ============================================================================= + +/** + * Service tag for the OpenRouter client. + * + * **When to use** + * + * Use when accessing or providing the OpenRouter client service through + * Effect's context. + * + * @see {@link make} for constructing an OpenRouter client effectfully + * @see {@link layer} for providing a client from explicit options + * @see {@link layerConfig} for providing a client from `Config` + * + * @category services + * @since 4.0.0 + */ +export class OpenRouterClient extends Context.Service< + OpenRouterClient, + Service +>()("@effect/ai-openrouter/OpenRouterClient") {} + +// ============================================================================= +// Options +// ============================================================================= + +/** + * Configuration for creating an OpenRouter client. + * + * @category options + * @since 4.0.0 + */ +export type Options = { + readonly apiKey?: Redacted.Redacted | undefined + + readonly apiUrl?: string | undefined + + /** + * Optional URL of your site for rankings on `openrouter.ai`. + */ + readonly siteReferrer?: string | undefined + + /** + * Optional title of your site for rankings on `openrouter.ai`. + */ + readonly siteTitle?: string | undefined + + /** + * Optional transformer for the underlying HTTP client. + * + * **When to use** + * + * Use to add middleware, logging, or custom request/response handling. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +} + +// ============================================================================= +// Constructor +// ============================================================================= + +/** + * Creates an OpenRouter client service from explicit options. + * + * **When to use** + * + * Use when you need the OpenRouter client service value inside an effect. + * + * **Details** + * + * The returned service uses the current `HttpClient`, prepends `apiUrl` or + * `https://openrouter.ai/api/v1`, adds the bearer token and optional + * `HTTP-Referer` and `X-Title` headers, accepts JSON responses, and applies + * `transformClient` when provided. + * + * **Gotchas** + * + * Scoped `OpenRouterConfig.withClientTransform` applies to generated client + * request methods. Streaming chat completion requests are sent directly by this + * module and do not read that scoped transform. + * + * @see {@link layer} for providing this client from explicit options + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*(options: Options): Effect.fn.Return { + const baseClient = yield* HttpClient.HttpClient + + const httpClient = baseClient.pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? "https://openrouter.ai/api/v1"), + options.apiKey ? HttpClientRequest.bearerToken(options.apiKey) : identity, + options.siteReferrer ? HttpClientRequest.setHeader("HTTP-Referer", options.siteReferrer) : identity, + options.siteTitle ? HttpClientRequest.setHeader("X-Title", options.siteTitle) : identity, + HttpClientRequest.acceptJson + ) + ), + options.transformClient ?? identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = Generated.make(httpClient, { + transformClient: Effect.fnUntraced(function*(client) { + const config = yield* OpenRouterConfig.getOrUndefined + if (Predicate.isNotUndefined(config?.transformClient)) { + return config.transformClient(client) + } + return client + }) + }) + + const createChatCompletion: Service["createChatCompletion"] = (payload) => + client.sendChatCompletionRequest({ payload, config: { includeResponse: true } }).pipe( + Effect.catchTags({ + SendChatCompletionRequest400: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest401: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest429: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest500: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + HttpClientError: (error) => Errors.mapHttpClientError(error, "createChatCompletion"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createChatCompletion")) + }) + ) + + const buildChatCompletionStream = ( + response: HttpClientResponse.HttpClientResponse + ): [ + HttpClientResponse.HttpClientResponse, + Stream.Stream + ] => { + const stream = response.stream.pipe( + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decode()), + Stream.mapEffect((event) => decodeChatCompletionSseData(event.data)), + Stream.takeWhile((data) => data !== "[DONE]"), + Stream.catchTags({ + // TODO: handle SSE retries + Retry: (error) => Stream.die(error), + HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createChatCompletionStream")), + SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createChatCompletionStream")) + }) + ) as any + return [response, stream] + } + + const createChatCompletionStream: Service["createChatCompletionStream"] = (payload) => + httpClientOk.execute( + HttpClientRequest.post("/chat/completions", { + body: HttpBody.jsonUnsafe({ + ...payload, + stream: true, + stream_options: { include_usage: true } + }) + }) + ).pipe( + Effect.map(buildChatCompletionStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createChatCompletionStream") + ) + ) + + return OpenRouterClient.of({ + client, + createChatCompletion, + createChatCompletionStream + }) + } +) + +// ============================================================================= +// Layers +// ============================================================================= + +/** + * Creates a layer for the OpenRouter client with the given options. + * + * **When to use** + * + * Use when you already have the OpenRouter client options in code and want to + * provide `OpenRouterClient` as a layer. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layerConfig} for loading client settings from `Config` + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: Options): Layer.Layer => + Layer.effect(OpenRouterClient, make(options)) + +/** + * Creates a layer for the OpenRouter client from provided `Config` values. + * + * **When to use** + * + * Use when you need client settings for OpenRouter to be read from Effect + * `Config` values while providing `OpenRouterClient` as a layer. + * + * **Details** + * + * Only config values supplied in `options` are loaded. Omitted fields are + * passed to `make` as `undefined`, and `transformClient` is forwarded as a + * plain option. + * + * @see {@link make} for constructing the client service effectfully + * @see {@link layer} for providing the client from already-resolved options + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = (options?: { + /** + * The config value to load for the API key. + */ + readonly apiKey?: Config.Config | undefined> | undefined + + /** + * The config value to load for the API URL. + */ + readonly apiUrl?: Config.Config | undefined + + /** + * The config value to load for the site referrer URL. + */ + readonly siteReferrer?: Config.Config | undefined + + /** + * The config value to load for the site title. + */ + readonly siteTitle?: Config.Config | undefined + + /** + * Optional transformer for the HTTP client. + */ + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined +}): Layer.Layer => + Layer.effect( + OpenRouterClient, + Effect.gen(function*() { + const apiKey = Predicate.isNotUndefined(options?.apiKey) + ? yield* options.apiKey + : undefined + const apiUrl = Predicate.isNotUndefined(options?.apiUrl) + ? yield* options.apiUrl + : undefined + const siteReferrer = Predicate.isNotUndefined(options?.siteReferrer) + ? yield* options.siteReferrer + : undefined + const siteTitle = Predicate.isNotUndefined(options?.siteTitle) + ? yield* options.siteTitle + : undefined + return yield* make({ + apiKey, + apiUrl, + siteReferrer, + siteTitle, + transformClient: options?.transformClient + }) + }) + ) + +// ============================================================================= +// Internal Utilities +// ============================================================================= + +const ChatStreamingResponseChunkDataFromString = Schema.fromJsonString(Generated.ChatStreamingResponseChunk.fields.data) +const decodeChatStreamingResponseChunkData = Schema.decodeUnknownEffect(ChatStreamingResponseChunkDataFromString) + +const decodeChatCompletionSseData = ( + data: string +): Effect.Effect => + data === "[DONE]" + ? Effect.succeed(data) + : decodeChatStreamingResponseChunkData(data) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts new file mode 100644 index 0000000..7ca9978 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts @@ -0,0 +1,99 @@ +/** + * The `OpenRouterConfig` module lets a workflow temporarily customize the HTTP + * client used by generated OpenRouter request methods. `OpenRouterClient` reads + * this scoped transform when generated client operations execute, so callers can + * add middleware or instrumentation without rebuilding the client layer. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * Context service for scoped OpenRouter provider configuration used by client + * operations. + * + * **When to use** + * + * Use as the context service tag when manually providing or reading scoped + * OpenRouter provider configuration in an Effect context. + * + * @see {@link withClientTransform} for scoping an HTTP client transformation + * + * @category services + * @since 4.0.0 + */ +export class OpenRouterConfig extends Context.Service< + OpenRouterConfig, + OpenRouterConfig.Service +>()("@effect/ai-openrouter/OpenRouterConfig") { + /** + * Gets the configured OpenRouter service from the current context when present. + * + * @since 4.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (services) => services.mapUnsafe.get(OpenRouterConfig.key) + ) +} + +/** + * Types associated with the `OpenRouterConfig` context service. + * + * @since 4.0.0 + */ +export declare namespace OpenRouterConfig { + /** + * Configuration values read by OpenRouter provider operations when resolving + * the generated HTTP client. + * + * @category models + * @since 4.0.0 + */ + export interface Service { + readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined + } +} + +/** + * Provides a scoped transform for the OpenRouter HTTP client used by provider + * operations. + * + * **When to use** + * + * Use when you need temporary OpenRouter HTTP client customization for a + * single effect or workflow without rebuilding the client layer. + * + * **Details** + * + * Supports both data-first and data-last forms. The transform is stored in the + * scoped `OpenRouterConfig` service and read by generated OpenRouter request + * operations while running the supplied effect. + * + * **Gotchas** + * + * If a transform is already present in the scoped config, this helper replaces + * it. Compose transforms manually when both should apply. Streaming chat + * completion requests are sent directly by `OpenRouterClient.make` and do not + * read this scoped transform. + * + * @category configuration + * @since 4.0.0 + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + OpenRouterConfig.getOrUndefined, + (config) => Effect.provideService(self, OpenRouterConfig, { ...config, transformClient }) + ) +) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterError.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterError.ts new file mode 100644 index 0000000..00db386 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterError.ts @@ -0,0 +1,225 @@ +/** + * OpenRouter error metadata augmentation. + * + * Provides OpenRouter-specific metadata fields for AI error types through + * module augmentation, enabling typed access to OpenRouter error details. + * + * @since 4.0.0 + */ + +/** + * OpenRouter-specific error metadata fields. + * + * @category models + * @since 4.0.0 + */ +export type OpenRouterErrorMetadata = { + /** + * The error code returned by the API. + */ + readonly errorCode: string | number | null + /** + * The error type returned by the API. + */ + readonly errorType: string | null + /** + * The unique request ID for debugging. + */ + readonly requestId: string | null +} + +/** + * OpenRouter-specific rate limit metadata fields. + * + * @category models + * @since 4.0.0 + */ +export type OpenRouterRateLimitMetadata = OpenRouterErrorMetadata & { + readonly limit: string | null + readonly remaining: number | null + readonly resetRequests: string | null + readonly resetTokens: string | null +} + +declare module "effect/unstable/ai/AiError" { + /** + * OpenRouter metadata attached to `RateLimitError` values. + * + * **Details** + * + * Captures OpenRouter error details together with rate limit header + * information from responses where the provider rejected the request because + * a limit was reached. + * + * @category configuration + * @since 4.0.0 + */ + export interface RateLimitErrorMetadata { + /** + * OpenRouter-specific details for the rate limit response. + */ + readonly openrouter?: OpenRouterRateLimitMetadata | null + } + + /** + * OpenRouter metadata attached to `QuotaExhaustedError` values. + * + * **Details** + * + * Preserves provider error details for failures caused by exhausted account, + * billing, or usage quota. + * + * @category configuration + * @since 4.0.0 + */ + export interface QuotaExhaustedErrorMetadata { + /** + * OpenRouter-specific details for the quota exhaustion response. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `AuthenticationError` values. + * + * **Details** + * + * Preserves provider error details for failed API key, authorization, or + * permission checks. + * + * @category configuration + * @since 4.0.0 + */ + export interface AuthenticationErrorMetadata { + /** + * OpenRouter-specific details for the authentication failure. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `ContentPolicyError` values. + * + * **Details** + * + * Preserves provider error details when OpenRouter rejects input or output + * because it violates a content policy. + * + * @category configuration + * @since 4.0.0 + */ + export interface ContentPolicyErrorMetadata { + /** + * OpenRouter-specific details for the content policy response. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `InvalidRequestError` values. + * + * **Details** + * + * Preserves provider error details for malformed requests, unsupported + * parameters, or other request validation failures reported by OpenRouter. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidRequestErrorMetadata { + /** + * OpenRouter-specific details for the invalid request response. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `InternalProviderError` values. + * + * **Details** + * + * Preserves provider error details for OpenRouter-side failures such as + * transient server errors or overload responses. + * + * @category configuration + * @since 4.0.0 + */ + export interface InternalProviderErrorMetadata { + /** + * OpenRouter-specific details for the internal provider response. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `InvalidOutputError` values. + * + * **Details** + * + * Preserves provider error details when an OpenRouter response cannot be + * parsed or validated as the expected output. + * + * @category configuration + * @since 4.0.0 + */ + export interface InvalidOutputErrorMetadata { + /** + * OpenRouter-specific details for the invalid output response. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `StructuredOutputError` values. + * + * **Details** + * + * Preserves provider error details when OpenRouter returns content that does + * not satisfy the requested structured output schema. + * + * @category configuration + * @since 4.0.0 + */ + export interface StructuredOutputErrorMetadata { + /** + * OpenRouter-specific details for the structured output failure. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `UnsupportedSchemaError` values. + * + * **Details** + * + * Preserves provider error details when an unsupported schema failure is + * associated with an OpenRouter response. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnsupportedSchemaErrorMetadata { + /** + * OpenRouter-specific details for the unsupported schema failure. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } + + /** + * OpenRouter metadata attached to `UnknownError` values. + * + * **Details** + * + * Preserves provider error details for OpenRouter failures that do not map + * cleanly to a more specific AI error category. + * + * @category configuration + * @since 4.0.0 + */ + export interface UnknownErrorMetadata { + /** + * OpenRouter-specific details for the unclassified provider failure. + */ + readonly openrouter?: OpenRouterErrorMetadata | null + } +} diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts new file mode 100644 index 0000000..f937ca4 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -0,0 +1,1861 @@ +/** + * The `OpenRouterLanguageModel` module provides the OpenRouter implementation + * of Effect AI's `LanguageModel` service. It translates provider-neutral + * prompts, tools, files, structured output requests, reasoning metadata, + * cache-control hints, and provider options into OpenRouter chat completion + * requests, records GenAI telemetry around those calls, and converts normal or + * streaming results back into Effect AI response content and metadata. + * + * @since 4.0.0 + */ +/** @effect-diagnostics preferSchemaOverJson:skip-file */ +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import type * as Schema from "effect/Schema" +import * as SchemaAST from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { DeepMutable, Mutable, Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput" +import * as IdGenerator from "effect/unstable/ai/IdGenerator" +import * as LanguageModel from "effect/unstable/ai/LanguageModel" +import * as AiModel from "effect/unstable/ai/Model" +import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" +import type * as Prompt from "effect/unstable/ai/Prompt" +import type * as Response from "effect/unstable/ai/Response" +import { addGenAIAnnotations } from "effect/unstable/ai/Telemetry" +import * as Tool from "effect/unstable/ai/Tool" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type * as Generated from "./Generated.ts" +import { ReasoningDetailsDuplicateTracker, resolveFinishReason } from "./internal/utilities.ts" +import { type ChatStreamingResponseChunkData, OpenRouterClient } from "./OpenRouterClient.ts" + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Context service for OpenRouter language model configuration. + * + * **When to use** + * + * Use to provide scoped OpenRouter chat completion defaults or per-operation + * overrides for an OpenRouter language model service. + * + * @see {@link withConfigOverride} for scoping language model request overrides + * + * @category services + * @since 4.0.0 + */ +export class Config extends Context.Service< + Config, + Simplify< + & Partial< + Omit< + typeof Generated.ChatGenerationParams.Encoded, + "messages" | "response_format" | "tools" | "tool_choice" | "stream" | "stream_options" + > + > + & { + /** + * Whether to use strict JSON schema validation for structured outputs. + * + * Only applies to models that support structured outputs. Defaults to + * `true` when structured outputs are supported. + */ + readonly strictJsonSchema?: boolean | undefined + } + > +>()("@effect/ai-openrouter/OpenRouterLanguageModel/Config") {} + +// ============================================================================= +// Provider Options / Metadata +// ============================================================================= + +/** + * OpenRouter assistant reasoning detail blocks preserved for multi-turn + * conversations. + * + * @category models + * @since 4.0.0 + */ +export type ReasoningDetails = Exclude + +/** + * File annotations emitted on OpenRouter assistant messages and exposed in + * finish metadata. + * + * @category models + * @since 4.0.0 + */ +export type FileAnnotation = Extract< + NonNullable[number], + { type: "file" } +> + +declare module "effect/unstable/ai/Prompt" { + /** + * OpenRouter-specific options for system messages. + * + * **Details** + * + * These options are used when translating system instructions into + * OpenRouter chat messages. + * + * @category request + * @since 4.0.0 + */ + export interface SystemMessageOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the system message. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } + + /** + * OpenRouter-specific options for user messages. + * + * **Details** + * + * These options are used when translating user content into OpenRouter chat + * messages. + * + * @category request + * @since 4.0.0 + */ + export interface UserMessageOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the user message. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } + + /** + * OpenRouter-specific options for assistant messages. + * + * **Details** + * + * Preserves reasoning metadata when assistant messages are replayed in later + * OpenRouter requests. + * + * @category request + * @since 4.0.0 + */ + export interface AssistantMessageOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the assistant message. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + /** + * Reasoning details associated with the assistant message. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter-specific options for tool messages. + * + * **Details** + * + * These options are used when converting tool results into OpenRouter chat + * messages. + * + * @category request + * @since 4.0.0 + */ + export interface ToolMessageOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the tool message. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } + + /** + * OpenRouter-specific options for text prompt parts. + * + * **When to use** + * + * Use when you use these options to control how text content is sent to OpenRouter. + * + * @category request + * @since 4.0.0 + */ + export interface TextPartOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the text part. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } + + /** + * OpenRouter-specific options for reasoning prompt parts. + * + * **Details** + * + * Preserves provider reasoning blocks so reasoning-aware conversations can + * continue across OpenRouter requests. + * + * @category request + * @since 4.0.0 + */ + export interface ReasoningPartOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the reasoning part. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + /** + * Reasoning details associated with the reasoning part. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter-specific options for file prompt parts. + * + * **Details** + * + * Controls file naming and prompt caching for files sent to OpenRouter. + * + * @category request + * @since 4.0.0 + */ + export interface FilePartOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the file part. + */ + readonly openrouter?: { + /** + * The name to give to the file. Will be prioritized over the file name + * associated with the file part, if present. + */ + readonly fileName?: string | null + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } + + /** + * OpenRouter-specific options for tool call prompt parts. + * + * **Details** + * + * Preserves reasoning details associated with tool calls when a conversation + * is sent back to OpenRouter. + * + * @category request + * @since 4.0.0 + */ + export interface ToolCallPartOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the tool call part. + */ + readonly openrouter?: { + /** + * Reasoning details associated with the tool call part. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter-specific options for tool result prompt parts. + * + * **Details** + * + * Controls prompt caching for tool results sent to OpenRouter. + * + * @category request + * @since 4.0.0 + */ + export interface ToolResultPartOptions extends ProviderOptions { + /** + * Provider-specific options sent to OpenRouter for the tool result part. + */ + readonly openrouter?: { + /** + * A breakpoint which marks the end of reusable content eligible for caching. + */ + readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + } | null + } +} + +declare module "effect/unstable/ai/Response" { + /** + * OpenRouter metadata attached to completed reasoning response parts. + * + * **Details** + * + * Preserves provider reasoning details that can be sent back in later turns. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the reasoning part. + */ + readonly openrouter?: { + /** + * Reasoning details emitted by the underlying provider for this part. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter metadata emitted when a streamed reasoning part starts. + * + * **Details** + * + * Carries the first reasoning detail chunk when OpenRouter exposes one. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningStartPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning start. + */ + readonly openrouter?: { + /** + * Reasoning details emitted by the underlying provider for this part. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter metadata emitted for streamed reasoning deltas. + * + * **Details** + * + * Carries provider reasoning detail chunks as they arrive from OpenRouter. + * + * @category response + * @since 4.0.0 + */ + export interface ReasoningDeltaPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the streamed reasoning delta. + */ + readonly openrouter?: { + /** + * Reasoning details emitted by the underlying provider for this delta. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter metadata attached to tool-call response parts. + * + * **Details** + * + * Associates tool calls with provider reasoning details when the model emits + * reasoning and tool calls together. + * + * @category response + * @since 4.0.0 + */ + export interface ToolCallPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned for the tool call. + */ + readonly openrouter?: { + /** + * Reasoning details associated with this tool call. + */ + readonly reasoningDetails?: ReasoningDetails | null + } | null + } + + /** + * OpenRouter metadata attached to URL source citations. + * + * **Details** + * + * Includes citation text and offsets returned by providers that support URL + * annotations. + * + * @category response + * @since 4.0.0 + */ + export interface UrlSourcePartMetadata extends ProviderMetadata { + /** + * Provider-specific citation metadata returned for the URL source. + */ + readonly openrouter?: { + /** + * The cited source content returned by the provider. + */ + readonly content?: string | null + /** + * The zero-based start index of the citation in the generated text. + */ + readonly startIndex?: number | null + /** + * The zero-based end index of the citation in the generated text. + */ + readonly endIndex?: number | null + } | null + } + + /** + * OpenRouter metadata attached to finish response parts. + * + * **Details** + * + * Exposes provider response details that are not represented by the common + * Effect AI finish part fields. + * + * @category response + * @since 4.0.0 + */ + export interface FinishPartMetadata extends ProviderMetadata { + /** + * Provider-specific metadata returned when the OpenRouter response finishes. + */ + readonly openrouter?: { + /** + * Provider fingerprint for the backend configuration that served the request. + */ + readonly systemFingerprint?: string | null + /** + * Raw token usage reported by OpenRouter. + */ + readonly usage?: typeof Generated.ChatGenerationTokenUsage.Encoded | null + /** + * File annotations returned by the provider. + */ + readonly annotations?: ReadonlyArray | null + /** + * The OpenRouter provider that served the request, when reported. + */ + readonly provider?: string | null + } | null + } +} + +// ============================================================================= +// Language Model +// ============================================================================= + +/** + * Creates an OpenRouter model descriptor that can be provided with + * `Effect.provide`. + * + * **When to use** + * + * Use when you want an OpenRouter language model value that carries provider + * and model metadata and can be supplied directly to an Effect program. + * + * **Details** + * + * The returned model requires `OpenRouterClient` and provides + * `LanguageModel.LanguageModel`. + * + * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly + * @see {@link make} for constructing the language model service effectfully + * @see {@link withConfigOverride} for scoping OpenRouter request overrides + * + * @category constructors + * @since 4.0.0 + */ +export const model = ( + model: string, + config?: Omit +): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> => + AiModel.make("openai", model, layer({ model, config })) + +/** + * Creates an OpenRouter `LanguageModel` service from a model identifier and + * optional request defaults. + * + * **When to use** + * + * Use when you need to construct a `LanguageModel.Service` value backed by + * `OpenRouterClient` inside an Effect. + * + * **Details** + * + * The returned effect requires `OpenRouterClient`. Request defaults from the + * `config` option are merged with any `Config` service in the context, with + * context values taking precedence. The service supports both `generateText` + * and `streamText`. + * + * **Gotchas** + * + * Provider-defined tools are not supported by this provider integration; + * requests that include them fail with an `InvalidUserInputError`. + * + * @see {@link layer} for providing the service as a `Layer` + * @see {@link model} for creating a model descriptor for `Effect.provide` + * @see {@link withConfigOverride} for scoping request defaults around operations + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: { + readonly model: string + readonly config?: Omit | undefined +}): Effect.fn.Return { + const client = yield* OpenRouterClient + const codecTransformer = getCodecTransformer(model) + + const makeConfig = Effect.gen(function*() { + const services = yield* Effect.context() + return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) } + }) + + const makeRequest = Effect.fnUntraced( + function*({ config, options }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + }): Effect.fn.Return { + const messages = yield* prepareMessages({ options }) + const { tools, toolChoice } = yield* prepareTools({ options, transformer: codecTransformer }) + const responseFormat = yield* getResponseFormat({ config, options, transformer: codecTransformer }) + const request: typeof Generated.ChatGenerationParams.Encoded = { + ...config, + messages, + ...(Predicate.isNotUndefined(responseFormat) ? { response_format: responseFormat } : undefined), + ...(Predicate.isNotUndefined(tools) ? { tools } : undefined), + ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined) + } + return request + } + ) + + return yield* LanguageModel.make({ + codecTransformer: toCodecOpenAI, + generateText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const request = yield* makeRequest({ config, options }) + annotateRequest(options.span, request) + const [rawResponse, response] = yield* client.createChatCompletion(request) + annotateResponse(options.span, rawResponse) + return yield* makeResponse({ rawResponse, response }) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const config = yield* makeConfig + const request = yield* makeRequest({ config, options }) + annotateRequest(options.span, request) + const [response, stream] = yield* client.createChatCompletionStream(request) + return yield* makeStreamResponse({ response, stream }) + }, + (effect, options) => + effect.pipe( + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * Creates a layer for the OpenRouter language model. + * + * **When to use** + * + * Use when composing application layers and you want OpenRouter to satisfy + * `LanguageModel.LanguageModel` while supplying `OpenRouterClient` from another + * layer. + * + * @see {@link make} for constructing the language model service effectfully + * @see {@link model} for creating a model descriptor for `Effect.provide` + * + * @category layers + * @since 4.0.0 + */ +export const layer = (options: { + readonly model: string + readonly config?: Omit | undefined +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make(options)) + +/** + * Provides config overrides for OpenRouter language model operations. + * + * **When to use** + * + * Use to apply OpenRouter request configuration to one effect without changing + * the model's default configuration. + * + * **Details** + * + * The overrides are merged with any existing `Config` service for the duration + * of the supplied effect. Fields in `overrides` take precedence over existing + * config, and the helper supports both pipe form and + * `withConfigOverride(effect, overrides)`. + * + * @see {@link Config} for available OpenRouter request configuration fields + * + * @category configuration + * @since 4.0.0 + */ +export const withConfigOverride: { + (overrides: typeof Config.Service): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, overrides: typeof Config.Service): Effect.Effect> +} = dual< + ( + overrides: typeof Config.Service + ) => (self: Effect.Effect) => Effect.Effect>, + (self: Effect.Effect, overrides: typeof Config.Service) => Effect.Effect> +>(2, (self, overrides) => + Effect.flatMap( + Effect.serviceOption(Config), + (config) => + Effect.provideService(self, Config, { + ...(config._tag === "Some" ? config.value : {}), + ...overrides + }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages = Effect.fnUntraced( + function*({ options }: { + readonly options: LanguageModel.ProviderOptions + }): Effect.fn.Return, AiError.AiError> { + const messages: Array = [] + + const reasoningDetailsTracker = new ReasoningDetailsDuplicateTracker() + + for (const message of options.prompt.content) { + switch (message.role) { + case "system": { + const cache_control = getCacheControl(message) + + messages.push({ + role: "system", + content: [{ + type: "text", + text: message.content, + ...(Predicate.isNotNull(cache_control) ? { cache_control } : undefined) + }] + }) + + break + } + + case "user": { + const content: Array = [] + + // Get the message-level cache control + const messageCacheControl = getCacheControl(message) + + if (message.content.length === 1 && message.content[0].type === "text") { + messages.push({ + role: "user", + content: Predicate.isNotNull(messageCacheControl) + ? [{ type: "text", text: message.content[0].text, cache_control: messageCacheControl }] + : message.content[0].text + }) + + break + } + + // Find the index of the last text part in the message content + let lastTextPartIndex = -1 + for (let i = message.content.length - 1; i >= 0; i--) { + if (message.content[i].type === "text") { + lastTextPartIndex = i + break + } + } + + for (let index = 0; index < message.content.length; index++) { + const part = message.content[index] + const isLastTextPart = part.type === "text" && index === lastTextPartIndex + const partCacheControl = getCacheControl(part) + + switch (part.type) { + case "text": { + const cache_control = Predicate.isNotNull(partCacheControl) + ? partCacheControl + : isLastTextPart + ? messageCacheControl + : null + + content.push({ + type: "text", + text: part.text, + ...(Predicate.isNotNull(cache_control) ? { cache_control } : undefined) + }) + + break + } + + case "file": { + if (part.mediaType.startsWith("image/")) { + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + content.push({ + type: "image_url", + image_url: { + url: part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` + : part.data + }, + ...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined) + }) + + break + } + + if (part.mediaType.startsWith("audio/")) { + const format = audioFormats[part.mediaType.toLowerCase()] + + if (Predicate.isUndefined(format)) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidUserInputError({ + description: `Detected unsupported media type for audio file: '${part.mediaType}' ` + + `- OpenRouter supports ${supportedAudioFormats} audio` + }) + }) + } + + if (part.data instanceof URL) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidUserInputError({ + description: "Detected URL data for audio file - OpenRouter requires " + + "audio to be provided as base64-encoded data" + }) + }) + } + + content.push({ + type: "input_audio", + input_audio: { + data: part.data instanceof Uint8Array + ? Encoding.encodeBase64(part.data) + : getBase64FromDataUrl(part.data), + format + }, + ...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined) + }) + + break + } + + const options = part.options.openrouter + const fileName = options?.fileName ?? part.fileName ?? "" + + content.push({ + type: "file", + file: { + filename: fileName, + file_data: part.data instanceof URL + ? part.data.toString() + : part.data instanceof Uint8Array + ? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}` + : part.data + }, + ...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined) + } as any) + + break + } + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + let text = "" + let reasoning = "" + const toolCalls: Array = [] + + for (const part of message.content) { + switch (part.type) { + case "text": { + text += part.text + break + } + + case "reasoning": { + reasoning += part.text + break + } + + case "tool-call": { + toolCalls.push({ + type: "function", + id: part.id, + function: { name: part.name, arguments: JSON.stringify(part.params) } + }) + break + } + + default: { + break + } + } + } + + const messageReasoningDetails = message.options.openrouter?.reasoningDetails + + // Use message-level reasoning details if available, otherwise find from parts + // Priority: message-level > first tool call > first reasoning part + // This prevents duplicate thinking blocks when Claude makes parallel tool calls + const candidateReasoningDetails: ReasoningDetails | null = Predicate.isNotNullish(messageReasoningDetails) + && Array.isArray(messageReasoningDetails) + && messageReasoningDetails.length > 0 + ? messageReasoningDetails + : findFirstReasoningDetails(message.content) + + // Deduplicate reasoning details across all messages to prevent "Duplicate + // item found with id" errors in multi-turn conversations. + let reasoningDetails: ReasoningDetails | null = null + if (Predicate.isNotNull(candidateReasoningDetails) && candidateReasoningDetails.length > 0) { + const uniqueReasoningDetails: Mutable = [] + for (const detail of candidateReasoningDetails) { + if (reasoningDetailsTracker.upsert(detail)) { + uniqueReasoningDetails.push(detail) + } + } + if (uniqueReasoningDetails.length > 0) { + reasoningDetails = uniqueReasoningDetails + } + } + + messages.push({ + role: "assistant", + content: text, + reasoning: reasoning.length > 0 ? reasoning : null, + ...(Predicate.isNotNull(reasoningDetails) ? { reasoning_details: reasoningDetails } : undefined), + ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : undefined) + }) + + break + } + + case "tool": { + for (const part of message.content) { + // Skip tool approval parts + if (part.type === "tool-approval-response") { + continue + } + + messages.push({ + role: "tool", + tool_call_id: part.id, + content: JSON.stringify(part.result) + }) + } + + break + } + } + } + + return messages + } +) + +// ============================================================================= +// HTTP Details +// ============================================================================= + +const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +const buildHttpResponseDetails = ( + response: HttpClientResponse.HttpClientResponse +): typeof Response.HttpResponseDetails.Type => ({ + status: response.status, + headers: Redactable.redact(response.headers) as Record +}) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse = Effect.fnUntraced( + function*({ rawResponse, response }: { + readonly rawResponse: Generated.SendChatCompletionRequest200 + readonly response: HttpClientResponse.HttpClientResponse + }): Effect.fn.Return, AiError.AiError, IdGenerator.IdGenerator> { + const idGenerator = yield* IdGenerator.IdGenerator + + const parts: Array = [] + let hasToolCalls = false + let hasEncryptedReasoning = false + + const createdAt = new Date(rawResponse.created * 1000) + parts.push({ + type: "response-metadata", + id: rawResponse.id, + modelId: rawResponse.model, + timestamp: DateTime.formatIso(DateTime.fromDateUnsafe(createdAt)), + request: buildHttpRequestDetails(response.request) + }) + + const choice = rawResponse.choices[0] + if (Predicate.isUndefined(choice)) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "makeResponse", + reason: new AiError.InvalidOutputError({ + description: "Received response with empty choices" + }) + }) + } + + const message = choice.message + let finishReason = choice.finish_reason + + const reasoningDetails = message.reasoning_details + if (Predicate.isNotNullish(reasoningDetails) && reasoningDetails.length > 0) { + for (const detail of reasoningDetails) { + switch (detail.type) { + case "reasoning.text": { + if (Predicate.isNotNullish(detail.text) && detail.text.length > 0) { + parts.push({ + type: "reasoning", + text: detail.text, + metadata: { openrouter: { reasoningDetails: [detail] } } + }) + } + break + } + case "reasoning.summary": { + if (detail.summary.length > 0) { + parts.push({ + type: "reasoning", + text: detail.summary, + metadata: { openrouter: { reasoningDetails: [detail] } } + }) + } + break + } + case "reasoning.encrypted": { + if (detail.data.length > 0) { + hasEncryptedReasoning = true + parts.push({ + type: "reasoning", + text: "[REDACTED]", + metadata: { openrouter: { reasoningDetails: [detail] } } + }) + } + break + } + } + } + } else if (Predicate.isNotNullish(message.reasoning) && message.reasoning.length > 0) { + // message.reasoning fallback only when reasoning_details absent/empty + parts.push({ + type: "reasoning", + text: message.reasoning + }) + } + + const content = message.content + if (Predicate.isNotNullish(content)) { + if (typeof content === "string") { + if (content.length > 0) { + parts.push({ type: "text", text: content }) + } + } else { + for (const item of content) { + if (item.type === "text") { + parts.push({ type: "text", text: item.text }) + } + } + } + } + + const toolCalls = message.tool_calls + if (Predicate.isNotNullish(toolCalls) && toolCalls.length > 0) { + hasToolCalls = true + for (let index = 0; index < toolCalls.length; index++) { + const toolCall = toolCalls[index] + const toolName = toolCall.function.name + const toolParams = toolCall.function.arguments ?? "{}" + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: (cause) => + AiError.make({ + module: "OpenRouterLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams: {}, + description: `Failed to securely JSON parse tool parameters: ${cause}` + }) + }) + }) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolName, + params, + // Only attach reasoning_details to the first tool call to avoid + // duplicating thinking blocks for parallel tool calls (Claude) + ...(index === 0 && Predicate.isNotNullish(reasoningDetails) && reasoningDetails.length > 0 + ? { metadata: { openrouter: { reasoningDetails } } } + : undefined) + }) + } + } + + const images = message.images + if (Predicate.isNotNullish(images)) { + for (const image of images) { + const url = image.image_url.url + if (url.startsWith("data:")) { + const mediaType = getMediaType(url, "image/jpeg") + const data = getBase64FromDataUrl(url) + parts.push({ type: "file", mediaType, data }) + } else { + const id = yield* idGenerator.generateId() + parts.push({ type: "source", sourceType: "url", id, url, title: "" }) + } + } + } + + const annotations = choice.message.annotations + if (Predicate.isNotNullish(annotations)) { + for (const annotation of annotations) { + if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: annotation.url_citation.url, + url: annotation.url_citation.url, + title: annotation.url_citation.title ?? "", + metadata: { + openrouter: { + ...(Predicate.isNotUndefined(annotation.url_citation.content) + ? { content: annotation.url_citation.content } + : undefined), + ...(Predicate.isNotUndefined(annotation.url_citation.start_index) + ? { startIndex: annotation.url_citation.start_index } + : undefined), + ...(Predicate.isNotUndefined(annotation.url_citation.end_index) + ? { endIndex: annotation.url_citation.end_index } + : undefined) + } + } + }) + } + } + } + + // Extract file annotations to expose in provider metadata + const fileAnnotations = annotations?.filter((annotation) => { + return annotation.type === "file" + }) + + // Fix for Gemini 3 thoughtSignature: when there are tool calls with encrypted + // reasoning (thoughtSignature), the model returns 'stop' but expects continuation. + // Override to 'tool-calls' so the SDK knows to continue the conversation. + if (hasEncryptedReasoning && hasToolCalls && finishReason === "stop") { + finishReason = "tool_calls" + } + + parts.push({ + type: "finish", + reason: resolveFinishReason(finishReason), + usage: getUsage(rawResponse.usage), + response: buildHttpResponseDetails(response), + metadata: { + openrouter: { + systemFingerprint: rawResponse.system_fingerprint ?? null, + usage: rawResponse.usage ?? null, + ...(Predicate.isNotUndefined(fileAnnotations) && fileAnnotations.length > 0 + ? { annotations: fileAnnotations } + : undefined), + ...(Predicate.hasProperty(rawResponse, "provider") && Predicate.isString(rawResponse.provider) + ? { provider: rawResponse.provider } + : undefined) + } + } + }) + + return parts + } +) + +const makeStreamResponse = Effect.fnUntraced( + function*({ response, stream }: { + readonly response: HttpClientResponse.HttpClientResponse + readonly stream: Stream.Stream + }): Effect.fn.Return< + Stream.Stream, + AiError.AiError, + IdGenerator.IdGenerator + > { + const idGenerator = yield* IdGenerator.IdGenerator + + let textStarted = false + let reasoningStarted = false + let responseMetadataEmitted = false + let reasoningDetailsAttachedToToolCall = false + let finishReason: Response.FinishReason = "other" + let openRouterResponseId: string | undefined = undefined + let activeReasoningId: string | undefined = undefined + let activeTextId: string | undefined = undefined + + let totalToolCalls = 0 + const activeToolCalls: Record = {} + + // Track reasoning details to preserve for multi-turn conversations + const accumulatedReasoningDetails: DeepMutable = [] + + // Track file annotations to expose in provider metadata + const accumulatedFileAnnotations: Array = [] + + const usage: DeepMutable = { + inputTokens: { + total: undefined, + uncached: undefined, + cacheRead: undefined, + cacheWrite: undefined + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined + } + } + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if (Predicate.isNotUndefined(event.error)) { + finishReason = "error" + parts.push({ type: "error", error: event.error }) + } + + if (Predicate.isNotUndefined(event.id) && !responseMetadataEmitted) { + const timestamp = yield* DateTime.now + parts.push({ + type: "response-metadata", + id: event.id, + modelId: event.model, + timestamp: DateTime.formatIso(timestamp), + request: buildHttpRequestDetails(response.request) + }) + responseMetadataEmitted = true + } + + if (Predicate.isNotUndefined(event.usage)) { + const computed = getUsage(event.usage) + usage.inputTokens = computed.inputTokens + usage.outputTokens = computed.outputTokens + } + + const choice = event.choices[0] + if (Predicate.isNotUndefined(choice)) { + if (Predicate.isNotNullish(choice.finish_reason)) { + finishReason = resolveFinishReason(choice.finish_reason) + } + + const delta = choice.delta + if (Predicate.isNullish(delta)) { + return parts + } + + const emitReasoning = Effect.fnUntraced( + function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) { + if (!reasoningStarted) { + activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId()) + parts.push({ + type: "reasoning-start", + id: activeReasoningId, + metadata + }) + reasoningStarted = true + } + parts.push({ + type: "reasoning-delta", + id: activeReasoningId!, + delta, + metadata + }) + } + ) + + const reasoningDetails = delta.reasoning_details + if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) { + // Accumulate reasoning_details to preserve for multi-turn conversations + // Merge consecutive reasoning.text items into a single entry + for (const detail of reasoningDetails) { + if (detail.type === "reasoning.text") { + const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1] + if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") { + // Merge with the previous text detail + lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "") + lastDetail.signature = lastDetail.signature ?? detail.signature ?? null + lastDetail.format = lastDetail.format ?? detail.format ?? null + } else { + // Start a new text detail + accumulatedReasoningDetails.push({ ...detail }) + } + } else { + // Non-text details (encrypted, summary) are pushed as-is + accumulatedReasoningDetails.push(detail) + } + } + + // Emit reasoning_details in providerMetadata for each delta chunk + // so users can accumulate them on their end before sending back + const metadata: Response.ReasoningDeltaPart["metadata"] = { + openrouter: { + reasoningDetails + } + } + for (const detail of reasoningDetails) { + switch (detail.type) { + case "reasoning.text": { + if (Predicate.isNotNullish(detail.text)) { + yield* emitReasoning(detail.text, metadata) + } + break + } + + case "reasoning.summary": { + if (Predicate.isNotNullish(detail.summary)) { + yield* emitReasoning(detail.summary, metadata) + } + break + } + + case "reasoning.encrypted": { + if (Predicate.isNotNullish(detail.data)) { + yield* emitReasoning("[REDACTED]", metadata) + } + break + } + } + } + } else if (Predicate.isNotNullish(delta.reasoning)) { + yield* emitReasoning(delta.reasoning) + } + + const content = delta.content + if (Predicate.isNotNullish(content)) { + // If reasoning was previously active and now we're starting text content, + // we should end the reasoning first to maintain proper order + if (reasoningStarted && !textStarted) { + parts.push({ + type: "reasoning-end", + id: activeReasoningId!, + // Include accumulated reasoning_details so the we can update the + // reasoning part's provider metadata with the correct signature. + // The signature typically arrives in the last reasoning delta, + // but reasoning-start only carries the first delta's metadata. + metadata: accumulatedReasoningDetails.length > 0 + ? { openRouter: { reasoningDetails: accumulatedReasoningDetails } } + : undefined + }) + reasoningStarted = false + } + + if (!textStarted) { + activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId()) + parts.push({ + type: "text-start", + id: activeTextId + }) + textStarted = true + } + + parts.push({ + type: "text-delta", + id: activeTextId!, + delta: content + }) + } + + const annotations = delta.annotations + if (Predicate.isNotNullish(annotations)) { + for (const annotation of annotations) { + if (annotation.type === "url_citation") { + parts.push({ + type: "source", + sourceType: "url", + id: annotation.url_citation.url, + url: annotation.url_citation.url, + title: annotation.url_citation.title ?? "", + metadata: { + openrouter: { + ...(Predicate.isNotUndefined(annotation.url_citation.content) + ? { content: annotation.url_citation.content } + : undefined), + ...(Predicate.isNotUndefined(annotation.url_citation.start_index) + ? { startIndex: annotation.url_citation.start_index } + : undefined), + ...(Predicate.isNotUndefined(annotation.url_citation.end_index) + ? { startIndex: annotation.url_citation.end_index } + : undefined) + } + } + }) + } else if (annotation.type === "file") { + accumulatedFileAnnotations.push(annotation) + } + } + } + + const toolCalls = delta.tool_calls + if (Predicate.isNotNullish(toolCalls)) { + for (const toolCall of toolCalls) { + const index = toolCall.index ?? toolCalls.length - 1 + let activeToolCall = activeToolCalls[index] + + // Tool call start - OpenRouter returns all information except the + // tool call parameters in the first chunk + if (Predicate.isUndefined(activeToolCall)) { + if (toolCall.type !== "function") { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "makeStreamResponse", + reason: new AiError.InvalidOutputError({ + description: "Received tool call delta that was not of type: 'function'" + }) + }) + } + + if (Predicate.isNullish(toolCall.id)) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "makeStreamResponse", + reason: new AiError.InvalidOutputError({ + description: "Received tool call delta without a tool call identifier" + }) + }) + } + + if (Predicate.isNullish(toolCall.function?.name)) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "makeStreamResponse", + reason: new AiError.InvalidOutputError({ + description: "Received tool call delta without a tool call name" + }) + }) + } + + activeToolCall = { + id: toolCall.id, + type: "function", + name: toolCall.function.name, + params: toolCall.function.arguments ?? "" + } + + activeToolCalls[index] = activeToolCall + + parts.push({ + type: "tool-params-start", + id: activeToolCall.id, + name: activeToolCall.name + }) + + // Emit a tool call delta part if parameters were also sent + if (activeToolCall.params.length > 0) { + parts.push({ + type: "tool-params-delta", + id: activeToolCall.id, + delta: activeToolCall.params + }) + } + } else { + // If an active tool call was found, update and emit the delta for + // the tool call's parameters + activeToolCall.params += toolCall.function?.arguments ?? "" + parts.push({ + type: "tool-params-delta", + id: activeToolCall.id, + delta: activeToolCall.params + }) + } + + // Check if the tool call is complete + // @effect-diagnostics-next-line tryCatchInEffectGen:off + try { + const params = Tool.unsafeSecureJsonParse(activeToolCall.params) + + parts.push({ + type: "tool-params-end", + id: activeToolCall.id + }) + + parts.push({ + type: "tool-call", + id: activeToolCall.id, + name: activeToolCall.name, + params, + // Only attach reasoning_details to the first tool call to avoid + // duplicating thinking blocks for parallel tool calls (Claude) + metadata: reasoningDetailsAttachedToToolCall ? undefined : { + openrouter: { reasoningDetails: accumulatedReasoningDetails } + } + }) + + reasoningDetailsAttachedToToolCall = true + + // Increment the total tool calls emitted by the stream and + // remove the active tool call + totalToolCalls += 1 + delete activeToolCalls[toolCall.index] + } catch { + // Tool call incomplete, continue parsing + continue + } + } + } + + const images = delta.images + if (Predicate.isNotNullish(images)) { + for (const image of images) { + parts.push({ + type: "file", + mediaType: getMediaType(image.image_url.url, "image/jpeg"), + data: getBase64FromDataUrl(image.image_url.url) + }) + } + } + } + + // Usage is only emitted by the last part of the stream, so we need to + // handle flushing any remaining text / reasoning / tool calls + if (Predicate.isNotUndefined(event.usage)) { + // Fix for Gemini 3 thoughtSignature: when there are tool calls with encrypted + // reasoning (thoughtSignature), the model returns 'stop' but expects continuation. + // Override to 'tool-calls' so the SDK knows to continue the conversation. + const hasEncryptedReasoning = accumulatedReasoningDetails.some( + (detail) => detail.type === "reasoning.encrypted" && detail.data.length > 0 + ) + if (totalToolCalls > 0 && hasEncryptedReasoning && finishReason === "stop") { + finishReason = resolveFinishReason("tool-calls") + } + + // Forward any unsent tool calls if finish reason is 'tool-calls' + if (finishReason === "tool-calls") { + for (const toolCall of Object.values(activeToolCalls)) { + // Coerce invalid tool call parameters to an empty object + let params: unknown + // @effect-diagnostics-next-line tryCatchInEffectGen:off + try { + params = Tool.unsafeSecureJsonParse(toolCall.params) + } catch { + params = {} + } + + // Only attach reasoning_details to the first tool call to avoid + // duplicating thinking blocks for parallel tool calls (Claude) + parts.push({ + type: "tool-call", + id: toolCall.id, + name: toolCall.name, + params, + metadata: reasoningDetailsAttachedToToolCall ? undefined : { + openrouter: { reasoningDetails: accumulatedReasoningDetails } + } + }) + + reasoningDetailsAttachedToToolCall = true + } + } + + // End reasoning first if it was started, to maintain proper order + if (reasoningStarted) { + parts.push({ + type: "reasoning-end", + id: activeReasoningId!, + // Include accumulated reasoning_details so that we can update the + // reasoning part's provider metadata with the correct signature, + metadata: accumulatedReasoningDetails.length > 0 + ? { openrouter: { reasoningDetails: accumulatedReasoningDetails } } + : undefined + }) + } + + if (textStarted) { + parts.push({ type: "text-end", id: activeTextId! }) + } + + const metadata: Response.FinishPart["metadata"] = { + openrouter: { + ...(Predicate.isNotNullish(event.system_fingerprint) + ? { systemFingerprint: event.system_fingerprint } + : undefined), + ...(Predicate.isNotUndefined(event.usage) ? { usage: event.usage } : undefined), + ...(Predicate.hasProperty(event, "provider") && Predicate.isString(event.provider) + ? { provider: event.provider } + : undefined), + ...(accumulatedFileAnnotations.length > 0 ? { annotations: accumulatedFileAnnotations } : undefined) + } + } + + parts.push({ + type: "finish", + reason: finishReason, + usage, + response: buildHttpResponseDetails(response), + metadata + }) + } + + return parts + })), + Stream.flattenIterable + ) + } +) + +// ============================================================================= +// Tool Conversion +// ============================================================================= + +const prepareTools = Effect.fnUntraced( + function*({ options, transformer }: { + readonly options: LanguageModel.ProviderOptions + readonly transformer: LanguageModel.CodecTransformer + }): Effect.fn.Return<{ + readonly tools: ReadonlyArray | undefined + readonly toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined + }, AiError.AiError> { + if (options.tools.length === 0) { + return { tools: undefined, toolChoice: undefined } + } + + const hasProviderDefinedTools = options.tools.some((tool) => Tool.isProviderDefined(tool)) + if (hasProviderDefinedTools) { + return yield* AiError.make({ + module: "OpenRouterLanguageModel", + method: "prepareTools", + reason: new AiError.InvalidUserInputError({ + description: "Provider-defined tools are unsupported by the OpenRouter " + + "provider integration at this time" + }) + }) + } + + let tools: Array = [] + let toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined = undefined + + for (const tool of options.tools) { + const description = Tool.getDescription(tool) + const parameters = yield* tryJsonSchema(tool.parametersSchema, "prepareTools", transformer) + const strict = Tool.getStrictMode(tool) ?? null + + tools.push({ + type: "function", + function: { + name: tool.name, + parameters, + strict, + ...(Predicate.isNotUndefined(description) ? { description } : undefined) + } + }) + } + + if (options.toolChoice === "none") { + toolChoice = "none" + } else if (options.toolChoice === "auto") { + toolChoice = "auto" + } else if (options.toolChoice === "required") { + toolChoice = "required" + } else if ("tool" in options.toolChoice) { + toolChoice = { type: "function", function: { name: options.toolChoice.tool } } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.function.name)) + toolChoice = options.toolChoice.mode === "required" ? "required" : "auto" + } + + return { tools, toolChoice } + } +) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof Generated.ChatGenerationParams.Encoded +): void => { + addGenAIAnnotations(span, { + system: "openrouter", + operation: { name: "chat" }, + request: { + model: request.model, + temperature: request.temperature, + topP: request.top_p, + maxTokens: request.max_tokens, + stopSequences: Arr.ensure(request.stop).filter( + Predicate.isNotNullish + ) + } + }) +} + +const annotateResponse = (span: Span, response: Generated.SendChatCompletionRequest200): void => { + addGenAIAnnotations(span, { + response: { + id: response.id, + model: response.model, + finishReasons: response.choices.map((choice) => choice.finish_reason).filter(Predicate.isNotNullish) + }, + usage: { + inputTokens: response.usage?.prompt_tokens, + outputTokens: response.usage?.completion_tokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens.total, + outputTokens: part.usage.outputTokens.total + } + }) + } +} + +// ============================================================================= +// Internal Utilities +// ============================================================================= + +const getCacheControl = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage + | Prompt.TextPart + | Prompt.ReasoningPart + | Prompt.FilePart + | Prompt.ToolResultPart +): typeof Generated.ChatMessageContentItemCacheControl.Encoded | null => part.options.openrouter?.cacheControl ?? null + +const findFirstReasoningDetails = (content: ReadonlyArray): ReasoningDetails | null => { + for (const part of content) { + // First try tool calls since they have complete accumulated reasoning details + if (part.type === "tool-call") { + const details = part.options.openrouter?.reasoningDetails + if (Predicate.isNotNullish(details) && Array.isArray(details) && details.length > 0) { + return details as ReasoningDetails + } + } + + // Fallback to reasoning parts which have delta reasoning details + if (part.type === "reasoning") { + const details = part.options.openrouter?.reasoningDetails + if (Predicate.isNotNullish(details) && Array.isArray(details) && details.length > 0) { + return details as ReasoningDetails + } + } + } + + return null +} + +const getCodecTransformer = (model: string): LanguageModel.CodecTransformer => { + if (model.startsWith("anthropic/") || model.startsWith("claude-")) { + return toCodecAnthropic + } + if ( + model.startsWith("openai/") || + model.startsWith("gpt-") || + model.startsWith("o1-") || + model.startsWith("o3-") || + model.startsWith("o4-") + ) { + return toCodecOpenAI + } + return LanguageModel.defaultCodecTransformer +} + +const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError => + AiError.make({ + module: "OpenRouterLanguageModel", + method, + reason: new AiError.UnsupportedSchemaError({ + description: error instanceof Error ? error.message : String(error) + }) + }) + +const tryJsonSchema = ( + schema: S, + method: string, + transformer: LanguageModel.CodecTransformer +) => + Effect.try({ + try: () => Tool.getJsonSchemaFromSchema(schema, { transformer }), + catch: (error) => unsupportedSchemaError(error, method) + }) + +const getResponseFormat = Effect.fnUntraced(function*({ config, options, transformer }: { + readonly config: typeof Config.Service + readonly options: LanguageModel.ProviderOptions + readonly transformer: LanguageModel.CodecTransformer +}): Effect.fn.Return { + if (options.responseFormat.type === "json") { + const description = SchemaAST.resolveDescription(options.responseFormat.schema.ast) + const jsonSchema = yield* tryJsonSchema(options.responseFormat.schema, "getResponseFormat", transformer) + return { + type: "json_schema", + json_schema: { + name: options.responseFormat.objectName, + schema: jsonSchema, + strict: config.strictJsonSchema ?? null, + ...(Predicate.isNotUndefined(description) ? { description } : undefined) + } + } + } + return undefined +}) + +/** + * Maps audio media types to the formats supported by OpenRouter. + * + * @see https://openrouter.ai/docs/guides/overview/multimodal/audio + */ +const audioFormats: Record = { + "audio/aac": "aac", + "audio/aiff": "aiff", + "audio/x-aiff": "aiff", + "audio/flac": "flac", + "audio/x-flac": "flac", + "audio/l16": "pcm16", + "audio/l24": "pcm24", + "audio/m4a": "m4a", + "audio/x-m4a": "m4a", + "audio/mp4": "m4a", + "audio/mp3": "mp3", + "audio/mpeg": "mp3", + "audio/ogg": "ogg", + "audio/wav": "wav", + "audio/wave": "wav", + "audio/x-wav": "wav" +} + +const supportedAudioFormats = Array.from(new Set(Object.values(audioFormats))).join(", ") + +const getMediaType = (dataUrl: string, defaultMediaType: string): string => { + const match = dataUrl.match(/^data:([^;]+)/) + return match ? (match[1] ?? defaultMediaType) : defaultMediaType +} + +const getBase64FromDataUrl = (dataUrl: string): string => { + const match = dataUrl.match(/^data:[^;]*;base64,(.+)$/) + return match ? match[1]! : dataUrl +} + +const getUsage = (usage: Generated.ChatGenerationTokenUsage | undefined): Response.Usage => { + if (Predicate.isUndefined(usage)) { + return { + inputTokens: { uncached: undefined, total: 0, cacheRead: undefined, cacheWrite: undefined }, + outputTokens: { total: 0, text: undefined, reasoning: undefined } + } + } + const promptTokens = usage.prompt_tokens + const completionTokens = usage.completion_tokens + const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0 + const cacheWriteTokens = usage.prompt_tokens_details?.cache_write_tokens ?? 0 + const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0 + return { + inputTokens: { + uncached: promptTokens - cacheReadTokens, + total: promptTokens, + cacheRead: cacheReadTokens, + cacheWrite: cacheWriteTokens + }, + outputTokens: { + total: completionTokens, + text: completionTokens - reasoningTokens, + reasoning: reasoningTokens + } + } +} diff --git a/repos/effect/packages/ai/openrouter/src/index.ts b/repos/effect/packages/ai/openrouter/src/index.ts new file mode 100644 index 0000000..2ea005e --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/index.ts @@ -0,0 +1,30 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as Generated from "./Generated.ts" + +/** + * @since 4.0.0 + */ +export * as OpenRouterClient from "./OpenRouterClient.ts" + +/** + * @since 4.0.0 + */ +export * as OpenRouterConfig from "./OpenRouterConfig.ts" + +/** + * @since 4.0.0 + */ +export * as OpenRouterError from "./OpenRouterError.ts" + +/** + * @since 4.0.0 + */ +export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.ts" diff --git a/repos/effect/packages/ai/openrouter/src/internal/errors.ts b/repos/effect/packages/ai/openrouter/src/internal/errors.ts new file mode 100644 index 0000000..d01128e --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/internal/errors.ts @@ -0,0 +1,375 @@ +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Number from "effect/Number" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import type * as Response from "effect/unstable/ai/Response" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type * as Generated from "../Generated.ts" +import type { OpenRouterErrorMetadata } from "../OpenRouterError.ts" + +// ============================================================================= +// OpenRouter Error Body Schema +// ============================================================================= + +/** @internal */ +export const OpenRouterErrorBody = Schema.Struct({ + error: Schema.Struct({ + message: Schema.String, + type: Schema.optional(Schema.NullOr(Schema.String)), + code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())]))) + }) +}) + +/** @internal */ +export type OpenRouterClientErrorBody = { + readonly error: { + readonly code: string | number | null + readonly message: string + readonly param?: string | null + readonly type?: string | null + } +} + +// ============================================================================= +// Error Mappers +// ============================================================================= + +/** @internal */ +export const mapSchemaError = dual< + (method: string) => (error: Schema.SchemaError) => AiError.AiError, + (error: Schema.SchemaError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenRouterClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + })) + +/** @internal */ +export const mapClientError = dual< + (method: string) => (error: Generated.OpenRouterClientError) => AiError.AiError, + (error: Generated.OpenRouterClientError, method: string) => AiError.AiError +>(2, (error, method) => { + const { request, response, cause } = error + const status = response.status + const headers = response.headers as Record + const metadata: OpenRouterErrorMetadata = { + errorCode: cause.error.code ?? null, + errorType: cause.error.type ?? null, + requestId: headers["x-request-id"] ?? null + } + const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) + const reason = mapStatusCodeToReason({ + status, + headers, + message: cause.error.message, + metadata, + http + }) + return AiError.make({ module: "OpenRouterClient", method, reason }) +}) + +/** @internal */ +export const mapHttpClientError = dual< + (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, + (error: HttpClientError.HttpClientError, method: string) => Effect.Effect +>(2, (error, method) => { + const reason = error.reason + switch (reason._tag) { + case "TransportError": { + return Effect.fail(AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.NetworkError({ + reason: "TransportError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "EncodeError": { + return Effect.fail(AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.NetworkError({ + reason: "EncodeError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "InvalidUrlError": { + return Effect.fail(AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.NetworkError({ + reason: "InvalidUrlError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "StatusCodeError": { + return mapStatusCodeError(reason, method) + } + case "DecodeError": { + return Effect.fail(AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Failed to decode response" + }) + })) + } + case "EmptyBodyError": { + return Effect.fail(AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Response body was empty" + }) + })) + } + } +}) + +/** @internal */ +const mapStatusCodeError = Effect.fnUntraced(function*( + error: HttpClientError.StatusCodeError, + method: string +) { + const { request, response, description } = error + const status = response.status + const headers = response.headers as Record + const requestId = headers["x-request-id"] + + let body: string | undefined = description + if (!description || !description.startsWith("{")) { + const responseBody = yield* Effect.option(response.text) + if (Option.isSome(responseBody) && responseBody.value) { + body = responseBody.value + } + } + + let json: unknown = undefined + // @effect-diagnostics effect/tryCatchInEffectGen:off + try { + json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined + } catch { + json = undefined + } + const decoded = Schema.decodeUnknownOption(OpenRouterErrorBody)(json) + + const reason = mapStatusCodeToReason({ + status, + headers, + message: Option.isSome(decoded) ? decoded.value.error.message : undefined, + http: buildHttpContext({ request, response, body }), + metadata: { + errorCode: Option.isSome(decoded) ? decoded.value.error.code ?? null : null, + errorType: Option.isSome(decoded) ? decoded.value.error.type ?? null : null, + requestId: requestId ?? null + } + }) + + return yield* AiError.make({ module: "OpenRouterClient", method, reason }) +}) + +// ============================================================================= +// Rate Limits +// ============================================================================= + +/** @internal */ +export const parseRateLimitHeaders = (headers: Record) => { + const retryAfterRaw = headers["retry-after"] + let retryAfter: Duration.Duration | undefined + if (Predicate.isNotUndefined(retryAfterRaw)) { + const parsed = Number.parse(retryAfterRaw) + if (Option.isSome(parsed)) { + retryAfter = Duration.seconds(parsed.value) + } + } + const remainingRaw = headers["x-ratelimit-remaining-requests"] + const remaining = Predicate.isNotUndefined(remainingRaw) + ? Option.getOrNull(Number.parse(remainingRaw)) + : null + return { + retryAfter, + limit: headers["x-ratelimit-limit-requests"] ?? null, + remaining, + resetRequests: headers["x-ratelimit-reset-requests"] ?? null, + resetTokens: headers["x-ratelimit-reset-tokens"] ?? null + } +} + +// ============================================================================= +// HTTP Context +// ============================================================================= + +/** @internal */ +export const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +/** @internal */ +export const buildHttpContext = (params: { + readonly request: HttpClientRequest.HttpClientRequest + readonly response?: HttpClientResponse.HttpClientResponse + readonly body?: string | undefined +}): typeof AiError.HttpContext.Type => ({ + request: buildHttpRequestDetails(params.request), + response: Predicate.isNotUndefined(params.response) + ? { + status: params.response.status, + headers: Redactable.redact(params.response.headers) as Record + } + : undefined, + body: params.body +}) + +// ============================================================================= +// HTTP Status Code +// ============================================================================= + +const buildInvalidRequestDescription = (params: { + readonly status: number + readonly message: string | undefined + readonly method: string + readonly url: string + readonly errorCode: string | number | null + readonly errorType: string | null + readonly requestId: string | null + readonly body: string | undefined +}): string => { + const parts: Array = [] + + if (params.message) { + parts.push(params.message) + } else { + parts.push(`HTTP ${params.status}`) + } + + parts.push(`(${params.method} ${params.url})`) + + if (params.errorCode) { + parts.push(`[code: ${params.errorCode}]`) + } else if (params.errorType) { + parts.push(`[type: ${params.errorType}]`) + } + + if (params.requestId) { + parts.push(`[requestId: ${params.requestId}]`) + } + + if (!params.message && params.body) { + const truncated = params.body.length > 200 + ? params.body.slice(0, 200) + "..." + : params.body + parts.push(`Response: ${truncated}`) + } + + return parts.join(" ") +} + +/** @internal */ +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { + readonly status: number + readonly headers: Record + readonly message: string | undefined + readonly metadata: OpenRouterErrorMetadata + readonly http: typeof AiError.HttpContext.Type +}): AiError.AiErrorReason => { + const invalidRequestDescription = buildInvalidRequestDescription({ + status, + message, + method: http.request.method, + url: http.request.url, + errorCode: metadata.errorCode, + errorType: metadata.errorType, + requestId: metadata.requestId, + body: http.body + }) + + switch (status) { + case 400: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openrouter: metadata }, + http + }) + case 401: + return new AiError.AuthenticationError({ + kind: "InvalidKey", + metadata: { openrouter: metadata }, + http + }) + case 403: + return new AiError.AuthenticationError({ + kind: "InsufficientPermissions", + metadata: { openrouter: metadata }, + http + }) + case 404: + case 409: + case 422: + return new AiError.InvalidRequestError({ + description: invalidRequestDescription, + metadata: { openrouter: metadata }, + http + }) + case 429: { + if ( + metadata.errorCode === "insufficient_quota" || + metadata.errorType === "insufficient_quota" + ) { + return new AiError.QuotaExhaustedError({ + metadata: { openrouter: metadata }, + http + }) + } + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + return new AiError.RateLimitError({ + retryAfter, + metadata: { + openrouter: { + ...metadata, + ...rateLimitMetadata + } + }, + http + }) + } + case 529: + return new AiError.InternalProviderError({ + description: message ?? "OpenRouter API is overloaded", + metadata: { openrouter: metadata }, + http + }) + default: + if (status >= 500) { + return new AiError.InternalProviderError({ + description: message ?? "Server error", + metadata: { openrouter: metadata }, + http + }) + } + return new AiError.UnknownError({ + description: message, + metadata: { openrouter: metadata }, + http + }) + } +} diff --git a/repos/effect/packages/ai/openrouter/src/internal/utilities.ts b/repos/effect/packages/ai/openrouter/src/internal/utilities.ts new file mode 100644 index 0000000..5df64c5 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/src/internal/utilities.ts @@ -0,0 +1,92 @@ +import * as Predicate from "effect/Predicate" +import type * as Response from "effect/unstable/ai/Response" +import type { ReasoningDetails } from "../OpenRouterLanguageModel.ts" + +const finishReasonMap: Record = { + content_filter: "content-filter", + error: "error", + function_call: "tool-calls", + length: "length", + tool_calls: "tool-calls", + stop: "stop" +} + +/** @internal */ +export const resolveFinishReason = ( + finishReason: string | null | undefined +): Response.FinishReason => + Predicate.isNotNullish(finishReason) + ? finishReasonMap[finishReason] + : "other" + +/** + * Tracks ReasoningDetailUnion entries and deduplicates them based + * on a derived canonical key. + * + * This is used when converting messages to ensure the API request only + * contains unique reasoning details, preventing "Duplicate item found with id" + * errors in multi-turn conversations. + * + * The canonical key logic matches the OpenRouter API's deduplication exactly + * (see openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts): + * - Summary: key = summary field + * - Encrypted: key = id field (if truthy) or data field + * - Text: key = text field (if truthy) or signature field (if truthy) + * + * @internal + */ +export class ReasoningDetailsDuplicateTracker { + readonly #seenKeys = new Set() + + /** + * Attempts to track a detail. + * + * or `false` if it was skipped (no valid key) or already seen (duplicate). + */ + upsert(detail: ReasoningDetails[number]): boolean { + const key = this.getCanonicalKey(detail) + + if (Predicate.isNull(key)) { + return false + } + + if (this.#seenKeys.has(key)) { + return false + } + + this.#seenKeys.add(key) + + return true + } + + private getCanonicalKey(detail: ReasoningDetails[number]): string | null { + // This logic matches the OpenRouter API's deduplication exactly. + // See: openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts + switch (detail.type) { + case "reasoning.summary": { + return detail.summary + } + + case "reasoning.encrypted": { + return Predicate.isNotNullish(detail.id) ? detail.id : detail.data + } + + case "reasoning.text": { + if (Predicate.isNotNullish(detail.text)) { + return detail.text + } + + if (Predicate.isNotNullish(detail.signature)) { + return detail.signature + } + + return null + } + + default: { + // Handle unknown types gracefully + return null + } + } + } +} diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts new file mode 100644 index 0000000..2937111 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts @@ -0,0 +1,265 @@ +import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" +import { assert, describe, it } from "@effect/vitest" +import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { Array, Context, Effect, Layer, Redacted, Ref, Schema } from "effect" +import { LanguageModel, Prompt } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenRouterLanguageModel", () => { + describe("generateText", () => { + describe("message preparation", () => { + describe("audio file parts", () => { + it.effect("converts audio bytes to input_audio", () => + Effect.gen(function*() { + const audioData = new Uint8Array([0x49, 0x44, 0x33, 0x04]) // ID3v2 magic bytes + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "audio/mpeg", + data: audioData + }) + ] + }]) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.messages.find((message: any) => message.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "input_audio", + input_audio: { + data: "SUQzBA==", + format: "mp3" + } + }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("converts base64 data url audio to input_audio", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "audio/wav", + data: "data:audio/wav;base64,UklGRg==" + }) + ] + }]) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.messages.find((message: any) => message.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "input_audio", + input_audio: { + data: "UklGRg==", + format: "wav" + } + }]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("maps audio media types to OpenRouter audio formats", () => + Effect.gen(function*() { + const mediaTypes: ReadonlyArray = [ + ["audio/aac", "aac"], + ["audio/x-aiff", "aiff"], + ["audio/flac", "flac"], + ["audio/L16", "pcm16"], + ["audio/mp4", "m4a"], + ["audio/mp3", "mp3"], + ["audio/ogg", "ogg"], + ["audio/x-wav", "wav"] + ] + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: mediaTypes.map(([mediaType]) => + Prompt.filePart({ + mediaType, + data: new Uint8Array([0x00]) + }) + ) + }]) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.messages.find((message: any) => message.role === "user") + deepStrictEqual( + userMessage.content.map((item: any) => item.input_audio.format), + mediaTypes.map(([, format]) => format) + ) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("fails on unsupported audio media types", () => + Effect.gen(function*() { + const error = yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "audio/webm", + data: new Uint8Array([0x00]) + }) + ] + }]) + }).pipe( + Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")), + Effect.flip + ) + + strictEqual(error.reason._tag, "InvalidUserInputError") + assert.include(error.message, "audio/webm") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("fails on audio URLs", () => + Effect.gen(function*() { + const error = yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "audio/mpeg", + data: new URL("https://example.com/audio.mp3") + }) + ] + }]) + }).pipe( + Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")), + Effect.flip + ) + + strictEqual(error.reason._tag, "InvalidUserInputError") + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("converts non-audio files to file blocks", () => + Effect.gen(function*() { + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [ + Prompt.filePart({ + mediaType: "application/pdf", + fileName: "document.pdf", + data: pdfData + }) + ] + }]) + }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + const userMessage = body.messages.find((message: any) => message.role === "user") + deepStrictEqual(userMessage.content, [{ + type: "file", + file: { + filename: "document.pdf", + file_data: "data:application/pdf;base64,JVBERg==" + } + }]) + }).pipe(Effect.provide(makeTestLayer()))) + }) + }) + }) +}) + +// ============================================================================= +// Test Infrastructure +// ============================================================================= + +class MockOpenRouterResponse extends Context.Service | undefined +}>()("MockOpenRouterResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = Effect.service(MockHttpClient).pipe( + Effect.flatMap((client) => client.requests) + ) +} + +const encodeResponse = Schema.encodeEffect(Generated.SendChatCompletionRequest200) + +const makeHttpClient = Effect.gen(function*() { + const capturedRequests = yield* Ref.make>([]) + const response = yield* MockOpenRouterResponse + const body = yield* Effect.orDie(encodeResponse(response.body)) + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(capturedRequests, Array.append(request)) + return HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + headers: response.headers ?? {}, + status: response.status + }) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) })) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClient) + +const makeDefaultResponse = ( + overrides: Partial = {} +): typeof Generated.SendChatCompletionRequest200.Type => ({ + id: "gen-test123", + choices: [{ + finish_reason: "stop", + index: 0, + message: { + role: "assistant", + content: "Hello!" + } + }], + created: 1234567890, + model: "google/gemini-2.5-flash", + object: "chat.completion", + ...overrides +}) + +const makeTestLayer = (options: { + readonly body?: Partial + readonly status?: number + readonly headers?: Record +} = {}) => + OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenRouterResponse, { + body: makeDefaultResponse(options.body), + status: options.status ?? 200, + headers: options.headers ?? {} + })) + ) + +const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => + Effect.gen(function*() { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) + }) diff --git a/repos/effect/packages/ai/openrouter/tsconfig.json b/repos/effect/packages/ai/openrouter/tsconfig.json new file mode 100644 index 0000000..a9b59a3 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/tsconfig.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" }, + { "path": "../anthropic" }, + { "path": "../openai" } + ] +} diff --git a/repos/effect/packages/ai/openrouter/vitest.config.ts b/repos/effect/packages/ai/openrouter/vitest.config.ts new file mode 100644 index 0000000..c8a52c1 --- /dev/null +++ b/repos/effect/packages/ai/openrouter/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/repos/effect/packages/atom/react/CHANGELOG.md b/repos/effect/packages/atom/react/CHANGELOG.md new file mode 100644 index 0000000..21cf509 --- /dev/null +++ b/repos/effect/packages/atom/react/CHANGELOG.md @@ -0,0 +1,711 @@ +# @effect/atom-react + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- [#1314](https://github.com/Effect-TS/effect-smol/pull/1314) [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8) Thanks @zeyuri! - Port ReactHydration to effect-smol. + + Add `Hydration` module to `effect/unstable/reactivity` with `dehydrate`, `hydrate`, and `toValues` for SSR state serialization. Add `HydrationBoundary` React component to `@effect/atom-react` with two-phase hydration (new atoms in render, existing atoms after commit). + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/atom/react/LICENSE b/repos/effect/packages/atom/react/LICENSE new file mode 100644 index 0000000..7f6fe48 --- /dev/null +++ b/repos/effect/packages/atom/react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/atom/react/README.md b/repos/effect/packages/atom/react/README.md new file mode 100644 index 0000000..1cd2f20 --- /dev/null +++ b/repos/effect/packages/atom/react/README.md @@ -0,0 +1,7 @@ +# `@effect/atom-react` + +React bindings for the Effect Atom modules. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-react). diff --git a/repos/effect/packages/atom/react/docgen.json b/repos/effect/packages/atom/react/docgen.json new file mode 100644 index 0000000..2eb419b --- /dev/null +++ b/repos/effect/packages/atom/react/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/react/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/atom/react/package.json b/repos/effect/packages/atom/react/package.json new file mode 100644 index 0000000..56ff32b --- /dev/null +++ b/repos/effect/packages/atom/react/package.json @@ -0,0 +1,78 @@ +{ + "name": "@effect/atom-react", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "React bindings for the Effect Atom modules", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/atom/react" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "react", + "database" + ], + "keywords": [ + "typescript", + "react", + "database" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "effect": "workspace:^", + "react": "^19.2.4", + "scheduler": "*" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.2", + "@types/scheduler": "^0.26.0", + "effect": "workspace:^", + "jsdom": "^29.1.1", + "react": "19.2.7", + "react-dom": "19.2.7", + "react-error-boundary": "^6.1.2", + "scheduler": "^0.27.0" + } +} diff --git a/repos/effect/packages/atom/react/src/Hooks.ts b/repos/effect/packages/atom/react/src/Hooks.ts new file mode 100644 index 0000000..381d86d --- /dev/null +++ b/repos/effect/packages/atom/react/src/Hooks.ts @@ -0,0 +1,476 @@ +/** + * React hooks for working with Effect atoms from components. The hooks read, + * write, mount, refresh, and subscribe to atoms from `RegistryContext`, handle + * `AsyncResult` atoms with React Suspense, and expose helpers for reading and + * deriving `AtomRef` values. + * + * @since 4.0.0 + */ +"use client" + +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult" +import * as Atom from "effect/unstable/reactivity/Atom" +import type * as AtomRef from "effect/unstable/reactivity/AtomRef" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import * as React from "react" +import { RegistryContext } from "./RegistryContext.ts" + +interface AtomStore { + readonly subscribe: (f: () => void) => () => void + readonly snapshot: () => A + readonly getServerSnapshot: () => A +} + +const storeRegistry = new WeakMap, AtomStore>>() + +function makeStore(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom): AtomStore { + let stores = storeRegistry.get(registry) + if (stores === undefined) { + stores = new WeakMap() + storeRegistry.set(registry, stores) + } + const store = stores.get(atom) + if (store !== undefined) { + return store + } + const newStore: AtomStore = { + subscribe(f) { + return registry.subscribe(atom, f) + }, + snapshot() { + return registry.get(atom) + }, + getServerSnapshot() { + return Atom.getServerValue(atom, registry) + } + } + stores.set(atom, newStore) + return newStore +} + +function useStore(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom): A { + const store = makeStore(registry, atom) + + return React.useSyncExternalStore(store.subscribe, store.snapshot, store.getServerSnapshot) +} + +const initialValuesSet = new WeakMap>>() + +/** + * Seeds initial atom values in the current React atom registry. + * + * **When to use** + * + * Use to seed atom values from a React component after the current registry + * already exists. + * + * **Gotchas** + * + * Each atom is initialized at most once for a given registry by this hook, so + * later calls for the same atom in that registry are ignored. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomInitialValues = (initialValues: Iterable, any]>): void => { + const registry = React.useContext(RegistryContext) + let set = initialValuesSet.get(registry) + if (set === undefined) { + set = new WeakSet() + initialValuesSet.set(registry, set) + } + for (const [atom, value] of initialValues) { + if (!set.has(atom)) { + set.add(atom) + ;(registry as any).ensureNode(atom).setValue(value) + } + } +} + +/** + * Subscribes to an atom in the current React registry and returns its current + * value, optionally mapped through a selector. + * + * **When to use** + * + * Use when a React component needs to render from an atom value without also + * returning a setter. + * + * **Details** + * + * When a selector is provided, the hook maps the atom before subscribing so the + * component reads the selected value from the current `RegistryContext`. + * + * @see {@link useAtom} for reading and updating a writable atom from one component + * @see {@link useAtomRef} for reading an `AtomRef` directly + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomValue: { + (atom: Atom.Atom): A + (atom: Atom.Atom, f: (_: A) => B): B +} = (atom: Atom.Atom, f?: (_: A) => A): A => { + const registry = React.useContext(RegistryContext) + if (f) { + const atomB = React.useMemo(() => Atom.map(atom, f), [atom, f]) + return useStore(registry, atomB) + } + return useStore(registry, atom) +} + +function mountAtom(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom): void { + React.useEffect(() => registry.mount(atom), [atom, registry]) +} + +function setAtom( + registry: AtomRegistry.AtomRegistry, + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +{ + if (options?.mode === "promise" || options?.mode === "promiseExit") { + return React.useCallback((value: W) => { + registry.set(atom, value) + const promise = Effect.runPromiseExit( + AtomRegistry.getResult(registry, atom as Atom.Atom>, { + suspendOnWaiting: true + }) + ) + return options!.mode === "promise" ? promise.then(flattenExit) : promise + }, [registry, atom, options.mode]) as any + } + return React.useCallback((value: W | ((value: R) => W)) => { + registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value) + }, [registry, atom]) as any +} + +const flattenExit = (exit: Exit.Exit): A => { + if (Exit.isSuccess(exit)) return exit.value + throw Cause.squash(exit.cause) +} + +/** + * Mounts an atom in the current React registry for the lifetime of the + * component. + * + * **When to use** + * + * Use to keep an atom mounted from a React component without reading, writing, + * or refreshing it. + * + * **Details** + * + * The hook uses the current `RegistryContext` and releases the mount through + * React effect cleanup when the component unmounts or when the registry or atom + * dependency changes. + * + * @see {@link useAtomSet} for mounting a writable atom while returning a setter + * @see {@link useAtomRefresh} for mounting an atom while returning a refresh callback + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomMount = (atom: Atom.Atom): void => { + const registry = React.useContext(RegistryContext) + mountAtom(registry, atom) +} + +/** + * Mounts a writable atom and returns a setter without subscribing to its value. + * + * **When to use** + * + * Use when a React component needs to update a writable atom without rendering + * from that atom's value. + * + * **Details** + * + * The hook mounts the atom and returns a setter. In value mode the setter + * accepts a write value or updater function; for `AsyncResult` atoms, `promise` + * and `promiseExit` modes return a promise for the success value or full `Exit`. + * + * @see {@link useAtom} for reading and updating the same writable atom + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomSet = < + R, + W, + Mode extends "value" | "promise" | "promiseExit" = never +>( + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) => +{ + const registry = React.useContext(RegistryContext) + mountAtom(registry, atom) + return setAtom(registry, atom, options) +} + +/** + * Mounts an atom and returns a callback that refreshes it in the current React + * registry. + * + * **When to use** + * + * Use to expose a React callback that requests a refresh for an atom without + * reading or writing its value. + * + * **Details** + * + * The hook uses the current `RegistryContext`, mounts the atom for the + * component lifetime, and returns a callback that calls `registry.refresh`. + * + * @see {@link useAtomMount} for mounting an atom without returning a refresh callback + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefresh = (atom: Atom.Atom): () => void => { + const registry = React.useContext(RegistryContext) + mountAtom(registry, atom) + return React.useCallback(() => { + registry.refresh(atom) + }, [registry, atom]) +} + +/** + * Subscribes to a writable atom and returns its current value together with a + * setter for updating it. + * + * **When to use** + * + * Use when a React component needs both to render the current value of a + * writable atom and update it from the same component. + * + * @see {@link useAtomValue} for subscribing to an atom without a setter + * @see {@link useAtomSet} for updating a writable atom without subscribing to its value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtom = ( + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): readonly [ + value: R, + write: "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +] => { + const registry = React.useContext(RegistryContext) + return [ + useStore(registry, atom), + setAtom(registry, atom, options) + ] as const +} + +const atomPromiseMap = { + suspendOnWaiting: new Map, Promise>(), + default: new Map, Promise>() +} + +function atomToPromise( + registry: AtomRegistry.AtomRegistry, + atom: Atom.Atom>, + suspendOnWaiting: boolean +) { + const map = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default + let promise = map.get(atom) + if (promise !== undefined) { + return promise + } + promise = new Promise((resolve) => { + const dispose = registry.subscribe(atom, (result) => { + if (result._tag === "Initial" || (suspendOnWaiting && result.waiting)) { + return + } + setTimeout(dispose, 1000) + resolve() + map.delete(atom) + }) + }) + map.set(atom, promise) + return promise +} + +function atomResultOrSuspend( + registry: AtomRegistry.AtomRegistry, + atom: Atom.Atom>, + suspendOnWaiting: boolean +) { + const value = useStore(registry, atom) + if (value._tag === "Initial" || (suspendOnWaiting && value.waiting)) { + throw atomToPromise(registry, atom, suspendOnWaiting) + } + return value +} + +/** + * Reads an `AsyncResult` atom through React Suspense, suspending while the + * result is initial or configured as waiting. + * + * **When to use** + * + * Use when a React component should render only after an `AsyncResult` atom has + * left its initial state, with loading delegated to a Suspense boundary. + * + * **Details** + * + * `suspendOnWaiting` defaults to `false`. When `includeFailure` is `true`, a + * failure result is returned instead of being thrown. + * + * **Gotchas** + * + * Without `includeFailure`, failure results are thrown with + * `Cause.squash(result.cause)`, so callers need an error boundary for failures. + * + * @see {@link useAtomValue} for reading the raw `AsyncResult` value without Suspense + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomSuspense = ( + atom: Atom.Atom>, + options?: { + readonly suspendOnWaiting?: boolean | undefined + readonly includeFailure?: IncludeFailure | undefined + } +): AsyncResult.Success | (IncludeFailure extends true ? AsyncResult.Failure : never) => { + const registry = React.useContext(RegistryContext) + const result = atomResultOrSuspend(registry, atom, options?.suspendOnWaiting ?? false) + if (result._tag === "Failure" && !options?.includeFailure) { + throw Cause.squash(result.cause) + } + return result as any +} + +/** + * Subscribes a callback to an atom in the current React registry for the + * component lifetime. + * + * **When to use** + * + * Use when a React component needs to run a callback for atom changes without + * reading the atom value during render. + * + * **Details** + * + * The subscription is installed in a React effect and cleaned up on unmount or + * dependency change. When `options.immediate` is enabled, the callback receives + * the current value when the effect subscribes. + * + * @see {@link useAtomValue} for reading an atom value during render instead of running a callback + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomSubscribe = ( + atom: Atom.Atom, + f: (_: A) => void, + options?: { readonly immediate?: boolean } +): void => { + const registry = React.useContext(RegistryContext) + React.useEffect( + () => registry.subscribe(atom, f, options), + [registry, atom, f, options?.immediate] + ) +} + +/** + * Subscribes to an atom ref and returns its latest value. + * + * **When to use** + * + * Use when a React component should render from an `AtomRef.ReadonlyRef` + * directly instead of reading an atom through the current registry. + * + * **Details** + * + * The hook subscribes with `ref.subscribe`, triggers re-renders through React + * state, and returns the current `ref.value`. + * + * @see {@link useAtomValue} for reading an `Atom` from the current registry + * @see {@link useAtomRefPropValue} for reading a property ref value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRef = (ref: AtomRef.ReadonlyRef): A => { + const [, setValue] = React.useState(ref.value) + React.useEffect(() => ref.subscribe(setValue), [ref]) + return ref.value +} + +/** + * Returns a memoized atom ref for a property of another atom ref. + * + * **When to use** + * + * Use to derive an `AtomRef` for one property of an object-shaped atom ref. + * + * **Details** + * + * The hook memoizes `ref.prop(prop)` for the `[ref, prop]` dependency pair and + * returns the property ref so callers can read, set, update, or subscribe to + * that nested property. + * + * @see {@link useAtomRef} for subscribing to an atom ref value + * @see {@link useAtomRefPropValue} for subscribing directly to a property value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefProp = (ref: AtomRef.AtomRef, prop: K): AtomRef.AtomRef => + React.useMemo(() => ref.prop(prop), [ref, prop]) + +/** + * Subscribes to a property ref derived from an atom ref and returns its current + * value. + * + * **When to use** + * + * Use when a React component needs only the current value of one property from + * an object-shaped `AtomRef`. + * + * **Details** + * + * The hook composes `useAtomRefProp(ref, prop)` with `useAtomRef`, so the + * property ref is memoized for the `[ref, prop]` pair and then subscribed + * through `ref.subscribe`. + * + * @see {@link useAtomRefProp} for returning the property ref directly + * @see {@link useAtomRef} for subscribing to a whole atom ref value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefPropValue = (ref: AtomRef.AtomRef, prop: K): A[K] => + useAtomRef(useAtomRefProp(ref, prop)) diff --git a/repos/effect/packages/atom/react/src/ReactHydration.ts b/repos/effect/packages/atom/react/src/ReactHydration.ts new file mode 100644 index 0000000..e160d67 --- /dev/null +++ b/repos/effect/packages/atom/react/src/ReactHydration.ts @@ -0,0 +1,109 @@ +/** + * React helpers for applying dehydrated Effect Atom state to a React subtree. + * The `HydrationBoundary` component reads the nearest `RegistryContext`, + * hydrates new Atom values before children render, and delays updates for + * existing Atom values until after commit so React transitions do not update + * the current UI too early. + * + * @since 4.0.0 + */ +"use client" +import * as Hydration from "effect/unstable/reactivity/Hydration" +import * as React from "react" +import { RegistryContext } from "./RegistryContext.ts" + +/** + * Props for a boundary that applies dehydrated Atom values to the nearest + * {@link RegistryContext} while rendering its children. + * + * @category components + * @since 4.0.0 + */ +export interface HydrationBoundaryProps { + state?: Iterable + children?: React.ReactNode +} + +/** + * Provides a React hydration boundary that loads dehydrated Atom values into + * the current Atom registry. + * + * **When to use** + * + * Use to apply dehydrated Atom state to a React subtree that reads from the + * nearest `RegistryContext`. + * + * **Details** + * + * New Atom values are hydrated during render so descendants can read them + * immediately, while values for existing Atoms are deferred until after commit + * so transition data does not update the current UI before React accepts it. + * + * @see {@link Hydration.dehydrate} for producing dehydrated Atom state + * @see {@link Hydration.hydrate} for lower-level non-React hydration + * + * @category components + * @since 4.0.0 + */ +export const HydrationBoundary: React.FC = ({ + children, + state +}) => { + const registry = React.useContext(RegistryContext) + + // This useMemo is for performance reasons only, everything inside it must + // be safe to run in every render and code here should be read as "in render". + // + // This code needs to happen during the render phase, because after initial + // SSR, hydration needs to happen _before_ children render. Also, if hydrating + // during a transition, we want to hydrate as much as is safe in render so + // we can prerender as much as possible. + // + // For any Atom values that already exist in the registry, we want to hold back on + // hydrating until _after_ the render phase. The reason for this is that during + // transitions, we don't want the existing Atom values and subscribers to update to + // the new data on the current page, only _after_ the transition is committed. + // If the transition is aborted, we will have hydrated any _new_ Atom values, but + // we throw away the fresh data for any existing ones to avoid unexpectedly + // updating the UI. + const hydrationQueue: Array | undefined = React.useMemo(() => { + if (state) { + const dehydratedAtoms = Array.from(state) as Array + const nodes = registry.getNodes() + + const newDehydratedAtoms: Array = [] + const existingDehydratedAtoms: Array = [] + + for (const dehydratedAtom of dehydratedAtoms) { + const existingNode = nodes.get(dehydratedAtom.key) + + if (!existingNode) { + // This is a new Atom value, safe to hydrate immediately + newDehydratedAtoms.push(dehydratedAtom) + } else { + // This Atom value already exists, queue it for later hydration + existingDehydratedAtoms.push(dehydratedAtom) + } + } + + if (newDehydratedAtoms.length > 0) { + // It's actually fine to call this with state that already exists + // in the registry, or is older. hydrate() is idempotent. + Hydration.hydrate(registry, newDehydratedAtoms) + } + + if (existingDehydratedAtoms.length > 0) { + return existingDehydratedAtoms + } + } + return undefined + }, [registry, state]) + + React.useEffect(() => { + if (hydrationQueue) { + Hydration.hydrate(registry, hydrationQueue) + } + }, [registry, hydrationQueue]) + + return React.createElement(React.Fragment, {}, children) +} diff --git a/repos/effect/packages/atom/react/src/RegistryContext.ts b/repos/effect/packages/atom/react/src/RegistryContext.ts new file mode 100644 index 0000000..0a179bb --- /dev/null +++ b/repos/effect/packages/atom/react/src/RegistryContext.ts @@ -0,0 +1,108 @@ +/** + * React context and provider for the Atom registry used by Effect Atom hooks. + * The registry stores atom values, schedules update work, and cleans up unused + * atoms. Sharing one registry through React context lets components in the same + * subtree read and write the same atom state. + * + * @since 4.0.0 + */ +"use client" + +import type * as Atom from "effect/unstable/reactivity/Atom" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import * as React from "react" +import * as Scheduler from "scheduler" + +/** + * Schedules Atom registry work with React's scheduler at low priority and + * returns a cancellation function for the scheduled task. + * + * @category context + * @since 4.0.0 + */ +export function scheduleTask(f: () => void): () => void { + const node = Scheduler.unstable_scheduleCallback(Scheduler.unstable_LowPriority, f) + return () => Scheduler.unstable_cancelCallback(node) +} + +/** + * Provides a React context that supplies the `AtomRegistry` used by Atom hooks and + * hydration helpers, defaulting to a standalone registry when no provider is + * present. + * + * **When to use** + * + * Use to supply an existing `AtomRegistry` through React context when hooks or + * hydration helpers need to share registry state that is managed outside + * `RegistryProvider`. + * + * @see {@link RegistryProvider} for creating and providing a registry for a React subtree + * + * @category context + * @since 4.0.0 + */ +export const RegistryContext = React.createContext(AtomRegistry.make({ + scheduleTask, + defaultIdleTTL: 400 +})) + +/** + * Provides a stable `AtomRegistry` to a React subtree, optionally seeding + * initial atom values and overriding registry scheduling or idle settings. + * + * **When to use** + * + * Use to scope atom state, scheduling, and idle cleanup to a React subtree. + * + * **Details** + * + * The provider creates one `AtomRegistry` with `AtomRegistry.make`, passes it + * through `RegistryContext.Provider`, and forwards `initialValues`, + * `scheduleTask`, `timeoutResolution`, and `defaultIdleTTL` only when that + * registry is created. + * + * **Gotchas** + * + * Option changes after the first render do not rebuild the registry. When the + * provider unmounts, registry disposal is delayed briefly and canceled if the + * provider remounts before the timeout fires. + * + * @see {@link RegistryContext} for the React context supplied by this provider + * + * @category context + * @since 4.0.0 + */ +export const RegistryProvider = (options: { + readonly children?: React.ReactNode | undefined + readonly initialValues?: Iterable, any]> | undefined + readonly scheduleTask?: ((f: () => void) => () => void) | undefined + readonly timeoutResolution?: number | undefined + readonly defaultIdleTTL?: number | undefined +}) => { + const ref = React.useRef<{ + readonly registry: AtomRegistry.AtomRegistry + timeout?: number | undefined + }>(null) + if (ref.current === null) { + ref.current = { + registry: AtomRegistry.make({ + scheduleTask: options.scheduleTask ?? scheduleTask, + initialValues: options.initialValues, + timeoutResolution: options.timeoutResolution, + defaultIdleTTL: options.defaultIdleTTL + }) + } + } + React.useEffect(() => { + if (ref.current?.timeout !== undefined) { + clearTimeout(ref.current.timeout) + } + return () => { + ref.current!.timeout = setTimeout(() => { + ref.current?.registry.dispose() + ref.current = null + }, 500) as any + } + }, [ref]) + return React.createElement(RegistryContext.Provider, { value: ref.current.registry }, options?.children) +} diff --git a/repos/effect/packages/atom/react/src/ScopedAtom.ts b/repos/effect/packages/atom/react/src/ScopedAtom.ts new file mode 100644 index 0000000..8a60482 --- /dev/null +++ b/repos/effect/packages/atom/react/src/ScopedAtom.ts @@ -0,0 +1,151 @@ +/** + * React helpers for creating Atom instances that belong to one component + * subtree. `make` returns a scoped atom with a provider, context, and `use` + * accessor. Each provider creates its own Atom once, so different subtrees can + * use the same scoped atom definition without sharing state. + * + * @since 4.0.0 + */ +"use client" + +import type * as Atom from "effect/unstable/reactivity/Atom" +import * as React from "react" + +/** + * Literal type used as the `ScopedAtom` type identifier. + * + * **Details** + * + * Used as the computed property key and marker value stored on `ScopedAtom` + * objects. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~@effect/atom-react/ScopedAtom" + +/** + * Type identifier for ScopedAtom. + * + * **Details** + * + * Used as the computed property key and marker value stored on `ScopedAtom` + * objects. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom" + +/** + * Scoped Atom interface with a provider-backed instance. + * + * **Example** (Providing and reading a scoped atom) + * + * ```ts + * import { make, useAtomValue } from "@effect/atom-react" + * import { Atom } from "effect/unstable/reactivity" + * import * as React from "react" + * + * const Counter = make(() => Atom.make(0)) + * + * function View() { + * const atom = Counter.use() + * const value = useAtomValue(atom) + * return React.createElement("div", null, value) + * } + * + * export function App() { + * return React.createElement(Counter.Provider, null, React.createElement(View)) + * } + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface ScopedAtom, Input = never> { + readonly [TypeId]: TypeId + use(): A + Provider: [Input] extends [never] ? React.FC<{ readonly children?: React.ReactNode | undefined }> + : React.FC<{ readonly children?: React.ReactNode | undefined; readonly value: Input }> + Context: React.Context +} + +/** + * Creates a ScopedAtom from a factory function. + * + * **When to use** + * + * Use to create an atom instance that is owned by a React provider and scoped + * to a component subtree. + * + * **Details** + * + * The returned scoped atom includes a `Provider`, `Context`, and `use` + * accessor. The provider creates the atom once for its lifetime, passing the + * `value` prop to the factory when the scoped atom expects input. + * + * **Gotchas** + * + * `use` must run under the matching provider. Changing the provider `value` + * prop after mount does not recreate the atom. + * + * **Example** (Creating a scoped atom with input) + * + * ```ts + * import { make, useAtomValue } from "@effect/atom-react" + * import { Atom } from "effect/unstable/reactivity" + * import * as React from "react" + * + * const User = make((name: string) => Atom.make(name)) + * + * function UserName() { + * const atom = User.use() + * const value = useAtomValue(atom) + * return React.createElement("span", null, value) + * } + * + * export function App() { + * return React.createElement( + * User.Provider, + * { value: "Ada" }, + * React.createElement(UserName) + * ) + * } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = , Input = never>( + f: (() => A) | ((input: Input) => A) +): ScopedAtom => { + const Context = React.createContext(undefined as unknown as A) + + const use = (): A => { + const atom = React.useContext(Context) + if (atom === undefined) { + throw new Error("ScopedAtom used outside of its Provider") + } + return atom + } + + const Provider: React.FC<{ readonly children?: React.ReactNode | undefined; readonly value?: Input }> = (props) => { + const atom = React.useRef(null) + if (atom.current === null) { + if ("value" in props) { + atom.current = (f as (input: Input) => A)(props.value as Input) + } else { + atom.current = (f as () => A)() + } + } + return React.createElement(Context.Provider, { value: atom.current }, props.children) + } + + return { + [TypeId]: TypeId, + use, + Provider: Provider as any, + Context + } +} diff --git a/repos/effect/packages/atom/react/src/index.ts b/repos/effect/packages/atom/react/src/index.ts new file mode 100644 index 0000000..554a0df --- /dev/null +++ b/repos/effect/packages/atom/react/src/index.ts @@ -0,0 +1,23 @@ +/** + * @since 4.0.0 + */ + +/** + * @since 4.0.0 + */ +export * from "./Hooks.ts" + +/** + * @since 4.0.0 + */ +export * from "./RegistryContext.ts" + +/** + * @since 4.0.0 + */ +export * from "./ReactHydration.ts" + +/** + * @since 4.0.0 + */ +export * from "./ScopedAtom.ts" diff --git a/repos/effect/packages/atom/react/test/index.test.tsx b/repos/effect/packages/atom/react/test/index.test.tsx new file mode 100644 index 0000000..df4d8ca --- /dev/null +++ b/repos/effect/packages/atom/react/test/index.test.tsx @@ -0,0 +1,650 @@ +// +import { act, render, screen, waitFor } from "@testing-library/react" +import { Cause, Context, Effect, Latch, Layer } from "effect" +import * as Schema from "effect/Schema" +import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" +import * as Atom from "effect/unstable/reactivity/Atom" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import * as Hydration from "effect/unstable/reactivity/Hydration" +import * as React from "react" +import { Suspense } from "react" +import { renderToString } from "react-dom/server" +import { ErrorBoundary } from "react-error-boundary" +import { beforeEach, describe, expect, it, test, vi } from "vitest" +import { HydrationBoundary, RegistryContext, RegistryProvider, useAtomSuspense, useAtomValue } from "../src/index.ts" +import * as ScopedAtom from "../src/ScopedAtom.ts" + +describe("atom-react", () => { + let registry: AtomRegistry.AtomRegistry + + beforeEach(() => { + registry = AtomRegistry.make() + }) + + describe("runtime", () => { + test("can inject test layers", () => { + class TheNumber extends Context.Service()("TheNumber", { + make: Effect.succeed({ n: 42 as number }) + }) { + static readonly layer = Layer.effect(this, this.make) + } + const runtime = Atom.runtime(TheNumber.layer) + const numberAtom = runtime.atom(TheNumber.use((_) => Effect.succeed(_.n))) + + function TestComponent() { + const value = useAtomValue(numberAtom, AsyncResult.getOrThrow) + return

+ } + + render( + + + + ) + + expect(screen.getByTestId("value")).toHaveTextContent("69") + }) + }) + + describe("useAtomValue", () => { + test("should read value from simple Atom", () => { + const atom = Atom.make(42) + + function TestComponent() { + const value = useAtomValue(atom) + return
{value}
+ } + + render() + + expect(screen.getByTestId("value")).toHaveTextContent("42") + }) + + test("should read value with transform function", () => { + const atom = Atom.make(42) + + function TestComponent() { + const value = useAtomValue(atom, (x) => x * 2) + return
{value}
+ } + + render() + + expect(screen.getByTestId("value")).toHaveTextContent("84") + }) + + test("should update when Atom value changes", async () => { + const atom = Atom.make("initial") + + function TestComponent() { + const value = useAtomValue(atom) + return
{value}
+ } + + render( + + + + ) + + expect(screen.getByTestId("value")).toHaveTextContent("initial") + + act(() => { + registry.set(atom, "updated") + }) + + await waitFor(() => { + expect(screen.getByTestId("value")).toHaveTextContent("updated") + }) + }) + + test("should work with computed Atom", () => { + const baseAtom = Atom.make(10) + const computedAtom = Atom.make((get) => get(baseAtom) * 2) + + function TestComponent() { + const value = useAtomValue(computedAtom) + return
{value}
+ } + + render() + + expect(screen.getByTestId("value")).toHaveTextContent("20") + }) + + test("suspense success", () => { + const atom = Atom.make(Effect.never) + + function TestComponent() { + const value = useAtomSuspense(atom).value + return
{value}
+ } + + render( + Loading...}> + + + ) + + expect(screen.getByTestId("loading")).toBeInTheDocument() + }) + }) + + describe("ScopedAtom", () => { + test("throws when used outside Provider", () => { + const counter = ScopedAtom.make(() => Atom.make(0)) + + function TestComponent() { + counter.use() + return
ok
+ } + + expect(() => render()).toThrow("ScopedAtom used outside of its Provider") + }) + + test("scopes atom instances per Provider", async () => { + const counter = ScopedAtom.make(() => Atom.make(0)) + + function Count() { + const atom = counter.use() + const value = useAtomValue(atom) + return
{value}
+ } + + function SetTo({ value }: { readonly value: number }) { + const atom = counter.use() + const registry = React.useContext(RegistryContext) + React.useEffect(() => { + registry.set(atom, value) + }, [registry, atom, value]) + return null + } + + render( +
+ + + + + + + + +
+ ) + + await waitFor(() => { + const values = screen.getAllByTestId("value") + expect(values[0]).toHaveTextContent("1") + expect(values[1]).toHaveTextContent("2") + }) + }) + + test("input factory uses provider value once", () => { + const makeAtom = vi.fn((value: number) => Atom.make(value)) + const scoped = ScopedAtom.make(makeAtom) + + function Count() { + const atom = scoped.use() + const value = useAtomValue(atom) + return
{value}
+ } + + const { rerender } = render( + + + + ) + + expect(screen.getByTestId("value")).toHaveTextContent("10") + expect(makeAtom).toHaveBeenCalledTimes(1) + expect(makeAtom).toHaveBeenCalledWith(10) + + rerender( + + + + ) + + expect(screen.getByTestId("value")).toHaveTextContent("10") + expect(makeAtom).toHaveBeenCalledTimes(1) + }) + + test("integrates with useAtomValue", async () => { + const scoped = ScopedAtom.make(() => Atom.make(0)) + + function Counter() { + const atom = scoped.use() + const value = useAtomValue(atom) + return
{value}
+ } + + function IncrementOnce() { + const atom = scoped.use() + const registry = React.useContext(RegistryContext) + React.useEffect(() => { + registry.set(atom, 1) + }, [registry, atom]) + return null + } + + render( + + + + + ) + + await waitFor(() => { + expect(screen.getByTestId("value")).toHaveTextContent("1") + }) + }) + }) + + test("suspense error", () => { + const atom = Atom.make(Effect.fail(new Error("test"))) + function TestComponent() { + const value = useAtomSuspense(atom).value + return
{value}
+ } + + render( + Error}> + Loading...}> + + + , + { + onCaughtError: ((error: unknown) => { + if (error instanceof Error && error.message === "test") { + return + } + // eslint-disable-next-line no-console + console.error(error) + }) as unknown as undefined // todo: fix idk why the types are weird + } + ) + + expect(screen.getByTestId("error")).toBeInTheDocument() + }) + + describe("hydration", () => { + test("basic hydration with number atom and result atoms", () => { + const atomBasic = Atom.make(0).pipe( + Atom.serializable({ + key: "basic", + schema: Schema.Number + }) + ) + const e: Effect.Effect = Effect.never + const makeAtomResult = (key: string) => + Atom.make(e).pipe( + Atom.serializable({ + key, + schema: AsyncResult.Schema({ + success: Schema.Number, + error: Schema.String + }) + }) + ) + + const atomResult1 = makeAtomResult("success") + const atomResult2 = makeAtomResult("errored") + const atomResult3 = makeAtomResult("pending") + + // Use a server-side registry to generate properly encoded dehydrated state + const serverRegistry = AtomRegistry.make() + serverRegistry.mount(atomBasic) + serverRegistry.set(atomBasic, 1) + serverRegistry.mount(atomResult1) + ;(serverRegistry.getNodes().get("success") as any).setValue( + AsyncResult.success(123) + ) + serverRegistry.mount(atomResult2) + ;(serverRegistry.getNodes().get("errored") as any).setValue( + AsyncResult.failure(Cause.fail("error")) + ) + serverRegistry.mount(atomResult3) + // atomResult3 stays Initial (just mounted, effect is Effect.never) + + const dehydratedState = Hydration.dehydrate(serverRegistry, { + encodeInitialAs: "value-only" + }) + + function Basic() { + const value = useAtomValue(atomBasic) + return
{value}
+ } + + function Result1() { + const value = useAtomValue(atomResult1) + return AsyncResult.match(value, { + onSuccess: (result) =>
{result.value}
, + onFailure: () =>
Error
, + onInitial: () =>
Loading...
+ }) + } + + function Result2() { + const value = useAtomValue(atomResult2) + return AsyncResult.match(value, { + onSuccess: (result) =>
{result.value}
, + onFailure: () =>
Error
, + onInitial: () =>
Loading...
+ }) + } + + function Result3() { + const value = useAtomValue(atomResult3) + return AsyncResult.match(value, { + onSuccess: (result) =>
{result.value}
, + onFailure: () =>
Error
, + onInitial: () =>
Loading...
+ }) + } + + render( + + + + + + + + + ) + + expect(screen.getByTestId("hydration-basic-value")).toHaveTextContent("1") + expect(screen.getByTestId("value-1")).toHaveTextContent("123") + expect(screen.getByTestId("error-2")).toBeInTheDocument() + expect(screen.getByTestId("loading-3")).toBeInTheDocument() + }) + + test("hydration streaming with resultPromise", async () => { + const latch = Latch.makeUnsafe() + let start = 0 + let stop = 0 + const atom = Atom.make( + Effect.gen(function*() { + start = start + 1 + yield* latch.await + stop = stop + 1 + return 1 + }) + ).pipe( + Atom.serializable({ + key: "test", + schema: AsyncResult.Schema({ + success: Schema.Number + }) + }) + ) + + registry.mount(atom) + + expect(start).toBe(1) + expect(stop).toBe(0) + + const dehydratedState = Hydration.dehydrate(registry, { + encodeInitialAs: "promise" + }) + + function TestComponent() { + const value = useAtomValue(atom) + return
{value._tag}
+ } + + render( + // provide a fresh registry each time to simulate hydration + + + + + + ) + + expect(screen.getByTestId("value")).toHaveTextContent("Initial") + + act(() => { + Effect.runSync(latch.open) + }) + await Effect.runPromise(latch.await) + + const result = registry.get(atom) + expect(result._tag).toBe("Success") + if (result._tag === "Success") { + expect(result.value).toBe(1) + } + + expect(screen.getByTestId("value")).toHaveTextContent("Success") + expect(start).toBe(1) + expect(stop).toBe(1) + }) + + test("HydrationBoundary splits new vs existing atoms", () => { + const newAtom = Atom.make(0).pipe( + Atom.serializable({ key: "new-atom", schema: Schema.Number }) + ) + + // Use a server registry to generate dehydrated state + const serverRegistry = AtomRegistry.make() + serverRegistry.mount(newAtom) + serverRegistry.set(newAtom, 99) + const dehydratedState = Hydration.dehydrate(serverRegistry) + + function NewValue() { + const value = useAtomValue(newAtom) + return
{value}
+ } + + // Render with a fresh client registry (no pre-existing atoms) + render( + + + + + + ) + + // New atom should be hydrated immediately during render + expect(screen.getByTestId("new")).toHaveTextContent("99") + }) + + test("dehydrate with encodeInitialAs ignore (default)", () => { + const atom = Atom.make(Effect.never as Effect.Effect).pipe( + Atom.serializable({ + key: "initial-atom", + schema: AsyncResult.Schema({ success: Schema.Number }) + }) + ) + + registry.mount(atom) + + // Default behavior: Initial values should be ignored + const state = Hydration.dehydrate(registry) + const values = Hydration.toValues(state) + + expect(values.length).toBe(0) + }) + + test("dehydrate with encodeInitialAs value-only", () => { + const atom = Atom.make(Effect.never as Effect.Effect).pipe( + Atom.serializable({ + key: "initial-atom", + schema: AsyncResult.Schema({ success: Schema.Number }) + }) + ) + + registry.mount(atom) + + // value-only: should encode the Initial value without a resultPromise + const state = Hydration.dehydrate(registry, { + encodeInitialAs: "value-only" + }) + const values = Hydration.toValues(state) + + expect(values.length).toBe(1) + expect(values[0].key).toBe("initial-atom") + expect(values[0].resultPromise).toBeUndefined() + }) + + test("serializable encode/decode survives JSON roundtrip (wire transfer)", () => { + const atom = Atom.make(0 as never).pipe( + Atom.serializable({ + key: "wire-test", + schema: AsyncResult.Schema({ + success: Schema.Struct({ name: Schema.String }), + error: Schema.String + }) + }) + ) + + const original = AsyncResult.success({ name: "hello" }) + + // Encode using the atom's serializable encode + const encoded = atom[Atom.SerializableTypeId].encode(original) + + // Simulate wire transfer (seroval / JSON serialization roundtrip) + const wireTransferred = JSON.parse(JSON.stringify(encoded)) + + // Decode after wire transfer — this was the bug: decode would fail + // because the encoded value lost its AsyncResult prototype + const decoded = atom[Atom.SerializableTypeId].decode(wireTransferred) + + expect(AsyncResult.isAsyncResult(decoded)).toBe(true) + expect(decoded._tag).toBe("Success") + if (AsyncResult.isSuccess(decoded)) { + expect(decoded.value).toEqual({ name: "hello" }) + } + }) + + test("dehydrate + JSON roundtrip + hydrate works (SSR simulation)", () => { + const atom = Atom.make(Effect.never as Effect.Effect).pipe( + Atom.serializable({ + key: "ssr-wire", + schema: AsyncResult.Schema({ success: Schema.Number }) + }) + ) + + // Server: dehydrate + const serverRegistry = AtomRegistry.make() + serverRegistry.mount(atom) + ;(serverRegistry.getNodes().get("ssr-wire") as any).setValue( + AsyncResult.success(42) + ) + const dehydratedState = Hydration.dehydrate(serverRegistry) + + // Simulate wire transfer (seroval / JSON) + const wireTransferred = JSON.parse(JSON.stringify(dehydratedState)) + + // Client: hydrate from wire-transferred state + function TestComponent() { + const value = useAtomValue(atom) + return ( +
+ {AsyncResult.isSuccess(value) ? value.value : "not-success"} +
+ ) + } + + render( + + + + + + ) + + expect(screen.getByTestId("ssr-wire-value")).toHaveTextContent("42") + }) + + test("empty state is a no-op", () => { + function TestComponent() { + return
OK
+ } + + render( + + + + ) + + expect(screen.getByTestId("hydration-empty-state")).toHaveTextContent("OK") + }) + + test("hydrate with no state is a no-op", () => { + function TestComponent() { + return
OK
+ } + + render( + + + + ) + + expect(screen.getByTestId("hydration-no-state")).toHaveTextContent("OK") + }) + }) + + describe("SSR", () => { + it("should run atom's during SSR by default", () => { + const getCount = vi.fn(() => 0) + const counterAtom = Atom.make(getCount) + + function TestComponent() { + const count = useAtomValue(counterAtom) + return
{count}
+ } + + function App() { + return + } + + const ssrHtml = renderToString() + + expect(getCount).toHaveBeenCalled() + expect(ssrHtml).toContain("0") + + render() + + expect(getCount).toHaveBeenCalled() + expect(screen.getByText("0")).toBeInTheDocument() + }) + }) + + it("should not execute Atom effects during SSR when using withServerSnapshot", () => { + const mockFetchData = vi.fn(() => 0) + + const userDataAtom = Atom.make(Effect.sync(() => mockFetchData())).pipe( + Atom.withServerValueInitial + ) + + function TestComponent() { + const result = useAtomValue(userDataAtom) + + return
{result._tag}
+ } + + function App() { + return + } + + const ssrHtml = renderToString() + + expect(mockFetchData).not.toHaveBeenCalled() + expect(ssrHtml).toContain("Initial") + + render() + + expect(mockFetchData).toHaveBeenCalled() + expect(screen.getByText("Success")).toBeInTheDocument() + }) +}) diff --git a/repos/effect/packages/atom/react/tsconfig.json b/repos/effect/packages/atom/react/tsconfig.json new file mode 100644 index 0000000..19a2f5d --- /dev/null +++ b/repos/effect/packages/atom/react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/atom/react/vitest.config.ts b/repos/effect/packages/atom/react/vitest.config.ts new file mode 100644 index 0000000..d9ba942 --- /dev/null +++ b/repos/effect/packages/atom/react/vitest.config.ts @@ -0,0 +1,9 @@ +import { mergeConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +export default mergeConfig(shared, { + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"] + } +}) diff --git a/repos/effect/packages/atom/react/vitest.setup.ts b/repos/effect/packages/atom/react/vitest.setup.ts new file mode 100644 index 0000000..5f520a5 --- /dev/null +++ b/repos/effect/packages/atom/react/vitest.setup.ts @@ -0,0 +1,8 @@ +// oxlint-disable-next-line no-unassigned-import +import "@testing-library/jest-dom/vitest" +import { cleanup } from "@testing-library/react" +import { afterEach } from "vitest" + +afterEach(() => { + cleanup() +}) diff --git a/repos/effect/packages/atom/solid/CHANGELOG.md b/repos/effect/packages/atom/solid/CHANGELOG.md new file mode 100644 index 0000000..29af6c4 --- /dev/null +++ b/repos/effect/packages/atom/solid/CHANGELOG.md @@ -0,0 +1,707 @@ +# @effect/atom-solid + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1938](https://github.com/Effect-TS/effect-smol/pull/1938) [`23bbaac`](https://github.com/Effect-TS/effect-smol/commit/23bbaace8fd22a76e5f57aea8b4899374c40194e) Thanks @tim-smart! - allow atoms to be computed in solid bindings + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/atom/solid/LICENSE b/repos/effect/packages/atom/solid/LICENSE new file mode 100644 index 0000000..7f6fe48 --- /dev/null +++ b/repos/effect/packages/atom/solid/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/atom/solid/README.md b/repos/effect/packages/atom/solid/README.md new file mode 100644 index 0000000..a4239bf --- /dev/null +++ b/repos/effect/packages/atom/solid/README.md @@ -0,0 +1,7 @@ +# `@effect/atom-solid` + +SolidJS bindings for the Effect Atom modules. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-solid). diff --git a/repos/effect/packages/atom/solid/docgen.json b/repos/effect/packages/atom/solid/docgen.json new file mode 100644 index 0000000..a0d90fe --- /dev/null +++ b/repos/effect/packages/atom/solid/docgen.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "projectHomepage": "https://effect-ts.github.io/effect/docs/atom-solid", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/solid/src/", + "enforceVersion": true, + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/atom/solid/package.json b/repos/effect/packages/atom/solid/package.json new file mode 100644 index 0000000..bba5fcd --- /dev/null +++ b/repos/effect/packages/atom/solid/package.json @@ -0,0 +1,71 @@ +{ + "name": "@effect/atom-solid", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "SolidJS bindings for the Effect Atom modules", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/atom/solid" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "solid", + "database" + ], + "keywords": [ + "typescript", + "solid", + "database" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "effect": "workspace:^", + "solid-js": ">=1 <2" + }, + "devDependencies": { + "@solidjs/testing-library": "^0.8.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "effect": "workspace:^", + "jsdom": "^29.1.1", + "solid-js": "^1.9.14" + } +} diff --git a/repos/effect/packages/atom/solid/src/Hooks.ts b/repos/effect/packages/atom/solid/src/Hooks.ts new file mode 100644 index 0000000..d4d98de --- /dev/null +++ b/repos/effect/packages/atom/solid/src/Hooks.ts @@ -0,0 +1,359 @@ +/** + * Solid hooks for using Effect Atoms from components and computations. The + * hooks read and write atoms through the current `RegistryContext`, mount atoms + * for cleanup, subscribe callbacks, seed initial values, expose `AsyncResult` + * atoms as Solid resources, and read values from `AtomRef` references. + * + * @since 4.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" +import * as Atom from "effect/unstable/reactivity/Atom" +import type * as AtomRef from "effect/unstable/reactivity/AtomRef" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import type { Accessor, ResourceOptions, ResourceReturn } from "solid-js" +import { createComputed, createEffect, createMemo, createResource, createSignal, onCleanup, useContext } from "solid-js" +import { RegistryContext } from "./RegistryContext.ts" + +const initialValuesSet = new WeakMap>>() + +/** + * Seeds initial atom values in the current Solid atom registry. + * + * **When to use** + * + * Use to seed atom values from a Solid component after the current registry + * already exists. + * + * **Details** + * + * For each atom in the current registry, this hook applies the first value + * supplied through the hook. Later calls for the same atom in that registry are + * ignored. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomInitialValues = (initialValues: Iterable, any]>): void => { + const registry = useContext(RegistryContext) + let set = initialValuesSet.get(registry) + if (set === undefined) { + set = new WeakSet() + initialValuesSet.set(registry, set) + } + for (const [atom, value] of initialValues) { + if (!set.has(atom)) { + set.add(atom) + ;(registry as any).ensureNode(atom).setValue(value) + } + } +} + +/** + * Subscribes to an atom in the current Solid registry and returns its value as + * a Solid accessor. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomValue: { +
(atom: () => Atom.Atom): Accessor + (atom: () => Atom.Atom, f: (_: A) => B): Accessor +} = (atom: () => Atom.Atom, f?: (_: A) => A): Accessor => { + const registry = useContext(RegistryContext) + return createAtomAccessor(registry, f ? () => Atom.map(atom(), f) : atom) +} + +function createAtomAccessor(registry: AtomRegistry.AtomRegistry, atom: () => Atom.Atom): Accessor { + const [value, setValue] = createSignal(null as any) + createComputed(() => { + onCleanup(registry.subscribe(atom(), setValue as any, constImmediate)) + }) + return value +} + +const constImmediate = { immediate: true } + +function mountAtom(registry: AtomRegistry.AtomRegistry, atom: () => Atom.Atom): void { + createComputed(() => { + onCleanup(registry.mount(atom())) + }) +} + +function setAtom( + registry: AtomRegistry.AtomRegistry, + atom: () => Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +{ + const memo = createMemo(atom) + if (options?.mode === "promise" || options?.mode === "promiseExit") { + return ((value: W) => { + registry.set(memo(), value) + const promise = Effect.runPromiseExit( + AtomRegistry.getResult(registry, memo() as Atom.Atom>, { + suspendOnWaiting: true + }) + ) + return options!.mode === "promise" ? promise.then(flattenExit) : promise + }) as any + } + return ((value: W | ((value: R) => W)) => { + registry.set(memo(), typeof value === "function" ? (value as any)(registry.get(memo())) : value) + }) as any +} + +const flattenExit = (exit: Exit.Exit): A => { + if (Exit.isSuccess(exit)) return exit.value + throw Cause.squash(exit.cause) +} + +/** + * Mounts an atom in the current Solid registry for the lifetime of the current + * Solid computation. + * + * **When to use** + * + * Use to keep an atom mounted from a Solid owner without reading, writing, or + * refreshing it. + * + * **Details** + * + * The hook uses the current `RegistryContext`, mounts inside a Solid + * computation, and releases the mount through Solid cleanup when the + * computation changes or the owner is disposed. + * + * @see {@link useAtomSet} for mounting a writable atom while returning a setter + * @see {@link useAtomRefresh} for mounting an atom while returning a refresh callback + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomMount = (atom: () => Atom.Atom): void => { + const registry = useContext(RegistryContext) + mountAtom(registry, atom) +} + +/** + * Returns a setter for a writable atom without subscribing to its value. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomSet = < + R, + W, + Mode extends "value" | "promise" | "promiseExit" = never +>( + atom: () => Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) => +{ + const registry = useContext(RegistryContext) + mountAtom(registry, atom) + return setAtom(registry, atom, options) +} + +/** + * Mounts an atom and returns a callback that refreshes the current atom. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefresh = (atom: () => Atom.Atom): () => void => { + const registry = useContext(RegistryContext) + mountAtom(registry, atom) + const memo = createMemo(atom) + return () => registry.refresh(memo()) +} + +/** + * Returns a Solid accessor for a writable atom together with a setter for + * updating it. + * + * **When to use** + * + * Use when a Solid component or computation needs both a reactive accessor for + * a writable atom and a write function for that same atom. + * + * **Details** + * + * The setter accepts either a write value or an updater function. For + * `AsyncResult` atoms, `promise` and `promiseExit` modes return promises for the + * success value or full `Exit`. + * + * @see {@link useAtomValue} for subscribing to an atom without a setter + * @see {@link useAtomSet} for updating a writable atom without subscribing to its value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtom = ( + atom: () => Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): readonly [ + value: Accessor, + write: "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +] => { + const registry = useContext(RegistryContext) + return [ + createAtomAccessor(registry, atom), + setAtom(registry, atom, options) + ] as const +} + +/** + * Subscribes a callback to an atom in the current Solid registry. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomSubscribe = ( + atom: () => Atom.Atom, + f: (_: A) => void, + options?: { readonly immediate?: boolean } +): void => { + const registry = useContext(RegistryContext) + createEffect(() => { + onCleanup(registry.subscribe(atom(), f, options)) + }) +} + +/** + * Converts an `AsyncResult` atom into a Solid resource. + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomResource = ( + atom: () => Atom.Atom>, + options?: ResourceOptions & { + readonly suspendOnWaiting?: boolean | undefined + } +): ResourceReturn => { + const result = useAtomValue(atom) + return createResource(result, (result) => { + if (AsyncResult.isInitial(result) || (options?.suspendOnWaiting && result.waiting)) { + return constUnresolvedPromise + } else if (AsyncResult.isSuccess(result)) { + return Promise.resolve(result.value) + } + return Promise.reject(Cause.squash(result.cause)) + }) +} + +const constUnresolvedPromise = new Promise(() => {}) + +/** + * Subscribes to an atom ref and returns its value as a Solid accessor. + * + * **When to use** + * + * Use when a Solid component or computation should render from an + * `AtomRef.ReadonlyRef` directly instead of reading an atom through the current + * registry. + * + * **Details** + * + * The hook accepts a thunk for the ref, reads `ref().value`, subscribes with + * `ref.subscribe`, and releases the subscription through Solid cleanup when + * the selected ref changes or the owner is disposed. + * + * @see {@link useAtomValue} for reading an `Atom` from the current registry + * @see {@link useAtomRefPropValue} for reading a property ref value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRef = (ref: () => AtomRef.ReadonlyRef): Accessor => { + const [value, setValue] = createSignal(null as A) + createComputed(() => { + const r = ref() + setValue(r.value as any) + onCleanup(r.subscribe(setValue)) + }) + return value +} + +/** + * Returns a Solid accessor for a property ref derived from an atom ref. + * + * **When to use** + * + * Use to derive an `AtomRef` for one property of an object-shaped atom ref in a + * Solid computation. + * + * **Details** + * + * The returned accessor memoizes `ref().prop(prop)`, updating when the source + * ref thunk produces a different ref. + * + * **Gotchas** + * + * The `prop` argument is captured as a plain value. Recreate the hook call when + * the property key should change. + * + * @see {@link useAtomRef} for subscribing to an atom ref value + * @see {@link useAtomRefPropValue} for subscribing directly to a property value + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefProp = ( + ref: () => AtomRef.AtomRef, + prop: K +): Accessor> => createMemo(() => ref().prop(prop)) + +/** + * Returns a Solid accessor for the value of a property ref derived from an atom + * ref. + * + * **When to use** + * + * Use when a Solid component or computation needs the value of one property + * from an object-shaped `AtomRef` without keeping the intermediate property ref. + * + * **Details** + * + * The hook composes `useAtomRefProp(ref, prop)` with `useAtomRef`, returning a + * Solid accessor for the selected property value. + * + * **Gotchas** + * + * The `prop` argument is captured as a plain value. Recreate the hook call when + * the property key should change. + * + * @see {@link useAtomRef} for subscribing to a whole atom ref value + * @see {@link useAtomRefProp} for returning the property ref directly + * + * @category hooks + * @since 4.0.0 + */ +export const useAtomRefPropValue = (ref: () => AtomRef.AtomRef, prop: K): Accessor => + useAtomRef(useAtomRefProp(ref, prop)) diff --git a/repos/effect/packages/atom/solid/src/RegistryContext.ts b/repos/effect/packages/atom/solid/src/RegistryContext.ts new file mode 100644 index 0000000..7c1b6c0 --- /dev/null +++ b/repos/effect/packages/atom/solid/src/RegistryContext.ts @@ -0,0 +1,80 @@ +/** + * Solid context and provider for the Atom registry used by Effect Atom hooks. + * The registry stores atom values, schedules update work, and cleans up unused + * atoms. Sharing one registry through Solid context lets components and + * computations in the same owner tree read and write the same atom state. + * + * @since 4.0.0 + */ +import type * as Atom from "effect/unstable/reactivity/Atom" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import type { JSX } from "solid-js" +import { createComponent, createContext, onCleanup } from "solid-js" + +/** + * Provides a Solid context that carries the `AtomRegistry` used by atom hooks in the + * current owner tree. + * + * **When to use** + * + * Use when you need to integrate lower-level Solid context APIs, such as custom + * providers or hooks, instead of using the default atom hook setup. + * + * **Details** + * + * When no provider is present, the context uses a standalone default registry. + * + * @see {@link RegistryProvider} for creating and providing a registry for a Solid subtree + * + * @category context + * @since 4.0.0 + */ +export const RegistryContext = createContext(AtomRegistry.make()) + +/** + * Creates an `AtomRegistry` for a Solid subtree, optionally seeding initial atom + * values and scheduler settings, and disposes the registry when the owner is + * cleaned up. + * + * **When to use** + * + * Use to scope atom state, scheduling, and cleanup to a Solid subtree. + * + * **Details** + * + * The provider creates an `AtomRegistry` with `AtomRegistry.make`, forwards + * `initialValues`, `scheduleTask`, `timeoutResolution`, and + * `defaultIdleTTL`, and supplies the registry through `RegistryContext`. + * + * **Gotchas** + * + * Provider options are consumed when the registry is created; they are not + * reactive updates. A custom `scheduleTask` should return a cancellation + * function that is safe to call during Solid cleanup. + * + * @see {@link RegistryContext} for the context supplied by this provider + * + * @category context + * @since 4.0.0 + */ +export const RegistryProvider = (options: { + readonly children?: JSX.Element | undefined + readonly initialValues?: Iterable, any]> | undefined + readonly scheduleTask?: ((f: () => void) => () => void) | undefined + readonly timeoutResolution?: number | undefined + readonly defaultIdleTTL?: number | undefined +}) => { + const registry = AtomRegistry.make({ + scheduleTask: options.scheduleTask, + initialValues: options.initialValues, + timeoutResolution: options.timeoutResolution, + defaultIdleTTL: options.defaultIdleTTL ?? 400 + }) + onCleanup(() => registry.dispose()) + return createComponent(RegistryContext.Provider, { + value: registry, + get children() { + return options.children + } + }) +} diff --git a/repos/effect/packages/atom/solid/src/index.ts b/repos/effect/packages/atom/solid/src/index.ts new file mode 100644 index 0000000..e8c4482 --- /dev/null +++ b/repos/effect/packages/atom/solid/src/index.ts @@ -0,0 +1,13 @@ +/** + * @since 4.0.0 + */ + +/** + * @since 4.0.0 + */ +export * from "./Hooks.ts" + +/** + * @since 4.0.0 + */ +export * from "./RegistryContext.ts" diff --git a/repos/effect/packages/atom/solid/test/index.test.tsx b/repos/effect/packages/atom/solid/test/index.test.tsx new file mode 100644 index 0000000..cd90407 --- /dev/null +++ b/repos/effect/packages/atom/solid/test/index.test.tsx @@ -0,0 +1,296 @@ +import { + RegistryContext, + useAtom, + useAtomInitialValues, + useAtomRef, + useAtomRefProp, + useAtomRefPropValue, + useAtomResource, + useAtomValue +} from "@effect/atom-solid" +import { assert, describe, it } from "@effect/vitest" +import { AsyncResult, Atom, AtomRef, AtomRegistry } from "effect/unstable/reactivity" +import { type Accessor, createComponent, createEffect, createRoot, type Resource } from "solid-js" + +describe("atom-solid", () => { + describe("useAtomValue", () => { + it("reads value from simple Atom", () => { + const atom = Atom.make(42) + let observed: number | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }) + assert.strictEqual(observed, 42) + dispose() + }) + + it("reads value with transform function", () => { + const atom = Atom.make(42) + let observed: number | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }, { map: (value) => value * 2 }) + assert.strictEqual(observed, 84) + dispose() + }) + + it("updates when Atom value changes", () => { + const registry = AtomRegistry.make() + const atom = Atom.make("initial") + let observed: string | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }, { registry }) + assert.strictEqual(observed, "initial") + registry.set(atom, "updated") + assert.strictEqual(observed, "updated") + dispose() + }) + + it("works with computed Atom", () => { + const baseAtom = Atom.make(10) + const computedAtom = Atom.make((get) => get(baseAtom) * 2) + let observed: number | undefined + const dispose = renderAtomValue(computedAtom, (value) => { + observed = value + }) + assert.strictEqual(observed, 20) + dispose() + }) + }) + + describe("useAtom", () => { + it("updates value with setter", () => { + const atom = Atom.make(0) + let observed: number | undefined + const dispose = createRoot((dispose) => { + const [value, setValue] = useAtom(() => atom) + createEffect(() => { + observed = value() + }) + createEffect(() => { + if (value() !== 0) { + return + } + setValue(1) + setValue((current) => current + 1) + }) + return dispose + }) + assert.strictEqual(observed, 2) + dispose() + }) + }) + + describe("useAtomInitialValues", () => { + it("applies initial values once per registry", () => { + const registry = AtomRegistry.make() + const atom = Atom.make(0) + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + useAtomInitialValues([[atom, 1]]) + useAtomInitialValues([[atom, 2]]) + assert.strictEqual(registry.get(atom), 1) + return null + } + }) + return dispose + }) + }) + }) + + describe("AtomRef", () => { + it("updates when AtomRef changes", () => { + const ref = AtomRef.make(0) + let observed: number | undefined + const dispose = renderAtomRef(ref, (value) => { + observed = value + }) + assert.strictEqual(observed, 0) + ref.set(1) + assert.strictEqual(observed, 1) + dispose() + }) + + it("updates when AtomRef prop changes", () => { + const ref = AtomRef.make({ count: 0, label: "a" }) + const propRef = useAtomRefProp(() => ref, "count") + let observed: number | undefined + const dispose = renderAtomRef(propRef(), (value) => { + observed = value + }) + assert.strictEqual(observed, 0) + ref.set({ count: 1, label: "a" }) + assert.strictEqual(observed, 1) + dispose() + }) + + it("updates when AtomRef prop value changes", () => { + const ref = AtomRef.make({ count: 0, label: "a" }) + let observed: number | undefined + const dispose = renderAccessor(() => useAtomRefPropValue(() => ref, "count"), (value) => { + observed = value + }) + assert.strictEqual(observed, 0) + ref.set({ count: 2, label: "a" }) + assert.strictEqual(observed, 2) + dispose() + }) + }) + + describe("useAtomResource", () => { + it("suspends on Initial result", () => { + const atom = Atom.make(AsyncResult.initial()) + const { resource, dispose } = renderAtomResource(() => atom) + assert.strictEqual(resource.loading, true) + assert.strictEqual(resource(), undefined) + dispose() + }) + }) + + // + // it("suspends on waiting when suspendOnWaiting is true", () => { + // const atom = Atom.make(AsyncResult.success(1, { waiting: true })) + // const { resource, dispose } = renderAtomResource(atom, { suspendOnWaiting: true }) + // assert.strictEqual(resource.loading, true) + // assert.strictEqual(resource(), undefined) + // dispose() + // }) + // + // it("returns success value by default", async () => { + // const atom = Atom.make(AsyncResult.success(5, { waiting: true })) + // const { resource, dispose } = renderAtomResource(atom) + // await Promise.resolve() + // await Promise.resolve() + // assert.strictEqual(resource.loading, false) + // assert.strictEqual(resource(), 5) + // dispose() + // }) + // + // it("surfaces failure via Cause.squash", async () => { + // const error = new Error("boom") + // const atom = Atom.make(AsyncResult.fail(error)) + // let resource: ReturnType[0] | undefined + // let caught: unknown + // const dispose = createRoot((dispose) => { + // catchError(() => { + // ;[resource] = useAtomResource(atom) + // createEffect(() => { + // resource?.() + // }) + // }, (err) => { + // caught = err + // }) + // return dispose + // }) + // await Promise.resolve() + // await Promise.resolve() + // assert.ok(caught instanceof Error) + // assert.strictEqual(caught.message, "boom") + // assert.strictEqual(resource?.error, caught) + // assert.strictEqual(resource?.loading, false) + // dispose() + // }) + // + // it("preserves success result when preserveResult is true", async () => { + // const atom = Atom.make(Effect.succeed(7)) + // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true }) + // await Promise.resolve() + // await Promise.resolve() + // const result = resource()! + // assert.strictEqual(AsyncResult.isSuccess(result), true) + // if (AsyncResult.isSuccess(result)) { + // assert.strictEqual(result.value, 7) + // } + // assert.strictEqual(resource.error, undefined) + // dispose() + // }) + // + // it("preserves failure result when preserveResult is true", async () => { + // const error = new Error("failure") + // const atom = Atom.make(Effect.fail(error)) + // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true }) + // await Promise.resolve() + // await Promise.resolve() + // const result = resource()! + // assert.strictEqual(AsyncResult.isFailure(result), true) + // if (AsyncResult.isFailure(result)) { + // const squashed = Cause.squash(result.cause) + // assert.ok(squashed instanceof Error) + // assert.strictEqual(squashed.message, "failure") + // } + // assert.strictEqual(resource.error, undefined) + // dispose() + // }) + // }) +}) + +const renderAtomRef = function(ref: AtomRef.ReadonlyRef, onValue: (_: A) => void) { + return createRoot((dispose) => { + const accessor = useAtomRef(() => ref) + createEffect(() => { + onValue(accessor()) + }) + return dispose + }) +} + +const renderAccessor = function(makeAccessor: () => Accessor, onValue: (_: A) => void) { + return createRoot((dispose) => { + const accessor = makeAccessor() + createEffect(() => { + onValue(accessor()) + }) + return dispose + }) +} + +const renderAtomValue = function( + atom: Atom.Atom, + onValue: (_: B) => void, + options?: { readonly registry?: AtomRegistry.AtomRegistry; readonly map?: (_: A) => B } +) { + return createRoot((dispose) => { + const run = () => { + const accessor = options?.map ? useAtomValue(() => atom, options.map) : useAtomValue(() => atom) + createEffect(() => { + onValue(accessor() as B) + }) + return null + } + + if (options?.registry) { + createComponent(RegistryContext.Provider, { + value: options.registry, + get children() { + return run() + } + }) + } else { + run() + } + + return dispose + }) +} + +const renderAtomResource = function( + atom: () => Atom.Atom>, + options?: { + readonly suspendOnWaiting?: boolean | undefined + } +) { + let resource: + | Resource + | undefined + const dispose = createRoot((dispose) => { + ;[resource] = useAtomResource(atom, options) + createEffect(() => { + resource?.() + }) + return dispose + }) + return { resource: resource!, dispose } +} diff --git a/repos/effect/packages/atom/solid/tsconfig.json b/repos/effect/packages/atom/solid/tsconfig.json new file mode 100644 index 0000000..19a2f5d --- /dev/null +++ b/repos/effect/packages/atom/solid/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/repos/effect/packages/atom/solid/vitest.config.ts b/repos/effect/packages/atom/solid/vitest.config.ts new file mode 100644 index 0000000..48bb912 --- /dev/null +++ b/repos/effect/packages/atom/solid/vitest.config.ts @@ -0,0 +1,15 @@ +import { mergeConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +export default mergeConfig(shared, { + resolve: { + conditions: ["browser"] + }, + esbuild: { + target: "es2022" + }, + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"] + } +}) diff --git a/repos/effect/packages/atom/solid/vitest.setup.ts b/repos/effect/packages/atom/solid/vitest.setup.ts new file mode 100644 index 0000000..ce48643 --- /dev/null +++ b/repos/effect/packages/atom/solid/vitest.setup.ts @@ -0,0 +1,8 @@ +// oxlint-disable-next-line no-unassigned-import +import "@testing-library/jest-dom/vitest" +import { cleanup } from "@solidjs/testing-library" +import { afterEach } from "vitest" + +afterEach(() => { + cleanup() +}) diff --git a/repos/effect/packages/atom/vue/CHANGELOG.md b/repos/effect/packages/atom/vue/CHANGELOG.md new file mode 100644 index 0000000..4899d75 --- /dev/null +++ b/repos/effect/packages/atom/vue/CHANGELOG.md @@ -0,0 +1,705 @@ +# @effect/atom-vue + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/repos/effect/packages/atom/vue/LICENSE b/repos/effect/packages/atom/vue/LICENSE new file mode 100644 index 0000000..7f6fe48 --- /dev/null +++ b/repos/effect/packages/atom/vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/atom/vue/README.md b/repos/effect/packages/atom/vue/README.md new file mode 100644 index 0000000..5ef604a --- /dev/null +++ b/repos/effect/packages/atom/vue/README.md @@ -0,0 +1,7 @@ +# `@effect/atom-vue` + +Vue bindings for the Effect Atom modules. + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-vue). diff --git a/repos/effect/packages/atom/vue/docgen.json b/repos/effect/packages/atom/vue/docgen.json new file mode 100644 index 0000000..26f71de --- /dev/null +++ b/repos/effect/packages/atom/vue/docgen.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/vue/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/repos/effect/packages/atom/vue/package.json b/repos/effect/packages/atom/vue/package.json new file mode 100644 index 0000000..a9e8d81 --- /dev/null +++ b/repos/effect/packages/atom/vue/package.json @@ -0,0 +1,67 @@ +{ + "name": "@effect/atom-vue", + "version": "4.0.0-beta.99", + "type": "module", + "license": "MIT", + "description": "Vue bindings for the Effect Atom modules", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/atom/vue" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "vue", + "database" + ], + "keywords": [ + "typescript", + "vue", + "database" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^", + "vue": "^3.5.39" + }, + "peerDependencies": { + "effect": "workspace:^", + "vue": "^3.5.0" + } +} diff --git a/repos/effect/packages/atom/vue/src/index.ts b/repos/effect/packages/atom/vue/src/index.ts new file mode 100644 index 0000000..799c9eb --- /dev/null +++ b/repos/effect/packages/atom/vue/src/index.ts @@ -0,0 +1,211 @@ +/** + * @since 4.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult" +import type * as Atom from "effect/unstable/reactivity/Atom" +import type * as AtomRef from "effect/unstable/reactivity/AtomRef" +import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" +import { computed, type ComputedRef, inject, type InjectionKey, type Ref, shallowRef, watchEffect } from "vue" + +/** + * @since 4.0.0 + * @category modules + */ +export * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" + +/** + * @since 4.0.0 + * @category modules + */ +export * as AsyncResult from "effect/unstable/reactivity/AsyncResult" + +/** + * @since 4.0.0 + * @category modules + */ +export * as Atom from "effect/unstable/reactivity/Atom" + +/** + * @since 4.0.0 + * @category modules + */ +export * as AtomRef from "effect/unstable/reactivity/AtomRef" + +/** + * @since 4.0.0 + * @category re-exports + */ +export * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi" + +/** + * @since 4.0.0 + * @category modules + */ +export * as AtomRpc from "effect/unstable/reactivity/AtomRpc" + +/** + * @since 4.0.0 + * @category registry + */ +export const registryKey = Symbol.for("@effect/atom-vue/registryKey") as InjectionKey + +/** + * @since 4.0.0 + * @category registry + */ +export const defaultRegistry: AtomRegistry.AtomRegistry = AtomRegistry.make() + +/** + * @since 4.0.0 + * @category registry + */ +export const injectRegistry = (): AtomRegistry.AtomRegistry => { + return inject(registryKey, defaultRegistry) +} + +const useAtomValueRef = >(atom: () => A) => { + const registry = injectRegistry() + const atomRef = computed(atom) + const value = shallowRef(undefined as any as A) + watchEffect((onCleanup) => { + onCleanup(registry.subscribe(atomRef.value, (nextValue: Atom.Type) => { + value.value = nextValue + }, { immediate: true })) + }) + return [value as Readonly>>, atomRef, registry] as const +} + +/** + * @since 4.0.0 + * @category composables + */ +export const useAtom = ( + atom: () => Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): readonly [ + Readonly>, + write: "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +] => { + const [value, atomRef, registry] = useAtomValueRef(atom) + return [value as Readonly>, setAtom(registry, atomRef, options)] +} + +/** + * @since 4.0.0 + * @category composables + */ +export const useAtomValue = (atom: () => Atom.Atom): Readonly> => useAtomValueRef(atom)[0] + +const flattenExit = (exit: Exit.Exit): A => { + if (Exit.isSuccess(exit)) return exit.value + throw Cause.squash(exit.cause) +} + +function setAtom( + registry: AtomRegistry.AtomRegistry, + atomRef: ComputedRef>, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + ( + value: W, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ) => Promise> + ) : + "promiseExit" extends Mode ? ( + ( + value: W, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +{ + if (options?.mode === "promise" || options?.mode === "promiseExit") { + return ((value: W, opts?: any) => { + registry.set(atomRef.value, value) + const promise = Effect.runPromiseExit( + AtomRegistry.getResult( + registry, + atomRef.value as Atom.Atom>, + { suspendOnWaiting: true } + ), + opts + ) + return options!.mode === "promise" ? promise.then(flattenExit) : promise + }) as any + } + return ((value: W | ((value: R) => W)) => { + registry.set(atomRef.value, typeof value === "function" ? (value as any)(registry.get(atomRef.value)) : value) + }) as any +} + +/** + * @since 4.0.0 + * @category composables + */ +export const useAtomSet = < + R, + W, + Mode extends "value" | "promise" | "promiseExit" = never +>( + atom: () => Atom.Writable, + options?: { + readonly mode?: ([R] extends [AsyncResult.AsyncResult] ? Mode : "value") | undefined + } +): "promise" extends Mode ? ( + ( + value: W, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ) => Promise> + ) : + "promiseExit" extends Mode ? ( + ( + value: W, + options?: { + readonly signal?: AbortSignal | undefined + } | undefined + ) => Promise, AsyncResult.AsyncResult.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) => +{ + const registry = injectRegistry() + const atomRef = computed(atom) + watchEffect((onCleanup) => { + onCleanup(registry.mount(atomRef.value)) + }) + return setAtom(registry, atomRef, options) +} + +/** + * @since 4.0.0 + * @category composables + */ +export const useAtomRef = (atomRef: () => AtomRef.ReadonlyRef): Readonly> => { + const atomRefRef = computed(atomRef) + const value = shallowRef(atomRefRef.value.value) + watchEffect((onCleanup) => { + const ref = atomRefRef.value + onCleanup(ref.subscribe((next: A) => { + value.value = next + })) + }) + return value as Readonly> +} diff --git a/repos/effect/packages/atom/vue/test/index.test.ts b/repos/effect/packages/atom/vue/test/index.test.ts new file mode 100644 index 0000000..79cf1df --- /dev/null +++ b/repos/effect/packages/atom/vue/test/index.test.ts @@ -0,0 +1,5 @@ +import { describe, test } from "vitest" + +describe("atom-vue", () => { + test("", () => {}) +}) diff --git a/repos/effect/packages/atom/vue/tsconfig.json b/repos/effect/packages/atom/vue/tsconfig.json new file mode 100644 index 0000000..d3687aa --- /dev/null +++ b/repos/effect/packages/atom/vue/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ], + "compilerOptions": { + "paths": { + "effect": ["../../effect/src/index.ts"], + "effect/*": ["../../effect/src/*.ts"] + } + } +} diff --git a/repos/effect/packages/atom/vue/vitest.config.ts b/repos/effect/packages/atom/vue/vitest.config.ts new file mode 100644 index 0000000..d5870de --- /dev/null +++ b/repos/effect/packages/atom/vue/vitest.config.ts @@ -0,0 +1,8 @@ +import { mergeConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +export default mergeConfig(shared, { + test: { + environment: "happy-dom" + } +}) diff --git a/repos/effect/packages/effect/CHANGELOG.md b/repos/effect/packages/effect/CHANGELOG.md new file mode 100644 index 0000000..dd62784 --- /dev/null +++ b/repos/effect/packages/effect/CHANGELOG.md @@ -0,0 +1,2254 @@ +# effect + +## 4.0.0-beta.99 + +### Patch Changes + +- [#6397](https://github.com/Effect-TS/effect/pull/6397) [`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4) Thanks @IMax153! - Add a scoped `CliConfig` service for customizing the built-in global flags used by CLI command runners. + + For example, provide an explicit list that omits `GlobalFlag.LogLevel` to remove the built-in `--log-level` flag: + + ```ts + import { Effect } from "effect"; + import { CliConfig, Command, GlobalFlag } from "effect/unstable/cli"; + + const program = Command.run(command, { version: "1.0.0" }).pipe( + Effect.provide( + CliConfig.layer({ + builtIns: [GlobalFlag.Help, GlobalFlag.Version, GlobalFlag.Completions], + }), + ), + ); + ``` + +- [#6409](https://github.com/Effect-TS/effect/pull/6409) [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada) Thanks @IMax153! - Reintroduce interactive CLI wizard mode through the `--wizard` flag and `Command.wizard`. + +- [#6394](https://github.com/Effect-TS/effect/pull/6394) [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199) Thanks @lloydrichards! - add a `radius` option to `Graph` search configuration, allowing `dfs`, `bfs`, and `dfsPostOrder` traversals to limit returned nodes by edge distance from the configured start nodes. Traversals can also use `direction: "undirected"` to follow edges in either direction. + +- [#6457](https://github.com/Effect-TS/effect/pull/6457) [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f) Thanks @fubhy! - Improve `Graph.dijkstra` and `Graph.astar` priority queue performance. + +- [#6468](https://github.com/Effect-TS/effect/pull/6468) [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c) Thanks @gcanti! - Distribute `HttpApiBuilder` handler requirements per service so request middleware layers can provide them, closes [#6464](https://github.com/Effect-TS/effect/issues/6464). + +- [#6389](https://github.com/Effect-TS/effect/pull/6389) [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a) Thanks @IMax153! - Report an error when a CLI flag, including `--completions`, is provided without its required value. + +- [#6359](https://github.com/Effect-TS/effect/pull/6359) [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e) Thanks @evermake! - - Fix `Command.withSubcommands` collapsing the inferred requirements type to `never` when given more than one subcommand + - Export a `Command.Services` utility type to extract the required services from a `Command` + +- [#6462](https://github.com/Effect-TS/effect/pull/6462) [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162) Thanks @fubhy! - Fix immutable Graph equality and hashing to include future node and edge identifier allocation. + +- [#6425](https://github.com/Effect-TS/effect/pull/6425) [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210) Thanks @fubhy! - Fix `Graph.bellmanFord` to detect reachable negative cycles when the source and target are the same node. + +- [#6454](https://github.com/Effect-TS/effect/pull/6454) [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde) Thanks @fubhy! - Fix standalone data-last `Graph.getNode` and `Graph.getEdge` inference. + +- [#6418](https://github.com/Effect-TS/effect/pull/6418) [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af) Thanks @fubhy! - Fix `Graph.mapEdges` and `Graph.filterMapEdges` to preserve `Graph.Edge` instances when transforming edge data. + +- [#6426](https://github.com/Effect-TS/effect/pull/6426) [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4) Thanks @fubhy! - Reject `NaN` and `-Infinity` edge weights in Graph shortest-path algorithms. + +- [#6461](https://github.com/Effect-TS/effect/pull/6461) [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf) Thanks @fubhy! - Fix mutable Graph equality and hashing to use reference identity while preserving structural semantics for immutable graphs. + +- [#6456](https://github.com/Effect-TS/effect/pull/6456) [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1) Thanks @fubhy! - Fix topological walkers silently completing with an incomplete order when a mutable graph becomes cyclic after walker creation. + +- [#6460](https://github.com/Effect-TS/effect/pull/6460) [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38) Thanks @fubhy! - Restrict `Graph.topo` to directed graphs at the type level while retaining runtime validation for unsafe undirected inputs. + +- [#6455](https://github.com/Effect-TS/effect/pull/6455) [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c) Thanks @fubhy! - Fix `Graph.Walker` to create a fresh iterable for each direct iteration. + +- [#6414](https://github.com/Effect-TS/effect/pull/6414) [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802) Thanks @fubhy! - Fix `Graph.toGraphViz` to quote DOT graph names and escape labels as literal text. + +- [#6438](https://github.com/Effect-TS/effect/pull/6438) [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f) Thanks @tim-smart! - Fix one-shot iterable handling in Array.rotate, Iterable.cartesian, and in-memory RunnerStorage acquisition + +- [#6371](https://github.com/Effect-TS/effect/pull/6371) [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd) Thanks @polRk! - Tool: preserve the tool kind when cloning provider-defined and dynamic tools. + + `Tool.addDependency`, `setParameters`, `setSuccess`, `setFailure`, `annotate`, and `annotateMerge` previously rebuilt the tool as a user-defined tool, which flipped `Tool.isProviderDefined` to `false`, corrupted the provider `id` (e.g. `anthropic.memory_20250818`), and crashed `Tool.getStrictMode`. These operations now clone the tool while preserving its prototype, `id`, and kind. Provider-defined tools also now carry an empty annotations context so `Tool.getStrictMode`/`annotate` work on them. Closes [#2615](https://github.com/Effect-TS/effect/issues/2615). + +- [#6421](https://github.com/Effect-TS/effect/pull/6421) [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456) Thanks @fubhy! - Preserve null edge data in Graph.floydWarshall costs. + +- [#6438](https://github.com/Effect-TS/effect/pull/6438) [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f) Thanks @tim-smart! - ensure one-shot iterables work with Fiber apis + +- [#6420](https://github.com/Effect-TS/effect/pull/6420) [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd) Thanks @fubhy! - Fix `Graph.isAcyclic` to detect cycles formed by parallel undirected edges. + +- [#6417](https://github.com/Effect-TS/effect/pull/6417) [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209) Thanks @fubhy! - Reject `Graph` mutation operations on mutable handles after `Graph.endMutation` finalizes them. + +- [#6423](https://github.com/Effect-TS/effect/pull/6423) [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206) Thanks @fubhy! - Fix Graph.isGraph narrowing for mutable and undirected graphs. + +- [#6459](https://github.com/Effect-TS/effect/pull/6459) [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015) Thanks @fubhy! - Reject asynchronous `Graph` mutation callbacks and finalize scoped mutable handles when callbacks fail. + +- [#6458](https://github.com/Effect-TS/effect/pull/6458) [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e) Thanks @fubhy! - Fix undirected `Graph` equality and hashing to ignore stored edge endpoint orientation. + +- [#6415](https://github.com/Effect-TS/effect/pull/6415) [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a) Thanks @fubhy! - Fix `Graph.Walker` iteration for receiver-sensitive iterables. + +- [#6394](https://github.com/Effect-TS/effect/pull/6394) [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199) Thanks @lloydrichards! - added graph set operations for combining and comparing graphs + - `Graph.make` - creates a graph constructor for a dynamically selected graph kind + - `Graph.compose` - composition of two graphs, merging nodes by identity + - `Graph.intersection` - intersection of two graphs, keeping only common nodes and edges + - `Graph.difference` - difference of two graphs, removing edges present in the second graph + - `Graph.symmetricDifference` - symmetric difference of two graphs, keeping edges present in exactly one graph + +- [#6395](https://github.com/Effect-TS/effect/pull/6395) [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400) Thanks @Chaoran-Huang! - Fix multipart parser limit violations being silently swallowed + +- [#6419](https://github.com/Effect-TS/effect/pull/6419) [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d) Thanks @fubhy! - Make the public Graph interfaces opaque by hiding internal mutable storage fields from their TypeScript surface. + +- [#6390](https://github.com/Effect-TS/effect/pull/6390) [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91) Thanks @alvarosevilla95! - Fix Redis script evaluation so transient `SCRIPT LOAD` failures are retried instead of being cached indefinitely. + +- [#6394](https://github.com/Effect-TS/effect/pull/6394) [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199) Thanks @lloydrichards! - add advanced graph set operations for deriving related graph structures + - `Graph.complement` - complement over the existing node set, adding missing edges between distinct nodes + - `Graph.neighborhood` - induced subgraph containing nodes within a radius of a node + - `Graph.sum` - disjoint union of two graphs without merging equal node data + +- [#6430](https://github.com/Effect-TS/effect/pull/6430) [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a) Thanks @fubhy! - Fix Graph BFS, topological sort, and DFS postorder iterators to skip nodes removed from a MutableGraph without recursive self-calls. + +- [#6465](https://github.com/Effect-TS/effect/pull/6465) [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604) Thanks @gcanti! - Fix `isInt32` to apply custom annotations only to its filter group. + +## 4.0.0-beta.98 + +### Patch Changes + +- [#2587](https://github.com/Effect-TS/effect-smol/pull/2587) [`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842) Thanks @gcanti! - Expose `SchemaError` as a public module and re-export `Schema.isSchemaError`. + + This gives consumers a stable import path and guard for schema failures without + depending on the internal schema implementation, while preserving the existing + `Schema.SchemaError` surface. + +- [#2592](https://github.com/Effect-TS/effect-smol/pull/2592) [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e) Thanks @gcanti! - Apply `transformClient` when building an individual HttpApi endpoint client, preserving the supplied client's error and service channels. + +- [#2598](https://github.com/Effect-TS/effect-smol/pull/2598) [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe) Thanks @gcanti! - Preserve `__proto__` group and endpoint identifiers in HTTP APIs, generated clients, and URL builders. + +- [#2578](https://github.com/Effect-TS/effect-smol/pull/2578) [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f) Thanks @tim-smart! - Fix `Atom.kvs` async mode to retain its `AsyncResult` value shape after writes. + +- [#2612](https://github.com/Effect-TS/effect-smol/pull/2612) [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709) Thanks @gptguy! - Fix replay of persisted `DurableDeferred.raceAll` results. + +- [#2580](https://github.com/Effect-TS/effect-smol/pull/2580) [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c) Thanks @gcanti! - Fix HttpApi authorization decoding. + + Previously, `HttpApiBuilder.securityDecode` removed the expected scheme length and one following character from the `Authorization` header without verifying either value. A Bearer decoder could therefore pass credentials from a different scheme such as `Basic`, accept a malformed header without a separating space, or retain leading spaces when more than one separator was present. + + The decoder now validates the declared scheme before returning credentials, matches it case-insensitively as required by [RFC 9110 section 11.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-11.1), and consumes one or more separating spaces. Missing, malformed, or mismatched headers produce the existing empty credential value so security middleware can reject them consistently. + + Basic authentication previously split the decoded `user-pass` value at every colon, causing otherwise valid passwords containing `:` to be discarded. It now uses only the first colon as the separator and preserves the rest of the password, following [RFC 7617 section 2](https://www.rfc-editor.org/rfc/rfc7617.html#section-2). + +- [#2581](https://github.com/Effect-TS/effect-smol/pull/2581) [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671) Thanks @gcanti! - Fix HttpApi client error decoding. + + Generated clients previously combined every error schema for a status into one union decoder. When schemas used different encodings, their declaration order could determine the decoded error instead of the response `Content-Type`; for example, a text decoder could accept a JSON response before the JSON decoder was tried. + + Error responses are now grouped and selected by normalized content type, matching buffered success responses. Normalization happens before grouping, so declarations that differ only by casing or parameters such as `charset` share one union decoder instead of making later schemas unreachable. + + No-content schemas are represented by a headerless alternative, allowing empty error responses without a `Content-Type` header to decode correctly. Unsupported content types preserve the existing combination of `StatusCodeError` and the response decoding failure. + +- [#2605](https://github.com/Effect-TS/effect-smol/pull/2605) [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc) Thanks @gcanti! - Fix `Number.remainder` for very small and large values formatted in scientific notation. + +- [#2611](https://github.com/Effect-TS/effect-smol/pull/2611) [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f) Thanks @tim-smart! - Fix `PersistedQueue` to count schema decoding and malformed SQL payload failures as processing attempts. + +- [#2500](https://github.com/Effect-TS/effect-smol/pull/2500) [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b) Thanks @hsubra89! - Fix Redis-backed `PersistedQueue` reset and failed-item handling. + +- [#2602](https://github.com/Effect-TS/effect-smol/pull/2602) [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7) Thanks @gcanti! - Fix a bug where decoding bracket paths from FormData or URLSearchParams could mutate inherited object prototypes. + +- [#2588](https://github.com/Effect-TS/effect-smol/pull/2588) [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7) Thanks @gcanti! - Fix `SchemaAST.isJson` to reject class instances and other non-record objects. + +- [#2605](https://github.com/Effect-TS/effect-smol/pull/2605) [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc) Thanks @gcanti! - Fix JSON Schema `allOf` imports for tuple intersections and preserve primitive refinements when combining literal constraints. + +- [#2604](https://github.com/Effect-TS/effect-smol/pull/2604) [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786) Thanks @gcanti! - Fix `Schema.toFormatter` and `Schema.toEquivalence` indexing for tuples with multiple post-rest elements. + +- [#2603](https://github.com/Effect-TS/effect-smol/pull/2603) [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f) Thanks @gcanti! - Fix union candidate selection and decoding order so that unions now: + - consider matches from every sentinel key instead of dropping valid members after the first match; + - reject ambiguous `oneOf` inputs when members with different sentinel keys both match; + - preserve declared member order when combining discriminated members with non-discriminated fallbacks; + - commit concurrent decoding results in declaration order instead of completion order. + + Reserved SSE failure event names with non-`Cause` data are now emitted as application events instead of producing a runtime defect. + +- [#2609](https://github.com/Effect-TS/effect-smol/pull/2609) [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e) Thanks @tim-smart! - Fix SQL-backed persisted queues to refresh locks for actively acquired elements. + +- [#2583](https://github.com/Effect-TS/effect-smol/pull/2583) [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953) Thanks @MrGovindan! - Fixed Clock.sleep handling of large durations + +- [#2582](https://github.com/Effect-TS/effect-smol/pull/2582) [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba) Thanks @gcanti! - Harden HttpApi documentation HTML rendering. + + Scalar descriptions and CDN versions were interpolated without attribute-safe escaping. Embedded OpenAPI JSON in Scalar and Swagger also handled only the exact `` sequence, not other valid [script end-tag forms](https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state). + + Attribute values and CDN versions are now encoded for their contexts, and embedded JSON escapes `<` so it cannot close its script element. + +- [#2591](https://github.com/Effect-TS/effect-smol/pull/2591) [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9) Thanks @gcanti! - Keep HttpApi composition immutable. + + `HttpApi.addHttpApi` applied annotations from the added API by mutating its shared groups. It now creates annotated group copies, keeping the source API and independently annotated variants unchanged while preserving annotation precedence. + +- [#2584](https://github.com/Effect-TS/effect-smol/pull/2584) [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de) Thanks @gcanti! - Normalize HttpApi payload media types. + + Payload schemas were stored under their exact declared `Content-Type`, but the server lowercased the incoming header and removed its parameters before looking it up. For example, a schema declared as `Application/Vnd.Effect+JSON; profile=declared` was stored under that value, while the server looked for `application/vnd.effect+json`. This could produce a `415` response even when the generated client and server used the same API. + + The same mismatch allowed incompatible encodings for equivalent media types to bypass validation. Generated form-urlencoded requests also ignored custom content types and always used the default one. + + Payload maps now use normalized keys for matching and conflict checks, while each encoding keeps its declared content type. Generated requests and OpenAPI use the declared values, including every parameterized variant, and custom form-urlencoded content types are preserved. + +- [#2476](https://github.com/Effect-TS/effect-smol/pull/2476) [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3) Thanks @gcanti! - Improve unstable `HttpApi` type-level performance. + + The implementation now uses identifier-keyed maps and lighter structural + constraints in several hot type-level paths. Generated group clients consume the + concrete endpoint map directly instead of rebuilding it from the endpoint union. + + ## New Features + - Add `HttpApiBuilder.Handlers.handleAll`, which registers an identifier-keyed batch of endpoint handlers for a group. Each entry can be either a handler function or `{ handler, options }`, and the object can be supplied in multiple partial batches. Endpoint identifiers that were already handled are rejected across batches. + - `HttpApi.groups` now preserves the concrete group type for each group identifier. For example, `Api.groups.users` is typed as the `users` group instead of the full group union. + - `HttpApiGroup.endpoints` now preserves the concrete endpoint type for each endpoint identifier. For example, `Group.endpoints.getUser` is typed as the `getUser` endpoint instead of the full endpoint union. + - `HttpApiEndpoint` values can now be extended as classes, matching the class-like + runtime shape already used by `HttpApi` and `HttpApiGroup`. + + ## Measured Type-Level Performance + + Main/current comparisons use identical generated fixtures compiled once per + revision with TypeScript 7.0.2. The recorded revisions are `main` at + `97fdaa9c1f52` and the branch source at `5798fc5fafcd`. The focused pre/post + curves below were captured with the regular `httpapi` regression suite during + development. The retained suite uses representative stress points instead of + rerunning every point in those historical curves. All numbers are + type-instantiation deltas over the corresponding shared baseline. + + Endpoint declaration costs now grow with a lower slope: + + | endpoints | main | current | + | --------: | ------: | ------: | + | 10 | 4,580 | 2,808 | + | 50 | 15,500 | 9,168 | + | 100 | 29,150 | 17,118 | + | 500 | 138,350 | 80,718 | + + Class-like endpoint declarations are slightly cheaper than inline endpoint + values in the same 500-endpoint fixture shape: + + | fixture | inline | class-like | + | ------------- | -----: | ---------: | + | 500 endpoints | 82,207 | 71,850 | + + `HttpApiBuilder` fluent handler registration avoids the previous non-linear + blow-up in the cross-ref comparison: + + | fixture | main | current | + | ---------------- | ---------: | --------: | + | 10 endpoints | 37,856 | 11,582 | + | 50 endpoints | 568,576 | 63,702 | + | 100 endpoints | 2,154,476 | 182,852 | + | 500 endpoints | 51,741,676 | 3,296,052 | + | 500 raw handlers | 51,734,176 | 3,294,550 | + + In the recorded regular-suite measurements, `handleAll` remains the scalable + alternative to the equivalent fluent chain: + + | fixture | fluent | `handleAll` | + | -------------------- | --------: | ----------: | + | 10 endpoints | 11,579 | 9,146 | + | 50 endpoints | 63,699 | 25,106 | + | 100 endpoints | 182,849 | 45,056 | + | 500 endpoints | 3,296,049 | 204,656 | + | 500 eps, two batches | 3,296,049 | 223,613 | + + Generated-client type production also improves for the hot method-building + paths: + + | fixture | main | current | + | --------------------------------------- | ------: | ------: | + | client methods, 500 endpoints | 245,795 | 176,850 | + | top-level client methods, 500 endpoints | 243,651 | 179,809 | + | client endpoint method, 500 endpoints | 56,738 | 46,294 | + | client groups, 100 groups x 5 endpoints | 49,019 | 25,893 | + + The following focused curves were captured immediately before and after each + isolated type-level change. + + The focused `Client.Group` curve shows the improvement from consuming the + identifier-keyed endpoint map directly: + + | endpoints | union remapping | endpoint map | + | --------: | --------------: | -----------: | + | 10 | 12,448 | 12,294 | + | 50 | 19,169 | 18,935 | + | 100 | 27,570 | 27,236 | + | 500 | 94,770 | 93,636 | + + The focused `Client.TopLevelMethods` curve improves by reading endpoint + identifiers directly from the endpoint union: + + | endpoints | pre-change | post-change | + | --------: | ---------: | ----------: | + | 10 | 12,531 | 12,476 | + | 50 | 19,252 | 19,197 | + | 100 | 27,653 | 27,598 | + | 500 | 94,853 | 94,798 | + + The focused `HttpApiClient.endpoint` selection curve improves by reading + endpoint identifiers directly from the selected endpoint union: + + | endpoints | pre-change | post-change | + | --------: | ---------: | ----------: | + | 10 | 7,666 | 7,588 | + | 50 | 8,707 | 8,629 | + | 100 | 10,008 | 9,930 | + | 500 | 20,408 | 20,330 | + + The focused `HttpApiBuilder.endpoint` selection curve improves by reading + endpoint identifiers directly from the selected endpoint union: + + | endpoints | pre-change | post-change | + | --------: | ---------: | ----------: | + | 10 | 12,828 | 12,745 | + | 50 | 13,869 | 13,786 | + | 100 | 15,170 | 15,087 | + | 500 | 25,570 | 25,487 | + + URL builder types now avoid repeatedly expanding the full API/group shape: + + | fixture | main | current | + | ------------------------------------ | ------: | ------: | + | URL builder, 500 endpoints | 211,356 | 91,610 | + | top-level URL builder, 500 endpoints | 210,724 | 93,118 | + | builder endpoint, 500 endpoints | 62,894 | 51,952 | + + ## Breaking Changes + + These changes affect unstable `HttpApi` type-level APIs and structural API, + group, and endpoint types. + + ### Renamed Constraint Types + - Broad structural constraint exports have been renamed to align with + `Schema.Constraint` terminology: `HttpApi.Any` to `HttpApi.Constraint`, + `HttpApi.AnyWithProps` to `HttpApi.Top`, `HttpApiGroup.Any` to + `HttpApiGroup.Constraint`, `HttpApiGroup.AnyWithProps` to `HttpApiGroup.Top`, + and `HttpApiEndpoint.Any` to `HttpApiEndpoint.Constraint`. + - `HttpApiEndpoint.AnyWithProps` has been replaced by `HttpApiEndpoint.Top`, whose + schema parameters are constrained to `Schema.Top`, including success and error + schemas. + - Type guards now expose the widened runtime-prop shapes: `HttpApi.isHttpApi` + returns `HttpApi.Top`, `HttpApiGroup.isHttpApiGroup` returns + `HttpApiGroup.Top`, and `HttpApiEndpoint.isHttpApiEndpoint` returns + `HttpApiEndpoint.Top`. + - `HttpApiGroup.ApiGroup` has been renamed to `HttpApiGroup.Service`. + + ### API, Group, And Endpoint Shapes + - `HttpApi.groups` is now typed as an identifier-keyed group map instead of + `ReadonlyRecord`, and `HttpApi` tracks its group union + invariantly. Dynamic string indexing must refine the key first or cast to a + broad runtime record. + - `HttpApiGroup.endpoints` is now typed as an identifier-keyed endpoint map instead of + `ReadonlyRecord`, and `HttpApiGroup` tracks its endpoint + union invariantly. Dynamic string indexing must refine the key first or cast to + a broad runtime record. + - `HttpApiEndpoint` now exposes its stable key as `identifier` instead of `name`, + aligning endpoints with APIs and groups and leaving `name` available for future + class-based endpoint patterns. + - `HttpApiEndpoint` values are now function objects instead of plain objects. + Runtime checks such as `typeof endpoint` now return `"function"`, and + `endpoint.name` is the native function name. Use `endpoint.identifier` for the + stable endpoint key. + - Identifier helper types have been renamed from `Name` / `WithName` to + `Identifier` / `WithIdentifier`; `HttpApiGroup.Service` now exposes + `identifier` instead of `name`. + + ### Builder Handler Types + - `HttpApiBuilder.Handlers` now tracks endpoints through an identifier-keyed endpoint map and a set of handled endpoint identifiers, instead of tracking the remaining endpoint union. Its public type parameters changed from `Handlers` to `Handlers`, and its phantom fields changed from `_Endpoints` to `~EndpointsByIdentifier` / `~HandledIdentifiers`. + - The unused `HttpApiBuilder.Handlers.Any` helper type has been removed. + - The exported `HttpApiBuilder.HandlersTypeId` symbol has been removed; `Handlers` + now uses a private string type id. + - Duplicate `handle` / `handleRaw` registrations for the same endpoint are rejected + at the call site, and `handleAll` rejects endpoint identifiers that were already + handled by an earlier batch. Missing endpoint handlers are still rejected by + the final `HttpApiBuilder.group` return validation. + + ### Client Types + - `HttpApiClient.Client.Group` now derives a client from a concrete group type: `Client.Group`. The previous group-union plus group-identifier form is no longer supported. + - `HttpApiClient.Client.TopLevelMethods` now returns an identifier-keyed method record instead of a union of `[identifier, method]` tuples. + - `HttpApiClient.makeWith` removes the default `HttpClientError.HttpClientError` from custom client error types in the returned `Client`, while preserving any additional custom client errors. + + ### Endpoint Helper Types + - `HttpApiEndpoint.HttpApiEndpoint` now stores lightweight phantom metadata for middleware and request shapes: `~Middleware`, `~MiddlewareServices`, `~Request`, and `~RequestRaw`. Its type identifier field is now `readonly [TypeId]: typeof TypeId`. + - `HttpApiEndpoint.Constraint` is now a lightweight structural endpoint constraint and does not extend `Pipeable`; values typed only as `HttpApiEndpoint.Constraint` do not expose `.pipe`. + - `HttpApiEndpoint.AddError` has been removed; it was not used internally by the `HttpApi` implementation. + - `HttpApiEndpoint.Json` and `HttpApiEndpoint.StringTree` have been removed in + favor of the canonical `Schema.toCodecJson` and `Schema.toCodecStringTree` + types. + - Omitted request-part metadata now remains `never` instead of being wrapped as + `Schema.toCodecStringTree`; codec metadata is applied only when + a params, query, payload, or headers schema is present. + - Success metadata now applies `Schema.toCodecJson` only to buffered + success schemas and preserves stream success schemas unchanged, including + mixed buffered and streaming success arrays. + - Handler request parts are now flattened with `Struct.Simplify`, improving + displayed request types while reducing handler instantiations. + - Endpoint helper types now read metadata fields directly instead of re-inferring all type parameters from the full `HttpApiEndpoint` interface. This affects helpers such as `Identifier`, `Success`, `Error`, `Params`, `Query`, `Payload`, `Headers`, `Middleware`, `MiddlewareServices`, `Errors`, `ErrorServicesEncode`, `ErrorServicesDecode`, `Request`, `RequestRaw`, `ServerServices`, and `ClientServices`. + - `HttpApiClient.Client.Method` and related generated-client helpers now require endpoint types that satisfy `HttpApiEndpoint.ConstraintRequest`. Endpoint-like structural types must include the lightweight request metadata fields to be accepted. + +- [#2585](https://github.com/Effect-TS/effect-smol/pull/2585) [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf) Thanks @gcanti! - Reuse HttpApi response schemas. + + `HttpApiBuilder` looked up cached response schemas by their source AST but stored them by the transformed AST, so the cache normally missed. It now uses the source AST consistently. + +- [#2590](https://github.com/Effect-TS/effect-smol/pull/2590) [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e) Thanks @IMax153! - Cleanup internals of CLI package + +- [#2607](https://github.com/Effect-TS/effect-smol/pull/2607) [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76) Thanks @tim-smart! - ensure WithTransaction wraps entire rpc handler + +- [#2613](https://github.com/Effect-TS/effect-smol/pull/2613) [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79) Thanks @tim-smart! - Add `HttpApiError.UnprocessableEntity` and `HttpApiError.UnprocessableEntityNoContent` for status 422 responses. + +- [#2594](https://github.com/Effect-TS/effect-smol/pull/2594) [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7) Thanks @gcanti! - Reject unknown and duplicate HttpApi handler registrations with descriptive errors. + +- [#2595](https://github.com/Effect-TS/effect-smol/pull/2595) [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5) Thanks @gcanti! - Reject duplicate OpenAPI operations and operation identifiers, and reject incompatible security schemes that reuse a name. + +## 4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- [#2563](https://github.com/Effect-TS/effect-smol/pull/2563) [`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62) Thanks @tim-smart! - update dependencies + +- [#2566](https://github.com/Effect-TS/effect-smol/pull/2566) [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459) Thanks @tim-smart! - change rpc ids to string | number + +- [#2561](https://github.com/Effect-TS/effect-smol/pull/2561) [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5) Thanks @tim-smart! - Remove `Schedule.elapsed`. + +- [#2561](https://github.com/Effect-TS/effect-smol/pull/2561) [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5) Thanks @tim-smart! - Remove `Schedule.tapInput` and `Schedule.tapOutput`. Use `Schedule.tap` instead. + +- [#2561](https://github.com/Effect-TS/effect-smol/pull/2561) [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5) Thanks @tim-smart! - Update `Schedule.addDelay` and `Schedule.modifyDelay` to receive full schedule metadata instead of separate output and delay arguments. + +- [#2562](https://github.com/Effect-TS/effect-smol/pull/2562) [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805) Thanks @tim-smart! - use Sets to track atom relationships + +## 4.0.0-beta.95 + +### Patch Changes + +- [#2542](https://github.com/Effect-TS/effect-smol/pull/2542) [`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7) Thanks @IGassmann! - Add `Schema.DateFromMillis` and `SchemaTransformation.dateFromMillis` for decoding millisecond timestamps into `Date` values. + +- [#2559](https://github.com/Effect-TS/effect-smol/pull/2559) [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590) Thanks @tim-smart! - fix activity retry policy + +- [#2547](https://github.com/Effect-TS/effect-smol/pull/2547) [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102) Thanks @fubhy! - Allow cron fields like `5/15` to expand from the starting value through the field maximum. + +- [#2557](https://github.com/Effect-TS/effect-smol/pull/2557) [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d) Thanks @fubhy! - Fix `Schedule.cron` when the test clock is adjusted to infinity. + +- [#2560](https://github.com/Effect-TS/effect-smol/pull/2560) [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b) Thanks @gcanti! - Treat empty strings as missing values in built-in `ConfigProvider`s by default. + + `ConfigProvider.fromEnv`, `ConfigProvider.fromDotEnvContents`, `ConfigProvider.fromDotEnv`, `ConfigProvider.fromUnknown`, and `ConfigProvider.fromDir` now treat literal empty strings as absent values when loaded as values, allowing `Config.withDefault` and `Config.option` to recover. Container discovery still reflects the source structure. Pass `preserveEmptyStrings: true` to restore the previous behavior. + + `ConfigProvider.fromDotEnv({ expandVariables: true })` now expands variables consistently with `ConfigProvider.fromDotEnvContents`. + +- [#2554](https://github.com/Effect-TS/effect-smol/pull/2554) [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95) Thanks @tim-smart! - Add Schedule.upTo options for limiting schedules by duration and/or recurrence count. + +- [#2556](https://github.com/Effect-TS/effect-smol/pull/2556) [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7) Thanks @fubhy! - Fix cron parsing and scheduling edge cases for whitespace, Sunday `7`, strict numeric tokens, explicit full day ranges, and month-constrained day-of-month / weekday matching. + +- [#2551](https://github.com/Effect-TS/effect-smol/pull/2551) [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa) Thanks @tim-smart! - Remove the `Schedule.both` APIs and add `Schedule.max` for combining schedules by their slowest delay. + +- [#2553](https://github.com/Effect-TS/effect-smol/pull/2553) [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d) Thanks @tim-smart! - Remove some Schedule APIs: `collectInputs`, `collectOutputs`, `collectWhile`, `delays`, `reduce`, `satisfiesErrorType`, `satisfiesInputType`, `satisfiesOutputType`, `satisfiesServicesType`, and `unfold`. + +- [#2558](https://github.com/Effect-TS/effect-smol/pull/2558) [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2) Thanks @tim-smart! - Remove the Schedule.either APIs and add Schedule.min for fastest-duration schedule composition. + +## 4.0.0-beta.94 + +### Patch Changes + +- [#2538](https://github.com/Effect-TS/effect-smol/pull/2538) [`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5) Thanks @tim-smart! - fork memo map on nested builds + +- [#2545](https://github.com/Effect-TS/effect-smol/pull/2545) [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63) Thanks @marbemac! - Use registration context for cluster entities + +- [#2524](https://github.com/Effect-TS/effect-smol/pull/2524) [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d) Thanks @gcanti! - Fix `HttpApi.make` so it stores the API identifier and starts with an empty `groups` object instead of a `Map`. This makes empty APIs match the shape they have after groups are added. + +- [#2546](https://github.com/Effect-TS/effect-smol/pull/2546) [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528) Thanks @tim-smart! - Fix ClusterWorkflowEngine partial workflow clients colliding with full workflow clients. + +- [#2523](https://github.com/Effect-TS/effect-smol/pull/2523) [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c) Thanks @rajzik! - Add glob to filesystem + +- [#2541](https://github.com/Effect-TS/effect-smol/pull/2541) [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814) Thanks @tim-smart! - add LayerRef module + +- [#2539](https://github.com/Effect-TS/effect-smol/pull/2539) [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d) Thanks @gcanti! - Schema: add `Schema.Decoder` and `Schema.Encoder`, and accept simpler schema types in APIs that only decode, only encode, or only need the basic schema shape, closes [#2536](https://github.com/Effect-TS/effect-smol/issues/2536) + +- [#2545](https://github.com/Effect-TS/effect-smol/pull/2545) [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63) Thanks @marbemac! - add Effect.setContext for fully replacing the fiber context + +## 4.0.0-beta.93 + +### Patch Changes + +- [#2512](https://github.com/Effect-TS/effect-smol/pull/2512) [`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677) Thanks @gcanti! - Preserve content schema identifiers when emitting JSON Schema for `Schema.fromJsonString`. + + This keeps user-defined identifiers attached to the decoded JSON payload while giving the generated JSON string wrapper its own derived name, avoiding client codegen outputs where the payload type is renamed behind the transport wrapper. + +- [#2492](https://github.com/Effect-TS/effect-smol/pull/2492) [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3) Thanks @maxprilutskiy! - Map HttpApi json defects to SchemaError + +- [#2519](https://github.com/Effect-TS/effect-smol/pull/2519) [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc) Thanks @tim-smart! - Fix structural equality for request-style values when structural hashes collide. + +- [#2507](https://github.com/Effect-TS/effect-smol/pull/2507) [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e) Thanks @tim-smart! - ensure handler errors don't cause httpapi security middleware to fallback + +- [#2518](https://github.com/Effect-TS/effect-smol/pull/2518) [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f) Thanks @tim-smart! - Move `UrlParams.makeUrl` to `Url.make` and return `Url.UrlError` for URL construction failures. + +- [#2505](https://github.com/Effect-TS/effect-smol/pull/2505) [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c) Thanks @KhraksMamtsov! - accept UrlParams.Input in some UrlParams apis + +## 4.0.0-beta.92 + +### Patch Changes + +- [#2501](https://github.com/Effect-TS/effect-smol/pull/2501) [`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9) Thanks @gcanti! - Fix excess property handling in schema-backed class constructors, closes [#2499](https://github.com/Effect-TS/effect-smol/issues/2499). + +## 4.0.0-beta.91 + +### Patch Changes + +- [#2498](https://github.com/Effect-TS/effect-smol/pull/2498) [`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075) Thanks @gcanti! - Fix `Schedule.andThenResult` to emit `self` outputs as `Failure` and `other` outputs as `Success`, closes [#2497](https://github.com/Effect-TS/effect-smol/issues/2497). + +- [#2488](https://github.com/Effect-TS/effect-smol/pull/2488) [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150) Thanks @fubhy! - Fix `String.camelCase` and `String.pascalCase` handling of numeric word segments, and add `String.configCase` for configuration key casing. + +- [#2485](https://github.com/Effect-TS/effect-smol/pull/2485) [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6) Thanks @tim-smart! - fix RequestResolver interruption + +## 4.0.0-beta.90 + +### Patch Changes + +- [#2483](https://github.com/Effect-TS/effect-smol/pull/2483) [`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5) Thanks @tim-smart! - Fix `Config.schema` so missing array values are treated as missing data, allowing `Config.withDefault` to apply. + +## 4.0.0-beta.89 + +### Patch Changes + +- [#2475](https://github.com/Effect-TS/effect-smol/pull/2475) [`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad) Thanks @tim-smart! - Update `Schema.Void` to model ignored `void` return values. + + Runtime parsing now accepts any present value and discards it as `undefined`. + This matches TypeScript `void` return values, where callers do not observe the + returned value. Use `Schema.Undefined` when the input must be exactly + `undefined`. + +- [#2479](https://github.com/Effect-TS/effect-smol/pull/2479) [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669) Thanks @tim-smart! - Add custom error callbacks to Effect.fromOption. + +- [#2480](https://github.com/Effect-TS/effect-smol/pull/2480) [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6) Thanks @tim-smart! - render causes in OtlpTracer exception events + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2472](https://github.com/Effect-TS/effect-smol/pull/2472) [`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b) Thanks @tim-smart! - Add adaptive consume and feedback operations to the unstable persistent RateLimiterStore API, including in-memory and Redis-backed bounded cooldown, learning, learned pacing, and expiry behavior for 429 Retry-After feedback. + +- [#2457](https://github.com/Effect-TS/effect-smol/pull/2457) [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4) Thanks @P0lip! - Localize missing rpc method errors to the provided request id + +- [#2428](https://github.com/Effect-TS/effect-smol/pull/2428) [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da) Thanks @MrGovindan! - Add `isOpen` to `Latch` to allow querying the latch's open state + +## 4.0.0-beta.87 + +### Patch Changes + +- [#2468](https://github.com/Effect-TS/effect-smol/pull/2468) [`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956) Thanks @gcanti! - Expose the original input schema on `Schema.toType`, `Schema.toEncoded`, `Schema.toCodecJson`, and `Schema.toCodecStringTree` results via the `schema` property. This aligns these schema wrappers with other wrappers that retain their source schema for type-level and runtime introspection. + +- [#2466](https://github.com/Effect-TS/effect-smol/pull/2466) [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed) Thanks @gcanti! - Use `URL.canParse` to validate URL string schema decoding before constructing a `URL`. This avoids relying on thrown exceptions for routine validation while preserving the same invalid URL issue and successful decode output. + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- [#2455](https://github.com/Effect-TS/effect-smol/pull/2455) [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e) Thanks @fubhy! - Fix `Cron.next` skipping earlier matching days when the upcoming day-of-month does not exist in the current month. + +- [#2454](https://github.com/Effect-TS/effect-smol/pull/2454) [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab) Thanks @StarpTech! - Exclude response metadata from HTTP server span failures after response headers have been sent. + +- [#2449](https://github.com/Effect-TS/effect-smol/pull/2449) [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc) Thanks @gcanti! - Fix Schema handling of encoded-side checks for container ASTs. + + Checks added after `flip` are now preserved as `encodingChecks` across + `Declaration`, `Arrays`, `Objects`, and `Union`, even when rebuilding the AST + does not change child nodes. `toType` now projects those checks consistently, + and parsing applies encoded-side checks to the local encoded value when an + encoding chain is present without allowing encoded-side `parseOptions` + annotations to affect the current parser side. + +- [#2446](https://github.com/Effect-TS/effect-smol/pull/2446) [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9) Thanks @IMax153! - Allow schemas provided to CLI flags / arguments to utilize the environment required by the CLI + +- [#2452](https://github.com/Effect-TS/effect-smol/pull/2452) [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12) Thanks @gcanti! - Remove the `keepDeclarations` option from `Schema.toCodecStringTree`. + +- [#2461](https://github.com/Effect-TS/effect-smol/pull/2461) [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46) Thanks @tim-smart! - Fail RpcClient HTTP requests with a defect when the response stream closes before the request receives a terminal response. + +- [#2442](https://github.com/Effect-TS/effect-smol/pull/2442) [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9) Thanks @gcanti! - Improve Schema type-level performance by lazily computing schema views, + specializing common struct projections, and using lighter schema constraints at + API boundaries that do not need the full schema protocol. + + This also adds the Schema type-performance benchmark suite, introduces + `Schema.toCodecArrayFromSingle`, preserves canonical StringTree array codecs, + renames the arbitrary-generation annotation constraint for clarity, and updates + affected codec, parser, channel, SQL, HTTP API, persistence, RPC, AI, OpenAPI, + and workflow typings to match the refined Schema surface. + +- [#2464](https://github.com/Effect-TS/effect-smol/pull/2464) [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e) Thanks @tim-smart! - do not use performance.timeOrigin and calculate origins lazily + +## 4.0.0-beta.85 + +### Patch Changes + +- [#2436](https://github.com/Effect-TS/effect-smol/pull/2436) [`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c) Thanks @MohanedMashaly! - change default operation in redis from LPUSH TO RPUSH + +- [#2431](https://github.com/Effect-TS/effect-smol/pull/2431) [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134) Thanks @gcanti! - Derive template literal arbitraries from encoded parts, closes [#2414](https://github.com/Effect-TS/effect-smol/issues/2414). + +- [#2439](https://github.com/Effect-TS/effect-smol/pull/2439) [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a) Thanks @gcanti! - Allow schema class `.extend` to accept a `Struct` and preserve checks from the extension schema, closes [#2419](https://github.com/Effect-TS/effect-smol/issues/2419). + +- [#2444](https://github.com/Effect-TS/effect-smol/pull/2444) [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a) Thanks @bweis! - Avoid throwing when `Error.stackTraceLimit` is non-writable (frozen intrinsics / SES / deterministic sandboxes such as Temporal). + + Effect manipulates `Error.stackTraceLimit` in several internal spots to capture short or empty stack traces cheaply. In hardened environments where `Error` is frozen and `stackTraceLimit` is read-only, assigning to it throws, which broke Effect entirely. Stack-trace-limit manipulation is now best-effort and silently no-ops when the property cannot be modified, mirroring Node's own internal guard. Behavior in normal (writable) environments is unchanged. + +- [#2425](https://github.com/Effect-TS/effect-smol/pull/2425) [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5) Thanks @tim-smart! - Add Random.choice for selecting a random element from an iterable. + +- [#2434](https://github.com/Effect-TS/effect-smol/pull/2434) [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1) Thanks @gcanti! - Use semantic matching for TemplateLiteral parsing and index signature keys + + Replace regex-based TemplateLiteral parsing with backtracking segmentation over + template literal parts, applying part checks during matching. + + Use schema membership when selecting Record index signature keys, including + checked string, number, symbol, and TemplateLiteral parameters. Tighten valid + index signature parameters on both type and encoded sides, and preserve key + parameter semantics in codec transformations. + +## 4.0.0-beta.84 + +### Patch Changes + +- [#2420](https://github.com/Effect-TS/effect-smol/pull/2420) [`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e) Thanks @tim-smart! - Add `Effect.transposeOption` for converting an `Option>` into an `Effect, E, R>`. + +- [#2374](https://github.com/Effect-TS/effect-smol/pull/2374) [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda) Thanks @gcanti! - Import unconstrained JSON Schema nodes as `Schema.Json` instead of `Schema.Unknown`. + +- [#2407](https://github.com/Effect-TS/effect-smol/pull/2407) [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f) Thanks @gcanti! - Normalize error behavior for Schema and SchemaParser boundary APIs. + + `SchemaError` now extends `Data.TaggedError`, so it is also a native `Error`. + SchemaParser Promise APIs now reject an `Error` whose cause is the + `SchemaIssue.Issue` for schema failures. + + Schema and SchemaParser `Effect` and `Exit` adapters now preserve full causes + while mapping schema issue failures to their public error type. The `is`, + `asserts`, `Promise`, `Sync`, `Result`, `Option`, `make`, and `makeOption` + adapters now distinguish schema issues from non-schema causes. Schema-only + failures are converted to the adapter's normal representation (`false`, + rejected or thrown schema error, `Result.fail`, or `None`), while non-schema + causes throw or reject with an `Error` whose cause is the underlying `Cause`. + +- [#2409](https://github.com/Effect-TS/effect-smol/pull/2409) [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b) Thanks @tim-smart! - Fix Stream.runForEachWhile so it continues across chunk boundaries while the predicate returns true and stops when the predicate returns false. + +- [#2424](https://github.com/Effect-TS/effect-smol/pull/2424) [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610) Thanks @tim-smart! - Fix cluster workflow activity defect hydration + +- [#2403](https://github.com/Effect-TS/effect-smol/pull/2403) [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246) Thanks @lloydrichards! - align ProcessInput.Input runtime field name with type definition on Prompt.custom + +- [#2423](https://github.com/Effect-TS/effect-smol/pull/2423) [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc) Thanks @tim-smart! - RpcGroup.toHandlers is definition first + +- [#2383](https://github.com/Effect-TS/effect-smol/pull/2383) [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a) Thanks @gcanti! - Fix config path composition and directory-backed lookup behavior. + + `ConfigProvider.orElse` now keeps each side's own `nested` and `mapInput` + behavior. Applying `nested` or `mapInput` to a combined provider now applies the + same transformation to both sides. + + `ConfigProvider` path transformations now compose as a single path function. + This makes `nested` and `mapInput` behave consistently with normal function + composition. + + `Config.nested` now tracks the logical config path in `Config` itself instead of + wrapping the provider. This keeps lookup paths and schema error paths aligned. + The low-level `Config.make` constructor is no longer exported; use config + constructors and combinators, or implement custom lookup behavior with + `ConfigProvider.make`. + + `ConfigProvider.fromDir` now returns `undefined` when neither a file nor a + directory exists at the requested path, so `orElse` can fall back instead of + failing with `SourceError`. + +- [#2415](https://github.com/Effect-TS/effect-smol/pull/2415) [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2) Thanks @gcanti! - Fix `Effect.try` thunk usage and `Effect.tryPromise` mapper and signal handling defects. + + `Effect.try` now supports passing a thunk directly, matching `Effect.tryPromise`. Thrown values from direct-thunk usage are mapped to `Cause.UnknownError`. + + When a promise handled by `Effect.tryPromise` rejected and the custom `catch` mapper threw while mapping that rejection, the effect could remain pending and produce an unhandled rejection. The mapper is now guarded consistently with the synchronous throw path, so a thrown mapper error becomes an Effect defect. The JSDoc for `Effect.try` and `Effect.tryPromise` was also corrected. + + `Effect.tryPromise` now also only creates an `AbortController` when the wrapped thunk declares an `AbortSignal` parameter. + +- [#2417](https://github.com/Effect-TS/effect-smol/pull/2417) [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72) Thanks @tim-smart! - deduplicate SqlResolver.findById requests + +## 4.0.0-beta.83 + +### Patch Changes + +- [#2394](https://github.com/Effect-TS/effect-smol/pull/2394) [`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9) Thanks @IMax153! - Fix published HttpApi declaration files by exporting schema metadata types referenced by public declarations. + +## 4.0.0-beta.82 + +### Patch Changes + +- [#2391](https://github.com/Effect-TS/effect-smol/pull/2391) [`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3) Thanks @IMax153! - Fix HttpApiEndpoint endpoint error inference when success schemas include streams. + +## 4.0.0-beta.81 + +### Patch Changes + +- [#2387](https://github.com/Effect-TS/effect-smol/pull/2387) [`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914) Thanks @gcanti! - `Config.withDefault` now only recovers from missing data for literal/union + schemas. Invalid present values now propagate validation errors instead of + using the default, closes [#2384](https://github.com/Effect-TS/effect-smol/issues/2384). + +- [#2388](https://github.com/Effect-TS/effect-smol/pull/2388) [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39) Thanks @gcanti! - `Config.withDefault` no longer recovers from schema filter failures. A filter + failure means a present value reached refinement checks, so using the default + could hide invalid configuration values. + +- [#2389](https://github.com/Effect-TS/effect-smol/pull/2389) [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484) Thanks @gcanti! - Fix `Schema.toTaggedUnion(...).isAnyOf` narrowing for custom discriminant keys, closes [#2386](https://github.com/Effect-TS/effect-smol/issues/2386). + + Previously, the type predicate always extracted union members by `_tag`, even + when `toTaggedUnion` was created with a different discriminant key. Runtime + behavior already used the supplied key, so this aligns the type-level narrowing + with the existing runtime behavior. + +- [#2270](https://github.com/Effect-TS/effect-smol/pull/2270) [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36) Thanks @IMax153! - Add HTTP API streaming response support + +## 4.0.0-beta.80 + +### Patch Changes + +- [#2205](https://github.com/Effect-TS/effect-smol/pull/2205) [`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6) Thanks @lloydrichards! - add support for merging external events into `Prompt.custom` render loops via an optional `events` dequeue and `receive` handler. + + The prompt races user input against events from the dequeue, allowing background events to trigger re-renders without waiting for a keypress: + + ```ts + const eventQueue = yield * Queue.make(); + + const prompt = Prompt.custom( + { count: 0 }, + Queue.asDequeue(eventQueue), // <-- provide the event queue as a dequeue to the prompt + { + render: (state) => Effect.succeed(`Count: ${state.count}`), + process: (input, state) => + Effect.succeed( + Match.value(input).pipe( + // handle user input + Match.tag("Input", () => Action.Submit({ value: state.count })), + // handle external events from the queue + Match.tag("Event", (input) => + Action.NextFrame({ state: { count: state.count + input.value } }), + ), + Match.exhaustive, + ), + ), + clear: () => Effect.succeed(""), + }, + ); + ``` + +- [#2369](https://github.com/Effect-TS/effect-smol/pull/2369) [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182) Thanks @gcanti! - Round fractional durations symmetrically when normalizing to nanoseconds. + +- [#2373](https://github.com/Effect-TS/effect-smol/pull/2373) [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12) Thanks @StarpTech! - Stream.fromReadableStream: swallow the `reader.cancel()` rejection in the finalizer. Cancelling the reader of an already-errored ReadableStream rejects with the stored error, which turned the typed `onError` failure into a defect. + +- [#2371](https://github.com/Effect-TS/effect-smol/pull/2371) [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d) Thanks @gcanti! - Emit `Schema.ObjectKeyword` as an object-or-array JSON Schema union. + +- [#2376](https://github.com/Effect-TS/effect-smol/pull/2376) [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b) Thanks @fubhy! - Add `Graph.successors` and `Graph.predecessors`, deprecate `Graph.neighborsDirected`, and fix graph algorithm edge cases around reversal, undirected edge queries, shortest-path weight validation, topological sort initials, and strongly connected components. + +## 4.0.0-beta.79 + +### Patch Changes + +- [#2364](https://github.com/Effect-TS/effect-smol/pull/2364) [`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3) Thanks @mikearnaldi! - Fix module-level side effects that defeated bundler tree-shaking. + + Bare top-level statements cannot be `#__PURE__`-annotated by the build, so + bundlers must retain them and everything they reference, even in bundles that + never use the code: + - `Option`: the standalone `Object.defineProperty(SomeProto, "valueOrUndefined", ...)` + statement anchored the whole `Option` proto chain into every bundle. It is + now folded into the `SomeProto` initializer. + - `Headers`: same pattern with `Object.defineProperties(Proto, ...)`, folded + into the initializer. + - `Logger`: module-level `process.stdout.isTTY` property reads (potential + getters, never droppable) moved inside `consolePretty`. + - `Utils`: when `internalCall` was unused, its dropped binding left behind a + retained initializer tail (`standard`/`forced` probe with computed property + reads). The selection is now wrapped in a single pure-annotated call. + + A minimal `Effect.succeed(123).pipe(Effect.runFork)` bundle shrinks by ~1.3% + gzipped; bundles that don't use `Option` or `Headers` no longer pay for them. + +- [#2339](https://github.com/Effect-TS/effect-smol/pull/2339) [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079) Thanks @tim-smart! - Fix EntityManager defect restarts so in-flight requests are replayed instead of being dropped when the old entity scope is interrupted. + +- [#2362](https://github.com/Effect-TS/effect-smol/pull/2362) [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3) Thanks @fubhy! - Fix Graph traversal and shortest-path algorithms to traverse undirected edges independently of their stored source/target orientation. + +- [#2366](https://github.com/Effect-TS/effect-smol/pull/2366) [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44) Thanks @IMax153! - Fix string seed encoding in Random.withSeed so short, trailing, and astral UTF-8 bytes affect deterministic streams. + +- [#2352](https://github.com/Effect-TS/effect-smol/pull/2352) [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6) Thanks @alvarosevilla95! - Fix the Redis `RateLimiterStore` token-bucket failing with opaque errors under memory pressure: it now writes its keys with a TTL and guards against a missing refill timestamp. + +- [#2359](https://github.com/Effect-TS/effect-smol/pull/2359) [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd) Thanks @gcanti! - Fix `Struct` key renaming and `Schema.encodeKeys` to support symbol keys, and reject duplicate encoded keys. + +- [#2365](https://github.com/Effect-TS/effect-smol/pull/2365) [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f) Thanks @gcanti! - Fix `Schema` encoding so container-level checks are validated against the decoded value instead of the encoded output. + + Disallow adding checks directly to `Schema.suspend(...)`; add the checks to the suspended schema instead. + + Fix `StructWithRest` so index signatures do not re-parse or overwrite fixed properties. + +- [#2342](https://github.com/Effect-TS/effect-smol/pull/2342) [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6) Thanks @gcanti! - Use generic ordered constraints for schema arbitrary derivation. + + Range checks such as `isGreaterThan`, `isLessThan`, and `isBetween` now populate `ctx.constraints.ordered` + instead of type-specific range fields on `number`, `date`, or `bigint` constraints. Custom `toArbitrary` + annotations that read range constraints should migrate to `ctx.constraints.ordered`. + + This also fixes BigDecimal arbitrary generation by adapting decimal bounds to the generated scale, avoiding + invalid fast-check bigint ranges for narrow decimal intervals. + +- [#2368](https://github.com/Effect-TS/effect-smol/pull/2368) [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d) Thanks @IMax153! - Encode HTTP API client path parameters when building request URLs. + +- [#2348](https://github.com/Effect-TS/effect-smol/pull/2348) [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75) Thanks @gcanti! - Update Schema arbitrary derivation to use the new filter metadata, candidate generation, optional derivation reports, recursion-aware generation, and the renamed `OrderedConstraint` model. + + Migration from the previous v4 API: + - Replace filter annotations from `toArbitraryConstraint: constraint` to `arbitrary: { constraint }`. When a filter cannot be described as a constraint, use `arbitrary: { candidate }` to add a weighted source that is still checked by the filter. + - Replace bucketed constraints with the flat `Schema.Annotations.ToArbitrary.Constraint` shape: + - `string.minLength`, `array.minLength`, object property counts, collection sizes -> `minLength` + - `string.maxLength`, `array.maxLength`, object property counts, collection sizes -> `maxLength` + - `string.patterns` -> `patterns` + - `number.isInteger` -> `integer` + - `number.noNaN` -> `noNaN` + - `number.noDefaultInfinity` -> `noInfinity` + - `date.noInvalidDate` -> `valid` + - `array.comparator` for uniqueness -> `unique` using Effect equality + - `ordered.min` / `minExcluded` / `max` / `maxExcluded` -> `ordered.minimum` / `exclusiveMinimum` / `maximum` / `exclusiveMaximum` + - In arbitrary hooks, read `context.constraint` instead of `context.constraints`. Replace `context.isSuspend` with `context.recursion`; when combining finite and recursive branches, pass `context.recursion` to `fc.oneof` with the finite branch first. + - Generic declaration hooks now receive type parameters as `{ arbitrary, terminal }`. Atomic declarations may still return a bare `FastCheck.Arbitrary`, but generic declarations should return `{ arbitrary, terminal }` when they can preserve a finite terminal branch. + - `Schema.toArbitrary(schema, { report: true })` now returns `{ value, report }`; without `{ report: true }`, it keeps returning the arbitrary directly. `Schema.toArbitraryLazy` always returns a lazy arbitrary. + +- [#2343](https://github.com/Effect-TS/effect-smol/pull/2343) [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03) Thanks @MohanedMashaly! - Add meta-var that shows log level and bash options in command line. + +## 4.0.0-beta.78 + +### Patch Changes + +- [#2333](https://github.com/Effect-TS/effect-smol/pull/2333) [`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1) Thanks @tim-smart! - Fix Schema.Defect JSON encoding for Error values whose message property is not a string. + +- [#2329](https://github.com/Effect-TS/effect-smol/pull/2329) [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba) Thanks @alvarosevilla95! - Retry Redis scripts after `NOSCRIPT` and declare the token bucket refill key + +- [#2334](https://github.com/Effect-TS/effect-smol/pull/2334) [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd) Thanks @tim-smart! - clean up otlp config + +## 4.0.0-beta.77 + +### Patch Changes + +- [#2326](https://github.com/Effect-TS/effect-smol/pull/2326) [`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23) Thanks @fubhy! - Prefer OTEL resource environment variables over explicit `OtlpResource.fromConfig` options. + +- [#2325](https://github.com/Effect-TS/effect-smol/pull/2325) [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51) Thanks @fubhy! - Add OTEL environment variable configuration for unstable OTLP observability. + +## 4.0.0-beta.76 + +### Patch Changes + +- [#2320](https://github.com/Effect-TS/effect-smol/pull/2320) [`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12) Thanks @gcanti! - Add `Schema.isGUID` and update `Schema.isUUID` to accept the RFC 9562 max UUID. + +- [#2319](https://github.com/Effect-TS/effect-smol/pull/2319) [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84) Thanks @fubhy! - Add support for configuring Scalar API reference pages with a custom fetch implementation. + +- [#2318](https://github.com/Effect-TS/effect-smol/pull/2318) [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386) Thanks @gcanti! - Replace the `Schema.Error` and `Schema.Defect` schema constants with constructor + functions, `Schema.Error()` and `Schema.Defect()`. + + Unify `Schema.ErrorWithStack` into `Schema.Error({ includeStack: true })` and + `Schema.DefectWithStack` into `Schema.Defect({ includeStack: true })`. + + Error causes are encoded by default using the same JSON defect encoding + semantics used by `Schema.Defect`; pass `{ excludeCause: true }` to omit nested + cause data. + + Equivalent `Schema.Error` and `Schema.Defect` options are canonicalized, so + repeated constructor calls with the same option values reuse the same schema. + + `Schema.Defect()` now models defects as `unknown` values with a JSON encoded + form. Error-shaped JSON objects with a string `message` decode to JavaScript + `Error` values, so non-`Error` objects such as `{ message: "boom" }` do not + round-trip unchanged. Other non-`Error` values are normalized through JSON + serialization, with non-JSON values falling back to Effect's formatted string + representation. + +## 4.0.0-beta.75 + +### Patch Changes + +- [#2294](https://github.com/Effect-TS/effect-smol/pull/2294) [`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7) Thanks @mattiamanzati! - Align workflow tags with RPCs by changing `Workflow.make` to accept the tag as its first argument, exposing workflow tags as `_tag`, and supporting `class MyWorkflow extends Workflow.make(...) {}`. + +- [#2312](https://github.com/Effect-TS/effect-smol/pull/2312) [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e) Thanks @gcanti! - Validate `Schema.StructWithRest` fixed fields against rest index signatures at the type level so schemas cannot be constructed with incompatible decoded, encoded, or make shapes. This keeps `StructWithRest` types sound and updates the generated OpenAI conversation-items request schema to keep accepting arbitrary additional fields under the stricter validation. + +- [#2314](https://github.com/Effect-TS/effect-smol/pull/2314) [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c) Thanks @gcanti! - Preserve `Schema.Redacted` options when roundtripping through schema representations. + This keeps `label` validation and `disallowJsonEncode` behavior intact when + schemas are revived from a representation or emitted through code generation. + +- [#2298](https://github.com/Effect-TS/effect-smol/pull/2298) [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3) Thanks @gcanti! - Remove the `Types.MergeRecord` alias. Use `Types.MergeLeft` instead. + +- [#2298](https://github.com/Effect-TS/effect-smol/pull/2298) [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3) Thanks @gcanti! - Align Schema adapter failures: `Schema` result, promise, and sync adapters now surface `SchemaError`, while `SchemaParser` result, promise, and sync adapters expose `SchemaIssue.Issue`. Mark `SchemaParser` option adapters as internal because their error details are discarded. + +- [#2313](https://github.com/Effect-TS/effect-smol/pull/2313) [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00) Thanks @MohanedMashaly! - Add -v alias for version flag + +- [#2306](https://github.com/Effect-TS/effect-smol/pull/2306) [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427) Thanks @sam-goodwin! - Fix `HttpApiSecurity` bearer/http credential decoding + +## 4.0.0-beta.74 + +### Patch Changes + +- [#2295](https://github.com/Effect-TS/effect-smol/pull/2295) [`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e) Thanks @jgoux! - Fix CLI parsing so command-local flags can override globals without breaking global flags before subcommands. + +## 4.0.0-beta.73 + +### Patch Changes + +- [#2291](https://github.com/Effect-TS/effect-smol/pull/2291) [`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75) Thanks @tim-smart! - Add HttpApiSecurity.http for passing custom schemes + +- [#2289](https://github.com/Effect-TS/effect-smol/pull/2289) [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e) Thanks @tim-smart! - make EntityResource lazy by default + +## 4.0.0-beta.72 + +### Patch Changes + +- [#2287](https://github.com/Effect-TS/effect-smol/pull/2287) [`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb) Thanks @tim-smart! - Ensure ClusterWorkflowEngine routes durable clock wakeups and registered workflow deferred completions through the owning workflow's shard group. + +- [#2286](https://github.com/Effect-TS/effect-smol/pull/2286) [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5) Thanks @tim-smart! - Add default value support to `Prompt.file`. + +- [#2285](https://github.com/Effect-TS/effect-smol/pull/2285) [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239) Thanks @tim-smart! - Add default value support to CLI integer prompts. + +## 4.0.0-beta.71 + +### Patch Changes + +- [#2252](https://github.com/Effect-TS/effect-smol/pull/2252) [`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293) Thanks @tim-smart! - Added `Schedule.tap`, which allows observing full schedule metadata without altering schedule inputs or outputs. + +- [#2261](https://github.com/Effect-TS/effect-smol/pull/2261) [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230) Thanks @gcanti! - Add JSON Schema custom annotation passthrough option, closes [#2260](https://github.com/Effect-TS/effect-smol/issues/2260) + +- [#2269](https://github.com/Effect-TS/effect-smol/pull/2269) [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63) Thanks @gcanti! - Schema: reintroduce `.value` on `Schema.Array` and `Schema.NonEmptyArray` for consistency with other collection wrappers (`Chunk`, `HashSet`, etc.), closes [#2268](https://github.com/Effect-TS/effect-smol/issues/2268). + +- [#2272](https://github.com/Effect-TS/effect-smol/pull/2272) [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485) Thanks @gcanti! - Clarify that `Data.$is(tag)` only checks the `_tag` field, not the full structure, closes [#2271](https://github.com/Effect-TS/effect-smol/issues/2271). + +- [#2257](https://github.com/Effect-TS/effect-smol/pull/2257) [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04) Thanks @bwbuchanan! - Fixed the `catch*` combinators silently dropping unhandled error types + +- [#2263](https://github.com/Effect-TS/effect-smol/pull/2263) [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349) Thanks @patroza! - Use `WeakMap` for `pendingBatches` instead of `Map`, to allow GC to collect resolvers + +- [#2266](https://github.com/Effect-TS/effect-smol/pull/2266) [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618) Thanks @gcanti! - Fix schema arbitrary constraints for exclusive BigInt, Date, and integer number bounds. + +- [#2249](https://github.com/Effect-TS/effect-smol/pull/2249) [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d) Thanks @tim-smart! - allow encoding Redacted by default, and add option to disallow encoding + +- [#2276](https://github.com/Effect-TS/effect-smol/pull/2276) [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771) Thanks @tim-smart! - Fix AtomRef notifications when a listener re-subscribes itself during notification. + +- [#2250](https://github.com/Effect-TS/effect-smol/pull/2250) [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba) Thanks @tim-smart! - Fix `Argument.variadic(argument)` so it supports direct calls without options. + +- [#2277](https://github.com/Effect-TS/effect-smol/pull/2277) [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190) Thanks @tim-smart! - Fix string messages and annotations being double-quoted by simple and logfmt loggers. + +## 4.0.0-beta.70 + +### Patch Changes + +- [#2228](https://github.com/Effect-TS/effect-smol/pull/2228) [`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e) Thanks @avallete! - Add `Command.withHidden` to hide subcommands from `--help` output, shell completions, and "did you mean?" suggestions, while keeping them fully invocable by exact name. + + Useful for experimental or internal subcommands that should be accepted but not advertised on the public CLI surface. + + ```ts + import { Command } from "effect/unstable/cli"; + + const experimental = Command.make("experimental").pipe(Command.withHidden); + + const root = Command.make("mycli").pipe( + Command.withSubcommands([experimental]), + ); + ``` + +- [#2244](https://github.com/Effect-TS/effect-smol/pull/2244) [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839) Thanks @tim-smart! - Fix TestClock adjustment when its layer is provided to programs run without an ambient Scope. + +## 4.0.0-beta.69 + +### Patch Changes + +- [#2227](https://github.com/Effect-TS/effect-smol/pull/2227) [`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a) Thanks @avallete! - Add `Flag.withHidden` (and `Param.withHidden`) to hide flags from `--help` output and shell completions while keeping them fully parseable on the command line. + + Useful for experimental, internal, or deprecated flags that should be accepted but not advertised, e.g. `--experimental-foo`, debug toggles, or escape hatches that are not yet committed to the public CLI surface. + + ```ts + import { Flag } from "effect/unstable/cli"; + + const experimental = Flag.boolean("experimental-foo").pipe(Flag.withHidden); + ``` + +- [#2240](https://github.com/Effect-TS/effect-smol/pull/2240) [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178) Thanks @tim-smart! - pass workflow parent on discard + +- [#2237](https://github.com/Effect-TS/effect-smol/pull/2237) [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118) Thanks @notkadez! - Fix `Stream.scoped` and `Channel.scoped` so pull effects run with the scoped resource scope. + +- [#2239](https://github.com/Effect-TS/effect-smol/pull/2239) [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b) Thanks @tim-smart! - fix RpcWorker Protocol service key + +- [#2242](https://github.com/Effect-TS/effect-smol/pull/2242) [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470) Thanks @tim-smart! - Accept `.mjs` and `.mts` migration files in SQL migrator loaders. + +## 4.0.0-beta.68 + +### Patch Changes + +- [#2210](https://github.com/Effect-TS/effect-smol/pull/2210) [`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217) Thanks @tim-smart! - Add Stream.broadcastN for fixed-size stream broadcasts. + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - update Model uuid helpers + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- [#2221](https://github.com/Effect-TS/effect-smol/pull/2221) [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa) Thanks @gcanti! - Change `Schema.asserts` and `SchemaParser.asserts` to assert a value directly with `asserts(schema, input)` and remove `Schema.Codec.ToAsserts`. + +- [#2209](https://github.com/Effect-TS/effect-smol/pull/2209) [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557) Thanks @tim-smart! - Fix Channel.decodeText corrupting UTF-8 characters split across chunk boundaries. + +- [#2207](https://github.com/Effect-TS/effect-smol/pull/2207) [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6) Thanks @tim-smart! - rename Model.Generated to Model.GeneratedByDb + +## 4.0.0-beta.67 + +### Patch Changes + +- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal + +- [#2111](https://github.com/Effect-TS/effect-smol/pull/2111) [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8) Thanks @thiagofelix! - Fix `EntityProxyServer.layerHttpApi` using `path.entityId` instead of `params.entityId` + +- [#2201](https://github.com/Effect-TS/effect-smol/pull/2201) [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836) Thanks @sjh9714! - Fix `MutableList.filter` and `MutableList.remove` length updates. + +- [#2181](https://github.com/Effect-TS/effect-smol/pull/2181) [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527) Thanks @zeyuri! - Fix workflow proxy RPC handlers to provide the context expected by RpcServer. + +- [#2177](https://github.com/Effect-TS/effect-smol/pull/2177) [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87) Thanks @mikearnaldi! - Add forked memo maps so nested layer scopes can reuse parent allocations without leaking sibling-local layers. Update `@effect/vitest` to fork memo maps for nested `it.layer` suites, isolating sibling setup while preserving parent sharing. + +- [#2206](https://github.com/Effect-TS/effect-smol/pull/2206) [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85) Thanks @tim-smart! - add `availableShardGroups` to ShardingConfig, to ensure advisory locks do not conflict + +- [#2184](https://github.com/Effect-TS/effect-smol/pull/2184) [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b) Thanks @gcanti! - Restore support for passing schema parse options when creating decode and encode helpers, closes [#2174](https://github.com/Effect-TS/effect-smol/issues/2174). + +- [#2176](https://github.com/Effect-TS/effect-smol/pull/2176) [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075) Thanks @patroza! - Allow Schema decoding defaults to require Effect services. + + The `Effect` passed to `Schema.withDecodingDefault`, `Schema.withDecodingDefaultKey`, `Schema.withDecodingDefaultType`, and `Schema.withDecodingDefaultTypeKey` now accepts a context `R` in its third type parameter. The required services are propagated into the resulting schema's `DecodingServices`. `SchemaGetter.withDefault` is widened in the same way. + +- [#2113](https://github.com/Effect-TS/effect-smol/pull/2113) [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f) Thanks @patroza! - Allow Schema constructor and decoding defaults to fail with `SchemaError`. + + The `Effect` passed to `Schema.withConstructorDefault`, `Schema.withDecodingDefault`, `Schema.withDecodingDefaultKey`, `Schema.withDecodingDefaultType`, and `Schema.withDecodingDefaultTypeKey` now accepts `SchemaError` in its error channel. When a default fails, the parser unwraps the underlying `SchemaIssue.Issue` and propagates it as a parse failure with the surrounding path attached. This makes it easy to use another schema's `makeEffect` / `decode*` as the default value. + +- [#2172](https://github.com/Effect-TS/effect-smol/pull/2172) [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e) Thanks @gcanti! - Rename `SchemaParser.makeUnsafe` to `SchemaParser.make`. + +- [#2167](https://github.com/Effect-TS/effect-smol/pull/2167) [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd) Thanks @tim-smart! - update dependencies + +## 4.0.0-beta.66 + +### Patch Changes + +- [#2163](https://github.com/Effect-TS/effect-smol/pull/2163) [`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2) Thanks @tim-smart! - remove Effect.Yieldable + +- [#2161](https://github.com/Effect-TS/effect-smol/pull/2161) [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436) Thanks @wking-io! - Fix request ID tracking in the RPC server HTTP protocol finalizer. + +- [#2158](https://github.com/Effect-TS/effect-smol/pull/2158) [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b) Thanks @ColaFanta! - Change `Type_<>` implementation, from using `Exclude` type util to `keyof F as xx`, this implementation keeps IDE provenance link. This enables clicking "Go to definition (F12)" in VSCode on an object made from Schema Struct jumps to the correct Struct field definition. + +- [#2153](https://github.com/Effect-TS/effect-smol/pull/2153) [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900) Thanks @Gabrola! - Allow `HttpApiTest.groups` to accept an optional `baseUrl` override while preserving the existing default of `"http://localhost:3000"`. + +- [#2160](https://github.com/Effect-TS/effect-smol/pull/2160) [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4) Thanks @tim-smart! - Remove the auto-incrementing suffix from HTTP server logger log span names. + +- [#2164](https://github.com/Effect-TS/effect-smol/pull/2164) [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3) Thanks @tim-smart! - Add the unstable workflow DurableQueue module. + +## 4.0.0-beta.65 + +### Patch Changes + +- [#2148](https://github.com/Effect-TS/effect-smol/pull/2148) [`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2) Thanks @tim-smart! - Add `UniqueViolation` as a new SQL error reason. Supported unique constraint violations now classify as `UniqueViolation` instead of the broader `ConstraintError` reason. + + This covers PostgreSQL, PGlite, MySQL, MSSQL, and the shared SQLite classification used by the SQLite-family clients. `UniqueViolation.constraint` contains the best available constraint, index, or key identifier and falls back to exactly `"unknown"` when no reliable identifier is available. + +## 4.0.0-beta.64 + +### Patch Changes + +- [#2137](https://github.com/Effect-TS/effect-smol/pull/2137) [`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5) Thanks @tim-smart! - Add optional soft delete column support to SqlModel repositories and resolvers. + +## 4.0.0-beta.63 + +### Patch Changes + +- [#2136](https://github.com/Effect-TS/effect-smol/pull/2136) [`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac) Thanks @tim-smart! - add HttpApiTest module + +- [#2123](https://github.com/Effect-TS/effect-smol/pull/2123) [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102) Thanks @lewxdev! - add `Effect.acquireDisposable` + +## 4.0.0-beta.62 + +### Patch Changes + +- [#2131](https://github.com/Effect-TS/effect-smol/pull/2131) [`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7) Thanks @tim-smart! - Allow Kubernetes pod condition `lastTransitionTime` values to be null in K8sHttpClient schemas. + +## 4.0.0-beta.61 + +### Patch Changes + +- [#2130](https://github.com/Effect-TS/effect-smol/pull/2130) [`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f) Thanks @tim-smart! - Record fiber runtime start metrics when fibers are constructed so yielded fibers are only counted once. + +- [#2120](https://github.com/Effect-TS/effect-smol/pull/2120) [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c) Thanks @tim-smart! - Port `Effect.firstSuccessOf` from Effect v3. + +- [#2122](https://github.com/Effect-TS/effect-smol/pull/2122) [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b) Thanks @tim-smart! - fix empty body decoding in HttpApiBuilder + +## 4.0.0-beta.60 + +### Patch Changes + +- [#2115](https://github.com/Effect-TS/effect-smol/pull/2115) [`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09) Thanks @tim-smart! - add Rpc.custom + +- [#2119](https://github.com/Effect-TS/effect-smol/pull/2119) [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620) Thanks @gcanti! - Remove `Inspectable.stringifyCircular` and fix `Formatter.formatJson` so shared object references are preserved while only circular references are omitted. + +- [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4) Thanks @tim-smart! - allow using Duration.Input with accessors + +- [#2117](https://github.com/Effect-TS/effect-smol/pull/2117) [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe) Thanks @gcanti! - Add `Schema.DurationFromString` and `SchemaTransformation.durationFromString`, support `"Infinity"` and `"-Infinity"` in `Duration.fromInput`, and simplify config duration parsing around the shared schema codec, closes [#2092](https://github.com/Effect-TS/effect-smol/issues/2092). + +- [#2116](https://github.com/Effect-TS/effect-smol/pull/2116) [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a) Thanks @gcanti! - Add a `Config.literals` convenience constructor for `Schema.Literals`, closes [#2091](https://github.com/Effect-TS/effect-smol/issues/2091). + +## 4.0.0-beta.59 + +### Patch Changes + +- [#2106](https://github.com/Effect-TS/effect-smol/pull/2106) [`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d) Thanks @IMax153! - Fix entity proxy RPC handlers to provide the context expected by RpcServer. + +## 4.0.0-beta.58 + +### Patch Changes + +- [#2097](https://github.com/Effect-TS/effect-smol/pull/2097) [`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7) Thanks @Leka74! - Add an exhaustive finalizer to the AsyncResult builder. + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - generate binary arrays from streams with less copying + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption + +## 4.0.0-beta.57 + +### Patch Changes + +- [#2085](https://github.com/Effect-TS/effect-smol/pull/2085) [`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060) Thanks @tim-smart! - add Effect.abortSignal + +- [#2088](https://github.com/Effect-TS/effect-smol/pull/2088) [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851) Thanks @tim-smart! - ensure each sql client gets a unique transaction service + +## 4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- [#2081](https://github.com/Effect-TS/effect-smol/pull/2081) [`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93) Thanks @gcanti! - Export the `Schema.encodeKeys` interface, closes [#2070](https://github.com/Effect-TS/effect-smol/issues/2070). + + Previously the interface was internal, so exporting a value whose inferred type referenced it triggered TypeScript error `TS4023: Exported variable has or is using name 'encodeKeys' from external module ... but cannot be named`, e.g.: + +- [#2067](https://github.com/Effect-TS/effect-smol/pull/2067) [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a) Thanks @mrazauskas! - fix `isNullish()` type predicate + +## 4.0.0-beta.54 + +### Patch Changes + +- [#2078](https://github.com/Effect-TS/effect-smol/pull/2078) [`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8) Thanks @tim-smart! - add Socket.make + +- [#2075](https://github.com/Effect-TS/effect-smol/pull/2075) [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96) Thanks @tim-smart! - ensure workflow failures are not squashed by suspension interrupts + +## 4.0.0-beta.53 + +### Patch Changes + +- [#2068](https://github.com/Effect-TS/effect-smol/pull/2068) [`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e) Thanks @tim-smart! - Fix `AtomHttpApi` query and mutation error inference to include endpoint middleware and client middleware errors, matching `HttpApiClient` behavior (including response-only mutation mode). + +- [#2062](https://github.com/Effect-TS/effect-smol/pull/2062) [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141) Thanks @aldotestino! - Fix `HttpIncomingMessage.schemaBodyJson` to forward parse options via the `parseOptions` annotation key. + +- [#2074](https://github.com/Effect-TS/effect-smol/pull/2074) [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5) Thanks @tim-smart! - fix Latch.release + +- [#2069](https://github.com/Effect-TS/effect-smol/pull/2069) [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88) Thanks @mikearnaldi! - Fix `TestClock.currentTimeNanosUnsafe()` to floor fractional millisecond instants before converting them to `BigInt`. + +- [#2065](https://github.com/Effect-TS/effect-smol/pull/2065) [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac) Thanks @tim-smart! - add Effectable module + +## 4.0.0-beta.52 + +### Patch Changes + +- [#2057](https://github.com/Effect-TS/effect-smol/pull/2057) [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499) Thanks @tim-smart! - add HttpApiSchemaError for determining where a schema error originates from + +- [#2055](https://github.com/Effect-TS/effect-smol/pull/2055) [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1) Thanks @tim-smart! - ensure tagged enum \_tag is correctly set + +- [#2057](https://github.com/Effect-TS/effect-smol/pull/2057) [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499) Thanks @tim-smart! - make HttpApi schema errors defects unless transformed + +- [#2058](https://github.com/Effect-TS/effect-smol/pull/2058) [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e) Thanks @tim-smart! - mcp http request with no session header is 404 response + +## 4.0.0-beta.51 + +### Patch Changes + +- [#2049](https://github.com/Effect-TS/effect-smol/pull/2049) [`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344) Thanks @bohdanbirdie! - Add `RpcSerialization.makeMsgPack` for creating MessagePack serialization with custom msgpackr options. On Cloudflare Workers with `allow_eval_during_startup` (default for `compatibility_date >= 2025-06-01`), pass `{ useRecords: false }` to prevent msgpackr's JIT code generation via `new Function()`, which is blocked during request handling. Also fixes silent error swallowing in the `msgPack` decode path — non-incomplete errors are now rethrown instead of returning `[]`. + +- [#2010](https://github.com/Effect-TS/effect-smol/pull/2010) [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a) Thanks @tim-smart! - process schema properties / elements concurrently + +- [#2052](https://github.com/Effect-TS/effect-smol/pull/2052) [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7) Thanks @gcanti! - Schema: expand `FilterOutput` and add `FilterIssue` for richer filter failures. + + The return type of a `Schema.makeFilter` predicate now supports two additional shapes: + - `{ path, issue }` where `issue` is `string | SchemaIssue.Issue` (previously only `{ path, message: string }` was accepted). The `issue` arm lets you attach a fully-formed `Issue` at a nested path without manually constructing a `Pointer`. + - `ReadonlyArray` to report several failures at once. An empty array is success, a single-element array is equivalent to returning that element, and multi-entry arrays are grouped into an `Issue.Composite`. This removes the need to import `SchemaIssue` and hand-build a `Composite` for multi-field validators. + + The single-failure shapes (`undefined`, `true`, `false`, `string`, `SchemaIssue.Issue`) are unchanged. + + **Breaking**: the object shape renamed from `{ path, message }` to `{ path, issue }`. Call sites that used the old shape must rename the field; the migration is mechanical. + + ```ts + // before + Schema.makeFilter((o) => ({ path: ["a"], message: "bad" })); + + // after + Schema.makeFilter((o) => ({ path: ["a"], issue: "bad" })); + ``` + + Also renamed `{ path, message }` to `{ path, issue }` in the accepted return type of `SchemaGetter.checkEffect`. + +- [#2047](https://github.com/Effect-TS/effect-smol/pull/2047) [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd) Thanks @gcanti! - Fix `SchemaAST.isJson` rejecting DAGs as cycles, closes [#2021](https://github.com/Effect-TS/effect-smol/issues/2021). + + The previous implementation marked every visited object in a single `seen` set and never removed it, so any value that referenced the same object through two different paths (a DAG, e.g. `{ x: shared, y: shared }`) was treated as a cycle and returned `false`. Cycle detection now tracks only the current recursion path (popping on exit) and memoizes fully validated subtrees, so DAGs are accepted while true cycles are still rejected. + +- [#2051](https://github.com/Effect-TS/effect-smol/pull/2051) [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27) Thanks @tim-smart! - disable sql traces for EventLog, RunnerStorage + +- [#2053](https://github.com/Effect-TS/effect-smol/pull/2053) [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82) Thanks @tim-smart! - remove use of bigint literals + +- [#2046](https://github.com/Effect-TS/effect-smol/pull/2046) [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0) Thanks @gcanti! - Remove the `options` parameter from `OpenApi.fromApi`. + + The parameter only carried `additionalProperties`, but the function caches results in a `WeakMap` keyed solely on the `api` instance. Passing different options across calls for the same api was silently ignored, making the parameter order-dependent and effectively single-shot. No call sites were using it, so the signature is now simply `fromApi(api)`. + +- [#2044](https://github.com/Effect-TS/effect-smol/pull/2044) [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0) Thanks @tim-smart! - ensure envelope payloads are correctly encoded for notify path + +## 4.0.0-beta.50 + +### Patch Changes + +- [#2038](https://github.com/Effect-TS/effect-smol/pull/2038) [`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414) Thanks @tim-smart! - add support for deferred responses in rpc + +- [#2040](https://github.com/Effect-TS/effect-smol/pull/2040) [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b) Thanks @tim-smart! - require a option to make AtomRpc.query atoms serializatable + +## 4.0.0-beta.49 + +### Patch Changes + +- [#2035](https://github.com/Effect-TS/effect-smol/pull/2035) [`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45) Thanks @tim-smart! - Add support for common HTTP status string literals in `HttpApiSchema.status` (for example, `HttpApiSchema.status("Created")` resolves to status code `201`). + +- [#2036](https://github.com/Effect-TS/effect-smol/pull/2036) [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5) Thanks @tim-smart! - add RpcGroup.omit + +- [#2034](https://github.com/Effect-TS/effect-smol/pull/2034) [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac) Thanks @IMax153! - Fix issue with exported CLI `Completions` types + +## 4.0.0-beta.48 + +### Patch Changes + +- [#2025](https://github.com/Effect-TS/effect-smol/pull/2025) [`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8) Thanks @tim-smart! - update dependencies + +- [#2029](https://github.com/Effect-TS/effect-smol/pull/2029) [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a) Thanks @tim-smart! - omit scope from HttpApi handlers + +- [#2023](https://github.com/Effect-TS/effect-smol/pull/2023) [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070) Thanks @tim-smart! - EventLog Identity string encodes to base 64 + +- [#2023](https://github.com/Effect-TS/effect-smol/pull/2023) [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070) Thanks @tim-smart! - disable tracer propagation for otlp exporter + +## 4.0.0-beta.47 + +### Patch Changes + +- [#2017](https://github.com/Effect-TS/effect-smol/pull/2017) [`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45) Thanks @gcanti! - Schema: add `annotateEncoded` function for annotating the encoded side of a schema. + +- [#2013](https://github.com/Effect-TS/effect-smol/pull/2013) [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db) Thanks @gcanti! - Schema: add withDecodingDefaultTypeKey / withDecodingDefaultType, closes #2012 + +- [#2018](https://github.com/Effect-TS/effect-smol/pull/2018) [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12) Thanks @gcanti! - Schema: allow `Class` constructors to accept `void` when all fields are optional, closes #2015. + +- [#2016](https://github.com/Effect-TS/effect-smol/pull/2016) [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406) Thanks @gcanti! - Schema: rename `"~rebuild.out"` to `"Rebuild"` + +## 4.0.0-beta.46 + +### Patch Changes + +- [#2008](https://github.com/Effect-TS/effect-smol/pull/2008) [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee) Thanks @tim-smart! - fix eventlog skipping entries + +## 4.0.0-beta.45 + +### Patch Changes + +- [#1883](https://github.com/Effect-TS/effect-smol/pull/1883) [`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb) Thanks @tim-smart! - Add EventLogServerUnencrypted module + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1943](https://github.com/Effect-TS/effect-smol/pull/1943) [`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1) Thanks @gcanti! - Add `DateFromString`, `BigIntFromString`, `BigDecimalFromString`, `TimeZoneNamedFromString`, `TimeZoneFromString`, and `DateTimeZonedFromString` schemas, closes #1941. + +- [#1996](https://github.com/Effect-TS/effect-smol/pull/1996) [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced) Thanks @gcanti! - Schema: add `StringFromBase64`, `StringFromBase64Url`, `StringFromHex`, and `StringFromUriComponent` schemas for decoding encoded strings into UTF-8 strings, closes #1995. + +- [#1952](https://github.com/Effect-TS/effect-smol/pull/1952) [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16) Thanks @tim-smart! - Effect.repeat now uses effect return value when using options + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename Atom's `Context` type to `AtomContext` + +- [#1975](https://github.com/Effect-TS/effect-smol/pull/1975) [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82) Thanks @tim-smart! - catch defects when building Entity handlers + +- [#2000](https://github.com/Effect-TS/effect-smol/pull/2000) [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877) Thanks @tim-smart! - fix cache constructor inference by moving the lookup option + +- [#1928](https://github.com/Effect-TS/effect-smol/pull/1928) [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e) Thanks @tim-smart! - Add `schema.makeEffect(input, options?)` to `Schema.Bottom` and schema-backed classes, matching the existing constructor behavior exposed by `makeUnsafe` / `makeOption` while returning an `Effect` failure with `Schema.SchemaError`. + +- [#1949](https://github.com/Effect-TS/effect-smol/pull/1949) [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664) Thanks @tim-smart! - Update the unstable HTTP middleware logger to annotate only the request path in `http.url` instead of including the full URL (query / fragment), and add a regression test. + +- [#1962](https://github.com/Effect-TS/effect-smol/pull/1962) [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9) Thanks @tim-smart! - Add `KeyValueStore.layerSql` to back key-value storage with a SQL database via `SqlClient`. + +- [#1963](https://github.com/Effect-TS/effect-smol/pull/1963) [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9) Thanks @tim-smart! - Fix `Unify.unify` so Layer unions merge correctly, and add type tests covering Layer unification. + +- [#2004](https://github.com/Effect-TS/effect-smol/pull/2004) [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b) Thanks @tim-smart! - Fix `Stream.toQueue` types and implementation to return a `Queue.Dequeue` in both overloads and delegate to `Channel.toQueueArray`. + +- [#1974](https://github.com/Effect-TS/effect-smol/pull/1974) [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb) Thanks @juliusmarminge! - Fix unstable CLI boolean flags so `Flag.optional(Flag.boolean(...))` returns `Option.none()` when omitted, and support canonical `--no-` negation for boolean flags. + +- [#1980](https://github.com/Effect-TS/effect-smol/pull/1980) [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5) Thanks @tim-smart! - fix Entity.keepAlive + +- [#1929](https://github.com/Effect-TS/effect-smol/pull/1929) [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02) Thanks @gcanti! - Simplify and align the default-value APIs. + + `Schema.withConstructorDefault` now accepts an `Effect` instead of `(o: Option) => Option | Effect>`. + + `Schema.withDecodingDefault` / `Schema.withDecodingDefaultKey` now accept an `Effect` instead of `() => T`, enabling effectful defaults. + + `SchemaGetter.withDefault` follows the same change, accepting `Effect` instead of `() => T`. + +- [#1966](https://github.com/Effect-TS/effect-smol/pull/1966) [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22) Thanks @gcanti! - Reuse existing references when duplicate identifiers have the same representation, closes #1927. + +- [#1942](https://github.com/Effect-TS/effect-smol/pull/1942) [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4) Thanks @gcanti! - Fix `ErrorClass` and `TaggedErrorClass` `toString` to match native `Error` output format (e.g. `E: my message` instead of `E({"message":"my message"})`), closes #1940. + + Also fix prototype properties (e.g. `name`) being lost after `.extend()`. + +- [#1956](https://github.com/Effect-TS/effect-smol/pull/1956) [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888) Thanks @gcanti! - Add `Schema.resolveAnnotationsKey` API to retrieve the context (key-level) annotations from a schema, closes #1947. + + Also rename `Schema.resolveInto` to `Schema.resolveAnnotations`. + +- [#2005](https://github.com/Effect-TS/effect-smol/pull/2005) [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0) Thanks @gcanti! - Fix `Stream.splitLines` to correctly handle standalone `\r` as a line terminator and flush the final unterminated line when the stream ends, closes #2002. + +- [#1936](https://github.com/Effect-TS/effect-smol/pull/1936) [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226) Thanks @IMax153! - Fix `Stream.groupedWithin` dropping partial batches when the upstream ends or goes idle. + +- [#1981](https://github.com/Effect-TS/effect-smol/pull/1981) [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee) Thanks @tim-smart! - add rpc ConnectionHooks + +- [#1965](https://github.com/Effect-TS/effect-smol/pull/1965) [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c) Thanks @tim-smart! - return resolvers directly from SqlModel.makeResolvers + +- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. + +- [#1984](https://github.com/Effect-TS/effect-smol/pull/1984) [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060) Thanks @jannabiforever! - Make `Effect.retry` with `times` argument to propagate the original error. + +- [#1930](https://github.com/Effect-TS/effect-smol/pull/1930) [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4) Thanks @mikearnaldi! - Add `Stream.service` and `Stream.serviceOption` for accessing services as single-element streams. + +- [#1935](https://github.com/Effect-TS/effect-smol/pull/1935) [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9) Thanks @gcanti! - Schema: add `asClass` API to turn any schema into a class with static method support. + + **Example** + + ```ts + import { Schema } from "effect"; + + class MyString extends Schema.asClass(Schema.String) { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this); + } + + MyString.decodeUnknownSync("a"); // "a" + ``` + +- [#1958](https://github.com/Effect-TS/effect-smol/pull/1958) [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6) Thanks @gcanti! - Schema: add `MissingSelfGeneric` compile-time error for `Class`, `TaggedClass`, `ErrorClass`, and `TaggedErrorClass` when the `Self` type parameter is omitted. + +- [#1957](https://github.com/Effect-TS/effect-smol/pull/1957) [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a) Thanks @gcanti! - Schema: remove `"~annotate.in"` type from `Bottom` interface, inlining it where needed + +- [#1951](https://github.com/Effect-TS/effect-smol/pull/1951) [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a) Thanks @gcanti! - Rename `Schema.makeUnsafe` instance method back to `Schema.make` on all schemas and schema-backed classes. + + Also remove the `static readonly make` override from `ShardId` to avoid conflicting with the inherited schema `make` method. The module-level `ShardId.make(group, id)` function is still available. + +- [#1999](https://github.com/Effect-TS/effect-smol/pull/1999) [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4) Thanks @tim-smart! - use NoInfer in Layer constructors to prevent type erasure + +- [#1971](https://github.com/Effect-TS/effect-smol/pull/1971) [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0) Thanks @joepjoosten! - Allow unstable CLI fallback prompts to be created dynamically from an `Effect`. + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- [#1973](https://github.com/Effect-TS/effect-smol/pull/1973) [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780) Thanks @joepjoosten! - Underline the active label in CLI multi-select prompts and add a scratchpad example for manual verification. + +- [#1967](https://github.com/Effect-TS/effect-smol/pull/1967) [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020) Thanks @tim-smart! - clean up ShardId + +## 4.0.0-beta.43 + +### Patch Changes + +- [#1904](https://github.com/Effect-TS/effect-smol/pull/1904) [`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596) Thanks @juliusmarminge! - Fix JSON-RPC serialization for `id` values that are falsey but valid, including `0` and `""`, while still mapping `null` to Effect's internal notification sentinel. + +- [#1900](https://github.com/Effect-TS/effect-smol/pull/1900) [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e) Thanks @tim-smart! - Fix AI structured output schema generation for `Schema.Class` and `Schema.ErrorClass` by resolving top-level `$ref` entries before passing JSON Schema to providers and default codec transformers. + +- [#1908](https://github.com/Effect-TS/effect-smol/pull/1908) [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e) Thanks @tim-smart! - Fix stream requests in Entity.toLayerQueue + +- [#1907](https://github.com/Effect-TS/effect-smol/pull/1907) [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8) Thanks @tim-smart! - add WorkflowEngine interruptUnsafe + +- [#1903](https://github.com/Effect-TS/effect-smol/pull/1903) [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02) Thanks @kitlangton! - Add `Layer.suspend` as a lazy constructor for dynamically choosing a layer while preserving normal layer sharing. + +## 4.0.0-beta.42 + +### Patch Changes + +- [#1897](https://github.com/Effect-TS/effect-smol/pull/1897) [`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee) Thanks @IMax153! - Append concrete choice values to CLI flag help descriptions so generated help shows valid command-line inputs. + +- [#1894](https://github.com/Effect-TS/effect-smol/pull/1894) [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa) Thanks @tim-smart! - Fix `MutableList.appendAll` / `appendAllUnsafe` so empty arrays are treated as a no-op instead of leaving behind an empty internal bucket. + +- [#1895](https://github.com/Effect-TS/effect-smol/pull/1895) [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f) Thanks @tim-smart! - Changed socket close handling so all close codes are treated as errors by default unless `closeCodeIsError` is overridden. + +- [#1899](https://github.com/Effect-TS/effect-smol/pull/1899) [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133) Thanks @gcanti! - SchemaRepresentation: support `anyOf`/`oneOf` with sibling keywords in `fromJsonSchemaMultiDocument` + +- [#1893](https://github.com/Effect-TS/effect-smol/pull/1893) [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6) Thanks @gcanti! - `Number.remainder`: fix incorrect results for small floats in scientific notation (e.g. `1e-7`). + +- [#1898](https://github.com/Effect-TS/effect-smol/pull/1898) [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623) Thanks @mikearnaldi! - Rename `Effect.transaction` to `Effect.tx` and `Effect.retryTransaction` to `Effect.txRetry`, remove `Effect.transactionWith` / `Effect.withTxState`, make nested `Effect.tx` calls compose into the active transaction, and make the public `Tx*` APIs establish atomic transactions without requiring `Transaction` in common usage. + +## 4.0.0-beta.41 + +### Patch Changes + +- [#1881](https://github.com/Effect-TS/effect-smol/pull/1881) [`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283) Thanks @gcanti! - Added `BigDecimal.sumAll` and `BigDecimal.multiplyAll` for feature parity with `Number` and `BigInt`, closes #1880. + +- [#1869](https://github.com/Effect-TS/effect-smol/pull/1869) [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30) Thanks @gcanti! - Schema: collapse same-type literal branches in JSON Schema output into a single `enum` array, closes #1868. + + Before: + + ```json + { + "anyOf": [ + { "type": "string", "enum": ["A"] }, + { "type": "string", "enum": ["B"] } + ] + } + ``` + + After: + + ```json + { + "type": "string", + "enum": ["A", "B"] + } + ``` + +- [#1879](https://github.com/Effect-TS/effect-smol/pull/1879) [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814) Thanks @tim-smart! - Highlight active option labels in `Prompt.select` and `Prompt.multiSelect` using cyan text so selection state is visible beyond the pointer / checkbox icon. + +- [#1884](https://github.com/Effect-TS/effect-smol/pull/1884) [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86) Thanks @tim-smart! - Fail RpcClient HTTP requests when the server response contains no RPC messages instead of leaving requests pending. + +- [#1875](https://github.com/Effect-TS/effect-smol/pull/1875) [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84) Thanks @IMax153! - Fix AI text method toolkit typing to support generic handler toolkits, preserve toolkit union inference, and keep response part narrowing by tool name. + +- [#1876](https://github.com/Effect-TS/effect-smol/pull/1876) [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48) Thanks @tim-smart! - Track ManagedRuntime fibers in a scope + +- [#1886](https://github.com/Effect-TS/effect-smol/pull/1886) [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f) Thanks @tim-smart! - add ClusterSchema.WithTransaction annotation + +- [#1877](https://github.com/Effect-TS/effect-smol/pull/1877) [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95) Thanks @tim-smart! - allow Context.Key to be covariant + +## 4.0.0-beta.40 + +### Patch Changes + +- [#1863](https://github.com/Effect-TS/effect-smol/pull/1863) [`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a) Thanks @tim-smart! - fix issues with metro bundler + +- [#1866](https://github.com/Effect-TS/effect-smol/pull/1866) [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8) Thanks @tim-smart! - add Stream.timeoutOrElse + +## 4.0.0-beta.39 + +### Patch Changes + +- [#1844](https://github.com/Effect-TS/effect-smol/pull/1844) [`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0) Thanks @tim-smart! - Relax `HttpApiClient.urlBuilder` to accept `HttpApi.Any` instead of requiring `HttpApi.AnyWithProps`. + This allows use in helpers generic over `HttpApi.Any` while preserving inferred URL builder types. + +- [#1851](https://github.com/Effect-TS/effect-smol/pull/1851) [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071) Thanks @tim-smart! - Re-export additional core runtime references from `effect/References`, including logger and error reporter references. + +- [#1856](https://github.com/Effect-TS/effect-smol/pull/1856) [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513) Thanks @gcanti! - Fix `Struct` utility return types (for example `pick`) to preserve the previous simplified shape instead of exposing raw utility types like `Pick`, closes #1855. + +- [#1849](https://github.com/Effect-TS/effect-smol/pull/1849) [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b) Thanks @tim-smart! - Fix the `Queue.takeN` documentation example to end the queue before showing a partial batch. + +- [#1848](https://github.com/Effect-TS/effect-smol/pull/1848) [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996) Thanks @tim-smart! - Remove `Schedule.compose` in favor of `Schedule.both`, and update schedule examples to use `Schedule.both`. + +## 4.0.0-beta.38 + +### Patch Changes + +- [#1842](https://github.com/Effect-TS/effect-smol/pull/1842) [`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd) Thanks @gcanti! - Schema: rename `MakeOptions.disableValidation` to `disableChecks`. Apply constructor defaults when `disableChecks` is true, closes #1841. + +- [#1837](https://github.com/Effect-TS/effect-smol/pull/1837) [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb) Thanks @kitlangton! - Fix `HttpApiBuilder` security middleware caching so separate handler builds do not reuse the first provided middleware implementation. + +- [#1840](https://github.com/Effect-TS/effect-smol/pull/1840) [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6) Thanks @tim-smart! - Rename HttpApiClient request option `withResponse` to `responseMode` and add support for `responseMode: "response-only"` to return the raw `HttpClientResponse` without decoding. + +- [#1838](https://github.com/Effect-TS/effect-smol/pull/1838) [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd) Thanks @tim-smart! - Update `HttpApiClient.urlBuilder` to mirror client shape, and encode params/query via endpoint schemas before building URLs. + +- [#1700](https://github.com/Effect-TS/effect-smol/pull/1700) [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0) Thanks @tim-smart! - add `useCodecs` option to HttpClientEndpoint constructors + +## 4.0.0-beta.37 + +### Patch Changes + +- [#1812](https://github.com/Effect-TS/effect-smol/pull/1812) [`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2) Thanks @tim-smart! - Consolidate the SqlError changes to the new reason-based shape across effect and the SQL drivers, classifying native failures into structured reasons with Unknown fallback where native codes are unavailable. + +- [#1816](https://github.com/Effect-TS/effect-smol/pull/1816) [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80) Thanks @tim-smart! - unstable/http HttpClientRequest: add toWeb and fromWeb conversions for web Request objects + +- [#1829](https://github.com/Effect-TS/effect-smol/pull/1829) [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608) Thanks @tim-smart! - Fix sql migrator lock handling to only treat duplicate migration-row inserts as a concurrent migration lock. + +- [#1831](https://github.com/Effect-TS/effect-smol/pull/1831) [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6) Thanks @tim-smart! - Fix `Schedule.fixed` to run the next iteration immediately when the previous action takes longer than the configured interval. + +- [#1833](https://github.com/Effect-TS/effect-smol/pull/1833) [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2) Thanks @tim-smart! - Fix `Unify.unify` so unions of `Effect` values collapse to a single unified `Effect` type again. + +- [#1825](https://github.com/Effect-TS/effect-smol/pull/1825) [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4) Thanks @skoshx! - Fix DevToolsClient not flushing final span events on teardown. + + The stream consumer was `forkScoped`, causing it to be interrupted before + it could drain remaining queue items. Replaced with `forkChild` and + `Fiber.await` in the finalizer so the stream drains naturally after the + queue is failed. + +- [#1824](https://github.com/Effect-TS/effect-smol/pull/1824) [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c) Thanks @tim-smart! - Ignore unsupported Ctrl key combinations in interactive CLI prompts to avoid rendering control characters such as Ctrl+L form feed into prompt input. + +- [#1819](https://github.com/Effect-TS/effect-smol/pull/1819) [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd) Thanks @j! - HttpServerResponse: fix `fromWeb` to preserve Content-Type header when response has a body + + Previously, when converting a web `Response` to an `HttpServerResponse` via `fromWeb`, the `Content-Type` header was not passed to `Body.stream()`, causing it to default to `application/octet-stream`. This affected any code using `HttpApp.fromWebHandler` to wrap web handlers, as JSON responses would incorrectly have their Content-Type set to `application/octet-stream` instead of `application/json`. + +- [#1821](https://github.com/Effect-TS/effect-smol/pull/1821) [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe) Thanks @gcanti! - Schema: relax `asserts` and `is` constraints. + +- [#1822](https://github.com/Effect-TS/effect-smol/pull/1822) [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7) Thanks @tim-smart! - improve runSync error when executing async effects + +## 4.0.0-beta.36 + +### Patch Changes + +- [#1793](https://github.com/Effect-TS/effect-smol/pull/1793) [`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81) Thanks @tim-smart! - Ensure streamed tool results are emitted before the finish part so chat history includes tool outputs before stream termination. + +- [#1762](https://github.com/Effect-TS/effect-smol/pull/1762) [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e) Thanks @kitlangton! - Allow unstable HttpApi middleware to declare multiple error schemas with arrays. + + Middleware errors now follow endpoint error behavior for response status resolution, client decoding, and generated API schemas. + +- [#1805](https://github.com/Effect-TS/effect-smol/pull/1805) [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e) Thanks @tim-smart! - Fix `Effect.cachedWithTTL` and `Effect.cachedInvalidateWithTTL` to start TTL expiration when the cached value is produced instead of when computation starts. + +- [#1808](https://github.com/Effect-TS/effect-smol/pull/1808) [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6) Thanks @tim-smart! - Backport `Cron.prev` with reverse lookup tables and cron stepping logic, including DST-aware reverse traversal. + +- [#1789](https://github.com/Effect-TS/effect-smol/pull/1789) [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b) Thanks @mikearnaldi! - Fix `Stream.scanEffect` hanging and repeatedly emitting the initial state. + +- [#1810](https://github.com/Effect-TS/effect-smol/pull/1810) [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480) Thanks @tim-smart! - Support key-derived `idleTimeToLive` in `LayerMap` options (`make`, `fromRecord`, and `LayerMap.Service`) and add `LayerMap` tests for dynamic TTL behavior. + +- [#1802](https://github.com/Effect-TS/effect-smol/pull/1802) [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c) Thanks @kitlangton! - PubSub.publish and PubSub.publishAll now return false on shutdown instead of interrupting, matching Queue.offer semantics. + +- [#1796](https://github.com/Effect-TS/effect-smol/pull/1796) [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c) Thanks @tim-smart! - Improve `Prompt.file` to support incremental filtering while typing, including backspace and ctrl-u handling. + +- [#1806](https://github.com/Effect-TS/effect-smol/pull/1806) [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185) Thanks @tim-smart! - Fix a regression in `PubSub.shutdown` so shutting down a pubsub interrupts suspended subscribers (including `takeAll`) by ensuring subscriptions are scoped under the pubsub shutdown scope. + +- [#1797](https://github.com/Effect-TS/effect-smol/pull/1797) [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611) Thanks @tim-smart! - Add \`Ctrl-A\` and \`Ctrl-E\` key handling for editable CLI text prompts to move the cursor to the beginning or end of the current input line. + +- [#1633](https://github.com/Effect-TS/effect-smol/pull/1633) [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32) Thanks @kitlangton! - Schema: add `decodeUnknownResult` / `decodeResult` and `encodeUnknownResult` / `encodeResult` helpers for synchronous `Result`-based parsing. + +- [#1798](https://github.com/Effect-TS/effect-smol/pull/1798) [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61) Thanks @gcanti! - Schema: allow using `Struct` type helpers directly, e.g. `Schema.Struct.Type` instead of `Schema.Schema.Type>`. + +- [#1794](https://github.com/Effect-TS/effect-smol/pull/1794) [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b) Thanks @tim-smart! - Fix ai LanguageModel streaming finish parts so finish events are always emitted when a toolkit is provided. + +- [#1785](https://github.com/Effect-TS/effect-smol/pull/1785) [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542) Thanks @KhraksMamtsov! - add missing Equivalence.Date + +## 4.0.0-beta.35 + +### Patch Changes + +- [#1782](https://github.com/Effect-TS/effect-smol/pull/1782) [`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8) Thanks @gcanti! - Add `Schema.ArrayEnsure`. + +- [#1784](https://github.com/Effect-TS/effect-smol/pull/1784) [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04) Thanks @gcanti! - Add `Config.Success` type utility, closes #1783. + +- [#1778](https://github.com/Effect-TS/effect-smol/pull/1778) [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07) Thanks @tim-smart! - Allow `Effect.acquireRelease` release finalizers to depend on the surrounding environment. + +- [#1777](https://github.com/Effect-TS/effect-smol/pull/1777) [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2) Thanks @tim-smart! - Remove an unreachable array branch in `decodeJsonRpcRaw` to simplify JSON-RPC decode logic without changing behavior. + +- [#1774](https://github.com/Effect-TS/effect-smol/pull/1774) [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216) Thanks @tim-smart! - Align CLI help flag and global flag descriptions to a single column even when some flag names are very long. + +- [#1780](https://github.com/Effect-TS/effect-smol/pull/1780) [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98) Thanks @tim-smart! - Fix `LanguageModel` incremental prompt fallback to reliably retry with the full prompt when an incremental request fails with `InvalidRequestError`. + +- [#1781](https://github.com/Effect-TS/effect-smol/pull/1781) [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801) Thanks @gcanti! - Fix `DateTime.makeUnsafe` incorrectly appending "Z" to date strings containing "GMT" + +- [#1772](https://github.com/Effect-TS/effect-smol/pull/1772) [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7) Thanks @tim-smart! - make Layer.mock work with Stream and Channel + +## 4.0.0-beta.34 + +### Patch Changes + +- [#1758](https://github.com/Effect-TS/effect-smol/pull/1758) [`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1) Thanks @tim-smart! - Use a normal Map in ResponseIdTracker and clear it on divergence / reset instead of reallocating a WeakMap. + +- [#1764](https://github.com/Effect-TS/effect-smol/pull/1764) [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f) Thanks @tim-smart! - Add unstable EmbeddingModel support across core and OpenAI providers. + - Add the unstable EmbeddingModel module API surface in `effect`, including service, request, response, and provider types. + - Implement the unstable EmbeddingModel runtime constructor in `effect`, with `RequestResolver` batching, `embed` / `embedMany` spans, provider error propagation, deterministic ordering, and empty-input `embedMany` fast-path behavior. + - Add and align EmbeddingModel behavior tests in `effect` for embedding usage, batching, ordering, and error handling. + - Add `OpenAiEmbeddingModel` in `@effect/ai-openai`, including model / make / layer constructors, config overrides, and provider output index validation with deterministic reordering. + - Add OpenAI-compatible EmbeddingModel provider support in `@effect/ai-openai-compat`, including config overrides, layer constructors, and output index validation. + +- [#1766](https://github.com/Effect-TS/effect-smol/pull/1766) [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685) Thanks @tim-smart! - Fix JSDoc wording for `Effect.catch` to consistently reference the current API name. + +- [#1771](https://github.com/Effect-TS/effect-smol/pull/1771) [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e) Thanks @tim-smart! - Add `EmbeddingModel.ModelDimensions` and require dimensions in embedding provider `model` constructors. + +- [#1767](https://github.com/Effect-TS/effect-smol/pull/1767) [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28) Thanks @gcanti! - Add `isMutableHashMap` and `isMutableHashSet`, and align nominal guard implementations and tests across collections and transactional data types. + +- [#1765](https://github.com/Effect-TS/effect-smol/pull/1765) [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2) Thanks @tim-smart! - retry incremental prompt on invalid request + +- [#1756](https://github.com/Effect-TS/effect-smol/pull/1756) [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9) Thanks @tim-smart! - add HttpApiMiddleware.layerSchemaErrorTransform + +- [#1732](https://github.com/Effect-TS/effect-smol/pull/1732) [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0) Thanks @KhraksMamtsov! - port Url module from v3 + +- [#1761](https://github.com/Effect-TS/effect-smol/pull/1761) [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba) Thanks @gcanti! - Fix `Tool.make` type and runtime behavior when `parameters` is not provided. + +## 4.0.0-beta.33 + +### Patch Changes + +- [#1754](https://github.com/Effect-TS/effect-smol/pull/1754) [`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295) Thanks @tim-smart! - narrow types for Effect.retry/repeat while option + +## 4.0.0-beta.32 + +### Patch Changes + +- [#1717](https://github.com/Effect-TS/effect-smol/pull/1717) [`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a) Thanks @gcanti! - Schema: add `OptionFromOptionalNullOr` schema, closes #1707. + +- [#1722](https://github.com/Effect-TS/effect-smol/pull/1722) [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74) Thanks @tim-smart! - Fix `RpcSerialization.json` decode so JSON array payloads are not wrapped in an extra outer array. + +- [#1725](https://github.com/Effect-TS/effect-smol/pull/1725) [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba) Thanks @tim-smart! - Improve unstable HttpApi runtime failures for missing server middleware and missing group implementations. + - HttpApiBuilder.applyMiddleware now resolves middleware services via Context.getUnsafe, so missing middleware fails with a clear "Service not found: " error instead of an opaque is not a function TypeError. + - HttpApiBuilder.layer now reports missing groups with actionable context (group identifier, service key, suggested HttpApiBuilder.group(...) call, and available group keys). + - Added regression tests in packages/platform-node/test/HttpApi.test.ts covering: + - addHttpApi + API-level middleware applied across merged groups + - missing middleware service diagnostics + - missing addHttpApi group layer diagnostics + +- [#1727](https://github.com/Effect-TS/effect-smol/pull/1727) [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e) Thanks @tim-smart! - Make all built-in `HttpApiError` classes implement `HttpServerRespondable`, so they can be returned directly from plain HTTP server handlers outside of `HttpApi`. + +- [#1739](https://github.com/Effect-TS/effect-smol/pull/1739) [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732) Thanks @tim-smart! - Use predicate-based `dual` dispatch for `Stream.merge` so data-last calls with optional `options` are handled correctly. + +- [#1741](https://github.com/Effect-TS/effect-smol/pull/1741) [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7) Thanks @tim-smart! - Add `Layer.tap`, `Layer.tapError`, and `Layer.tapCause` APIs for effectful observation of layer success and failure without changing layer outputs. + +- [#1740](https://github.com/Effect-TS/effect-smol/pull/1740) [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c) Thanks @tim-smart! - Refactor call sites with multiple `Context` mutations to use `Context.mutate` for batched updates. + +- [#1750](https://github.com/Effect-TS/effect-smol/pull/1750) [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f) Thanks @gcanti! - Improve unstable AI structured output handling for empty tool params and add `Tool.EmptyParams`, closes #1749. + +- [#1525](https://github.com/Effect-TS/effect-smol/pull/1525) [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3) Thanks @tim-smart! - use Option instead of undefined | A + +- [#1747](https://github.com/Effect-TS/effect-smol/pull/1747) [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f) Thanks @tim-smart! - seperate scheduler dispatch from yield decisions + +- [#1729](https://github.com/Effect-TS/effect-smol/pull/1729) [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245) Thanks @tim-smart! - add Context.mutate + +- [#1753](https://github.com/Effect-TS/effect-smol/pull/1753) [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0) Thanks @tim-smart! - Add dtslint coverage for `Stream.catchIf` to lock in predicate and refinement inference behavior in both data-first and data-last forms. + +- [#1716](https://github.com/Effect-TS/effect-smol/pull/1716) [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d) Thanks @gcanti! - Remove unused `effect/NullOr` module. + +- [#1721](https://github.com/Effect-TS/effect-smol/pull/1721) [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb) Thanks @IMax153! - Correct the type of the schema parameter accepted by the `fileSchema` methods in the CLI to be `Schema.Decoder` + +- [#1709](https://github.com/Effect-TS/effect-smol/pull/1709) [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7) Thanks @mikearnaldi! - Add module-level helpers for `Semaphore`, `Latch`, and extracted `PartitionedSemaphore` operations. + +- [#1752](https://github.com/Effect-TS/effect-smol/pull/1752) [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45) Thanks @tim-smart! - simplify SubscriptionRef + +- [#1743](https://github.com/Effect-TS/effect-smol/pull/1743) [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4) Thanks @tim-smart! - default ws close codes to 1001 in case they are undefined + +- [#1728](https://github.com/Effect-TS/effect-smol/pull/1728) [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e) Thanks @tim-smart! - add graceful shutdown to http servers + +## 4.0.0-beta.31 + +### Patch Changes + +- [#1696](https://github.com/Effect-TS/effect-smol/pull/1696) [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462) Thanks @krzkaczor! - Add `DurationObject` to `Duration.Input` to support Temporal-style object input. + + Durations can now be created from objects with named unit properties like `{ hours: 1, minutes: 30 }`, similar to `Temporal.Duration.from()`. Supported fields: `weeks`, `days`, `hours`, `minutes`, `seconds`, `millis`, `micros`, `nanos`. + +- [#1705](https://github.com/Effect-TS/effect-smol/pull/1705) [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544) Thanks @tim-smart! - Preserve message item ordering in the default logger when logging a `Cause` with message values. + +- [#1711](https://github.com/Effect-TS/effect-smol/pull/1711) [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28) Thanks @tim-smart! - Fix `RpcGroup.toLayer` and `RpcGroup.toLayerHandler` service requirement inference so handler dependencies are preserved for non-stream RPC handlers. + +- [#1712](https://github.com/Effect-TS/effect-smol/pull/1712) [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf) Thanks @tim-smart! - Expose CLI completions as a public unstable module at `effect/unstable/cli/Completions`. + +- [#1713](https://github.com/Effect-TS/effect-smol/pull/1713) [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7) Thanks @tim-smart! - Make `Layer.mock` a dual API so it supports both `Layer.mock(Service)(impl)` and `Layer.mock(Service, impl)`. + +- [#1704](https://github.com/Effect-TS/effect-smol/pull/1704) [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2) Thanks @tim-smart! - Support toolkit unions in `LanguageModel` options. + +- [#1701](https://github.com/Effect-TS/effect-smol/pull/1701) [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45) Thanks @tim-smart! - wrap httpapi request context with HttpRouter.Request + +- [#1696](https://github.com/Effect-TS/effect-smol/pull/1696) [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462) Thanks @krzkaczor! - allow assigning Temporal types to DateTime & Duration input + +- [#1698](https://github.com/Effect-TS/effect-smol/pull/1698) [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064) Thanks @tim-smart! - Include toolkit tool handler requirements in AI generation API environment inference. + +- [#1703](https://github.com/Effect-TS/effect-smol/pull/1703) [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee) Thanks @tim-smart! - Relax `Ndjson` byte-stream channel signatures to accept plain `Uint8Array`. + +- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. + + Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. + +## 4.0.0-beta.30 + +### Patch Changes + +- [#1675](https://github.com/Effect-TS/effect-smol/pull/1675) [`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32) Thanks @gijsbartman! - Fix consolePretty ignoring explicit colors option in non-TTY environments. + + When colors is explicitly set to true, prettyLoggerTty was still gating it with processStdoutIsTTY check, making it impossible to enable colors in non-TTY environments like Vite dev server. + +- [#1690](https://github.com/Effect-TS/effect-smol/pull/1690) [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f) Thanks @gcanti! - Fix `Cause.hasInterruptsOnly` to return `false` for empty causes. + +- [#1620](https://github.com/Effect-TS/effect-smol/pull/1620) [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4) Thanks @kitlangton! - Fix `TaggedUnion.match` to use `Unify` for return types, allowing + branches to return distinct Effect types that are properly merged. + +- [#1680](https://github.com/Effect-TS/effect-smol/pull/1680) [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5) Thanks @KhraksMamtsov! - make HttpClientResponse pipeable + +- [#1681](https://github.com/Effect-TS/effect-smol/pull/1681) [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103) Thanks @mikearnaldi! - Add an optional `message` field to `Effect.ignore` and `Effect.ignoreCause` for custom log output. + +- [#1695](https://github.com/Effect-TS/effect-smol/pull/1695) [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748) Thanks @gcanti! - Remove unused APIs from the `Utils` module. + +- [#1644](https://github.com/Effect-TS/effect-smol/pull/1644) [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b) Thanks @patroza! - fix: update Service interface to use 'this: void' in 'of' method signatures + +- [#1693](https://github.com/Effect-TS/effect-smol/pull/1693) [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268) Thanks @tim-smart! - fix cli subcommand context + +## 4.0.0-beta.29 + +### Patch Changes + +- [#1672](https://github.com/Effect-TS/effect-smol/pull/1672) [`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662) Thanks @gcanti! - Add `Newtype` module. + +- [#1677](https://github.com/Effect-TS/effect-smol/pull/1677) [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07) Thanks @gcanti! - Fix `Schema.isUUID` so the `version` parameter is optional in its public signature. + +- [#1667](https://github.com/Effect-TS/effect-smol/pull/1667) [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d) Thanks @tim-smart! - Preserve `Atom.withReactivity(...)` refresh behavior when registry initial values seed the wrapped atom. + +- [#1678](https://github.com/Effect-TS/effect-smol/pull/1678) [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f) Thanks @tim-smart! - Abort HTTP client requests when response streams are consumed only partially. + +- [#1665](https://github.com/Effect-TS/effect-smol/pull/1665) [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67) Thanks @tim-smart! - Remove placeholder fallback behavior from CLI prompt inputs now that default values are prefilled. + +## 4.0.0-beta.28 + +### Minor Changes + +- [#1637](https://github.com/Effect-TS/effect-smol/pull/1637) [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2) Thanks @tim-smart! - Add a new `effect/unstable/http/HttpStaticServer` module for static file serving with MIME resolution, directory index fallback, SPA fallback, and safe path resolution. + +### Patch Changes + +- [#1659](https://github.com/Effect-TS/effect-smol/pull/1659) [`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6) Thanks @tim-smart! - Persist MCP HTTP session and protocol headers after initialize so follow-up JSON-RPC requests include `MCP-Protocol-Version`. + +- [#1663](https://github.com/Effect-TS/effect-smol/pull/1663) [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556) Thanks @tim-smart! - Add `HttpServerResponse.fromClientResponse` for directly converting client responses into server responses. + +- [#1657](https://github.com/Effect-TS/effect-smol/pull/1657) [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa) Thanks @tim-smart! - Add `Ctrl-U` line clearing support to editable CLI prompts. + +- [#1645](https://github.com/Effect-TS/effect-smol/pull/1645) [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270) Thanks @gijsbartman! - ensure transformed Atom's don't extend idle ttl + +- [#1655](https://github.com/Effect-TS/effect-smol/pull/1655) [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834) Thanks @tim-smart! - Make `AtomRpc.query` and `AtomHttpApi.query` return serializable atoms by default when query results are schema-backed. + + The atom serialization key now uses each API's built-in request schemas so dehydrated state can be keyed consistently across server and client. + +- [#1662](https://github.com/Effect-TS/effect-smol/pull/1662) [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57) Thanks @tim-smart! - Add `HttpServerRequest.toClientRequest` for direct server-to-client request conversion. + +- [#1648](https://github.com/Effect-TS/effect-smol/pull/1648) [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc) Thanks @gcanti! - Fix `Types.VoidIfEmpty` to correctly detect empty object types. Remove deprecated `Types.MatchRecord` in favor of the simplified implementation, closes #1647. + +- [#1664](https://github.com/Effect-TS/effect-smol/pull/1664) [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8) Thanks @tim-smart! - Add `HttpServerRequest.fromClientRequest` for direct client-request-backed server request conversion. + +- [#1656](https://github.com/Effect-TS/effect-smol/pull/1656) [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf) Thanks @tim-smart! - Persist MCP client capability context across HTTP requests by resolving initialized payloads through the standard `Mcp-Session-Id` HTTP header in `McpServer`. + + Adds a regression test that initializes an MCP HTTP client, verifies the MCP server echoes `Mcp-Session-Id`, and then checks a later tool call can still read `McpServer.clientCapabilities`. + +- [#1639](https://github.com/Effect-TS/effect-smol/pull/1639) [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc) Thanks @tim-smart! - Add `Scheduler.PreventSchedulerYield` and expose it via `References` so fibers can skip scheduler `shouldYield` checks when needed. + +- [#1649](https://github.com/Effect-TS/effect-smol/pull/1649) [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e) Thanks @tim-smart! - Set `Schema.TaggedErrorClass` instance `name` to the tag value, matching `Data.TaggedError` behavior. + +- [#1646](https://github.com/Effect-TS/effect-smol/pull/1646) [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc) Thanks @tim-smart! - Simplify internal and documented request usage by passing request resolvers directly to `Effect.request` instead of wrapping them with `Effect.succeed`. + +- [#1641](https://github.com/Effect-TS/effect-smol/pull/1641) [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429) Thanks @tim-smart! - Don't transform Tool result schemas, as they aren't sent to the providers as + json schemas + +- [#1636](https://github.com/Effect-TS/effect-smol/pull/1636) [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70) Thanks @tim-smart! - Add `Cookies.expireCookie` / `expireCookieUnsafe` and `HttpServerResponse.expireCookie` / `expireCookieUnsafe` for emitting expired cookies. + +- [#1653](https://github.com/Effect-TS/effect-smol/pull/1653) [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a) Thanks @tim-smart! - expose mcp client capabilities + +- [#1660](https://github.com/Effect-TS/effect-smol/pull/1660) [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341) Thanks @tim-smart! - Add `HttpServerResponse.toClientResponse` for converting server responses into `HttpClientResponse` values. + +## 4.0.0-beta.27 + +### Patch Changes + +- [#1621](https://github.com/Effect-TS/effect-smol/pull/1621) [`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4) Thanks @kitlangton! - unstable/http Headers: add `removeMany` combinator for removing multiple headers at once + +- [#1622](https://github.com/Effect-TS/effect-smol/pull/1622) [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7) Thanks @tim-smart! - Add `Model.BooleanSqlite`, a model field schema that uses `0 | 1` encoding for database variants and plain `boolean` encoding for JSON variants. + +- [#1631](https://github.com/Effect-TS/effect-smol/pull/1631) [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142) Thanks @gcanti! - unstable/httpapi HttpApiBuilder: fix void responses producing a non-empty body instead of `Response.empty`, closes #1628. + +- [#1618](https://github.com/Effect-TS/effect-smol/pull/1618) [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7) Thanks @tim-smart! - Default `Effect.context()` to `Effect.context()` when no type parameter is provided. + +## 4.0.0-beta.26 + +### Patch Changes + +- [#1603](https://github.com/Effect-TS/effect-smol/pull/1603) [`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42) Thanks @tim-smart! - Add `responseText` to `AiError.StructuredOutputError` and populate it from `LanguageModel.generateObject` so failed structured output decodes include the full LLM text. + +- [#1613](https://github.com/Effect-TS/effect-smol/pull/1613) [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86) Thanks @lucas-barake! - Add `disableFatalDefects` to `RpcServer.layerHttp`, `RpcServer.toHttpEffect`, and `RpcServer.toHttpEffectWebsocket` option types to match existing runtime support. + +- [#1599](https://github.com/Effect-TS/effect-smol/pull/1599) [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07) Thanks @tim-smart! - add trait for customizing exit codes + +- [#1611](https://github.com/Effect-TS/effect-smol/pull/1611) [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f) Thanks @WebWalks! - Fixed the Error Type on AtomHttpApiClient (Server errors were being incorrectly reported, and we could not determine \_tag to handle) + +- [#1612](https://github.com/Effect-TS/effect-smol/pull/1612) [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc) Thanks @tim-smart! - Expose the optional `orElse` fallback parameter in `Effect.catchTags`. + +- [#1606](https://github.com/Effect-TS/effect-smol/pull/1606) [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99) Thanks @gcanti! - Schema: `toJsonSchemaDocument` now emits JSON Schema `false` for unannotated + `Never` index signatures (including `additionalProperties`) instead of `{ not: {} }`. + Annotated `Never` still emits a schema object so metadata like `description` is preserved. + +- [#1607](https://github.com/Effect-TS/effect-smol/pull/1607) [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f) Thanks @gcanti! - Schema: improve `Schema.Unknown` / `Schema.ObjectKeyword` handling in `toCodecJson` and `toCodecStringTree` + +- [#1616](https://github.com/Effect-TS/effect-smol/pull/1616) [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541) Thanks @lucas-barake! - Add `Atom.swr` to `effect/unstable/reactivity` for staleTime-gated stale-while-revalidate reads, optional mount and window-focus revalidation, and forceful manual refresh. + +- [#1600](https://github.com/Effect-TS/effect-smol/pull/1600) [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1) Thanks @tim-smart! - add args to Stdio service + +- [#1610](https://github.com/Effect-TS/effect-smol/pull/1610) [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446) Thanks @kitlangton! - Refine unstable CLI parent/subcommand flag composition. + - Add `Command.withSharedFlags` conflict validation against existing subcommands, including the `withSubcommands(...).withSharedFlags(...)` composition order. + - Reorder `Command` type parameters to `Command` for clearer parent-context modeling. + - Make `Command.withSubcommands` input typing sound for downstream input-based combinators by reflecting that subcommand paths only carry parent context input. + +- [#1604](https://github.com/Effect-TS/effect-smol/pull/1604) [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c) Thanks @lucas-barake! - Fix `unstable/sql/SqlSchema` request input typing so `findAll` and `findNonEmpty` accept `Request["Type"]` instead of `Request["Encoded"]`. + +- [#1602](https://github.com/Effect-TS/effect-smol/pull/1602) [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926) Thanks @tim-smart! - Replace the default HttpApi schema-validation error with `HttpApiError.BadRequestNoContent`. + +## 4.0.0-beta.25 + +### Patch Changes + +- [#1597](https://github.com/Effect-TS/effect-smol/pull/1597) [`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714) Thanks @tim-smart! - Fix `Effect.forkScoped` data-first typings to include `Scope` in requirements. + +- [#1598](https://github.com/Effect-TS/effect-smol/pull/1598) [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f) Thanks @tim-smart! - compare transaction connections by reference + +- [#1596](https://github.com/Effect-TS/effect-smol/pull/1596) [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63) Thanks @tim-smart! - improve HttpClient.withRateLimiter initial state tracking + +- [#1594](https://github.com/Effect-TS/effect-smol/pull/1594) [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d) Thanks @tim-smart! - HttpClient.withRateLimiter adds delay from retry-after headers + +## 4.0.0-beta.24 + +### Patch Changes + +- [#1586](https://github.com/Effect-TS/effect-smol/pull/1586) [`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9) Thanks @gcanti! - Schema: add `Chunk` schema, closes #1585. + +- [#1588](https://github.com/Effect-TS/effect-smol/pull/1588) [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399) Thanks @gcanti! - Fix `Schema.toTaggedUnion` discriminant detection for class-based schemas, including unique symbol tags, closes #1584. + +- [#1591](https://github.com/Effect-TS/effect-smol/pull/1591) [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0) Thanks @tim-smart! - Add `HttpClient.withRateLimiter` for integrating the `RateLimiter` service with HTTP clients, including optional response-header driven limit updates and automatic 429 retry behavior. + +- [#1583](https://github.com/Effect-TS/effect-smol/pull/1583) [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2) Thanks @patroza! - feat: Support Reference classes + +- [#1592](https://github.com/Effect-TS/effect-smol/pull/1592) [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385) Thanks @tim-smart! - Fix `HttpApi.prefix` so it updates endpoint path types the same way `HttpApiGroup.prefix` does. + +## 4.0.0-beta.23 + +### Patch Changes + +- [#1561](https://github.com/Effect-TS/effect-smol/pull/1561) [`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966) Thanks @gcanti! - SchemaRepresentation: only create references for recursive/mutually recursive schemas and schemas with an `identifier` annotation, closes #1560. + +## 4.0.0-beta.22 + +### Patch Changes + +- [#1578](https://github.com/Effect-TS/effect-smol/pull/1578) [`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479) Thanks @tim-smart! - Proxy function arity from `Effect.fn` APIs so wrapped functions preserve the original `length` value. + +- [#1580](https://github.com/Effect-TS/effect-smol/pull/1580) [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183) Thanks @tim-smart! - simplify Filter by removing Args type parameter + +- [#1575](https://github.com/Effect-TS/effect-smol/pull/1575) [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294) Thanks @tim-smart! - fix Chat constructor types + +- [#1581](https://github.com/Effect-TS/effect-smol/pull/1581) [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4) Thanks @tim-smart! - fix Duration.toMillis regression + +- [#1579](https://github.com/Effect-TS/effect-smol/pull/1579) [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b) Thanks @tim-smart! - Remove fiber-level keep-alive intervals and keep the process alive from `Runtime.makeRunMain` instead. + +- [#1571](https://github.com/Effect-TS/effect-smol/pull/1571) [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e) Thanks @tim-smart! - Add `HttpApiClient.urlBuilder` for type-safe endpoint URL construction from group + method/path keys. + +- [#1573](https://github.com/Effect-TS/effect-smol/pull/1573) [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95) Thanks @tim-smart! - Expose a `chunkSize` option on `Stream.fromIterable` to control emitted chunk boundaries when constructing streams from iterables. + +- [#1574](https://github.com/Effect-TS/effect-smol/pull/1574) [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939) Thanks @tim-smart! - Fix AI tool handler error typing so `LanguageModel.generateText` with a toolkit exposes wrapped `AiError` values rather than leaking raw `AiErrorReason` in the error channel. + +- [#1577](https://github.com/Effect-TS/effect-smol/pull/1577) [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b) Thanks @tim-smart! - fix VariantSchema.Union + +## 4.0.0-beta.21 + +### Patch Changes + +- [#1555](https://github.com/Effect-TS/effect-smol/pull/1555) [`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb) Thanks @tim-smart! - fix Stream.withSpan options + +- [#1548](https://github.com/Effect-TS/effect-smol/pull/1548) [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7) Thanks @effect-bot! - Fix `TxPubSub.publish` and `TxPubSub.publishAll` overloads to require `Effect.Transaction` in their return environment. + +- [#1557](https://github.com/Effect-TS/effect-smol/pull/1557) [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57) Thanks @A386official! - Fix MCP resource template parameter names resolving as `param0`, `param1` instead of actual names by checking `isParam` on the original schema before `toCodecStringTree` transformation. + +- [#1547](https://github.com/Effect-TS/effect-smol/pull/1547) [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28) Thanks @effect-bot! - Fix `Schedule.reduce` to persist state updates when the combine function returns a synchronous value. + +- [#1545](https://github.com/Effect-TS/effect-smol/pull/1545) [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55) Thanks @effect-bot! - Fix TupleWithRest post-rest validation to check each tail index sequentially. + +- [#1552](https://github.com/Effect-TS/effect-smol/pull/1552) [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2) Thanks @tim-smart! - Constrain `HttpServerRequest.source` to `object` and key server-side request weak caches by `request.source` so middleware request wrappers share the same cache entries. + +- [#1556](https://github.com/Effect-TS/effect-smol/pull/1556) [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e) Thanks @tim-smart! - rename WorkflowEngine.layer + +- [#1558](https://github.com/Effect-TS/effect-smol/pull/1558) [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb) Thanks @tim-smart! - Fix `Workflow.executionId` to use schema `makeUnsafe` instead of the removed `.make` API. + +- [#1553](https://github.com/Effect-TS/effect-smol/pull/1553) [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882) Thanks @kaylynb! - Fix spans never having parent span + +- [#1541](https://github.com/Effect-TS/effect-smol/pull/1541) [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007) Thanks @tim-smart! - Add `Effect.findFirst` and `Effect.findFirstFilter` for short-circuiting effectful searches over iterables. + +## 4.0.0-beta.20 + +### Patch Changes + +- [#1533](https://github.com/Effect-TS/effect-smol/pull/1533) [`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74) Thanks @tim-smart! - move ChildProcess apis into spawner service + +- [#1536](https://github.com/Effect-TS/effect-smol/pull/1536) [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862) Thanks @tim-smart! - add Context.Key type, used a base for Context.Service and Context.Reference + +- [#1531](https://github.com/Effect-TS/effect-smol/pull/1531) [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c) Thanks @gcanti! - Revert `Config.withDefault` to v3 behavior, closes #1530. + + Make `Config.withDefault` accept an eager value instead of `LazyArg`, aligning with CLI module conventions. + +- [#1535](https://github.com/Effect-TS/effect-smol/pull/1535) [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732) Thanks @tim-smart! - change default ErrorReporter severity to Info + +- [#1529](https://github.com/Effect-TS/effect-smol/pull/1529) [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8) Thanks @tim-smart! - Add dedicated AiError metadata interfaces per reason so provider packages can safely augment metadata without conflicting module declarations. + +- [#1531](https://github.com/Effect-TS/effect-smol/pull/1531) [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c) Thanks @gcanti! - Fix `Config.withDefault` type inference, closes #1530. + +- [#1528](https://github.com/Effect-TS/effect-smol/pull/1528) [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34) Thanks @tim-smart! - Add `Model.ModelName` and provide it from AI model constructors. + +- [#1537](https://github.com/Effect-TS/effect-smol/pull/1537) [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827) Thanks @tim-smart! - Revert `Effect.partition` to Effect v3 behavior by accumulating failures from the effect error channel and never failing. + +## 4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Minor Changes + +- [#1515](https://github.com/Effect-TS/effect-smol/pull/1515) [`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8) Thanks @mikearnaldi! - Add transactional STM modules: TxDeferred, TxPriorityQueue, TxPubSub, TxReentrantLock, TxSubscriptionRef. + + Refactor transaction model: remove `Effect.atomic`/`Effect.atomicWith`, add `Effect.withTxState`. All Tx operations now return `Effect` requiring explicit `Effect.transaction(...)` at boundaries. + + Expose `TxPubSub.acquireSubscriber`/`releaseSubscriber` for composable transaction boundaries. Fix `TxSubscriptionRef.changes` race condition ensuring current value is delivered first. + + Remove `TxRandom` module. + +### Patch Changes + +- [#1518](https://github.com/Effect-TS/effect-smol/pull/1518) [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5) Thanks @IMax153! - Fix `Command.withGlobalFlags` type inference when mixing `GlobalFlag.action` and `GlobalFlag.setting`. + + `Setting` service identifiers are now correctly removed from command requirements in mixed global flag arrays. + +- [#1520](https://github.com/Effect-TS/effect-smol/pull/1520) [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d) Thanks @IMax153! - Ensure that OpenAI JSON schemas for tool calls and structured outputs are properly transformed + +## 4.0.0-beta.17 + +### Patch Changes + +- [#1516](https://github.com/Effect-TS/effect-smol/pull/1516) [`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781) Thanks @gcanti! - Fix `Schema.encodeKeys` to encode non-remapped struct fields during encoding. + +## 4.0.0-beta.16 + +### Patch Changes + +- [#1513](https://github.com/Effect-TS/effect-smol/pull/1513) [`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a) Thanks @gcanti! - Add `SchemaParser.makeOption` and `Schema.makeOption` for constructing schema values as `Option`. + +- [#1508](https://github.com/Effect-TS/effect-smol/pull/1508) [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b) Thanks @gcanti! - Schema: add `OptionFromUndefinedOr` and `OptionFromNullishOr` schemas. + +- [#1498](https://github.com/Effect-TS/effect-smol/pull/1498) [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb) Thanks @kaylynb! - Fix OpenApi Multipart file upload schema generation + +## 4.0.0-beta.15 + +### Patch Changes + +- [#1500](https://github.com/Effect-TS/effect-smol/pull/1500) [`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba) Thanks @qadama831! - Unwrap `_Success` schema to enable field access. + +- [#1486](https://github.com/Effect-TS/effect-smol/pull/1486) [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e) Thanks @tim-smart! - Fix `Stream.groupedWithin` to stop emitting empty arrays when schedule ticks fire while upstream is idle. + +- [#1503](https://github.com/Effect-TS/effect-smol/pull/1503) [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b) Thanks @tim-smart! - allow creating standalone http handlers from HttpApiEndpoints + +- [#1499](https://github.com/Effect-TS/effect-smol/pull/1499) [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28) Thanks @tim-smart! - fix atom node timeout cleanup + +- [#1494](https://github.com/Effect-TS/effect-smol/pull/1494) [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566) - Refine `ExtractServices` to omit tool handler requirements when automatic tool resolution is explicitly disabled through the `disableToolCallResolution` option. + +- [#1496](https://github.com/Effect-TS/effect-smol/pull/1496) [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2) Thanks @IMax153! - Refactor unstable CLI global flags to command-scoped declarations. + + ### Breaking changes + - Remove `GlobalFlag.add`, `GlobalFlag.remove`, and `GlobalFlag.clear` + - Add `Command.withGlobalFlags(...)` as the declaration API for command/subcommand scope + - Change `GlobalFlag.setting` constructor to curried form which carries type-level identifier: + - before: `GlobalFlag.setting({ flag, ... })` + - after: `GlobalFlag.setting("id")({ flag })` + - Change setting context identity to a stable type-level string: + - `effect/unstable/cli/GlobalFlag/${id}` + + ### Behavior changes + - Global flags are now scoped by command path (root-to-leaf declarations) + - Out-of-scope global flags are rejected for the selected subcommand path + - Help now renders only global flags active for the requested command path + - Setting defaults are sourced from `Flag` combinators (`optional`, `withDefault`) rather than setting constructor defaults + +## 4.0.0-beta.14 + +### Patch Changes + +- [#1471](https://github.com/Effect-TS/effect-smol/pull/1471) [`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350) Thanks @IMax153! - Make CLI global settings directly yieldable and simplify built-in names. + + `GlobalFlag.setting` now takes `{ flag, defaultValue }` and returns a setting that is a `Context.Reference`, so handlers and `Command.provide*` effects can `yield*` global setting values directly. + + Built-in settings keep internal behavior in `runWith` (for example, `--log-level` still configures `References.MinimumLogLevel`) while also being readable as values. + + Also renamed built-in globals: + - `GlobalFlag.CompletionsFlag` -> `GlobalFlag.Completions` + - `GlobalFlag.LogLevelFlag` -> `GlobalFlag.LogLevel` + +- [#1490](https://github.com/Effect-TS/effect-smol/pull/1490) [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9) Thanks @gcanti! - Fix `OpenApi.fromApi` preserving multiple response content types for one status code, closes #1485. + +## 4.0.0-beta.13 + +### Patch Changes + +- [#1454](https://github.com/Effect-TS/effect-smol/pull/1454) [`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd) Thanks @lucas-barake! - Expose `NoSuchElementError` in the error type of stream-based `Atom.make` overloads. + +- [#1469](https://github.com/Effect-TS/effect-smol/pull/1469) [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b) Thanks @tim-smart! - Update unstable schema variant helpers to use array-based arguments for `FieldOnly`, `FieldExcept`, and `Union`, aligning `VariantSchema` and `Model` with other v4 API shapes. + +- [#1457](https://github.com/Effect-TS/effect-smol/pull/1457) [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562) Thanks @tim-smart! - Run request resolver batch fibers with request services by using `Effect.runForkWith`, so resolver delay effects and `runAll` execution see the request service map. + +- [#1461](https://github.com/Effect-TS/effect-smol/pull/1461) [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2) Thanks @mikearnaldi! - Fix `Schedule.fixed` double-executing the effect due to clock jitter. + + The `elapsedSincePrevious > window` check included sleep time from the + previous step, so any timer imprecision (e.g. 1001ms for a 1000ms sleep) + triggered an immediate zero-delay re-execution. + +- [#1464](https://github.com/Effect-TS/effect-smol/pull/1464) [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa) Thanks @gcanti! - Use the `identifier` annotation as the expected message when available, closes #1458. + +- [#1475](https://github.com/Effect-TS/effect-smol/pull/1475) [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08) Thanks @tim-smart! - Add a CI check job that runs `pnpm ai-docgen` and fails if it produces uncommitted changes. + +- [#1448](https://github.com/Effect-TS/effect-smol/pull/1448) [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de) Thanks @IMax153! - Refactor CLI built-in options to use Effect services with `GlobalFlag` + + Built-in CLI flags (`--help`, `--version`, `--completions`, `--log-level`) are now implemented as Effect services using `Context.Reference`. This provides: + - **Visibility**: Built-in flags now appear in help output's "GLOBAL FLAGS" section + - **Extensibility**: Users can register custom global flags via `GlobalFlag.add` + - **Override capability**: Built-in flag behavior can be replaced or disabled + - **Composability**: Flags compose via Effect's service system + + New `GlobalFlag` module exports: + - `Action` and `Setting` types for different flag behaviors + - `Help`, `Version`, `Completions`, `LogLevel` references for built-in flags + - `add`, `remove`, `clear` functions for managing global flags + + Example: + + ```typescript + const app = Command.make("myapp"); + Command.run(app, { version: "1.0.0" }).pipe( + GlobalFlag.add(CustomFlag, customFlagValue), + ); + ``` + +- [#1468](https://github.com/Effect-TS/effect-smol/pull/1468) [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb) Thanks @lucas-barake! - Fix `Rpc.ExtractProvides` to use middleware service ID instead of constructor type. + +- [#1465](https://github.com/Effect-TS/effect-smol/pull/1465) [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f) Thanks @lloydrichards! - tighten Schema on \_meta fields in McpSchema; closes #1463 + +- [#1470](https://github.com/Effect-TS/effect-smol/pull/1470) [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab) Thanks @tim-smart! - Add `Command.withAlias` for unstable CLI commands, including subcommand parsing by alias and help output that renders aliases as `name, alias` in subcommand listings. + +## 4.0.0-beta.12 + +### Patch Changes + +- [#1439](https://github.com/Effect-TS/effect-smol/pull/1439) [`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07) Thanks @gcanti! - Add `Config.nested` combinator to scope a config under a named prefix, closes #1437. + +- [#1452](https://github.com/Effect-TS/effect-smol/pull/1452) [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857) Thanks @tim-smart! - make fiber keepAlive setInterval evaluation lazy + +- [#1431](https://github.com/Effect-TS/effect-smol/pull/1431) [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb) Thanks @tim-smart! - Add `Random.nextBoolean` for generating random boolean values. + +- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts + +- [#1445](https://github.com/Effect-TS/effect-smol/pull/1445) [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5) Thanks @mattiamanzati! - Fix `Graph.toMermaid` to escape special characters using HTML entity codes per the Mermaid specification. + +- [#1443](https://github.com/Effect-TS/effect-smol/pull/1443) [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca) Thanks @mikearnaldi! - Fix `HttpClient.retryTransient` autocomplete leaking `Schedule` internals by splitting the `{...} | Schedule` union into separate overloads. + +- [#1444](https://github.com/Effect-TS/effect-smol/pull/1444) [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc) Thanks @gcanti! - Schema.encodeKeys: relax input constraint from Struct to schemas with fields so Schema.Class works, closes #1412. + +- [#1438](https://github.com/Effect-TS/effect-smol/pull/1438) [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2) Thanks @mikearnaldi! - Atom.searchParam: decode initial URL values correctly when a schema is provided + +- [#1425](https://github.com/Effect-TS/effect-smol/pull/1425) [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb) Thanks @candrewlee14! - Fix LanguageModel stripping of resolved approval artifacts across multi-round conversations. + + Previously, `stripResolvedApprovals` only ran when there were pending approvals + in the current round. Stale artifacts from earlier rounds would leak to the + provider, causing errors. The stripping now runs unconditionally. + + In streaming mode, pre-resolved tool results are also emitted as stream parts + so `Chat.streamText` persists them to history, preventing re-resolution on + subsequent rounds. + +- [#1453](https://github.com/Effect-TS/effect-smol/pull/1453) [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160) Thanks @tim-smart! - allow mcp errors to be encoded correctly + +- [#1440](https://github.com/Effect-TS/effect-smol/pull/1440) [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882) Thanks @lloydrichards! - extend McpSchema to work with extensions + +- [#1447](https://github.com/Effect-TS/effect-smol/pull/1447) [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a) Thanks @tim-smart! - remove all non-regional service usage + +- [#1451](https://github.com/Effect-TS/effect-smol/pull/1451) [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e) Thanks @tim-smart! - Add `Effect.annotateLogsScoped` to apply log annotations for the current scope and automatically restore previous annotations when the scope closes. + +- [#1434](https://github.com/Effect-TS/effect-smol/pull/1434) [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd) Thanks @tim-smart! - Fix JSON-RPC serialization to return an object for non-batched requests while preserving array responses for true batch requests. + +## 4.0.0-beta.11 + +### Patch Changes + +- [#1429](https://github.com/Effect-TS/effect-smol/pull/1429) [`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16) Thanks @tim-smart! - Add grouped subcommand support to `Command.withSubcommands`, including help output sections for named groups while keeping ungrouped commands under `SUBCOMMANDS`. + +- [#1426](https://github.com/Effect-TS/effect-smol/pull/1426) [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199) Thanks @tim-smart! - Add `Effect.validate` for validating collections while accumulating all failures, equivalent to the v3 `Effect.validateAll` behavior. + +- [#1430](https://github.com/Effect-TS/effect-smol/pull/1430) [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310) Thanks @tim-smart! - Add `Command.withExamples` to attach concrete usage examples to CLI commands, expose them through `HelpDoc.examples`, and render them in the default help formatter. + +- [#1415](https://github.com/Effect-TS/effect-smol/pull/1415) [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c) Thanks @mikearnaldi! - HashMap: compare HAMT bit positions as unsigned to preserve entry lookup when bit 31 is set + +- [#1417](https://github.com/Effect-TS/effect-smol/pull/1417) [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814) Thanks @mikearnaldi! - unstable/http Headers: hide inspectable prototype methods from for..in iteration to avoid invalid header names in runtime fetch polyfills + +- [#1418](https://github.com/Effect-TS/effect-smol/pull/1418) [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3) Thanks @mikearnaldi! - runtime: guard keepAlive setInterval / clearInterval so Effect.runPromise works in runtimes that block timer APIs + +- [#1416](https://github.com/Effect-TS/effect-smol/pull/1416) [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1) Thanks @mikearnaldi! - Queue.collect: stop duplicating drained messages by appending each batch once + +- [#1413](https://github.com/Effect-TS/effect-smol/pull/1413) [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997) Thanks @gcanti! - Fix `Schema.TupleWithRest` incorrectly accepting inputs with missing post-rest elements, closes #1410. + +- [#1409](https://github.com/Effect-TS/effect-smol/pull/1409) [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5) Thanks @tim-smart! - add ErrorReporter module + +- [#1427](https://github.com/Effect-TS/effect-smol/pull/1427) [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49) Thanks @tim-smart! - Add `Command.annotate` and `Command.annotateMerge` to unstable CLI commands, and include command annotations in `HelpDoc` so custom help formatters can access command metadata. + +- [#1401](https://github.com/Effect-TS/effect-smol/pull/1401) [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0) Thanks @tim-smart! - Add `WorkflowEngine.layer`, an in-memory layer for the unstable workflow engine. + +- [#1428](https://github.com/Effect-TS/effect-smol/pull/1428) [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8) Thanks @tim-smart! - Add `Command.withShortDescription` and use short descriptions for CLI subcommand listings, with fallback to the full command description. + +- [#1405](https://github.com/Effect-TS/effect-smol/pull/1405) [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5) Thanks @candrewlee14! - Strip resolved tool approval artifacts from prompt before sending to provider, preventing errors when providers reject pre-resolved approval requests. + +- [#1424](https://github.com/Effect-TS/effect-smol/pull/1424) [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e) Thanks @tim-smart! - expose more atom Node properties + +## 4.0.0-beta.10 + +### Patch Changes + +- [#1396](https://github.com/Effect-TS/effect-smol/pull/1396) [`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74) Thanks @gcanti! - Add `unstable/encoding` subpath export. + +- [#1392](https://github.com/Effect-TS/effect-smol/pull/1392) [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4) Thanks @tim-smart! - Fix a race in `Semaphore.take` where interruption could leak permits after a waiter was resumed. + +- [#1388](https://github.com/Effect-TS/effect-smol/pull/1388) [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a) Thanks @tim-smart! - Export `Effect` do notation APIs (`Do`, `bindTo`, `bind`, and `let`) from `effect/Effect` and add runtime and type-level coverage. + +- [#1387](https://github.com/Effect-TS/effect-smol/pull/1387) [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36) Thanks @tim-smart! - short circuit when Fiber.joinAll is called with an empty iterable + +- [#1386](https://github.com/Effect-TS/effect-smol/pull/1386) [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2) Thanks @tim-smart! - simplify http logger disabling + +- [#1381](https://github.com/Effect-TS/effect-smol/pull/1381) [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2) Thanks @tim-smart! - Fix `UrlParams.Input` usage to accept interface-typed records in HTTP client and server helpers while keeping coercion constraints for url parameter values. + +- [#1383](https://github.com/Effect-TS/effect-smol/pull/1383) [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab) Thanks @tim-smart! - Rename `HttpClient.retryTransient` option `mode` to `retryOn` and rename `"both"` to `"errors-and-responses"`. + +- [#1399](https://github.com/Effect-TS/effect-smol/pull/1399) [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8) Thanks @tim-smart! - Add `Random.shuffle` to shuffle iterables with seeded randomness support. + +## 4.0.0-beta.9 + +### Patch Changes + +- [#1376](https://github.com/Effect-TS/effect-smol/pull/1376) [`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350) Thanks @gcanti! - HttpApiEndpoint: relax `params`, `query`, and `headers` constraints to accept a full schema in addition to a record of fields. + +- [#1379](https://github.com/Effect-TS/effect-smol/pull/1379) [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422) Thanks @tim-smart! - Fix `AtomHttpApi.query` to forward v4 `params` / `query` request fields to `HttpApiClient` at runtime. + Also align `AtomHttpApi` endpoint type inference with v4 `HttpApiEndpoint` params/query naming and add a regression test. + +## 4.0.0-beta.8 + +### Patch Changes + +- [#1371](https://github.com/Effect-TS/effect-smol/pull/1371) [`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752) Thanks @IMax153! - Fix `ChildProcess` options type and implement `PgMigrator` + +- [#1372](https://github.com/Effect-TS/effect-smol/pull/1372) [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df) Thanks @pawelblaszczyk5! - Remove superfluous error from SqlSchema.findAll signature + +## 4.0.0-beta.7 + +### Patch Changes + +- [#1366](https://github.com/Effect-TS/effect-smol/pull/1366) [`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a) Thanks @tim-smart! - rename SqlSchema.findOne\* apis + +- [#1360](https://github.com/Effect-TS/effect-smol/pull/1360) [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1) Thanks @tim-smart! - Add `Schedule.jittered` to randomize schedule delays between 80% and 120% of the original delay. + +- [#1364](https://github.com/Effect-TS/effect-smol/pull/1364) [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd) Thanks @gcanti! - Schema: avoid eager resolution for type-level helpers, closes #1332 + +- [#1369](https://github.com/Effect-TS/effect-smol/pull/1369) [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf) Thanks @tim-smart! - align HttpClientRequest constructors with http method names + +- [#1369](https://github.com/Effect-TS/effect-smol/pull/1369) [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf) Thanks @tim-smart! - remove body restriction for HttpClientRequest's + +- [#1358](https://github.com/Effect-TS/effect-smol/pull/1358) [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f) Thanks @tim-smart! - Add `LogLevel.isEnabled` for checking a log level against `References.MinimumLogLevel`. + +- [#1363](https://github.com/Effect-TS/effect-smol/pull/1363) [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430) Thanks @tim-smart! - rename DurationInput to Duration.Input + +- [#1363](https://github.com/Effect-TS/effect-smol/pull/1363) [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430) Thanks @tim-smart! - DateTime.distance now returns a Duration + +- [#1367](https://github.com/Effect-TS/effect-smol/pull/1367) [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e) Thanks @tim-smart! - refactor SqlSchema apis + +- [#1363](https://github.com/Effect-TS/effect-smol/pull/1363) [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430) Thanks @tim-smart! - remove rpc client nesting to improve type performance + +## 4.0.0-beta.6 + +### Patch Changes + +- [#1338](https://github.com/Effect-TS/effect-smol/pull/1338) [`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1) Thanks @Leka74! - Add `showOperationId` to `HttpApiScalar.ScalarConfig`. + +- [#1326](https://github.com/Effect-TS/effect-smol/pull/1326) [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b) Thanks @gcanti! - Schema: add `BigDecimal` schema with comparison checks (`isGreaterThanBigDecimal`, `isGreaterThanOrEqualToBigDecimal`, `isLessThanBigDecimal`, `isLessThanOrEqualToBigDecimal`, `isBetweenBigDecimal`). + +- [#1328](https://github.com/Effect-TS/effect-smol/pull/1328) [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85) Thanks @gcanti! - Schema: add `DateTimeZoned`, `TimeZoneOffset`, `TimeZoneNamed`, and `TimeZone` schemas. + +- [#1325](https://github.com/Effect-TS/effect-smol/pull/1325) [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5) Thanks @KhraksMamtsov! - Make `Data.Class`, `Data.TaggedClass`, and `Cause.YieldableError` pipeable. + +- [#1323](https://github.com/Effect-TS/effect-smol/pull/1323) [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253) Thanks @KhraksMamtsov! - Port `Pipeable.Class` from v3. + + ```ts + class MyClass extends Pipeable.Class() { + constructor(public a: number) { + super(); + } + methodA() { + return this.a; + } + } + console.log(new MyClass(2).pipe((x) => x.methodA())); // 2 + ``` + + ```ts + class A { + constructor(public a: number) {} + methodA() { + return this.a; + } + } + class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length); + } + methodB() { + return [this.b, this.methodA()]; + } + } + console.log(new B("pipe").pipe((x) => x.methodB())); // ['pipe', 4] + ``` + +- [#1337](https://github.com/Effect-TS/effect-smol/pull/1337) [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b) Thanks @IMax153! - Encoding: consolidate `effect/encoding` sub-modules (Base64, Base64Url, Hex, EncodingError) into a top-level `Encoding` module. Functions are now prefixed: `encodeBase64`, `decodeBase64`, `encodeHex`, `decodeHex`, etc. The `effect/encoding` sub-path export is removed. + +- [#1351](https://github.com/Effect-TS/effect-smol/pull/1351) [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8) Thanks @tim-smart! - add `Schema.HashSet` for decoding and encoding `HashSet` values. + +- [#1336](https://github.com/Effect-TS/effect-smol/pull/1336) [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d) Thanks @mikearnaldi! - Extract `Semaphore` and `Latch` into their own modules. + + `Semaphore.make` / `Semaphore.makeUnsafe` replace `Effect.makeSemaphore` / `Effect.makeSemaphoreUnsafe`. + `Latch.make` / `Latch.makeUnsafe` replace `Effect.makeLatch` / `Effect.makeLatchUnsafe`. + + Merge `PartitionedSemaphore` into `Semaphore` as `Semaphore.Partitioned`, `Semaphore.makePartitioned`, `Semaphore.makePartitionedUnsafe`. + +- [#1345](https://github.com/Effect-TS/effect-smol/pull/1345) [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4) Thanks @tim-smart! - allocate less effects when reading a file + +- [#1350](https://github.com/Effect-TS/effect-smol/pull/1350) [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c) Thanks @tim-smart! - Add "Previously Known As" JSDoc migration notes for the `Semaphore` and `Latch` APIs extracted from `Effect`. + +- [#1355](https://github.com/Effect-TS/effect-smol/pull/1355) [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906) Thanks @tim-smart! - ensure non-middleware http errors are correctly handled + +- [#1352](https://github.com/Effect-TS/effect-smol/pull/1352) [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f) Thanks @tim-smart! - Add `Schema.HashMap` for decoding and encoding `HashMap` values. + +- [#1347](https://github.com/Effect-TS/effect-smol/pull/1347) [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a) Thanks @tim-smart! - use .toJSON for default .toString implementations + +- [#1329](https://github.com/Effect-TS/effect-smol/pull/1329) [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4) Thanks @gcanti! - Schema: extract shared `dateTimeUtcFromString` transformation for `DateTimeUtc` and `DateTimeUtcFromString`. + +- [#1318](https://github.com/Effect-TS/effect-smol/pull/1318) [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11) Thanks @gcanti! - Schema: rename `$` suffix to `$` prefix for type-level identifiers that conflict with built-in names (`Array$` → `$Array`, `Record$` → `$Record`, `ReadonlyMap$` → `$ReadonlyMap`, `ReadonlySet$` → `$ReadonlySet`). + +- [#1356](https://github.com/Effect-TS/effect-smol/pull/1356) [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d) Thanks @tim-smart! - allow passing void for request constructors + +- [#1348](https://github.com/Effect-TS/effect-smol/pull/1348) [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005) Thanks @tim-smart! - Fix `Schedule.andThenResult` to initialize the right schedule only after the left schedule completes. + This removes the extra immediate transition tick and correctly completes when the right schedule is finite. + +- [#1320](https://github.com/Effect-TS/effect-smol/pull/1320) [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe) Thanks @gcanti! - Struct: add `Struct.Record` constructor for creating records with the given keys and value. + +- [#1342](https://github.com/Effect-TS/effect-smol/pull/1342) [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894) Thanks @cevr! - `Schema.TaggedErrorClass`, `Schema.Class`, and `Schema.ErrorClass` constructors now allow omitting the props argument when all fields have constructor defaults (e.g. `new MyError()` instead of `new MyError({})`). + +- [#1322](https://github.com/Effect-TS/effect-smol/pull/1322) [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0) Thanks @tim-smart! - Add a `requireServicesAt` option to `PersistedCache.make` so lookup-service requirements can be configured like `Cache`. + +## 4.0.0-beta.5 + +### Patch Changes + +- [#1317](https://github.com/Effect-TS/effect-smol/pull/1317) [`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600) Thanks @tim-smart! - support tag unions in Effect.catchTag/Reason + +- [#1314](https://github.com/Effect-TS/effect-smol/pull/1314) [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8) Thanks @zeyuri! - Fix `Atom.serializable` encode/decode for wire transfer. + + Use `Schema.toCodecJson` instead of `Schema.encodeSync`/`Schema.decodeSync` directly, so that encoded values are plain JSON objects that survive serialization roundtrips (JSON, seroval, etc.). Previously, `AsyncResult.Schema` encode produced instances with custom prototypes that were lost after wire transfer, causing decode to fail with "Expected AsyncResult" errors during SSR hydration. + +- [#1315](https://github.com/Effect-TS/effect-smol/pull/1315) [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083) Thanks @tim-smart! - add Filter.reason api + +- [#1314](https://github.com/Effect-TS/effect-smol/pull/1314) [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8) Thanks @zeyuri! - Port ReactHydration to effect-smol. + + Add `Hydration` module to `effect/unstable/reactivity` with `dehydrate`, `hydrate`, and `toValues` for SSR state serialization. Add `HydrationBoundary` React component to `@effect/atom-react` with two-phase hydration (new atoms in render, existing atoms after commit). + +## 4.0.0-beta.4 + +### Patch Changes + +- [#1308](https://github.com/Effect-TS/effect-smol/pull/1308) [`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32) Thanks @tim-smart! - improve Schema.TaggedUnion .match auto completion + +- [#1310](https://github.com/Effect-TS/effect-smol/pull/1310) [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c) Thanks @tim-smart! - Add `Schedule.duration`, a one-shot schedule that waits for the provided duration and then completes. + +## 4.0.0-beta.3 + +### Patch Changes + +- [#1303](https://github.com/Effect-TS/effect-smol/pull/1303) [`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530) Thanks @tim-smart! - add Result.failVoid + +- [#1307](https://github.com/Effect-TS/effect-smol/pull/1307) [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e) Thanks @tim-smart! - Add `HttpClientRequest.bodyFormDataRecord` and `HttpBody.makeFormDataRecord` helpers for creating multipart form bodies from plain records. + +## 4.0.0-beta.2 + +### Patch Changes + +- [#1302](https://github.com/Effect-TS/effect-smol/pull/1302) [`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0) Thanks @tim-smart! - allow undefined for VariantSchema.Overridable input + +- [#1299](https://github.com/Effect-TS/effect-smol/pull/1299) [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78) Thanks @tim-smart! - Port `SqlSchema.findOne` from effect v3 to return `Option` on empty results and add `SqlSchema.single` for the fail-on-empty behavior. + +- [#1298](https://github.com/Effect-TS/effect-smol/pull/1298) [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae) Thanks @tim-smart! - Add `Effect.catchNoSuchElement`, a renamed port of v3 `Effect.optionFromOptional` that converts `NoSuchElementError` failures into `Option.none`. + +## 4.0.0-beta.1 + +### Patch Changes + +- [#1293](https://github.com/Effect-TS/effect-smol/pull/1293) [`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743) Thanks @mikearnaldi! - Add `Effect.filter` support for synchronous `Filter.Filter` overloads and correctly handle non-effect `Result` return values at runtime. + +- [#1294](https://github.com/Effect-TS/effect-smol/pull/1294) [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43) Thanks @tim-smart! - Fix `Prompt.text` and related text prompts to initialize from `default` values so users can edit the default input directly. + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta diff --git a/repos/effect/packages/effect/CONFIG.md b/repos/effect/packages/effect/CONFIG.md new file mode 100644 index 0000000..1587686 --- /dev/null +++ b/repos/effect/packages/effect/CONFIG.md @@ -0,0 +1,692 @@ +# Configuration in Effect + +This guide shows you how to load and validate configuration in an Effect application. Two modules work together: + +- **`ConfigProvider`** — reads raw data from a source (environment variables, JSON objects, `.env` files, directory trees). +- **`Config`** — describes what shape and types you expect, then decodes the raw data into typed values. + +You describe _what_ you need with `Config`, and the library figures out _how_ to read and validate it using a `ConfigProvider`. + +## Getting Started + +### Reading a Single Value + +The simplest case: read one value from an environment variable. + +```ts +import { Config, Effect } from "effect" + +const program = Effect.gen(function*() { + const host = yield* Config.string("HOST") + console.log(host) +}) + +Effect.runSync(program) +// reads HOST from process.env +``` + +When you yield a `Config` inside `Effect.gen`, it automatically uses the default `ConfigProvider` (which reads from `process.env`). + +### Reading Multiple Values + +Use `Config.all` to group related keys: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const dbConfig = Config.all({ + host: Config.string("host"), + port: Config.int("port") +}) + +const provider = ConfigProvider.fromUnknown({ + host: "localhost", + port: 5432 +}) + +const result = Effect.runSync(dbConfig.parse(provider)) +// { host: "localhost", port: 5432 } +``` + +### Reading Structured Config with a Schema + +For larger configs, use `Config.schema` with a `Schema.Struct`: + +```ts +import { Config, ConfigProvider, Effect, Schema } from "effect" + +const AppConfig = Config.schema( + Schema.Struct({ + host: Schema.String, + port: Schema.Int, + debug: Schema.Boolean + }) +) + +const provider = ConfigProvider.fromUnknown({ + host: "localhost", + port: 8080, + debug: true +}) + +const result = Effect.runSync(AppConfig.parse(provider)) +// { host: "localhost", port: 8080, debug: true } +``` + +The schema automatically decodes raw string values into their target types. For example, when reading from environment variables, `"8080"` becomes the number `8080` and `"true"` becomes the boolean `true`. + +## Config Constructors + +Each constructor reads a single value and decodes it into the appropriate type. + +| Constructor | Decoded type | Notes | +| ------------------------------ | ------------------ | ------------------------------------------------------------------------ | +| `Config.string(name?)` | `string` | Any string | +| `Config.nonEmptyString(name?)` | `string` | Rejects `""` | +| `Config.number(name?)` | `number` | Includes `NaN`, `Infinity` | +| `Config.finite(name?)` | `number` | Rejects `NaN` and `Infinity` | +| `Config.int(name?)` | `number` | Integers only | +| `Config.boolean(name?)` | `boolean` | Accepts `true/false`, `yes/no`, `on/off`, `1/0`, `y/n` | +| `Config.port(name?)` | `number` | Integer in 1–65535 | +| `Config.url(name?)` | `URL` | Parsed via the `URL` constructor | +| `Config.date(name?)` | `Date` | Rejects invalid dates | +| `Config.duration(name?)` | `Duration` | Parses `"10 seconds"`, `"500 millis"`, `"Infinity"`, `"-Infinity"`, etc. | +| `Config.logLevel(name?)` | `string` | One of `All`, `Fatal`, `Error`, `Warn`, `Info`, `Debug`, `Trace`, `None` | +| `Config.redacted(name?)` | `Redacted` | Hidden from logs and `toString` | +| `Config.literal(value, name?)` | literal type | Accepts only the given literal | + +The optional `name` parameter sets the local path segment for lookup. If the config is wrapped with `Config.nested`, the nested prefix is prepended to this local path. Omit `name` when the config should decode the provider root. + +## Config Combinators + +### `Config.withDefault` — Fallback for Missing Keys + +Only triggers when data is missing. Validation errors (wrong type, out of range) still propagate. + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const port = Config.int("port").pipe(Config.withDefault(3000)) + +const provider = ConfigProvider.fromUnknown({}) +Effect.runSync(port.parse(provider)) // 3000 +``` + +### `Config.option` — Optional Values + +Returns `Option.some(value)` on success and `Option.none()` when data is missing. + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const maybePort = Config.option(Config.int("port")) + +const provider = ConfigProvider.fromUnknown({}) +Effect.runSync(maybePort.parse(provider)) // { _tag: "None" } +``` + +### `Config.map` — Transform a Value + +```ts +import { Config } from "effect" + +const upperHost = Config.string("HOST").pipe( + Config.map((s) => s.toUpperCase()) +) +``` + +### `Config.orElse` — Fallback on Any Error + +Unlike `withDefault`, this catches **all** `ConfigError`s: + +```ts +import { Config } from "effect" + +const host = Config.string("HOST").pipe( + Config.orElse(() => Config.succeed("localhost")) +) +``` + +### `Config.nested` — Scope a Config Under a Prefix + +Prepends a logical path segment to every key the inner config reads. The prefix is used for both provider lookups and schema error paths: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const dbConfig = Config.all({ + host: Config.string("host"), + port: Config.int("port") +}).pipe(Config.nested("database")) + +const provider = ConfigProvider.fromUnknown({ + database: { host: "localhost", port: 5432 } +}) + +Effect.runSync(dbConfig.parse(provider)) +// { host: "localhost", port: 5432 } +``` + +With environment variables, nesting uses `_` as separator: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const host = Config.string("host").pipe(Config.nested("database")) + +const provider = ConfigProvider.fromEnv({ + env: { database_host: "localhost" } +}) + +Effect.runSync(host.parse(provider)) // "localhost" +``` + +Multiple `Config.nested` calls compose with the outermost prefix first: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const config = Config.string("host").pipe( + Config.nested("database"), + Config.nested("production") +) + +const provider = ConfigProvider.fromUnknown({ + production: { + database: { + host: "localhost" + } + } +}) + +Effect.runSync(config.parse(provider)) // "localhost" +``` + +### `Config.all` — Combine Multiple Configs + +Accepts a record or a tuple: + +```ts +import { Config } from "effect" + +// As a record +const appConfig = Config.all({ + host: Config.string("host"), + port: Config.int("port"), + debug: Config.boolean("debug") +}) + +// As a tuple +const pair = Config.all([Config.string("a"), Config.int("b")]) +``` + +### Custom Config Logic + +There is no public low-level `Config.make` constructor. For custom validation or transformation, start from one of the public constructors or `Config.schema`, then use `Config.map`, `Config.mapOrFail`, `Config.all`, `Config.orElse`, or `Config.withDefault`. + +If you need custom lookup behavior for a new backing source, implement a `ConfigProvider` with `ConfigProvider.make` instead. + +## Config Schemas + +For reusable codecs you can pass directly to `Config.schema`: + +| Schema | Type | Notes | +| --------------------------- | -------------- | ------------------------------------------ | +| `Config.Boolean` | `boolean` | Decodes `true/false/yes/no/on/off/1/0/y/n` | +| `Schema.DurationFromString` | `Duration` | Decodes human-readable duration strings | +| `Config.Port` | `number` | Integer in 1–65535 | +| `Config.LogLevel` | `string` | One of the standard log level literals | +| `Config.Record(key, value)` | `Record` | Also parses flat `"k1=v1,k2=v2"` strings | + +## ConfigProvider Sources + +The concrete built-in source providers `fromEnv`, `fromDotEnvContents`, `fromDotEnv`, `fromUnknown`, and `fromDir` treat literal empty strings as missing values by default when they are loaded as values. Container discovery still reflects the source structure, so a key or file can appear in a `Record` or `Array` node and then load as missing. Pass `{ preserveEmptyStrings: true }` to preserve empty strings as explicit values. + +### `ConfigProvider.fromEnv` — Environment Variables (Default) + +This is the default provider. Path segments are joined with `_` for lookup. + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const provider = ConfigProvider.fromEnv({ + env: { + DATABASE_HOST: "localhost", + DATABASE_PORT: "5432" + } +}) + +const host = Config.string("HOST").parse( + provider.pipe(ConfigProvider.nested("DATABASE")) +) + +Effect.runSync(host) // "localhost" +``` + +**How `_` splitting works**: env var names are split on `_` to build a tree. This means `DATABASE_HOST=localhost` is accessible at both `["DATABASE_HOST"]` (flat) and `["DATABASE", "HOST"]` (nested). Querying `["DATABASE"]` returns a Record node with child key `"HOST"`. + +Pass `{ env: { ... } }` for testing. Omit to use `process.env` (merged with `import.meta.env` when available). + +### `ConfigProvider.fromUnknown` — Plain JS Objects + +Ideal for testing or embedding config in code: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const provider = ConfigProvider.fromUnknown({ + database: { + host: "localhost", + port: 5432, + credentials: { + username: "admin", + password: "secret" + } + }, + servers: ["server1", "server2", "server3"] +}) +``` + +Path traversal follows standard JS rules: string segments index into object keys, numeric segments index into arrays. Primitive values are automatically stringified. + +### `ConfigProvider.fromDotEnvContents` — Parse `.env` Strings + +When you already have the `.env` content as a string: + +```ts +import { ConfigProvider } from "effect" + +const contents = ` +# Database settings +HOST=localhost +PORT=3000 +SECRET="my-secret-value" +` + +const provider = ConfigProvider.fromDotEnvContents(contents) +``` + +Supports `export` prefixes, single/double/backtick quoting, inline comments, and escaped newlines. Enable variable expansion with `{ expandVariables: true }`: + +```ts +import { ConfigProvider } from "effect" + +const contents = ` +PASSWORD=secret +DB_PASS=$PASSWORD +` + +const provider = ConfigProvider.fromDotEnvContents(contents, { + expandVariables: true +}) +``` + +### `ConfigProvider.fromDotEnv` — Load `.env` Files + +Reads a `.env` file from disk. Returns an `Effect` (requires `FileSystem` in context): + +```ts +import { ConfigProvider, Effect } from "effect" + +const program = Effect.gen(function*() { + const provider = yield* ConfigProvider.fromDotEnv() + // or: yield* ConfigProvider.fromDotEnv({ path: "/custom/.env" }) + return provider +}) +``` + +### `ConfigProvider.fromDir` — Directory Trees + +Reads config from a file-system tree where each file is a leaf and each directory is a container. Useful for Kubernetes ConfigMap/Secret volume mounts. + +``` +/etc/myapp/ + database/ + host # contains "localhost" + port # contains "5432" + api_key # contains "sk-abc123" +``` + +```ts +import { ConfigProvider, Effect } from "effect" + +const program = Effect.gen(function*() { + const provider = yield* ConfigProvider.fromDir({ + rootPath: "/etc/myapp" + }) + return provider +}) +``` + +Requires `Path` and `FileSystem` in the Effect context. + +Missing files and directories return `undefined`, so fallback providers can handle the path. Empty files also return `undefined` by default after trimming their contents, while directory listings still report the file names present on disk; pass `{ preserveEmptyStrings: true }` to preserve them as `Value("")`. Other file-system failures are reported as `SourceError`. + +### `ConfigProvider.make` — Custom Sources + +Build a provider from any backing store: + +```ts +import { ConfigProvider, Effect } from "effect" + +const data: Record = { + host: "localhost", + port: "5432" +} + +const provider = ConfigProvider.make((path) => { + const key = path.join(".") + const value = data[key] + return Effect.succeed( + value !== undefined ? ConfigProvider.makeValue(value) : undefined + ) +}) +``` + +Return `undefined` for "not found". Only fail with `SourceError` for actual I/O errors. + +## ConfigProvider Combinators + +### `ConfigProvider.orElse` — Fallback Sources + +Falls back to a second provider when the first returns `undefined` (path not found). Does **not** catch `SourceError`. + +```ts +import { ConfigProvider } from "effect" + +const envProvider = ConfigProvider.fromEnv({ + env: { HOST: "prod.example.com" } +}) +const defaults = ConfigProvider.fromUnknown({ + HOST: "localhost", + PORT: "3000" +}) + +const combined = ConfigProvider.orElse(envProvider, defaults) +``` + +Each side keeps its own path transformations. If you combine providers that were already scoped or mapped, those transformations remain local to that side: + +```ts +import { ConfigProvider } from "effect" + +const envProvider = ConfigProvider.fromEnv({ + env: { DATABASE_HOST: "localhost" } +}).pipe(ConfigProvider.constantCase) + +const defaults = ConfigProvider.fromEnv({ + env: { APP_PORT: "3000" } +}).pipe(ConfigProvider.nested("APP")) + +const combined = envProvider.pipe(ConfigProvider.orElse(defaults)) +``` + +### `ConfigProvider.nested` — Prefix All Lookups + +Prepends path segments so that all lookups are scoped: + +```ts +import { ConfigProvider } from "effect" + +const provider = ConfigProvider.fromEnv({ + env: { APP_HOST: "localhost", APP_PORT: "3000" } +}) + +// Lookups for ["HOST"] now resolve to ["APP", "HOST"] +const scoped = ConfigProvider.nested(provider, "APP") +``` + +Accepts a single string or a full `Path` array. + +Provider transformations compose in application order. A later `nested` becomes the outer prefix: + +```ts +import { ConfigProvider } from "effect" + +const provider = ConfigProvider.fromEnv({ + env: { B_A_KEY: "value" } +}).pipe( + ConfigProvider.nested("A"), + ConfigProvider.nested("B") +) + +// path ["KEY"] resolves to ["B", "A", "KEY"] +``` + +When `nested` is applied to a provider built with `ConfigProvider.orElse`, the prefix is applied to both operands. + +### `ConfigProvider.constantCase` — CamelCase to SCREAMING_SNAKE_CASE + +Bridges camelCase schema keys to environment variable naming: + +```ts +import { ConfigProvider } from "effect" + +const provider = ConfigProvider.fromEnv({ + env: { DATABASE_HOST: "localhost" } +}).pipe(ConfigProvider.constantCase) + +// path ["databaseHost"] now resolves to ["DATABASE_HOST"] +``` + +Ordering matters with `nested`. `constantCase` is a path transform, so it only converts the path it receives at that point in the pipeline: + +```ts +import { ConfigProvider } from "effect" + +const convertedPrefix = ConfigProvider.fromEnv({ + env: { APP_HOST: "localhost" } +}).pipe( + ConfigProvider.nested("app"), + ConfigProvider.constantCase +) + +// path ["host"] resolves to ["APP", "HOST"] + +const literalPrefix = ConfigProvider.fromEnv({ + env: { app_HOST: "localhost" } +}).pipe( + ConfigProvider.constantCase, + ConfigProvider.nested("app") +) + +// path ["host"] resolves to ["app", "HOST"] +``` + +Put `constantCase` after `nested` when the prefix should be converted too. + +### `ConfigProvider.mapInput` — Arbitrary Path Transforms + +Transform the whole path before lookup: + +```ts +import { ConfigProvider } from "effect" + +const provider = ConfigProvider.fromEnv({ + env: { APP_HOST: "localhost" } +}) + +const upper = ConfigProvider.mapInput( + provider, + (path) => path.map((seg) => typeof seg === "string" ? seg.toUpperCase() : seg) +) +``` + +`mapInput` runs after earlier provider transformations, so it sees the full path produced so far: + +```ts +import { ConfigProvider } from "effect" + +const appendLeaf = ConfigProvider.mapInput((path) => [...path, "leaf"]) + +const provider = ConfigProvider.fromEnv({ + env: { APP_KEY_leaf: "value" } +}).pipe( + ConfigProvider.nested("APP"), + appendLeaf +) + +// path ["KEY"] resolves to ["APP", "KEY", "leaf"] +``` + +When `mapInput` is applied to a provider built with `ConfigProvider.orElse`, the mapping is applied to both operands. + +## Installing a Provider + +### Using `ConfigProvider.layer` + +Replaces the active provider for all downstream effects: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const TestLayer = ConfigProvider.layer( + ConfigProvider.fromUnknown({ port: 8080 }) +) + +const program = Effect.gen(function*() { + const port = yield* Config.int("port") + return port +}) + +Effect.runSync(Effect.provide(program, TestLayer)) // 8080 +``` + +### Using `ConfigProvider.layerAdd` + +Adds a provider without replacing the existing one. By default, the new provider is a **fallback**: + +```ts +import { ConfigProvider } from "effect" + +const defaults = ConfigProvider.fromUnknown({ + HOST: "localhost", + PORT: "3000" +}) + +// process.env is tried first; `defaults` is the fallback +const DefaultsLayer = ConfigProvider.layerAdd(defaults) +``` + +Set `{ asPrimary: true }` to make the new provider the primary source instead. + +### Using `Effect.provideService` + +For one-off overrides without layers: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const provider = ConfigProvider.fromUnknown({ HOST: "localhost" }) + +const program = Effect.gen(function*() { + const host = yield* Config.string("HOST") + return host +}).pipe( + Effect.provideService(ConfigProvider.ConfigProvider, provider) +) +``` + +## Two Ways to Run a Config + +1. **Yield in `Effect.gen`** — automatically uses the current `ConfigProvider` from the service map: + + ```ts + const program = Effect.gen(function*() { + const host = yield* Config.string("HOST") + }) + ``` + +2. **Call `.parse(provider)` directly** — useful for testing or when you have a specific provider: + + ```ts + const host = Config.string("HOST") + const result = Effect.runSync(host.parse(provider)) + ``` + +## Error Handling + +Config operations fail with `ConfigError`, which wraps either: + +- **`SourceError`** — the provider could not read data (I/O failure, permission error). Has `message` and optional `cause` properties. +- **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key). + +Check `error.cause._tag` to distinguish: + +```ts +import { Config, ConfigProvider, Effect } from "effect" + +const program = Config.int("PORT").parse( + ConfigProvider.fromUnknown({ PORT: "not-a-number" }) +).pipe( + Effect.tapError((error) => + Effect.sync(() => { + if (error.cause._tag === "SchemaError") { + console.log("Validation failed:", error.message) + } else { + console.log("Source error:", error.message) + } + }) + ) +) +``` + +**Important**: `Config.withDefault` and `Config.option` only recover from missing-data errors. Validation errors still propagate. + +## Practical Example: Web Server Config + +```ts +import { Config, ConfigProvider, Effect, Schema } from "effect" + +// Define your config shape +const ServerConfig = Config.schema( + Schema.Struct({ + host: Schema.String, + port: Schema.Int, + logLevel: Schema.Literals(["debug", "info", "warn", "error"]) + }), + "server" +) + +const DbConfig = Config.schema( + Schema.Struct({ + url: Schema.String, + poolSize: Schema.Int + }), + "db" +) + +const AppConfig = Config.all({ + server: ServerConfig, + db: DbConfig, + debug: Config.boolean("debug").pipe(Config.withDefault(false)) +}) + +// In production, just yield it — reads from process.env +const program = Effect.gen(function*() { + const config = yield* AppConfig + console.log(config) +}) + +// For testing, provide a specific provider +const testProvider = ConfigProvider.fromUnknown({ + server: { host: "localhost", port: 3000, logLevel: "debug" }, + db: { url: "postgres://localhost/testdb", poolSize: 5 }, + debug: true +}) + +Effect.runSync( + program.pipe(Effect.provide(ConfigProvider.layer(testProvider))) +) +``` + +With environment variables, the same config reads: + +``` +server_host=localhost +server_port=3000 +server_logLevel=debug +db_url=postgres://localhost/mydb +db_poolSize=10 +debug=true +``` diff --git a/repos/effect/packages/effect/HTTPAPI.md b/repos/effect/packages/effect/HTTPAPI.md new file mode 100644 index 0000000..041389d --- /dev/null +++ b/repos/effect/packages/effect/HTTPAPI.md @@ -0,0 +1,3270 @@ +# Overview + +The `HttpApi` modules let you describe your HTTP API once and use that description to run a server, generate documentation, and create a type-safe client. + +An API is built from three building blocks: + +- **HttpEndpoint** — a single route (path + HTTP method) with schemas for its request and response. +- **HttpApiGroup** — a collection of related endpoints (e.g., all user-related routes). +- **HttpApi** — the top-level object that combines groups into a complete API. + +``` +HttpApi +├── HttpGroup +│ ├── HttpEndpoint +│ └── HttpEndpoint +└── HttpGroup + ├── HttpEndpoint + ├── HttpEndpoint + └── HttpEndpoint +``` + +From one API definition you can: + +- **Start a server** that implements and serves every endpoint. +- **Generate documentation** (Scalar or Swagger) automatically. +- **Derive a client** with a typed method for each endpoint. + +One definition powers the server, docs, and client — change it once and everything stays in sync. + +# Getting Started + +## Defining and Implementing an API + +Let's build a minimal API with one endpoint that returns `"Hello, World!"`. You'll define what the endpoint looks like, implement it, and start a server. + +``` +HttpApi ("MyApi") +└── HttpGroup ("Greetings") + └── HttpEndpoint ("hello-world") +``` + +**Example** (Hello World) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +// Definition +const Api = HttpApi.make("MyApi").add( + // Define the API group + HttpApiGroup.make("Greetings").add( + // Define the endpoint + HttpApiEndpoint.get("hello", "/", { + // Define the success schema + success: Schema.String + }) + ) +) + +// Implementation +const GroupLive = HttpApiBuilder.group( + Api, + "Greetings", // The name of the group to handle + (handlers) => + handlers.handle( + "hello", // The name of the endpoint to handle + () => Effect.succeed("Hello, World!") // The handler function + ) +) + +// Server +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +// Launch +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +After running the code, open a browser and navigate to http://localhost:3000. The server will respond with: + +``` +Hello, World! +``` + +## Serving The Auto Generated OpenAPI Documentation + +Adding a documentation layer gives you an interactive page where you (and your API consumers) can explore endpoints, try requests, and see response shapes — all generated automatically from your API definition. You can choose between the `HttpApiScalar` module (Scalar UI) or the `HttpApiSwagger` module (Swagger UI); both do the same job. + +**Example** (Serving Scalar Documentation) + +To include Scalar in your server setup, provide the `HttpApiScalar.layer` when configuring the server. + +```ts +const ApiLive = HttpApiBuilder.layer(Api).pipe( + // Provide the Scalar layer so clients can access auto-generated docs + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) +``` + +After running the server, open your browser and navigate to http://localhost:3000/docs. + +This URL will display the Scalar documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively. + +**Example** (Serving Swagger Documentation) + +To include Swagger in your server setup, provide the `HttpApiSwagger.layer` when configuring the server. + +```ts +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + // Provide the Swagger layer so clients can access auto-generated docs + Layer.provide(HttpApiSwagger.layer(Api)), // "/docs" is the default path. + // or Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) +``` + +After running the server, open your browser and navigate to http://localhost:3000/docs. + +This URL will display the Swagger documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively. + +## Adding Annotations to Schemas + +Annotations attach extra information to your schemas — like a human-readable description or an identifier shown in the docs UI. They don't change runtime behavior; they enrich the generated documentation. + +```ts +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}).annotate({ + description: "A user", // The description of the user + identifier: "User" // Used in the Scalar UI under the Model section +}) +``` + +## Deriving a Client + +Once you've defined an API, you can generate a fully typed client from it using the `HttpApiClient` module. The client gives you a method for every endpoint, so calling your API feels like calling a local function — with full type safety and no manual HTTP handling. + +**Example** (Deriving and Using a Client) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { FetchHttpClient } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Greetings") + .add( + HttpApiEndpoint.get("hello", "/", { + success: Schema.String + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Greetings", + (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!")) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// Create a program that derives and uses the client +const program = Effect.gen(function*() { + // Derive the client + const client = yield* HttpApiClient.make(Api, { + baseUrl: "http://localhost:3000" + }) + // Call the "hello-world" endpoint + const hello = yield* client.Greetings.hello() + console.log(hello) +}) + +// Provide a Fetch-based HTTP client and run the program +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +/* +Output: +[18:55:26.051] INFO (#2): Listening on http://0.0.0.0:3000 +[18:55:26.057] INFO (#12) http.span=2ms: Sent HTTP response { 'http.method': 'GET', 'http.url': '/', 'http.status': 200 } +Hello, World! +*/ +``` + +# Design Principles + +- **Schemas first**: Every piece of data flowing in or out of an endpoint — path params, query strings, headers, payloads, responses, and errors — is described by a schema. The framework uses these schemas to validate requests, serialize responses, generate docs, and type the client. +- **Metadata lives on schemas**: Configuration like HTTP status codes and content types is attached directly to the schema via annotations, not to the endpoint. This keeps all the information about a data shape in one place. + +In particular: + +- **Request** + - **Payload encoding / content type** is controlled with `HttpApiSchema.as*` helpers: + - `asJson` — parse the body as JSON (default) + - `asFormUrlEncoded` — parse the body as URL-encoded form data + - `asText` — parse the body as plain text + - `asUint8Array` — parse the body as raw binary data + - `asMultipart` — parse the body as a multipart form (for file uploads) + - `asMultipartStream` — parse the body as a streaming multipart form +- **Response** + - **Status code** is set via the `HttpApiSchema.status` API (or `httpApiStatus` annotation) + - **Encoding / content type** is controlled with `HttpApiSchema.as*` helpers: + - `asJson` — send the body as JSON (default) + - `asFormUrlEncoded` — send the body as URL-encoded form data + - `asText` — send the body as plain text + - `asUint8Array` — send the body as raw binary data + +## Anatomy of an Endpoint + +An endpoint definition describes everything the framework needs to know about a single HTTP route: which URL parameters it expects, what query strings and headers it reads, what the request body looks like, and what it can respond with (both successes and errors). All of these are optional. + +`HttpApiEndpoint` automatically coerces request and response schemas by default. Path / query / header schemas use `Schema.toCodecStringTree`, while JSON payload / success / error schemas use `Schema.toCodecJson`. This means you can define schemas in their natural domain types (for example `Schema.Int`), without manually adding string / JSON transformations. + +```ts +const User = Schema.Struct({ + id: Schema.String, + name: Schema.String +}) + +// ┌─── Endpoint name (used in the client as the method name) +// │ ┌─── Endpoint path +// ▼ ▼ +HttpApiEndpoint.patch("updateUser", "/user/:id", { + // Parameters from the route pattern (e.g. /user/:id). + // Can be a record of fields or a full schema. + params: { + // ┌─── Schema for the "id" parameter. + // ▼ + id: Schema.String + }, + + // (optional) Query string parameters (e.g. ?mode=merge). + // Can be a record of fields or a full schema. + query: { + // ┌─── Schema for the "mode" query parameter + // ▼ + mode: Schema.Literals(["merge", "replace"]) + }, + + // (optional) Request headers. + // Can be a record of fields or a full schema. + headers: { + "x-api-key": Schema.String, + "x-request-id": Schema.String + }, + + // The request payload can be a single schema or an array of schemas. + // - Default encoding is JSON. + // - Default status for success is 200. + // For GET requests, the payload must be a record of schemas. + payload: [ + // JSON payload (default encoding). + Schema.Struct({ + name: Schema.String + }), + // text/plain payload. + Schema.String.pipe(HttpApiSchema.asText()) + ], + + // Possible success responses. + // Default is 200 OK with no content if omitted. + success: [ + // JSON response (default encoding). + User, + // text/plain response with a custom status code. + Schema.String + .pipe( + HttpApiSchema.status(206), + HttpApiSchema.asText() + ) + ], + + // Possible error responses. + error: [ + // Default is 500 Internal Server Error with JSON encoding. + Schema.Finite, + + // text/plain error with a custom status code. + Schema.String + .pipe( + HttpApiSchema.status(404), + HttpApiSchema.asText() + ), + + // Any schema that encodes to `Schema.Void` is treated as "no content". + // Here it uses a custom status code. + Schema.Void + .pipe(HttpApiSchema.status(401)) + ] +}) +``` + +# Routing + +This section walks through defining endpoints for common HTTP methods — GET, POST, DELETE, and PATCH — using a user-management API as a running example: + +- `GET /users` — retrieve all users. +- `GET /users/:userId` — retrieve a specific user by ID. +- `POST /users` — create a new user. +- `DELETE /users/:userId` — delete a user by ID. +- `PATCH /users/:userId` — update a user by ID. + +## GET + +Use `HttpApiEndpoint.get` to create a GET endpoint. Provide a name (used as the method name in generated clients), a path, and optionally a `success` schema describing what the endpoint returns. Without a success schema the default response is `204 No Content`. + +**Example** (Defining a GET Endpoint to Retrieve All Users) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +// Define a schema representing a User entity +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + // Define the "getUsers" endpoint, returning a list of users + // ┌─── Endpoint name (used in the client as the method name) + // │ ┌─── Endpoint path + // ▼ ▼ + HttpApiEndpoint.get("getUsers", "/users", { + // ┌─── success schema + // │ + // ▼ + success: Schema.Array(User) + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers.handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## POST + +Use `HttpApiEndpoint.post` to create an endpoint that accepts data. The `payload` option describes the shape of the request body, and `success` describes what the endpoint returns. + +**Example** (Defining a POST Endpoint with Payload and Success Schemas) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User + }), + // Define a POST endpoint for creating a new user + HttpApiEndpoint.post("createUser", "/user", { + // Define the request body schema (payload) + payload: User, + // Define the schema for a successful response + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("createUser", (ctx) => { + // ┌─── User + // ▼ + const user = ctx.payload + return Effect.succeed(user) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## DELETE + +Use `HttpApiEndpoint.delete` to create an endpoint that removes a resource. + +**Example** (Defining a DELETE Endpoint with Parameters) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const IdParam = Schema.Int + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: IdParam + }, + success: User + }), + HttpApiEndpoint.post("createUser", "/user", { + payload: User, + success: User + }), + HttpApiEndpoint.delete("deleteUser", "/user/:id", { + params: { + id: IdParam + } + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("createUser", (ctx) => { + const user = ctx.payload + return Effect.succeed(user) + }) + .handle("deleteUser", (ctx) => { + const id = ctx.params.id + return Effect.log(`Deleting user ${id}`) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## PATCH + +Use `HttpApiEndpoint.patch` to create an endpoint that partially updates a resource. Like POST, you can define `payload` (the fields to update) and `success` (the response after the update). + +**Example** (Defining a PATCH Endpoint for Updating a User) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const IdParam = Schema.Int + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: IdParam + }, + success: User + }), + HttpApiEndpoint.post("createUser", "/user", { + payload: User, + success: User + }), + HttpApiEndpoint.delete("deleteUser", "/user/:id", { + params: { + id: IdParam + } + }), + HttpApiEndpoint.patch("updateUser", "/user/:id", { + params: { + id: IdParam + }, + // Specify the schema for the request payload + payload: Schema.Struct({ + name: Schema.String // Only the name can be updated + }), + // Specify the schema for a successful response + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("createUser", (ctx) => { + const user = ctx.payload + return Effect.succeed(user) + }) + .handle("deleteUser", (ctx) => { + const id = ctx.params.id + return Effect.log(`Deleting user ${id}`) + }) + .handle("updateUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## Parameters + +Path parameters let you capture dynamic values from the URL. For example, `/user/:id` extracts the `id` segment. Use the `params` option to declare a record of fields or a full schema — the framework will parse and validate the value before your handler runs. + +**Example** (Defining a GET Endpoint to Retrieve a User by ID) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + // a GET endpoint with a parameter ":id" + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + // ┌─── schema for the "id" parameter + // ▼ + id: Schema.Int + }, + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + // ┌─── number + // ▼ + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## Catch-All Endpoints + +Set the path to `"*"` to match any URL that no other endpoint handles. This is useful for custom "not found" pages or fallback responses. + +**Example** (Defining a Catch-All Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const IdParam = Schema.Int + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: IdParam + }, + success: User + }), + HttpApiEndpoint.post("createUser", "/user", { + payload: User, + success: User + }), + HttpApiEndpoint.delete("deleteUser", "/user/:id", { + params: { + id: IdParam + } + }), + HttpApiEndpoint.patch("updateUser", "/user/:id", { + params: { + id: IdParam + }, + payload: Schema.Struct({ + name: Schema.String + }), + success: User + }), + // catch-all endpoint + HttpApiEndpoint.get("catchAll", "*", { + success: Schema.String + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("createUser", (ctx) => { + const user = ctx.payload + return Effect.succeed(user) + }) + .handle("deleteUser", (ctx) => { + const id = ctx.params.id + return Effect.log(`Deleting user ${id}`) + }) + .handle("updateUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("catchAll", () => { + return Effect.succeed("Not found") + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +> [!IMPORTANT] +> The catch-all endpoint must be the last endpoint in the group. + +> [!IMPORTANT] +> (OpenAPI). A catch-all endpoint is not included in the OpenAPI specification because can't be represented as a path. + +## Prefixing + +Prefixes let you prepend a common path segment to endpoints, groups, or an entire API. This avoids repeating the same base path on every endpoint. + +**Example** (Using Prefixes for Common Path Management) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("endpointA", "/a", { + success: Schema.String + }) + // Prefix for this endpoint + .prefix("/endpointPrefix"), + HttpApiEndpoint.get("endpointB", "/b", { + success: Schema.String + }) + ) + // Prefix for all endpoints in the group + .prefix("/groupPrefix") + ) + // Prefix for the entire API + .prefix("/apiPrefix") + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers + .handle("endpointA", () => Effect.succeed("Endpoint A")) + .handle("endpointB", () => Effect.succeed("Endpoint B")) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test this endpoint using a GET request. For example: + +```sh +curl http://localhost:3000/apiPrefix/groupPrefix/endpointPrefix/a # Returns 200 OK +curl http://localhost:3000/apiPrefix/groupPrefix/b # Returns 200 OK +``` + +# Request + +## Query Parameters + +Query parameters are the `?key=value` pairs appended to a URL. Use the `query` option to declare a record of fields or a full schema — the framework will parse, validate, and type them for you. + +**Example** (Defining Query Parameters with Metadata) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Page = Schema.Int.check(Schema.isGreaterThan(0)) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User), + // Specify a schema for each query parameter + query: { + // Parameter "page" for pagination + page: Schema.optionalKey(Page), + // Parameter "sort" for sorting options + sort: Schema.optionalKey(Schema.Literals(["id", "name"])) + } + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", (ctx) => { + const { page, sort } = ctx.query + console.log(`Getting users with page ${page} and sort ${sort}`) + return Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + ) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +#### Defining an Array of Values for a Query Parameter + +A single query parameter can carry multiple values (e.g., `?a=1&a=2`). Wrap the parameter's schema in `Schema.Array` to accept an array of values. + +**Example** (Defining an Array of String Values for a Query Parameter) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User), + query: { + a: Schema.optionalKey(Schema.Array(Schema.String)) + } + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", (ctx) => { + console.log(ctx.query) + return Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + ) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test this endpoint by passing an array of values in the query string. For example: + +```sh +curl "http://localhost:3000/users?a=1&a=2" # Two values for the `a` parameter +``` + +The query string sends two values (`1` and `2`) for the `a` parameter. The server will process and validate these values according to the schema. + +Both the following requests will be valid: + +```sh +curl "http://localhost:3000/users" # No values for the `a` parameter +curl "http://localhost:3000/users?a=1" # One value for the `a` parameter +``` + +## Request Headers + +Use the `headers` option to declare a record of fields or a full schema for the request headers the endpoint expects. + +> [!IMPORTANT] +> All headers are normalized to lowercase. Always use lowercase keys for the headers. + +**Example** (Describe and validate custom headers) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + // Always use lowercase keys for the headers + headers: { + "x-api-key": Schema.String, + "x-request-id": Schema.String + }, + success: Schema.Array(User) + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers.handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test the endpoint by sending the headers: + +```sh +curl -H "X-API-Key: 1234567890" -H "X-Request-ID: 1234567890" http://localhost:3000/users +``` + +The server validates these headers against the declared schema before handling the request. + +## Handling Multipart Requests + +To accept file uploads, mark the payload as multipart with `HttpApiSchema.asMultipart`. Use `Multipart.FilesSchema` for the file fields — uploaded files will be persisted to disk automatically. + +**Example** (Defining an Endpoint for File Uploads) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter, Multipart } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.post("upload", "/users/upload", { + // Specify that the payload is a multipart request + payload: HttpApiSchema.asMultipart( + Schema.Struct({ + // Define a "files" field to handle file uploads + files: Multipart.FilesSchema + }) + ), + success: Schema.String + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("upload", (ctx) => { + // ┌─── readonly Multipart.PersistedFile[] + // ▼ + const { files } = ctx.payload + console.log(files) + return Effect.succeed("Uploaded") + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test this endpoint by sending a multipart request with a file upload. For example: + +```sh +echo "Sample file content" | curl -X POST -F "files=@-" http://localhost:3000/users/upload +``` + +## Changing the Request Encoding + +By default, request bodies are JSON. To accept a different format — like form-urlencoded data — pipe the payload schema through the appropriate `HttpApiSchema.as*` helper. + +**Example** (Customizing Request Encoding) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.post("createUser", "/user", { + // Set the request payload as a string encoded with query parameters + payload: Schema.Struct({ + id: Schema.Int, + name: Schema.String + }) + // Specify the encoding as form url encoded + .pipe(HttpApiSchema.asFormUrlEncoded()), + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("createUser", (ctx) => { + const user = ctx.payload + return Effect.succeed(user) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test this endpoint using a URL-encoded request body. For example: + +```sh +curl http://localhost:3000/user \ + --request POST \ + --header 'Accept: */*' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'id=1' \ + --data-urlencode 'name=John' +``` + +## Accessing the HttpServerRequest + +Inside a handler, `ctx.request` gives you access to the raw incoming HTTP request. Use this when you need low-level details not covered by the endpoint schema (e.g., the HTTP method or raw URL). + +**Example** (Accessing the Request Object in a GET Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi").add( + HttpApiGroup.make("Greetings").add( + HttpApiEndpoint.get("hello", "/", { + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "Greetings", + (handlers) => + handlers.handle( + "hello", + (ctx) => { + // ┌─── HttpServerRequest + // ▼ + const req = ctx.request + // Access the request method + console.log(req.method) + return Effect.succeed("Hello, World!") + } + ) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## Validating Request Cookies + +There is no `cookies` option on endpoints. Instead, validated cookie access goes through the security middleware system: define an `HttpApiSecurity.apiKey` with `in: "cookie"` and attach it to a middleware. The cookie value is decoded and handed to your security handler as a `Redacted` credential. + +**Example** (Validating a Session Cookie) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Context, Effect, Layer, Redacted, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiScalar, + HttpApiSecurity +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +// Define the service providing the current user +class CurrentUser + extends Context.Service()("CurrentUser") +{} + +// Define the security scheme: read the "session" cookie +const sessionCookie = HttpApiSecurity.apiKey({ in: "cookie", key: "session" }) + +class Auth extends HttpApiMiddleware.Service()("Auth", { + error: Schema.String.annotate({ + httpApiStatus: 401, + description: "Auth error" + }), + security: { session: sessionCookie } +}) {} + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("me", "/me", { + success: Schema.Struct({ id: Schema.Finite }) + }) + ) + .middleware(Auth) +) + +const AuthLive = Layer.succeed( + Auth, + { + session: (effect, opts) => + Effect.provideServiceEffect( + effect, + CurrentUser, + Effect.gen(function*() { + const value = Redacted.value(opts.credential) + if (value !== "valid-session") { + return yield* Effect.fail("Invalid session") + } + return { id: 1, name: "John Doe" } + }) + ) + } +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("me", () => + Effect.gen(function*() { + const user = yield* CurrentUser + return { id: user.id } + })) +).pipe(Layer.provide(AuthLive)) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// Valid session: +// curl "http://localhost:3000/me" --cookie "session=valid-session" +// {"id":1} +// +// Invalid session: +// curl "http://localhost:3000/me" --cookie "session=wrong" +// "Invalid session" +``` + +For quick, unvalidated access you can read cookies directly from `ctx.request.cookies` inside any handler. These cookies won't appear in the OpenAPI spec. + +**Example** (Reading Cookies Directly in a Handler) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("me", "/me", { + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("me", (ctx) => { + const lang = ctx.request.cookies.lang ?? "en" + return Effect.succeed(`Language: ${lang}`) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl "http://localhost:3000/me" --cookie "lang=it" +// "Language: it" +``` + +## Streaming Requests + +To receive large or continuous data from the client, define the payload as a `Uint8Array` and pipe it through `HttpApiSchema.asUint8Array()`. The handler receives the raw bytes, which you can decode as needed. + +**Example** (Handling Streaming Requests) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("acceptStream", "/stream", { + // Define the payload as a Uint8Array with a specific encoding + payload: Schema.Uint8Array.pipe( + HttpApiSchema.asUint8Array() // default content type: application/octet-stream + ), + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle( + "acceptStream", + (ctx) => { + // Decode the incoming binary data into a string + return Effect.succeed(new TextDecoder().decode(ctx.payload)) + } + ) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test the streaming request using `curl` or any tool that supports sending binary data. For example: + +```sh +echo "abc" | curl -X POST 'http://localhost:3000/stream' --data-binary @- -H "Content-Type: application/octet-stream" +# Output: abc +``` + +# Response + +## Status Codes + +Success responses default to `200 OK`. To use a different status code, annotate the success schema with `HttpApiSchema.status(code)` or set the `httpApiStatus` annotation. + +**Example** (Defining a GET Endpoint with a custom status code) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + .pipe(HttpApiSchema.status(206)) + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => { + return Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + ) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## Changing the Response Encoding + +Responses default to JSON. To return a different format — like CSV or plain text — pipe the success schema through the matching `HttpApiSchema.as*` helper and, optionally, set a custom `contentType`. + +**Example** (Returning Data as `text/csv`) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("csv", "/users/csv", { + success: Schema.String.pipe( + // Set the success response as a string with CSV encoding + HttpApiSchema.asText({ + // Define the content type as text/csv + contentType: "text/csv" + }) + ) + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("csv", (ctx) => { + return Effect.succeed("id,name\n1,John\n2,Jane") + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test this endpoint using a GET request. For example: + +```sh +curl http://localhost:3000/users/csv +``` + +The following encodings are supported: + +- `Json` the default encoding (default content type: `application/json`) +- `Uint8Array` the encoding for binary data (default content type: `application/octet-stream`) +- `Text` the encoding for text data (default content type: `text/plain`) + +## Setting Response Headers + +To add custom headers to the outgoing response, call `HttpEffect.appendPreResponseHandler` inside your handler. The callback receives the request and response objects and must return the updated response. + +**Example** (Adding a Custom Response Header) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("hello", "/hello", { + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("hello", () => + Effect.gen(function*() { + yield* HttpEffect.appendPreResponseHandler((_req, response) => + Effect.succeed(HttpServerResponse.setHeader(response, "x-custom", "hello")) + ) + return "Hello, World!" + })) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl -v "http://localhost:3000/hello" 2>&1 | grep -i "x-custom" +// < x-custom: hello +``` + +## Setting Response Cookies + +Set cookies on the response using `HttpEffect.appendPreResponseHandler` together with `HttpServerResponse.setCookie`. For cookies tied to an `HttpApiSecurity.apiKey`, use the shortcut `HttpApiBuilder.securitySetCookie` instead. + +**Example** (Setting a Response Cookie) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpEffect, HttpRouter, HttpServerResponse } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("hello", "/hello", { + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("hello", () => + Effect.gen(function*() { + yield* HttpEffect.appendPreResponseHandler((_req, response) => + Effect.succeed(HttpServerResponse.setCookieUnsafe(response, "my-cookie", "my-value", { + httpOnly: true, + secure: true, + path: "/" + })) + ) + return "Hello, World!" + })) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl -v "http://localhost:3000/hello" 2>&1 | grep -i "set-cookie" +// < set-cookie: my-cookie=my-value; Path=/; HttpOnly; Secure +``` + +## Redirects + +To redirect the client to a different URL, return an `HttpServerResponse.redirect` from the handler. The redirect is not modeled in the schema — the endpoint definition stays as "no content". + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpServerResponse } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("newPage", "/new", { + success: Schema.String + }), + // Schema-wise this is just "no content" (redirect headers aren't modeled here) + HttpApiEndpoint.get("oldPage", "/old") + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers + .handle("newPage", () => Effect.succeed("You are on /new")) + .handle("oldPage", () => + Effect.succeed( + HttpServerResponse.redirect("/new", { status: 302 }) + )) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl "http://localhost:3000/old" -L +``` + +## Streaming Responses + +To stream data to the client over time, return an `HttpServerResponse.stream` from the handler. The stream emits chunks at whatever pace you choose. + +**Example** (Implementing a Streaming Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schedule, Schema, Stream } from "effect" +import { HttpRouter, HttpServerResponse } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getStream", "/stream", { + success: Schema.String.pipe( + HttpApiSchema.asText({ + contentType: "application/octet-stream" + }) + ) + }) + ) +) + +// Simulate a stream of data +const stream = Stream.make("a", "b", "c").pipe( + Stream.schedule(Schedule.spaced("500 millis")), + Stream.map((s) => new TextEncoder().encode(s)) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle( + "getStream", + () => Effect.succeed(HttpServerResponse.stream(stream)) + ) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test the streaming response using `curl` or any similar HTTP client that supports streaming: + +```sh +curl 'http://localhost:3000/stream' --no-buffer +``` + +The response will stream data (`a`, `b`, `c`) with a 500ms interval between each item. + +# Error Handling + +## Adding Custom Error Responses + +Endpoints can declare the errors they may return. Each error is a schema annotated with an HTTP status code via `HttpApiSchema.status(code)`. The status is set once on the schema and reused wherever that schema appears. When your handler fails with a matching error, the framework serializes it and responds with the declared status. + +**Example** (Defining Error Responses for an Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const UserNotFound = Schema.Struct({ + _tag: Schema.tag("UserNotFound"), + message: Schema.String +}).pipe(HttpApiSchema.status(404)) + +const Unauthorized = Schema.Struct({ + _tag: Schema.tag("Unauthorized") +}).pipe(HttpApiSchema.status(401)) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User, + error: [UserNotFound, Unauthorized /** etc. */] + }) + ) + .add( + HttpApiEndpoint.delete("deleteUser", "/user/:id", { + params: { + id: Schema.Int + }, + error: [UserNotFound, Unauthorized /** etc. */] + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUser", (ctx) => { + const id = ctx.params.id + if (id === 1) { + return Effect.fail(UserNotFound.make({ message: "User not found" })) + } + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("deleteUser", (ctx) => { + const id = ctx.params.id + if (id === 1) { + return Effect.fail(UserNotFound.make({ message: "User not found" })) + } + return Effect.succeed(void 0) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +You can test these endpoints. For example: + +```sh +curl http://localhost:3000/user/1 # Returns 404 Not Found +curl http://localhost:3000/user/2 # Returns 200 OK +curl -X DELETE http://localhost:3000/user/1 # Returns 404 Not Found +curl -X DELETE http://localhost:3000/user/2 # Returns 200 OK +``` + +## Predefined Error Types + +The `HttpApiError` module provides ready-made error schemas for common HTTP status codes (404, 401, etc.). Using these saves you from defining boilerplate error types and keeps error handling consistent across your API. + +**Example** (Adding a Predefined Error to an Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiError, + HttpApiGroup, + HttpApiScalar +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User, + error: [ + // Add a 404 error JSON response for this endpoint + HttpApiError.NotFound, + // Add a 401 error JSON response for unauthorized access + HttpApiError.Unauthorized + ] + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUser", (ctx) => { + const id = ctx.params.id + if (id === 1) { + return Effect.fail(new HttpApiError.NotFound({})) + } + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +| Name | Status | Description | +| --------------------- | ------ | -------------------------------------------------------------------------------------------------- | +| `HttpApiDecodeError` | 400 | Represents an error where the request did not match the expected schema. Includes detailed issues. | +| `BadRequest` | 400 | Indicates that the request was malformed or invalid. | +| `Unauthorized` | 401 | Indicates that authentication is required but missing or invalid. | +| `Forbidden` | 403 | Indicates that the client does not have permission to access the requested resource. | +| `NotFound` | 404 | Indicates that the requested resource could not be found. | +| `MethodNotAllowed` | 405 | Indicates that the HTTP method used is not allowed for the requested resource. | +| `NotAcceptable` | 406 | Indicates that the requested resource cannot be delivered in a format acceptable to the client. | +| `RequestTimeout` | 408 | Indicates that the server timed out waiting for the client request. | +| `Conflict` | 409 | Indicates a conflict in the request, such as conflicting data. | +| `Gone` | 410 | Indicates that the requested resource is no longer available and will not return. | +| `InternalServerError` | 500 | Indicates an unexpected server error occurred. | +| `NotImplemented` | 501 | Indicates that the requested functionality is not implemented on the server. | +| `ServiceUnavailable` | 503 | Indicates that the server is temporarily unavailable, often due to maintenance or overload. | + +#### Predefined NoContent Error Types + +Each predefined error also has a `NoContent` variant that responds with the status code but no body. + +**Example** (Using a Predefined NoContent Error Type) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiError, + HttpApiGroup, + HttpApiScalar +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User, + error: [ + // Add a 404 error no-content response for this endpoint + HttpApiError.NotFoundNoContent, + // Add a 401 error no-content response for unauthorized access + HttpApiError.UnauthorizedNoContent + ] + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUser", (ctx) => { + const id = ctx.params.id + if (id === 1) { + return Effect.fail(new HttpApiError.NotFound({})) + } + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +## Customizing Schema Error Responses + +By default, when a request fails schema validation (e.g., an invalid query parameter or a malformed path parameter), the framework responds with an empty `400 Bad Request`. If you want to replace that response with a custom error, use `HttpApiMiddleware.layerSchemaErrorTransform`. + +This function creates a [middleware](#middlewares) layer that intercepts any `SchemaError` thrown during request decoding and lets you return your own error instead. + +**Example** (Returning a Custom Error on Validation Failure) + +In this example, if a client sends a non-integer `id` query parameter, the API responds with a `422` status and a JSON body describing the problem, instead of the default empty `400`. + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +// Define a custom error for validation failures +class ValidationError extends Schema.TaggedErrorClass()( + "ValidationError", + { + message: Schema.String + } +) {} + +// Define the middleware service, declaring the error it can produce +class SchemaErrorHandler extends HttpApiMiddleware.Service()( + "api/SchemaErrorHandler", + { + error: ValidationError.pipe(HttpApiSchema.status(422)) + } +) {} + +// Implement the middleware layer +const SchemaErrorHandlerLive = HttpApiMiddleware.layerSchemaErrorTransform( + SchemaErrorHandler, + (schemaError) => + Effect.fail( + new ValidationError({ + message: `Invalid request: ${schemaError.message}` + }) + ) +) + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi").add( + HttpApiGroup.make("Users").add( + HttpApiEndpoint.get("getUser", "/user", { + query: { + id: Schema.Int + }, + success: User + }) + // Attach the middleware to this endpoint only + .middleware(SchemaErrorHandler) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => handlers.handle("getUser", (ctx) => Effect.succeed({ id: ctx.query.id, name: `User ${ctx.query.id}` })) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(SchemaErrorHandlerLive), + Layer.provide(HttpApiScalar.layer(Api)), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// Test: +// curl "http://localhost:3000/user?id=1" # 200 OK +// curl "http://localhost:3000/user?id=abc" # 422 with ValidationError JSON +``` + +The middleware can be attached at different scopes: + +- **Endpoint**: `.middleware(SchemaErrorHandler)` on a single endpoint (as shown above). +- **Group**: `.middleware(SchemaErrorHandler)` on a group to cover all its endpoints. +- **API**: `.middleware(SchemaErrorHandler)` on the API to cover every endpoint. + +# Middlewares + +Middleware lets you run shared logic — like logging or authentication — before (or around) your handlers. Define a middleware as a class extending `HttpApiMiddleware.Service`, implement it as a `Layer`, and attach it to an endpoint, a group, or the entire API. + +**Example** (Defining a Logger Middleware) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter, HttpServerRequest } from "effect/unstable/http" +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiMiddleware, + HttpApiScalar, + HttpApiSchema +} from "effect/unstable/httpapi" +import { createServer } from "node:http" + +class Logger extends HttpApiMiddleware.Service()("Http/Logger", { + // default is 500 Internal Server Error with JSON encoding + error: Schema.String + .pipe( + HttpApiSchema.status(405), // override default status code + HttpApiSchema.asText() // override default encoding + ) +}) {} + +const User = Schema.Struct({ + id: Schema.Finite, + name: Schema.String +}) + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User + }) + // Apply the middleware to a single endpoint + .middleware(Logger) + ) + // Or apply the middleware to the entire group + .middleware(Logger) +) +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const LoggerLive = Layer.effect( + Logger, + Effect.gen(function*() { + yield* Effect.log("creating Logger middleware") + + return (res) => + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* Effect.log(`Request: ${request.method} ${request.url}`) + return yield* res + }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + Layer.provide(LoggerLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// Test this with this curl command: +// curl "http://localhost:3000/user/1" +``` + +## Interdependent Middleware + +Middleware can depend on services provided by other middleware. Declare the dependency with `requires` and declare the produced service with `provides`. + +When you attach interdependent middleware to an endpoint, group, or API, the middleware that uses `requires` must come **BEFORE** the middleware that provides that service. The earlier middleware wraps the later middleware, so it can consume the service that the later middleware adds to the request effect. + +**Example** (Middleware that consumes another middleware's output) + +```ts +import { Context, Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/unstable/httpapi" + +class AuthInfo extends Context.Service()("AuthInfo") {} + +class LoadAuth extends HttpApiMiddleware.Service()("LoadAuth") {} + +class RequireAuth extends HttpApiMiddleware.Service()("RequireAuth") {} + +const Api = HttpApi.make("api").add( + HttpApiGroup.make("users").add( + HttpApiEndpoint.get("me", "/me", { + success: Schema.String + }) + // RequireAuth reads AuthInfo, so it must appear first. + .middleware(RequireAuth) + // LoadAuth provides AuthInfo to middleware that appears before it. + .middleware(LoadAuth) + ) +) + +const LoadAuthLive = Layer.effect( + LoadAuth, + Effect.succeed((effect) => + Effect.provideService(effect, AuthInfo, { + userId: "user-1" + }) + ) +) + +const RequireAuthLive = Layer.effect( + RequireAuth, + Effect.succeed( + Effect.fnUntraced(function*(effect) { + const authInfo = yield* AuthInfo + yield* Effect.log(`authenticated user ${authInfo.userId}`) + return yield* effect + }) + ) +) +``` + +# Security + +The `HttpApiSecurity` module lets you declare how an endpoint is protected. These declarations show up in the generated OpenAPI spec and are enforced at runtime through middleware. + +Supported authorization types: + +| Authorization Type | Description | +| ------------------------ | ---------------------------------------------------------------- | +| `HttpApiSecurity.apiKey` | API key authorization via headers, query parameters, or cookies. | +| `HttpApiSecurity.basic` | HTTP Basic authentication. | +| `HttpApiSecurity.bearer` | Bearer token authentication. | + +Attach a security scheme to an endpoint, group, or the entire API via `HttpApiMiddleware`. + +**Example** (Defining Security Middleware) + +```ts +import { Context, Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" + +// Define a schema for the "User" +class User extends Schema.Class("User")({ id: Schema.Finite }) {} + +// Define a schema for the "Unauthorized" error +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + {}, + // Specify the HTTP status code for unauthorized errors + { httpApiStatus: 401 } +) {} + +// Define a Context.Tag for the authenticated user +class CurrentUser extends Context.Service()("CurrentUser") {} + +// Create the Authorization middleware +class Authorization extends HttpApiMiddleware.Service()( + "Authorization", + { + // Define the error schema for unauthorized access + error: Unauthorized, + // Add security definitions + security: { + // ┌─── Custom name for the security definition + // ▼ + myBearer: HttpApiSecurity.bearer + // Additional security definitions can be added here. + // They will attempt to be resolved in the order they are defined. + } + } +) {} + +const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("get", "/", { + success: Schema.String + }) + // Apply the middleware to a single endpoint + .middleware(Authorization) + ) + // Or apply the middleware to the entire group + .middleware(Authorization) + ) + // Or apply the middleware to the entire API + .middleware(Authorization) +``` + +## Implementing HttpApiSecurity middleware + +To enforce a security scheme, implement its middleware as a `Layer`. The layer returns an object with a handler for each security definition. Each handler receives the credential (e.g., a Bearer token as a `Redacted` value) and must return the resource the middleware provides (e.g., the current user). + +**Example** (Implementing Bearer Token Authentication Middleware) + +```ts +import { Context, Effect, Layer, Redacted, Schema } from "effect" +import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" + +class User extends Schema.Class("User")({ id: Schema.Finite }) {} + +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + {}, + // Specify the HTTP status code for unauthorized errors + { httpApiStatus: 401 } +) {} + +class CurrentUser extends Context.Service()("CurrentUser") {} + +class Authorization extends HttpApiMiddleware.Service()( + "Authorization", + { + error: Unauthorized, + security: { + myBearer: HttpApiSecurity.bearer + } + } +) {} + +const AuthorizationLive = Layer.succeed( + Authorization, + // Return the security handlers for the middleware + { + // Define the handler for the Bearer token + // The Bearer token is redacted for security + myBearer: (effect, opts) => + Effect.provideServiceEffect( + effect, + CurrentUser, + Effect.gen(function*() { + yield* Effect.log( + "checking bearer token", + Redacted.value(opts.credential) + ) + // Return a mock User object as the CurrentUser + return new User({ id: 1 }) + }) + ) + } +) +``` + +## Adding Descriptions to Security Definitions + +Use `HttpApiSecurity.annotate` to attach metadata — like a description — to a security definition. This metadata appears in the generated docs. + +**Example** (Adding a Description to a Bearer Token Security Definition) + +```ts +import { Context, Schema } from "effect" +import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from "effect/unstable/httpapi" + +class User extends Schema.Class("User")({ id: Schema.Finite }) {} + +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + {}, + // Specify the HTTP status code for unauthorized errors + { httpApiStatus: 401 } +) {} + +class CurrentUser extends Context.Service()("CurrentUser") {} + +class Authorization extends HttpApiMiddleware.Service()( + "Authorization", + { + error: Unauthorized, + security: { + myBearer: HttpApiSecurity.bearer.pipe( + // Add a description to the security definition + HttpApiSecurity.annotate(OpenApi.Description, "my description") + ) + } + } +) {} +``` + +## Setting HttpApiSecurity cookies + +Use `HttpApiBuilder.securitySetCookie` to set a security cookie from a handler. The cookie is created with `HttpOnly` and `Secure` flags by default. + +**Example** (Setting a Security Cookie in a Login Handler) + +```ts +import { Redacted, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSecurity } from "effect/unstable/httpapi" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("login", "/login", { + params: { + success: Schema.String + } + }) + ) + ) + +// Define the security configuration for an API key stored in a cookie +const security = HttpApiSecurity.apiKey({ + // Specify that the API key is stored in a cookie + in: "cookie", + // Define the cookie name, + key: "token" +}) + +const UsersApiLive = HttpApiBuilder.group(Api, "Users", (handlers) => + handlers.handle("login", () => + // Set the security cookie with a redacted value + HttpApiBuilder.securitySetCookie(security, Redacted.make("keep me secret")))) +``` + +# Using Services Inside a HttpApiEndpoint + +Handlers can access any Effect service. Because `HttpApiBuilder.group` returns an `Effect`, you can `yield*` services directly inside your handler logic. + +**Example** (Using Services in a Endpoint Implementation) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Context, Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +// Define the UsersRepository service +class UsersRepository extends Context.Service Effect.Effect +}>()("UsersRepository") {} + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.gen(function*() { + // Access the UsersRepository service + const repository = yield* UsersRepository + return yield* repository.findById(id) + }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + Layer.provide( + Layer.succeed(UsersRepository, { + findById: (id) => Effect.succeed({ id, name: `User ${id}` }) + }) + ), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +# OpenAPI Documentation + +Add interactive API documentation with `HttpApiScalar` (Scalar UI) or `HttpApiSwagger` (Swagger UI). Both read your API definition and generate a browsable docs page at `/docs`. + +**Example** (Adding Scalar Documentation to an API) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const IdParam = Schema.Int + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Users") + .add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User) + }), + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: IdParam + }, + success: User + }), + HttpApiEndpoint.post("createUser", "/user", { + payload: User, + success: User + }), + HttpApiEndpoint.delete("deleteUser", "/user/:id", { + params: { + id: IdParam + } + }), + HttpApiEndpoint.patch("updateUser", "/user/:id", { + params: { + id: IdParam + }, + // Specify the schema for the request payload + payload: Schema.Struct({ + name: Schema.String // Only the name can be updated + }), + // Specify the schema for a successful response + success: User + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers + .handle("getUsers", () => + Effect.succeed( + [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }] + )) + .handle("getUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) + .handle("createUser", (ctx) => { + const user = ctx.payload + return Effect.succeed(user) + }) + .handle("deleteUser", (ctx) => { + const id = ctx.params.id + return Effect.log(`Deleting user ${id}`) + }) + .handle("updateUser", (ctx) => { + const id = ctx.params.id + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), // "/docs" is the default path. + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) +``` + +After running the server, open your browser and navigate to http://localhost:3000/docs. + +This URL will display the Scalar documentation, allowing you to explore the API's endpoints, request parameters, and response structures interactively. + +## Adding OpenAPI Annotations + +Annotations let you enrich the generated OpenAPI spec with titles, descriptions, server URLs, and more. They are added via the `.annotate` method on `HttpApi`, `HttpApiGroup`, or `HttpApiEndpoint`. + +#### HttpApi + +Below is a list of available annotations for a top-level `HttpApi`. They can be added using the `.annotate` method: + +| Annotation | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `HttpApi.AdditionalSchemas` | Adds custom schemas to the final OpenAPI specification. Only schemas with an `identifier` annotation are included. | +| `OpenApi.Description` | Sets a general description for the API. | +| `OpenApi.Title` | Sets the title of the API. | +| `OpenApi.Version` | Sets the version of the API. | +| `OpenApi.License` | Defines the license used by the API. | +| `OpenApi.Summary` | Provides a brief summary of the API. | +| `OpenApi.Servers` | Lists server URLs and optional metadata such as variables. | +| `OpenApi.Override` | Merges the supplied fields into the resulting specification. | +| `OpenApi.Transform` | Allows you to modify the final specification with a custom function. | + +**Example** (Annotating the Top-Level API) + +```ts +import { Schema } from "effect" +import { HttpApi, OpenApi } from "effect/unstable/httpapi" + +const api = HttpApi.make("api") + // Provide additional schemas + .annotate(HttpApi.AdditionalSchemas, [ + Schema.String.annotate({ identifier: "MyString" }) + ]) + // Add a description + .annotate(OpenApi.Description, "my description") + // Set license information + .annotate(OpenApi.License, { name: "MIT", url: "http://example.com" }) + // Provide a summary + .annotate(OpenApi.Summary, "my summary") + // Define servers + .annotate(OpenApi.Servers, [ + { + url: "http://example.com", + description: "example", + variables: { a: { default: "b", enum: ["c"], description: "d" } } + } + ]) + // Override parts of the generated specification + .annotate(OpenApi.Override, { + tags: [{ name: "a", description: "a-description" }] + }) + // Apply a transform function to the final specification + .annotate(OpenApi.Transform, (spec) => ({ + ...spec, + tags: [...spec.tags, { name: "b", description: "b-description" }] + })) + +// Generate the OpenAPI specification from the annotated API +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1", + "description": "my description", + "license": { + "name": "MIT", + "url": "http://example.com" + }, + "summary": "my summary" + }, + "paths": {}, + "components": { + "schemas": { + "MyString": { + "type": "string" + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "a", + "description": "a-description" + }, + { + "name": "b", + "description": "b-description" + } + ], + "servers": [ + { + "url": "http://example.com", + "description": "example", + "variables": { + "a": { + "default": "b", + "enum": [ + "c" + ], + "description": "d" + } + } + } + ] +} +*/ +``` + +#### HttpApiGroup + +The following annotations can be added to an `HttpApiGroup`: + +| Annotation | Description | +| ---------------------- | --------------------------------------------------------------------- | +| `OpenApi.Description` | Sets a description for this group. | +| `OpenApi.ExternalDocs` | Provides external documentation links for the group. | +| `OpenApi.Override` | Merges specified fields into the resulting specification. | +| `OpenApi.Transform` | Lets you modify the final group specification with a custom function. | +| `OpenApi.Exclude` | Excludes the group from the final OpenAPI specification. | + +**Example** (Annotating a Group) + +```ts +import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const api = HttpApi.make("api") + .add( + HttpApiGroup.make("group") + // Add a description for the group + .annotate(OpenApi.Description, "my description") + // Provide external documentation links + .annotate(OpenApi.ExternalDocs, { + url: "http://example.com", + description: "example" + }) + // Override parts of the final output + .annotate(OpenApi.Override, { name: "my name" }) + // Transform the final specification for this group + .annotate(OpenApi.Transform, (spec) => ({ + ...spec, + name: spec.name + "-transformed" + })) + ) + .add( + HttpApiGroup.make("excluded") + // Exclude the group from the final specification + .annotate(OpenApi.Exclude, true) + ) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": {}, + "components": { + "schemas": {}, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "my name-transformed", + "description": "my description", + "externalDocs": { + "url": "http://example.com", + "description": "example" + } + } + ] +} +*/ +``` + +#### HttpApiEndpoint + +For an `HttpApiEndpoint`, you can use the following annotations: + +| Annotation | Description | +| ---------------------- | --------------------------------------------------------------------------- | +| `OpenApi.Description` | Adds a description for this endpoint. | +| `OpenApi.Summary` | Provides a short summary of the endpoint's purpose. | +| `OpenApi.Deprecated` | Marks the endpoint as deprecated. | +| `OpenApi.ExternalDocs` | Supplies external documentation links for the endpoint. | +| `OpenApi.Override` | Merges specified fields into the resulting specification for this endpoint. | +| `OpenApi.Transform` | Lets you modify the final endpoint specification with a custom function. | +| `OpenApi.Exclude` | Excludes the endpoint from the final OpenAPI specification. | + +**Example** (Annotating an Endpoint) + +```ts +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const api = HttpApi.make("api").add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("get", "/", { + success: Schema.String + }) + // Add a description + .annotate(OpenApi.Description, "my description") + // Provide a summary + .annotate(OpenApi.Summary, "my summary") + // Mark the endpoint as deprecated + .annotate(OpenApi.Deprecated, true) + // Provide external documentation + .annotate(OpenApi.ExternalDocs, { + url: "http://example.com", + description: "example" + }) + ) + .add( + HttpApiEndpoint.get("excluded", "/excluded", { + success: Schema.String + }) + // Exclude this endpoint from the final specification + .annotate(OpenApi.Exclude, true) + ) +) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec, null, 2)) +/* +Output: +{ + "openapi": "3.1.0", + "info": { + "title": "Api", + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/String_" + } + } + } + }, + "400": { + "description": "The request or response did not match the expected schema", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "HttpApiSchemaError" + ] + }, + "message": { + "$ref": "#/components/schemas/String_" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + } + } + } + } + }, + "description": "my description", + "summary": "my summary", + "deprecated": true, + "externalDocs": { + "url": "http://example.com", + "description": "example" + } + } + } + }, + "components": { + "schemas": { + "String_": { + "type": "string" + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "group" + } + ] +} +*/ +``` + +The default response description is "Success". You can override this by annotating the schema. + +**Example** (Defining a custom response description) + +```ts +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const User = Schema.Struct({ + id: Schema.Finite, + name: Schema.String +}).annotate({ identifier: "User" }) + +const api = HttpApi.make("api").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getUsers", "/users", { + success: Schema.Array(User).annotate({ + description: "Returns an array of users" + }) + }) + ) +) + +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec.paths, null, 2)) +/* +Output: +{ + "/users": { + "get": { + "tags": [ + "group" + ], + "operationId": "group.getUsers", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Returns an array of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "$ref": "#/components/schemas/String_" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "description": "Returns an array of users" + } + } + } + }, + "400": { + "description": "The request or response did not match the expected schema", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "HttpApiSchemaError" + ] + }, + "message": { + "$ref": "#/components/schemas/String_" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + } + } + } + } + } + } + } +} +*/ +``` + +## Top Level Groups + +When a group is `topLevel`, its name is not prepended to operation IDs in the OpenAPI spec. Use this when the group is just for tagging and you want shorter, cleaner operation IDs. + +**Example** (Using a Top-Level Group) + +```ts +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const api = HttpApi.make("api").add( + // Mark the group as top-level + HttpApiGroup.make("group", { topLevel: true }).add( + HttpApiEndpoint.get("get", "/", { + success: Schema.String + }) + ) +) + +// Generate the OpenAPI spec +const spec = OpenApi.fromApi(api) + +console.log(JSON.stringify(spec.paths, null, 2)) +/* +Output: +{ + "/": { + "get": { // The operation ID is not prefixed with "group" + "tags": [ + "group" + ], + "operationId": "get", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/String_" + } + } + } + }, + "400": { + "description": "The request or response did not match the expected schema", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "HttpApiSchemaError" + ] + }, + "message": { + "$ref": "#/components/schemas/String_" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + } + } + } + } + } + } + } +} +*/ +``` + +# Deriving a Client + +The `HttpApiClient` module generates a fully typed client from your API definition. Each endpoint becomes a method — grouped by `HttpApiGroup` name — so calling your API is as simple as calling a function. + +**Example** (Deriving and Using a Client) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { FetchHttpClient } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Greetings") + .add( + HttpApiEndpoint.get("hello", "/", { + success: Schema.String + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Greetings", + (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!")) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// Create a program that derives and uses the client +const program = Effect.gen(function*() { + // Derive the client + const client = yield* HttpApiClient.make(Api, { + baseUrl: "http://localhost:3000" + }) + // Call the "hello-world" endpoint + const hello = yield* client.Greetings.hello() + console.log(hello) +}) + +// Provide a Fetch-based HTTP client and run the program +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +/* +Output: +[18:55:26.051] INFO (#2): Listening on http://0.0.0.0:3000 +[18:55:26.057] INFO (#12) http.span=2ms: Sent HTTP response { 'http.method': 'GET', 'http.url': '/', 'http.status': 200 } +Hello, World! +*/ +``` + +## Top Level Groups + +When a group is `topLevel`, its endpoints are exposed as top-level methods on the client instead of being nested under the group name. + +**Example** (Using a Top-Level Group in the Client) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { FetchHttpClient } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const Api = HttpApi.make("MyApi") + .add( + HttpApiGroup.make("Greetings", { topLevel: true }) + .add( + HttpApiEndpoint.get("hello", "/", { + success: Schema.String + }) + ) + ) + +const GroupLive = HttpApiBuilder.group( + Api, + "Greetings", + (handlers) => handlers.handle("hello", () => Effect.succeed("Hello, World!")) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +const program = Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api, { + baseUrl: "http://localhost:3000" + }) + // The `hello` method is not nested under the "group" name + const hello = yield* client.hello() + console.log(hello) +}) + +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +``` + +# Converting to a Web Handler + +If you need to plug your API into an existing HTTP server (instead of using `NodeHttpServer`), convert it to a standard web handler with `HttpApiBuilder.toWebHandler`. The returned `handler` function takes a `Request` and returns a `Response`. + +**Example** (Creating and Serving a Web Handler) + +```ts +import { Effect, Layer, Schema } from "effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiScalar } from "effect/unstable/httpapi" +import * as http from "node:http" + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("get", "/", { + success: Schema.String + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "group", + (handlers) => handlers.handle("get", () => Effect.succeed("Hello, world!")) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + Layer.provide(HttpApiScalar.layer(Api)), + Layer.provide(HttpServer.layerServices) +) + +// Convert the API to a web handler +const { dispose, handler } = HttpRouter.toWebHandler( + Layer.mergeAll(ApiLive) +) + +// Serving the handler using a custom HTTP server +http + .createServer(async (req, res) => { + const url = `http://${req.headers.host}${req.url}` + const init: RequestInit = { + method: req.method! + } + + const response = await handler(new Request(url, init)) + + res.writeHead( + response.status, + response.statusText, + Object.fromEntries(response.headers.entries()) + ) + const responseBody = await response.arrayBuffer() + res.end(Buffer.from(responseBody)) + }) + .listen(3000, () => { + console.log("Server running at http://localhost:3000/") + }) + .on("close", () => { + dispose() + }) +``` diff --git a/repos/effect/packages/effect/LICENSE b/repos/effect/packages/effect/LICENSE new file mode 100644 index 0000000..be1f5c1 --- /dev/null +++ b/repos/effect/packages/effect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/repos/effect/packages/effect/MCP.md b/repos/effect/packages/effect/MCP.md new file mode 100644 index 0000000..ff30493 --- /dev/null +++ b/repos/effect/packages/effect/MCP.md @@ -0,0 +1,369 @@ +## Introduction + +The `McpServer.ts` module provides an implementation of an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) server using the +[Effect](https://effect.website) eco system. + +## Getting Started + +It's important to understand the architecture of the Effect MCP server. +Here is an example of a MCP server implementation: + +```typescript +import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" +import { Effect, Layer, Logger } from "effect" +import { Schema } from "effect/schema" +import { McpServer, Tool, Toolkit } from "effect/unstable/ai" + +// Define a simple tool +const DemoTool = Tool.make("DemoTool", { + description: "A demo tool that echoes back the input", + parameters: { + message: Schema.String + }, + success: Schema.String +}) + +const MyToolkit = Toolkit.make(DemoTool) + +const DemoResource = McpServer.resource({ + uri: "file:///demo.txt", + name: "Demo Resource", + content: Effect.succeed("# Demo Content\nThis is a demo resource.") +}) + +const DemoPrompt = McpServer.prompt({ + name: "Demo Prompt", + description: "A demo prompt", + parameters: { + topic: Schema.String + }, + completion: { + topic: () => Effect.succeed(["AI", "programming", "Effect"]) + }, + content: ({ topic }) => Effect.succeed(`Tell me about ${topic}`) +}) + +const ServerLayer = Layer.mergeAll( + DemoResource, + DemoPrompt, + McpServer.toolkit(MyToolkit).pipe( + Layer.provideMerge( + MyToolkit.toLayer({ + DemoTool: ({ message }) => Effect.succeed(`Echo: ${message}`) + }) + ) + ) +).pipe( + Layer.provide( + McpServer.layerStdio({ + name: "Demo MCP Server", + version: "1.0.0", + stdin: NodeStream.stdin, + stdout: NodeSink.stdout + }) + ), + Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])) +) + +Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) +``` + +The server exposes three main parts: + +- **`Resource`**, which represents a readable MCP resource such as a file accessible to the client +- **`Prompt`**, which defines a prompt template that can be used by the client and should not be + confused with `Prompt.ts` +- **`ToolkitLayer`**, which contains the definitions of all tools the server exposes, provided with + their implementations through `ToolImplLayer`. + +The part layers are merged into one layer that has a MCP server implementation as dependency. +`McpServer.layerStdio` is used to create a standard I/O–based MCP server identified by its name and +version. Because of the layer architecture the server implementation can be easily exchanged with an +HTTP based implementation with `McpServer.layerHttp`. Finally, a logging layer is added with +`Logger.layer([Logger.consolePretty({ stderr: true })])`, ensuring logs are written to `stderr`. +This is essential when using stdio, as any output to `stdout` would interfere with the protocol +communication. + +## Resources + +Resources in the MCP server represent files or data that can be accessed by an MCP client. Each +resource is defined as a template that specifies its location, behavior, and metadata. The +`McpServer.resource` helper allows you to declaratively define such resources with dynamic +parameters, completions, and content generation. + +```typescript +import { Effect } from "effect" +import { Schema } from "effect/schema" +import { McpSchema, McpServer } from "effect/unstable/ai" + +const SimpleResource = McpServer.resource({ + uri: "file:///demo.txt", + name: "Demo Resource", + description: "A simple demo resource", + mimeType: "text/plain", + content: Effect.succeed("This is demo content") +}) + +const idParam = McpSchema.param("id", Schema.NumberFromString) + +const TemplateResource = McpServer.resource`file://path/to/file/${idParam}`({ + name: "Demo Resource Template", + description: "A parameterized resource template", + completion: { + id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) + }, + content: Effect.fn(function*(_uri, id) { + return `# MCP Server Demo - ID: ${id}` + }), + mimeType: "text/x-markdown", + audience: ["assistant", "user"] +}) +``` + +In this example, the resource is parameterized by an `id` that forms part of the URI. The +`completion` function enables clients to request valid parameter values dynamically. The `content` +function defines how the resource's data is generated at runtime—in this case, returning a Markdown +string containing the provided `id`. The `mimeType` specifies the format of the resource, while the +`audience` property determines who can access it (either `"assistant"` and/or `"user"`). + +## Prompts + +Prompts define reusable templates that an MCP client can invoke with parameters. They serve as +structured, parameterized instructions or messages that the client can send to the server. Using +`McpServer.prompt`, you can describe the prompt's schema, auto-completion behavior, and content +generation logic in a declarative way. + +```typescript +import { Effect } from "effect" +import { Schema } from "effect/schema" +import { McpServer } from "effect/unstable/ai" + +const DemoPrompt = McpServer.prompt({ + name: "Demo Prompt", + description: "A demo prompt to demonstrate MCP server capabilities", + parameters: { + name: Schema.String + }, + completion: { + name: () => Effect.succeed(["Tom", "Tim", "Jerry"]) + }, + content: ({ name }) => Effect.succeed(`Use the greetings tool to write a greeting for ${name}.`) +}) +``` + +In this example, the prompt defines a single parameter, `name`. The `completion` property provides +an auto-completion mechanism, allowing the client to suggest or autofill common names. The `content` +function then generates the actual prompt text dynamically based on the provided parameter. + +## Tools and Toolkit + +Tools define executable capabilities that the MCP server exposes to clients. Each tool describes a +contract while the actual logic is provided separately through an implementation layer. Tools are +grouped into toolkits, which can be combined and converted into layers. + +```typescript +import { Effect, Layer } from "effect" +import { Schema } from "effect/schema" +import { McpServer, Tool, Toolkit } from "effect/unstable/ai" + +const DemoTool = Tool.make("DemoTool", { + description: "This is a demo tool for the documentation", + parameters: { + demoId: Schema.Number, + demoName: Schema.String + }, + success: Schema.String +}) + +const OtherDemoTool = Tool.make("OtherDemoTool", { + description: "Another demo tool", + parameters: { + value: Schema.Number + }, + success: Schema.String +}) + +const MyToolkit = Toolkit.make(DemoTool, OtherDemoTool) + +const ToolkitLayer = McpServer.toolkit(MyToolkit).pipe( + Layer.provideMerge( + MyToolkit.toLayer({ + DemoTool: ({ demoId, demoName }) => Effect.succeed(`Processed ${demoName} with ID ${demoId}`), + OtherDemoTool: ({ value }) => Effect.succeed(`Other tool result: ${value * 2}`) + }) + ) +) +``` + +In this example, `Tool.make` defines new tools with typed parameters and result schemas for success +outcomes. Multiple tools can be grouped into a single `Toolkit` using `Toolkit.make`. + +The toolkit is then transformed into a layer defining the interface of the tools using +`McpServer.toolkit()`. The corresponding implementations are attached using `.toLayer`, which binds +each tool definition to its concrete logic. Finally, the completed toolkit layer can be merged with +other layers to create the MCP server. + +## Elicitation requests + +Elicitation requests are used to request additional input directly from the user. An elicitation +defines both the message shown to the user and the expected response schema, ensuring structured and +validated user input. + +```typescript +import { Effect } from "effect" +import { Schema } from "effect/schema" +import { McpServer } from "effect/unstable/ai" + +const DemoElicitation = McpServer.elicit({ + message: `Please answer the question ("yes" | "no") (default "no"):`, + schema: Schema.Struct({ + answer: Schema.Union([Schema.Literal("yes"), Schema.Literal("no")]) + }) +}).pipe( + Effect.catchTag("ElicitationDeclined", (_error) => { + return Effect.succeed({ answer: "no" }) + }) +) +``` + +In this example, the server poses a simple yes/no question to the user. The input is validated +against the defined schema, ensuring that only `"yes"` or `"no"` responses are accepted. If the user +declines to answer or the elicitation fails, a fallback value is provided—here, the default answer +is `"no"`. + +## Complete Working Example + +Here's a complete, copy/pastable MCP server example that combines all the concepts: + +```typescript +import { NodeRuntime, NodeStdio } from "@effect/platform-node" +import { Effect, Layer, Logger, Schema } from "effect" +import { McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai" + +// Define tools +const GreetTool = Tool.make("GreetTool", { + description: "Generate a greeting message", + parameters: Schema.Struct({ + name: Schema.String, + style: Schema.Union([Schema.Literal("formal"), Schema.Literal("casual")]) + }), + success: Schema.String +}) + +const CalculatorTool = Tool.make("CalculatorTool", { + description: "Perform basic arithmetic operations", + parameters: Schema.Struct({ + operation: Schema.Union([ + Schema.Literal("add"), + Schema.Literal("subtract"), + Schema.Literal("multiply"), + Schema.Literal("divide") + ]), + a: Schema.Number, + b: Schema.Number + }), + success: Schema.Number +}) + +// Create toolkit +const MyToolkit = Toolkit.make(GreetTool, CalculatorTool) + +// Define a resource +const ReadmeResource = McpServer.resource({ + uri: "file:///README.md", + name: "README", + description: "Project README file", + mimeType: "text/markdown", + content: Effect.succeed("# MCP Server Demo\n\nThis is a demo MCP server built with Effect.") +}) + +// Define a parameterized resource +const idParam = McpSchema.param("id", Schema.NumberFromString) + +const UserResource = McpServer.resource`file://users/${idParam}.json`({ + name: "User Data", + description: "User information by ID", + completion: { + id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) + }, + content: Effect.fn(function*(_uri, id) { + return JSON.stringify( + { + id, + name: `User ${id}`, + email: `user${id}@example.com` + }, + null, + 2 + ) + }), + mimeType: "application/json" +}) + +// Define a prompt +const AnalysisPrompt = McpServer.prompt({ + name: "Analyze Data", + description: "Analyze data and provide insights", + parameters: { + dataType: Schema.String, + focus: Schema.Union([Schema.Literal("summary"), Schema.Literal("details")]) + }, + completion: { + dataType: () => Effect.succeed(["sales", "users", "metrics"]), + focus: () => Effect.succeed(["summary" as const, "details" as const]) + }, + content: ({ dataType, focus }) => + Effect.succeed( + `Please analyze the ${dataType} data and provide a ${focus} analysis. Use available tools to gather information.` + ) +}) + +// Create the server layer +const ServerLayer = Layer.mergeAll( + ReadmeResource, + UserResource, + AnalysisPrompt, + McpServer.toolkit(MyToolkit).pipe( + Layer.provideMerge( + MyToolkit.toLayer({ + GreetTool: ({ name, style }) => { + const greeting = style === "formal" + ? `Good day, ${name}. It is a pleasure to meet you.` + : `Hey ${name}! What's up?` + return Effect.succeed(greeting) + }, + CalculatorTool: ({ operation, a, b }) => { + let result: number + switch (operation) { + case "add": + result = a + b + break + case "subtract": + result = a - b + break + case "multiply": + result = a * b + break + case "divide": + result = a / b + break + } + return Effect.succeed(result) + } + }) + ) + ) +).pipe( + Layer.provide( + McpServer.layerStdio({ + name: "Demo MCP Server", + version: "1.0.0" + }) + ), + Layer.provide(NodeStdio.layer), + Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) +) + +// Run the server +Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) +``` diff --git a/repos/effect/packages/effect/OPTIC.md b/repos/effect/packages/effect/OPTIC.md new file mode 100644 index 0000000..c424778 --- /dev/null +++ b/repos/effect/packages/effect/OPTIC.md @@ -0,0 +1,620 @@ +## Introduction + +`effect/Optic` provides tools for building and composing functional optics. + +Functional optics let you focus on parts of immutable data structures to read or update them in a safe, composable way. + +Immutability keeps previous references valid after an update. This is useful in many domains, not only in concurrent programs. + +## Mental model + +Think of an optic as a reusable focus into a nested structure. It behaves like a pure, composable "getter + setter": + +- **get** a focused value (or no value if the focus does not exist) +- **replace** the focused value +- **modify** the focused value with a function + +You build small optics and compose them to reach deeper fields, optional data, or union variants. + +## Glossary + +- **Iso**: reversible focus between two types, like a lossless conversion. +- **Lens**: focus on a field that is always present. +- **Prism**: focus on one case of a union. +- **Optional**: focus that might or might not exist. +- **Traversal**: focus on zero or more items inside a collection. + +## Features + +- **Unified representation of optics.** All optics compose the same way because they share a single data type: `Optional`. +- **Integration.** Generate `Iso` values from schemas with `Schema.toIso`. + +## Known Limitations + +The `Optic` module only works with **plain JavaScript objects** and collections (structs, records, tuples, and arrays). + +## Getting started + +These are the three operations you will use most: + +```ts +import { Optic } from "effect" + +type S = { readonly a: number } +const _a = Optic.id().key("a") + +/** + * Get the value of the focused field + */ +const value = _a.get({ a: 1 }) +console.log(value) // 1 + +/** + * Replace the value of the focused field + */ +const replaced = _a.replace(2, { a: 1 }) +console.log(replaced) // { a: 2 } + +/** + * Modify the value of the focused field + */ +const modified = _a.modify((n) => n + 1)({ a: 1 }) +console.log(modified) // { a: 2 } +``` + +### Nested data structures + +Suppose we have an employee object, and we want to capitalize the first character of the street name of the company address. + +**Example** (Uppercasing the first character of a street name) + +```ts +import { Optic, String } from "effect" + +// Define some nested data structures +interface Street { + readonly num: number + readonly name: string +} +interface Address { + readonly city: string + readonly street: Street +} +interface Company { + readonly name: string + readonly address: Address +} +interface Employee { + readonly name: string + readonly company: Company +} + +// A sample employee object +const from: Employee = { + name: "john", + company: { + name: "awesome inc", + address: { + city: "london", + street: { + num: 23, + name: "high street" + } + } + } +} + +// Build an optic that drills down to the street name +const _streetName = Optic.id() + .key("company") // access "company" + .key("address") // access "address" + .key("street") // access "street" + .key("name") // access "name" + +// Modify the targeted value +const capitalizeStreetName = _streetName.modify(String.capitalize) + +console.dir(capitalizeStreetName(from), { depth: null }) +/* +{ + name: 'john', + company: { + name: 'awesome inc', + address: { + city: 'london', + street: { num: 23, name: 'High street' } + } + } +} +*/ +``` + +## Basic Usage + +### Accessing a key in a struct or a tuple + +**Example** (Reading and updating a single struct field) + +```ts +import { Optic } from "effect" + +type S = { + readonly a: string +} + +// Build an optic to access the "a" field +const _a = Optic.id().key("a") + +console.log(_a.replace("b", { a: "a" })) +// { a: 'b' } +``` + +**Example** (Reading and updating the first element of a tuple) + +```ts +import { Optic } from "effect" + +type S = readonly [string] + +// Build an optic to access the first element +const _0 = Optic.id().key(0) + +console.log(_0.replace("b", ["a"])) +// ["b"] +``` + +### Choosing an optic quickly + +| Data shape | Use | +| --------------------------------- | ------------------------------------ | +| Always-present field | `key` | +| Optional field (keep `undefined`) | `key` | +| Optional field (drop `undefined`) | `optionalKey` | +| Union case | `tag` | +| Record or array index | `at` | +| Filter and update items | `forEach` + `check` / `notUndefined` | + +### Accessing a group of keys in a struct + +#### pick + +**Example** (Updating multiple fields with `pick`) + +```ts +import { Optic } from "effect" + +type S = { + readonly a: number + readonly b: number + readonly c: number +} + +// Build an optic to access the "a" and "c" fields +const _a = Optic.id().pick(["a", "c"]) + +console.log(_a.replace({ a: 4, c: 5 }, { a: 1, b: 2, c: 3 })) +// { a: 4, b: 2, c: 5 } +``` + +#### omit + +**Example** (Updating all fields except a set with `omit`) + +```ts +import { Optic } from "effect" + +type S = { + readonly a: number + readonly b: number + readonly c: number +} + +// Build an optic to access the "a" and "c" fields +const _a = Optic.id().omit(["b"]) + +console.log(_a.replace({ a: 4, c: 5 }, { a: 1, b: 2, c: 3 })) +// { a: 4, b: 2, c: 5 } +``` + +### Accessing an optional key in a struct or a tuple + +There are two ways to handle an optional key in a struct or a tuple, depending on how you want to treat the `undefined` value: + +1. when setting `undefined`, the key is preserved +2. when setting `undefined`, the key is removed + +**Example** (Preserving the key when setting `undefined`) + +```ts +import { Optic } from "effect" + +type S = { + readonly a?: number | undefined +} + +// Lens +const _a = Optic.id().key("a") + +console.log(String(_a.getResult({ a: 1 }))) +// success(1) + +console.log(String(_a.getResult({}))) +// success(undefined) + +console.log(String(_a.getResult({ a: undefined }))) +// success(undefined) + +console.log(_a.replace(2, { a: 1 })) +// { a: 2 } + +console.log(_a.replace(2, {})) +// { a: 2 } + +console.log(_a.replace(undefined, { a: 1 })) +// { a: undefined } + +console.log(_a.replace(undefined, {})) +// { a: undefined } + +console.log(_a.replace(2, { a: undefined })) +// { a: 2 } +``` + +**Example** (Removing the key when setting `undefined`) + +```ts +import { Optic } from "effect" + +type S = { + readonly a?: number +} + +// Lens +const _a = Optic.id().optionalKey("a") + +console.log(String(_a.getResult({ a: 1 }))) +// success(1) + +console.log(String(_a.getResult({}))) +// success(undefined) + +console.log(_a.replace(2, { a: 1 })) +// { a: 2 } + +console.log(_a.replace(2, {})) +// { a: 2 } + +console.log(_a.replace(undefined, { a: 1 })) +// {} + +console.log(_a.replace(undefined, {})) +// {} +``` + +**Example** (Dropping a tuple element when setting `undefined`) + +```ts +import { Optic } from "effect" + +type S = readonly [number, number?] + +// Build an optic to access the optional second element +const _1 = Optic.id().optionalKey(1) + +console.log(_1.get([1, 2])) +// 2 + +console.log(_1.get([1])) +// undefined + +console.log(_1.replace(3, [1, 2])) +// [1, 3] + +console.log(_1.replace(undefined, [1, 2])) +// [1] +``` + +### Accessing a key in a record or an array + +**Example** (Reading and updating a record entry) + +```ts +import { Optic } from "effect" + +type S = { [key: string]: number } + +// Build an optic to access the value at key "a" +const _a = Optic.id().at("a") + +console.log(_a.replace(2, { a: 1 })) +// { a: 2 } +``` + +**Example** (Reading and updating an array element) + +```ts +import { Optic } from "effect" + +type S = ReadonlyArray + +// Build an optic to access the first element +const _0 = Optic.id().at(0) + +console.log(_0.replace(3, [1, 2])) +// [3, 2] +``` + +### Accessing a member in a tagged union + +**Aside** (Convention for tagged unions) +The convention is to use `"_tag"` as the field that identifies the variant. + +**Example** (Focusing a field inside one variant) + +```ts +import { Optic } from "effect" + +// A union of two tagged types +type S = + | { + readonly _tag: "A" + readonly a: number + } + | { + readonly _tag: "B" + readonly b: number + } + +// Build an optic that focuses on the "a" field of the "A" variant +const _a = Optic.id().tag("A").key("a") + +console.log(_a.replace(2, { _tag: "A", a: 1 })) +// { _tag: 'A', a: 2 } + +console.log(_a.replace(2, { _tag: "B", b: 1 })) // no match, so no change +// { _tag: 'B', b: 1 } +``` + +### Traversing a collection + +**Example** (Incrementing only positive numbers in an array field) + +```ts +import { Optic, Schema } from "effect" + +type S = { + readonly a: ReadonlyArray +} + +// Build an optic that focuses the field "a" and then +// narrows the focus to elements that pass the positivity check +const _positive = Optic.id() + .key("a") // focus the "a" array + .forEach((item) => item.check(Schema.isGreaterThan(0))) // keep only positive elements + +// Create a function that increments only the focused elements +const addOne = _positive.modifyAll((n) => n + 1) + +console.log(addOne({ a: [1, -2, 3] })) +// { a: [ 2, -2, 4 ] } +``` + +**Technical detail** + +Unlike many optic libraries, `Traversal` is not an optic on its own. It is modeled as an `Optional` whose focus is a `ReadonlyArray`: + +```ts +export interface Traversal extends Optional> {} +``` + +To operate on each `A` inside a `Traversal`, use `forEach`. +`forEach` takes a function whose argument is an `Iso`, so you can keep drilling down by composing that `Iso` with other optics. + +### Debugging focus failures + +If a focus does not exist, `getResult` lets you see success vs failure explicitly: + +```ts +import { Optic, Result } from "effect" + +type S = { readonly a?: number } +const _a = Optic.id().at("a") + +const result = _a.getResult({}) +const message = Result.match(result, { + onSuccess: (value) => `value: ${value}`, + onFailure: () => "no focus" +}) + +console.log(message) // no focus +``` + +## Generating an Optic from a Schema + +**Example** (Generating an Optic from a Struct) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}) + +/* +const _b: Lens<{ + readonly a: string; + readonly b: number; +}, number> +*/ +const _b = Schema.toIso(schema).key("b") + +console.log(_b.replace(2, { a: "a", b: 1 })) +// { a: 'a', b: 2 } +``` + +You can also call `Schema.toIso` on custom types when their schema supplies `toCodecIso` or `toCodec` annotations. `Schema.Class` provides these, so class-based schemas work out of the box: + +**Example** (Generating an Optic from a Class schema) + +```ts +import { Schema } from "effect" + +// Define a class schema +class Person extends Schema.Class("Person")({ + name: Schema.String, + age: Schema.Number +}) {} + +const _name = Schema.toIso(Person).key("name") + +console.log(_name.replace("b", new Person({ name: "a", age: 1 }))) +// Person { name: 'b', age: 1 } +``` + +## Why use functional optics when we already have Immer? + +Immer is great: it lets you write "mutating" code that produces new immutable objects under the hood. For many teams that is enough. If you work with nested data, union types, and reusable update logic, **functional optics** (Iso, Lens, Prism, Optional, Traversal) cover use cases that Immer does not aim to address. + +Below are the main differences, with small examples. + +### Reusable focus instead of ad-hoc navigation + +**Immer:** you repeat the path to the field each time you update it. + +```ts +import { produce } from "immer" + +type S = { + readonly user: { + readonly profile: { + readonly name: string + } + } +} + +declare const state: S + +const upperName = produce(state, (draft) => { + // Navigate to the field inline + draft.user.profile.name = draft.user.profile.name.toUpperCase() +}) + +const lowerName = produce(state, (draft) => { + // Repeat the same navigation again + draft.user.profile.name = draft.user.profile.name.toLowerCase() +}) +``` + +**Optics:** define a **Lens** once, then reuse it. + +```ts +import { Optic } from "effect" + +type S = { + readonly user: { + readonly profile: { + readonly name: string + } + } +} + +// Define a reusable Lens focusing the "name" field +// Lens +const _name = Optic.id().key("user").key("profile").key("name") + +declare const state: S + +// Apply different transformations without repeating the path +const upperName = _name.modify((name) => name.toUpperCase())(state) +const lowerName = _name.modify((name) => name.toLowerCase())(state) +``` + +Why this matters: if the path changes, you update it in one place. You also get small, testable building blocks that can be shared across modules instead of repeating object navigation. + +### Declarative vs manual handling of optional data + +**Immer:** manual checks for each optional field. + +**Example** (Uppercasing titles with optional fields) + +```ts +import { produce } from "immer" + +type S = { + readonly todos?: ReadonlyArray<{ + readonly title?: string + readonly description: string + }> +} + +const state: S = { + todos: [{ title: "milk", description: "buy milk" }, { description: "buy bread" }] +} + +const next = produce(state, (draft) => { + // Guard the optional array + if (!draft.todos) return + + for (const item of draft.todos) { + // Guard the optional field + if (item.title !== undefined) { + item.title = item.title.toUpperCase() + } + } +}) + +console.log(next) +/* +{ + todos: [ + { title: 'MILK', description: 'buy milk' }, + { description: 'buy bread' } + ] +} +*/ +``` + +**Optics:** declare the focus; types carry the safety. + +**Example** (Uppercasing titles with declarative focus) + +```ts +import { Optic } from "effect" + +type S = { + readonly todos?: ReadonlyArray<{ + readonly title?: string + readonly description: string + }> +} + +const _title = Optic.id() + .key("todos") + .notUndefined() // proceed only if 'todos' exists + .forEach((item) => item.key("title").notUndefined()) // proceed only if 'title' exists + +const state: S = { + todos: [{ title: "milk", description: "buy milk" }, { description: "buy bread" }] +} + +// Modify only the focused values (titles) +console.log(_title.modifyAll((title) => title.toUpperCase())(state)) +/* +{ + todos: [ + { title: 'MILK', description: 'buy milk' }, + { description: 'buy bread' } + ] +} +*/ +``` + +### Composition over nesting + +**Immer:** you often nest update blocks or repeat the same property paths. + +**Optics:** compose small optics into larger ones. Composition keeps code flat and readable. + +**Aside** (Reusing optics across modules) +Define an optic once (for example, a `User.address` lens) and import it wherever you need it. This avoids duplicating paths and centralizes changes when the data shape evolves. diff --git a/repos/effect/packages/effect/README.md b/repos/effect/packages/effect/README.md new file mode 100644 index 0000000..f989484 --- /dev/null +++ b/repos/effect/packages/effect/README.md @@ -0,0 +1,51 @@ +# `effect` Core Package + +The `effect` package is the heart of the Effect framework, providing robust primitives for managing side effects, ensuring type safety, and supporting concurrency in your TypeScript applications. + +## Requirements + +- **TypeScript 5.9 or Newer:** + Ensure you are using a compatible TypeScript version. + +- **Strict Type-Checking:** + The `strict` flag must be enabled in your `tsconfig.json`. For example: + + ```json + { + "compilerOptions": { + "strict": true + // ...other options + } + } + ``` + +## Installation + +Install the core package using your preferred package manager. For example, with npm: + +```bash +npm install effect +``` + +## Documentation + +- **Website:** + For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). + +- **API Reference:** + For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). + +## Overview of Effect Modules + +The `effect` package provides a collection of modules designed for functional programming in TypeScript. Below is a brief overview of the core modules: + +| Module | Description | +| -------- | -------------------------------------------------------------------------------------------------------------------------- | +| Effect | The core abstraction for managing side effects, concurrency, and error handling in a structured way. | +| Context | A lightweight dependency injection mechanism that enables passing services through computations without direct references. | +| Layer | A system for managing dependencies, allowing for modular and composable resource allocation. | +| Fiber | Lightweight virtual threads with resource-safe cancellation capabilities, enabling many features in Effect. | +| Stream | A powerful abstraction for handling asynchronous, event-driven data processing. | +| Schedule | A module for defining retry and repeat policies with composable schedules. | +| Scope | Manages the lifecycle of resources, ensuring proper acquisition and release. | +| Schema | A powerful library for defining, validating, and transforming structured data with type-safe encoding and decoding. | diff --git a/repos/effect/packages/effect/SCHEMA.md b/repos/effect/packages/effect/SCHEMA.md new file mode 100644 index 0000000..4e15382 --- /dev/null +++ b/repos/effect/packages/effect/SCHEMA.md @@ -0,0 +1,7272 @@ +# Schema + +`Schema` is a TypeScript-first library for defining data shapes, validating unknown input, and transforming values between formats. + +Two key concepts appear throughout this guide: + +- **Decoding** — turning unknown external data (API responses, form submissions, config files) into typed, validated values. +- **Encoding** — turning typed values back into a serializable format (JSON, FormData, etc.). + +Use Schema to: + +- **Define types** — declare the shape of your data once and get both the TypeScript type and a runtime validator. +- **Validate input** — decode unknown data into type-safe values, with clear error messages when it doesn't match. +- **Transform values** — convert between your domain types and serialization formats like JSON, FormData, and URLSearchParams. +- **Generate tooling** — derive JSON Schemas, test data generators, equivalence checks, and more from a single schema definition. + +## Design Philosophy + +- **Lightweight by default** — only import the features you need, keeping your bundle small. +- **Familiar API** — naming conventions and patterns are consistent with popular validation libraries, so getting started is easy. +- **Explicit** — you choose which features to use. Nothing is included implicitly. + +### What's in This Guide + +1. **Elementary schemas** — built-in schemas for primitives, literals, strings, numbers, dates, and template literals. +2. **Composite schemas** — combine elementary schemas into structs (objects), tuples, arrays, records, and unions. +3. **Validation** — add runtime checks (filters) to constrain values, report multiple errors, and define custom rules. +4. **Constructors** — create validated values at runtime, with support for defaults, brands, and refinements. +5. **Transformations** — convert values between types during decoding and encoding. Transformations are reusable objects you compose with schemas. +6. **Flipping** — swap a schema's decoding and encoding directions. +7. **Classes and opaque types** — create distinct TypeScript types backed by structs, with optional methods and equality. +8. **Serialization** — convert values to and from JSON, FormData, URLSearchParams, and XML using canonical codecs. +9. **Tooling** — generate JSON Schemas, test data generators (Arbitraries), equivalence checks, optics, and JSON Patch differs from a single schema. +10. **Error handling** — format validation errors for display, with hooks for internationalization. +11. **Middlewares** — intercept decoding/encoding to provide fallbacks or inject services. +12. **Advanced topics** — internal type model and type hierarchy (for library authors). +13. **Integrations** — working examples for TanStack Form and Elysia. +14. **Migration from v3** — API mapping from Schema v3 to v4. + +# Defining Elementary Schemas + +Schema provides built-in schemas for all common TypeScript types. These schemas represent a single value — like a string or a number — and they are the building blocks you combine into more complex shapes. + +## Primitives + +Use these schemas when a value should be exactly one of the basic JavaScript types. + +```ts +import { Schema } from "effect" + +// primitive types +Schema.String +Schema.Number +Schema.BigInt +Schema.Boolean +Schema.Symbol +Schema.Undefined +Schema.Null +``` + +Sometimes you receive data that is not the right type yet — for example, a number that should become a string. You can build a schema that converts (coerces) values to the target type during decoding: + +```ts +import { Getter, Parser, Schema } from "effect/schema" + +// ┌─── Codec +// ▼ +const schema = Schema.Unknown.pipe( + Schema.decodeTo(Schema.String, { + decode: Getter.String(), + encode: Getter.passthrough() + }) +) + +const parser = Parser.decodeUnknownSync(schema) + +console.log(parser("tuna")) // => "tuna" +console.log(parser(42)) // => "42" +console.log(parser(true)) // => "true" +console.log(parser(null)) // => "null" +``` + +## Literals + +A literal schema matches one exact value. Use it when a field must be a specific string, number, or other constant. + +```ts +import { Schema } from "effect" + +const tuna = Schema.Literal("tuna") +const twelve = Schema.Literal(12) +const twobig = Schema.Literal(2n) +const tru = Schema.Literal(true) +``` + +Symbol literals: + +```ts +import { Schema } from "effect" + +const terrific = Schema.UniqueSymbol(Symbol("terrific")) +``` + +`null`, `undefined`, and `void`: + +```ts +import { Schema } from "effect" + +Schema.Null +Schema.Undefined +Schema.Void +``` + +To allow multiple literal values: + +```ts +import { Schema } from "effect" + +const schema = Schema.Literals(["red", "green", "blue"]) +``` + +To extract the set of allowed values from a literal schema: + +```ts +import { Schema } from "effect" + +const schema = Schema.Literals(["red", "green", "blue"]) + +// readonly ["red", "green", "blue"] +schema.literals + +// readonly [Schema.Literal<"red">, Schema.Literal<"green">, Schema.Literal<"blue">] +schema.members +``` + +## Strings + +You can add validation rules to a string schema. Each rule is applied with `.check(...)` and returns a new schema that enforces that constraint. + +```ts +import { Schema } from "effect" + +Schema.String.check(Schema.isMaxLength(5)) +Schema.String.check(Schema.isMinLength(5)) +Schema.String.check(Schema.isLengthBetween(5, 5)) +Schema.String.check(Schema.isPattern(/^[a-z]+$/)) +Schema.String.check(Schema.isStartsWith("aaa")) +Schema.String.check(Schema.isEndsWith("zzz")) +Schema.String.check(Schema.isIncludes("---")) +Schema.String.check(Schema.isUppercased()) +Schema.String.check(Schema.isLowercased()) +``` + +To perform some simple string transforms: + +```ts +import { Schema, SchemaTransformation } from "effect" + +Schema.String.decode(SchemaTransformation.trim()) +Schema.String.decode(SchemaTransformation.toLowerCase()) +Schema.String.decode(SchemaTransformation.toUpperCase()) +``` + +## String formats + +Schema includes built-in checks for common string formats. + +```ts +import { Schema } from "effect" + +Schema.String.check(Schema.isUUID()) +Schema.String.check(Schema.isBase64()) +Schema.String.check(Schema.isBase64Url()) +``` + +## Numbers + +```ts +import { Schema } from "effect" + +Schema.Number // all numbers +Schema.Finite // finite numbers (i.e. not +/-Infinity or NaN) +``` + +You can add validation rules to a number schema. Each rule constrains the allowed range or value. + +```ts +import { Schema } from "effect" + +Schema.Number.check(Schema.isBetween({ minimum: 5, maximum: 10 })) +Schema.Number.check(Schema.isGreaterThan(5)) +Schema.Number.check(Schema.isGreaterThanOrEqualTo(5)) +Schema.Number.check(Schema.isLessThan(5)) +Schema.Number.check(Schema.isLessThanOrEqualTo(5)) +Schema.Number.check(Schema.isMultipleOf(5)) +``` + +## Integers + +To require that a number has no decimal part, use `isInt()`. For 32-bit integers specifically, use `isInt32()`. + +```ts +import { Schema } from "effect" + +Schema.Number.check(Schema.isInt()) +Schema.Number.check(Schema.isInt32()) +``` + +## BigInts + +Schema does not ship pre-built BigInt validation factories (unlike numbers). Instead, you create your own using helper functions and a BigInt-compatible ordering. The example below shows how. + +```ts +import { BigInt, Order, Schema } from "effect" + +const options = { order: Order.BigInt } + +const isBetween = Schema.makeIsBetween(options) +const isGreaterThan = Schema.makeIsGreaterThan(options) +const isGreaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo(options) +const isLessThan = Schema.makeIsLessThan(options) +const isLessThanOrEqualTo = Schema.makeIsLessThanOrEqualTo(options) +const isMultipleOf = Schema.makeIsMultipleOf({ + remainder: BigInt.remainder, + zero: 0n +}) + +const isPositive = isGreaterThan(0n) +const isNonNegative = isGreaterThanOrEqualTo(0n) +const isNegative = isLessThan(0n) +const isNonPositive = isLessThanOrEqualTo(0n) + +Schema.BigInt.check(isBetween({ minimum: 5n, maximum: 10n })) +Schema.BigInt.check(isGreaterThan(5n)) +Schema.BigInt.check(isGreaterThanOrEqualTo(5n)) +Schema.BigInt.check(isLessThan(5n)) +Schema.BigInt.check(isLessThanOrEqualTo(5n)) +Schema.BigInt.check(isMultipleOf(5n)) +Schema.BigInt.check(isPositive) +Schema.BigInt.check(isNonNegative) +Schema.BigInt.check(isNegative) +Schema.BigInt.check(isNonPositive) +``` + +## Dates + +The `Schema.Date` schema matches `Date` objects (even invalid dates). + +If you want to validate only valid dates, use `Schema.DateValid` instead. + +## Template literals + +You can use `Schema.TemplateLiteral` to define structured string patterns made of multiple parts. Each part can be a literal or a schema, and **additional constraints** (such as `isMinLength` or `isMaxLength`) can be applied to individual parts. + +Template literal matching is based on the semantics of each part rather than only a generated regular expression. Checks on string, number, and bigint schema parts are applied while matching each segment. + +**Example** (Constraining parts of an email-like string) + +```ts +import { Schema } from "effect" + +// Construct a template literal schema for values like `${string}@${string}` +// Apply constraints to both sides of the "@" symbol +const email = Schema.TemplateLiteral([ + // Left part: must be a non-empty string + Schema.String.check(Schema.isMinLength(1)), + + // Separator + "@", + + // Right part: must be a string with a maximum length of 64 + Schema.String.check(Schema.isMaxLength(64)) +]) + +// The inferred type is `${string}@${string}` +export type Type = typeof email.Type + +console.log(String(Schema.decodeUnknownExit(email)("a@b.com"))) +/* +Success("a@b.com") +*/ + +console.log(String(Schema.decodeUnknownExit(email)("@b.com"))) +/* +Failure(Cause([Fail(SchemaError(Expected a string matching template literal parts, got "@b.com"))])) +*/ +``` + +### Template literal parser + +If you want to extract the parts of a string that match a template, you can use `Schema.TemplateLiteralParser`. This allows you to parse the input into its individual components rather than treat it as a single string. + +**Example** (Parsing a template literal into components) + +```ts +import { Schema } from "effect" + +const schema = Schema.TemplateLiteralParser([ + Schema.String.check(Schema.isMinLength(2)), + ":", + Schema.Int +]) + +// The inferred type is `readonly [string, ":", number]` +export type Type = typeof schema.Type + +console.log(String(Schema.decodeUnknownExit(schema)("aa:1"))) +// Success(["aa",":",1]) + +console.log(String(Schema.decodeUnknownExit(schema)("a:1"))) +// Failure(Cause([Fail(SchemaError(Expected a value with a length of at least 2, got "a" +// at [0]))])) + +console.log(String(Schema.decodeUnknownExit(schema)("aa:1.2"))) +// Failure(Cause([Fail(SchemaError(Expected an integer, got 1.2 +// at [2]))])) +``` + +# Defining Composite Schemas + +Once you have elementary schemas, you can combine them into composite schemas that describe objects, arrays, tuples, key-value maps, and unions. + +## Structs + +A struct schema describes a JavaScript object with a known set of keys. Each key maps to a schema that validates and types its value. + +### Optional and Mutable Keys + +By default, every key in a struct is required and readonly. Use `Schema.optionalKey` to make a key optional (the key can be absent from the object), and `Schema.mutableKey` to make it writable. + +You can mark struct properties as optional or mutable using `Schema.optionalKey` and `Schema.mutableKey`. + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.optionalKey(Schema.String), + c: Schema.mutableKey(Schema.String), + d: Schema.optionalKey(Schema.mutableKey(Schema.String)) +}) + +/* +with "exactOptionalPropertyTypes": true + +type Type = { + readonly a: string; + readonly b?: string; + c: string; + d?: string; +} +*/ +type Type = (typeof schema)["Type"] +``` + +### Optional Fields + +There are several ways to represent optional properties, depending on whether you want `undefined` in the type, `null` in the type, or just a missing key. By combining `Schema.optionalKey`, `Schema.optional`, and `Schema.NullOr`, you can represent any variant. + +```ts +import { Schema } from "effect" + +export const schema = Schema.Struct({ + // Exact Optional Property + a: Schema.optionalKey(Schema.FiniteFromString), + // Optional Property + b: Schema.optional(Schema.FiniteFromString), + // Exact Optional Property with Nullability + c: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)), + // Optional Property with Nullability + d: Schema.optional(Schema.NullOr(Schema.FiniteFromString)) +}) + +/* +type Encoded = { + readonly a?: string; + readonly b?: string | undefined; + readonly c?: string | null; + readonly d?: string | null | undefined; +} +*/ +type Encoded = typeof schema.Encoded + +/* +type Type = { + readonly a?: number; + readonly b?: number | undefined; + readonly c?: number | null; + readonly d?: number | null | undefined; +} +*/ +type Type = typeof schema.Type +``` + +#### Omitting Values When Transforming Optional Fields + +If an optional field arrives as `undefined`, you may want to omit it from the output entirely rather than keeping it. + +```ts +import { Option, Predicate, Schema, SchemaGetter } from "effect" + +export const schema = Schema.Struct({ + a: Schema.optional(Schema.FiniteFromString).pipe( + Schema.decodeTo(Schema.optionalKey(Schema.Number), { + decode: SchemaGetter.transformOptional( + Option.filter(Predicate.isNotUndefined) // omit undefined + ), + encode: SchemaGetter.passthrough() + }) + ) +}) + +/* +type Encoded = { + readonly a?: string | undefined; +} +*/ +type Encoded = typeof schema.Encoded + +/* +type Type = { + readonly a?: number; +} +*/ +type Type = typeof schema.Type +``` + +#### Representing Optional Fields with never Type + +You can use `Schema.Never` inside an optional key to represent a field that should never have a value but may still appear as a key in the type. + +```ts +import { Schema } from "effect" + +export const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.Never) +}) + +/* +type Encoded = { + readonly a?: never; +} +*/ +type Encoded = typeof schema.Encoded + +/* +type Type = { + readonly a?: never; +} +*/ +type Type = typeof schema.Type +``` + +### Decoding Defaults + +You can assign default values to fields during decoding using: + +| API | Encoded side | Default value type | +| ----------------------------------- | ------------------------- | ------------------ | +| `Schema.withDecodingDefaultKey` | key absent | `Encoded` | +| `Schema.withDecodingDefault` | key absent or `undefined` | `Encoded` | +| `Schema.withDecodingDefaultTypeKey` | key absent | `Type` | +| `Schema.withDecodingDefaultType` | key absent or `undefined` | `Type` | + +The "Key" variants use `optionalKey` (the key may be absent but not `undefined`), while the non-"Key" variants use `optional` (the key may be absent **or** `undefined`). + +The "Type" variants accept a default specified as a `Type` (decoded) value, which is useful when the schema has a transformation and you want to provide the default in the decoded representation. + +#### Encoded-Side Defaults + +`withDecodingDefaultKey` and `withDecodingDefault` accept a default specified as an +**`Encoded` value** (before any decoding transformation). This is the most common +case and works well when the Encoded and Type representations are the same, or +when you already have the value in encoded form. + +**Example** (Default as an Encoded value) + +In `FiniteFromString`, the `Encoded` type is `string` and the `Type` is `number`. +The default `"1"` is a **string** (the Encoded type), which is then decoded to `1`. + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + // ┌─── "1" is a string (Encoded type) + // ▼ + a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"))) +}) + +// ┌─── { readonly a?: string | undefined; } +// ▼ +type Encoded = typeof schema.Encoded + +// ┌─── { readonly a: number; } +// ▼ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: undefined })) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: "2" })) +// Output: { a: 2 } +``` + +#### Type-Side Defaults + +`withDecodingDefaultTypeKey` and `withDecodingDefaultType` accept a default +specified as a **`Type` value** (the decoded representation). This is useful when +the schema has a transformation and you want to provide the default directly as a +decoded value, bypassing the decoding step. + +**Example** (Default as a Type value) + +Here the default `1` is a **number** (the Type), not a string. It does not go +through the `FiniteFromString` decoding transformation. + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + // ┌─── 1 is a number (Type) + // ▼ + a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1))) +}) + +// ┌─── { readonly a?: string | undefined; } +// ▼ +type Encoded = typeof schema.Encoded + +// ┌─── { readonly a: number; } +// ▼ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: undefined })) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: "2" })) +// Output: { a: 2 } +``` + +#### Nested Decoding Defaults + +You can also apply decoding defaults within nested structures. + +**Example** (Nested struct with defaults for missing or undefined fields) + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.Struct({ + b: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"))) + }).pipe(Schema.withDecodingDefault(Effect.succeed({}))) +}) + +/* +type Encoded = { + readonly a?: { + readonly b?: string | undefined; + } | undefined; +} +*/ +type Encoded = typeof schema.Encoded + +/* +type Type = { + readonly a: { + readonly b: number; + }; +} +*/ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { a: { b: 1 } } + +console.log(Schema.decodeUnknownSync(schema)({ a: undefined })) +// Output: { a: { b: 1 } } + +console.log(Schema.decodeUnknownSync(schema)({ a: {} })) +// Output: { a: { b: 1 } } + +console.log(Schema.decodeUnknownSync(schema)({ a: { b: undefined } })) +// Output: { a: { b: 1 } } + +console.log(Schema.decodeUnknownSync(schema)({ a: { b: "2" } })) +// Output: { a: { b: 2 } } +``` + +### Manual Decoding Defaults + +If the defaulting logic is more specific than just handling `undefined` or missing values, you can use `Schema.decodeTo` to apply custom fallback rules. + +This is useful when you need to account for values like `null` or other invalid states. + +**Example** (Providing a fallback when value is `null` or missing) + +```ts +import { Option, Predicate, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.NullOr(Schema.String)).pipe( + Schema.decodeTo(Schema.FiniteFromString, { + decode: SchemaGetter.transformOptional((oe) => + oe.pipe( + // remove null values + Option.filter(Predicate.isNotNull), + // default to "1" if none + Option.orElseSome(() => "1") + ) + ), + encode: SchemaGetter.passthrough() + }) + ) +}) + +// ┌─── { readonly a?: string | null; } +// ▼ +type Encoded = typeof schema.Encoded + +// ┌─── { readonly a: number; } +// ▼ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { a: 1 } + +// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) +// throws + +console.log(Schema.decodeUnknownSync(schema)({ a: null })) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: "2" })) +// Output: { a: 2 } +``` + +**Example** (Providing a fallback when value is `null`, `undefined`, or missing) + +```ts +import { Option, Predicate, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.optional(Schema.NullOr(Schema.String)).pipe( + Schema.decodeTo(Schema.FiniteFromString, { + decode: SchemaGetter.transformOptional((oe) => + oe.pipe( + // remove null and undefined + Option.filter(Predicate.isNotNullish), + // default to "1" if none + Option.orElseSome(() => "1") + ) + ), + encode: SchemaGetter.passthrough() + }) + ) +}) + +// ┌─── { readonly a?: string | null | undefined; } +// ▼ +type Encoded = typeof schema.Encoded + +// ┌─── { readonly a: number; } +// ▼ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: undefined })) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: null })) +// Output: { a: 1 } + +console.log(Schema.decodeUnknownSync(schema)({ a: "2" })) +// Output: { a: 2 } +``` + +### Optional Fields as Options + +Effect's `Option` type is a safer alternative to `undefined` for representing the presence or absence of a value. These helpers convert between optional struct fields and `Option` values. + +#### Exact Optional Property + +```ts +import { Option, Schema } from "effect" + +const Product = Schema.Struct({ + quantity: Schema.OptionFromOptionalKey(Schema.FiniteFromString) +}) + +// ┌─── { readonly quantity?: string; } +// ▼ +type Encoded = typeof Product.Encoded + +// ┌─── { readonly quantity: Option; } +// ▼ +type Type = typeof Product.Type + +console.log(Schema.decodeUnknownSync(Product)({})) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) +// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } + +// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) +// throws + +console.log(Schema.encodeSync(Product)({ quantity: Option.some(2) })) +// Output: { quantity: "2" } + +console.log(Schema.encodeSync(Product)({ quantity: Option.none() })) +// Output: {} +``` + +#### Optional Property + +```ts +import { Option, Schema } from "effect" + +const Product = Schema.Struct({ + quantity: Schema.OptionFromOptional(Schema.FiniteFromString) +}) + +// ┌─── { readonly quantity?: string | undefined; } +// ▼ +type Encoded = typeof Product.Encoded + +// ┌─── { readonly quantity: Option; } +// ▼ +type Type = typeof Product.Type + +console.log(Schema.decodeUnknownSync(Product)({})) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) +// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.encodeSync(Product)({ quantity: Option.some(2) })) +// Output: { quantity: "2" } + +console.log(Schema.encodeSync(Product)({ quantity: Option.none() })) +// Output: {} +``` + +#### Exact Optional Property with Nullability + +```ts +import { Option, Predicate, Schema, SchemaTransformation } from "effect" + +const Product = Schema.Struct({ + quantity: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)).pipe( + Schema.decodeTo( + Schema.Option(Schema.Number), + SchemaTransformation.transformOptional({ + decode: (oe) => oe.pipe(Option.filter(Predicate.isNotNull), Option.some), + encode: Option.flatten + }) + ) + ) +}) + +// ┌─── { readonly quantity?: string | null; } +// ▼ +type Encoded = typeof Product.Encoded + +// ┌─── { readonly quantity: Option; } +// ▼ +type Type = typeof Product.Type + +console.log(Schema.decodeUnknownSync(Product)({})) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) +// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } + +// console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) +// throws +``` + +#### Optional Property with Nullability + +```ts +import { Schema } from "effect" + +const Product = Schema.Struct({ + quantity: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString) +}) + +// ┌─── { readonly quantity?: string | null | undefined; } +// ▼ +type Encoded = typeof Product.Encoded + +// ┌─── { readonly quantity: Option; } +// ▼ +type Type = typeof Product.Type + +console.log(Schema.decodeUnknownSync(Product)({})) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) +// Output: { quantity: { _id: 'Option', _tag: 'None' } } + +console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) +// Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } +``` + +### Key Annotations + +You can annotate individual keys using the `annotateKey` method. This is useful for adding a description or customizing the error message shown when the key is missing. + +**Example** (Annotating a required `username` field) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + username: Schema.String.annotateKey({ + description: "The username used to log in", + // Custom message shown if the key is missing + messageMissingKey: "Username is required" + }) +}) + +console.log(String(Schema.decodeUnknownExit(schema)({}))) +/* +Failure(Cause([Fail(SchemaError: Username is required + at ["username"] +)])) +*/ +``` + +### Unexpected Key Message + +You can annotate a struct with a custom message to use when a key is unexpected (when `onExcessProperty` is `error`). + +**Example** (Annotating a struct with a custom message) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String +}).annotate({ messageUnexpectedKey: "Custom message" }) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: "b" }, { onExcessProperty: "error" }))) +/* +Failure(Cause([Fail(SchemaError: Custom message + at ["b"] +)])) +*/ +``` + +### Preserve unexpected keys + +You can preserve unexpected keys by setting `onExcessProperty` to `preserve`. + +**Example** (Preserving unexpected keys) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String +}) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: "b" }, { onExcessProperty: "preserve" }))) +/* +Output: +Success({"b":"b","a":"a"}) +*/ +``` + +### Index Signatures + +An index signature lets a struct accept any string key in addition to its fixed keys. Use `Schema.StructWithRest` to combine a struct with one or more record schemas. + +Filters applied to either the struct or the record are preserved when combined. + +**Example** (Combining fixed properties with an index signature) + +```ts +import { Schema } from "effect" + +// Define a schema with one fixed key "a" and any number of string keys mapping to numbers +export const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [ + Schema.Record(Schema.String, Schema.Number) +]) + +/* +type Type = { + readonly [x: string]: number; + readonly a: number; +} +*/ +type Type = typeof schema.Type + +/* +type Encoded = { + readonly [x: string]: number; + readonly a: number; +} +*/ +type Encoded = typeof schema.Encoded +``` + +If you want the record part to be mutable, you can wrap it in `Schema.mutable`. + +**Example** (Allowing dynamic keys to be mutable) + +```ts +import { Schema } from "effect" + +// Define a schema with one fixed key "a" and any number of string keys mapping to numbers +export const schema = Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [ + Schema.Record(Schema.String, Schema.mutableKey(Schema.Number)) +]) + +/* +type Type = { + [x: string]: number; + readonly a: number; +} +*/ +type Type = typeof schema.Type + +/* +type Encoded = { + [x: string]: number; + readonly a: number; +} +*/ +type Encoded = typeof schema.Encoded +``` + +### Renaming Encoded Keys + +Use `Schema.encodeKeys` to rename one or more keys only in the encoded representation of a struct. + +Pass a mapping of `{ decodedKey: encodedKey }`. During decoding, the schema expects the mapped encoded keys. During encoding, it produces those keys. Keys not in the mapping are left unchanged. + +Unlike `Struct.renameKeys`, this does not rename the struct's own field names. It only remaps keys at the encoding / decoding boundary. + +**Example** (Using snake_case keys in the encoded form) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + userId: Schema.FiniteFromString, + accountName: Schema.String +}).pipe( + Schema.encodeKeys({ + userId: "user_id", + accountName: "account_name" + }) +) + +console.log(Schema.decodeUnknownSync(schema)({ user_id: "1", account_name: "alice" })) +// { userId: 1, accountName: "alice" } + +console.log(Schema.encodeUnknownSync(schema)({ userId: 1, accountName: "alice" })) +// { user_id: "1", account_name: "alice" } +``` + +If you are building a struct from reused fields or `Schema.fieldsAssign`, apply `Schema.encodeKeys` after defining the full struct. + +### Reusing Fields + +Every `Schema.Struct` exposes a `.fields` property containing its field definitions. You can spread these fields into a new struct to reuse them, similar to how TypeScript interfaces use `extends`. + +**Example** (Single inheritance) + +```ts +import { Schema } from "effect" + +const Timestamped = Schema.Struct({ + createdAt: Schema.Date, + updatedAt: Schema.Date +}) + +const User = Schema.Struct({ + ...Timestamped.fields, + name: Schema.String, + email: Schema.String +}) + +const Post = Schema.Struct({ + ...Timestamped.fields, + title: Schema.String, + body: Schema.String +}) +``` + +**Example** (Multiple inheritance) + +```ts +import { Schema } from "effect" + +const Timestamped = Schema.Struct({ + createdAt: Schema.Date, + updatedAt: Schema.Date +}) + +const SoftDeletable = Schema.Struct({ + deletedAt: Schema.optionalKey(Schema.Date) +}) + +const User = Schema.Struct({ + ...Timestamped.fields, + ...SoftDeletable.fields, + name: Schema.String, + email: Schema.String +}) +``` + +### Deriving Structs + +You can derive new struct schemas from existing ones — picking, omitting, renaming, or transforming individual fields — without rewriting the schema from scratch. The `mapFields` method on `Schema.Struct` accepts a function that transforms the struct's fields and returns a new `Schema.Struct` based on the result. + +#### Pick + +Use `Struct.pick` to keep only a selected set of fields. + +**Example** (Picking specific fields from a struct) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.String; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields(Struct.pick(["a"])) +``` + +#### Omit + +Use `Struct.omit` to remove specified fields from a struct. + +**Example** (Omitting fields from a struct) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.String; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields(Struct.omit(["b"])) +``` + +#### Merge + +Use `Struct.assign` to add new fields to an existing struct. + +**Example** (Adding fields to a struct) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.String; + readonly b: Schema.Number; + readonly c: Schema.Boolean; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields( + Struct.assign({ + c: Schema.Boolean + }) +) + +// or more succinctly +const schema2 = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.fieldsAssign({ c: Schema.Boolean })) +``` + +If you want to preserve the checks of the original struct, you can pass `{ unsafePreserveChecks: true }` to the `map` method. + +**Warning**: This is an unsafe operation. Since `mapFields` transformations change the schema type, the original refinement functions may no longer be valid or safe to apply to the transformed schema. Only use this option if you have verified that your refinements remain correct after the transformation. + +**Example** (Preserving checks when merging fields) + +```ts +import { Schema, Struct } from "effect" + +const original = Schema.Struct({ + a: Schema.String, + b: Schema.String +}).check(Schema.makeFilter(({ a, b }) => a === b, { title: "a === b" })) + +const schema = original.mapFields(Struct.assign({ c: Schema.String }), { + unsafePreserveChecks: true +}) + +console.log( + String( + Schema.decodeUnknownExit(schema)({ + a: "a", + b: "b", + c: "c" + }) + ) +) +// Failure(Cause([Fail(SchemaError: Expected a === b, got {"a":"a","b":"b","c":"c"})])) +``` + +#### Mapping individual fields + +Use `Struct.evolve` to transform the value schema of individual fields. + +**Example** (Modifying the type of a single field) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.optionalKey; + readonly b: Schema.Number; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields( + Struct.evolve({ + a: (field) => Schema.optionalKey(field) + }) +) +``` + +#### Mapping all fields at once + +If you want to transform the value schema of multiple fields at once, you can use `Struct.map`. + +**Example** (Making all fields optional) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.optionalKey; + readonly b: Schema.optionalKey; + readonly c: Schema.optionalKey; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean +}).mapFields(Struct.map(Schema.optionalKey)) +``` + +#### Mapping a subset of fields at once + +If you want to map a subset of elements, you can use `Struct.mapPick` or `Struct.mapOmit`. + +**Example** (Making a subset of fields optional) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.optionalKey; + readonly b: Schema.Number; + readonly c: Schema.optionalKey; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean +}).mapFields(Struct.mapPick(["a", "c"], Schema.optionalKey)) +``` + +Or if it's more convenient, you can use `Struct.mapOmit`. + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly a: Schema.optionalKey; + readonly b: Schema.Number; + readonly c: Schema.optionalKey; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean +}).mapFields(Struct.mapOmit(["b"], Schema.optionalKey)) +``` + +#### Mapping individual keys + +Use `Struct.evolveKeys` to rename field keys while keeping the corresponding value schemas. + +**Example** (Uppercasing keys in a struct) + +```ts +import { String } from "effect" +import { Schema } from "effect" +import { Struct } from "effect/data" + +/* +const schema: Schema.Struct<{ + readonly A: Schema.String; + readonly b: Schema.Number; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields( + Struct.evolveKeys({ + a: (key) => String.toUpperCase(key) + }) +) +``` + +If you simply want to rename keys with static keys, you can use `Struct.renameKeys`. + +**Example** (Renaming keys in a struct) + +```ts +import { Schema, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly A: Schema.String; + readonly b: Schema.Number; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields( + Struct.renameKeys({ + a: "A" + }) +) +``` + +#### Mapping individual entries + +Use `Struct.evolveEntries` when you want to transform both the key and the value of specific fields. + +**Example** (Transforming keys and value schemas) + +```ts +import { Schema, String, Struct } from "effect" + +/* +const schema: Schema.Struct<{ + readonly b: Schema.Number; + readonly A: Schema.optionalKey; +}> +*/ +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).mapFields( + Struct.evolveEntries({ + a: (key, value) => [String.toUpperCase(key), Schema.optionalKey(value)] + }) +) +``` + +#### Opaque Structs + +The previous examples can be applied to opaque structs as well. + +```ts +import { Schema, Struct } from "effect" + +class A extends Schema.Opaque()( + Schema.Struct({ + a: Schema.String, + b: Schema.Number + }) +) {} + +/* +const schema: Schema.Struct<{ + readonly a: Schema.optionalKey; + readonly b: Schema.Number; +}> +*/ +const schema = A.mapFields( + Struct.evolve({ + a: (field) => Schema.optionalKey(field) + }) +) +``` + +### Tagged Structs + +A tagged struct is a struct that includes a `_tag` field. This field is used to identify the specific variant of the object, which is especially useful when working with union types. + +When using the `make` method, the `_tag` field is optional and will be added automatically. However, when decoding or encoding, the `_tag` field must be present in the input. + +**Example** (Tagged struct as a shorthand for a struct with a `_tag` field) + +```ts +import { Schema } from "effect" + +// Defines a struct with a fixed `_tag` field +const tagged = Schema.TaggedStruct("A", { + a: Schema.String +}) + +// This is the same as writing: +const equivalent = Schema.Struct({ + _tag: Schema.tag("A"), + a: Schema.String +}) +``` + +**Example** (Accessing the literal value of the tag) + +```ts +// The `_tag` field is a schema with a known literal value +const literal = tagged.fields._tag.schema.literal +// literal: "A" +``` + +## Tuples + +A tuple schema describes a fixed-length array where each position has its own type. Use tuples when the order and count of elements matters — for example, a `[string, number]` pair. + +### Rest Elements + +You can add rest elements to a tuple using `Schema.TupleWithRest`. + +**Example** (Adding rest elements to a tuple) + +```ts +import { Schema } from "effect" + +export const schema = Schema.TupleWithRest(Schema.Tuple([Schema.FiniteFromString, Schema.String]), [ + Schema.Boolean, + Schema.String +]) + +/* +type Type = readonly [number, string, ...boolean[], string] +*/ +type Type = typeof schema.Type + +/* +type Encoded = readonly [string, string, ...boolean[], string] +*/ +type Encoded = typeof schema.Encoded +``` + +### Element Annotations + +You can annotate elements using the `annotateKey` method. + +**Example** (Annotating an element) + +```ts +import { Schema } from "effect" + +const schema = Schema.Tuple([ + Schema.String.annotateKey({ + description: "my element description", + // a message to display when the element is missing + messageMissingKey: "this element is required" + }) +]) + +console.log(String(Schema.decodeUnknownExit(schema)([]))) +/* +Failure(Cause([Fail(SchemaError: this element is required + at [0] +)])) +*/ +``` + +### Deriving Tuples + +You can map the elements of a tuple schema using the `mapElements` static method on `Schema.Tuple`. The `mapElements` static method accepts a function from `Tuple.elements` to new elements, and returns a new `Schema.Tuple` based on the result. + +#### Pick + +Use `Tuple.pick` to keep only a selected set of elements. + +**Example** (Picking specific elements from a tuple) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.pick([0, 2])) +``` + +#### Omit + +Use `Tuple.omit` to remove specified elements from a tuple. + +**Example** (Omitting elements from a tuple) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.omit([1])) +``` + +#### Adding Elements + +You can add elements to a tuple schema using the `appendElement` and `appendElements` APIs of the `Tuple` module. + +**Example** (Adding elements to a tuple) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number]) + .mapElements(Tuple.appendElement(Schema.Boolean)) // adds a single element + .mapElements(Tuple.appendElements([Schema.String, Schema.Number])) // adds multiple elements +``` + +#### Mapping individual elements + +You can evolve the elements of a tuple schema using the `evolve` API of the `Tuple` module + +**Example** + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple, + Schema.Number, + Schema.NullOr +]> +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( + Tuple.evolve([ + (v) => Schema.NullOr(v), + undefined, // no change + (v) => Schema.NullOr(v) + ]) +) +``` + +#### Mapping all elements at once + +You can map all elements of a tuple schema using the `map` API of the `Tuple` module. + +**Example** (Making all elements nullable) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple, + Schema.NullOr, + Schema.NullOr +]> +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.map(Schema.NullOr)) +``` + +#### Mapping a subset of elements at once + +If you want to map a subset of elements, you can use `Tuple.mapPick` or `Tuple.mapOmit`. + +**Example** (Making a subset of elements nullable) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple, + Schema.Number, + Schema.NullOr +]> +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( + Tuple.mapPick([0, 2], Schema.NullOr) +) +``` + +Or if it's more convenient, you can use `Tuple.mapOmit`. + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple, + Schema.Number, + Schema.NullOr +]> +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( + Tuple.mapOmit([1], Schema.NullOr) +) +``` + +#### Renaming Indices + +You can rename the indices of a tuple schema using the `renameIndices` API of the `Tuple` module. + +**Example** (Partial index mapping) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( + Tuple.renameIndices(["1", "0"]) // flip the first and second elements +) +``` + +**Example** (Full index mapping) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Tuple +*/ +const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( + Tuple.renameIndices([ + "2", // last element becomes first + "1", // second element keeps its index + "0" // first element becomes third + ]) +) +``` + +## Arrays + +An array schema describes a variable-length list where every element shares the same type. + +### Unique Arrays + +You can deduplicate arrays using `Schema.UniqueArray`. + +Internally, `Schema.UniqueArray` uses `Schema.Array` and adds a check based on `Schema.isUnique` using `ToEquivalence.make(item)` for the equivalence. + +```ts +import { Schema } from "effect" + +const schema = Schema.UniqueArray(Schema.String) + +console.log(String(Schema.decodeUnknownExit(schema)(["a", "b", "a"]))) +// Failure(Cause([Fail(SchemaError: Expected an array with unique items, got ["a","b","a"])])) +``` + +## Records + +A record schema describes an object whose keys are dynamic (not known ahead of time). The key schema selects which own properties belong to the record, and the value schema validates the selected property values. + +Properties that are not selected by the key schema are ignored by that record. For example, `Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.Number)` decodes only string keys that start with `"a"`. + +### Key Transformations + +`Schema.Record` supports transforming keys during decoding and encoding. This can be useful when working with different naming conventions. + +When a key schema has a transformation, dynamic property selection is based on the encoded property names. The selected keys are then decoded using the key schema. + +**Example** (Transforming snake_case keys to camelCase) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())) + +const schema = Schema.Record(SnakeToCamel, Schema.Number) + +console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, c_d: 2 })) +// { aB: 1, cD: 2 } +``` + +By default, if a transformation results in duplicate keys, the last value wins. + +**Example** (Merging transformed keys by keeping the last one) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())) + +const schema = Schema.Record(SnakeToCamel, Schema.Number) + +console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, aB: 2 })) +// { aB: 2 } +``` + +You can customize how key conflicts are resolved by providing a `combine` function. + +**Example** (Combining values for conflicting keys) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())) + +const schema = Schema.Record(SnakeToCamel, Schema.Number, { + keyValueCombiner: { + decode: { + // When decoding, combine values of conflicting keys by summing them + combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] // you can pass a Semigroup to combine keys + }, + encode: { + // Same logic applied when encoding + combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] + } + } +}) + +console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, aB: 2 })) +// { aB: 3 } + +console.log(Schema.encodeUnknownSync(schema)({ a_b: 1, aB: 2 })) +// { a_b: 3 } +``` + +### Number Keys + +Records with number keys are supported. + +**Example** (Record with number keys) + +```ts +import { Schema } from "effect" + +const schema = Schema.Record(Schema.Int, Schema.String) + +console.log(String(Schema.decodeUnknownExit(schema)({ 1: "a", 2: "b" }))) +// Success({"1":"a","2":"b"}) + +console.log(String(Schema.decodeUnknownExit(schema)({ 1.1: "ignored" }))) +// Success({}) + +console.log(String(Schema.decodeUnknownExit(schema)({ 1: null }))) +// Failure(Cause([Fail(SchemaError(Expected string, got null +// at ["1"]))])) +``` + +### Mutability + +By default, records are tagged as `readonly`. You can mark a record as mutable using `Schema.mutableKey` as you do with structs. + +**Example** (Defining a mutable record) + +```ts +import { Schema } from "effect" + +export const schema = Schema.Record(Schema.String, Schema.mutableKey(Schema.Number)) + +/* +type Type = { + [x: string]: number; +} +*/ +type Type = typeof schema.Type + +/* +type Encoded = { + [x: string]: number; +} +*/ +type Encoded = typeof schema.Encoded +``` + +### Literal Structs + +When you pass a union of string literals as the key schema to `Schema.Record`, you get a struct-like schema where each literal becomes a required key. This mirrors how TypeScript's built-in `Record` type behaves. + +**Example** (Creating a literal struct with fixed string keys) + +```ts +import { Schema } from "effect" + +const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.Number) + +/* +type Type = { + readonly a: number; + readonly b: number; +} +*/ +type Type = typeof schema.Type +``` + +#### Mutable Keys + +By default, keys are readonly. To make them mutable, use `Schema.mutableKey` just as you would with a standard struct. + +**Example** (Literal struct with mutable keys) + +```ts +import { Schema } from "effect" + +const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.mutableKey(Schema.Number)) + +/* +type Type = { + a: number; + b: number; +} +*/ +type Type = typeof schema.Type +``` + +#### Optional Keys + +You can make the keys optional by wrapping the value schema with `Schema.optional`. + +**Example** (Literal struct with optional keys) + +```ts +import { Schema } from "effect" + +const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.optional(Schema.Number)) + +/* +type Type = { + readonly a?: number; + readonly b?: number; +} +*/ +type Type = typeof schema.Type +``` + +## Unions + +A union schema accepts a value if it matches any one of its members. Unions are useful when a field can hold more than one type — for example, a value that is either a string or a number. + +By default, unions are _inclusive_: a value is accepted if it matches **any** of the union's members. + +The members are checked in order, and the first one that matches is used. + +### Excluding Incompatible Members + +If a union member is not compatible with the input, it is automatically excluded during validation. + +**Example** (Excluding incompatible members from the union) + +```ts +import { Schema } from "effect" + +const schema = Schema.Union([Schema.NonEmptyString, Schema.Number]) + +console.log(String(Schema.decodeUnknownExit(schema)(""))) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "")])) +``` + +If none of the union members match the input, the union fails with a message at the top level. + +**Example** (All members excluded) + +```ts +import { Schema } from "effect" + +const schema = Schema.Union([Schema.NonEmptyString, Schema.Number]) + +console.log(String(Schema.decodeUnknownExit(schema)(null))) +// Failure(Cause([Fail(SchemaError: Expected string | number, got null)])) +``` + +This behavior is especially helpful when working with literal values. Instead of producing a separate error for each literal (as in version 3), the schema reports a single, clear message. + +**Example** (Validating against a set of literals) + +```ts +import { Schema } from "effect" + +const schema = Schema.Literals(["a", "b"]) + +console.log(String(Schema.decodeUnknownExit(schema)(null))) +// Failure(Cause([Fail(SchemaError: Expected "a" | "b", got null)])) +``` + +### Exclusive Unions + +You can create an exclusive union, where the union matches if exactly one member matches, by passing the `{ mode: "oneOf" }` option. + +**Example** (Exclusive Union) + +```ts +import { Schema } from "effect" + +const schema = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })], { + mode: "oneOf" +}) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: 1 }))) +// Failure(Cause([Fail(SchemaError: Expected exactly one member to match the input {"a":"a","b":1})])) +``` + +### Deriving Unions + +You can map the members of a union schema using the `mapMembers` static method on `Schema.Union`. The `mapMembers` static method accepts a function from `Union.members` to new members, and returns a new `Schema.Union` based on the result. + +#### Adding Members + +You can add members to a union schema using the `appendElement` and `appendElements` APIs of the `Tuple` module. + +**Example** (Adding members to a union) + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Union +*/ +const schema = Schema.Union([Schema.String, Schema.Number]) + .mapMembers(Tuple.appendElement(Schema.Boolean)) // adds a single member + .mapMembers(Tuple.appendElements([Schema.String, Schema.Number])) // adds multiple members +``` + +#### Mapping individual members + +You can evolve the members of a union schema using the `evolve` API of the `Tuple` module + +**Example** + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Union, + Schema.Number, + Schema.Array$ +]> +*/ +const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers( + Tuple.evolve([ + (v) => Schema.Array(v), + undefined, // no change + (v) => Schema.Array(v) + ]) +) +``` + +#### Mapping all members at once + +You can map all members of a union schema using the `map` API of the `Tuple` module. + +**Example** + +```ts +import { Schema, Tuple } from "effect" + +/* +const schema: Schema.Union, + Schema.Array$, + Schema.Array$ +]> +*/ +const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers(Tuple.map(Schema.Array)) +``` + +### Union of Literals + +You can create a union of literals using `Schema.Literals`. + +```ts +import { Schema } from "effect" + +const schema = Schema.Literals(["red", "green", "blue"]) +``` + +#### Deriving new literals + +You can map the members of a `Schema.Literals` schema using the `mapMembers` method. The `mapMembers` method accepts a function from `Literals.members` to new members, and returns a new `Schema.Union` based on the result. + +```ts +import { Schema, Tuple } from "effect" + +const schema = Schema.Literals(["red", "green", "blue"]).mapMembers( + Tuple.evolve([ + (a) => Schema.Struct({ _tag: a, a: Schema.String }), + (b) => Schema.Struct({ _tag: b, b: Schema.Number }), + (c) => Schema.Struct({ _tag: c, c: Schema.Boolean }) + ]) +) + +/* +type Type = { + readonly _tag: "red"; + readonly a: string; +} | { + readonly _tag: "green"; + readonly b: number; +} | { + readonly _tag: "blue"; + readonly c: boolean; +} +*/ +type Type = (typeof schema)["Type"] +``` + +### Tagged Unions + +You can define a tagged union using the `Schema.TaggedUnion` helper. This is useful when combining multiple tagged structs into a union. + +**Example** (Defining a tagged union with `Schema.TaggedUnion`) + +```ts +import { Schema } from "effect" + +// Create a union of two tagged structs +const schema = Schema.TaggedUnion({ + A: { a: Schema.String }, + B: { b: Schema.Finite } +}) +``` + +This is equivalent to writing: + +```ts +const schema = Schema.Union([ + Schema.TaggedStruct("A", { a: Schema.String }), + Schema.TaggedStruct("B", { b: Schema.Finite }) +]) +``` + +The result is a tagged union schema with built-in helpers based on the tag values. See the next section for more details. + +### Augmenting Tagged Unions + +The `asTaggedUnion` function enhances a tagged union schema by adding helper methods for working with its members. + +You need to specify the name of the tag field used to differentiate between variants. + +**Example** (Adding tag-based helpers to a union) + +```ts +import { Schema } from "effect" + +const original = Schema.Union([ + Schema.Struct({ type: Schema.tag("A"), a: Schema.String }), + Schema.Struct({ type: Schema.tag("B"), b: Schema.Finite }), + Schema.Struct({ type: Schema.tag("C"), c: Schema.Boolean }) +]) + +// Enrich the union with tag-based utilities +const tagged = original.pipe(Schema.toTaggedUnion("type")) +``` + +This helper has some advantages over a dedicated constructor: + +- It does not require changes to the original schema, just call a helper. +- You can apply it to schemas from external sources. +- You can choose among multiple possible tag fields if present. +- It supports unions that include nested unions. + +**Note**. If the tag is the standard `_tag` field, you can use `Schema.TaggedUnion` instead. + +#### Accessing Members by Tag + +The `cases` property gives direct access to each member schema of the union. + +**Example** (Getting a member schema from a tagged union) + +```ts +const A = tagged.cases.A +const B = tagged.cases.B +const C = tagged.cases.C +``` + +#### Checking Membership in a Subset of Tags + +The `isAnyOf` method lets you check if a value belongs to a selected subset of tags. + +**Example** (Checking membership in a subset of union tags) + +```ts +console.log(tagged.isAnyOf(["A", "B"])({ type: "A", a: "a" })) // true +console.log(tagged.isAnyOf(["A", "B"])({ type: "B", b: 1 })) // true + +console.log(tagged.isAnyOf(["A", "B"])({ type: "C", c: true })) // false +``` + +#### Type Guards + +The `guards` property provides a type guard for each tag. + +**Example** (Using type guards for tagged members) + +```ts +console.log(tagged.guards.A({ type: "A", a: "a" })) // true +console.log(tagged.guards.B({ type: "B", b: 1 })) // true + +console.log(tagged.guards.A({ type: "B", b: 1 })) // false +``` + +#### Matching on a Tag + +You can define a matcher function using the `match` method. This is a concise way to handle each variant of the union. + +**Example** (Handling union members with `match`) + +```ts +const matcher = tagged.match({ + A: (a) => `This is an A: ${a.a}`, + B: (b) => `This is a B: ${b.b}`, + C: (c) => `This is a C: ${c.c}` +}) + +console.log(matcher({ type: "A", a: "a" })) // This is an A: a +console.log(matcher({ type: "B", b: 1 })) // This is a B: 1 +console.log(matcher({ type: "C", c: true })) // This is a C: true +``` + +## Recursive Schemas + +Use `Schema.suspend` when a schema needs to refer to itself (or to another schema that eventually refers back). `suspend` wraps a thunk, so the recursive reference is resolved lazily during decode / encode instead of eagerly during declaration. + +**Example** (Recursive Struct with Same Encoded and Type) + +```ts +import { Schema } from "effect" + +interface Category { + readonly name: string + readonly children: ReadonlyArray +} + +const Category: Schema.Codec = Schema.Struct({ + name: Schema.String, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) +}) +``` + +The explicit `Schema.Codec` annotation is important in recursive declarations because `Category` is referenced inside its own initializer. Without the annotation, TypeScript often cannot stabilize the self-referential type and falls back to an implicit `any` style error. + +**Example** (Recursive Struct with Different Encoded and Type) + +```ts +import { Schema } from "effect" + +interface Category { + readonly name: number + readonly children: ReadonlyArray +} + +interface CategoryEncoded { + readonly name: string + readonly children: ReadonlyArray +} + +const Category: Schema.Codec = Schema.Struct({ + name: Schema.FiniteFromString, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) +}) +``` + +Here the encoded shape differs from the runtime shape (`name` is `string` when encoded, `number` after decoding), so both type parameters must be explicit: `Schema.Codec`. + +Using only `Schema.Codec` would force encoded and decoded types to be the same, which does not describe this schema. + +**Example** (Recursive Union) + +```ts +import { Schema } from "effect" + +type U = A | B + +interface A { + readonly a: string + readonly next: U +} +interface B { + readonly b: number + readonly next: U +} + +const URef = Schema.suspend((): Schema.Codec => U) + +const A: Schema.Codec = Schema.Struct({ + a: Schema.String, + next: URef +}) + +const B: Schema.Codec = Schema.Struct({ + b: Schema.Number, + next: URef +}) + +const U: Schema.Codec = Schema.Union([A, B]) +``` + +`URef` factors the recursive edge (`U -> U`) into one shared `Schema.suspend` value. Reusing it across members avoids duplicating the lazy reference and makes the intent clear: every variant points back to the same union schema. + +# Declaring Custom Types + +When none of the built-in schema combinators fit your data type, use `Schema.declare` or `Schema.declareConstructor`. + +## `Schema.declare` (non-parametric types) + +`Schema.declare` creates a schema from a **type guard** — a function that checks whether an unknown value is of a given type. This is useful when you have a type that doesn't fit the built-in combinators (like `Struct`, `Array`, etc.) and you need to teach Schema how to recognize it. + +```ts +Schema.declare( + is: (u: unknown) => u is T, + annotations?: { expected?: string; toCodecJson?: ...; ... } +) +``` + +The first argument is your type guard. Schema will call it on any input value: if it returns `true`, decoding succeeds; if `false`, decoding fails. + +**Example** (Creating a schema for `URL`) + +```ts +import { Schema } from "effect" + +// The type guard tells Schema how to recognize a URL instance +const URLSchema = Schema.declare( + (u): u is URL => u instanceof URL +) + +console.log(String(Schema.decodeUnknownExit(URLSchema)(new URL("https://example.com")))) +// Success(https://example.com/) + +console.log(String(Schema.decodeUnknownExit(URLSchema)(null))) +// Failure(Cause([Fail(SchemaError(Expected , got null))])) +``` + +> **Tip**: For simple `instanceof` checks, prefer `Schema.instanceOf(URL)`, it wraps `Schema.declare` with an `instanceof` guard automatically. + +### Customizing the error message with `expected` + +The default error message `Expected ` is not very descriptive. Use the `expected` annotation (second argument) to provide a human-readable name for your type. + +**Example** (Adding an `expected` annotation) + +```ts +import { Schema } from "effect" + +const URLSchema = Schema.declare( + (u): u is URL => u instanceof URL, + { expected: "URL" } +) + +console.log(String(Schema.decodeUnknownExit(URLSchema)(null))) +// Failure(Cause([Fail(SchemaError(Expected URL, got null))])) +// ^^^ +// Now the error message shows "URL" instead of "" +``` + +### Adding JSON support with `toCodecJson` + +`Schema.toCodecJson` derives a codec that can convert your type **to and from JSON**. By default, declared schemas have no JSON representation — encoding produces `null`: + +```ts +import { Schema } from "effect" + +const URLSchema = Schema.declare( + (u): u is URL => u instanceof URL, + { expected: "URL" } +) + +// Derive a JSON codec from the schema +const codec = Schema.toCodecJson(URLSchema) + +// Encoding a URL produces null because Schema doesn't know +// how to serialize a URL to JSON yet +console.log(String(Schema.encodeUnknownExit(codec)(new URL("https://example.com")))) +// Success(null) +``` + +To fix this, provide a `toCodecJson` annotation. This annotation is a function that returns an `AST.Link`, a bridge that describes how to convert between your custom type and a JSON-friendly representation. + +You build a `Link` using `Schema.link()`, which takes two arguments: + +1. **A JSON-side schema** — the shape of the JSON value (e.g. `Schema.String` for a URL string) +2. **A transformation** — how to convert back and forth between your type and the JSON value + +**Example** (Making `URL` JSON-serializable) + +```ts +import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" + +const URLSchema = Schema.declare( + (u): u is URL => u instanceof URL, + { + expected: "URL", + // Teach Schema how to convert URL <-> JSON + toCodecJson: () => + Schema.link()( + // The JSON representation is a plain string + Schema.String, + // How to convert between URL and string + SchemaTransformation.transformOrFail({ + // JSON string -> URL (may fail if the string is not a valid URL) + decode: (s) => + Effect.try({ + try: () => new URL(s), + catch: (e) => new SchemaIssue.InvalidValue(Option.some(s), { message: globalThis.String(e) }) + }), + // URL -> JSON string (always succeeds) + encode: (url) => Effect.succeed(url.href) + }) + ) + } +) + +const codec = Schema.toCodecJson(URLSchema) + +// Now encoding produces the URL's href string +console.log(String(Schema.encodeUnknownExit(codec)(new URL("https://example.com")))) +// Success("https://example.com/") + +// And decoding parses a string back into a URL +console.log(String(Schema.decodeUnknownExit(codec)("https://example.com"))) +// Success(https://example.com/) +``` + +## `Schema.declareConstructor` (parametric types) + +While `Schema.declare` works for fixed types like `URL` or `File`, some types are **generic** — they contain other types as parameters. Think of `Array`, `Option`, or a custom `Box`. The schema for `Box` is different from `Box` because the inner value has a different type. + +`Schema.declareConstructor` handles this by letting you define a **schema factory**: a function that takes schemas for the type parameters and returns a schema for the full type. + +> **Important:** `declareConstructor` is for types where the **container shape is the same** on both sides: only the inner type parameter changes (e.g. `Box` to `Box`). If you need to convert a structurally different type into your declared type (e.g. `T` to `Box`), first declare `Box` with `declareConstructor`, then define a separate transformation schema to express the conversion. + +### How the two-step call works + +`declareConstructor` uses a curried (two-step) call pattern: + +```ts +Schema.declareConstructor()( + typeParameters, // array of schemas, one per type parameter + run, // factory that produces the parsing function + annotations // optional metadata (same as Schema.declare) +) +``` + +1. **Outer call** `declareConstructor()` — fixes the TypeScript types. `Type` is the decoded type, `Encoded` is the encoded type. +2. **Inner call** `(typeParameters, run, annotations)` — provides the runtime behavior: + - `typeParameters` — an array of schemas, one for each type variable (e.g. `[itemSchema]` for `Box`) + - `run` — a function that receives **resolved codecs** for those type parameters and returns a **parsing function** `(input, ast, options) => Effect` + - `annotations` — optional metadata like `expected`, `toCodecJson`, etc. + +The parsing function you return from `run` is responsible for: + +1. Checking that the input has the right shape (e.g. is an object with a `value` property) +2. Recursively decoding inner values using the provided codecs +3. Returning an `Effect` that succeeds with the decoded value or fails with an issue + +**Example** (A generic `Box` container) + +```ts +import { Effect, Option, Schema, SchemaIssue, SchemaParser } from "effect" + +// 1. Define the type +interface Box { + readonly value: A +} + +// 2. A type guard that checks the shape (ignoring the inner type) +const isBox = (u: unknown): u is Box => typeof u === "object" && u !== null && "value" in u + +// 3. Create a schema factory: given a schema for A, return a schema for Box +const Box = (item: A) => + Schema.declareConstructor, Box>()( + // Pass the inner schema as a type parameter + [item], + // `run` receives the resolved codec for `item` + ([itemCodec]) => + // Return the parsing function + (u, ast, options) => { + // First, check the outer shape + if (!isBox(u)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(u))) + } + // Then, decode the inner value using the item codec + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(itemCodec)(u.value, options), + { + onSuccess: (value) => ({ value }), + // Wrap inner errors with a Pointer so the error path shows ["value"] + onFailure: (issue) => new SchemaIssue.Pointer(["value"], issue) + } + ) + } + ) + +// Use it: Box that decodes strings to finite numbers +const schema = Box(Schema.FiniteFromString) + +console.log(String(Schema.decodeUnknownExit(schema)({ value: "1" }))) +// Success({ value: 1 }) + +console.log(String(Schema.decodeUnknownExit(schema)({ value: "a" }))) +// Failure(Cause([Fail(SchemaError(Expected a finite number, got NaN +// at ["value"]))])) +``` + +> `declareConstructor` accepts the same `annotations` as `declare` — including `expected` (for custom error messages) and `toCodecJson` (for JSON serialization). See the [`Schema.declare` section above](#schemadeclare-non-parametric-types) for details on how to use them. + +# Validation + +After defining a schema's shape, you can add validation rules called _filters_. Filters check runtime values against constraints like minimum length, numeric range, or custom predicates. Validation happens at runtime — Schema checks the actual value against the rules you define and reports any violations. + +You can apply filters with the `.check` method or the `Schema.check` function. + +Define custom filters with `Schema.makeFilter`. + +**Example** (Custom filter that checks minimum length) + +```ts +import { Schema } from "effect" + +// Filter: the string must have at least 3 characters +const schema = Schema.String.check(Schema.makeFilter((s) => s.length >= 3)) + +console.log(String(Schema.decodeUnknownExit(schema)(""))) +// Failure(Cause([Fail(SchemaError: Expected , got "")])) +``` + +You can attach annotations and provide a custom error message when defining a filter. + +**Example** (Filter with annotations and a custom message) + +```ts +import { Schema } from "effect" + +// Filter with a title, description, and custom error message +const schema = Schema.String.check( + Schema.makeFilter((s) => s.length >= 3 || `length must be >= 3, got ${s.length}`, { + title: "length >= 3", + description: "a string with at least 3 characters" + }) +) + +console.log(String(Schema.decodeUnknownExit(schema)(""))) +// Failure(Cause([Fail(SchemaError: length must be >= 3, got 0)])) +``` + +### Filter error messages and schema identifiers + +The default formatter chooses the error label from the level that failed: + +- If the input does not match the base schema type, the formatter reports a + type-level failure. In that case, a schema `identifier` is used as the + expected label. +- If the base type matches but a filter fails, the formatter reports a filter + failure. In that case, the filter's `message` annotation is used first, then + its `expected` annotation, and finally `` if neither is provided. + +An `identifier` does not name a failed filter. Use `expected` to name the +filter in the default formatter, or `message` to replace the filter failure +message completely. + +**Example** (Schema identifier versus filter expected message) + +```ts +import { Schema } from "effect" + +const Username = Schema.NonEmptyString.annotate({ identifier: "Username" }) + +console.log(String(Schema.decodeUnknownExit(Username)(null))) +// Failure(Cause([Fail(SchemaError: Expected Username, got null)])) + +console.log(String(Schema.decodeUnknownExit(Username)(""))) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "")])) +``` + +### Filter return shapes + +A filter predicate can return any of the shapes described by `Schema.FilterOutput`: + +- `undefined` or `true` — success. +- `false` — generic failure (no custom message). +- `string` — failure with the string used as the error message. +- `SchemaIssue.Issue` — a fully-formed issue, returned as-is (escape hatch for `Composite`, `AnyOf`, etc.). +- `{ path, issue }` — failure attached to a nested path. `issue` can be a `string` (wrapped in an `InvalidValue`) or a full `SchemaIssue.Issue`. +- `ReadonlyArray` — several failures reported together. Empty arrays are success; a single element is unwrapped; multiple entries are grouped into an `Issue.Composite`. + +**Example** (Failure at a nested path) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check( + Schema.makeFilter((o) => + o.password === o.confirmPassword + ? undefined + : { path: ["password"], issue: "password and confirmPassword must match" } + ) +) + +console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" }))) +// Failure(Cause([Fail(SchemaError: password and confirmPassword must match +// at ["password"])])) +``` + +**Example** (Reporting multiple failures at once) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check( + Schema.makeFilter((o) => { + const issues: Array = [] + if (o.a > 0) { + if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" }) + if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" }) + } + return issues + }) +) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 }))) +// Failure(Cause([Fail(SchemaError: b must be greater than 0 +// at ["b"] +// c must be greater than 0 +// at ["c"])])) +``` + +## Preserving Schema Type After Filtering + +Adding a filter does not change the schema's type. You can still use all schema-specific methods (like `.fields` on a struct or `.make`) after calling `.check(...)`. + +**Example** (Chaining filters and annotations without losing type information) + +```ts +import { Schema } from "effect" + +// ┌─── Schema.String +// ▼ +Schema.String + +// ┌─── Schema.String +// ▼ +const NonEmptyString = Schema.String.check(Schema.isNonEmpty()) + +// ┌─── Schema.String +// ▼ +const schema = NonEmptyString.annotate({}) +``` + +Even after adding a filter and an annotation, the schema is still a `Schema.String`. + +**Example** (Accessing struct fields after filtering) + +```ts +import { Schema } from "effect" + +// Define a struct and apply a (dummy) filter +const schema = Schema.Struct({ + name: Schema.String, + age: Schema.Number +}).check(Schema.makeFilter(() => true)) + +// The `.fields` property is still available +const fields = schema.fields +``` + +## Filters as First-Class + +Filters are standalone values that you can define once and reuse across different schemas. The same filter (for example, `Schema.isMinLength`) works on strings, arrays, or any type with a compatible shape. + +You can pass multiple filters to a single `.check(...)` call. + +**Example** (Combining filters on a string) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.check( + Schema.isMinLength(3), // value must be at least 3 chars long + Schema.isTrimmed() // no leading/trailing whitespace +) + +console.log(String(Schema.decodeUnknownExit(schema)(" a"))) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a")])) +``` + +**Example** (Using `isMinLength` with an object that has `length`) + +```ts +import { Schema } from "effect" + +// Object must have a numeric `length` field that is >= 3 +const schema = Schema.Struct({ length: Schema.Number }).check(Schema.isMinLength(3)) + +console.log(String(Schema.decodeUnknownExit(schema)({ length: 2 }))) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got {"length":2}])) +``` + +**Example** (Validating array length) + +```ts +import { Schema } from "effect" + +// Array must contain at least 3 strings +const schema = Schema.Array(Schema.String).check(Schema.isMinLength(3)) + +console.log(String(Schema.decodeUnknownExit(schema)(["a", "b"]))) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got ["a","b"]])) +``` + +## Multiple Issues Reporting + +By default, when `{ errors: "all" }` is passed, all filters are evaluated, even if one fails. This allows multiple issues to be reported at once. + +**Example** (Collecting multiple validation issues) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.check(Schema.isMinLength(3), Schema.isTrimmed()) + +console.log( + String( + Schema.decodeUnknownExit(schema)(" a", { + errors: "all" + }) + ) +) +/* +Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a" +Expected a string with no leading or trailing whitespace, got " a")])) +*/ +``` + +## Aborting Validation + +If you want to stop validation as soon as a filter fails, you can call the `abort` method on the filter. + +**Example** (Short-circuit on first failure) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.check( + Schema.isMinLength(3).abort(), // Stop on failure here + Schema.isTrimmed() // This will not run if minLength fails +) + +console.log( + String( + Schema.decodeUnknownExit(schema)(" a", { + errors: "all" + }) + ) +) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a")])) +``` + +## Filter Groups + +Group filters into a reusable unit with `Schema.makeFilterGroup`. This helps when the same set of checks appears in multiple places. + +**Example** (Reusable group for 32-bit integers) + +```ts +import { Schema } from "effect" + +// ┌─── FilterGroup +// ▼ +const isInt32 = Schema.makeFilterGroup( + [Schema.isInt(), Schema.isBetween({ minimum: -2147483648, maximum: 2147483647 })], + { + title: "isInt32", + description: "a 32-bit integer" + } +) +``` + +## Refinements + +Use `Schema.refine` to refine a schema to a more specific type. + +**Example** (Require at least two items in a string array) + +```ts +import { Schema } from "effect" + +// ┌─── refine> +// ▼ +const refined = Schema.Array(Schema.String).pipe( + Schema.refine((arr): arr is readonly [string, string, ...Array] => arr.length >= 2) +) +``` + +## Branding + +Use `Schema.brand` to add a brand to a schema. + +**Example** (Brand a string as a UserId) + +```ts +import { Schema } from "effect" + +// ┌─── Schema.brand +// ▼ +const branded = Schema.String.pipe(Schema.brand("UserId")) +``` + +## Structural Filters + +Some filters check the structure of a value rather than its contents — for example, the number of items in an array or the number of keys in an object. These are called **structural filters**. + +Structural filters are evaluated separately from item-level filters, which allows multiple issues to be reported when `{ errors: "all" }` is used. Examples include: + +- `isMinLength` or `isMaxLength` on arrays +- `isMinSize` or `isMaxSize` on objects with a `size` property +- `isMinProperties` or `isMaxProperties` on objects +- any constraint that applies to the "shape" of a value rather than to its nested values + +These filters are evaluated separately from item-level filters and allow multiple issues to be reported when `{ errors: "all" }` is used. + +**Example** (Validating an array with item and structural constraints) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + tags: Schema.Array(Schema.String.check(Schema.isNonEmpty())).check( + Schema.isMinLength(3) // structural filter + ) +}) + +console.log(String(Schema.decodeUnknownExit(schema)({ tags: ["a", ""] }, { errors: "all" }))) +/* +Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "" + at ["tags"][1] +Expected a value with a length of at least 3, got ["a",""] + at ["tags"])])) +*/ +``` + +## Effectful Filters + +Filters passed to `.check(...)` must be synchronous. When you need to call an API or use a service during validation, use an effectful filter instead. Effectful filters run inside an `Effect`, which means they can be asynchronous and access services. + +Define an effectful filter with `Getter.checkEffect` as part of a transformation. + +**Example** (Asynchronous validation of a numeric value) + +```ts +import { Effect, Option, Result, Schema, SchemaGetter, SchemaIssue } from "effect" + +// Simulated API call that fails when userId is 0 +const myapi = (userId: number) => + Effect.gen(function*() { + if (userId === 0) { + return new Error("not found") + } + return { userId } + }).pipe(Effect.delay(100)) + +const schema = Schema.Finite.pipe( + Schema.decode({ + decode: SchemaGetter.checkEffect((n) => + Effect.gen(function*() { + // Call the async API and wrap the result in a Result + const user = yield* Effect.result(myapi(n)) + + // If the result is an error, return a SchemaIssue + return Result.isFailure(user) ? new SchemaIssue.InvalidValue(Option.some(n), { title: "not found" }) : undefined // No issue, value is valid + }) + ), + encode: SchemaGetter.passthrough() + }) +) +``` + +## Filter Factories + +A filter factory is a function that returns a new filter each time you call it, letting you parameterize the constraint (for example, "greater than X" for any value of X). + +**Example** (Factory for a `isGreaterThan` filter on ordered values) + +```ts +import { Order, Schema } from "effect" + +// Create a filter factory for values greater than a given value +export const makeGreaterThan = (options: { + readonly order: Order.Order + readonly annotate?: ((exclusiveMinimum: T) => Schema.Annotations.Filter) | undefined + readonly format?: (value: T) => string | undefined +}) => { + const greaterThan = Order.isGreaterThan(options.order) + const format = options.format ?? globalThis.String + return (exclusiveMinimum: T, annotations?: Schema.Annotations.Filter) => { + return Schema.makeFilter((input) => greaterThan(input, exclusiveMinimum), { + title: `greaterThan(${format(exclusiveMinimum)})`, + description: `a value greater than ${format(exclusiveMinimum)}`, + ...options.annotate?.(exclusiveMinimum), + ...annotations + }) + } +} +``` + +# Constructors + +A constructor creates a value of the schema's type, running all validations at the time of creation. If the value does not satisfy the schema, the constructor throws an error. Every schema exposes a `make` method for this purpose. + +For an alternative that does not throw on schema validation failures, use `Schema.makeOption` (or `SchemaParser.makeOption`), which returns `Option.Some` on success and `Option.None` for schema issues. Non-schema failures, such as defects, still throw. + +```ts +import { Schema, SchemaParser } from "effect" + +const schema = Schema.Struct({ + a: Schema.Number.check(Schema.isGreaterThan(0)) +}) + +console.log(schema.makeOption({ a: 1 })) +// { _id: 'Option', _tag: 'Some', value: { a: 1 } } + +console.log(schema.makeOption({ a: -1 })) +// { _id: 'Option', _tag: 'None' } + +// Equivalent standalone usage: +const parse = SchemaParser.makeOption(schema) + +console.log(parse({ a: 1 })) +// { _id: 'Option', _tag: 'Some', value: { a: 1 } } +``` + +## Constructors in Composed Schemas + +To support constructing values from composed schemas, `make` is now available on all schemas, including unions. + +```ts +import { Schema } from "effect" + +const schema = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number })]) + +schema.make({ a: "hello" }) +schema.make({ b: 1 }) +``` + +## Branded Constructors + +Branding adds an invisible marker to a type so that values from different domains cannot be accidentally mixed — even when they have the same underlying shape (for example, both are `string`). For branded schemas, the default constructor accepts an unbranded input and returns a branded output. + +```ts +import { Schema } from "effect" + +const schema = Schema.String.pipe(Schema.brand<"a">()) + +// make(input: string, options?: Schema.MakeOptions): string & Brand<"a"> +schema.make +``` + +However, when a branded schema is part of a composite (such as a struct), you must pass a branded value. + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String.pipe(Schema.brand<"a">()), + b: Schema.Number +}) + +/* +make(input: { + readonly a: string & Brand<"a">; + readonly b: number; +}, options?: Schema.MakeOptions): { + readonly a: string & Brand<"a">; + readonly b: number; +} +*/ +schema.make +``` + +## Refined Constructors + +For refined schemas, the constructor accepts the unrefined type and returns the refined one. + +```ts +import { Option, Schema } from "effect" + +const schema = Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome)) + +// make(input: Option.Option, options?: Schema.MakeOptions): Option.Some +schema.make +``` + +As with branding, when used in a composite schema, the refined value must be provided. + +```ts +import { Option, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.Option(Schema.String).pipe(Schema.refine(Option.isSome)), + b: Schema.Number +}) + +/* +make(input: { + readonly a: Option.Some; + readonly b: number; +}, options?: Schema.MakeOptions): { + readonly a: Option.Some; + readonly b: number; +} +*/ +schema.make +``` + +## Default Values in Constructors + +You can define a default value for a field using `Schema.withConstructorDefault`. If no value is provided at runtime (either the key is missing or the value is `undefined`), the constructor uses this default. + +**Example** (Providing a default number) + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) +}) + +console.log(schema.make({ a: 5 })) +// { a: 5 } + +console.log(schema.make({})) +// { a: -1 } +``` + +The Effect passed to `withConstructorDefault` will be executed each time a default value is needed. + +**Example** (Re-executing the default function) + +```ts +import { Effect, Schema } from "effect" + +let counter = 0 + +const schema = Schema.Struct({ + a: Schema.Date.pipe(Schema.withConstructorDefault(Effect.sync(() => new Date(counter++)))) +}) + +console.log(schema.make({})) +// { a: 1970-01-01T00:00:00.000Z } + +console.log(schema.make({})) +// { a: 1970-01-01T00:00:00.001Z } +``` + +### Nested Constructor Default Values + +Default values can be nested inside composed schemas. In this case, inner defaults are resolved first. + +**Example** (Nested default values) + +```ts +import { Effect, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.Struct({ + b: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) + }).pipe(Schema.withConstructorDefault(Effect.succeed({}))) +}) + +console.log(schema.make({})) +// { a: { b: -1 } } +console.log(schema.make({ a: {} })) +// { a: { b: -1 } } +``` + +## Effectful Defaults + +Default values can also come from an `Effect`, for example, reading from a configuration service or performing an asynchronous operation. The environment must be `never` (no required services). + +**Example** (Using an effect to provide a default) + +```ts +import { Effect, Schema, SchemaParser } from "effect" + +const schema = Schema.Struct({ + a: Schema.Number.pipe( + Schema.withConstructorDefault( + Effect.gen(function*() { + yield* Effect.sleep(100) + return -1 + }) + ) + ) +}) + +SchemaParser.makeEffect(schema)({}).pipe(Effect.runPromise).then(console.log) +// { a: -1 } +``` + +**Example** (Providing a default from an optional service) + +```ts +import { Context, Effect, Option, Schema, SchemaParser } from "effect" + +// Define a service that may provide a default value +class ConstructorService extends Context.Service }>()( + "ConstructorService" +) {} + +const schema = Schema.Struct({ + a: Schema.Number.pipe( + Schema.withConstructorDefault( + Effect.gen(function*() { + yield* Effect.sleep(100) + const oservice = yield* Effect.serviceOption(ConstructorService) + if (Option.isNone(oservice)) { + return -1 + } + return yield* oservice.value.defaultValue + }) + ) + ) +}) + +SchemaParser.makeEffect(schema)({}) + .pipe( + Effect.provideService(ConstructorService, ConstructorService.of({ defaultValue: Effect.succeed(0) })), + Effect.runPromise + ) + .then(console.log, console.error) +// { a: 0 } +``` + +# Transformations + +Transformations convert values from one type to another during decoding or encoding. They are standalone, reusable objects you compose with schemas. + +## Transformations as First-Class + +In previous versions, transformations were directly embedded in schemas. In the current version, they are defined as independent values that can be reused across schemas. + +**Example** (Previous approach: inline transformation) + +```ts +const Trim = transform( + String, + Trimmed, + // non re-usable transformation + { + decode: (i) => i.trim(), + encode: identity + } +) {} +``` + +This style made it difficult to reuse logic across different schemas. + +Now, transformations like `trim` are declared once and reused wherever needed. + +**Example** (The `trim` built-in transformation) + +```ts +import { SchemaTransformation } from "effect" + +// const t: Transformation +const t = SchemaTransformation.trim() +``` + +You can apply a transformation to any compatible schema. In this example, `trim` is applied to a string schema using `Schema.decode` (more on this later). + +**Example** (Applying `trim` to a string schema) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const schema = Schema.String.pipe(Schema.decode(SchemaTransformation.trim())) + +console.log(Schema.decodeUnknownSync(schema)(" 123")) +// 123 +``` + +## The Transformation Type + +A `Transformation` carries four type parameters: + +```ts +Transformation +``` + +- `T`: the decoded (output) type +- `E`: the encoded (input) type +- `RD`: the context used while decoding +- `RE`: the context used while encoding + +A `Transformation` consists of two `Getter` functions: + +- `decode: Getter` — transforms a value during decoding +- `encode: Getter` — transforms a value during encoding + +Each `Getter` receives an input and an optional context and returns either a value or an error. Getters can be composed to build more complex logic. + +**Example** (Implementation of `Transformation.trim`) + +```ts +/** + * @category String transformations + * @since 4.0.0 + */ +export function trim(): Transformation { + return new Transformation(Getter.trim(), Getter.passthrough()) +} +``` + +In this case: + +- The `decode` process uses `Getter.trim()` to remove leading and trailing whitespace. +- The `encode` process uses `Getter.passthrough()`, which returns the input as is. + +## Composing Transformations + +You can combine transformations using the `.compose` method. The resulting transformation applies the `decode` and `encode` logic of both transformations in sequence. + +**Example** (Trim and lowercase a string) + +```ts +import { Option, SchemaTransformation } from "effect" + +// Compose two transformations: trim followed by toLowerCase +const trimToLowerCase = SchemaTransformation.trim().compose(SchemaTransformation.toLowerCase()) + +// Run the decode logic manually to inspect the result +console.log(trimToLowerCase.decode.run(Option.some(" Abc"), {})) +/* +{ + _id: 'Exit', + _tag: 'Success', + value: { _id: 'Option', _tag: 'Some', value: 'abc' } +} +*/ +``` + +In this example: + +- The `decode` logic applies `Getter.trim()` followed by `Getter.toLowerCase()`, producing a string that is trimmed and lowercased. +- The `encode` logic is `Getter.passthrough()`, which simply returns the input as-is. + +## Transforming One Schema into Another + +To define how one schema transforms into another, you can use: + +- `Schema.decodeTo` (and its inverse `Schema.encodeTo`) +- `Schema.decode` (and its inverse `Schema.encode`) + +These functions let you attach transformations to schemas, defining how values should be converted during decoding or encoding. + +### decodeTo + +Use `Schema.decodeTo` when you want to transform a source schema into a different target schema. + +You must provide: + +1. The target schema +2. An optional transformation + +If no transformation is provided, the operation is called "schema composition" (see below). + +**Example** (Parsing a number from a string) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const NumberFromString = + // source schema: String + Schema.String.pipe( + Schema.decodeTo( + Schema.Number, // target schema: Number + SchemaTransformation.numberFromString // built-in transformation that coerce a string to a number (and back) + ) + ) + +console.log(Schema.decodeUnknownSync(NumberFromString)("123")) +// 123 +console.log(Schema.decodeUnknownSync(NumberFromString)("a")) +// NaN +``` + +### decode + +Use `Schema.decode` when the source and target schemas are the same and you only want to apply a transformation. + +This is a shorter version of `decodeTo`. + +**Example** (Trimming whitespace from a string) + +```ts +import { Schema, SchemaTransformation } from "effect" + +// Equivalent to decodeTo(Schema.String, Transformation.trim()) +const TrimmedString = Schema.String.pipe(Schema.decode(SchemaTransformation.trim())) +``` + +### Defining an Inline Transformation + +You can create a transformation directly using helpers from the `SchemaTransformation` module. + +For example, `SchemaTransformation.transform` lets you define a simple transformation by providing `decode` and `encode` functions. + +**Example** (Converting meters to kilometers and back) + +```ts +import { Schema, SchemaTransformation } from "effect" + +// Defines a transformation that converts meters (number) to kilometers (number) +// 1000 meters -> 1 kilometer (decode) +// 1 kilometer -> 1000 meters (encode) +const Kilometers = Schema.Finite.pipe( + Schema.decode( + SchemaTransformation.transform({ + decode: (meters) => meters / 1000, + encode: (kilometers) => kilometers * 1000 + }) + ) +) +``` + +You can define transformations that may fail during decoding or encoding using `SchemaTransformation.transformOrFail`. + +This is useful when you need to validate input or enforce rules that may not always succeed. + +**Example** (Converting a string URL into a `URL` object) + +```ts +import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" + +const URLFromString = Schema.String.pipe( + Schema.decodeTo( + Schema.instanceOf(URL), + SchemaTransformation.transformOrFail({ + decode: (s) => + Effect.try({ + try: () => new URL(s), + catch: () => new Issue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` }) + }), + encode: (url) => Effect.succeed(url.href) + }) + ) +) +``` + +## Schema composition + +You can compose transformations, but you can also compose schemas with `Schema.decodeTo`. + +**Example** (Converting meters to miles via kilometers) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const KilometersFromMeters = Schema.Finite.pipe( + Schema.decode( + SchemaTransformation.transform({ + decode: (meters) => meters / 1000, + encode: (kilometers) => kilometers * 1000 + }) + ) +) + +const MilesFromKilometers = Schema.Finite.pipe( + Schema.decode( + SchemaTransformation.transform({ + decode: (kilometers) => kilometers * 0.621371, + encode: (miles) => miles / 0.621371 + }) + ) +) + +const MilesFromMeters = KilometersFromMeters.pipe(Schema.decodeTo(MilesFromKilometers)) +``` + +This approach does not require the source and target schemas to be type-compatible. If you need more control over type compatibility, you can use one of the `Transformation.passthrough*` helpers. + +## Passthrough Helpers + +The `passthrough`, `passthroughSubtype`, and `passthroughSupertype` helpers let you compose schemas by describing how their types relate. + +### passthrough + +Use `passthrough` when the encoded output of the target schema matches the type of the source schema. + +**Example** (When `To.Encoded === From.Type`) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const From = Schema.Struct({ + a: Schema.String +}) + +const To = Schema.Struct({ + a: Schema.FiniteFromString +}) + +// To.Encoded (string) = From.Type (string) +const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthrough())) +``` + +### passthroughSubtype + +Use `passthroughSubtype` when the source type is a subtype of the target's encoded output. + +**Example** (When `From.Type` is a subtype of `To.Encoded`) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const From = Schema.FiniteFromString + +const To = Schema.UndefinedOr(Schema.Number) + +// From.Type (number) extends To.Encoded (number | undefined) +const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthroughSubtype())) +``` + +### passthroughSupertype + +Use `passthroughSupertype` when the target's encoded output is a subtype of the source type. + +**Example** (When `To.Encoded` is a subtype of `From.Type`) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const From = Schema.UndefinedOr(Schema.String) + +const To = Schema.FiniteFromString + +// To.Encoded (string) extends From.Type (string | undefined) +const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthroughSupertype())) +``` + +### Turning off strict mode + +Strict mode ensures that decoding and encoding fully match. You can disable it by passing `{ strict: false }` to `passthrough`. + +**Example** (Turning off strict mode) + +```ts +import { Schema, SchemaTransformation } from "effect" + +const From = Schema.Struct({ + a: Schema.Literals(["a", "b"]), + b: Schema.Number +}) + +const To = Schema.Struct({ + a: Schema.String, + b: Schema.Literals([1, 2]) +}) + +// Neither From.Type nor To.Encoded extends the other. +const schema = From.pipe(Schema.decodeTo(To, SchemaTransformation.passthrough({ strict: false }))) +``` + +## Managing Optional Keys + +You can control how optional values are handled during transformations using the `SchemaTransformation.transformOptional` helper. + +This helper works with `Option` and returns an `Option`, where: + +- `E` is the encoded type +- `T` is the decoded type + +This function is useful when dealing with optional values that may be present or missing during decoding or encoding. + +If the input is `Option.none()`, it means the value is not provided. +If it is `Option.some(value)`, then the transformation logic is applied to `value`. + +You control the optionality of the output by returning an `Option`: + +- `Option.none()`: exclude the key from the output +- `Option.some(transformedValue)`: include the transformed value + +**Example** (Optional string key transformed to `Option`) + +```ts +import { Option, Schema, SchemaTransformation } from "effect" + +const OptionFromNonEmptyString = Schema.optionalKey(Schema.String).pipe( + Schema.decodeTo( + Schema.Option(Schema.NonEmptyString), + SchemaTransformation.transformOptional({ + // Convert empty strings to None, and non-empty strings to Some(value) + decode: (oe) => + Option.isSome(oe) && oe.value !== "" ? Option.some(Option.some(oe.value)) : Option.some(Option.none()), + + // Flatten nested Options back to a single optional string + encode: (ot) => Option.flatten(ot) + }) + ) +) + +const schema = Schema.Struct({ + foo: OptionFromNonEmptyString +}) + +// Decoding examples + +console.log(Schema.decodeUnknownSync(schema)({})) +// Output: { foo: None } + +console.log(Schema.decodeUnknownSync(schema)({ foo: "" })) +// Output: { foo: None } + +console.log(Schema.decodeUnknownSync(schema)({ foo: "hi" })) +// Output: { foo: Some("hi") } + +// Encoding examples + +console.log(Schema.encodeSync(schema)({ foo: Option.none() })) +// Output: {} + +console.log(Schema.encodeSync(schema)({ foo: Option.some("hi") })) +// Output: { foo: "hi" } +``` + +## Omitting a Key During Encoding + +Use `SchemaGetter.omit()` to exclude a field from the encoded output. At runtime, `omit()` returns `Option.none()`, which tells the struct parser to skip writing that key. + +For this to work, the encoded side must be marked as optional with `Schema.optionalKey`. Otherwise, producing `None` for a required field causes a `MissingKey` error. + +**Example** (Field present when decoded, omitted when encoded) + +```ts +import { Effect, Schema, SchemaGetter } from "effect" + +const schema = Schema.Struct({ + a: Schema.FiniteFromString, + b: Schema.String.pipe( + Schema.encodeTo(Schema.optionalKey(Schema.String), { + decode: SchemaGetter.withDefault(Effect.succeed("default_value")), + encode: SchemaGetter.omit() + }) + ) +}) + +// ┌─── { readonly a: string; readonly b?: string; } +// ▼ +type Encoded = typeof schema.Encoded + +// ┌─── { readonly a: number; readonly b: string; } +// ▼ +type Type = typeof schema.Type + +console.log(Schema.decodeUnknownSync(schema)({ a: "1", b: "value" })) +// Output: { a: 1, b: "value" } + +console.log(Schema.decodeUnknownSync(schema)({ a: "1" })) +// Output: { a: 1, b: "default_value" } + +console.log(Schema.encodeSync(schema)({ a: 1, b: "default_value" })) +// Output: { a: "1" } +``` + +For the common case of a discriminator tag that should be omitted during encoding, use `Schema.tagDefaultOmit`: + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + _tag: Schema.tagDefaultOmit("MyTag"), + a: Schema.FiniteFromString +}) + +console.log(Schema.decodeUnknownSync(schema)({ a: "1" })) +// Output: { a: 1, _tag: "MyTag" } + +console.log(Schema.encodeSync(schema)({ a: 1, _tag: "MyTag" })) +// Output: { a: "1" } +``` + +# Flipping Schemas + +Flipping a schema swaps its decoding and encoding directions. If a schema decodes a `string` into a `number`, the flipped version decodes a `number` into a `string`. This is useful when you want to reuse an existing schema but invert its direction. + +**Example** (Flipping a schema that parses a string into a number) + +```ts +import { Schema } from "effect" + +// Flips a schema that decodes a string into a number, +// turning it into one that decodes a number into a string +// +// ┌─── flip +// ▼ +const StringFromFinite = Schema.flip(Schema.FiniteFromString) +``` + +You can access the original schema using the `.schema` property: + +**Example** (Accessing the original schema) + +```ts +import { Schema } from "effect" + +const StringFromFinite = Schema.flip(Schema.FiniteFromString) + +// ┌─── FiniteFromString +// ▼ +StringFromFinite.schema +``` + +Flipping a schema twice returns a schema with the same structure and behavior as the original: + +**Example** (Double flipping restores the original schema) + +```ts +import { Schema } from "effect" + +// ┌─── FiniteFromString +// ▼ +const schema = Schema.flip(Schema.flip(Schema.FiniteFromString)) +``` + +## How it works + +All internal operations in the Schema AST are symmetrical. Encoding with a schema is equivalent to decoding with its flipped version: + +```ts +// Encoding with a schema is the same as decoding with its flipped version +encode(schema) = decode(flip(schema)) +``` + +This symmetry ensures that flipping works consistently across all schema types. + +## Flipped constructors + +A flipped schema also includes a constructor. It builds values of the **encoded** type from the original schema. + +**Example** (Using a flipped schema to construct an encoded value) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.FiniteFromString +}) + +/* +type Encoded = { + readonly a: string; +} +*/ +type Encoded = (typeof schema)["Encoded"] + +// make: { readonly a: string } ──▶ { readonly a: string } +Schema.flip(schema).make +``` + +# Classes and Opaque Types + +Schema supports two kinds of nominal types: _opaque structs_ for lightweight distinct types, and _classes_ for full-featured types with methods and prototype-backed instances. + +## Opaque Structs + +Goal: opaque typing without changing runtime behavior. + +`Schema.Opaque` lets you take an ordinary `Schema.Struct` and wrap it in a thin class shell whose **only** purpose is to create a distinct TypeScript type. + +Internally the value is **still the same plain struct schema**. + +Instance methods and custom constructors **are not allowed** in opaque structs (no `new ...`). +This is not enforced at the type level, but it may be enforced through a linter in the future. + +### How is this different from `Schema.Class`? + +`Schema.Class` also wraps a `Struct`, **but** it turns the wrapper into a proper class: + +- You can add instance methods, getters, setters, custom constructors. +- Instances compare structurally with `Equal.equals`, but they do not implement `Equal`. +- Instances carry the class prototype at runtime, so `instanceof` checks succeed and methods are callable. + +**Example** (Creating an Opaque Struct) + +```ts +import { Schema } from "effect" + +class Person extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String + }) +) {} + +// ┌─── Codec +// ▼ +const codec = Schema.revealCodec(Person) + +// const person: Person +const person = Person.make({ name: "John" }) + +console.log(person.name) +// "John" + +// The class itself holds the original schema and its metadata +console.log(Person) +// -> [Function: Person] Struct$ + +// { readonly name: Schema.String } +Person.fields + +/* +const another: Schema.Struct<{ + readonly name: typeof Person; +}> +*/ +const another = Schema.Struct({ name: Person }) // You can use the opaque type inside other schemas + +/* +type Type = { + readonly name: Person; +} +*/ +type Type = (typeof another)["Type"] +``` + +Opaque structs can be used just like regular structs, with no other changes needed. + +**Example** (Retrieving Schema Fields) + +```ts +import { Schema } from "effect" + +// A function that takes a generic struct +const getFields = (struct: Schema.Struct) => struct.fields + +class Person extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String + }) +) {} + +/* +const fields: { + readonly name: Schema.String; +} +*/ +const fields = getFields(Person) +``` + +### Static methods + +You can add static members to an opaque struct class to extend its behavior. + +**Example** (Custom serializer via static method) + +```ts +import { Schema } from "effect" + +class Person extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String, + createdAt: Schema.Date + }) +) { + // Create a custom serializer using the class itself + static readonly serializer = Schema.toCodecJson(this) +} + +console.log( + Schema.encodeUnknownSync(Person)({ + name: "John", + createdAt: new Date() + }) +) +// { name: 'John', createdAt: 2025-05-02T13:49:29.926Z } + +console.log( + Schema.encodeUnknownSync(Person.serializer)({ + name: "John", + createdAt: new Date() + }) +) +// { name: 'John', createdAt: '2025-05-02T13:49:29.928Z' } +``` + +### Annotations and filters + +You can attach filters and annotations to the struct passed into `Opaque`. + +**Example** (Applying a filter and title annotation) + +```ts +import { Schema } from "effect" + +class Person extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String + }).annotate({ identifier: "Person" }) +) {} + +console.log(String(Schema.decodeUnknownExit(Person)(null))) +// Failure(Cause([Fail(SchemaError: Expected Person, got null)])) +``` + +When you call methods like `annotate` on an opaque struct, you get back the original struct, not a new class. + +```ts +import { Schema } from "effect" + +class Person extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String + }) +) {} + +/* +const S: Schema.Struct<{ + readonly name: Schema.String; +}> +*/ +const S = Person.annotate({ title: "Person" }) // `annotate` returns the wrapped struct type +``` + +### Recursive Opaque Structs + +**Example** (Recursive Opaque Struct with Same Encoded and Type) + +```ts +import { Schema } from "effect" + +export class Category extends Schema.Opaque()( + Schema.Struct({ + name: Schema.String, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + }) +) {} + +/* +type Encoded = { + readonly children: readonly Category[]; + readonly name: string; +} +*/ +export type Encoded = (typeof Category)["Encoded"] +``` + +**Example** (Recursive Opaque Struct with Different Encoded and Type) + +```ts +import { Schema } from "effect" + +interface CategoryEncoded extends Schema.Codec.Encoded {} + +export class Category extends Schema.Opaque()( + Schema.Struct({ + name: Schema.FiniteFromString, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + }) +) {} + +/* +type Encoded = { + readonly children: readonly CategoryEncoded[]; + readonly name: string; +} +*/ +export type Encoded = (typeof Category)["Encoded"] +``` + +**Example** (Mutually Recursive Schemas) + +```ts +import { Schema } from "effect" + +class Expression extends Schema.Opaque()( + Schema.Struct({ + type: Schema.Literal("expression"), + value: Schema.Union([Schema.Number, Schema.suspend((): Schema.Codec => Operation)]) + }) +) {} + +class Operation extends Schema.Opaque()( + Schema.Struct({ + type: Schema.Literal("operation"), + operator: Schema.Literals(["+", "-"]), + left: Expression, + right: Expression + }) +) {} + +/* +type Encoded = { + readonly type: "operation"; + readonly operator: "+" | "-"; + readonly left: { + readonly type: "expression"; + readonly value: number | Operation; + }; + readonly right: { + readonly type: "expression"; + readonly value: number | Operation; + }; +} +*/ +export type Encoded = (typeof Operation)["Encoded"] +``` + +### Branded Opaque Structs + +You can brand an opaque struct using the `Brand` generic parameter. + +**Example** (Branded Opaque Struct) + +```ts +import { Schema } from "effect" + +class A extends Schema.Opaque()( + Schema.Struct({ + a: Schema.String + }) +) {} +class B extends Schema.Opaque()( + Schema.Struct({ + a: Schema.String + }) +) {} + +const f = (a: A) => a +const g = (b: B) => b + +f(A.make({ a: "a" })) // ok +g(B.make({ a: "a" })) // ok + +f(B.make({ a: "a" })) // error: Argument of type 'B' is not assignable to parameter of type 'A'. +g(A.make({ a: "a" })) // error: Argument of type 'A' is not assignable to parameter of type 'B'. +``` + +Like with branded classes, you can use the `Brand` module to create branded opaque structs. + +```ts +import { Schema } from "effect" +import type { Brand } from "effect" + +class A extends Schema.Opaque>()( + Schema.Struct({ + a: Schema.String + }) +) {} +class B extends Schema.Opaque>()( + Schema.Struct({ + a: Schema.String + }) +) {} + +const f = (a: A) => a +const g = (b: B) => b + +f(A.make({ a: "a" })) // ok +g(B.make({ a: "a" })) // ok + +f(B.make({ a: "a" })) // error: Argument of type 'B' is not assignable to parameter of type 'A'. +g(A.make({ a: "a" })) // error: Argument of type 'A' is not assignable to parameter of type 'B'. +``` + +## Schema as a Class + +`Schema.asClass` turns any schema into a class that can be extended with `extends`. The resulting class inherits the full schema API (e.g. `annotate`) and supports static methods that reference `this`. + +Unlike `Schema.Opaque`, it does **not** make the decoded type nominally distinct, and unlike `Schema.Class`, it does **not** create prototype-backed instances with methods or constructors. It is a lightweight way to attach custom static helpers to a schema. + +### Wrapping a Primitive Schema + +```ts +import { Schema } from "effect" + +class MyString extends Schema.asClass(Schema.String) { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) +} + +console.log(MyString.decodeUnknownSync("a")) +// "a" +``` + +### Wrapping a Struct Schema + +```ts +import { Schema } from "effect" + +class MyStruct extends Schema.asClass( + Schema.Struct({ name: Schema.String }) +) { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) +} + +console.log(MyStruct.decodeUnknownSync({ name: "a" })) +// { name: "a" } +``` + +### Subclassing + +You can extend an `asClass` class to layer on more static helpers: + +```ts +import { Schema } from "effect" + +class MyString extends Schema.asClass(Schema.FiniteFromString) { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) +} + +class MyString2 extends MyString { + static readonly encodeSync = Schema.encodeSync(this) +} + +console.log(MyString2.decodeUnknownSync("1")) +// 1 +console.log(MyString2.encodeSync(1)) +// "1" +``` + +## Classes + +### Existing Classes + +#### Validating the Constructor + +**Use Case**: When you want to validate the constructor arguments of an existing class. + +**Example** (Using a tuple to validate the constructor arguments) + +```ts +import { Schema } from "effect" + +const PersonConstructorArguments = Schema.Tuple([Schema.String, Schema.Finite]) + +// Existing class +class Person { + constructor(readonly name: string, readonly age: number) { + PersonConstructorArguments.make([name, age]) + } +} + +try { + new Person("John", NaN) +} catch (error) { + if (error instanceof Error) { + console.log(error.message) + } +} +/* +Expected a finite number, got NaN + at [1] +*/ +``` + +**Example** (Inheritance) + +```ts +import { Schema } from "effect" + +const PersonConstructorArguments = Schema.Tuple([Schema.String, Schema.Finite]) + +class Person { + constructor(readonly name: string, readonly age: number) { + PersonConstructorArguments.make([name, age]) + } +} + +const PersonWithEmailConstructorArguments = Schema.Tuple([Schema.String]) + +class PersonWithEmail extends Person { + constructor(name: string, age: number, readonly email: string) { + // Only validate the additional argument + PersonWithEmailConstructorArguments.make([email]) + super(name, age) + } +} +``` + +#### Defining a Schema + +```ts +import { Schema, SchemaTransformation } from "effect" + +class Person { + constructor(readonly name: string, readonly age: number) {} +} + +const PersonSchema = Schema.instanceOf(Person, { + title: "Person", + // optional: default JSON serialization + toCodecJson: () => + Schema.link()( + Schema.Tuple([Schema.String, Schema.Number]), + SchemaTransformation.transform({ + decode: (args) => new Person(...args), + encode: (instance) => [instance.name, instance.age] as const + }) + ) +}) + // optional: explicit encoding + .pipe( + Schema.encodeTo( + Schema.Struct({ + name: Schema.String, + age: Schema.Number + }), + SchemaTransformation.transform({ + decode: (args) => new Person(args.name, args.age), + encode: (instance) => instance + }) + ) + ) +``` + +**Example** (Inheritance) + +```ts +import { Schema, SchemaTransformation } from "effect" + +class Person { + constructor(readonly name: string, readonly age: number) {} +} + +const PersonSchema = Schema.instanceOf(Person, { + title: "Person", + // optional: default JSON serialization + toCodecJson: () => + Schema.link()( + Schema.Tuple([Schema.String, Schema.Number]), + SchemaTransformation.transform({ + decode: (args) => new Person(...args), + encode: (instance) => [instance.name, instance.age] as const + }) + ) +}) + // optional: explicit encoding + .pipe( + Schema.encodeTo( + Schema.Struct({ + name: Schema.String, + age: Schema.Number + }), + SchemaTransformation.transform({ + decode: (args) => new Person(args.name, args.age), + encode: (instance) => instance + }) + ) + ) + +class PersonWithEmail extends Person { + constructor(name: string, age: number, readonly email: string) { + super(name, age) + } +} + +// const PersonWithEmailSchema = ...repeat the pattern above... +``` + +#### Errors + +**Example** (Extending Data.Error) + +```ts +import { Data, Effect, identity, Schema, SchemaTransformation, SchemaUtils } from "effect" + +const Props = Schema.Struct({ + message: Schema.String +}) + +class Err extends Data.Error { + constructor(props: typeof Props.Type) { + super(Props.make(props)) + } +} + +const program = Effect.gen(function*() { + yield* new Err({ message: "Uh oh" }) +}) + +Effect.runPromiseExit(program).then((exit) => console.log(JSON.stringify(exit, null, 2))) +/* +{ + "_id": "Exit", + "_tag": "Failure", + "cause": { + "_id": "Cause", + "failures": [ + { + "_tag": "Fail", + "error": { + "message": "Uh oh" + } + } + ] + } +} +*/ + +const transformation = SchemaTransformation.transform({ + decode: (props) => new Err(props), + encode: identity +}) + +const schema = Schema.instanceOf(Err, { + title: "Err", + serialization: { + json: () => Schema.link()(Props, transformation) + } +}).pipe(Schema.encodeTo(Props, transformation)) + +// built-in helper? +const builtIn = SchemaUtils.getNativeClassSchema(Err, { encoding: Props }) +``` + +### Class API + +**Example** (Constructing and decoding a class) + +```ts +import { Schema } from "effect" + +// Define a class with a single string field "a" +class A extends Schema.Class("A")({ + a: Schema.String +}) { + // Regular class fields are allowed + readonly _a = 1 +} + +console.log(new A({ a: "a" })) +// A { a: 'a', _a: 1 } +console.log(A.make({ a: "a" })) +// A { a: 'a', _a: 1 } +console.log(Schema.decodeUnknownSync(A)({ a: "a" })) +// A { a: 'a', _a: 1 } +``` + +#### Filters + +To attach a filter to the whole class, pass a `Struct` instead of a field record and call `.check(...)` on it. + +**Example** (Validating a relationship between fields) + +```ts +import { Schema } from "effect" + +class A extends Schema.Class("A")( + Schema.Struct({ + a: Schema.String, + b: Schema.String + }).check(Schema.makeFilter(({ a, b }) => a === b, { title: "a === b" })) +) {} + +try { + new A({ a: "a", b: "b" }) +} catch (error: any) { + console.log(error.message) +} +// Expected a === b, got {"a":"a","b":"b"} + +try { + Schema.decodeUnknownSync(A)({ a: "a", b: "b" }) +} catch (error: any) { + console.log(error.message) +} +// Expected a === b, got {"a":"a","b":"b"} +``` + +#### Branded Classes + +Attach a brand to a class to avoid mixing values from different domains that share the same structure. + +**Example** (Unique brands block assignment) + +```ts +import { Schema } from "effect" + +// Brand the class using a unique symbol type parameter +class A extends Schema.Class("A")({ + a: Schema.String +}) {} + +class B extends Schema.Class("B")({ + a: Schema.String +}) {} + +// Even though A and B have the same fields, their brands are different, +// so they are not assignable to each other. + +// @ts-expect-error +export const a: A = B.make({ a: "a" }) +// @ts-expect-error +export const b: B = A.make({ a: "a" }) +``` + +**Example** (Using the Brand module) + +```ts +import type { Brand } from "effect" +import { Schema } from "effect" + +class A extends Schema.Class>("A")({ + a: Schema.String +}) {} + +class B extends Schema.Class>("B")({ + a: Schema.String +}) {} + +// Different named brands are still not assignable + +// @ts-expect-error +export const a: A = B.make({ a: "a" }) +// @ts-expect-error +export const b: B = A.make({ a: "a" }) +``` + +#### Annotations + +Attach metadata to a class schema. The metadata is stored as annotations on the schema AST and can be read at runtime. + +**Example** (Attaching and reading annotations) + +```ts +import { Schema } from "effect" + +export class A extends Schema.Class("A")( + { + a: Schema.String + }, + // Attach metadata (e.g., title) alongside the schema + { title: "my title" } +) {} + +console.log(A.ast.annotations?.title) +// "my title" +``` + +#### extend + +Use `extend` to create a subclass that adds fields to the base schema. Instance fields declared on the base class are also available on the subclass. + +**Example** (Extending a class with new fields) + +```ts +import { Schema } from "effect" + +// Base class with one schema field ("a") and one regular class field ("_a") +class A extends Schema.Class("A")( + Schema.Struct({ + a: Schema.String + }) +) { + readonly _a = 1 +} + +// Subclass adds a new schema field ("b") and its own regular field ("_b") +class B extends A.extend("B")({ + b: Schema.Number +}) { + readonly _b = 2 +} + +console.log(new B({ a: "a", b: 2 })) +// B { a: 'a', _a: 1, _b: 2 } +console.log(B.make({ a: "a", b: 2 })) +// B { a: 'a', _a: 1, _b: 2 } +console.log(Schema.decodeUnknownSync(B)({ a: "a", b: 2 })) +// B { a: 'a', _a: 1, _b: 2 } +``` + +#### extends and static members + +To keep static members from the base class, pass `typeof Base` as the second generic parameter when calling `extend`. + +**Example** (Preserving static members on subclasses) + +```ts +import { Schema } from "effect" + +class A extends Schema.Class("A")({ + a: Schema.String +}) { + static readonly foo = "foo" +} + +class B extends A.extend("B")({ + b: Schema.Number +}) {} + +console.log(B.foo) +// "foo" +``` + +#### Recursive Classes + +Use `Schema.suspend` to reference a class inside its own definition. This is common for tree-like data structures. + +**Example** (Self-referential tree structure) + +```ts +import { Schema } from "effect" + +// A simple tree of categories where each node can have child categories. +// Use Schema.suspend to refer to Category while it is being defined. +export class Category extends Schema.Class("Category")( + Schema.Struct({ + name: Schema.String, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + }) +) {} + +/* +type Encoded = { + readonly children: readonly Category[]; + readonly name: string; +} +*/ +export type Encoded = (typeof Category)["Encoded"] +``` + +**Example** (Recursive schema with different Encoded and Type) + +```ts +import { Schema } from "effect" + +// Define the encoded representation for Category separately. +// This is useful when the Encoded type differs from the Type type. +interface CategoryEncoded extends Schema.Codec.Encoded {} + +// The runtime type is Category; the encoded form is CategoryEncoded. +// "name" is decoded from a string to a finite number to show that +// Type and Encoded types can differ. +export class Category extends Schema.Class("Category")( + Schema.Struct({ + name: Schema.FiniteFromString, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + }) +) {} + +/* +type Encoded = { + readonly children: readonly CategoryEncoded[]; + readonly name: string; +} +*/ +export type Encoded = (typeof Category)["Encoded"] +``` + +**Example** (Mutually recursive expression language) + +```ts +import { Schema } from "effect" + +class Expression extends Schema.Class("Expression")( + Schema.Struct({ + type: Schema.Literal("expression"), + value: Schema.Union([Schema.Number, Schema.suspend((): Schema.Codec => Operation)]) + }) +) {} + +class Operation extends Schema.Class("Operation")( + Schema.Struct({ + type: Schema.Literal("operation"), + operator: Schema.Literals(["+", "-"]), + left: Expression, + right: Expression + }) +) {} + +/* +type Encoded = { + readonly type: "operation"; + readonly operator: "+" | "-"; + readonly left: { + readonly type: "expression"; + readonly value: number | Operation; + }; + readonly right: { + readonly type: "expression"; + readonly value: number | Operation; + }; +} +*/ +export type Encoded = (typeof Operation)["Encoded"] +``` + +### TaggedClass + +`TaggedClass` is a convenience over `Class` that automatically adds a `_tag` field using `Schema.tag`. This is useful for discriminated unions where each variant needs a tag. + +The tag value doubles as the identifier by default. Pass an explicit identifier as the first argument to override it. + +**Example** (Basic tagged class) + +```ts +import { Schema } from "effect" + +class Person extends Schema.TaggedClass()("Person", { + name: Schema.String +}) {} + +const mike = new Person({ name: "Mike" }) +console.log(mike) +// Person { _tag: 'Person', name: 'Mike' } +console.log(mike._tag) +// "Person" +``` + +**Example** (Custom identifier) + +```ts +import { Schema } from "effect" + +class Person extends Schema.TaggedClass("MyPerson")("Person", { + name: Schema.String +}) {} + +console.log(Person.identifier) +// "MyPerson" +console.log(new Person({ name: "Mike" })._tag) +// "Person" +``` + +**Example** (Discriminated union) + +```ts +import { Schema } from "effect" + +class Cat extends Schema.TaggedClass()("Cat", { + lives: Schema.Number +}) {} + +class Dog extends Schema.TaggedClass()("Dog", { + wagsTail: Schema.Boolean +}) {} + +const Animal = Schema.Union([Cat, Dog]) + +console.log(Schema.decodeUnknownSync(Animal)({ _tag: "Cat", lives: 9 })) +// Cat { _tag: 'Cat', lives: 9 } +``` + +All features from `Class` are available: `extend`, `annotate`, `check`, branded classes, and recursive definitions. + +### ErrorClass + +```ts +import { Schema } from "effect" + +class E extends Schema.ErrorClass("E")({ + id: Schema.Number +}) {} +``` + +### TaggedErrorClass + +`TaggedErrorClass` combines `ErrorClass` with an automatic `_tag` field, giving you a tagged error that can be caught with `Effect.catchTag`. + +Like `TaggedClass`, the tag value doubles as the identifier by default, and you can pass an explicit identifier as the first argument to override it. + +**Example** (Defining and catching a tagged error) + +```ts +import { Effect, Schema } from "effect" + +class HttpError extends Schema.TaggedErrorClass()("HttpError", { + status: Schema.Number, + message: Schema.String +}) {} + +const program = Effect.gen(function*() { + yield* new HttpError({ status: 404, message: "Not found" }) +}) + +const recovered = program.pipe( + Effect.catchTag("HttpError", (err) => Effect.succeed(`Caught: ${err.status} ${err.message}`)) +) +``` + +**Example** (Multiple tagged errors in a union) + +```ts +import { Effect, Schema } from "effect" + +class NotFound extends Schema.TaggedErrorClass()("NotFound", { + path: Schema.String +}) {} + +class Unauthorized extends Schema.TaggedErrorClass()("Unauthorized", { + reason: Schema.String +}) {} + +const program = Effect.gen(function*() { + if (Math.random() < 0.5) { + yield* new Unauthorized({ reason: "Unauthorized" }) + } else { + yield* new NotFound({ path: "/missing" }) + } +}) + +// Each error can be caught independently by its tag +const recovered = program.pipe( + Effect.catchTags({ + NotFound: (err) => Effect.succeed(`Not found: ${err.path}`), + Unauthorized: (err) => Effect.succeed(`Unauthorized: ${err.reason}`) + }) +) +``` + +All features from `ErrorClass` are available: `extend`, `annotate`, and `check`. + +# Serialization + +Serialization converts typed values into a format suitable for storage or transmission (such as JSON, FormData, or XML). Deserialization reverses the process, turning raw data back into typed values. Schema provides built-in support for several common formats. + +## JSON Support + +#### UnknownFromJsonString + +A schema that decodes a JSON-encoded string into an unknown value. + +This schema takes a string as input and attempts to parse it as JSON during decoding. If parsing succeeds, the result is passed along as an unknown value. If the string is not valid JSON, decoding fails. + +When encoding, any value is converted back into a JSON string using JSON.stringify. If the value is not a valid JSON value, encoding fails. + +**Example** + +```ts +import { Schema } from "effect" + +Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(`{"a":1,"b":2}`) +// => { a: 1, b: 2 } +``` + +#### fromJsonString + +Returns a schema that decodes a JSON string and then decodes the parsed value using the given schema. + +This is useful when working with JSON-encoded strings where the actual structure of the value is known and described by an existing schema. + +The resulting schema first parses the input string as JSON, and then runs the provided schema on the parsed result. + +**Example** + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ a: Schema.Number }) +const schemaFromJsonString = Schema.fromJsonString(schema) + +Schema.decodeUnknownSync(schemaFromJsonString)(`{"a":1,"b":2}`) +// => { a: 1 } +``` + +## String Encoding Support + +Schema provides built-in schemas for common string encodings. Each one decodes an encoded string into a UTF-8 string (and encodes back). They can be composed with `fromJsonString` to decode structured data in a single pipeline. + +#### StringFromBase64 + +Decodes a Base64-encoded (RFC 4648) string into a UTF-8 string. + +```ts +import { Schema } from "effect" + +Schema.decodeUnknownSync(Schema.StringFromBase64)("aGVsbG8=") +// => "hello" +``` + +Compose with `fromJsonString` to decode Base64-encoded JSON into a validated struct: + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ a: Schema.Number }) + +// base64 string -> UTF-8 string -> parsed & validated struct +const schemaFromBase64 = Schema.StringFromBase64.pipe( + Schema.decodeTo(Schema.fromJsonString(schema)) +) +``` + +#### StringFromBase64Url + +Like `StringFromBase64`, but uses the URL-safe Base64 alphabet (RFC 4648 section 5). + +```ts +import { Schema } from "effect" + +Schema.decodeUnknownSync(Schema.StringFromBase64Url)("aGVsbG8") +// => "hello" +``` + +#### StringFromHex + +Decodes a hex-encoded string into a UTF-8 string. + +```ts +import { Schema } from "effect" + +Schema.decodeUnknownSync(Schema.StringFromHex)("68656c6c6f") +// => "hello" +``` + +#### StringFromUriComponent + +Decodes a URI-component-encoded string into a UTF-8 string. Useful for storing structured data in URL query parameters. + +```ts +import { Schema } from "effect" + +const PaginationSchema = Schema.Struct({ + maxItemPerPage: Schema.Number, + page: Schema.Number +}) + +const UrlSchema = Schema.StringFromUriComponent.pipe( + Schema.decodeTo(Schema.fromJsonString(PaginationSchema)) +) + +console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 })) +// %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D +``` + +#### Uint8Array variants + +For binary data, use the `Uint8Array` variants instead: + +- `Schema.Uint8ArrayFromBase64` - decodes Base64 into a `Uint8Array`. +- `Schema.Uint8ArrayFromBase64Url` - decodes URL-safe Base64 into a `Uint8Array`. +- `Schema.Uint8ArrayFromHex` - decodes hex into a `Uint8Array`. + +#### Low-level transformations + +The `SchemaTransformation` module exposes the underlying transformations (`stringFromBase64String`, `stringFromBase64UrlString`, `stringFromHexString`, `stringFromUriComponent`). Prefer the built-in `Schema.*` schemas above unless you need to build a custom pipeline. + +## FormData Support + +`Schema.fromFormData` returns a schema that reads a `FormData` instance, +converts it into a tree record using bracket notation, and then decodes the +resulting structure using the provided schema. + +The decoding process has two steps: + +1. Parse `FormData` into a nested tree record. +2. Decode the parsed value with the given schema. + +**Example** (Decoding a flat structure) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromFormData( + Schema.Struct({ + a: Schema.String + }) +) + +const formData = new FormData() +formData.append("a", "1") +formData.append("b", "2") + +console.log(String(Schema.decodeUnknownExit(schema)(formData))) +// Success({"a":"1"}) +``` + +You can express nested values using bracket notation. + +**Example** (Nested fields) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromFormData( + Schema.Struct({ + a: Schema.String, + b: Schema.Struct({ + c: Schema.String, + d: Schema.String + }) + }) +) + +const formData = new FormData() +formData.append("a", "1") +formData.append("b[c]", "2") +formData.append("b[d]", "3") + +console.log(String(Schema.decodeUnknownExit(schema)(formData))) +// Success({"a":"1","b":{"c":"2","d":"3"}}) +``` + +If you want to decode string fields into non-string primitive values, use `Schema.toCodecStringTree`. + +**Example** (Parsing non-string values) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromFormData( + Schema.toCodecStringTree( + Schema.Struct({ + a: Schema.Int + }) + ) +) + +const formData = new FormData() +formData.append("a", "1") + +console.log(String(Schema.decodeUnknownExit(schema)(formData))) +// Success({"a":1}) // Note: the value is a number +``` + +## URLSearchParams Support + +`Schema.fromURLSearchParams` returns a schema that reads a `URLSearchParams` +instance, converts it into a tree record using bracket notation, and then decodes +the resulting structure using the provided schema. + +The decoding process has two steps: + +1. Parse `URLSearchParams` into a nested tree record. +2. Decode the parsed value with the given schema. + +**Example** (Decoding a flat structure) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromURLSearchParams( + Schema.Struct({ + a: Schema.String + }) +) + +const urlSearchParams = new URLSearchParams("a=1&b=2") + +console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) +// Success({"a":"1"}) +``` + +You can express nested values using bracket notation. + +**Example** (Nested fields) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromURLSearchParams( + Schema.Struct({ + a: Schema.String, + b: Schema.Struct({ + c: Schema.String, + d: Schema.String + }) + }) +) + +const urlSearchParams = new URLSearchParams("a=1&b[c]=2&b[d]=3") + +console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) +// Success({"a":"1","b":{"c":"2","d":"3"}}) +``` + +If you want to decode values that are not strings, use `Schema.toCodecStringTree`. This serializer preserves values such as numbers when compatible with the schema. + +**Example** (Parsing non-string values) + +```ts +import { Schema } from "effect" + +const schema = Schema.fromURLSearchParams( + Schema.toCodecStringTree( + Schema.Struct({ + a: Schema.Int + }) + ) +) + +const urlSearchParams = new URLSearchParams("a=1&b=2") + +console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) +// Success({"a":1}) // Note: the value is a number +``` + +## Canonical Codecs + +When sending data over the network or storing it on disk, you need to convert your domain types to a format like JSON. Schema provides built-in support for serializing values to JSON, strings, FormData, URLSearchParams, and XML. + +Canonical codecs turn one schema into another schema (a "codec") that can serialize and deserialize values using a specific format (JSON, strings, `URLSearchParams`, `FormData`, and so on). This helps you map your domain types to formats that can only represent a limited set of values. + +To keep things concrete, the rest of this page focuses on JSON. + +### JSON Canonical Codec + +Many JavaScript values cannot be serialized to JSON in a safe and reversible way: + +- `Date`: `JSON.stringify()` converts a date to an ISO string, but `JSON.parse()` does not restore a `Date` object +- `Uint8Array`, `ReadonlyMap`, `ReadonlySet`: `JSON.stringify()` converts them to `{}`, so the original data is lost +- `Symbol`, `BigInt`: `JSON.stringify()` throws errors +- Custom classes and Effect data types (`Option`, `Result`, and so on): `JSON.stringify()` does not know how to encode or decode them + +This can lead to data loss, runtime errors, or values that decode into the wrong shape when you try to round-trip complex data through JSON. + +**The solution** + +A canonical codec describes how values that match a schema should be converted to a specific format. In practice, canonical codecs work like this: + +1. **Annotation-based**: you choose a serialization strategy by adding annotations to your schema (for example `toCodecJson`, `toCodecIso`, `toCodecStringTree`, and others). +2. **AST transformation**: the codec builder walks the schema AST and produces a new schema that represents the serialized form (this traversal is handled by Effect). +3. **Recursive composition**: codecs apply through nested structures (objects, arrays, unions, and so on) without you having to wire everything manually. + +The next example shows why a custom class needs a codec when working with JSON. + +**Example** (A custom class that does not round-trip through JSON) + +```ts +import { Schema } from "effect" + +class Point { + constructor(public readonly x: number, public readonly y: number) {} + + // Plain method on a class instance + distance(other: Point): number { + const dx = this.x - other.x + const dy = this.y - other.y + return Math.sqrt(dx * dx + dy * dy) + } +} + +const PointSchema = Schema.instanceOf(Point) +``` + +Even if encoding produces something JSON-looking, decoding cannot rebuild a `Point` instance (including its prototype and methods) from plain JSON data. + +```ts +// Encode a Point instance using the schema, then stringify it. +// This produces a plain JSON object, not a class instance. +const json = JSON.stringify(Schema.encodeUnknownSync(PointSchema)(new Point(1, 2))) + +console.log(json) +// '{"x":1,"y":2}' + +// Decode attempts to create a Point instance from parsed JSON. +// This fails because JSON.parse returns a plain object, not `new Point(...)`. +try { + Schema.decodeUnknownSync(PointSchema)(JSON.parse(json)) +} catch (error) { + console.error(String(error)) +} +``` + +The same issue shows up when generating a JSON Schema document: since the schema represents a class instance and there is no JSON representation for it, the generator falls back to a placeholder. + +```ts +console.log(Schema.toJsonSchemaDocument(PointSchema)) +// { dialect: 'draft-2020-12', schema: { type: 'null' }, definitions: {} } +``` + +#### Configuring the Codec + +You configure the canonical JSON codec by adding a `toCodecJson` annotation to your schema. + +Then you call `Schema.toCodecJson(schema)` to produce a codec schema that can encode and decode values to and from JSON-compatible data. + +**Example** (Encoding a class as a JSON tuple) + +```ts +import { Schema, SchemaTransformation } from "effect" + +class Point { + constructor(public readonly x: number, public readonly y: number) {} + + distance(other: Point): number { + const dx = this.x - other.x + const dy = this.y - other.y + return Math.sqrt(dx * dx + dy * dy) + } +} + +const PointSchema = Schema.instanceOf(Point, { + toCodecJson: () => + Schema.link()( + // Pick a JSON representation for Point. + // Here we use a fixed-length tuple: [x, y]. + Schema.Tuple([Schema.Finite, Schema.Finite]), + SchemaTransformation.transform({ + // Decode: convert the JSON representation into a Point instance. + decode: (args) => new Point(...args), + + // Encode: convert a Point instance into the JSON representation. + encode: (instance) => [instance.x, instance.y] as const + }) + ) +}) + +// Convert the schema into a JSON codec schema. +const codecJson = Schema.toCodecJson(PointSchema) + +// Encoding produces JSON-safe data, so it can be stringified. +console.log(JSON.stringify(Schema.encodeUnknownSync(codecJson)(new Point(1, 2)))) +// "[1,2]" + +// Decoding rebuilds the Point instance from parsed JSON. +console.log(Schema.decodeUnknownSync(codecJson)(JSON.parse("[1,2]"))) +// Point { x: 1, y: 2 } + +// JSON Schema generation now has a real representation to work with. +console.dir(Schema.toJsonSchemaDocument(PointSchema), { depth: null }) +/* +{ + dialect: 'draft-2020-12', + schema: { + type: 'array', + prefixItems: [ { type: 'number' }, { type: 'number' } ], + maxItems: 2, + minItems: 2 + }, + definitions: {} +} +*/ +``` + +When you use `toCodecJson`, you describe the JSON shape once (in the schema), and Effect can reuse that description in two places: + +- `Schema.toCodecJson(...)` uses it to encode and decode JSON data at runtime. +- `Schema.toJsonSchemaDocument(...)` uses it to produce a JSON Schema document for the same JSON shape. + +Because both outputs come from the same annotation, they describe the same format (in this example, a two-item array `[x, y]`). If you change the JSON representation in `toCodecJson`, both the codec and the generated JSON Schema will change with it. + +You can use the JSON Schema to validate or describe the JSON data (for example in OpenAPI), and use the codec schema to encode and decode values in that same format. + +#### How `toCodecJson` Works + +When you call `Schema.toCodecJson(schema)`, the library: + +1. **Walks the AST**: it traverses the schema's abstract syntax tree (AST) recursively. For details, see the `SchemaAST` module. +2. **Finds annotations**: it looks for `toCodecJson` annotations on nodes. +3. **Applies transformations**: it replaces types that are not JSON-friendly with types that are. +4. **Composes recursively**: it builds codecs for nested schemas by combining the codecs of their parts. + +#### Custom Encodings + +`Schema.toCodecJson` respects **explicit encodings** you add to a schema. If you choose a custom representation, that choice takes priority over the default. + +**Example** (Custom encoding takes priority over default Date handling) + +```ts +import { Schema, SchemaTransformation } from "effect" + +// Custom Date encoding (Date -> number) +const DateFromEpochMillis = Schema.Date.pipe( + Schema.encodeTo( + Schema.Number, + SchemaTransformation.transform({ + decode: (epochMillis) => new Date(epochMillis), + encode: (date) => date.getTime() + }) + ) +) + +const schema = Schema.Struct({ + date1: DateFromEpochMillis, + date2: Schema.Date +}) + +const toCodecJson = Schema.toCodecJson(schema) + +const data = { date1: new Date("2021-01-01"), date2: new Date("2021-01-01") } + +const serialized = Schema.encodeUnknownSync(toCodecJson)(data) +console.log(serialized) +// { date1: 1609459200000, date2: "2021-01-01T00:00:00.000Z" } +// date1 uses your custom number format, date2 uses the default ISO string format +``` + +### StringTree Canonical Codec + +The `StringTree` codec converts all values to strings, keeping the structure but not the original types. + +```ts +type StringTree = string | undefined | { readonly [key: string]: StringTree } | ReadonlyArray +``` + +A StringTree codec turns any value into a structure made only of: + +- strings +- `undefined` +- plain objects containing other `StringTree` values +- arrays of `StringTree` values + +#### toCodecJson vs toCodecStringTree + +**Example** (Comparing JSON and StringTree codecs) + +```ts +import { Schema, SchemaTransformation } from "effect" + +class Point { + constructor(public readonly x: number, public readonly y: number) {} + + distance(other: Point): number { + const dx = this.x - other.x + const dy = this.y - other.y + return Math.sqrt(dx * dx + dy * dy) + } +} + +const PointSchema = Schema.instanceOf(Point, { + toCodecJson: () => + Schema.link()( + Schema.Tuple([Schema.Finite, Schema.Finite]), + SchemaTransformation.transform({ + decode: (args) => new Point(...args), + encode: (instance) => [instance.x, instance.y] as const + }) + ) +}) + +const point = new Point(1, 2) + +const toCodecJson = Schema.toCodecJson(PointSchema) + +const json = Schema.encodeUnknownSync(toCodecJson)(point) + +// keeps numbers as numbers +console.log(json) +// [1, 2] + +const toCodecStringTree = Schema.toCodecStringTree(PointSchema) + +const stringTree = Schema.encodeUnknownSync(toCodecStringTree)(point) + +// every leaf value becomes a string +console.log(stringTree) +// [ '1', '2' ] +``` + +### ISO Canonical Codec + +The ISO canonical codec (`toCodecIso`) converts schemas to their `Iso` representation. This is useful when you want to build isomorphic transformations or optics. + +**Example** (Using the ISO canonical codec with a Class) + +```ts +import { Schema } from "effect" + +// Define a class schema +class Person extends Schema.Class("Person")({ + name: Schema.String, + age: Schema.Number +}) {} + +const codecIso = Schema.toCodecIso(Person) + +// The Iso type represents the "focus" of the schema. +// For Class schemas, the Iso type is the struct representation +// of the class fields: { readonly name: string; readonly age: number } +// This allows you to convert between the class instance and a plain object +// with the same shape, which is useful for optics and transformations. + +const person = new Person({ name: "John", age: 30 }) + +const serialized = Schema.encodeUnknownSync(codecIso)(person) +console.log(serialized) +// { name: 'John', age: 30 } + +const deserialized = Schema.decodeUnknownSync(codecIso)(serialized) +console.log(deserialized) +// Person { name: 'John', age: 30 } +``` + +ISO serializers are mainly used internally for building optics and reusable transformations. + +## XML Encoder + +`Schema.toEncoderXml` lets you serialize values to XML. +It uses the `toCodecStringTree` serializer internally. + +**Example** + +```ts +import { Effect, Option, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Array(Schema.NullOr(Schema.String)), + c: Schema.Struct({ + d: Schema.Option(Schema.String), + e: Schema.Date + }), + f: Schema.optional(Schema.String) +}) + +// const encoder: (t: {...}) => Effect +const xmlEncoder = Schema.toEncoderXml(schema) + +console.log( + Effect.runSync( + xmlEncoder({ + a: "", + b: ["bar", "baz", null], + c: { d: Option.some("qux"), e: new Date("2021-01-01") }, + f: undefined + }) + ) +) +/* + + + + bar + baz + + + + + <_tag>Some + qux + + 2021-01-01T00:00:00.000Z + + + +*/ +``` + +**Note**. Schemas representing custom types are encoded as `undefined`: + +# Schema Generation and Tooling + +Schema can derive JSON Schemas, test data generators (Arbitraries), equivalence checks, optics, and more from a single schema definition. + +### Generating a JSON Schema from a Schema + +#### Basic Conversion + +By default, a schema produces a draft-2020-12 JSON Schema. + +The result is a data structure including: + +- the source of the JSON Schema (e.g. `draft-2020-12`, `draft-07`, etc...) +- the JSON Schema itself +- any definitions referenced by `$ref` (if any) + +**Example** (Tuple to draft-2020-12 JSON Schema) + +```ts +import { Schema } from "effect" + +// Define a tuple: [string, number] +const schema = Schema.Tuple([Schema.String, Schema.Finite]) + +// Generate a draft-2020-12 JSON Schema +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +Output: +{ + "source": "draft-2020-12", + "schema": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "definitions": {} +} +*/ +``` + +To generate a draft-07 JSON Schema, use `JsonSchema.toDocumentDraft07` to convert the draft-2020-12 JSON Schema. + +**Example** (Tuple to draft-7 JSON Schema) + +```ts +import { JsonSchema, Schema } from "effect" + +const schema = Schema.Tuple([Schema.String, Schema.Finite]) + +const doc2020_12 = Schema.toJsonSchemaDocument(schema) +const doc07 = JsonSchema.toDocumentDraft07(doc2020_12) + +console.log(JSON.stringify(doc07, null, 2)) +/* +Output: +{ + "source": "draft-07", + "schema": { + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "definitions": {} +} +*/ +``` + +#### Attaching Standard Metadata + +Use `.annotate(...)` to attach standard JSON Schema annotations: + +- `title` +- `description` +- `default` +- `examples` +- `readOnly` +- `writeOnly` + +**Example** (Adding basic annotations) + +```ts +import { Schema } from "effect" + +const schema = Schema.NonEmptyString.annotate({ + title: "Username", + description: "A non-empty user name string", + default: "anonymous", + examples: ["alice", "bob"] +}) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "title": "Username", + "description": "A non-empty user name string", + "default": "anonymous", + "examples": [ + "alice", + "bob" + ] + } + ] + }, + "definitions": {} +} +*/ +``` + +#### Annotating the Encoded Side of a Transformation + +When a schema includes a transformation (e.g. `Schema.Trim`), the generated JSON Schema corresponds to the encoded side. Calling `.annotate(...)` on a transformation annotates the decoded side, so the annotations won't appear in the JSON Schema output. + +To annotate the encoded side, use `Schema.annotateEncoded`. + +**Example** (Annotating the encoded side of `Trim`) + +```ts +import { Schema } from "effect" + +const schema = Schema.Trim.pipe( + Schema.annotateEncoded({ + description: "my description", + title: "my title" + }) +) + +console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2)) +/* +{ + "dialect": "draft-2020-12", + "schema": { + "type": "string", + "title": "my title", + "description": "my description" + }, + "definitions": {} +} +*/ +``` + +Alternatively, build a custom transformation using `Schema.decodeTo`: + +```ts +import { Schema, SchemaTransformation } from "effect" + +const schema = Schema.String.annotate({ + description: "my description", + title: "my title" +}).pipe(Schema.decodeTo(Schema.Trimmed, SchemaTransformation.trim())) + +console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2)) +/* +{ + "dialect": "draft-2020-12", + "schema": { + "type": "string", + "title": "my title", + "description": "my description" + }, + "definitions": {} +} +*/ +``` + +#### Optional fields / elements + +Optional fields are converted to optional fields or elements in the JSON Schema. + +**Example** + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.String) +}) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + }, + "additionalProperties": false + }, + "definitions": {} +} +*/ +``` + +Fields including `undefined` (such as those defined unsing `Schema.optional` or `Schema.UndefinedOr`) are converted to optional fields or elements in the JSON Schema with a union with the `null` type. + +**Example** + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.optional(Schema.String) +}) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "object", + "properties": { + "a": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "definitions": {} +} +*/ +``` + +#### Defining a JSON-safe representation for custom types + +This example shows how `Schema.toCodecJson` and `Schema.toJsonSchema` can describe the same JSON shape for a custom type. + +`Headers` is not JSON-friendly by default. `JSON.stringify(new Headers({ a: "b" }))` produces `{}` because the header data is not stored in enumerable properties. By adding a `toCodecJson` annotation, you define a JSON-safe representation and use it for both serialization and JSON Schema generation. + +**Example** (Align a JSON serializer and JSON Schema for `Headers`) + +```ts +import { Schema, SchemaGetter } from "effect" + +const data = new Headers({ a: "b" }) + +// `Headers` does not serialize to JSON in a useful way by default. +console.log(JSON.stringify(data)) +// {} + +// Define a schema with a `toCodecJson` annotation. +// The JSON form will be: [ [name, value], ... ]. +const MyHeaders = Schema.instanceOf(Headers, { + toCodecJson: () => + Schema.link()( + // JSON-safe representation: array of [key, value] pairs + Schema.Array(Schema.Tuple([Schema.String, Schema.String])), + { + decode: SchemaGetter.transform((headers) => new Headers(headers.map(([key, value]) => [key, value]))), + encode: SchemaGetter.transform((headers) => [...headers.entries()]) + } + ) +}) + +const schema = Schema.Struct({ + headers: MyHeaders +}) + +// Build a serializer that produces JSON-safe values using the `toCodecJson` annotation. +const serializer = Schema.toCodecJson(schema) + +const json = Schema.encodeUnknownSync(serializer)({ + headers: data +}) + +// The JSON-encoded value: +console.log(json) +// { headers: [ [ 'a', 'b' ] ] } + +// Generate a JSON Schema that matches the JSON-safe shape produced by the serializer. +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document.schema, null, 2)) +/* +{ + "type": "object", + "properties": { + "headers": { + "type": "array", + "items": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "required": [ + "headers" + ], + "additionalProperties": false +} +*/ + +// Example (Decode a JSON-safe value using the same serializer) +// If a value matches the JSON Schema above, you can decode it with the serializer. +console.log(String(Schema.decodeUnknownExit(serializer)(json))) +// Success({"headers":Headers([["a","b"]])}) +``` + +#### Validation Constraints + +**Example** + +```ts +import { Schema } from "effect" + +const schema = Schema.String.check(Schema.isMinLength(1)) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "string", + "allOf": [ + { + "minLength": 1 + } + ] + }, + "definitions": {} +} +*/ +``` + +**Example** (Multiple filters) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.check( + Schema.isMinLength(1, { description: "description1" }), + Schema.isMaxLength(2, { description: "description2" }) +) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "string", + "allOf": [ + { + "minLength": 1, + "description": "description1" + }, + { + "maxLength": 2, + "description": "description2" + } + ] + }, + "definitions": {} +} +*/ +``` + +#### The fromJsonString combinator + +With `fromJsonString`, the generated schema uses `contentSchema` to embed the JSON Schema of the decoded value. + +**Example** (Embedding `contentSchema` for JSON string content) + +```ts +import { Schema } from "effect" + +// Original value is an object with a string field 'a' +const original = Schema.Struct({ a: Schema.String }) + +// fromJsonString: the outer value is a string, +// but its content must be valid JSON matching 'original' +const schema = Schema.fromJsonString(original) + +const document = Schema.toJsonSchemaDocument(schema) + +console.log(JSON.stringify(document, null, 2)) +/* +{ + "source": "draft-2020-12", + "schema": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + }, + "required": [ + "a" + ], + "additionalProperties": false + } + }, + "definitions": {} +} +*/ +``` + +### Generating an Arbitrary from a Schema + +Property-based tests need generators. `Schema.toArbitrary` derives a +`fast-check` `Arbitrary` that generates decoded `Type` values accepted by the +schema. + +Most schemas do not need any extra work: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const Person = Schema.Struct({ + name: Schema.String, + age: Schema.Int.check(Schema.isBetween({ minimum: 18, maximum: 80 })) +}) + +const PersonArbitrary = Schema.toArbitrary(Person) + +console.log(FastCheck.sample(PersonArbitrary, 3)) +``` + +Use `Schema.toArbitraryLazy` only when you want the caller to provide +`fast-check`: + +```ts +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +const makeStringArbitrary = Schema.toArbitraryLazy(Schema.String) + +const StringArbitrary = makeStringArbitrary(FastCheck) +``` + +`Schema.Never` and declaration schemas without a `toArbitrary` annotation cannot +be derived automatically. + +#### Filters + +Generated values are always checked by the schema filters before they are +returned. The important question is whether a filter can also help choose a good +generator. + +Built-in filters already do this: + +```ts +import { Schema } from "effect" + +const Username = Schema.String.check( + Schema.isMinLength(3), + Schema.isMaxLength(20), + Schema.isPattern(/^[a-z0-9_]+$/) +) + +const PositiveInteger = Schema.Int.check( + Schema.isGreaterThanOrEqualTo(1) +) + +const Tags = Schema.Array(Schema.String).check( + Schema.isMinLength(1), + Schema.isUnique() +) +``` + +For these schemas, `toArbitrary` does not generate random unconstrained strings, +numbers, or arrays and then hope the filters pass. It uses the length, range, +pattern, and uniqueness metadata to build a better generator first. + +A custom filter without metadata is still correct, but may be inefficient: + +```ts +import { Schema } from "effect" + +const isPalindrome = (s: string) => s === Array.from(s).reverse().join("") + +const Palindrome = Schema.String.check( + Schema.makeFilter(isPalindrome, { + expected: "a palindrome" + }) +) +``` + +This works because the final predicate check rejects strings that are not +palindromes. It may need many attempts, because the base string generator has no +reason to produce mirrored strings. + +#### Reports + +Use `{ report: true }` when you want to know which filters did not guide +generation: + +```ts +import { Schema } from "effect" + +const isPalindrome = (s: string) => s === Array.from(s).reverse().join("") + +const Palindrome = Schema.String.check( + Schema.makeFilter(isPalindrome, { + expected: "a palindrome" + }) +) + +const result = Schema.toArbitrary(Palindrome, { report: true }) + +result.value +result.report.warnings +``` + +An `OpaqueFilter` warning means: "this filter is still checked, but it did not +help build the generator." + +Reports contain warnings only. Unsupported schemas, impossible constraints, +invalid candidates, and recursive schemas without a finite terminal path still +fail immediately. + +#### Custom Filters With Constraints + +If part of a custom filter can be described as a normal generation constraint, +attach `arbitrary.constraint` to the filter. The constraint does not have to +prove the whole predicate; it just makes the base generator closer to the values +the predicate accepts. + +```ts +import { Order, Schema } from "effect" + +const isPrimeNumber = (n: number) => { + if (!Number.isInteger(n) || n < 2) { + return false + } + for (let divisor = 2; divisor * divisor <= n; divisor++) { + if (n % divisor === 0) { + return false + } + } + return true +} + +const prime = Schema.makeFilter(isPrimeNumber, { + expected: "a prime number", + arbitrary: { + constraint: { + integer: true, + ordered: { + order: Order.Number, + minimum: 2 + } + } + } +}) + +const Prime = Schema.Number.check(prime) +``` + +The filter still checks primality. The constraint only tells `toArbitrary` not +to waste time on non-integers or numbers below `2`. + +Think of `constraint` as a small vocabulary that the current schema node can +understand: + +- On strings, `minLength` and `maxLength` mean string length. +- On arrays, `minLength` and `maxLength` mean array length. +- On objects, `minLength` and `maxLength` mean final own-property count. +- On sets, maps, hash collections, and chunks, `minLength` and `maxLength` mean final collection size. +- `patterns` apply to string generation. +- `integer`, `noNaN`, `noInfinity`, `valid`, and `unique` are enabled when any contributing filter sets them. +- `ordered` stores bounds for ordered values such as numbers, bigints, dates, `DateTime`, and `BigDecimal`. + +Fields that do not make sense for the current node are ignored. The final filter +check still validates every generated value. + +#### Custom Filters With Candidates + +Use a candidate when the filter cannot be expressed with the constraint +vocabulary. + +```ts +import { Schema } from "effect" + +const reverse = (s: string) => Array.from(s).reverse().join("") + +const isPalindrome = (s: string) => s === reverse(s) + +const palindrome = Schema.makeFilter( + isPalindrome, + { + expected: "a palindrome", + arbitrary: { + candidate: { + weight: 5, + make: (fc) => fc.string().map((half) => `${half}${reverse(half)}`) + } + } + } +) + +const Palindrome = Schema.String.check(palindrome) +``` + +A candidate is an extra source used together with the schema node's base +generator. The base generator has weight `1`. A candidate has weight `1` unless +you set another positive integer weight. + +With one candidate at weight `5`, fast-check tries the candidate roughly five +times as often as the base generator. Candidate values are still checked by all +filters, so a bad candidate can waste attempts but cannot produce invalid +values. + +`make` receives the arbitrary context and may return `undefined` when the +candidate should not be used for that context. + +#### Schema-Level Overrides + +Use a `toArbitrary` annotation when you want to replace the generator for a +schema node. + +The annotation is not limited to declaration schemas. You can attach it to a +normal schema with `.annotate(...)`: + +```ts +import { Schema } from "effect" + +const Name = Schema.String.annotate({ + toArbitrary: () => (fc) => fc.constantFrom("Alice", "Bob", "Carol") +}) +``` + +Put override annotations on base schemas when possible, before adding filters: + +```ts +const Name = Schema.String.annotate({ + toArbitrary: () => (fc) => fc.constantFrom("Alice", "Bob", "Carol") +}).check(Schema.isMinLength(1)) +``` + +This shape is easier to reason about. The override provides the base generator; +the filter remains a normal filter. Schema still checks generated values at the +end. + +Avoid putting an override on a schema that already has filters unless the +override intentionally handles those filters too: + +```ts +const Name = Schema.String.check(Schema.isMinLength(1)).annotate({ + toArbitrary: () => (fc) => fc.constant("") +}) +``` + +This is valid TypeScript, but it is a bad generator: it always generates a value +that the filter rejects. + +The second argument of a `toArbitrary` hook is the arbitrary context. Its +`constraint` field contains constraints collected from filters on the same +schema node as the override. If the override is placed before `.check(...)`, the +context does not include the later filters. If the override is placed after +`.check(...)`, the context includes those filters and the override must respect +them. + +`context.recursion` is present while deriving inside a recursive schema. + +#### Declaration Schemas + +Declaration schemas are opaque to Schema. If you define one, provide a +`toArbitrary` hook. + +For an atomic declaration, return a normal `fast-check` arbitrary: + +```ts +import { Schema } from "effect" + +const Url = Schema.instanceOf(globalThis.URL, { + title: "URL", + toArbitrary: () => (fc) => fc.webUrl().map((s) => new globalThis.URL(s)) +}) +``` + +Generic declarations receive one derivation per type parameter: + +- `arbitrary`: the normal generator for the type parameter. +- `terminal`: a finite generator for the type parameter, used to close recursive generation. + +For an opaque wrapper type, you usually map both sources in the same way: + +```ts +import { Effect, Option, Schema, SchemaIssue, SchemaParser } from "effect" + +class Box { + private constructor(private readonly value: A) {} + + static make(value: A): Box { + return new Box(value) + } + + static unbox(box: Box): A { + return box.value + } +} + +const isBox = (u: unknown): u is Box => u instanceof Box + +const BoxSchema = (value: A) => + Schema.declareConstructor, Box>()( + [value], + ([valueCodec]) => (input, ast, options) => { + if (!isBox(input)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + } + return Effect.map( + SchemaParser.decodeUnknownEffect(valueCodec)(Box.unbox(input), options), + Box.make + ) + }, + { + toArbitrary: ([value]) => () => ({ + arbitrary: value.arbitrary.map(Box.make), + terminal: value.terminal?.map(Box.make) + }) + } + ) +``` + +This looks like duplicated code, but it is not the same generator twice. It is +the same opaque constructor applied to two different sources. + +Suppose someone later builds a recursive schema like this: + +```ts +interface Tree { + readonly value: A + readonly children: ReadonlyArray> +} + +type BoxedTree = Box> +``` + +`Box` does not know whether `A` is recursive. If `A` is `Tree`, then +`value.arbitrary` may generate a recursive tree, while `value.terminal` is the +finite tree generator used when the recursion budget is exhausted. Mapping both +sources through `Box.make` preserves that information. If `Box` returned only +`arbitrary`, it would hide the finite path from outer recursive schemas. + +If the type parameter has no finite terminal generator, `value.terminal` is +`undefined`, and the wrapper cannot provide a terminal branch either. + +#### Integration with Synthetic Data Generation Tools + +Synthetic data libraries such as `@faker-js/faker` are useful when the generated +values should look realistic. Put them behind a Fast-Check arbitrary instead of +calling them directly, so Fast-Check still controls randomness and shrinking. + +```ts +import { faker } from "@faker-js/faker" +import { Schema } from "effect" +import { FastCheck } from "effect/testing" + +/** + * Make it easy to plug a Faker generator into a Schema's `toArbitrary` override. + * The seed comes from Fast-Check so data is reproducible and shrinks correctly. + */ +function fake( + gen: (f: typeof faker) => A +): Schema.Annotations.ToArbitrary.Declaration { + return () => (fc) => + fc.nat().map((seed) => { + faker.seed(seed) + return gen(faker) + }) +} + +const FirstName = Schema.String.annotate({ + toArbitrary: fake((faker) => faker.person.firstName()) +}) + +const LastName = Schema.String.annotate({ + toArbitrary: fake((faker) => faker.person.lastName()) +}) + +const JobTitle = Schema.String.annotate({ + toArbitrary: fake((faker) => faker.person.jobTitle()) +}) + +const Company = Schema.String.annotate({ + toArbitrary: fake((faker) => faker.company.name()) +}) + +const Person = Schema.Struct({ + firstName: FirstName, + lastName: LastName, + jobTitle: JobTitle, + company: Company +}) + +console.log(FastCheck.sample(Schema.toArbitrary(Person), 3)) +``` + +These overrides are useful because the values have domain shape: names look like +names, job titles look like job titles, and companies look like companies. For +plain numeric ranges, prefer Schema constraints and the default arbitrary +derivation. + +If you combine a Faker source with filters, put the override on the base schema +first and add filters afterwards. This keeps the responsibilities simple: the +override chooses a realistic source, and the filter remains the final validation +rule. If you put the override after `.check(...)`, the override must respect +those filters itself, or generation will spend time producing values that are +rejected. + +### Generating an Equivalence from a Schema + +An equivalence function checks whether two values are structurally equal according to the schema's definition. Schema derives this automatically, so you do not need to write manual comparison logic. + +**Example** (Deriving equivalence for a basic schema) + +```ts +import { Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}) + +const equivalence = Schema.toEquivalence(schema) +``` + +#### Declarations + +**Example** (Providing a custom equivalence for a class) + +```ts +import { Schema } from "effect" + +class MyClass { + constructor(readonly a: string) {} +} + +const schema = Schema.instanceOf(MyClass, { + toEquivalence: () => (x, y) => x.a === y.a +}) + +const equivalence = Schema.toEquivalence(schema) +``` + +#### Overrides + +You can override the derived equivalence for a schema using `overrideToEquivalence`. This is useful when the default derivation does not fit your requirements. + +**Example** (Overriding equivalence for a struct) + +```ts +import { Equivalence, Schema } from "effect" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}).pipe(Schema.overrideToEquivalence(() => Equivalence.make((x, y) => x.a === y.a))) + +const equivalence = Schema.toEquivalence(schema) +``` + +### Generating an Optic from a Schema + +Optics provide a composable way to read and update deeply nested values without mutating the original object. Schema can derive optics automatically from your schema definition. + +#### Problem + +The `Optic` module only works with plain JavaScript objects and collections (structs, records, tuples, and arrays). +This can feel restrictive when working with custom types. + +To work around this, you can define an `Iso` between your custom type and a plain JavaScript object. + +**Example** (Defining an `Iso` manually between a custom type and a plain JavaScript object) + +```ts +import { Optic, Schema } from "effect" + +// Define custom schema-based classes +class A extends Schema.Class("A")({ s: Schema.String }) {} +class B extends Schema.Class("B")({ a: A }) {} + +// Create an Iso that converts between B and a plain object +const iso = Optic.makeIso( + (s) => ({ a: { s: s.a.s } }), // forward transformation + (a) => new B({ a: new A({ s: a.a.s }) }) // backward transformation +) + +// Build an optic that drills down to the "s" field inside "a" +const _s = iso.key("a").key("s") + +console.log(_s.replace("b", new B({ a: new A({ s: "a" }) }))) +// B { a: A { s: 'b' } } +``` + +#### Solution + +Manually creating `Iso` instances is repetitive and error-prone. +To simplify this, the library provides a helper function that generates an `Iso` directly from a schema. + +This allows you to keep working with plain JavaScript objects and collections while still benefiting from schema definitions. + +**Example** (Generating an `Iso` automatically from a schema) + +```ts +import { Schema } from "effect" + +class A extends Schema.Class("A")({ s: Schema.String }) {} +class B extends Schema.Class("B")({ a: A }) {} + +// Automatically generate an Iso from the schema of B +// const iso: Iso +const iso = Schema.toIso(B) + +const _s = iso.key("a").key("s") + +console.log(_s.replace("b", new B({ a: new A({ s: "a" }) }))) +// B { a: A { s: 'b' } } +``` + +### Using the Differ Module for Type-Safe JSON Patches + +The `Differ` module lets you compute and apply JSON Patch (RFC 6902) changes for any value described by a `Schema`. You give it a schema once, then use the returned differ to produce a patch from an old value to a new value, and to apply that patch. + +**Example** (Compare two values and apply the patch) + +```ts +import { Schema } from "effect" + +// Describe the shape of your data +const schema = Schema.Struct({ + id: Schema.Number, + name: Schema.String, + price: Schema.Number +}) + +// Build a differ tied to the schema +const differ = Schema.toDifferJsonPatch(schema) + +// Prepare two values to compare +const oldValue = { id: 1, name: "a", price: 1 } +const newValue = { id: 1, name: "b", price: 2 } + +// Compute a JSON Patch document (an array of operations) +const jsonPatch = differ.diff(oldValue, newValue) +console.log(jsonPatch) +/* +[ + { op: 'replace', path: '/name', value: 'b' }, + { op: 'replace', path: '/price', value: 2 } +] +*/ + +// Apply the patch to the old value to get the new value +const patched = differ.patch(oldValue, jsonPatch) +console.log(patched) +// { id: 1, name: 'b', price: 2 } +``` + +#### Works with custom types too + +**Example** (Compare two custom types) + +```ts +import { Schema } from "effect" + +class A extends Schema.Class("A")({ n: Schema.Number }) {} +class B extends Schema.Class("B")({ a: A }) {} + +const differ = Schema.toDifferJsonPatch(B) + +const oldValue = new B({ a: new A({ n: 0 }) }) +const newValue = new B({ a: new A({ n: 1 }) }) + +const patch = differ.diff(oldValue, newValue) +console.log(patch) +// [ { op: 'replace', path: '/a/n', value: 1 } ] + +console.log(differ.patch(oldValue, patch)) +// B { a: A { n: 1 } } +``` + +#### How it works + +The idea is simple: if you have a `Schema` for a type `T`, you can serialize any `T` to JSON and back. That lets us compute and apply JSON Patch on the JSON view, while keeping the public API typed as `T`. + +- `diff(oldValue, newValue)` + + 1. Encode `oldValue: T` and `newValue: T` to JSON with the schema serializer. + 2. Compute a JSON Patch document between the two JSON values. + 3. Return that patch (an array of `"add" | "remove" | "replace"` operations). + +- `patch(oldValue, patch)` + 1. Encode `oldValue: T` to JSON. + 2. Apply the JSON Patch to the JSON value. + 3. Decode the patched JSON back to `T` using the schema. + +This approach keeps patches independent from TypeScript types and uses the schema as the guardrail when turning JSON back into `T`. + +# Schema Representation + +The `SchemaRepresentation` module converts a `Schema` into a portable data structure and back again. + +Use it when you need to: + +- store schemas on disk (for example in a cache) +- send schemas over the network +- rebuild runtime schemas later +- convert to JSON Schema (Draft 2020-12) +- generate TypeScript code that recreates schemas + +At a high level: + +- `fromAST` / `fromASTs` turn a schema AST into a `Document` / `MultiDocument` +- `DocumentFromJson` (schema) round-trip that document through JSON +- `toSchema` rebuilds a runtime `Schema` from the stored representation +- `toJsonSchemaDocument` produces a Draft 2020-12 JSON Schema document +- `toCodeDocument` prepares data for code generation (via `toMultiDocument`) + +```mermaid +flowchart TD + S[Schema] -->|fromAST|D{"SchemaRepresentation.Document"} + S -->|fromASTs|MD{"SchemaRepresentation.MultiDocument"} + JS["JSON Schema (draft-07, draft-2020-12, openapi-3.0, openapi-3.1)"] -->JSD + JD --> JS + JD["JsonSchema.Document"] -->|fromJsonSchemaDocument|D + D <--> |"DocumentFromJson (schema)"|JSON + D --> |toJsonSchemaDocument|JD + D --> |toSchema|S + MD --> |toCodeDocument|CodeDocument["CodeDocument"] + D --> |toMultiDocument|MD + MD --> |toJsonSchemaMultiDocument|JMD[JsonSchema.MultiDocument] + MD <--> |"MultiDocumentFromJson (schema)"|JSON +``` + +## The data model + +### `Representation` + +A `Representation` is a tagged object tree (`_tag` fields like `"String"`, `"Objects"`, `"Union"`, ...). It describes the _structure_ of a schema in a JSON-friendly way. + +Only a subset of schema features can be represented. See "Limitations" below. + +### `Document` + +A `Document` has: + +- `representation`: the root `Representation` +- `references`: a map of named definitions used by the root representation + +References let the representation share definitions and support recursion. + +### `MultiDocument` + +A `MultiDocument` stores multiple root representations that share the same `references` table. + +This is useful if you want to serialize a set of schemas together, or if you want to generate code for multiple schemas while emitting shared definitions only once. + +## Limitations + +`SchemaRepresentation` is meant for schemas that can be described without user code. + +That has a few consequences. + +### Transformations are not supported + +The representation format describes the schema's _shape_ and a set of known checks. It does not store transformation logic. + +Schemas that rely on transformations cannot be round-tripped, including: + +- `Schema.transform(...)` +- `Schema.encodeTo(...)` +- custom codecs or any schema that changes how values are encoded/decoded + +If you serialize a transformed schema, the transformation logic will be lost. When you rebuild it with `toSchema`, you will only get the structural schema. + +> **Aside** (Why transformations are excluded) +> +> A transformation is user code (functions). JSON cannot store functions, and serializing functions as strings would not be safe or portable. + +### Only built-in checks can be represented + +Checks are stored as `Filter` / `FilterGroup` nodes with a small `meta` object. + +Only checks that match the built-in meta definitions are supported, such as: + +- string checks: `isMinLength`, `isPattern`, `isUUID`, ... +- number checks: `isInt`, `isBetween`, `isMultipleOf`, ... +- bigint checks: `isGreaterThanBigInt`, ... +- array checks: `isLength`, `isUnique`, ... +- object checks: `isMinProperties`, ... +- date checks: `isBetweenDate`, ... + +Custom predicates (for example `Schema.filter((x) => ...)`) are not supported, because the representation has nowhere to store the function. + +### Annotations are filtered + +Annotations are stored as a record, but: + +- only values that look like JSON primitives (plus `bigint` and `symbol` in the in-memory form) are kept +- some annotation keys are dropped using an internal blacklist + +In practice, documentation annotations like `title` and `description` are preserved, while complex values (functions, instances, nested objects) are ignored. + +### Declarations need a reviver + +Some runtime schemas are represented as `Declaration` nodes. Rebuilding them requires a "reviver" function. + +`toSchema` ships with a default reviver (`toSchemaDefaultReviver`) that recognizes a fixed set of constructors, including: + +- `effect/Option`, `effect/Result`, `effect/Exit`, ... +- `ReadonlyMap`, `ReadonlySet` +- `RegExp`, `URL`, `Date` +- `FormData`, `URLSearchParams`, `Uint8Array` +- `DateTime.Utc`, `effect/Duration` + +If your document contains other declarations, pass a custom `reviver` to `toSchema`. + +## JSON round-tripping + +### `toJson` / `fromJson` + +- `toJson(document)` returns JSON-compatible data (safe to `JSON.stringify`) +- `fromJson(unknown)` validates and parses JSON data back into a `Document` + +Internally, these functions use a canonical JSON codec for `Document$`. This is why values like `bigint` in annotations are encoded as strings in the JSON form and restored on decode. + +## Rebuilding runtime schemas + +### `toSchema` + +`toSchema(document)` walks the representation tree and recreates a runtime schema. + +What it does: + +- rebuilds the structural schema nodes (`Struct`, `Tuple`, `Union`, ...) +- resolves references from `document.references` +- supports recursive references using `Schema.suspend` +- re-attaches stored annotations via `.annotate(...)` and `.annotateKey(...)` +- re-applies supported checks via `.check(...)` + +If you need custom handling for declarations: + +```ts +SchemaRepresentation.toSchema(document, { + reviver: (declaration, recur) => { + // Return a runtime schema to override how a Declaration is rebuilt. + // Return undefined to fall back to the default behavior. + return undefined + } +}) +``` + +## JSON Schema output + +### `toJsonSchemaDocument` / `toJsonSchemaMultiDocument` + +These functions convert a `Document` or `MultiDocument` into a Draft 2020-12 JSON Schema document. + +This is useful for tooling that expects JSON Schema, or for producing OpenAPI-compatible schema pieces (depending on your pipeline). + +## Code generation + +### `toCodeDocument` + +`toCodeDocument` converts a `MultiDocument` into a structure that is convenient for generating TypeScript source. + +It: + +- sorts references so non-recursive definitions can be emitted in dependency order +- keeps recursive definitions separate (they must be emitted using `Schema.suspend`) +- sanitizes reference names into valid JavaScript identifiers +- collects extra artifacts that must be emitted (enums, symbols, imports) + +You can customize: + +- `sanitizeReference` to control how `$ref` strings become identifiers +- `reviver` to generate custom code for `Declaration` nodes + +# Error Handling and Formatting + +When validation fails, Schema produces structured error objects that describe what went wrong. Formatters turn those error objects into human-readable messages you can display to users or write to logs. + +### Formatters + +#### StandardSchemaV1 formatter + +The StandardSchemaV1 formatter is used by `Schema.toStandardSchemaV1` and will return a `StandardSchemaV1.FailureResult` object: + +```ts +export interface FailureResult { + /** The issues of failed validation. */ + readonly issues: ReadonlyArray +} + +export interface Issue { + /** The error message of the issue. */ + readonly message: string + /** The path of the issue. */ + readonly path: ReadonlyArray +} +``` + +You can customize the messages of the `Issue` object in two main ways: + +- By passing formatter hooks +- By annotating schemas with `message` or `messageMissingKey` or `messageUnexpectedKey` + +For the exact rule used by the default formatter for identifiers, filter +`expected`, and `message` annotations, see +[Filter error messages and schema identifiers](#filter-error-messages-and-schema-identifiers). + +##### Hooks + +Formatter hooks let you define custom messages in one place and apply them across different schemas. This can help avoid repeating message definitions and makes it easier to update them later. + +Hooks are **required**. There is a default implementation that can be overridden only for demo purposes. This design helps keep the bundle size smaller by avoiding unused message formatting logic. + +There are two kinds of hooks: + +- `LeafHook` — for issues that occur at leaf nodes in the schema. +- `CheckHook` — for custom validation checks. + +`LeafHook` handles these issue types: + +- `InvalidType` +- `InvalidValue` +- `MissingKey` +- `UnexpectedKey` +- `Forbidden` +- `OneOf` + +`CheckHook` handles `Check` issues, such as failed filters / refinements. + +**Example** (Default hooks) + +Default hooks are just for demo purposes: + +- LeafHook: returns the issue tag +- CheckHook: returns the meta infos of the check as a string + +```ts +import { Effect, Schema, SchemaIssue } from "effect" + +const schema = Schema.Struct({ + a: Schema.NonEmptyString, + b: Schema.NonEmptyString +}) + +Schema.decodeUnknownEffect(schema)({ b: "" }, { errors: "all" }) + .pipe( + Effect.mapError((error) => SchemaIssue.makeFormatterStandardSchemaV1()(error.issue)), + Effect.runPromise + ) + .then(console.log, (a) => console.dir(a, { depth: null })) +/* +Output: +{ + issues: [ + { path: [ 'a' ], message: 'Missing key' }, + { path: [ 'b' ], message: 'Expected a value with a length of at least 1, got ""' } + ] +} +*/ +``` + +##### Customizing messages + +If a schema has a `message` annotation, it will take precedence over any formatter hook. + +To make the examples easier to follow, we define a helper function that prints formatted validation messages using `SchemaFormatter`. + +**Example utilities** + +```ts +// utils.ts +import { Exit, Schema, SchemaIssue } from "effect" +import i18next from "i18next" + +i18next.init({ + lng: "en", + resources: { + en: { + translation: { + "string.mismatch": "Please enter a valid string", + "string.minLength": "Please enter at least {{minLength}} character(s)", + "struct.missingKey": "This field is required", + "struct.mismatch": "Please enter a valid object", + "default.mismatch": "Invalid type", + "default.invalidValue": "Invalid value", + "default.forbidden": "Forbidden operation", + "default.oneOf": "Too many successful values", + "default.check": "The value does not match the check" + } + } + } +}) + +export const t = i18next.t + +export function getLogIssues(options?: { + readonly leafHook?: SchemaIssue.LeafHook | undefined + readonly checkHook?: SchemaIssue.CheckHook | undefined +}) { + return >(schema: S, input: unknown) => { + console.log( + String( + Schema.decodeUnknownExit(schema)(input, { errors: "all" }).pipe( + Exit.mapError((err) => SchemaIssue.makeFormatterStandardSchemaV1(options)(err.issue).issues) + ) + ) + ) + } +} +``` + +**Example** (Using hooks to translate common messages) + +```ts +import { Schema } from "effect" +import { getLogIssues, t } from "./utils.js" + +const Person = Schema.Struct({ + name: Schema.String.check(Schema.isNonEmpty()) +}) + +// Configure hooks to customize how issues are rendered +const logIssues = getLogIssues({ + // Format leaf-level issues (missing key, wrong type, etc.) + leafHook: (issue) => { + switch (issue._tag) { + case "InvalidType": { + if (issue.ast._tag === "String") { + return t("string.mismatch") // Wrong type for a string + } else if (issue.ast._tag === "Objects") { + return t("struct.mismatch") // Value is not an object + } + return t("default.mismatch") // Fallback for other types + } + case "InvalidValue": { + return t("default.invalidValue") + } + case "MissingKey": + return t("struct.missingKey") + case "UnexpectedKey": + return t("struct.unexpectedKey") + case "Forbidden": + return t("default.forbidden") + case "OneOf": + return t("default.oneOf") + } + }, + // Format custom check errors (like isMinLength or user-defined validations) + checkHook: (issue) => { + const meta = issue.filter.annotations?.meta + if (meta) { + switch (meta._tag) { + case "isMinLength": { + return t("string.minLength", { minLength: meta.minLength }) + } + } + } + return t("default.check") + } +}) + +// Invalid object (not even a struct) +logIssues(Person, null) +// Failure(Cause([Fail([{"path":[],"message":"Please enter a valid object"}])])) + +// Missing "name" key +logIssues(Person, {}) +// Failure(Cause([Fail([{"path":["name"],"message":"This field is required"}])])) + +// "name" has the wrong type +logIssues(Person, { name: 1 }) +// Failure(Cause([Fail([{"path":["name"],"message":"Please enter a valid string"}])])) + +// "name" is an empty string +logIssues(Person, { name: "" }) +// Failure(Cause([Fail([{"path":["name"],"message":"Please enter at least 1 character(s)"}])])) +``` + +##### Inline custom messages + +You can attach custom error messages directly to a schema using annotations. These messages can either be plain strings or functions that return strings. This is useful when you want to provide field-specific wording or localization without relying on formatter hooks. + +**Example** (Attaching custom messages to a struct field) + +```ts +import { Schema } from "effect" +import { getLogIssues, t } from "./utils.js" + +const Person = Schema.Struct({ + name: Schema.String + // Message for invalid type (e.g., number instead of string) + .annotate({ message: t("string.mismatch") }) + // Message to show when the key is missing + .annotateKey({ messageMissingKey: t("struct.missingKey") }) + // Message to show when the string is empty + .check(Schema.isNonEmpty({ message: t("string.minLength", { minLength: 1 }) })) +}) + // Message to show when the whole object has the wrong shape + .annotate({ message: t("struct.mismatch") }) + +// Use defaults for leaf and check hooks +const logIssues = getLogIssues() + +// Invalid object (not even a struct) +logIssues(Person, null) +// Failure(Cause([Fail([{"path":[],"message":"Please enter a valid object"}])])) + +// Missing "name" key +logIssues(Person, {}) +// Failure(Cause([Fail([{"path":["name"],"message":"This field is required"}])])) + +// "name" has the wrong type +logIssues(Person, { name: 1 }) +// Failure(Cause([Fail([{"path":["name"],"message":"Please enter a valid string"}])])) + +// "name" is an empty string +logIssues(Person, { name: "" }) +// Failure(Cause([Fail([{"path":["name"],"message":"Please enter at least 1 character(s)"}])])) +``` + +##### Sending a FailureResult over the wire + +You can use the `Schema.StandardSchemaV1FailureResult` schema to send a `StandardSchemaV1.FailureResult` over the wire. + +**Example** (Sending a FailureResult over the wire) + +```ts +import { Schema, SchemaIssue, SchemaParser } from "effect" + +const b = Symbol.for("b") + +const schema = Schema.Struct({ + a: Schema.NonEmptyString, + [b]: Schema.Finite, + c: Schema.Tuple([Schema.String]) +}) + +const r = SchemaParser.decodeUnknownExit(schema)({ a: "", c: [] }, { errors: "all" }) + +if (r._tag === "Failure") { + const failures = r.cause.failures + if (failures[0]?._tag === "Fail") { + const failureResult = SchemaIssue.makeFormatterStandardSchemaV1()(failures[0].error) + const serializer = Schema.toCodecJson(Schema.StandardSchemaV1FailureResult) + console.dir(Schema.encodeSync(serializer)(failureResult), { depth: null }) + } +} +/* +{ + issues: [ + { + message: 'Expected a value with a length of at least 1, got ""', + path: [ 'a' ] + }, + { message: 'Missing key', path: [ 'c', 0 ] }, + { message: 'Missing key', path: [ 'Symbol(b)' ] } + ] +} +*/ +``` + +# Middlewares + +A middleware wraps around the decoding or encoding process, letting you intercept errors, provide fallback values, or inject services. The most common use case is returning a default value when decoding fails. + +## Fallbacks + +You can use `Schema.catchDecoding` to return a fallback value when decoding fails. +This API uses an Effect without a context. If you need a fallback value that depends on a service, use `Schema.catchDecodingWithContext`. + +**Example** (Returning a simple fallback value) + +```ts +import { Effect, Schema } from "effect" + +// Provide a fallback string when decoding does not succeed +const schema = Schema.String.pipe(Schema.catchDecoding(() => Effect.succeedSome("b"))) + +console.log(String(Schema.decodeUnknownExit(schema)(null))) +// Success("b") +``` + +You can also return `Option.none()` to omit a field from the output. +This is useful when working with optional fields. + +**Example** (Omitting a field when decoding fails) + +```ts +import { Effect, Schema } from "effect" + +// Omit the field when decoding does not succeed +const schema = Schema.Struct({ + a: Schema.optionalKey(Schema.String).pipe(Schema.catchDecoding(() => Effect.succeedNone)) +}) + +console.log(String(Schema.decodeUnknownExit(schema)({ a: null }))) +// Success({}) +``` + +### Using a Service to provide a fallback value + +You can use `Schema.catchDecodingWithContext` to get a fallback value from a service. + +**Example** (Retrieving a fallback value from a service) + +```ts +import { Context, Effect, Option, Schema } from "effect" + +// Define a service that provides a fallback value +class Service extends Context.Service }>()("Service") {} + +// ┌─── Codec +// ▼ +const schema = Schema.revealCodec( + Schema.revealCodec( + Schema.String.pipe( + Schema.catchDecodingWithContext(() => + Effect.gen(function*() { + const service = yield* Service + return Option.some(yield* service.fallback) + }) + ) + ) + ) +) + +// Provide the service during decoding +// ┌─── Codec +// ▼ +const provided = Schema.revealCodec( + schema.pipe(Schema.middlewareDecoding(Effect.provideService(Service, { fallback: Effect.succeed("b") }))) +) + +console.log(String(Schema.decodeUnknownExit(provided)(null))) +// Success("b") +``` + +# Advanced Topics + +This section covers Schema's internal type machinery and advanced features. You don't need this to use Schema — it's here for library authors and advanced users who want to understand or extend the type system. + +## Model + +A "schema" is a strongly typed wrapper around an untyped AST (abstract syntax tree) node. + +The base interface is `Bottom`, which sits at the bottom of the schema type hierarchy. In Schema v4, the number of tracked type parameters has increased to 15, allowing for more precise and flexible schema definitions. + +```ts +export interface Bottom< + out T, + out E, + out RD, + out RE, + out Ast extends AST.AST, + out RebuildOut extends Top, + out TypeMakeIn = T, + out Iso = T, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMake = TypeMakeIn, + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends Pipeable.Pipeable { + readonly [TypeId]: typeof TypeId + + readonly ast: Ast + readonly "Rebuild": RebuildOut + readonly "~type.parameters": TypeParameters + + readonly Type: T + readonly Encoded: E + readonly DecodingServices: RD + readonly EncodingServices: RE + + readonly "~type.make.in": TypeMakeIn + readonly "~type.make": TypeMake // useful to type the `refine` interface + readonly "~type.constructor.default": TypeConstructorDefault + readonly Iso: Iso + + readonly "~type.mutability": TypeMutability + readonly "~type.optionality": TypeOptionality + readonly "~encoded.mutability": EncodedMutability + readonly "~encoded.optionality": EncodedOptionality + + annotate(annotations: Annotations.Bottom): this["Rebuild"] + annotateKey(annotations: Annotations.Key): this["Rebuild"] + check(...checks: readonly [AST.Check, ...Array>]): this["Rebuild"] + rebuild(ast: this["ast"]): this["Rebuild"] + /** + * @throws {Error} The issue is contained in the error cause. + */ + make(input: this["~type.make.in"], options?: MakeOptions): this["Type"] +} +``` + +### Parameter Overview + +- `T`: the decoded output type +- `E`: the encoded representation +- `RD`: the type of the services required for decoding +- `RE`: the type of the services required for encoding +- `Ast`: the AST node type +- `RebuildOut`: the type returned when modifying the schema (namely when you add annotations or checks) +- `TypeMakeIn`: the type of the input to the `make` constructor +- `Iso`: the type of the focus of the default `Optic.Iso` +- `TypeParameters`: the type of the type parameters + +Contextual information about the schema (when the schema is used in a composite schema such as a struct or a tuple): + +- `TypeMake`: the type used to construct the value +- `TypeReadonly`: whether the schema is readonly on the type side +- `TypeIsOptional`: whether the schema is optional on the type side +- `TypeDefault`: whether the constructor has a default value +- `EncodedIsReadonly`: whether the schema is readonly on the encoded side +- `EncodedIsOptional`: whether the schema is optional on the encoded side + +### AST Node Structure + +Every schema is based on an AST node with a consistent internal shape: + +```mermaid +classDiagram + class ASTNode { + + annotations + + checks + + encoding + + context + + ...specific node fields... + } +``` + +- `annotations`: metadata attached to the schema node +- `checks`: an array of validation rules +- `encoding`: a list of transformations that describe how to encode the value +- `context`: includes details used when the schema appears inside composite schemas such as structs or tuples (e.g., whether the field is optional or mutable) + +## Type Hierarchy + +The `Bottom` type is the foundation of the schema system. It carries all internal type parameters used by the library. + +Higher-level schema types build on this base by narrowing those parameters. Common derived types include: + +- `Top`: a generic schema with no fixed shape +- `Schema`: represents the TypeScript type `T` +- `Codec`: a schema that decodes `E` to `T` and encodes `T` to `E`, possibly requiring services `RD` and `RE` + +```mermaid +flowchart TD + T[Top] --> S["Schema[T]"] + S --> C["Codec[T, E, RD, RE]"] + S --> O["Optic[T, Iso]"] + C --> B["Bottom[T, E, RD, RE, Ast, RebuildOut, TypeMakeIn, Iso, TypeParameters, TypeMake, TypeMutability, TypeOptionality, TypeConstructorDefault, EncodedMutability, EncodedOptionality]"] +``` + +### Best Practices + +Use `Top`, `Schema`, and `Codec` as _constraints_ only. Do not use them as explicit annotations or return types. + +**Example** (Prefer constraints over wide annotations) + +```ts +import { Schema } from "effect" + +// ✅ Use as a constraint. S can be any schema that extends Top. +declare function foo(schema: S) + +// ❌ Do not return Codec directly. It erases useful type information. +declare function bar(): Schema.Codec + +// ❌ Avoid wide annotations that lose details baked into a specific schema. +const schema: Schema.Codec = Schema.FiniteFromString +``` + +These wide types reset other internal parameters to defaults, which removes useful information: + +- `Top`: all type parameters are set to defaults +- `Schema`: all type parameters except `Type` are set to defaults +- `Codec`: all type parameters except `Type`, `Encoded`, `DecodingServices`, `EncodingServices` are set to defaults + +**Example** (How wide annotations erase information) + +```ts +import { Schema } from "effect" + +// Read a hidden type-level property from a concrete schema +type TypeMutability = (typeof Schema.FiniteFromString)["~type.mutability"] // "readonly" + +const schema: Schema.Codec = Schema.FiniteFromString + +// After widening to Codec<...>, the mutability info is broadened +type TypeMutability2 = (typeof schema)["~type.mutability"] // "readonly" | "mutable" +``` + +## Typed Annotations + +You can retrieve typed annotations with the `Schema.resolveAnnotations` function. The function is called "resolve" rather than "get" because it performs a lookup: if the schema has checks, the annotations are taken from the last check; otherwise they are taken from the base schema instance. This means annotations placed on a check (e.g. via `.check(myCheck.annotate({ ... }))`) take precedence over annotations on the schema itself. + +**Example** (Resolving annotations from a base schema) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.annotate({ title: "my string" }) + +console.log(Schema.resolveAnnotations(schema)) +// Output: { title: "my string" } +``` + +**Example** (Annotations on the last check take precedence) + +```ts +import { Schema } from "effect" + +const schema = Schema.String + .annotate({ title: "base" }) + .check(Schema.isNonEmpty().annotate({ title: "from check" })) + +console.log(Schema.resolveAnnotations(schema)?.title) +// Output: "from check" +``` + +You can also extend the available annotations by adding your own in a module declaration file. + +**Example** (Adding a custom annotation for versioning) + +```ts +import { Schema } from "effect" + +// Extend the Annotations interface with a custom `version` annotation +declare module "effect/Schema" { + namespace Annotations { + interface Augment { + readonly version?: readonly [major: number, minor: number, patch: number] | undefined + } + } +} + +// The `version` annotation is now recognized by the TypeScript compiler +const schema = Schema.String.annotate({ version: [1, 2, 0] }) + +// const version: readonly [major: number, minor: number, patch: number] | undefined +const version = Schema.resolveAnnotations(schema)?.["version"] + +if (version) { + // Access individual parts of the version + console.log(version[1]) + // Output: 2 +} +``` + +### Key-level Annotations + +Key-level annotations are attached via `annotateKey` and apply to a field's position inside a `Struct` or `Tuple` rather than to the field's value type. Use `Schema.resolveAnnotationsKey` to retrieve them. + +**Example** (Resolving key-level annotations) + +```ts +import { Schema } from "effect" + +const schema = Schema.String.annotateKey({ messageMissingKey: "required" }) + +console.log(Schema.resolveAnnotationsKey(schema)) +// Output: { messageMissingKey: "required" } +``` + +## Generics Improvements + +Using generics in schema composition and filters can be difficult. + +The plan is to make generics **covariant** and easier to use. + +## Separate Requirement Type Parameters + +In real-world applications, decoding and encoding often have different dependencies. For example, decoding may require access to a database, while encoding does not. + +To support this, schemas now have two separate requirement parameters: + +```ts +interface Codec { + // ... +} +``` + +- `RD`: services required **only for decoding** +- `RE`: services required **only for encoding** + +This makes it easier to work with schemas in contexts where one direction has no external dependencies. + +**Example** (Decoding requirements are ignored during encoding) + +```ts +import type { Effect } from "effect" +import { Context, Schema } from "effect" + +// A service that retrieves full user info from an ID +class UserDatabase extends Context.Service< + UserDatabase, + { + getUserById: (id: string) => Effect.Effect<{ readonly id: string; readonly name: string }> + } +>()("UserDatabase") {} + +// Schema that decodes from an ID to a user object using the database, +// but encodes just the ID +declare const User: Schema.Codec< + { id: string; name: string }, + string, + UserDatabase, // Decoding requires the database + never // Encoding does not require any services +> + +// ┌─── Effect<{ readonly id: string; readonly name: string; }, Schema.SchemaError, UserDatabase> +// ▼ +const decoding = Schema.decodeEffect(User)("user-123") + +// ┌─── Effect +// ▼ +const encoding = Schema.encodeEffect(User)({ id: "user-123", name: "John Doe" }) +``` + +# Integrations + +Schema integrates with popular frameworks and libraries. This section shows working examples for forms (TanStack Form) and web servers (Elysia). + +### Forms + +#### TanStack Form + +Features: + +- Errors are formatted with the `StandardSchemaV1` formatter. +- Fields are validated **and parsed** (not just strings). +- You can add form-level validation by attaching filters to the struct. +- Schemas may include async transformations. + +**Example** (Parse user input and surface form-level errors) + +```tsx +import { useForm } from "@tanstack/react-form" +import type { AnyFieldApi } from "@tanstack/react-form" +import { Effect, Schema, SchemaGetter, SchemaTransformation } from "effect" +import React from "react" + +// ---------------------------------------------------- +// Toolkit +// ---------------------------------------------------- + +// Treat an empty string from the UI as `undefined` for optional fields, +// and encode `undefined` back to an empty string when showing it. +const UndefinedFromEmptyString = Schema.Undefined.pipe( + Schema.encodeTo(Schema.Literal(""), { + decode: SchemaGetter.transform(() => undefined), + encode: SchemaGetter.transform(() => "" as const) + }) +) + +// Helper to make any schema "UI-optional": +// - empty string -> undefined +// - otherwise validate/parse with the given schema +function optional(schema: S) { + return Schema.Union([UndefinedFromEmptyString, schema]) +} + +// Decode helper that returns a `Promise` with either a typed value +// or a human-friendly error message string. +function decode(schema: Schema.Codec) { + return function(value: unknown) { + return Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError((error) => error.message), + Effect.result, + Effect.runPromise + ) + } +} + +// ---------------------------------------------------- +// Schemas +// ---------------------------------------------------- + +const FirstName = Schema.String.check( + Schema.isMinLength(3, { + message: "must be at least 3 characters" + }) +) +const Age = Schema.Number.check( + Schema.isInt({ message: "must be an integer" }).abort(), + Schema.isBetween( + { minimum: 18, maximum: 100 }, + { + message: "must be between 18 and 100" + } + ) +).pipe(Schema.encodeTo(Schema.String, SchemaTransformation.numberFromString)) + +// Whole-form schema with a form-level rule: +// If firstName is "John", age is required. +const schema = Schema.Struct({ + firstName: FirstName, + age: optional(Age) +}).check( + Schema.makeFilter(({ firstName, age }) => { + if (firstName === "John" && age === undefined) return "Age is required for John" + }) +) + +function FieldInfo({ field }: { field: AnyFieldApi }) { + return ( + <> + {field.state.meta.isTouched && !field.state.meta.isValid ? + {field.state.meta.errors.map((error) => error.message).join(", ")} : + null} + {field.state.meta.isValidating ? "Validating..." : null} + + ) +} + +export default function App() { + // We parse the whole form on submit and keep the typed value here + const parsedRef = React.useRef(undefined) + + const form = useForm({ + defaultValues: { + firstName: "John", + age: "" + } satisfies (typeof schema)["Encoded"], + validators: { + onChangeAsync: Schema.toStandardSchemaV1(schema), + + // Final guard before submit: + // - decode the entire form + // - on failure: return a string (form-level error) + // - on success: stash the typed value for `onSubmit` + onSubmitAsync: async ({ value }) => { + const r = await decode(schema)(value) + if (r._tag === "Failure") return r.failure + parsedRef.current = r.success + } + }, + + // Submit runs only if validators pass. + // At this point `parsedRef.current` holds the fully typed value. + onSubmit: async () => { + // get the parsed value from the ref + const parsed = parsedRef.current + if (!parsed) throw new Error("Unexpected submit without parsed data") + // Use the typed data here (no post-processing needed) + console.log(parsed) + } + }) + + return ( +
+

Simple Form Example

+
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > +
+ + {(field) => { + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} + +
+ +
+ + {(field) => { + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} + +
+ + [s.errorMap]}> + {([errorMap]) => + errorMap.onSubmit ? + ( +
+ {String(errorMap.onSubmit)} +
+ ) : + null} +
+ + [state.canSubmit, state.isSubmitting]}> + {([canSubmit, isSubmitting]) => ( + + )} + +
+
+ ) +} +``` + +### Integrations + +#### Elysia + +```ts +import { node } from "@elysiajs/node" +import { openapi } from "@elysiajs/openapi" +import { Schema } from "effect" +import { Elysia } from "elysia" + +// ---------------------------------------------------- +// Utilities +// ---------------------------------------------------- + +function encodingJsonSchema(schema: Schema.Codec) { + return Schema.toStandardSchemaV1( + Schema.flip(Schema.toCodecJson(schema)).annotate({ + direction: "encoding" + }) + ) +} + +function decodingJsonSchema(schema: Schema.Codec) { + return Schema.toStandardSchemaV1(Schema.toCodecJson(schema)) +} + +function decodingStringSchema(schema: Schema.Codec) { + return Schema.toStandardSchemaV1(Schema.toCodecStringTree(schema)) +} + +function mapJsonSchema(schema: Schema.Top) { + return Schema.toJsonSchema(schema.ast.annotations?.direction === "encoding" ? Schema.flip(schema) : schema, { + target: "draft-2020-12", // or "draft-07" + referenceStrategy: "skip" + }).schema +} + +// ---------------------------------------------------- +// Application +// ---------------------------------------------------- + +new Elysia({ adapter: node() }) + .use( + openapi({ + mapJsonSchema: { + effect: mapJsonSchema + } + }) + ) + .get( + "/id/:id", + async ({ status, params, query }) => { + console.log(`params: ${JSON.stringify(params)}`) + console.log(`query: ${JSON.stringify(query)}`) + return status(200, { date: new Date() }) + }, + { + params: decodingStringSchema( + Schema.Struct({ + id: Schema.Int + }) + ), + query: decodingStringSchema( + Schema.Struct({ + required: Schema.String, + optional: Schema.optionalKey(Schema.String), + array: Schema.Array(Schema.String), + tuple: Schema.Tuple([Schema.String, Schema.Int]) + }) + ), + response: { + 200: encodingJsonSchema( + Schema.Struct({ + date: Schema.ValidDate + }) + ) + } + } + ) + .post( + "/body", + ({ body }) => { + console.log(body) + return { bigint: body.bigint + 1n } + }, + { + body: decodingJsonSchema( + Schema.Struct({ + bigint: Schema.BigInt + }) + ), + response: { + 200: encodingJsonSchema( + Schema.Struct({ + bigint: Schema.BigInt + }) + ) + } + } + ) + .listen(3000) +``` diff --git a/repos/effect/packages/effect/benchmark/httpapi/sseClientDecoder.ts b/repos/effect/packages/effect/benchmark/httpapi/sseClientDecoder.ts new file mode 100644 index 0000000..0b41490 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/httpapi/sseClientDecoder.ts @@ -0,0 +1,52 @@ +import { Effect, Schema, Stream } from "effect" +import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { Bench } from "tinybench" + +const Event = Schema.Struct({ + event: Schema.Literal("tick"), + data: Schema.String +}) + +const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("events", "/events", { + success: HttpApiSchema.StreamSse({ + events: Event, + error: Schema.String + }) + }) + ) +) + +const httpClient = HttpClient.make((request) => + Effect.sync(() => + HttpClientResponse.fromWeb( + request, + new Response("event: tick\ndata: payload\n\n", { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) + ) +) + +const client = await Effect.runPromise(HttpApiClient.makeWith(Api, { + baseUrl: "http://localhost", + httpClient +})) +const consumeResponse = Effect.flatMap(client.test.events({}), Stream.runDrain) +const responsesPerIteration = 25 +const responses = Array.from({ length: responsesPerIteration }) +const runIteration = Effect.forEach(responses, () => consumeResponse, { + concurrency: 1, + discard: true +}) + +const bench = new Bench({ time: 3000 }) + +bench.add(`${responsesPerIteration} sequential SSE responses through one client`, () => Effect.runPromise(runIteration)) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/Enums.ts b/repos/effect/packages/effect/benchmark/schema/Enums.ts new file mode 100644 index 0000000..fad7abe --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/Enums.ts @@ -0,0 +1,40 @@ +import { Schema } from "effect" +import { Bench } from "tinybench" + +/* +┌─────────┬───────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼───────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'good' │ '79.26 ± 0.73%' │ '83.00 ± 1.00' │ '14443802 ± 0.02%' │ '12048193 ± 143431' │ 12616336 │ +│ 1 │ 'bad' │ '131.52 ± 0.92%' │ '125.00 ± 0.00' │ '7976946 ± 0.01%' │ '8000000 ± 0' │ 7603261 │ +└─────────┴───────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +const bench = new Bench() + +enum Enum { + A = "a", + B = "b" +} + +const schema = Schema.Enum(Enum) + +const good = "b" +const bad = "c" + +const decodeUnknownExit = Schema.decodeUnknownExit(schema) + +// console.log(decodeUnknownExit(valid)) +// console.log(decodeUnknownExit(invalid)) + +bench + .add("good", function() { + decodeUnknownExit(good) + }) + .add("bad", function() { + decodeUnknownExit(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/Optic.ts b/repos/effect/packages/effect/benchmark/schema/Optic.ts new file mode 100644 index 0000000..0581d5f --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/Optic.ts @@ -0,0 +1,79 @@ +import { Optic, Schema } from "effect" +import { Bench } from "tinybench" + +/* +┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'iso get' │ '907.53 ± 1.06%' │ '834.00 ± 1.00' │ '1159005 ± 0.02%' │ '1199041 ± 1439' │ 1101891 │ +│ 1 │ 'optic get' │ '32.79 ± 0.20%' │ '42.00 ± 1.00' │ '25353263 ± 0.00%' │ '23809524 ± 580720' │ 30500447 │ +│ 2 │ 'direct get' │ '23.12 ± 0.48%' │ '41.00 ± 1.00' │ '32734753 ± 0.01%' │ '24390244 ± 580720' │ 43255789 │ +│ 3 │ 'iso replace' │ '2693.0 ± 2.87%' │ '2459.0 ± 41.00' │ '396398 ± 0.03%' │ '406669 ± 6669' │ 371349 │ +│ 4 │ 'direct replace' │ '848.59 ± 0.45%' │ '792.00 ± 1.00' │ '1244301 ± 0.02%' │ '1262626 ± 1596' │ 1178430 │ +└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +const bench = new Bench() + +// Define a class with nested properties +class User extends Schema.Class("User")({ + id: Schema.Number, + profile: Schema.Struct({ + name: Schema.String, + email: Schema.String, + address: Schema.Struct({ + street: Schema.String, + city: Schema.String, + country: Schema.String + }) + }) +}) {} + +// Create a user instance +const user = User.make({ + id: 1, + profile: { + name: "John Doe", + email: "john@example.com", + address: { + street: "123 Main St", + city: "New York", + country: "USA" + } + } +}) + +const iso = Schema.toIso(User).key("profile").key("address").key("street") +const optic = Optic.id().key("profile").key("address").key("street") + +bench + .add("iso get", function() { + iso.get(user) + }) + .add("optic get", function() { + optic.get(user) + }) + .add("direct get", function() { + // oxlint-disable-next-line no-unused-expressions + user.profile.address.street + }) + .add("iso replace", function() { + iso.replace("Updated", user) + }) + .add("direct replace", function() { + // oxlint-disable-next-line no-new + new User({ + ...user, + profile: { + ...user.profile, + address: { + ...user.profile.address, + street: "Updated" + } + } + }) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/array.ts b/repos/effect/packages/effect/benchmark/schema/array.ts new file mode 100644 index 0000000..b4596fa --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/array.ts @@ -0,0 +1,74 @@ +import { type } from "arktype" +import { Schema } from "effect" +import { Bench } from "tinybench" +import * as v from "valibot" +import { z } from "zod/v4-mini" + +/* +┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'Schema (good)' │ '218.06 ± 1.54%' │ '208.00 ± 0.00' │ '4904782 ± 0.01%' │ '4807692 ± 0' │ 4585950 │ +│ 1 │ 'Schema (bad)' │ '362.29 ± 3.26%' │ '292.00 ± 1.00' │ '3199501 ± 0.01%' │ '3424658 ± 11769' │ 2760191 │ +│ 2 │ 'Valibot (good)' │ '67.50 ± 3.42%' │ '42.00 ± 1.00' │ '18944492 ± 0.02%' │ '23809524 ± 580720' │ 14872899 │ +│ 3 │ 'Valibot (bad)' │ '129.11 ± 0.74%' │ '125.00 ± 0.00' │ '8244804 ± 0.01%' │ '8000000 ± 0' │ 7745459 │ +│ 4 │ 'Arktype (good)' │ '25.76 ± 6.47%' │ '41.00 ± 1.00' │ '30181117 ± 0.01%' │ '24390244 ± 580720' │ 38824544 │ +│ 5 │ 'Arktype (bad)' │ '1837.1 ± 2.51%' │ '1750.0 ± 41.00' │ '567189 ± 0.02%' │ '571429 ± 13393' │ 544325 │ +│ 6 │ 'Zod (good)' │ '43.74 ± 4.91%' │ '42.00 ± 0.00' │ '23500345 ± 0.00%' │ '23809524 ± 0' │ 22863784 │ +│ 7 │ 'Zod (bad)' │ '5205.5 ± 0.76%' │ '4958.0 ± 83.00' │ '199317 ± 0.04%' │ '201694 ± 3360' │ 192106 │ +└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +const bench = new Bench() + +const schema = Schema.Array(Schema.String) + +const valibot = v.array(v.string()) + +const arktype = type("string[]") + +const zod = z.array(z.string()) + +const good = ["a", "b"] +const bad = ["a", 1] + +const decodeUnknownExit = Schema.decodeUnknownExit(schema) + +// console.log(decodeUnknownExit(good)) +// console.log(decodeUnknownExit(bad)) +// console.log(v.safeParse(valibot, good)) +// console.log(v.safeParse(valibot, bad)) +// console.log(arktype(good)) +// console.log(arktype(bad)) +// console.log(zod.safeParse(good)) +// console.log(zod.safeParse(bad)) + +bench + .add("Schema (good)", function() { + decodeUnknownExit(good) + }) + .add("Schema (bad)", function() { + decodeUnknownExit(bad) + }) + .add("Valibot (good)", function() { + v.safeParse(valibot, good) + }) + .add("Valibot (bad)", function() { + v.safeParse(valibot, bad) + }) + .add("Arktype (good)", function() { + arktype(good) + }) + .add("Arktype (bad)", function() { + arktype(bad) + }) + .add("Zod (good)", function() { + zod.safeParse(good) + }) + .add("Zod (bad)", function() { + zod.safeParse(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/filter.ts b/repos/effect/packages/effect/benchmark/schema/filter.ts new file mode 100644 index 0000000..6ff05e8 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/filter.ts @@ -0,0 +1,74 @@ +import { type } from "arktype" +import { Schema } from "effect" +import { Bench } from "tinybench" +import * as v from "valibot" +import { z } from "zod/v4-mini" + +/* +┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'Schema (good)' │ '129.33 ± 0.60%' │ '125.00 ± 0.00' │ '8158156 ± 0.01%' │ '8000000 ± 0' │ 7732112 │ +│ 1 │ 'Schema (bad)' │ '239.58 ± 1.44%' │ '209.00 ± 1.00' │ '4413764 ± 0.01%' │ '4784689 ± 23003' │ 4174352 │ +│ 2 │ 'Valibot (good)' │ '49.81 ± 1.01%' │ '42.00 ± 0.00' │ '22751446 ± 0.01%' │ '23809524 ± 1' │ 20075175 │ +│ 3 │ 'Valibot (bad)' │ '70.52 ± 1.11%' │ '83.00 ± 1.00' │ '16762925 ± 0.02%' │ '12048193 ± 143431' │ 14179523 │ +│ 4 │ 'Arktype (good)' │ '23.37 ± 0.14%' │ '41.00 ± 1.00' │ '32327042 ± 0.01%' │ '24390244 ± 580720' │ 42787691 │ +│ 5 │ 'Arktype (bad)' │ '1361.2 ± 2.65%' │ '1333.0 ± 41.00' │ '757323 ± 0.01%' │ '750188 ± 23806' │ 734659 │ +│ 6 │ 'Zod (good)' │ '44.10 ± 0.72%' │ '42.00 ± 0.00' │ '23479006 ± 0.00%' │ '23809524 ± 0' │ 22674716 │ +│ 7 │ 'Zod (bad)' │ '5005.9 ± 1.50%' │ '4834.0 ± 83.00' │ '204249 ± 0.03%' │ '206868 ± 3492' │ 199767 │ +└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +const bench = new Bench() + +const schema = Schema.String.check(Schema.isNonEmpty()) + +const valibot = v.pipe(v.string(), v.nonEmpty()) + +const arktype = type("string > 0") + +const zod = z.string().check(z.minLength(1)) + +const good = "a" +const bad = "" + +const decodeUnknownExit = Schema.decodeUnknownExit(schema) + +// console.log(decodeUnknownExit(good)) +// console.log(decodeUnknownExit(bad)) +// console.log(v.safeParse(valibot, good)) +// console.log(v.safeParse(valibot, bad)) +// console.log(arktype(good)) +// console.log(arktype(bad)) +// console.log(zod.safeParse(good)) +// console.log(zod.safeParse(bad)) + +bench + .add("Schema (good)", function() { + decodeUnknownExit(good) + }) + .add("Schema (bad)", function() { + decodeUnknownExit(bad) + }) + .add("Valibot (good)", function() { + v.safeParse(valibot, good) + }) + .add("Valibot (bad)", function() { + v.safeParse(valibot, bad) + }) + .add("Arktype (good)", function() { + arktype(good) + }) + .add("Arktype (bad)", function() { + arktype(bad) + }) + .add("Zod (good)", function() { + zod.safeParse(good) + }) + .add("Zod (bad)", function() { + zod.safeParse(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/object.ts b/repos/effect/packages/effect/benchmark/schema/object.ts new file mode 100644 index 0000000..7e164da --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/object.ts @@ -0,0 +1,82 @@ +import { type } from "arktype" +import { Schema, SchemaParser } from "effect" +import { Bench } from "tinybench" +import * as v from "valibot" +import { z } from "zod/v4-mini" + +/* +┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'Schema (good)' │ '124.63 ± 0.14%' │ '125.00 ± 0.00' │ '8434862 ± 0.01%' │ '8000000 ± 0' │ 8023546 │ +│ 1 │ 'Schema (bad)' │ '203.36 ± 6.27%' │ '208.00 ± 41.00' │ '5319764 ± 0.01%' │ '4807692 ± 807692' │ 4917416 │ +│ 2 │ 'Valibot (good)' │ '48.91 ± 0.14%' │ '42.00 ± 0.00' │ '22152363 ± 0.01%' │ '23809524 ± 1' │ 20444356 │ +│ 3 │ 'Valibot (bad)' │ '101.98 ± 0.88%' │ '84.00 ± 1.00' │ '10439854 ± 0.01%' │ '11904762 ± 143431' │ 9806230 │ +│ 4 │ 'Arktype (good)' │ '14.57 ± 1.46%' │ '0.00 ± 0.00' │ '53567410 ± 0.01%' │ '68616078 ± 0' │ 68616080 │ +│ 5 │ 'Arktype (bad)' │ '2001.6 ± 7.05%' │ '1750.0 ± 41.00' │ '554199 ± 0.03%' │ '571429 ± 13393' │ 499602 │ +│ 6 │ 'Zod (good)' │ '33.75 ± 6.13%' │ '41.00 ± 1.00' │ '25375235 ± 0.00%' │ '24390240 ± 580716' │ 29633666 │ +│ 7 │ 'Zod (bad)' │ '5392.2 ± 3.11%' │ '5167.0 ± 42.00' │ '190866 ± 0.03%' │ '193536 ± 1586' │ 185454 │ +└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ +*/ + +const bench = new Bench() + +const schema = Schema.Struct({ + a: Schema.String +}) + +const valibot = v.object({ + a: v.string() +}) + +const arktype = type({ + a: "string" +}) + +const zod = z.object({ + a: z.string() +}) + +const good = { a: "a" } +const bad = { a: 1 } + +const decodeUnknownExit = SchemaParser.decodeUnknownExit(schema) + +// console.log(decodeUnknownExit(good)) +// console.log(decodeUnknownExit(bad)) +// console.log(v.safeParse(valibot, good)) +// console.log(v.safeParse(valibot, bad)) +// console.log(arktype(good)) +// console.log(arktype(bad)) +// console.log(zod.safeParse(good)) +// console.log(zod.safeParse(bad)) + +bench + .add("Schema (good)", function() { + decodeUnknownExit(good) + }) + .add("Schema (bad)", function() { + decodeUnknownExit(bad) + }) + .add("Valibot (good)", function() { + v.safeParse(valibot, good) + }) + .add("Valibot (bad)", function() { + v.safeParse(valibot, bad) + }) + .add("Arktype (good)", function() { + arktype(good) + }) + .add("Arktype (bad)", function() { + arktype(bad) + }) + .add("Zod (good)", function() { + zod.safeParse(good) + }) + .add("Zod (bad)", function() { + zod.safeParse(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/tagged-union.ts b/repos/effect/packages/effect/benchmark/schema/tagged-union.ts new file mode 100644 index 0000000..2250ea4 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/tagged-union.ts @@ -0,0 +1,69 @@ +import { Array as RA, Schema } from "effect" +import { Bench } from "tinybench" + +/* +┌─────────┬────────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬─────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼────────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼─────────┤ +│ 0 │ 'Schema (good)' │ '488.43 ± 0.48%' │ '459.00 ± 1.00' │ '2119495 ± 0.01%' │ '2178649 ± 4757' │ 2047396 │ +│ 1 │ 'Schema (bad)' │ '629.17 ± 0.29%' │ '584.00 ± 1.00' │ '1645689 ± 0.01%' │ '1712329 ± 2937' │ 1589395 │ +│ 2 │ 'candidate (good)' │ '327.20 ± 0.27%' │ '292.00 ± 1.00' │ '3205291 ± 0.01%' │ '3424658 ± 11769' │ 3056264 │ +│ 3 │ 'candidate (bad)' │ '449.52 ± 2.41%' │ '417.00 ± 0.00' │ '2372897 ± 0.01%' │ '2398082 ± 0' │ 2224610 │ +└─────────┴────────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴─────────┘ +*/ + +const bench = new Bench({ time: 1000 }) + +const n = 100 +const f = (i: number) => + Schema.Struct({ + kind: Schema.Literal(i), + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean + }) +const members = RA.makeBy(n, f) + +const schema = Schema.Union(members) + +const candidate = f(n - 1) + +const good = { + kind: n - 1, + a: "a", + b: 1, + c: true +} + +const bad = { + kind: n - 1, + a: "a", + b: 1, + c: "c" +} + +const decodeUnknownExit = Schema.decodeUnknownExit(schema) +const decodeUnknownExitCandidate = Schema.decodeUnknownExit(candidate) + +// console.log(decodeUnknownExit(good)) +// console.log(decodeUnknownExit(bad)) +// console.log(decodeUnknownExitCandidate(good)) +// console.log(decodeUnknownExitCandidate(bad)) + +bench + .add("Schema (good)", function() { + decodeUnknownExit(good) + }) + .add("Schema (bad)", function() { + decodeUnknownExit(bad) + }) + .add("candidate (good)", function() { + decodeUnknownExitCandidate(good) + }) + .add("candidate (bad)", function() { + decodeUnknownExitCandidate(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/schema/transformation.ts b/repos/effect/packages/effect/benchmark/schema/transformation.ts new file mode 100644 index 0000000..40cae62 --- /dev/null +++ b/repos/effect/packages/effect/benchmark/schema/transformation.ts @@ -0,0 +1,91 @@ +import { Schema, SchemaTransformation } from "effect" +import { Bench } from "tinybench" +import { z } from "zod" + +/* +┌─────────┬─────────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼─────────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤ +│ 0 │ 'Schema (good)' │ '1097.8 ± 1.12%' │ '1042.0 ± 1.00' │ '949296 ± 0.01%' │ '959693 ± 922' │ 910953 │ +│ 1 │ 'Zod (good)' │ '267.92 ± 4.46%' │ '208.00 ± 0.00' │ '4505289 ± 0.02%' │ '4807692 ± 0' │ 3732515 │ +│ 2 │ 'Schema (bad)' │ '683.49 ± 1.54%' │ '625.00 ± 0.00' │ '1593775 ± 0.01%' │ '1600000 ± 0' │ 1463090 │ +│ 3 │ 'Zod (bad)' │ '8172.9 ± 4.43%' │ '6417.0 ± 125.00' │ '152563 ± 0.07%' │ '155836 ± 3096' │ 122357 │ +└─────────┴─────────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘ +*/ + +const bench = new Bench() + +const schema = Schema.Struct({ + a: Schema.String, + id: Schema.String, + c: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), + d: Schema.String +}).pipe(Schema.decodeTo( + Schema.Struct({ + a: Schema.String, + b: Schema.Struct({ id: Schema.String }), + c: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), + d: Schema.String + }), + SchemaTransformation.transform({ + decode: ({ id, ...v }) => ({ ...v, b: { id } }), + encode: ({ b: { id }, ...v }) => ({ ...v, id }) + }) +)) + +const zod = z.codec( + z.object({ + a: z.string(), + id: z.string(), + c: z.number().check(z.nonnegative()), + d: z.string() + }), + z.object({ + a: z.string(), + b: z.object({ id: z.string() }), + c: z.number().check(z.nonnegative()), + d: z.string() + }), + { + decode: ({ id, ...v }) => ({ ...v, b: { id } }), + encode: ({ b: { id }, ...v }) => ({ ...v, id }) + } +) + +const good = { + a: "a", + id: "id", + c: 1, + d: "d" +} +const bad = { + a: "a", + id: "id", + c: -1, + d: "d" +} + +const decodeUnknownExit = Schema.decodeUnknownExit(schema) + +// console.log(decodeUnknownExit(good)) +// console.log(String(decodeUnknownExit(bad))) +// console.log(zod.safeDecode(good)) +// console.log(zod.safeDecode(bad)) + +bench + .add("Schema (good)", function() { + decodeUnknownExit(good) + }) + .add("Zod (good)", function() { + zod.safeDecode(good) + }) + .add("Schema (bad)", function() { + decodeUnknownExit(bad) + }) + .add("Zod (bad)", function() { + zod.safeDecode(bad) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/benchmark/stream/splitLines.ts b/repos/effect/packages/effect/benchmark/stream/splitLines.ts new file mode 100644 index 0000000..c85569e --- /dev/null +++ b/repos/effect/packages/effect/benchmark/stream/splitLines.ts @@ -0,0 +1,46 @@ +import { Effect, Stream } from "effect" +import { Bench } from "tinybench" + +const bench = new Bench() + +// ~100 short lines, all in one chunk +const singleChunk = Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n") + "\n" + +// same content split into many small chunks (simulates streaming I/O) +const manyChunks: Array = [] +for (let i = 0; i < singleChunk.length; i += 8) { + manyChunks.push(singleChunk.substring(i, i + 8)) +} + +// mixed line endings +const mixedEndings = "alpha\r\nbravo\rcharlie\ndelta\r\necho\rfoxtrot\n" +const mixedChunks: Array = [] +for (let i = 0; i < mixedEndings.length; i += 5) { + mixedChunks.push(mixedEndings.substring(i, i + 5)) +} + +const run = (chunks: Array) => + Effect.runPromise( + Stream.fromIterable(chunks).pipe( + Stream.splitLines, + Stream.runCollect + ) + ) + +bench + .add("single chunk (100 lines)", async function() { + await run([singleChunk]) + }) + .add("many small chunks (100 lines)", async function() { + await run(manyChunks) + }) + .add("mixed line endings (single chunk)", async function() { + await run([mixedEndings]) + }) + .add("mixed line endings (small chunks)", async function() { + await run(mixedChunks) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/repos/effect/packages/effect/docgen.json b/repos/effect/packages/effect/docgen.json new file mode 100644 index 0000000..b47475e --- /dev/null +++ b/repos/effect/packages/effect/docgen.json @@ -0,0 +1,37 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "exclude": ["src/internal/**/*.ts", "src/unstable/**/internal/**/*.ts", "src/schema/StandardSchema.ts"], + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/effect/src/", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"] + }, + "plugins": [ + { + "name": "@effect/language-service", + "includeSuggestionsInTsc": false, + "diagnosticSeverity": { + "unusedDirective": "off", + "floatingEffect": "off", + "multipleEffectProvide": "off", + "globalErrorInEffectFailure": "off", + "unknownInEffectCatch": "off", + "globalErrorInEffectCatch": "off", + "missingReturnYieldStar": "off" + } + } + ] + } +} diff --git a/repos/effect/packages/effect/package.json b/repos/effect/packages/effect/package.json new file mode 100644 index 0000000..84c5213 --- /dev/null +++ b/repos/effect/packages/effect/package.json @@ -0,0 +1,126 @@ +{ + "name": "effect", + "type": "module", + "version": "4.0.0-beta.99", + "license": "MIT", + "description": "The missing standard library for TypeScript, for writing production-grade software.", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/effect" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "error-handling", + "concurrency", + "observability" + ], + "keywords": [ + "typescript", + "error-handling", + "concurrency", + "observability" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./testing": "./src/testing/index.ts", + "./unstable/ai": "./src/unstable/ai/index.ts", + "./unstable/cli": "./src/unstable/cli/index.ts", + "./unstable/cluster": "./src/unstable/cluster/index.ts", + "./unstable/devtools": "./src/unstable/devtools/index.ts", + "./unstable/encoding": "./src/unstable/encoding/index.ts", + "./unstable/eventlog": "./src/unstable/eventlog/index.ts", + "./unstable/http": "./src/unstable/http/index.ts", + "./unstable/httpapi": "./src/unstable/httpapi/index.ts", + "./unstable/observability": "./src/unstable/observability/index.ts", + "./unstable/persistence": "./src/unstable/persistence/index.ts", + "./unstable/process": "./src/unstable/process/index.ts", + "./unstable/reactivity": "./src/unstable/reactivity/index.ts", + "./unstable/rpc": "./src/unstable/rpc/index.ts", + "./unstable/schema": "./src/unstable/schema/index.ts", + "./unstable/socket": "./src/unstable/socket/index.ts", + "./unstable/sql": "./src/unstable/sql/index.ts", + "./unstable/workflow": "./src/unstable/workflow/index.ts", + "./unstable/workers": "./src/unstable/workers/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./unstable/cli/internal/*": null, + "./unstable/cluster/internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./testing": "./dist/testing/index.js", + "./unstable/ai": "./dist/unstable/ai/index.js", + "./unstable/cli": "./dist/unstable/cli/index.js", + "./unstable/cluster": "./dist/unstable/cluster/index.js", + "./unstable/devtools": "./dist/unstable/devtools/index.js", + "./unstable/encoding": "./dist/unstable/encoding/index.js", + "./unstable/eventlog": "./dist/unstable/eventlog/index.js", + "./unstable/http": "./dist/unstable/http/index.js", + "./unstable/httpapi": "./dist/unstable/httpapi/index.js", + "./unstable/observability": "./dist/unstable/observability/index.js", + "./unstable/persistence": "./dist/unstable/persistence/index.js", + "./unstable/process": "./dist/unstable/process/index.js", + "./unstable/reactivity": "./dist/unstable/reactivity/index.js", + "./unstable/rpc": "./dist/unstable/rpc/index.js", + "./unstable/schema": "./dist/unstable/schema/index.js", + "./unstable/socket": "./dist/unstable/socket/index.js", + "./unstable/sql": "./dist/unstable/sql/index.js", + "./unstable/workflow": "./dist/unstable/workflow/index.js", + "./unstable/workers": "./dist/unstable/workers/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./unstable/cli/internal/*": null, + "./unstable/cluster/internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest --sequence.concurrent=false", + "coverage": "vitest --run --coverage --sequence.concurrent=false" + }, + "devDependencies": { + "@types/ini": "^4.1.1", + "@types/node": "^26.1.1", + "ajv": "^8.20.0", + "arktype": "^2.2.3", + "ast-types": "^0.14.2", + "immer": "^11.1.11", + "tinybench": "^6.0.2", + "valibot": "^1.4.2" + }, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.9.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.4", + "multipasta": "^0.2.8", + "toml": "^4.1.2", + "uuid": "^14.0.1", + "yaml": "^2.9.0" + } +} diff --git a/repos/effect/packages/effect/src/Array.ts b/repos/effect/packages/effect/src/Array.ts new file mode 100644 index 0000000..7e3958d --- /dev/null +++ b/repos/effect/packages/effect/src/Array.ts @@ -0,0 +1,4908 @@ +/** + * Works with JavaScript arrays, readonly arrays, and non-empty arrays. + * + * The helpers cover common collection work such as creating arrays, reading + * elements, transforming values, sorting, grouping, splitting, combining, and + * reducing many values to one result. Helpers that change contents return new + * arrays and preserve non-empty array types when the result is guaranteed to + * contain values. + * + * @since 2.0.0 + */ +import * as Equal from "./Equal.ts" +import * as Equivalence from "./Equivalence.ts" +import type { LazyArg } from "./Function.ts" +import { dual, identity } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import * as internalArray from "./internal/array.ts" +import * as internalDoNotation from "./internal/doNotation.ts" +import * as moduleIterable from "./Iterable.ts" +import * as Option from "./Option.ts" +import * as Order from "./Order.ts" +import type * as Predicate from "./Predicate.ts" +import * as Record from "./Record.ts" +import * as Reducer from "./Reducer.ts" +import * as Result from "./Result.ts" +import * as Tuple from "./Tuple.ts" +import type { NoInfer, TupleOf } from "./Types.ts" + +/** + * Exposes the global array constructor. + * + * **When to use** + * + * Use to access native JavaScript array constructor methods such as `isArray` + * or `from` from the Effect module namespace. + * + * **Example** (Accessing the Array constructor) + * + * ```ts + * import { Array } from "effect" + * + * const arr = new Array.Array(3) + * console.log(arr) // [undefined, undefined, undefined] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const Array = globalThis.Array + +/** + * Type lambda for `ReadonlyArray`, used for higher-kinded type operations. + * + * @category type lambdas + * @since 2.0.0 + */ +export interface ReadonlyArrayTypeLambda extends TypeLambda { + readonly type: ReadonlyArray +} + +/** + * A readonly array guaranteed to have at least one element. + * + * **When to use** + * + * Use when non-emptiness must be tracked at the type level while preventing mutation. + * Many Array module functions accept or return this type. + * + * **Example** (Typing a non-empty array) + * + * ```ts + * import type { Array } from "effect" + * + * const nonEmpty: Array.NonEmptyReadonlyArray = [1, 2, 3] + * const head: number = nonEmpty[0] // guaranteed to exist + * ``` + * + * @see {@link NonEmptyArray} — mutable counterpart + * @see {@link isReadonlyArrayNonEmpty} — narrow a `ReadonlyArray` to this type + * + * @category models + * @since 2.0.0 + */ +export type NonEmptyReadonlyArray
= readonly [A, ...Array] + +/** + * A mutable array guaranteed to have at least one element. + * + * **When to use** + * + * Use when mutation is acceptable and non-emptiness must be tracked at the type + * level. + * + * **Details** + * + * This is the mutable counterpart of {@link NonEmptyReadonlyArray}. Most Array + * module functions return `NonEmptyArray` when the result is guaranteed + * non-empty. + * + * **Example** (Typing a mutable non-empty array) + * + * ```ts + * import type { Array } from "effect" + * + * const nonEmpty: Array.NonEmptyArray = [1, 2, 3] + * nonEmpty.push(4) + * ``` + * + * @see {@link NonEmptyReadonlyArray} — readonly counterpart + * @see {@link isArrayNonEmpty} — narrow an `Array` to this type + * + * @category models + * @since 2.0.0 + */ +export type NonEmptyArray = [A, ...Array] + +/** + * Creates a `NonEmptyArray` from one or more elements. + * + * **When to use** + * + * Use when you need to create a typed non-empty array from literal values. + * + * **Details** + * + * The element type is inferred as the union of all arguments. Because at least + * one argument is required, this always returns a `NonEmptyArray`. + * + * **Example** (Creating an array from values) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.make(1, 2, 3) + * console.log(result) // [1, 2, 3] + * ``` + * + * @see {@link of} — create a single-element array + * @see {@link fromIterable} — create from any iterable + * + * @category constructors + * @since 2.0.0 + */ +export const make = >( + ...elements: Elements +): NonEmptyArray => elements + +/** + * Creates a new `Array` of the specified length with all slots uninitialized. + * + * **When to use** + * + * Use when you need a pre-sized array that will be filled imperatively. + * + * **Details** + * + * Elements are typed as `A | undefined` because the slots are empty. + * + * **Example** (Allocating a fixed-size array) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.allocate(3) + * console.log(result.length) // 3 + * ``` + * + * @see {@link makeBy} — create an array by computing each element + * + * @category constructors + * @since 2.0.0 + */ +export const allocate = (n: number): Array => new Array(n) + +/** + * Creates a `NonEmptyArray` of length `n` where element `i` is computed by `f(i)`. + * + * **When to use** + * + * Use when you need to compute each array element from its index. + * + * **Details** + * + * `n` is normalized to an integer greater than or equal to 1, so this function + * always returns at least one element. Supports both data-first and data-last + * usage. + * + * **Example** (Generating values from indices) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.makeBy(5, (n) => n * 2) + * console.log(result) // [0, 2, 4, 6, 8] + * ``` + * + * @see {@link range} — create a range of integers + * @see {@link replicate} — repeat a single value + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy: { + (f: (i: number) => A): (n: number) => NonEmptyArray + (n: number, f: (i: number) => A): NonEmptyArray +} = dual(2, (n: number, f: (i: number) => A) => { + const max = Math.max(1, Math.floor(n)) + const out = new Array(max) + for (let i = 0; i < max; i++) { + out[i] = f(i) + } + return out as NonEmptyArray +}) + +/** + * Creates a `NonEmptyArray` containing a range of integers, inclusive on both + * ends. + * + * **When to use** + * + * Use when you need a non-empty sequence of consecutive integers. + * + * **Details** + * + * If `start > end`, returns `[start]`. + * + * **Example** (Creating a range) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.range(1, 3) + * console.log(result) // [1, 2, 3] + * ``` + * + * @see {@link makeBy} — generate values from a function + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end: number): NonEmptyArray => + start <= end ? makeBy(end - start + 1, (i) => start + i) : [start] + +/** + * Creates a `NonEmptyArray` containing a value repeated `n` times. + * + * **When to use** + * + * Use when you need a non-empty array containing repeated copies of one value. + * + * **Details** + * + * `n` is normalized to an integer greater than or equal to 1, so this function + * always returns at least one element. Supports both data-first and data-last + * usage. + * + * **Example** (Repeating a value) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.replicate("a", 3) + * console.log(result) // ["a", "a", "a"] + * ``` + * + * @see {@link makeBy} — vary values based on index + * + * @category constructors + * @since 2.0.0 + */ +export const replicate: { + (n: number): (a: A) => NonEmptyArray + (a: A, n: number): NonEmptyArray +} = dual(2, (a: A, n: number): NonEmptyArray => makeBy(n, () => a)) + +/** + * Converts an `Iterable` to an `Array`. + * + * **When to use** + * + * Use to convert any `Iterable` (Set, Generator, etc.) into an array. + * + * **Details** + * + * If the input is already an array, this returns it by reference without + * copying. Otherwise, it creates a new array from the iterable. Use `copy` if + * you need a fresh array even when the input is already an array. + * + * **Example** (Converting a Set to an array) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.fromIterable(new Set([1, 2, 3])) + * console.log(result) // [1, 2, 3] + * ``` + * + * @see {@link ensure} — wrap a single value or return an existing array + * @see {@link copy} — create a shallow copy of an array + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (collection: Iterable): Array => + Array.isArray(collection) ? collection : Array.from(collection) + +/** + * Normalizes a value that is either a single element or an array into an array. + * + * **When to use** + * + * Use to normalize input that may be a single value or an array into a consistent + * array. + * + * **Details** + * + * If the input is already an array, this returns it by reference. If the input + * is a single value, this wraps it in a one-element array. This is useful for + * APIs that accept `A | Array`. + * + * **Example** (Normalizing input) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.ensure("a")) // ["a"] + * console.log(Array.ensure(["a", "b", "c"])) // ["a", "b", "c"] + * ``` + * + * @see {@link of} — always wrap in a single-element array + * @see {@link fromIterable} — convert any iterable + * + * @category constructors + * @since 3.3.0 + */ +export const ensure = (self: ReadonlyArray | A): Array => Array.isArray(self) ? self : [self as A] + +/** + * Converts a record into an array of `[key, value]` tuples. + * + * **When to use** + * + * Use to convert a record into an array of key-value tuples for iteration or + * transformation. + * + * **Details** + * + * Key order follows `Object.entries` semantics. Empty records produce an empty + * array. + * + * **Example** (Converting a record to entries) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.fromRecord({ a: 1, b: 2, c: 3 }) + * console.log(result) // [["a", 1], ["b", 2], ["c", 3]] + * ``` + * + * @see {@link Record.toEntries} the equivalent function from the Record module + * @see {@link Record.fromEntries} to build a record from an array of tuples + * + * @category converting + * @since 2.0.0 + */ +export const fromRecord: (self: Readonly>) => Array<[K, A]> = Record.toEntries + +/** + * Converts an `Option` to an array: `Some(a)` becomes `[a]`, `None` becomes `[]`. + * + * **When to use** + * + * Use to convert a single `Option` into an array for downstream array operations. + * + * **Example** (Converting an Option to an array) + * + * ```ts + * import { Array, Option } from "effect" + * + * console.log(Array.fromOption(Option.some(1))) // [1] + * console.log(Array.fromOption(Option.none())) // [] + * ``` + * + * @see {@link getSomes} — extract `Some` values from an array of Options + * + * @category converting + * @since 2.0.0 + */ +export const fromOption: (self: Option.Option) => Array = Option.toArray + +/** + * Pattern-matches on an array, handling empty and non-empty cases separately. + * + * **When to use** + * + * Use when you need to branch on whether an array is empty. + * + * **Details** + * + * `onNonEmpty` receives a `NonEmptyReadonlyArray`. Supports both data-first and + * data-last usage. + * + * **Example** (Branching on emptiness) + * + * ```ts + * import { Array } from "effect" + * + * const describe = Array.match({ + * onEmpty: () => "empty", + * onNonEmpty: ([head, ...tail]) => `head: ${head}, tail: ${tail.length}` + * }) + * console.log(describe([])) // "empty" + * console.log(describe([1, 2, 3])) // "head: 1, tail: 2" + * ``` + * + * @see {@link matchLeft} — destructures into head + tail + * @see {@link matchRight} — destructures into init + last + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (self: NonEmptyReadonlyArray) => C + } +): B | C => isReadonlyArrayNonEmpty(self) ? onNonEmpty(self) : onEmpty()) + +/** + * Pattern-matches on an array from the left, providing the first element and + * the remaining elements separately. + * + * **When to use** + * + * Use when you need to branch on an array and handle the non-empty case as the + * first element plus the remaining elements. + * + * **Details** + * + * `onNonEmpty` receives `(head, tail)` where `tail` is the rest of the array. + * + * **Example** (Destructuring head and tail) + * + * ```ts + * import { Array } from "effect" + * + * const matchLeft = Array.matchLeft({ + * onEmpty: () => "empty", + * onNonEmpty: (head, tail) => `head: ${head}, tail: ${tail.length}` + * }) + * console.log(matchLeft([])) // "empty" + * console.log(matchLeft([1, 2, 3])) // "head: 1, tail: 2" + * ``` + * + * @see {@link match} — receives the full non-empty array + * @see {@link matchRight} — destructures into init + last + * + * @category pattern matching + * @since 2.0.0 + */ +export const matchLeft: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (head: A, tail: Array) => C + } +): B | C => isReadonlyArrayNonEmpty(self) ? onNonEmpty(headNonEmpty(self), tailNonEmpty(self)) : onEmpty()) + +/** + * Pattern-matches on an array from the right, providing all elements except the + * last and the last element separately. + * + * **When to use** + * + * Use when you need to branch on an array and handle the non-empty case as the + * elements before the last plus the last element. + * + * **Details** + * + * `onNonEmpty` receives `(init, last)` where `init` is everything but the last element. + * + * **Example** (Destructuring init and last) + * + * ```ts + * import { Array } from "effect" + * + * const matchRight = Array.matchRight({ + * onEmpty: () => "empty", + * onNonEmpty: (init, last) => `init: ${init.length}, last: ${last}` + * }) + * console.log(matchRight([])) // "empty" + * console.log(matchRight([1, 2, 3])) // "init: 2, last: 3" + * ``` + * + * @see {@link match} — receives the full non-empty array + * @see {@link matchLeft} — destructures into head + tail + * + * @category pattern matching + * @since 2.0.0 + */ +export const matchRight: { + ( + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } + ): (self: ReadonlyArray) => B | C + ( + self: ReadonlyArray, + options: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } + ): B | C +} = dual(2, ( + self: ReadonlyArray, + { onEmpty, onNonEmpty }: { + readonly onEmpty: LazyArg + readonly onNonEmpty: (init: Array, last: A) => C + } +): B | C => + isReadonlyArrayNonEmpty(self) ? + onNonEmpty(initNonEmpty(self), lastNonEmpty(self)) : + onEmpty()) + +/** + * Adds a single element to the front of an iterable, returning a `NonEmptyArray`. + * + * **When to use** + * + * Use when you need to guarantee a non-empty result after adding a required + * leading value. + * + * **Example** (Prepending an element) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.prepend([2, 3, 4], 1) + * console.log(result) // [1, 2, 3, 4] + * ``` + * + * @see {@link append} — add to the end + * @see {@link prependAll} — prepend multiple elements + * + * @category combining + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => NonEmptyArray + (self: Iterable, head: B): NonEmptyArray +} = dual(2, (self: Iterable, head: B): NonEmptyArray => [head, ...self]) + +/** + * Prepends all elements from a prefix iterable to the front of an array. + * + * **When to use** + * + * Use to prepend multiple elements from an iterable to the front of an array. + * + * **Details** + * + * If either input is non-empty, the result is a `NonEmptyArray`. + * + * **Example** (Prepending multiple elements) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.prependAll([2, 3], [0, 1]) + * console.log(result) // [0, 1, 2, 3] + * ``` + * + * @see {@link prepend} — add a single element to the front + * @see {@link appendAll} — add elements to the end + * + * @category combining + * @since 2.0.0 + */ +export const prependAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(that).concat(fromIterable(self)) +) + +/** + * Adds a single element to the end of an iterable, returning a `NonEmptyArray`. + * + * **When to use** + * + * Use when you need to guarantee a non-empty result after adding a required + * trailing value. + * + * **Example** (Appending an element) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.append([1, 2, 3], 4) + * console.log(result) // [1, 2, 3, 4] + * ``` + * + * @see {@link prepend} — add to the front + * @see {@link appendAll} — append multiple elements + * + * @category combining + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => NonEmptyArray + (self: Iterable, last: B): NonEmptyArray +} = dual(2, (self: Iterable, last: B): Array => [...self, last]) + +/** + * Concatenates two iterables into a single array. + * + * **When to use** + * + * Use to combine two iterable inputs into a new array with the second input's + * elements after the first. + * + * **Details** + * + * If either input is non-empty, the result is a `NonEmptyArray`. + * + * **Example** (Concatenating arrays) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.appendAll([1, 2], [3, 4]) + * console.log(result) // [1, 2, 3, 4] + * ``` + * + * @see {@link append} — add a single element to the end + * @see {@link prependAll} — add elements to the front + * + * @category combining + * @since 2.0.0 + */ +export const appendAll: { + , T extends Iterable>( + that: T + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: Iterable, that: NonEmptyReadonlyArray): NonEmptyArray + (self: NonEmptyReadonlyArray, that: Iterable): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => fromIterable(self).concat(fromIterable(that)) +) + +/** + * Folds left-to-right while keeping every intermediate accumulator value. + * + * **When to use** + * + * Use to compute a running accumulator where each intermediate value is needed. + * + * **Details** + * + * The output length is `input.length + 1` because it starts with the initial + * value. The result is always a `NonEmptyArray`. Use `reduce` if you only need + * the final accumulated value. + * + * **Example** (Running totals) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.scan([1, 2, 3, 4], 0, (acc, value) => acc + value) + * console.log(result) // [0, 1, 3, 6, 10] + * ``` + * + * @see {@link scanRight} — right-to-left scan + * @see {@link reduce} — fold without intermediate values + * + * @category folding + * @since 2.0.0 + */ +export const scan: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => NonEmptyArray + (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray => { + const out: NonEmptyArray = [b] + let i = 0 + for (const a of self) { + out[i + 1] = f(out[i], a) + i++ + } + return out +}) + +/** + * Folds right-to-left while keeping every intermediate accumulator value. + * + * **When to use** + * + * Use to compute a running accumulator from right to left where each intermediate + * value is needed. + * + * **Details** + * + * The output length is `input.length + 1` because it ends with the initial + * value. The result is always a `NonEmptyArray`. + * + * **Example** (Scanning running totals in reverse) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.scanRight([1, 2, 3, 4], 0, (acc, value) => acc + value) + * console.log(result) // [10, 9, 7, 4, 0] + * ``` + * + * @see {@link scan} — left-to-right scan + * @see {@link reduceRight} — fold without intermediate values + * + * @category folding + * @since 2.0.0 + */ +export const scanRight: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => NonEmptyArray + (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): NonEmptyArray => { + const input = fromIterable(self) + const out: NonEmptyArray = new Array(input.length + 1) as any + out[input.length] = b + for (let i = input.length - 1; i >= 0; i--) { + out[i] = f(out[i + 1], input[i]) + } + return out +}) + +/** + * Checks whether a value is an `Array`. + * + * **When to use** + * + * Use to verify a value is a mutable array, narrowing its type to `Array`. + * + * **Details** + * + * Acts as a type guard narrowing the input to `Array` and delegates to + * `globalThis.Array.isArray`. + * + * **Example** (Type-guarding an unknown value) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isArray(null)) // false + * console.log(Array.isArray([1, 2, 3])) // true + * ``` + * + * @see {@link isArrayEmpty} — check for an empty array + * @see {@link isArrayNonEmpty} — check for a non-empty array + * + * @category guards + * @since 2.0.0 + */ +export const isArray: { + (self: unknown): self is Array + (self: T): self is Extract> +} = Array.isArray + +/** + * Checks whether a mutable `Array` is empty, narrowing the type to `[]`. + * + * **Example** (Checking for an empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isArrayEmpty([])) // true + * console.log(Array.isArrayEmpty([1, 2, 3])) // false + * ``` + * + * @see {@link isReadonlyArrayEmpty} — readonly variant + * @see {@link isArrayNonEmpty} — opposite check + * + * @category guards + * @since 4.0.0 + */ +export const isArrayEmpty = (self: Array): self is [] => self.length === 0 + +/** + * Checks whether a `ReadonlyArray` is empty, narrowing the type to `readonly []`. + * + * **Example** (Checking for an empty readonly array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isReadonlyArrayEmpty([])) // true + * console.log(Array.isReadonlyArrayEmpty([1, 2, 3])) // false + * ``` + * + * @see {@link isArrayEmpty} — mutable variant + * @see {@link isReadonlyArrayNonEmpty} — opposite check + * + * @category guards + * @since 4.0.0 + */ +export const isReadonlyArrayEmpty: (self: ReadonlyArray) => self is readonly [] = isArrayEmpty as any + +/** + * Checks whether a mutable `Array` is non-empty, narrowing the type to + * `NonEmptyArray`. + * + * **When to use** + * + * Use when you need the narrowed value to remain a mutable `Array` after proving + * it has at least one element. + * + * **Example** (Checking for a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isArrayNonEmpty([])) // false + * console.log(Array.isArrayNonEmpty([1, 2, 3])) // true + * ``` + * + * @see {@link isReadonlyArrayNonEmpty} — readonly variant + * @see {@link isArrayEmpty} — opposite check + * + * @category guards + * @since 4.0.0 + */ +export const isArrayNonEmpty: (self: Array) => self is NonEmptyArray = internalArray.isArrayNonEmpty + +/** + * Checks whether a `ReadonlyArray` is non-empty, narrowing the type to + * `NonEmptyReadonlyArray`. + * + * **When to use** + * + * Use when you need to prove a readonly array has at least one element without + * requiring mutable array methods afterward. + * + * **Example** (Checking for a non-empty readonly array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.isReadonlyArrayNonEmpty([])) // false + * console.log(Array.isReadonlyArrayNonEmpty([1, 2, 3])) // true + * ``` + * + * @see {@link isArrayNonEmpty} — mutable variant + * @see {@link isReadonlyArrayEmpty} — opposite check + * + * @category guards + * @since 4.0.0 + */ +export const isReadonlyArrayNonEmpty: (self: ReadonlyArray) => self is NonEmptyReadonlyArray = + internalArray.isArrayNonEmpty + +/** + * Returns the number of elements in a `ReadonlyArray`. + * + * **When to use** + * + * Use when you need length as a composable function rather than a property access. + * + * **Example** (Getting the length) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.length([1, 2, 3])) // 3 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const length = (self: ReadonlyArray): number => self.length + +/** @internal */ +export function isOutOfBounds(i: number, as: ReadonlyArray): boolean { + return i < 0 || i >= as.length +} + +const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.min(Math.max(0, i), as.length)) + +/** + * Reads an element at the given index safely, returning `Option.some` or + * `Option.none` if the index is out of bounds. + * + * **When to use** + * + * Use when you need to read an array element by index and handle an + * out-of-bounds index as `Option.none`. + * + * **Details** + * + * The index is floored to an integer. This never throws. + * + * **Example** (Accessing indexes safely) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.get([1, 2, 3], 1)) // Some(2) + * console.log(Array.get([1, 2, 3], 10)) // None + * ``` + * + * @see {@link getUnsafe} for indexed access that throws when the index is out of bounds + * @see {@link head} for reading the first element as an `Option` + * @see {@link last} for reading the last element as an `Option` + * + * @category getters + * @since 2.0.0 + */ +export const get: { + (index: number): (self: ReadonlyArray) => Option.Option + (self: ReadonlyArray, index: number): Option.Option +} = dual(2, (self: ReadonlyArray, index: number): Option.Option => { + const i = Math.floor(index) + return isOutOfBounds(i, self) ? Option.none() : Option.some(self[i]) +}) + +/** + * Reads an element at the given index, throwing if the index is out of bounds. + * + * **When to use** + * + * Use to read an array element at a known valid index when out-of-bounds would + * be a programming error. + * + * **Details** + * + * Throws an `Error` with the message `"Index out of bounds: "`. Prefer + * `get` for safe access. + * + * **Example** (Accessing indexes unsafely) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.getUnsafe([1, 2, 3], 1)) // 2 + * // Array.getUnsafe([1, 2, 3], 10) // throws Error + * ``` + * + * @see {@link get} — safe version returning `Option` + * + * @category unsafe + * @since 4.0.0 + */ +export const getUnsafe: { + (index: number): (self: ReadonlyArray) => A + (self: ReadonlyArray, index: number): A +} = dual(2, (self: ReadonlyArray, index: number): A => { + const i = Math.floor(index) + if (isOutOfBounds(i, self)) { + throw new Error(`Index out of bounds: ${i}`) + } + return self[i] +}) + +/** + * Splits a non-empty array into its first element and the remaining elements. + * + * **When to use** + * + * Use when you have a `NonEmptyReadonlyArray` and need both its first element + * and the remaining elements as separate values. + * + * **Details** + * + * Returns a tuple `[head, tail]` and requires a `NonEmptyReadonlyArray`. + * + * **Example** (Destructuring head and tail) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.unprepend([1, 2, 3, 4]) + * console.log(result) // [1, [2, 3, 4]] + * ``` + * + * @see {@link unappend} for splitting a non-empty array into init and last + * @see {@link headNonEmpty} for getting only the first element + * @see {@link tailNonEmpty} for getting only the elements after the first + * + * @category splitting + * @since 2.0.0 + */ +export const unprepend = ( + self: NonEmptyReadonlyArray +): [firstElement: A, remainingElements: Array] => [headNonEmpty(self), tailNonEmpty(self)] + +/** + * Splits a non-empty array into all elements except the last, and the last + * element. + * + * **When to use** + * + * Use when you need to split a non-empty array into the elements before the + * last element and the last element. + * + * **Details** + * + * Returns a tuple `[init, last]` and requires a `NonEmptyReadonlyArray`. + * + * **Example** (Destructuring init and last) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.unappend([1, 2, 3, 4]) + * console.log(result) // [[1, 2, 3], 4] + * ``` + * + * @see {@link unprepend} for splitting a non-empty array into head and tail + * @see {@link initNonEmpty} for getting only the elements before the last + * @see {@link lastNonEmpty} for getting only the last element + * + * @category splitting + * @since 2.0.0 + */ +export const unappend = ( + self: NonEmptyReadonlyArray +): [arrayWithoutLastElement: Array, lastElement: A] => [initNonEmpty(self), lastNonEmpty(self)] + +/** + * Returns the first element of an array safely wrapped in `Option.some`, or + * `Option.none` if the array is empty. + * + * **When to use** + * + * Use to safely get the first element of an array that may be empty. + * + * **Example** (Getting the first element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.head([1, 2, 3])) // Some(1) + * console.log(Array.head([])) // None + * ``` + * + * @see {@link headNonEmpty} — direct access when array is known non-empty + * @see {@link last} — get the last element + * + * @category getters + * @since 2.0.0 + */ +export const head: (self: ReadonlyArray) => Option.Option = get(0) + +/** + * Returns the first element of a `NonEmptyReadonlyArray` directly (no `Option` + * wrapper). + * + * **When to use** + * + * Use to get the first element without `Option` wrapping when the array is known + * to be non-empty. + * + * **Example** (Getting the head of a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.headNonEmpty([1, 2, 3, 4])) // 1 + * ``` + * + * @see {@link head} — safe version for possibly-empty arrays + * + * @category getters + * @since 2.0.0 + */ +export const headNonEmpty: (self: NonEmptyReadonlyArray) => A = getUnsafe(0) + +/** + * Returns the last element of an array safely wrapped in `Option.some`, or + * `Option.none` if the array is empty. + * + * **When to use** + * + * Use to safely get the last element of an array that may be empty. + * + * **Example** (Getting the last element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.last([1, 2, 3])) // Some(3) + * console.log(Array.last([])) // None + * ``` + * + * @see {@link lastNonEmpty} — direct access when array is known non-empty + * @see {@link head} — get the first element + * + * @category getters + * @since 2.0.0 + */ +export const last = (self: ReadonlyArray): Option.Option => + isReadonlyArrayNonEmpty(self) ? Option.some(lastNonEmpty(self)) : Option.none() + +/** + * Returns the last element of a `NonEmptyReadonlyArray` directly (no `Option` + * wrapper). + * + * **When to use** + * + * Use to get the last element without `Option` wrapping when the array is known + * to be non-empty. + * + * **Example** (Getting the last of a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.lastNonEmpty([1, 2, 3, 4])) // 4 + * ``` + * + * @see {@link last} — safe version for possibly-empty arrays + * + * @category getters + * @since 2.0.0 + */ +export const lastNonEmpty = (self: NonEmptyReadonlyArray): A => self[self.length - 1] + +/** + * Returns all elements except the first safely, wrapped in an `Option`. + * + * **When to use** + * + * Use to safely get all elements after the first when the iterable may be empty. + * + * **Details** + * + * Allocates a new array via `slice(1)`. Empty inputs return `Option.none()`. + * + * **Example** (Getting the tail) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.tail([1, 2, 3, 4])) // Option.some([2, 3, 4]) + * console.log(Array.tail([])) // Option.none() + * ``` + * + * @see {@link tailNonEmpty} — when the array is known non-empty + * @see {@link init} — all elements except the last + * + * @category getters + * @since 2.0.0 + */ +export function tail(self: Iterable): Option.Option> { + const as = fromIterable(self) + return isReadonlyArrayNonEmpty(as) ? Option.some(tailNonEmpty(as)) : Option.none() +} + +/** + * Returns all elements except the first of a `NonEmptyReadonlyArray`. + * + * **When to use** + * + * Use to get all elements after the first when the array is known to be non-empty. + * + * **Example** (Getting the tail of a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.tailNonEmpty([1, 2, 3, 4])) // [2, 3, 4] + * ``` + * + * @see {@link tail} — safe version for possibly-empty arrays + * @see {@link initNonEmpty} — all elements except the last + * + * @category getters + * @since 2.0.0 + */ +export const tailNonEmpty = (self: NonEmptyReadonlyArray): Array => self.slice(1) + +/** + * Returns all elements except the last safely, wrapped in an `Option`. + * + * **When to use** + * + * Use to safely get all elements before the last when the iterable may be empty. + * + * **Details** + * + * Allocates a new array via `slice(0, -1)`. Empty inputs return + * `Option.none()`. + * + * **Example** (Getting init) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.init([1, 2, 3, 4])) // Option.some([1, 2, 3]) + * console.log(Array.init([])) // Option.none() + * ``` + * + * @see {@link initNonEmpty} — when the array is known non-empty + * @see {@link tail} — all elements except the first + * + * @category getters + * @since 2.0.0 + */ +export function init(self: Iterable): Option.Option> { + const as = fromIterable(self) + return isReadonlyArrayNonEmpty(as) ? Option.some(initNonEmpty(as)) : Option.none() +} + +/** + * Returns all elements except the last of a `NonEmptyReadonlyArray`. + * + * **When to use** + * + * Use to get all elements before the last when the array is known to be non-empty. + * + * **Example** (Getting init of a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.initNonEmpty([1, 2, 3, 4])) // [1, 2, 3] + * ``` + * + * @see {@link init} — safe version for possibly-empty arrays + * @see {@link tailNonEmpty} — all elements except the first + * + * @category getters + * @since 2.0.0 + */ +export const initNonEmpty = (self: NonEmptyReadonlyArray): Array => self.slice(0, -1) + +/** + * Keeps the first `n` elements, creating a new array. + * + * **When to use** + * + * Use to keep up to the first `n` elements from an iterable as a new array. + * + * **Details** + * + * `n` is clamped to `[0, length]`. Returns an empty array when `n <= 0`. + * + * **Example** (Taking from the start) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.take([1, 2, 3, 4, 5], 3)) // [1, 2, 3] + * ``` + * + * @see {@link takeRight} for keeping elements from the end + * @see {@link takeWhile} for keeping an initial prefix while a predicate holds + * @see {@link drop} for removing elements from the start + * + * @category getters + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(0, clamp(n, input)) +}) + +/** + * Keeps the last `n` elements, creating a new array. + * + * **When to use** + * + * Use to keep the last `n` elements of an iterable. + * + * **Details** + * + * `n` is clamped to `[0, length]`. Returns an empty array when `n <= 0`. + * + * **Example** (Taking from the end) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.takeRight([1, 2, 3, 4, 5], 3)) // [3, 4, 5] + * ``` + * + * @see {@link take} — keep from the start + * @see {@link dropRight} — remove from the end + * + * @category getters + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + const i = clamp(n, input) + return i === 0 ? [] : input.slice(-i) +}) + +/** + * Takes elements from the start while the predicate holds, stopping at the + * first element that fails. + * + * **When to use** + * + * Use to keep the leading elements of an iterable while each element satisfies + * a predicate, returning the retained prefix as an array. + * + * **Details** + * + * Supports refinements for type narrowing. The predicate receives + * `(element, index)`. + * + * **Example** (Taking while condition holds) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.takeWhile([1, 3, 2, 4, 1, 2], (x) => x < 4)) // [1, 3, 2] + * ``` + * + * @see {@link take} for keeping a fixed number of leading elements + * @see {@link dropWhile} for removing the matching prefix and keeping the rest + * @see {@link span} for splitting the matching prefix from the remaining elements + * + * @category getters + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Array + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, refinement: (a: A, i: number) => a is B): Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Array => { + let i = 0 + const out: Array = [] + for (const a of self) { + if (!predicate(a, i)) { + break + } + out.push(a) + i++ + } + return out +}) + +/** + * Takes elements from the start while a `Filter` succeeds, collecting transformed values. + * + * **When to use** + * + * Use when you need to take a prefix from an iterable while a function can + * successfully extract or transform elements, stopping at the first element + * that produces a failure result. + * + * **Details** + * + * The filter receives `(element, index)` and processing stops at the first + * filter failure. + * + * @see {@link takeWhile} for taking a prefix based on a boolean predicate + * + * @category getters + * @since 4.0.0 + */ +export const takeWhileFilter: { + (f: (input: NoInfer, i: number) => Result.Result): (self: Iterable) => Array + (self: Iterable, f: (input: NoInfer, i: number) => Result.Result): Array +} = dual(2, (self: Iterable, f: (input: NoInfer, i: number) => Result.Result): Array => { + let i = 0 + const out: Array = [] + for (const a of self) { + const result = f(a, i) + if (Result.isFailure(result)) { + break + } + out.push(result.success) + i++ + } + return out +}) + +const spanIndex = (self: Iterable, predicate: (a: A, i: number) => boolean): number => { + let i = 0 + for (const a of self) { + if (!predicate(a, i)) { + break + } + i++ + } + return i +} + +/** + * Splits an iterable into two arrays: the longest prefix where the predicate + * holds, and the remaining elements. + * + * **When to use** + * + * Use when you need both the longest predicate-matching prefix and the + * remaining elements. + * + * **Details** + * + * Equivalent to `[takeWhile(pred), dropWhile(pred)]`, but more efficient + * because it runs in a single pass. Supports refinements for type narrowing of + * the prefix. + * + * **Example** (Splitting at predicate boundary) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.span([1, 3, 2, 4, 5], (x) => x % 2 === 1)) // [[1, 3], [2, 4, 5]] + * ``` + * + * @see {@link takeWhile} for keeping only the matching prefix + * @see {@link dropWhile} for keeping only the elements after the matching prefix + * @see {@link splitWhere} for splitting at the first element that satisfies a predicate + * + * @category splitting + * @since 2.0.0 + */ +export const span: { + ( + refinement: (a: NoInfer, i: number) => a is B + ): (self: Iterable) => [init: Array, rest: Array>] + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => [init: Array, rest: Array] + ( + self: Iterable, + refinement: (a: A, i: number) => a is B + ): [init: Array, rest: Array>] + (self: Iterable, predicate: (a: A, i: number) => boolean): [init: Array, rest: Array] +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): [init: Array, rest: Array] => { + const input = fromIterable(self) + return splitAt(input, spanIndex(input, predicate)) + } +) + +/** + * Removes the first `n` elements, creating a new array. + * + * **When to use** + * + * Use to keep the suffix of an iterable after skipping a fixed number of + * leading elements. + * + * **Details** + * + * `n` is clamped to `[0, length]`. When `n <= 0`, this returns a copy of the + * full array. + * + * **Example** (Dropping from the start) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.drop([1, 2, 3, 4, 5], 2)) // [3, 4, 5] + * ``` + * + * @see {@link dropRight} for removing a fixed number of elements from the end + * @see {@link dropWhile} for removing a prefix based on a predicate instead of a fixed count + * @see {@link take} for keeping a fixed number of elements from the start + * + * @category getters + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(clamp(n, input), input.length) +}) + +/** + * Removes the last `n` elements, creating a new array. + * + * **When to use** + * + * Use to remove the last `n` elements from an iterable. + * + * **Details** + * + * `n` is clamped to `[0, length]`. + * + * **Example** (Dropping from the end) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dropRight([1, 2, 3, 4, 5], 2)) // [1, 2, 3] + * ``` + * + * @see {@link drop} — remove from the start + * @see {@link takeRight} — keep from the end + * + * @category getters + * @since 2.0.0 + */ +export const dropRight: { + (n: number): (self: Iterable) => Array + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + return input.slice(0, input.length - clamp(n, input)) +}) + +/** + * Drops elements from the start while the predicate holds, returning the rest. + * + * **When to use** + * + * Use to remove a leading prefix of elements that satisfy a predicate. + * + * **Details** + * + * The predicate receives `(element, index)`. + * + * **Example** (Dropping while condition holds) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dropWhile([1, 2, 3, 4, 5], (x) => x < 4)) // [4, 5] + * ``` + * + * @see {@link takeWhile} — keep the matching prefix instead + * @see {@link drop} — drop a fixed count + * + * @category getters + * @since 2.0.0 + */ +export const dropWhile: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Array => { + const input = fromIterable(self) + let i = 0 + while (i < input.length) { + if (!predicate(input[i], i)) { + break + } + i++ + } + return input.slice(i) +}) + +/** + * Drops elements from the start while a `Filter` succeeds. + * + * **When to use** + * + * Use when you need to drop a prefix from an iterable by computing a `Result` + * per element instead of using a simple boolean predicate. + * + * **Details** + * + * The filter receives `(element, index)`. The result contains the remaining + * original elements after the first filter failure. + * + * @see {@link dropWhile} for dropping a prefix with a simple boolean predicate + * @see {@link takeWhileFilter} for keeping only the matching prefix + * + * @category getters + * @since 4.0.0 + */ +export const dropWhileFilter: { + (f: (input: NoInfer, i: number) => Result.Result): (self: Iterable) => Array + (self: Iterable, f: (input: A, i: number) => Result.Result): Array +} = dual( + 2, + (self: Iterable, f: (input: A, i: number) => Result.Result): Array => { + const input = fromIterable(self) + let i = 0 + while (i < input.length) { + if (Result.isFailure(f(input[i], i))) { + break + } + i++ + } + return input.slice(i) + } +) + +/** + * Returns the index of the first element matching the predicate, wrapped in an + * `Option`. + * + * **When to use** + * + * Use to find the index of the first matching element from the start of an + * iterable. + * + * **Example** (Finding an index) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.findFirstIndex([5, 3, 8, 9], (x) => x > 5)) // Option.some(2) + * ``` + * + * @see {@link findLastIndex} — search from the end + * @see {@link findFirst} — get the element itself + * + * @category elements + * @since 2.0.0 + */ +export const findFirstIndex: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option => { + let i = 0 + for (const a of self) { + if (predicate(a, i)) { + return Option.some(i) + } + i++ + } + return Option.none() +}) + +/** + * Returns the index of the last element matching the predicate, wrapped in an + * `Option`. + * + * **When to use** + * + * Use to find the index of the last matching element from the end of an array. + * + * **Example** (Finding the last matching index) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.findLastIndex([1, 3, 8, 9], (x) => x < 5)) // Option.some(1) + * ``` + * + * @see {@link findFirstIndex} — search from the start + * @see {@link findLast} — get the element itself + * + * @category elements + * @since 2.0.0 + */ +export const findLastIndex: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option => { + const input = fromIterable(self) + for (let i = input.length - 1; i >= 0; i--) { + if (predicate(input[i], i)) { + return Option.some(i) + } + } + return Option.none() +}) + +/** + * Returns the first element matching a predicate, refinement, or mapping + * function, wrapped in `Option`. + * + * **When to use** + * + * Use to scan an iterable in iteration order and return the first selected + * element or mapped value as an `Option`. + * + * **Details** + * + * Accepts a predicate `(a, i) => boolean`, a refinement, or a function + * `(a, i) => Option` for simultaneous find-and-transform. If no element + * matches, this returns `Option.none()`. + * + * **Example** (Finding the first match) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.findFirst([1, 2, 3, 4, 5], (x) => x > 3)) // Option.some(4) + * ``` + * + * @see {@link findLast} — search from the end + * @see {@link findFirstIndex} — get the index instead + * @see {@link findFirstWithIndex} — get both element and index + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = moduleIterable.findFirst + +/** + * Returns the first selected value together with its index, wrapped in an + * `Option`. + * + * **When to use** + * + * Use to find both the first matching element and its index in one pass. + * + * **Details** + * + * Accepts a predicate, a refinement, or a function returning `Option`. For an + * `Option`-returning function, returns `[mappedValue, index]` for the first + * `Some`, or `Option.none()` if no element is selected. + * + * **Example** (Finding element with its index) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.findFirstWithIndex([1, 2, 3, 4, 5], (x) => x > 3)) // Option.some([4, 3]) + * ``` + * + * @see {@link findFirst} — get only the element + * @see {@link findFirstIndex} — get only the index + * + * @category elements + * @since 3.17.0 + */ +export const findFirstWithIndex: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option<[B, number]> + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option<[B, number]> + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option<[A, number]> + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option<[B, number]> + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option<[B, number]> + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option<[A, number]> +} = dual( + 2, + ( + self: Iterable, + f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option.Option) + ): Option.Option<[A, number]> => { + let i = 0 + for (const a of self) { + const o = f(a, i) + if (typeof o === "boolean") { + if (o) { + return Option.some([a, i]) + } + } else { + if (Option.isSome(o)) { + return Option.some([o.value, i]) + } + } + i++ + } + return Option.none() + } +) + +/** + * Returns the last element matching a predicate, refinement, or mapping + * function, wrapped in `Option`. + * + * **When to use** + * + * Use to find the last matching element from the end of an array. + * + * **Details** + * + * Searches from the end of the array. If no element matches, this returns + * `Option.none()`. + * + * **Example** (Finding the last match) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.findLast([1, 2, 3, 4, 5], (n) => n % 2 === 0)) // Option.some(4) + * ``` + * + * @see {@link findFirst} — search from the start + * @see {@link findLastIndex} — get the index instead + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (f: (a: NoInfer, i: number) => Option.Option): (self: Iterable) => Option.Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option.Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option.Option + (self: Iterable, f: (a: A, i: number) => Option.Option): Option.Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option.Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option.Option +} = dual( + 2, + ( + self: Iterable, + f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option.Option) + ): Option.Option => { + const input = fromIterable(self) + for (let i = input.length - 1; i >= 0; i--) { + const a = input[i] + const o = f(a, i) + if (typeof o === "boolean") { + if (o) { + return Option.some(a) + } + } else { + if (Option.isSome(o)) { + return o + } + } + } + return Option.none() + } +) + +/** + * Inserts an element at the specified index safely, returning a new `NonEmptyArray` + * wrapped in an `Option`. + * + * **When to use** + * + * Use to insert a single element at a specific position in an array. + * + * **Details** + * + * Valid indices are `0` to `length`, inclusive. Inserting at `length` appends. + * + * **Example** (Inserting at an index) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.insertAt(["a", "b", "c", "e"], 3, "d")) // Option.some(["a", "b", "c", "d", "e"]) + * ``` + * + * @see {@link replace} — replace an existing element + * @see {@link modify} — transform an element at an index + * + * @category elements + * @since 2.0.0 + */ +export const insertAt: { + (i: number, b: B): (self: Iterable) => Option.Option> + (self: Iterable, i: number, b: B): Option.Option> +} = dual(3, (self: Iterable, i: number, b: B): Option.Option> => { + const out: Array = Array.from(self) // copy because `splice` mutates the array + if (i < 0 || i > out.length) { + return Option.none() + } + out.splice(i, 0, b) + return Option.some(out as any) +}) + +/** + * Replaces the element at the specified index safely with a new value, returning the + * updated array in `Option.some`. + * + * **When to use** + * + * Use to set a fixed replacement value at a specific index. + * + * **Details** + * + * Returns `Option.none()` when the index is out of bounds. + * + * **Example** (Replacing an element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.replace([1, 2, 3], 1, 4)) // Option.some([1, 4, 3]) + * ``` + * + * @see {@link modify} — transform an element with a function + * @see {@link insertAt} — insert without removing + * + * @category elements + * @since 2.0.0 + */ +export const replace: { + (i: number, b: B): = Iterable>( + self: S + ) => Option.Option | B>> + = Iterable>( + self: S, + i: number, + b: B + ): Option.Option | B>> +} = dual( + 3, + (self: Iterable, i: number, b: B): Option.Option> => modify(self, i, () => b) +) + +/** + * Applies a function to the element at the specified index safely, returning the + * updated array in `Option.some`. + * + * **When to use** + * + * Use to derive a replacement value from an array element at a specific index + * while leaving the other elements unchanged. + * + * **Details** + * + * Returns `Option.none()` when the index is out of bounds. + * + * **Example** (Modifying an element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.modify([1, 2, 3, 4], 2, (n) => n * 2)) // Option.some([1, 2, 6, 4]) + * console.log(Array.modify([1, 2, 3, 4], 5, (n) => n * 2)) // Option.none() + * ``` + * + * @see {@link replace} — set a fixed value at an index + * @see {@link modifyHeadNonEmpty} — modify the first element + * @see {@link modifyLastNonEmpty} — modify the last element + * + * @category elements + * @since 2.0.0 + */ +export const modify: { + = Iterable>( + i: number, + f: (a: ReadonlyArray.Infer) => B + ): (self: S) => Option.Option | B>> + = Iterable>( + self: S, + i: number, + f: (a: ReadonlyArray.Infer) => B + ): Option.Option | B>> +} = dual(3, (self: Iterable, i: number, f: (a: A) => B): Option.Option> => { + const arr = Array.from(self) + if (isOutOfBounds(i, arr)) { + return Option.none() + } + const out: Array = arr + const b = f(arr[i]) + out[i] = b + return Option.some(out) +}) + +/** + * Removes the element at the specified index, returning a new array. If the + * index is out of bounds, returns a copy of the original. + * + * **When to use** + * + * Use when you want a missing index to be a no-op and need a fresh array result + * instead of an optional failure. + * + * **Example** (Removing an element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.remove([1, 2, 3, 4], 2)) // [1, 2, 4] + * console.log(Array.remove([1, 2, 3, 4], 5)) // [1, 2, 3, 4] + * ``` + * + * @see {@link insertAt} — insert an element + * @see {@link filter} — remove elements by predicate + * + * @category elements + * @since 2.0.0 + */ +export const remove: { + (i: number): (self: Iterable) => Array + (self: Iterable, i: number): Array +} = dual(2, (self: Iterable, i: number): Array => { + const out = Array.from(self) + if (isOutOfBounds(i, out)) { + return out + } + out.splice(i, 1) + return out +}) + +/** + * Reverses an iterable into a new array. + * + * **When to use** + * + * Use to reverse an iterable into a new array without mutating the original + * input. + * + * **Details** + * + * Preserves `NonEmptyArray` in the return type. + * + * **Example** (Reversing an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.reverse([1, 2, 3, 4])) // [4, 3, 2, 1] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const reverse = >( + self: S +): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => + Array.from(self).reverse() as any + +/** + * Sorts an array by the given `Order`, returning a new array. + * + * **When to use** + * + * Use to sort an array using a single `Order` comparator. + * + * **Details** + * + * Preserves `NonEmptyArray` in the return type. Use `sortWith` to sort by a + * derived key, or `sortBy` for multi-key sorting. + * + * **Example** (Sorting numbers) + * + * ```ts + * import { Array, Order } from "effect" + * + * console.log(Array.sort([3, 1, 4, 1, 5], Order.Number)) // [1, 1, 3, 4, 5] + * ``` + * + * @see {@link sortWith} — sort by a mapping function + * @see {@link sortBy} — sort by multiple orders + * + * @category sorting + * @since 2.0.0 + */ +export const sort: { + ( + O: Order.Order + ): >(self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, O: Order.Order): NonEmptyArray + (self: Iterable, O: Order.Order): Array +} = dual(2, (self: Iterable, O: Order.Order): Array => { + const out = Array.from(self) + out.sort(O) + return out +}) + +/** + * Sorts an array by a derived key using a mapping function and an `Order` for + * that key. + * + * **When to use** + * + * Use when you need to sort values by a derived key, such as a string length or + * object field, while keeping the original values. + * + * **Details** + * + * Equivalent to `sort(Order.mapInput(order, f))`, but more convenient. + * + * **Example** (Sorting strings by length) + * + * ```ts + * import { Array, Order } from "effect" + * + * console.log(Array.sortWith(["aaa", "b", "cc"], (s) => s.length, Order.Number)) + * // ["b", "cc", "aaa"] + * ``` + * + * @see {@link sort} for sorting with an `Order` that compares the elements directly + * @see {@link sortBy} for sorting with multiple `Order`s applied in sequence + * + * @category elements + * @since 2.0.0 + */ +export const sortWith: { + , B>( + f: (a: ReadonlyArray.Infer) => B, + order: Order.Order + ): (self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, f: (a: A) => B, O: Order.Order): NonEmptyArray + (self: Iterable, f: (a: A) => B, order: Order.Order): Array +} = dual( + 3, + (self: Iterable, f: (a: A) => B, order: Order.Order): Array => + Array.from(self).map((a) => [a, f(a)] as const).sort(([, a], [, b]) => order(a, b)).map(([_]) => _) +) + +/** + * Sorts an array by multiple `Order`s applied in sequence: the first order is + * used first; ties are broken by the second order, and so on. + * + * **When to use** + * + * Use to sort by multiple criteria where later orders break ties from earlier + * ones. + * + * **Details** + * + * This is data-last only and returns a function. The return type preserves + * `NonEmptyArray`. + * + * **Example** (Sorting by multiple keys) + * + * ```ts + * import { Array, Order, pipe } from "effect" + * + * const users = [ + * { name: "Alice", age: 30 }, + * { name: "Bob", age: 25 }, + * { name: "Charlie", age: 30 } + * ] + * + * const result = pipe( + * users, + * Array.sortBy( + * Order.mapInput(Order.Number, (user: (typeof users)[number]) => user.age), + * Order.mapInput(Order.String, (user: (typeof users)[number]) => user.name) + * ) + * ) + * console.log(result) + * // [{ name: "Bob", age: 25 }, { name: "Alice", age: 30 }, { name: "Charlie", age: 30 }] + * ``` + * + * @see {@link sort} — sort by a single `Order` + * @see {@link sortWith} — sort by a derived key + * + * @category sorting + * @since 2.0.0 + */ +export const sortBy = >( + ...orders: ReadonlyArray>> +) => { + const sortByAll = sort(Order.combineAll(orders)) + return ( + self: S + ): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + return sortByAll(input) as any + } + return [] as any + } +} + +/** + * Pairs elements from two iterables by position. If the iterables differ in + * length, the extra elements from the longer one are discarded. + * + * **When to use** + * + * Use when you need simple pairs of corresponding elements from two iterables. + * + * **Details** + * + * Returns `NonEmptyArray` when both inputs are non-empty. + * + * **Example** (Zipping two arrays) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.zip([1, 2, 3], ["a", "b"])) // [[1, "a"], [2, "b"]] + * ``` + * + * @see {@link zipWith} — zip with a combiner function + * @see {@link unzip} — inverse operation + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: NonEmptyReadonlyArray): (self: NonEmptyReadonlyArray) => NonEmptyArray<[A, B]> + (that: Iterable): (self: Iterable) => Array<[A, B]> + (self: NonEmptyReadonlyArray, that: NonEmptyReadonlyArray): NonEmptyArray<[A, B]> + (self: Iterable, that: Iterable): Array<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Array<[A, B]> => zipWith(self, that, Tuple.make) +) + +/** + * Combines elements from two iterables pairwise using a function. If the + * iterables differ in length, extra elements are discarded. + * + * **When to use** + * + * Use when zipping two iterables in an array pipeline and each pair should + * become a computed array element instead of a tuple. + * + * **Example** (Zipping with addition) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.zipWith([1, 2, 3], [4, 5, 6], (a, b) => a + b)) // [5, 7, 9] + * ``` + * + * @see {@link zip} — zip into tuples + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: NonEmptyReadonlyArray, f: (a: A, b: B) => C): (self: NonEmptyReadonlyArray) => NonEmptyArray + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Array + (self: NonEmptyReadonlyArray, that: NonEmptyReadonlyArray, f: (a: A, b: B) => C): NonEmptyArray + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Array +} = dual(3, (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Array => { + const as = fromIterable(self) + const bs = fromIterable(that) + if (isReadonlyArrayNonEmpty(as) && isReadonlyArrayNonEmpty(bs)) { + const out: NonEmptyArray = [f(headNonEmpty(as), headNonEmpty(bs))] + const len = Math.min(as.length, bs.length) + for (let i = 1; i < len; i++) { + out[i] = f(as[i], bs[i]) + } + return out + } + return [] +}) + +/** + * Splits an array of pairs into two arrays. Inverse of {@link zip}. + * + * **Example** (Unzipping pairs) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.unzip([[1, "a"], [2, "b"], [3, "c"]])) // [[1, 2, 3], ["a", "b", "c"]] + * ``` + * + * @see {@link zip} — combine two arrays into pairs + * + * @category zipping + * @since 2.0.0 + */ +export const unzip: >( + self: S +) => S extends NonEmptyReadonlyArray ? [NonEmptyArray, NonEmptyArray] + : S extends Iterable ? [Array, Array] + : never = ((self: Iterable): [Array, Array] => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + const fa: NonEmptyArray = [input[0][0]] + const fb: NonEmptyArray = [input[0][1]] + for (let i = 1; i < input.length; i++) { + fa[i] = input[i][0] + fb[i] = input[i][1] + } + return [fa, fb] + } + return [[], []] + }) as any + +/** + * Places a separator element between every pair of elements. + * + * **When to use** + * + * Use to insert a separator between elements, for example when preparing data for display or concatenation. + * + * **Details** + * + * The return type preserves `NonEmptyArray`. Empty inputs produce an empty + * result. + * + * **Example** (Interspersing a separator) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.intersperse([1, 2, 3], 0)) // [1, 0, 2, 0, 3] + * ``` + * + * @see {@link join} — intersperse and join into a string + * + * @category elements + * @since 2.0.0 + */ +export const intersperse: { + ( + middle: B + ): >(self: S) => ReadonlyArray.With | B> + (self: NonEmptyReadonlyArray, middle: B): NonEmptyArray + (self: Iterable, middle: B): Array +} = dual(2, (self: Iterable, middle: B): Array => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + const out: NonEmptyArray = [headNonEmpty(input)] + const tail = tailNonEmpty(input) + for (let i = 0; i < tail.length; i++) { + if (i < tail.length) { + out.push(middle) + } + out.push(tail[i]) + } + return out + } + return [] +}) + +/** + * Applies a function to the first element of a non-empty array, returning a + * new array. + * + * **When to use** + * + * Use to transform the first element of a non-empty array while preserving the rest. + * + * **Example** (Modifying the head) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.modifyHeadNonEmpty([1, 2, 3], (n) => n * 10)) // [10, 2, 3] + * ``` + * + * @see {@link setHeadNonEmpty} — replace with a fixed value + * @see {@link modifyLastNonEmpty} — modify the last element + * + * @category elements + * @since 4.0.0 + */ +export const modifyHeadNonEmpty: { + (f: (a: A) => B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray +} = dual( + 2, + ( + self: NonEmptyReadonlyArray, + f: (a: A) => B + ): NonEmptyArray => [f(headNonEmpty(self)), ...tailNonEmpty(self)] +) + +/** + * Replaces the first element of a non-empty array with a new value. + * + * **When to use** + * + * Use when you already know the array is non-empty and the replacement value + * does not depend on the current first element. + * + * **Example** (Setting the head) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.setHeadNonEmpty([1, 2, 3], 10)) // [10, 2, 3] + * ``` + * + * @see {@link modifyHeadNonEmpty} — transform the head with a function + * @see {@link setLastNonEmpty} — replace the last element + * + * @category elements + * @since 4.0.0 + */ +export const setHeadNonEmpty: { + (b: B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray => modifyHeadNonEmpty(self, () => b) +) + +/** + * Applies a function to the last element of a non-empty array, returning a + * new array. + * + * **When to use** + * + * Use when you already know the array is non-empty and the new last element + * depends on the current last element. + * + * **Example** (Modifying the last element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.modifyLastNonEmpty([1, 2, 3], (n) => n * 2)) // [1, 2, 6] + * ``` + * + * @see {@link setLastNonEmpty} — replace with a fixed value + * @see {@link modifyHeadNonEmpty} — modify the first element + * + * @category elements + * @since 4.0.0 + */ +export const modifyLastNonEmpty: { + (f: (a: A) => B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, f: (a: A) => B): NonEmptyArray => + append(initNonEmpty(self), f(lastNonEmpty(self))) +) + +/** + * Replaces the last element of a non-empty array with a new value. + * + * **When to use** + * + * Use when you already know the array is non-empty and the replacement value + * does not depend on the current last element. + * + * **Example** (Setting the last element) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.setLastNonEmpty([1, 2, 3], 4)) // [1, 2, 4] + * ``` + * + * @see {@link modifyLastNonEmpty} — transform the last element with a function + * @see {@link setHeadNonEmpty} — replace the first element + * + * @category elements + * @since 4.0.0 + */ +export const setLastNonEmpty: { + (b: B): (self: NonEmptyReadonlyArray) => NonEmptyArray + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray +} = dual( + 2, + (self: NonEmptyReadonlyArray, b: B): NonEmptyArray => modifyLastNonEmpty(self, () => b) +) + +/** + * Transforms an array by rotating it `n` steps. Positive `n` rotates right; negative `n` + * rotates left. + * + * **When to use** + * + * Use when elements should wrap around the end of the array rather than being + * dropped. + * + * **Details** + * + * `n` is rounded to the nearest integer before rotating. The return type + * preserves `NonEmptyArray`. Empty arrays, or rotations normalized to `0`, + * return a copy. + * + * **Example** (Rotating elements) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.rotate(["a", "b", "c", "d"], 2)) // ["c", "d", "a", "b"] + * ``` + * + * @see {@link take} for taking a fixed number of elements from the start + * @see {@link drop} for dropping a fixed number of elements from the start + * + * @category elements + * @since 2.0.0 + */ +export const rotate: { + (n: number): >(self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, n: number): NonEmptyArray + (self: Iterable, n: number): Array +} = dual(2, (self: Iterable, n: number): Array => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + const len = input.length + const m = Math.round(n) % len + if (isOutOfBounds(Math.abs(m), input) || m === 0) { + return copy(input) + } + if (m < 0) { + const [f, s] = splitAtNonEmpty(input, -m) + return appendAll(s, f) + } else { + return rotate(input, m - len) + } + } + return [] +}) + +/** + * Returns a membership-test function using a custom equivalence. + * + * **When to use** + * + * Use when checking membership with caller-provided equality instead of + * `Equal.equivalence()`. + * + * **Example** (Checking with custom equality) + * + * ```ts + * import { Array, pipe } from "effect" + * + * const containsNumber = Array.containsWith((a: number, b: number) => a === b) + * console.log(pipe([1, 2, 3, 4], containsNumber(3))) // true + * ``` + * + * @see {@link contains} for the `Equal.equivalence()` variant + * + * @category elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} => + dual(2, (self: Iterable, a: A): boolean => { + for (const i of self) { + if (isEquivalent(a, i)) { + return true + } + } + return false + }) + +/** + * Checks whether an array contains a value, using `Equal.equivalence()` for + * comparison. + * + * **When to use** + * + * Use to check whether an iterable contains a value using Effect's default + * equality instead of providing a comparison function. + * + * **Example** (Checking membership) + * + * ```ts + * import { Array, pipe } from "effect" + * + * console.log(pipe(["a", "b", "c", "d"], Array.contains("c"))) // true + * ``` + * + * @see {@link containsWith} — use custom equality + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} = containsWith(Equal.asEquivalence()) + +/** + * Applies a function repeatedly to consume prefixes of the array and collect + * the values it produces. + * + * **When to use** + * + * Use when you need custom grouping logic where each step returns both a value + * and the remaining input. + * + * **Details** + * + * The function receives a `NonEmptyReadonlyArray` and returns `[value, rest]`. + * Processing continues until the remaining array is empty. + * + * **Example** (Chopping an array) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.chop( + * [1, 2, 3, 4, 5], + * (as): [number, Array] => [as[0] * 2, as.slice(1)] + * ) + * console.log(result) // [2, 4, 6, 8, 10] + * ``` + * + * @see {@link chunksOf} — split into fixed-size chunks + * @see {@link splitAt} — split at an index + * + * @category elements + * @since 2.0.0 + */ +export const chop: { + , B>( + f: (as: NonEmptyReadonlyArray>) => readonly [B, ReadonlyArray>] + ): (self: S) => ReadonlyArray.With> + ( + self: NonEmptyReadonlyArray, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] + ): NonEmptyArray + ( + self: Iterable, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] + ): Array +} = dual(2, ( + self: Iterable, + f: (as: NonEmptyReadonlyArray) => readonly [B, ReadonlyArray] +): Array => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + const [b, rest] = f(input) + const out: NonEmptyArray = [b] + let next: ReadonlyArray = rest + while (internalArray.isArrayNonEmpty(next)) { + const [b, rest] = f(next) + out.push(b) + next = rest + } + return out + } + return [] +}) + +/** + * Splits an iterable into two arrays at the given index. + * + * **When to use** + * + * Use to divide an array into a prefix and suffix at a specific position. + * + * **Details** + * + * `n` can be `0`, in which case all elements are placed in the second array. + * The index is floored to an integer. + * + * **Example** (Splitting at an index) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.splitAt([1, 2, 3, 4, 5], 3)) // [[1, 2, 3], [4, 5]] + * ``` + * + * @see {@link splitAtNonEmpty} — for non-empty arrays + * @see {@link splitWhere} — split at a predicate boundary + * + * @category splitting + * @since 2.0.0 + */ +export const splitAt: { + (n: number): (self: Iterable) => [beforeIndex: Array, fromIndex: Array] + (self: Iterable, n: number): [beforeIndex: Array, fromIndex: Array] +} = dual(2, (self: Iterable, n: number): [Array, Array] => { + const input = Array.from(self) + const _n = Math.floor(n) + if (isReadonlyArrayNonEmpty(input)) { + if (_n >= 1) { + return splitAtNonEmpty(input, _n) + } + return [[], input] + } + return [input, []] +}) + +/** + * Splits a non-empty array into two parts at the given index. The first part + * is guaranteed to be non-empty (`n` is clamped to >= 1). + * + * **When to use** + * + * Use when downstream code requires the left side of the split to contain at + * least one element. + * + * **Example** (Splitting a non-empty array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.splitAtNonEmpty(["a", "b", "c", "d", "e"], 3)) + * // [["a", "b", "c"], ["d", "e"]] + * ``` + * + * @see {@link splitAt} — for possibly-empty arrays + * + * @category splitting + * @since 4.0.0 + */ +export const splitAtNonEmpty: { + (n: number): (self: NonEmptyReadonlyArray) => [beforeIndex: NonEmptyArray, fromIndex: Array] + (self: NonEmptyReadonlyArray, n: number): [beforeIndex: NonEmptyArray, fromIndex: Array] +} = dual(2, (self: NonEmptyReadonlyArray, n: number): [NonEmptyArray, Array] => { + const _n = Math.max(1, Math.floor(n)) + return _n >= self.length ? + [copy(self), []] : + [prepend(self.slice(1, _n), headNonEmpty(self)), self.slice(_n)] +}) + +/** + * Splits an iterable into `n` roughly equal-sized chunks. + * + * **When to use** + * + * Use to distribute elements across a fixed number of groups, such as when splitting work across threads. + * + * **Details** + * + * Uses `chunksOf(ceil(length / n))` internally. The last chunk may be shorter. + * + * **Example** (Splitting into groups) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.split([1, 2, 3, 4, 5, 6, 7, 8], 3)) // [[1, 2, 3], [4, 5, 6], [7, 8]] + * ``` + * + * @see {@link chunksOf} — split into fixed-size chunks + * + * @category splitting + * @since 2.0.0 + */ +export const split: { + (n: number): (self: Iterable) => Array> + (self: Iterable, n: number): Array> +} = dual(2, (self: Iterable, n: number) => { + const input = fromIterable(self) + return chunksOf(input, Math.ceil(input.length / Math.floor(n))) +}) + +/** + * Splits an iterable at the first element matching the predicate. The matching + * element is included in the second array. + * + * **When to use** + * + * Use when you need to split an array at the first element that marks a + * condition boundary. + * + * **Example** (Splitting at a condition) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.splitWhere([1, 2, 3, 4, 5], (n) => n > 3)) // [[1, 2, 3], [4, 5]] + * ``` + * + * @see {@link span} — splits at the first element that fails the predicate + * @see {@link splitAt} — split at a fixed index + * + * @category splitting + * @since 2.0.0 + */ +export const splitWhere: { + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: Iterable) => [beforeMatch: Array, fromMatch: Array] + (self: Iterable, predicate: (a: A, i: number) => boolean): [beforeMatch: Array, fromMatch: Array] +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): [beforeMatch: Array, fromMatch: Array] => + span(self, (a: A, i: number) => !predicate(a, i)) +) + +/** + * Creates a shallow copy of an array. + * + * **When to use** + * + * Use to create a distinct array reference for an existing array, for example + * before mutating the returned array. + * + * **Details** + * + * The return type preserves `NonEmptyArray`. Use this when you need a distinct + * reference, for example before mutating the returned array. + * + * **Example** (Copying an array) + * + * ```ts + * import { Array } from "effect" + * + * const original = [1, 2, 3] + * const copied = Array.copy(original) + * console.log(copied) // [1, 2, 3] + * console.log(original === copied) // false + * ``` + * + * @see {@link fromIterable} — returns the same reference for arrays + * + * @category elements + * @since 2.0.0 + */ +export const copy: { + (self: NonEmptyReadonlyArray): NonEmptyArray + (self: ReadonlyArray): Array +} = ((self: ReadonlyArray): Array => self.slice()) as any + +/** + * Pads or truncates an array to exactly `n` elements, filling with `fill` + * if the array is shorter, or slicing if longer. + * + * **When to use** + * + * Use to ensure an array has a specific length, padding with a fill value or truncating as needed. + * + * **Details** + * + * Returns an empty array when `n <= 0`. + * + * **Example** (Padding an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.pad([1, 2, 3], 6, 0)) // [1, 2, 3, 0, 0, 0] + * ``` + * + * @see {@link take} — truncate without padding + * @see {@link replicate} — create an array of a single repeated value + * + * @category elements + * @since 3.8.4 + */ +export const pad: { + ( + n: number, + fill: T + ): ( + self: Array + ) => Array + (self: Array, n: number, fill: T): Array +} = dual(3, (self: Array, n: number, fill: T): Array => { + if (self.length >= n) { + return take(self, n) + } + return appendAll( + self, + makeBy(n - self.length, () => fill) + ) +}) + +/** + * Splits an iterable into chunks of length `n`. The last chunk may be shorter + * if `n` does not evenly divide the length. + * + * **When to use** + * + * Use to divide an iterable into a new array of non-overlapping chunks with a + * maximum chunk size. + * + * **Details** + * + * `chunksOf(n)([])` is `[]`, not `[[]]`. Each chunk is a `NonEmptyArray`, and + * the outer return type preserves `NonEmptyArray`. + * + * **Example** (Chunking an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.chunksOf([1, 2, 3, 4, 5], 2)) // [[1, 2], [3, 4], [5]] + * ``` + * + * @see {@link split} — split into a given number of groups + * @see {@link window} — sliding windows + * + * @category splitting + * @since 2.0.0 + */ +export const chunksOf: { + ( + n: number + ): >( + self: S + ) => ReadonlyArray.With>> + (self: NonEmptyReadonlyArray, n: number): NonEmptyArray> + (self: Iterable, n: number): Array> +} = dual(2, (self: Iterable, n: number): Array> => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + return chop(input, splitAtNonEmpty(n)) + } + return [] +}) + +/** + * Creates overlapping sliding windows of size `n`. + * + * **When to use** + * + * Use to process sequences with a moving window, such as for computing running averages or detecting patterns. + * + * **Details** + * + * Returns an empty array if `n <= 0` or the array has fewer than `n` elements. + * Each window is a tuple of exactly `n` elements. + * + * **Example** (Creating sliding windows) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.window([1, 2, 3, 4, 5], 3)) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]] + * console.log(Array.window([1, 2, 3, 4, 5], 6)) // [] + * ``` + * + * @see {@link chunksOf} — non-overlapping chunks + * + * @category splitting + * @since 3.13.2 + */ +export const window: { + (n: N): (self: Iterable) => Array> + (self: Iterable, n: N): Array> +} = dual(2, (self: Iterable, n: number): Array> => { + const input = fromIterable(self) + if (n > 0 && isReadonlyArrayNonEmpty(input)) { + return Array.from( + { length: input.length - (n - 1) }, + (_, index) => input.slice(index, index + n) + ) + } + return [] +}) + +/** + * Groups consecutive equal elements using a custom equivalence function. + * + * **When to use** + * + * Use when you already have a non-empty array arranged so matching elements are + * adjacent and need a custom equivalence function. + * + * **Details** + * + * Only adjacent elements are grouped. Non-adjacent duplicates stay separate. + * Requires a `NonEmptyReadonlyArray`. + * + * **Example** (Grouping consecutive equal elements) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.groupWith(["a", "a", "b", "b", "b", "c", "a"], (x, y) => x === y)) + * // [["a", "a"], ["b", "b", "b"], ["c"], ["a"]] + * ``` + * + * @see {@link group} for grouping adjacent elements with `Equal.equivalence()` + * @see {@link groupBy} for grouping all elements into a record by key, regardless of adjacency + * + * @category grouping + * @since 2.0.0 + */ +export const groupWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: NonEmptyReadonlyArray) => NonEmptyArray> + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray> +} = dual( + 2, + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray> => + chop(self, (as) => { + const h = headNonEmpty(as) + const out: NonEmptyArray = [h] + let i = 1 + for (; i < as.length; i++) { + const a = as[i] + if (isEquivalent(a, h)) { + out.push(a) + } else { + break + } + } + return [out, as.slice(i)] + }) +) + +/** + * Groups consecutive equal elements using `Equal.equivalence()`. + * + * **When to use** + * + * Use when you already have adjacent equal values and Effect's default equality + * is the right comparison. + * + * **Details** + * + * Only adjacent elements are grouped. + * + * **Example** (Grouping adjacent equal elements) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.group([1, 1, 2, 2, 2, 3, 1])) // [[1, 1], [2, 2, 2], [3], [1]] + * ``` + * + * @see {@link groupWith} — use custom equality + * @see {@link groupBy} — group by a key function into a record + * + * @category grouping + * @since 2.0.0 + */ +export const group: (self: NonEmptyReadonlyArray) => NonEmptyArray> = groupWith( + Equal.asEquivalence() +) + +/** + * Groups elements into a record by a key-returning function. Each key maps + * to a `NonEmptyArray` of elements that produced that key. + * + * **When to use** + * + * Use to build buckets of elements indexed by a computed string or symbol key. + * + * **Details** + * + * Unlike `group` and `groupWith`, elements do not need to be adjacent to be + * grouped together. The key function must return a `string` or `symbol`. + * + * **Example** (Grouping by a property) + * + * ```ts + * import { Array } from "effect" + * + * const people = [ + * { name: "Alice", group: "A" }, + * { name: "Bob", group: "B" }, + * { name: "Charlie", group: "A" } + * ] + * + * const result = Array.groupBy(people, (person) => person.group) + * console.log(result) + * // { A: [{ name: "Alice", group: "A" }, { name: "Charlie", group: "A" }], B: [{ name: "Bob", group: "B" }] } + * ``` + * + * @see {@link group} — group adjacent equal elements + * @see {@link groupWith} — group adjacent elements by custom equality + * + * @category grouping + * @since 2.0.0 + */ +export const groupBy: { + ( + f: (a: A) => K + ): (self: Iterable) => Record, NonEmptyArray> + ( + self: Iterable, + f: (a: A) => K + ): Record, NonEmptyArray> +} = dual(2, ( + self: Iterable, + f: (a: A) => K +): Record, NonEmptyArray> => { + const out: Record> = {} + for (const a of self) { + const k = f(a) + if (Object.hasOwn(out, k)) { + out[k].push(a) + } else { + out[k] = [a] + } + } + return out +}) + +/** + * Computes the union of two arrays using a custom equivalence, removing + * duplicates. + * + * **When to use** + * + * Use when you need the union of two arrays but duplicate detection must use a + * custom equivalence instead of the default `Equal.equivalence()`. + * + * **Example** (Computing unions with custom equality) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.unionWith([1, 2], [2, 3], (a, b) => a === b)) // [1, 2, 3] + * ``` + * + * @see {@link union} for the `Equal.equivalence()` variant + * @see {@link intersectionWith} for keeping elements present in both arrays + * @see {@link differenceWith} for keeping elements present only in the first array + * + * @category elements + * @since 2.0.0 + */ +export const unionWith: { + , T extends Iterable>( + that: T, + isEquivalent: (self: ReadonlyArray.Infer, that: ReadonlyArray.Infer) => boolean + ): (self: S) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + ( + self: NonEmptyReadonlyArray, + that: Iterable, + isEquivalent: (self: A, that: B) => boolean + ): NonEmptyArray + ( + self: Iterable, + that: NonEmptyReadonlyArray, + isEquivalent: (self: A, that: B) => boolean + ): NonEmptyArray + (self: Iterable, that: Iterable, isEquivalent: (self: A, that: B) => boolean): Array +} = dual(3, (self: Iterable, that: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const a = fromIterable(self) + const b = fromIterable(that) + if (isReadonlyArrayNonEmpty(a)) { + if (isReadonlyArrayNonEmpty(b)) { + const dedupe = dedupeWith(isEquivalent) + return dedupe(appendAll(a, b)) + } + return a + } + return b +}) + +/** + * Computes the union of two arrays, removing duplicates using + * `Equal.equivalence()`. + * + * **Example** (Computing array unions) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.union([1, 2], [2, 3])) // [1, 2, 3] + * ``` + * + * @see {@link unionWith} — use custom equality + * @see {@link intersection} — elements in both arrays + * @see {@link difference} — elements only in the first array + * + * @category elements + * @since 2.0.0 + */ +export const union: { + >( + that: T + ): >( + self: S + ) => ReadonlyArray.OrNonEmpty | ReadonlyArray.Infer> + (self: NonEmptyReadonlyArray, that: ReadonlyArray): NonEmptyArray + (self: ReadonlyArray, that: NonEmptyReadonlyArray): NonEmptyArray + (self: Iterable, that: Iterable): Array +} = dual( + 2, + (self: Iterable, that: Iterable): Array => unionWith(self, that, Equal.asEquivalence()) +) + +/** + * Computes the intersection of two arrays using a custom equivalence. Order is + * determined by the first array. + * + * **When to use** + * + * Use when you need to keep only values present in both arrays and equality + * must be defined by a custom comparator, such as matching objects by id. + * + * **Example** (Computing intersections with custom equality) + * + * ```ts + * import { Array } from "effect" + * + * const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }] + * const array2 = [{ id: 3 }, { id: 4 }, { id: 1 }] + * const isEquivalent = (a: { id: number }, b: { id: number }) => a.id === b.id + * console.log(Array.intersectionWith(isEquivalent)(array2)(array1)) // [{ id: 1 }, { id: 3 }] + * ``` + * + * @see {@link intersection} for the `Equal.equivalence()` variant + * @see {@link unionWith} for keeping values from either array with custom equality + * @see {@link differenceWith} for keeping values only from the first array with custom equality + * + * @category elements + * @since 2.0.0 + */ +export const intersectionWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} => { + const has = containsWith(isEquivalent) + return dual( + 2, + (self: Iterable, that: Iterable): Array => { + const thatArray = fromIterable(that) + return fromIterable(self).filter((a) => has(thatArray, a)) + } + ) +} + +/** + * Computes the intersection of two arrays using `Equal.equivalence()`. Order is + * determined by the first array. + * + * **When to use** + * + * Use when Effect equality is the right membership test and you want to keep + * values present in both inputs while preserving the first input's order. + * + * **Example** (Computing array intersections) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.intersection([1, 2, 3], [3, 4, 1])) // [1, 3] + * ``` + * + * @see {@link intersectionWith} — use custom equality + * @see {@link union} — elements in either array + * @see {@link difference} — elements only in the first array + * + * @category elements + * @since 2.0.0 + */ +export const intersection: { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} = intersectionWith(Equal.asEquivalence()) + +/** + * Computes elements in the first array that are not in the second, using a + * custom equivalence. + * + * **When to use** + * + * Use when you need to keep only values from the first array and equality must + * be defined by a custom comparator, such as matching objects by id. + * + * **Example** (Computing differences with custom equality) + * + * ```ts + * import { Array } from "effect" + * + * const diff = Array.differenceWith((a, b) => a === b)([1, 2, 3], [2, 3, 4]) + * console.log(diff) // [1] + * ``` + * + * @see {@link difference} for the `Equal.equivalence()` variant + * @see {@link unionWith} for keeping values from either array with custom equality + * @see {@link intersectionWith} for keeping values present in both arrays with custom equality + * + * @category elements + * @since 2.0.0 + */ +export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} => { + const has = containsWith(isEquivalent) + return dual( + 2, + (self: Iterable, that: Iterable): Array => { + const thatArray = fromIterable(that) + return fromIterable(self).filter((a) => !has(thatArray, a)) + } + ) +} + +/** + * Computes elements in the first array that are not in the second, using + * `Equal.equivalence()`. + * + * **When to use** + * + * Use when you need to keep values from the first array that are absent from + * the second and the default `Equal.equivalence()` comparison is appropriate. + * + * **Example** (Computing array differences) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.difference([1, 2, 3], [2, 3, 4])) // [1] + * ``` + * + * @see {@link differenceWith} — use custom equality + * @see {@link union} — elements in either array + * @see {@link intersection} — elements in both arrays + * + * @category elements + * @since 2.0.0 + */ +export const difference: { + (that: Iterable): (self: Iterable) => Array + (self: Iterable, that: Iterable): Array +} = differenceWith(Equal.asEquivalence()) + +/** + * Creates an empty array. + * + * **When to use** + * + * Use to create a typed empty array without allocating placeholder elements. + * + * **Example** (Creating an empty array) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.empty() + * console.log(result) // [] + * ``` + * + * @see {@link of} — create a single-element array + * @see {@link make} — create from multiple values + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => Array = () => [] + +/** + * Wraps a single value in a `NonEmptyArray`. + * + * **Example** (Creating a single-element array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.of(1)) // [1] + * ``` + * + * @see {@link make} — create from multiple values + * @see {@link empty} — create an empty array + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): NonEmptyArray => [a] + +/** + * Utility types for working with `ReadonlyArray` at the type level. Use these + * to infer element types, preserve non-emptiness, and flatten nested arrays. + * + * @since 2.0.0 + */ +export declare namespace ReadonlyArray { + /** + * Infers the element type of an iterable. + * + * **Example** (Inferring an element type) + * + * ```ts + * import type { Array } from "effect" + * + * type StringArrayType = Array.ReadonlyArray.Infer> + * // StringArrayType is string + * ``` + * + * @category types + * @since 2.0.0 + */ + export type Infer> = S extends ReadonlyArray ? A + : S extends Iterable ? A + : never + + /** + * Constructs an array type preserving non-emptiness. + * + * **Example** (Preserving non-emptiness) + * + * ```ts + * import type { Array } from "effect" + * + * type Result = Array.ReadonlyArray.With + * // Result is NonEmptyArray + * ``` + * + * @category types + * @since 2.0.0 + */ + export type With, A> = S extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + + /** + * Creates a non-empty array if either input is non-empty. + * + * **Example** (Preserving non-emptiness from either input) + * + * ```ts + * import type { Array } from "effect" + * + * type Result = Array.ReadonlyArray.OrNonEmpty< + * readonly [number], + * ReadonlyArray, + * number + * > + * // Result is NonEmptyArray + * ``` + * + * @category types + * @since 2.0.0 + */ + export type OrNonEmpty< + S extends Iterable, + T extends Iterable, + A + > = S extends NonEmptyReadonlyArray ? NonEmptyArray + : T extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + + /** + * Creates a non-empty array only if both inputs are non-empty. + * + * **Example** (Preserving non-emptiness from both inputs) + * + * ```ts + * import type { Array } from "effect" + * + * type Result = Array.ReadonlyArray.AndNonEmpty< + * readonly [number], + * readonly [string], + * boolean + * > + * // Result is NonEmptyArray + * ``` + * + * @category types + * @since 2.0.0 + */ + export type AndNonEmpty< + S extends Iterable, + T extends Iterable, + A + > = S extends NonEmptyReadonlyArray ? T extends NonEmptyReadonlyArray ? NonEmptyArray + : Array + : Array + + /** + * Flattens a nested array type. + * + * **Example** (Flattening nested array types) + * + * ```ts + * import type { Array } from "effect" + * + * type Nested = ReadonlyArray> + * type Flattened = Array.ReadonlyArray.Flatten + * // Flattened is Array + * ``` + * + * @category types + * @since 2.0.0 + */ + export type Flatten>> = T extends + NonEmptyReadonlyArray> ? NonEmptyArray + : Array +} + +/** + * Transforms each element using a function, returning a new array. + * + * **When to use** + * + * Use to transform each element independently while preserving the array shape. + * + * **Details** + * + * The function receives `(element, index)`. The return type preserves + * `NonEmptyArray`. + * + * **Example** (Doubling values) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.map([1, 2, 3], (x) => x * 2)) // [2, 4, 6] + * ``` + * + * @see {@link flatMap} — map and flatten + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + , B>( + f: (a: ReadonlyArray.Infer, i: number) => B + ): (self: S) => ReadonlyArray.With + , B>(self: S, f: (a: ReadonlyArray.Infer, i: number) => B): ReadonlyArray.With +} = dual(2, (self: ReadonlyArray, f: (a: A, i: number) => B): Array => self.map(f)) + +/** + * Maps each element to an array and flattens the results into a single array. + * + * **When to use** + * + * Use to map each array element to zero or more values and concatenate the + * results in one pass. + * + * **Details** + * + * The function receives `(element, index)`. This returns `NonEmptyArray` when + * both the input and mapped arrays are non-empty. + * + * **Example** (Flat mapping an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.flatMap([1, 2, 3], (x) => [x, x * 2])) // [1, 2, 2, 4, 3, 6] + * ``` + * + * @see {@link map} — transform without flattening + * @see {@link flatten} — flatten without mapping + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + , T extends ReadonlyArray>( + f: (a: ReadonlyArray.Infer, i: number) => T + ): (self: S) => ReadonlyArray.AndNonEmpty> + (self: NonEmptyReadonlyArray, f: (a: A, i: number) => NonEmptyReadonlyArray): NonEmptyArray + (self: ReadonlyArray, f: (a: A, i: number) => ReadonlyArray): Array +} = dual( + 2, + (self: ReadonlyArray, f: (a: A, i: number) => ReadonlyArray): Array => { + if (isReadonlyArrayEmpty(self)) { + return [] + } + const out: Array = [] + for (let i = 0; i < self.length; i++) { + const inner = f(self[i], i) + for (let j = 0; j < inner.length; j++) { + out.push(inner[j]) + } + } + return out + } +) + +/** + * Flattens a nested array of arrays into a single array. + * + * **When to use** + * + * Use to collapse one level of nested arrays when no per-element mapping is + * needed. + * + * **Example** (Flattening nested arrays) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.flatten([[1, 2], [], [3, 4], [], [5, 6]])) // [1, 2, 3, 4, 5, 6] + * ``` + * + * @see {@link flatMap} — map then flatten in one step + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten: >>(self: S) => ReadonlyArray.Flatten = + flatMap(identity) as any + +/** + * Extracts all `Some` values from an iterable of `Option`s, discarding `None`s. + * + * **When to use** + * + * Use to collect only present values from an iterable of `Option` values while + * discarding `None` values. + * + * **Example** (Extracting Some values) + * + * ```ts + * import { Array, Option } from "effect" + * + * console.log(Array.getSomes([Option.some(1), Option.none(), Option.some(2)])) // [1, 2] + * ``` + * + * @see {@link fromOption} — convert a single Option + * @see {@link getSuccesses} — extract successes from Results + * + * @category filtering + * @since 2.0.0 + */ + +export const getSomes: >, X = any>( + self: T +) => Array>> = (self: any) => { + const out: Array = [] + for (const a of self) { + if (Option.isSome(a)) { + out.push(a.value) + } + } + return out +} + +/** + * Extracts all failure values from an iterable of `Result`s, discarding + * successes. + * + * **When to use** + * + * Use when you can drop the success channel and only need the failure + * payloads, not the original result wrappers. + * + * **Example** (Extracting failures) + * + * ```ts + * import { Array, Result } from "effect" + * + * console.log(Array.getFailures([Result.succeed(1), Result.fail("err"), Result.succeed(2)])) + * // ["err"] + * ``` + * + * @see {@link getSuccesses} — extract success values + * @see {@link separate} — split into failures and successes + * + * @category filtering + * @since 4.0.0 + */ +export const getFailures = >>( + self: T +): Array>> => { + const out: Array = [] + for (const a of self) { + if (Result.isFailure(a)) { + out.push(a.failure) + } + } + + return out +} + +/** + * Extracts all success values from an iterable of `Result`s, discarding + * failures. + * + * **When to use** + * + * Use when you can drop the failure channel and only need the success + * payloads, not the original result wrappers. + * + * **Example** (Extracting successes) + * + * ```ts + * import { Array, Result } from "effect" + * + * console.log(Array.getSuccesses([Result.succeed(1), Result.fail("err"), Result.succeed(2)])) + * // [1, 2] + * ``` + * + * @see {@link getFailures} — extract failure values + * @see {@link separate} — split into failures and successes + * + * @category filtering + * @since 4.0.0 + */ +export const getSuccesses = >>( + self: T +): Array>> => { + const out: Array = [] + for (const a of self) { + if (Result.isSuccess(a)) { + out.push(a.success) + } + } + + return out +} + +/** + * Keeps transformed values for elements where a `Filter` succeeds. + * + * **When to use** + * + * Use to filter an iterable with a `Result`-returning transformation while + * discarding failures. + * + * **Details** + * + * The filter receives `(element, index)`. Failures are discarded. + * + * **Example** (Filtering and transforming) + * + * ```ts + * import { Array, Result } from "effect" + * + * console.log(Array.filterMap([1, 2, 3, 4], (n) => n % 2 === 0 ? Result.succeed(n * 10) : Result.failVoid)) + * // [20, 40] + * ``` + * + * @see {@link filter} — keep original elements matching a predicate + * @see {@link partition} for keeping both failures and successes + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (input: NoInfer, i: number) => Result.Result): (self: Iterable) => Array + (self: Iterable, f: (input: A, i: number) => Result.Result): Array +} = dual(2, (self: Iterable, f: (input: A, i: number) => Result.Result): Array => { + const as = fromIterable(self) + const out: Array = [] + for (let i = 0; i < as.length; i++) { + const result = f(as[i], i) + if (Result.isSuccess(result)) { + out.push(result.success) + } + } + return out +}) + +/** + * Keeps only elements satisfying a predicate (or refinement). + * + * **When to use** + * + * Use to filter an iterable into a new array of original elements that satisfy + * a boolean predicate or refinement. + * + * **Details** + * + * The predicate receives `(element, index)`. Refinements are supported for type + * narrowing. + * + * **Example** (Filtering even numbers) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.filter([1, 2, 3, 4], (x) => x % 2 === 0)) // [2, 4] + * ``` + * + * @see {@link partition} — split into matching and non-matching + * @see {@link filterMap} for transforming while filtering + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Array + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Array + (self: Iterable, refinement: (a: A, i: number) => a is B): Array + (self: Iterable, predicate: (a: A, i: number) => boolean): Array +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): Array => { + const as = fromIterable(self) + const out: Array = [] + for (let i = 0; i < as.length; i++) { + if (predicate(as[i], i)) { + out.push(as[i]) + } + } + return out + } +) + +/** + * Splits an iterable using a `Filter` into failures and successes. + * + * **When to use** + * + * Use to partition an iterable by evaluating each element with a + * `Result`-returning filter and keeping both failure and success values. + * + * **Details** + * + * Returns `[excluded, satisfying]`. The filter receives `(element, index)`. + * + * **Example** (Partitioning with a filter) + * + * ```ts + * import { Array, Result } from "effect" + * + * console.log(Array.partition([1, -2, 3], (n, i) => + * n > 0 ? Result.succeed(n + i) : Result.fail(`negative:${n}`) + * )) + * // [["negative:-2"], [1, 5]] + * ``` + * + * @see {@link filter} — keep only matching elements + * @see {@link filterMap} for discarding failures + * @see {@link separate} — split an iterable of `Result` values + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + ( + f: (input: NoInfer, i: number) => Result.Result + ): (self: Iterable) => [excluded: Array, satisfying: Array] + ( + self: Iterable, + f: (input: A, i: number) => Result.Result + ): [excluded: Array, satisfying: Array] +} = dual( + 2, + ( + self: Iterable, + f: (input: A, i: number) => Result.Result + ): [excluded: Array, satisfying: Array] => { + const excluded: Array = [] + const satisfying: Array = [] + let i = 0 + for (const a of self) { + const result = f(a, i++) + if (Result.isSuccess(result)) { + satisfying.push(result.success) + } else { + excluded.push(result.failure) + } + } + return [excluded, satisfying] + } +) + +/** + * Separates an iterable of `Result`s into failure values and success values. + * + * **When to use** + * + * Use to split an iterable of `Result` values into failure and success arrays. + * + * **Details** + * + * Returns `[failures, successes]`. This is equivalent to + * `partition(identity)`. + * + * **Example** (Separating Results) + * + * ```ts + * import { Array, Result } from "effect" + * + * const [failures, successes] = Array.separate([ + * Result.succeed(1), Result.fail("error"), Result.succeed(2) + * ]) + * console.log(failures) // ["error"] + * console.log(successes) // [1, 2] + * ``` + * + * @see {@link getFailures} — extract only failures + * @see {@link getSuccesses} — extract only successes + * @see {@link partition} for computing `Result` values while splitting + * + * @category filtering + * @since 2.0.0 + */ +export const separate: >>( + self: T +) => [ + failures: Array>>, + successes: Array>> +] = partition(identity) + +/** + * Folds an iterable from left to right into a single value. + * + * **When to use** + * + * Use to combine all elements into one accumulated value from left to right. + * + * **Details** + * + * The function receives `(accumulator, element, index)`. + * + * **Example** (Summing an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.reduce([1, 2, 3], 0, (acc, n) => acc + n)) // 6 + * ``` + * + * @see {@link reduceRight} — fold from right to left + * @see {@link scan} — fold keeping intermediate values + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual( + 3, + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => + fromIterable(self).reduce((b, a, i) => f(b, a, i), b) +) + +/** + * Folds an iterable from right to left into a single value. + * + * **When to use** + * + * Use when you need to fold values from right to left. + * + * **Details** + * + * The function receives `(accumulator, element, index)`. + * + * **Example** (Folding from right to left) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.reduceRight([1, 2, 3], 0, (acc, n) => acc + n)) // 6 + * ``` + * + * @see {@link reduce} — fold from left to right + * @see {@link scanRight} — fold keeping intermediate values + * + * @category folding + * @since 2.0.0 + */ +export const reduceRight: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual( + 3, + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => + fromIterable(self).reduceRight((b, a, i) => f(b, a, i), b) +) + +/** + * Lifts a predicate into an array: returns `[value]` if the predicate holds, + * `[]` otherwise. + * + * **Example** (Wrapping values conditionally) + * + * ```ts + * import { Array } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * const to = Array.liftPredicate(isEven) + * console.log(to(1)) // [] + * console.log(to(2)) // [2] + * ``` + * + * @see {@link liftOption} — lift an Option-returning function + * + * @category lifting + * @since 2.0.0 + */ +export const liftPredicate: { // Note: I intentionally avoid using the NoInfer pattern here. + (refinement: Predicate.Refinement): (a: A) => Array + (predicate: Predicate.Predicate): (b: B) => Array +} = (predicate: Predicate.Predicate) => (b: B): Array => predicate(b) ? [b] : [] + +/** + * Lifts an `Option`-returning function into one that returns an array: + * `Some(a)` becomes `[a]`, `None` becomes `[]`. + * + * **When to use** + * + * Use when an optional parser or lookup should participate in array pipelines + * as zero-or-one results. + * + * **Example** (Lifting an Option function) + * + * ```ts + * import { Array, Option } from "effect" + * + * const parseNumber = Array.liftOption((s: string) => { + * const n = Number(s) + * return isNaN(n) ? Option.none() : Option.some(n) + * }) + * console.log(parseNumber("123")) // [123] + * console.log(parseNumber("abc")) // [] + * ``` + * + * @see {@link liftPredicate} — lift a boolean predicate + * @see {@link liftResult} — lift a Result-returning function + * + * @category lifting + * @since 2.0.0 + */ +export const liftOption = , B>( + f: (...a: A) => Option.Option +) => +(...a: A): Array => fromOption(f(...a)) + +/** + * Converts a nullable value to an array: `null`/`undefined` becomes `[]`, + * anything else becomes `[value]`. + * + * **When to use** + * + * Use to treat a nullable single value as zero or one array element. + * + * **Example** (Converting nullable values to an array) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.fromNullishOr(1)) // [1] + * console.log(Array.fromNullishOr(null)) // [] + * console.log(Array.fromNullishOr(undefined)) // [] + * ``` + * + * @see {@link liftNullishOr} — lift a nullable-returning function + * @see {@link fromOption} — convert from Option + * + * @category converting + * @since 4.0.0 + */ +export const fromNullishOr = (a: A): Array> => a == null ? empty() : [a as NonNullable] + +/** + * Lifts a nullable-returning function into one that returns an array: + * `null`/`undefined` becomes `[]`, anything else becomes `[value]`. + * + * **Example** (Lifting a nullable function) + * + * ```ts + * import { Array } from "effect" + * + * const parseNumber = Array.liftNullishOr((s: string) => { + * const n = Number(s) + * return isNaN(n) ? null : n + * }) + * console.log(parseNumber("123")) // [123] + * console.log(parseNumber("abc")) // [] + * ``` + * + * @see {@link fromNullishOr} — convert a single nullable value + * @see {@link liftOption} — lift an Option-returning function + * + * @category lifting + * @since 4.0.0 + */ +export const liftNullishOr = , B>( + f: (...a: A) => B +): (...a: A) => Array> => +(...a) => fromNullishOr(f(...a)) + +/** + * Maps each element with a nullable-returning function, keeping only non-null / + * non-undefined results. + * + * **When to use** + * + * Use when you need to map and filter in one step, where the mapper can return + * `null` or `undefined` to skip elements. + * + * **Example** (Flat mapping with nullable values) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.flatMapNullishOr([1, 2, 3], (n) => (n % 2 === 0 ? null : n))) + * // [1, 3] + * ``` + * + * @see {@link flatMap} for mapping each element to an array and flattening + * @see {@link fromNullishOr} for converting a single nullable value to an array + * + * @category sequencing + * @since 4.0.0 + */ +export const flatMapNullishOr: { + (f: (a: A) => B): (self: ReadonlyArray) => Array> + (self: ReadonlyArray, f: (a: A) => B): Array> +} = dual( + 2, + (self: ReadonlyArray, f: (a: A) => B): Array> => flatMap(self, (a) => fromNullishOr(f(a))) +) + +/** + * Lifts a `Result`-returning function into one that returns an array: failures + * produce `[]`, successes produce `[value]`. + * + * **When to use** + * + * Use when a fallible parser or lookup should participate in array pipelines as + * zero-or-one results and the failure value should be discarded. + * + * **Example** (Lifting a Result function) + * + * ```ts + * import { Array, Result } from "effect" + * + * const parseNumber = (s: string): Result.Result => + * isNaN(Number(s)) + * ? Result.fail(new Error("Not a number")) + * : Result.succeed(Number(s)) + * + * const liftedParseNumber = Array.liftResult(parseNumber) + * console.log(liftedParseNumber("42")) // [42] + * console.log(liftedParseNumber("not a number")) // [] + * ``` + * + * @see {@link liftOption} — lift an Option-returning function + * @see {@link liftPredicate} — lift a boolean predicate + * + * @category lifting + * @since 4.0.0 + */ +export const liftResult = , E, B>( + f: (...a: A) => Result.Result +) => +(...a: A): Array => { + const e = f(...a) + return Result.isFailure(e) ? [] : [e.success] +} + +/** + * Checks whether all elements satisfy the predicate. Supports refinements for + * type narrowing. + * + * **When to use** + * + * Use to check whether every array element satisfies a predicate, including + * refinement-based type narrowing. + * + * **Example** (Testing all elements) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.every([2, 4, 6], (x) => x % 2 === 0)) // true + * console.log(Array.every([2, 3, 6], (x) => x % 2 === 0)) // false + * ``` + * + * @see {@link some} — test if any element matches + * + * @category elements + * @since 2.0.0 + */ +export const every: { + ( + refinement: (a: NoInfer, i: number) => a is B + ): (self: ReadonlyArray) => self is ReadonlyArray + (predicate: (a: NoInfer, i: number) => boolean): (self: ReadonlyArray) => boolean + (self: ReadonlyArray, refinement: (a: A, i: number) => a is B): self is ReadonlyArray + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): boolean +} = dual( + 2, + (self: ReadonlyArray, refinement: (a: A, i: number) => a is B): self is ReadonlyArray => + self.every(refinement) +) + +/** + * Checks whether at least one element satisfies the predicate. Narrows the type + * to `NonEmptyReadonlyArray` on success. + * + * **Example** (Testing for any match) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.some([1, 3, 4], (x) => x % 2 === 0)) // true + * console.log(Array.some([1, 3, 5], (x) => x % 2 === 0)) // false + * ``` + * + * @see {@link every} — test if all elements match + * @see {@link contains} — test for a specific value + * + * @category elements + * @since 2.0.0 + */ +export const some: { + ( + predicate: (a: NoInfer, i: number) => boolean + ): (self: ReadonlyArray) => self is NonEmptyReadonlyArray + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): self is NonEmptyReadonlyArray +} = dual( + 2, + (self: ReadonlyArray, predicate: (a: A, i: number) => boolean): self is NonEmptyReadonlyArray => + self.some(predicate) +) + +/** + * Applies a function to each suffix of the array (starting from each index), + * collecting the results. + * + * **When to use** + * + * Use when you need to compute a result from every suffix of an array, such as + * cumulative aggregations from each position. + * + * **Details** + * + * For index `i`, the function receives `self.slice(i)`. + * + * **Example** (Computing suffix lengths) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.extend([1, 2, 3], (as) => as.length)) // [3, 2, 1] + * ``` + * + * @see {@link scan} for keeping intermediate accumulator values during a fold + * + * @category mapping + * @since 2.0.0 + */ +export const extend: { + (f: (as: ReadonlyArray) => B): (self: ReadonlyArray) => Array + (self: ReadonlyArray, f: (as: ReadonlyArray) => B): Array +} = dual( + 2, + (self: ReadonlyArray, f: (as: ReadonlyArray) => B): Array => self.map((_, i, as) => f(as.slice(i))) +) + +/** + * Returns the minimum element of a non-empty array according to the given + * `Order`. + * + * **Example** (Finding the minimum) + * + * ```ts + * import { Array, Order } from "effect" + * + * console.log(Array.min([3, 1, 2], Order.Number)) // 1 + * ``` + * + * @see {@link max} — find the maximum + * @see {@link sort} — sort the entire array + * + * @category elements + * @since 2.0.0 + */ +export const min: { + (O: Order.Order): (self: NonEmptyReadonlyArray) => A + (self: NonEmptyReadonlyArray, O: Order.Order): A +} = dual(2, (self: NonEmptyReadonlyArray, O: Order.Order): A => self.reduce(Order.min(O))) + +/** + * Returns the maximum element of a non-empty array according to the given + * `Order`. + * + * **Example** (Finding the maximum) + * + * ```ts + * import { Array, Order } from "effect" + * + * console.log(Array.max([3, 1, 2], Order.Number)) // 3 + * ``` + * + * @see {@link min} — find the minimum + * @see {@link sort} — sort the entire array + * + * @category elements + * @since 2.0.0 + */ +export const max: { + (O: Order.Order): (self: NonEmptyReadonlyArray) => A + (self: NonEmptyReadonlyArray, O: Order.Order): A +} = dual(2, (self: NonEmptyReadonlyArray, O: Order.Order): A => self.reduce(Order.max(O))) + +/** + * Builds an array by repeatedly applying a function to a seed value. The + * function returns `Option.some([element, nextSeed])` to continue, or + * `Option.none()` to stop. + * + * **Example** (Generating a sequence) + * + * ```ts + * import { Array, Option } from "effect" + * + * console.log(Array.unfold(1, (n) => n <= 5 ? Option.some([n, n + 1]) : Option.none())) + * // [1, 2, 3, 4, 5] + * ``` + * + * @see {@link makeBy} — generate from index + * @see {@link range} — generate a numeric range + * + * @category constructors + * @since 2.0.0 + */ +export const unfold = (b: B, f: (b: B) => Option.Option): Array => { + const out: Array = [] + let next: B = b + while (true) { + const o = f(next) + if (Option.isNone(o)) { + break + } + const [a, b] = o.value + out.push(a) + next = b + } + return out +} + +/** + * Creates an `Order` for arrays based on an element `Order`. Arrays are + * compared element-wise; if all compared elements are equal, shorter arrays + * come first. + * + * **Example** (Comparing arrays) + * + * ```ts + * import { Array, Order } from "effect" + * + * const arrayOrder = Array.makeOrder(Order.Number) + * console.log(arrayOrder([1, 2], [1, 3])) // -1 + * ``` + * + * @see {@link makeEquivalence} — create an equivalence for arrays + * + * @category instances + * @since 4.0.0 + */ +export const makeOrder: (O: Order.Order) => Order.Order> = Order.Array + +/** + * Creates an `Equivalence` for arrays based on an element `Equivalence`. Two + * arrays are equivalent when they have the same length and all elements are + * pairwise equivalent. + * + * **Example** (Comparing arrays for equality) + * + * ```ts + * import { Array } from "effect" + * + * const eq = Array.makeEquivalence((a, b) => a === b) + * console.log(eq([1, 2, 3], [1, 2, 3])) // true + * ``` + * + * @see {@link makeOrder} — create an ordering for arrays + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence: ( + isEquivalent: Equivalence.Equivalence +) => Equivalence.Equivalence> = Equivalence.Array + +/** + * Runs a side-effect for each element. The callback receives `(element, index)`. + * + * **When to use** + * + * Use to iterate over an array for side-effects only, when no transformed + * result is needed. + * + * **Example** (Iterating with side-effects) + * + * ```ts + * import { Array } from "effect" + * + * Array.forEach([1, 2, 3], (n) => console.log(n)) // 1, 2, 3 + * ``` + * + * @see {@link map} for transforming each element into a new array + * + * @category elements + * @since 2.0.0 + */ +export const forEach: { + (f: (a: A, i: number) => void): (self: Iterable) => void + (self: Iterable, f: (a: A, i: number) => void): void +} = dual(2, (self: Iterable, f: (a: A, i: number) => void): void => fromIterable(self).forEach((a, i) => f(a, i))) + +/** + * Removes duplicates using a custom equivalence, preserving the order of the + * first occurrence. + * + * **When to use** + * + * Use to remove all duplicate elements with a custom equivalence when default + * equality is not appropriate. + * + * **Example** (Deduplicating with custom equality) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dedupeWith([1, 2, 2, 3, 3, 3], (a, b) => a === b)) // [1, 2, 3] + * ``` + * + * @see {@link dedupe} — uses default equality + * @see {@link dedupeAdjacentWith} — only dedupes consecutive elements + * + * @category elements + * @since 2.0.0 + */ +export const dedupeWith: { + >( + isEquivalent: (self: ReadonlyArray.Infer, that: ReadonlyArray.Infer) => boolean + ): (self: S) => ReadonlyArray.With> + (self: NonEmptyReadonlyArray, isEquivalent: (self: A, that: A) => boolean): NonEmptyArray + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array +} = dual( + 2, + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const input = fromIterable(self) + if (isReadonlyArrayNonEmpty(input)) { + const out: NonEmptyArray = [headNonEmpty(input)] + const rest = tailNonEmpty(input) + for (const r of rest) { + if (out.every((a) => !isEquivalent(r, a))) { + out.push(r) + } + } + return out + } + return [] + } +) + +/** + * Removes duplicates using `Equal.equivalence()`, preserving the order of the + * first occurrence. + * + * **When to use** + * + * Use to remove repeated values from an iterable when Effect's default equality + * is the right comparison, preserving the first occurrence. + * + * **Example** (Removing duplicates) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dedupe([1, 2, 1, 3, 2, 4])) // [1, 2, 3, 4] + * ``` + * + * @see {@link dedupeWith} — use custom equality + * @see {@link dedupeAdjacent} — only dedupes consecutive elements + * + * @category elements + * @since 2.0.0 + */ +export const dedupe = >( + self: S +): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => + dedupeWith(self, Equal.asEquivalence()) as any + +/** + * Removes consecutive duplicate elements using a custom equivalence. + * + * **When to use** + * + * Use when consecutive duplicates should be collapsed using a custom + * equivalence, while equivalent values that appear later should remain in the + * result. + * + * **Details** + * + * Non-adjacent duplicates are preserved. + * + * **Example** (Deduplicating adjacent elements) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dedupeAdjacentWith([1, 1, 2, 2, 3, 3], (a, b) => a === b)) + * // [1, 2, 3] + * ``` + * + * @see {@link dedupeAdjacent} — uses default equality + * @see {@link dedupeWith} — dedupes all duplicates, not just adjacent + * + * @category elements + * @since 2.0.0 + */ +export const dedupeAdjacentWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Array + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array +} = dual(2, (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { + const out: Array = [] + let lastA: Option.Option = Option.none() + for (const a of self) { + if (Option.isNone(lastA) || !isEquivalent(a, lastA.value)) { + out.push(a) + lastA = Option.some(a) + } + } + return out +}) + +/** + * Removes consecutive duplicate elements using `Equal.equivalence()`. + * + * **When to use** + * + * Use when you need to collapse consecutive duplicates while preserving later + * non-consecutive repeats, and the default equality is sufficient. + * + * **Example** (Removing adjacent duplicates) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.dedupeAdjacent([1, 1, 2, 2, 3, 3])) // [1, 2, 3] + * ``` + * + * @see {@link dedupeAdjacentWith} — use custom equality + * @see {@link dedupe} — remove all duplicates + * + * @category elements + * @since 2.0.0 + */ +export const dedupeAdjacent: (self: Iterable) => Array = dedupeAdjacentWith(Equal.asEquivalence()) + +/** + * Joins string elements with a separator. + * + * **Example** (Joining strings) + * + * ```ts + * import { Array } from "effect" + * + * console.log(Array.join(["a", "b", "c"], "-")) // "a-b-c" + * ``` + * + * @see {@link intersperse} — insert separator elements without joining + * + * @category folding + * @since 2.0.0 + */ +export const join: { + (sep: string): (self: Iterable) => string + (self: Iterable, sep: string): string +} = dual(2, (self: Iterable, sep: string): string => fromIterable(self).join(sep)) + +/** + * Maps over an array while threading an accumulator through each step, returning both the final state and the mapped array. + * + * **When to use** + * + * Use when you need to map while threading state through each element and keep + * the final state. + * + * **Details** + * + * Combines `map` and `reduce` in a single pass. The callback receives the + * current state, element, and index, and returns `[nextState, mappedValue]`. + * The result is `[finalState, mappedArray]`. This can be used in both + * data-first and data-last style. + * + * **Example** (Running sum alongside mapped values) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.mapAccum([1, 2, 3], 0, (acc, n) => [acc + n, acc + n]) + * console.log(result) // [6, [1, 3, 6]] + * ``` + * + * @see {@link scan} — when you only need the accumulated results (not the final state) + * @see {@link reduce} — when you only need the final accumulated value + * + * @category folding + * @since 2.0.0 + */ +export const mapAccum: { + = Iterable>( + s: S, + f: (s: S, a: ReadonlyArray.Infer, i: number) => readonly [S, B] + ): (self: I) => [state: S, mappedArray: ReadonlyArray.With] + = Iterable>( + self: I, + s: S, + f: (s: S, a: ReadonlyArray.Infer, i: number) => readonly [S, B] + ): [state: S, mappedArray: ReadonlyArray.With] +} = dual( + 3, + (self: Iterable, s: S, f: (s: S, a: A, i: number) => [S, B]): [state: S, mappedArray: Array] => { + let i = 0 + let s1 = s + const out: Array = [] + for (const a of self) { + const r = f(s1, a, i) + s1 = r[0] + out.push(r[1]) + i++ + } + return [s1, out] + } +) + +/** + * Computes the cartesian product of two arrays, applying a combiner to each pair. + * + * **When to use** + * + * Use to compute every combination from two arrays and immediately transform + * each pair into a custom result. + * + * **Details** + * + * Produces every combination of an element from `self` with an element from + * `that`, so the result length is `self.length * that.length`. Iteration visits + * every element of `that` for each element of `self`. + * + * **Example** (Combining numbers and letters) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.cartesianWith([1, 2], ["a", "b"], (a, b) => `${a}-${b}`) + * console.log(result) // ["1-a", "1-b", "2-a", "2-b"] + * ``` + * + * @see {@link cartesian} for returning tuples instead of applying a combiner + * + * @category elements + * @since 2.0.0 + */ +export const cartesianWith: { + (that: ReadonlyArray, f: (a: A, b: B) => C): (self: ReadonlyArray) => Array + (self: ReadonlyArray, that: ReadonlyArray, f: (a: A, b: B) => C): Array +} = dual( + 3, + (self: ReadonlyArray, that: ReadonlyArray, f: (a: A, b: B) => C): Array => + flatMap(self, (a) => map(that, (b) => f(a, b))) +) + +/** + * Computes the cartesian product of two arrays, returning all pairs as tuples. + * + * **When to use** + * + * Use when you need every `[a, b]` pair from two arrays as tuples. + * + * **Details** + * + * Produces every `[a, b]` combination of an element from `self` with an element + * from `that`, so the result length is `self.length * that.length`. + * + * **Example** (Generating all pairs from two arrays) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.cartesian([1, 2], ["a", "b"]) + * console.log(result) // [[1, "a"], [1, "b"], [2, "a"], [2, "b"]] + * ``` + * + * @see {@link cartesianWith} — apply a combiner to each pair + * + * @category elements + * @since 2.0.0 + */ +export const cartesian: { + (that: ReadonlyArray): (self: ReadonlyArray) => Array<[A, B]> + (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> +} = dual( + 2, + (self: ReadonlyArray, that: ReadonlyArray): Array<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) +) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * Provides the starting point for the "do simulation" — an array comprehension pattern. + * + * **When to use** + * + * Use when you want array-comprehension style code with do notation. + * + * **Details** + * + * Use {@link bind} to introduce array variables and {@link let_ let} for plain + * values. Each `bind` produces the cartesian product of all bound variables, + * like nested loops. Use `filter` and `map` in the pipeline to add conditions + * and transformations. + * + * **Example** (Building array comprehensions with do notation) + * + * ```ts + * import { Array, pipe } from "effect" + * + * const result = pipe( + * Array.Do, + * Array.bind("x", () => [1, 3, 5]), + * Array.bind("y", () => [2, 4, 6]), + * Array.filter(({ x, y }) => x < y), + * Array.map(({ x, y }) => [x, y] as const) + * ) + * console.log(result) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * ``` + * + * @see {@link bind} — introduce an array variable into the scope + * @see {@link bindTo} — start a pipeline by naming the first array + * @see {@link let_ let} — introduce a plain computed value + * + * @category do notation + * @since 3.2.0 + */ +export const Do: ReadonlyArray<{}> = of({}) + +/** + * Adds a new array variable to a do-notation scope, producing the cartesian product with all previous bindings. + * + * **When to use** + * + * Use to add another array-producing binding to an `Array.Do` pipeline, pairing + * each existing scope with every value returned by the callback. + * + * **Details** + * + * Each `bind` call adds a named property to the accumulated object. The + * callback receives the current scope and must return an array. This is + * equivalent to `flatMap` plus merging the new value into the scope object. + * + * **Example** (Binding two arrays) + * + * ```ts + * import { Array, pipe } from "effect" + * + * const result = pipe( + * Array.Do, + * Array.bind("x", () => [1, 2]), + * Array.bind("y", () => ["a", "b"]) + * ) + * console.log(result) + * // [{ x: 1, y: "a" }, { x: 1, y: "b" }, { x: 2, y: "a" }, { x: 2, y: "b" }] + * ``` + * + * @see {@link Do} — start a do-notation pipeline + * @see {@link bindTo} — name the first array in a pipeline + * @see {@link let_ let} — add a plain computed value + * + * @category do notation + * @since 3.2.0 + */ +export const bind: { + ( + tag: Exclude, + f: (a: NoInfer) => ReadonlyArray + ): ( + self: ReadonlyArray + ) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: NoInfer) => ReadonlyArray + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = internalDoNotation.bind(map, flatMap) as any + +/** + * Wraps each array element in an object with the given key, starting a do-notation scope. + * + * **When to use** + * + * Use when you already have an array and want to start a do-notation pipeline + * by naming each element. + * + * **Details** + * + * Equivalent to `Array.map(self, (a) => ({ [tag]: a }))`. This is an + * alternative to starting with `Do` plus `bind` when you already have an array. + * + * **Example** (Naming an existing array) + * + * ```ts + * import { Array, pipe } from "effect" + * + * const result = pipe( + * [1, 2, 3], + * Array.bindTo("x") + * ) + * console.log(result) // [{ x: 1 }, { x: 2 }, { x: 3 }] + * ``` + * + * @see {@link Do} — start with an empty scope + * @see {@link bind} — add another array variable to the scope + * + * @category do notation + * @since 3.2.0 + */ +export const bindTo: { + (tag: N): (self: ReadonlyArray) => Array<{ [K in N]: A }> + (self: ReadonlyArray, tag: N): Array<{ [K in N]: A }> +} = internalDoNotation.bindTo(map) as any + +const let_: { + ( + tag: Exclude, + f: (a: NoInfer) => B + ): (self: ReadonlyArray) => Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: ReadonlyArray, + tag: Exclude, + f: (a: NoInfer) => B + ): Array<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = internalDoNotation.let_(map) as any + +export { + /** + * Adds a computed plain value to the do-notation scope without introducing a new array dimension. + * + * **When to use** + * + * Use when each do-notation branch needs a derived field from the current + * bindings without multiplying the number of branches. + * + * **Details** + * + * Unlike `bind`, the callback returns a single value instead of an array, so + * no cartesian product occurs. Use this for derived or intermediate values + * that depend on previously bound variables. + * + * **Example** (Adding a computed value) + * + * ```ts + * import { Array, pipe } from "effect" + * + * const result = pipe( + * Array.Do, + * Array.bind("x", () => [1, 2, 3]), + * Array.let("doubled", ({ x }) => x * 2) + * ) + * console.log(result) + * // [{ x: 1, doubled: 2 }, { x: 2, doubled: 4 }, { x: 3, doubled: 6 }] + * ``` + * + * @see {@link Do} — start a do-notation pipeline + * @see {@link bind} — introduce an array variable (produces cartesian product) + * + * @category do notation + * @since 3.2.0 + */ + let_ as let +} + +const reducer = Reducer.make((a, b) => a.concat(b), [] as any) + +/** + * Returns a `Reducer` that combines `ReadonlyArray` values by concatenation. + * + * @see {@link makeReducerConcat} — mutable `Array` variant + * + * @category folding + * @since 4.0.0 + */ +export function getReadonlyReducerConcat(): Reducer.Reducer> { + return reducer +} + +/** + * Returns a `Reducer` that combines `Array` values by concatenation. + * + * @see {@link getReadonlyReducerConcat} — readonly variant + * + * @category folding + * @since 4.0.0 + */ +export function makeReducerConcat(): Reducer.Reducer> { + return reducer +} + +/** + * Computes the number of elements in an iterable that satisfy a predicate. + * + * **When to use** + * + * Use when you need to count how many elements of an iterable satisfy a + * predicate. + * + * **Details** + * + * The predicate receives both the element and its index. Empty iterables return + * `0`. + * + * **Example** (Counting even numbers) + * + * ```ts + * import { Array } from "effect" + * + * const result = Array.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) + * console.log(result) // 2 + * ``` + * + * @see {@link filter} — when you need the matching elements, not just the count + * + * @category folding + * @since 3.16.0 + */ +export const countBy: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => number + (self: Iterable, predicate: (a: A, i: number) => boolean): number +} = dual( + 2, + ( + self: Iterable, + f: (a: A, i: number) => boolean + ): number => { + let count = 0 + const as = fromIterable(self) + for (let i = 0; i < as.length; i++) { + const a = as[i] + if (f(a, i)) { + count++ + } + } + return count + } +) diff --git a/repos/effect/packages/effect/src/BigDecimal.ts b/repos/effect/packages/effect/src/BigDecimal.ts new file mode 100644 index 0000000..8ca0678 --- /dev/null +++ b/repos/effect/packages/effect/src/BigDecimal.ts @@ -0,0 +1,1943 @@ +/** + * Decimal numbers and arithmetic for cases where JavaScript `number` rounding + * is not precise enough. A `BigDecimal` stores digits as a `bigint` plus a + * decimal scale, which lets the module parse, compare, add, subtract, multiply, + * divide, round, and format decimal values such as money, quantities, and + * measurements. + * + * @since 2.0.0 + */ + +import * as Equal from "./Equal.ts" +import * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import type { Ordering } from "./Ordering.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" + +const DEFAULT_PRECISION = 100 +const FINITE_INT_REGEXP = /^[+-]?\d+$/ + +const TypeId = "~effect/BigDecimal" + +/** + * Represents an arbitrary precision decimal number. + * + * **When to use** + * + * Use when decimal arithmetic needs to avoid JavaScript floating point + * representation errors. + * + * **Example** (Inspecting BigDecimal storage) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const d = BigDecimal.fromStringUnsafe("123.45") + * + * console.log(d.value) // 12345n + * console.log(d.scale) // 2 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface BigDecimal extends Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + readonly value: bigint + readonly scale: number + /** @internal */ + normalized?: BigDecimal +} + +const BigDecimalProto: Omit = { + [TypeId]: TypeId, + [Hash.symbol](this: BigDecimal): number { + const normalized = normalize(this) + return Hash.combine(Hash.hash(normalized.value), Hash.number(normalized.scale)) + }, + [Equal.symbol](this: BigDecimal, that: unknown): boolean { + return isBigDecimal(that) && equals(this, that) + }, + toString(this: BigDecimal) { + return `BigDecimal(${format(this)})` + }, + toJSON(this: BigDecimal) { + return { + _id: "BigDecimal", + value: String(this.value), + scale: this.scale + } + }, + [NodeInspectSymbol](this: BigDecimal) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} as const + +/** + * Checks whether a given value is a `BigDecimal`. + * + * **When to use** + * + * Use to validate unknown input and narrow it to `BigDecimal`. + * + * **Example** (Checking BigDecimal values) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const decimal = BigDecimal.fromNumber(123.45) + * console.log(BigDecimal.isBigDecimal(decimal)) // true + * console.log(BigDecimal.isBigDecimal(123.45)) // false + * console.log(BigDecimal.isBigDecimal("123.45")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBigDecimal = (u: unknown): u is BigDecimal => hasProperty(u, TypeId) + +/** + * Creates a `BigDecimal` from a `bigint` value and a scale. + * + * **When to use** + * + * Use to construct a decimal directly from its unscaled integer value and + * decimal scale. + * + * **Example** (Creating decimals from bigint and scale) + * + * ```ts + * import { BigDecimal } from "effect" + * + * // Create 123.45 (12345 with scale 2) + * const decimal = BigDecimal.make(12345n, 2) + * console.log(BigDecimal.format(decimal)) // "123.45" + * + * // Create 42 (42 with scale 0) + * const integer = BigDecimal.make(42n, 0) + * console.log(BigDecimal.format(integer)) // "42" + * ``` + * + * @see {@link fromBigInt} for constructing an integer decimal from a `bigint` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (value: bigint, scale: number): BigDecimal => { + const o = Object.create(BigDecimalProto) + o.value = value + o.scale = scale + return o +} + +/** + * Internal function used to create pre-normalized `BigDecimal`s. + * + * @internal + */ +export const makeNormalizedUnsafe = (value: bigint, scale: number): BigDecimal => { + if (value !== bigint0 && value % bigint10 === bigint0) { + throw new RangeError("Value must be normalized") + } + + const o = make(value, scale) + o.normalized = o + return o +} + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const bigint_1 = BigInt(-1) +const bigint2 = BigInt(2) +const bigint5 = BigInt(5) +const bigint_5 = BigInt(-5) +const bigint10 = BigInt(10) +const zero = makeNormalizedUnsafe(bigint0, 0) +const one = makeNormalizedUnsafe(bigint1, 0) + +/** + * Normalizes a given `BigDecimal` by removing trailing zeros. + * + * **When to use** + * + * Use to canonicalize decimals that have equivalent values but different + * internal scales. + * + * **Example** (Normalizing trailing zeros) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.normalize(BigDecimal.fromStringUnsafe("123.00000")), + * BigDecimal.normalize(BigDecimal.make(123n, 0)) + * ) + * assert.deepStrictEqual( + * BigDecimal.normalize(BigDecimal.fromStringUnsafe("12300000")), + * BigDecimal.normalize(BigDecimal.make(123n, -5)) + * ) + * ``` + * + * @see {@link format} for rendering normalized decimals as strings + * + * @category scaling + * @since 2.0.0 + */ +export const normalize = (self: BigDecimal): BigDecimal => { + if (self.normalized === undefined) { + if (self.value === bigint0) { + self.normalized = zero + } else { + const digits = `${self.value}` + + let trail = 0 + for (let i = digits.length - 1; i >= 0; i--) { + if (digits[i] === "0") { + trail++ + } else { + break + } + } + + if (trail === 0) { + self.normalized = self + } + + const value = BigInt(digits.substring(0, digits.length - trail)) + const scale = self.scale - trail + self.normalized = makeNormalizedUnsafe(value, scale) + } + } + + return self.normalized +} + +/** + * Changes a `BigDecimal` to the specified scale. + * + * **When to use** + * + * Use to change how many decimal places are represented by a `BigDecimal`. + * + * **Details** + * + * Increasing the scale appends decimal zeros. Decreasing the scale discards + * digits beyond the target scale by `bigint` division, which truncates toward + * zero. + * + * **Example** (Scaling decimal precision) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const decimal = BigDecimal.fromNumberUnsafe(123.45) + * + * // Increase scale (add more precision) + * const scaled = BigDecimal.scale(decimal, 4) + * console.log(BigDecimal.format(scaled)) // "123.4500" + * + * // Decrease scale (reduce precision, rounds down) + * const reduced = BigDecimal.scale(decimal, 1) + * console.log(BigDecimal.format(reduced)) // "123.4" + * ``` + * + * @see {@link round} for changing scale with configurable rounding + * + * @category scaling + * @since 2.0.0 + */ +export const scale: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale: number): BigDecimal +} = dual(2, (self: BigDecimal, scale: number): BigDecimal => { + if (scale > self.scale) { + return make(self.value * bigint10 ** BigInt(scale - self.scale), scale) + } + + if (scale < self.scale) { + return make(self.value / bigint10 ** BigInt(self.scale - scale), scale) + } + + return self +}) + +/** + * Provides an addition operation on `BigDecimal`s. + * + * **When to use** + * + * Use when you need a decimal addition function for piping or higher-order APIs + * while preserving decimal precision. + * + * **Example** (Adding decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.sum(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("5") + * ) + * ``` + * + * @see {@link sumAll} for summing an iterable of `BigDecimal` values + * + * @category math + * @since 2.0.0 + */ +export const sum: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + return self + } + + if (self.value === bigint0) { + return that + } + + if (self.scale > that.scale) { + return make(scale(that, self.scale).value + self.value, self.scale) + } + + if (self.scale < that.scale) { + return make(scale(self, that.scale).value + that.value, that.scale) + } + + return make(self.value + that.value, self.scale) +}) + +/** + * Takes an `Iterable` of `BigDecimal`s and returns their sum as a single `BigDecimal`. + * + * **When to use** + * + * Use when you need to aggregate decimal quantities with decimal precision + * instead of converting through JavaScript numbers. + * + * **Example** (Adding multiple decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.sumAll([BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4")]), + * BigDecimal.fromStringUnsafe("9") + * ) + * ``` + * + * @see {@link sum} for adding two `BigDecimal` values + * + * @category math + * @since 3.16.0 + */ +export const sumAll = (collection: Iterable): BigDecimal => { + let out: BigDecimal = zero + for (const n of collection) { + out = sum(out, n) + } + return out +} + +/** + * Provides a multiplication operation on `BigDecimal`s. + * + * **When to use** + * + * Use to multiply two `BigDecimal` values. + * + * **Example** (Multiplying decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.multiply(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("6") + * ) + * ``` + * + * @see {@link multiplyAll} for multiplying an iterable of `BigDecimal` values + * + * @category math + * @since 2.0.0 + */ +export const multiply: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0 || self.value === bigint0) { + return zero + } + + return make(self.value * that.value, self.scale + that.scale) +}) + +/** + * Takes an `Iterable` of `BigDecimal`s and returns their multiplication as a single `BigDecimal`. + * + * **When to use** + * + * Use to multiply all `BigDecimal` values in an iterable. + * + * **Example** (Multiplying multiple decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.multiplyAll([BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4")]), + * BigDecimal.fromStringUnsafe("24") + * ) + * ``` + * + * @see {@link multiply} for multiplying two `BigDecimal` values + * + * @category math + * @since 4.0.0 + */ +export const multiplyAll = (collection: Iterable): BigDecimal => { + let out: BigDecimal = one + for (const n of collection) { + if (n.value === bigint0) { + return zero + } + out = multiply(out, n) + } + return out +} + +/** + * Provides a subtraction operation on `BigDecimal`s. + * + * **When to use** + * + * Use to subtract one `BigDecimal` value from another. + * + * **Example** (Subtracting decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.subtract(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("-1") + * ) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const subtract: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + return self + } + + if (self.value === bigint0) { + return make(-that.value, that.scale) + } + + if (self.scale > that.scale) { + return make(self.value - scale(that, self.scale).value, self.scale) + } + + if (self.scale < that.scale) { + return make(scale(self, that.scale).value - that.value, that.scale) + } + + return make(self.value - that.value, self.scale) +}) + +/** + * Internal function used for arbitrary precision division. + */ +const divideWithPrecision = ( + num: bigint, + den: bigint, + scale: number, + precision: number +): BigDecimal => { + const numNegative = num < bigint0 + const denNegative = den < bigint0 + const negateResult = numNegative !== denNegative + + num = numNegative ? -num : num + den = denNegative ? -den : den + + // Shift digits until numerator is larger than denominator (set scale appropriately). + while (num < den) { + num *= bigint10 + scale++ + } + + // First division. + let quotient = num / den + let remainder = num % den + + if (remainder === bigint0) { + // No remainder, return immediately. + return make(negateResult ? -quotient : quotient, scale) + } + + // The quotient is guaranteed to be non-negative at this point. No need to consider sign. + let count = `${quotient}`.length + + // Shift the remainder by 1 decimal; The quotient will be 1 digit upon next division. + remainder *= bigint10 + while (remainder !== bigint0 && count < precision) { + const q = remainder / den + const r = remainder % den + quotient = quotient * bigint10 + q + remainder = r * bigint10 + + count++ + scale++ + } + + if (remainder !== bigint0) { + // Round final number with remainder. + quotient += roundTerminal(remainder / den) + } + + return make(negateResult ? -quotient : quotient, scale) +} + +/** + * Internal function used for rounding. + * + * Returns 1 if the most significant digit is >= 5, otherwise 0. + * + * This is used after dividing a number by a power of ten and rounding the last digit. + * + * @internal + */ +export const roundTerminal = (n: bigint): bigint => { + const pos = n >= bigint0 ? 0 : 1 + return Number(`${n}`[pos]) < 5 ? bigint0 : bigint1 +} + +/** + * Divides `BigDecimal`s safely. + * + * **When to use** + * + * Use to divide `BigDecimal` values while representing division by zero as + * `Option.none`. + * + * **Details** + * + * If the dividend is not a multiple of the divisor, the result will be a `BigDecimal` value + * with up to the default division precision. If the divisor is `0`, the result + * will be `Option.none()`. + * + * **Example** (Dividing decimals safely) + * + * ```ts + * import { BigDecimal, Option } from "effect" + * + * console.log( + * Option.getOrThrow( + * BigDecimal.divide( + * BigDecimal.fromStringUnsafe("6"), + * BigDecimal.fromStringUnsafe("3") + * ) + * ) + * ) // BigDecimal(2) + * console.log( + * Option.getOrThrow( + * BigDecimal.divide( + * BigDecimal.fromStringUnsafe("6"), + * BigDecimal.fromStringUnsafe("4") + * ) + * ) + * ) // BigDecimal(1.5) + * console.log( + * Option.isNone( + * BigDecimal.divide( + * BigDecimal.fromStringUnsafe("6"), + * BigDecimal.fromStringUnsafe("0") + * ) + * ) + * ) // true + * ``` + * + * @see {@link divideUnsafe} for division that throws when the divisor is zero + * @see {@link remainder} for the decimal remainder operation + * + * @category math + * @since 2.0.0 + */ +export const divide: { + (that: BigDecimal): (self: BigDecimal) => Option.Option + (self: BigDecimal, that: BigDecimal): Option.Option +} = dual(2, (self: BigDecimal, that: BigDecimal): Option.Option => { + if (that.value === bigint0) { + return Option.none() + } + + if (self.value === bigint0) { + return Option.some(zero) + } + + const scale = self.scale - that.scale + if (self.value === that.value) { + return Option.some(make(bigint1, scale)) + } + + return Option.some(divideWithPrecision(self.value, that.value, scale, DEFAULT_PRECISION)) +}) + +/** + * Provides an unsafe division operation on `BigDecimal`s. + * + * **When to use** + * + * Use when you need to divide `BigDecimal` values where the divisor is known + * to be non-zero, so division by zero should be a thrown exception. + * + * **Details** + * + * If the dividend is not a multiple of the divisor, the result will be a `BigDecimal` value + * with up to the default division precision. + * + * **Gotchas** + * + * Throws a `RangeError` if the divisor is `0`. + * + * **Example** (Dividing decimals unsafely) + * + * ```ts + * import { BigDecimal } from "effect" + * + * console.log(BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("3"))) // BigDecimal(2) + * console.log(BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("4"))) // BigDecimal(1.5) + * ``` + * + * @see {@link divide} for division that returns `Option.none` when the divisor is zero + * + * @category math + * @since 4.0.0 + */ +export const divideUnsafe: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, that: BigDecimal): BigDecimal => { + if (that.value === bigint0) { + throw new RangeError("Division by zero") + } + + if (self.value === bigint0) { + return zero + } + + const scale = self.scale - that.scale + if (self.value === that.value) { + return make(bigint1, scale) + } + return divideWithPrecision(self.value, that.value, scale, DEFAULT_PRECISION) +}) + +/** + * Provides an `Order` instance for `BigDecimal` that allows comparing and sorting BigDecimal values. + * + * **When to use** + * + * Use when you need to sort or compare decimal values through APIs that accept + * an ordering instance. + * + * **Example** (Comparing decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const a = BigDecimal.fromNumberUnsafe(1.5) + * const b = BigDecimal.fromNumberUnsafe(2.3) + * const c = BigDecimal.fromNumberUnsafe(1.5) + * + * console.log(BigDecimal.Order(a, b)) // -1 (a < b) + * console.log(BigDecimal.Order(b, a)) // 1 (b > a) + * console.log(BigDecimal.Order(a, c)) // 0 (a === c) + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.make((self, that) => { + const scmp = order.Number(sign(self), sign(that)) + if (scmp !== 0) { + return scmp + } + + if (self.scale > that.scale) { + return order.BigInt(self.value, scale(that, self.scale).value) + } + + if (self.scale < that.scale) { + return order.BigInt(scale(self, that.scale).value, that.value) + } + + return order.BigInt(self.value, that.value) +}) + +/** + * Returns `true` if the first argument is less than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one `BigDecimal` is strictly less than another. + * + * **Example** (Checking less-than comparisons) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * assert.deepStrictEqual( + * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * assert.deepStrictEqual( + * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThan: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.isLessThan(Order) + +/** + * Checks whether a given `BigDecimal` is less than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one `BigDecimal` is less than or equal to another. + * + * **Example** (Checking less-than-or-equal comparisons) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * assert.deepStrictEqual( + * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * assert.deepStrictEqual( + * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.isLessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one `BigDecimal` is strictly greater than another. + * + * **Example** (Checking greater-than comparisons) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * assert.deepStrictEqual( + * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * assert.deepStrictEqual( + * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.isGreaterThan(Order) + +/** + * Checks whether a given `BigDecimal` is greater than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one `BigDecimal` is greater than or equal to another. + * + * **Example** (Checking greater-than-or-equal comparisons) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * false + * ) + * assert.deepStrictEqual( + * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * assert.deepStrictEqual( + * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), + * true + * ) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** + * Checks whether a `BigDecimal` is between a `minimum` and `maximum` value (inclusive). + * + * **When to use** + * + * Use to test whether a `BigDecimal` falls inside an inclusive range. + * + * **Example** (Checking decimal ranges) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * const between = BigDecimal.between({ + * minimum: BigDecimal.fromStringUnsafe("1"), + * maximum: BigDecimal.fromStringUnsafe("5") + * }) + * + * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("3")), true) + * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("0")), false) + * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("6")), false) + * ``` + * + * @see {@link clamp} for forcing a `BigDecimal` into an inclusive range + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { + minimum: BigDecimal + maximum: BigDecimal + }): (self: BigDecimal) => boolean + (self: BigDecimal, options: { + minimum: BigDecimal + maximum: BigDecimal + }): boolean +} = order.isBetween(Order) + +/** + * Restricts the given `BigDecimal` to be within the range specified by the `minimum` and `maximum` values. + * + * **When to use** + * + * Use to force a `BigDecimal` into an inclusive range. + * + * **Details** + * + * If the `BigDecimal` is less than the `minimum` value, the function returns + * the `minimum` value. If it is greater than the `maximum` value, the function + * returns the `maximum` value. Otherwise, it returns the original `BigDecimal`. + * + * **Example** (Clamping decimals to a range) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * const clamp = BigDecimal.clamp({ + * minimum: BigDecimal.fromStringUnsafe("1"), + * maximum: BigDecimal.fromStringUnsafe("5") + * }) + * + * assert.deepStrictEqual( + * clamp(BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("3") + * ) + * assert.deepStrictEqual( + * clamp(BigDecimal.fromStringUnsafe("0")), + * BigDecimal.fromStringUnsafe("1") + * ) + * assert.deepStrictEqual( + * clamp(BigDecimal.fromStringUnsafe("6")), + * BigDecimal.fromStringUnsafe("5") + * ) + * ``` + * + * @see {@link between} for checking whether a `BigDecimal` is already inside a range + * + * @category math + * @since 2.0.0 + */ +export const clamp: { + (options: { + minimum: BigDecimal + maximum: BigDecimal + }): (self: BigDecimal) => BigDecimal + (self: BigDecimal, options: { + minimum: BigDecimal + maximum: BigDecimal + }): BigDecimal +} = order.clamp(Order) + +/** + * Returns the minimum between two `BigDecimal`s. + * + * **When to use** + * + * Use to select the smaller of two `BigDecimal` values. + * + * **Example** (Selecting the smaller decimal) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.min(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("2") + * ) + * ``` + * + * @see {@link max} for selecting the larger value + * + * @category math + * @since 2.0.0 + */ +export const min: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = order.min(Order) + +/** + * Returns the maximum between two `BigDecimal`s. + * + * **When to use** + * + * Use to select the larger of two `BigDecimal` values. + * + * **Example** (Selecting the larger decimal) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.max(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * BigDecimal.fromStringUnsafe("3") + * ) + * ``` + * + * @see {@link min} for selecting the smaller value + * + * @category math + * @since 2.0.0 + */ +export const max: { + (that: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, that: BigDecimal): BigDecimal +} = order.max(Order) + +/** + * Determines the sign of a given `BigDecimal`. + * + * **When to use** + * + * Use to classify a `BigDecimal` as negative, zero, or positive. + * + * **Example** (Reading decimal signs) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("-5")), -1) + * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("0")), 0) + * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("5")), 1) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sign = (n: BigDecimal): Ordering => n.value === bigint0 ? 0 : n.value < bigint0 ? -1 : 1 + +/** + * Determines the absolute value of a given `BigDecimal`. + * + * **When to use** + * + * Use to remove the sign from a `BigDecimal` while preserving its magnitude. + * + * **Example** (Calculating absolute values) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("-5")), BigDecimal.fromStringUnsafe("5")) + * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("0")), BigDecimal.fromStringUnsafe("0")) + * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("5")), BigDecimal.fromStringUnsafe("5")) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const abs = (n: BigDecimal): BigDecimal => n.value < bigint0 ? make(-n.value, n.scale) : n + +/** + * Provides a negate operation on `BigDecimal`s. + * + * **When to use** + * + * Use to flip the sign of a `BigDecimal`. + * + * **Example** (Negating decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.negate(BigDecimal.fromStringUnsafe("3")), BigDecimal.fromStringUnsafe("-3")) + * assert.deepStrictEqual(BigDecimal.negate(BigDecimal.fromStringUnsafe("-6")), BigDecimal.fromStringUnsafe("6")) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const negate = (n: BigDecimal): BigDecimal => make(-n.value, n.scale) + +/** + * Computes the decimal remainder safely when one operand is divided by a second + * operand. + * + * **When to use** + * + * Use to compute a decimal remainder while representing division by zero as + * `Option.none`. + * + * **Details** + * + * If the divisor is `0`, the result will be `Option.none()`. + * + * **Example** (Computing remainders safely) + * + * ```ts + * import { BigDecimal, Option } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.remainder( + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("2") + * ), + * Option.some(BigDecimal.fromStringUnsafe("0")) + * ) + * assert.deepStrictEqual( + * BigDecimal.remainder( + * BigDecimal.fromStringUnsafe("3"), + * BigDecimal.fromStringUnsafe("2") + * ), + * Option.some(BigDecimal.fromStringUnsafe("1")) + * ) + * assert.deepStrictEqual( + * BigDecimal.remainder( + * BigDecimal.fromStringUnsafe("-4"), + * BigDecimal.fromStringUnsafe("2") + * ), + * Option.some(BigDecimal.fromStringUnsafe("0")) + * ) + * ``` + * + * @see {@link remainderUnsafe} for remainder calculation that throws when the divisor is zero + * @see {@link divide} for decimal quotient calculation + * + * @category math + * @since 2.0.0 + */ +export const remainder: { + (divisor: BigDecimal): (self: BigDecimal) => Option.Option + (self: BigDecimal, divisor: BigDecimal): Option.Option +} = dual(2, (self: BigDecimal, divisor: BigDecimal): Option.Option => { + if (divisor.value === bigint0) { + return Option.none() + } + + const max = Math.max(self.scale, divisor.scale) + return Option.some(make(scale(self, max).value % scale(divisor, max).value, max)) +}) + +/** + * Returns the decimal remainder left over when one operand is divided by a + * non-zero second operand. + * + * **When to use** + * + * Use when you need to compute a `BigDecimal` remainder with a divisor known to + * be non-zero and want a plain `BigDecimal` result instead of an `Option`. + * + * **Gotchas** + * + * Throws a `RangeError` if the divisor is `0`. + * + * **Example** (Computing remainders unsafely) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("2")), + * BigDecimal.fromStringUnsafe("0") + * ) + * assert.deepStrictEqual( + * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("2")), + * BigDecimal.fromStringUnsafe("1") + * ) + * assert.deepStrictEqual( + * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("-4"), BigDecimal.fromStringUnsafe("2")), + * BigDecimal.fromStringUnsafe("0") + * ) + * ``` + * + * @see {@link remainder} for returning `Option.none` when the divisor is zero + * + * @category math + * @since 4.0.0 + */ +export const remainderUnsafe: { + (divisor: BigDecimal): (self: BigDecimal) => BigDecimal + (self: BigDecimal, divisor: BigDecimal): BigDecimal +} = dual(2, (self: BigDecimal, divisor: BigDecimal): BigDecimal => { + if (divisor.value === bigint0) { + throw new RangeError("Division by zero") + } + + const max = Math.max(self.scale, divisor.scale) + return make(scale(self, max).value % scale(divisor, max).value, max) +}) + +/** + * Provides an `Equivalence` instance for `BigDecimal` that determines equality between BigDecimal values. + * + * **When to use** + * + * Use when comparing decimal values through APIs that accept an equivalence + * relation. + * + * **Example** (Checking decimal equivalence) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const a = BigDecimal.fromStringUnsafe("1.50") + * const b = BigDecimal.fromStringUnsafe("1.5") + * const c = BigDecimal.fromStringUnsafe("2.0") + * + * console.log(BigDecimal.Equivalence(a, b)) // true (1.50 === 1.5) + * console.log(BigDecimal.Equivalence(a, c)) // false (1.50 !== 2.0) + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.make((self, that) => { + if (self.scale > that.scale) { + return scale(that, self.scale).value === self.value + } + + if (self.scale < that.scale) { + return scale(self, that.scale).value === that.value + } + + return self.value === that.value +}) + +/** + * Checks whether two `BigDecimal`s are equal. + * + * **When to use** + * + * Use to compare two `BigDecimal` values for numeric equality. + * + * **Example** (Checking decimal equality) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const a = BigDecimal.fromStringUnsafe("1.5") + * const b = BigDecimal.fromStringUnsafe("1.50") + * const c = BigDecimal.fromStringUnsafe("2.0") + * + * console.log(BigDecimal.equals(a, b)) // true + * console.log(BigDecimal.equals(a, c)) // false + * ``` + * + * @see {@link Equivalence} for passing decimal equality to APIs that require an `Equivalence` + * + * @category predicates + * @since 2.0.0 + */ +export const equals: { + (that: BigDecimal): (self: BigDecimal) => boolean + (self: BigDecimal, that: BigDecimal): boolean +} = dual(2, (self: BigDecimal, that: BigDecimal): boolean => Equivalence(self, that)) + +/** + * Creates a `BigDecimal` from a `bigint` value. + * + * **When to use** + * + * Use to construct an integer `BigDecimal` from a `bigint`. + * + * **Example** (Creating decimals from bigint) + * + * ```ts + * import { BigDecimal } from "effect" + * + * const decimal = BigDecimal.fromBigInt(123n) + * console.log(BigDecimal.format(decimal)) // "123" + * + * const largeBigInt = BigDecimal.fromBigInt(9007199254740991n) + * console.log(BigDecimal.format(largeBigInt)) // "9007199254740991" + * ``` + * + * @see {@link make} for constructing a decimal with an explicit scale + * + * @category constructors + * @since 2.0.0 + */ +export const fromBigInt = (n: bigint): BigDecimal => make(n, 0) + +/** + * Creates a `BigDecimal` from a finite `number`. + * + * **When to use** + * + * Use when you need to convert a trusted finite JavaScript number to a + * `BigDecimal` and want a plain result instead of an `Option`. + * + * **Gotchas** + * + * It is not recommended to convert a floating point number to a decimal + * directly, as the floating point representation may be unexpected. Throws a + * `RangeError` if the number is not finite (`NaN`, `+Infinity` or `-Infinity`). + * + * **Example** (Creating decimals from finite numbers) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.fromNumberUnsafe(123), BigDecimal.make(123n, 0)) + * assert.deepStrictEqual(BigDecimal.fromNumberUnsafe(123.456), BigDecimal.make(123456n, 3)) + * ``` + * + * @see {@link fromNumber} for returning `Option.none` when the number is not finite + * + * @category constructors + * @since 4.0.0 + */ +export const fromNumberUnsafe = (n: number): BigDecimal => { + return Option.getOrThrowWith(fromNumber(n), () => new RangeError(`Number must be finite, got ${n}`)) +} + +/** + * Creates a `BigDecimal` safely from a finite `number`. + * + * **When to use** + * + * Use to convert a finite JavaScript number to a `BigDecimal` without throwing + * on invalid input. + * + * **Details** + * + * Returns `Option.none()` for `NaN`, `+Infinity` or `-Infinity`. + * + * **Gotchas** + * + * It is not recommended to convert a floating point number to a decimal + * directly, as the floating point representation may be unexpected. + * + * **Example** (Creating decimals from numbers safely) + * + * ```ts + * import { BigDecimal, Option } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.fromNumber(123), Option.some(BigDecimal.make(123n, 0))) + * assert.deepStrictEqual( + * BigDecimal.fromNumber(123.456), + * Option.some(BigDecimal.make(123456n, 3)) + * ) + * assert.deepStrictEqual(BigDecimal.fromNumber(Infinity), Option.none()) + * ``` + * + * @see {@link fromNumberUnsafe} for throwing when the number is not finite + * @see {@link fromString} for parsing decimal strings directly + * + * @category constructors + * @since 2.0.0 + */ +export const fromNumber = (n: number): Option.Option => { + if (!Number.isFinite(n)) { + return Option.none() + } + + const string = `${n}` + if (string.includes("e")) { + return fromString(string) + } + + const [lead, trail = ""] = string.split(".") + return Option.some(make(BigInt(`${lead}${trail}`), trail.length)) +} + +/** + * Parses a decimal string into a `BigDecimal` safely. + * + * **When to use** + * + * Use to parse external decimal text without throwing on invalid input. + * + * **Details** + * + * Returns `Option.some` for valid decimal or exponent notation and + * `Option.none` when the string cannot be parsed or would produce an unsafe + * scale. The empty string parses as zero. + * + * **Example** (Parsing decimal strings safely) + * + * ```ts + * import { BigDecimal, Option } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.fromString("123"), Option.some(BigDecimal.make(123n, 0))) + * assert.deepStrictEqual( + * BigDecimal.fromString("123.456"), + * Option.some(BigDecimal.make(123456n, 3)) + * ) + * assert.deepStrictEqual(BigDecimal.fromString("123.abc"), Option.none()) + * ``` + * + * @see {@link fromStringUnsafe} for parsing that throws on invalid input + * @see {@link fromNumber} for converting finite JavaScript numbers + * + * @category constructors + * @since 2.0.0 + */ +export const fromString = (s: string): Option.Option => { + if (s === "") { + return Option.some(zero) + } + + let base: string + let exp: number + const seperator = s.search(/[eE]/) + if (seperator !== -1) { + const trail = s.slice(seperator + 1) + base = s.slice(0, seperator) + exp = Number(trail) + if (base === "" || !Number.isSafeInteger(exp) || !FINITE_INT_REGEXP.test(trail)) { + return Option.none() + } + } else { + base = s + exp = 0 + } + + let digits: string + let offset: number + const dot = base.search(/\./) + if (dot !== -1) { + const lead = base.slice(0, dot) + const trail = base.slice(dot + 1) + digits = `${lead}${trail}` + offset = trail.length + } else { + digits = base + offset = 0 + } + + if (!FINITE_INT_REGEXP.test(digits)) { + return Option.none() + } + + const scale = offset - exp + if (!Number.isSafeInteger(scale)) { + return Option.none() + } + + return Option.some(make(BigInt(digits), scale)) +} + +/** + * Parses a decimal string into a `BigDecimal`, throwing if the string is + * invalid. + * + * **When to use** + * + * Use when you expect decimal text to be valid and want parse errors to throw. + * + * **Details** + * + * Accepts the same syntax as `fromString`. Use `fromString` when invalid input + * should be represented as `Option.none` instead of throwing. + * + * **Example** (Parsing decimal strings unsafely) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.fromStringUnsafe("123"), BigDecimal.make(123n, 0)) + * assert.deepStrictEqual(BigDecimal.fromStringUnsafe("123.456"), BigDecimal.make(123456n, 3)) + * assert.throws(() => BigDecimal.fromStringUnsafe("123.abc")) + * ``` + * + * @see {@link fromString} for returning `Option.none` on invalid input + * + * @category constructors + * @since 4.0.0 + */ +export const fromStringUnsafe = (s: string): BigDecimal => { + return Option.getOrThrowWith(fromString(s), () => new Error(`Invalid numerical string: ${s}`)) +} + +/** + * Formats a `BigDecimal` as a string. + * + * **When to use** + * + * Use to render a `BigDecimal` as plain decimal text when possible. + * + * **Details** + * + * The value is normalized before formatting. Scientific notation is used when + * the absolute value of the normalized scale is at least `16`; otherwise plain + * decimal notation is used. + * + * **Example** (Formatting decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("-5")), "-5") + * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("123.456")), "123.456") + * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("-0.00000123")), "-0.00000123") + * ``` + * + * @see {@link toExponential} for always rendering scientific notation + * + * @category converting + * @since 2.0.0 + */ +export const format = (n: BigDecimal): string => { + const normalized = normalize(n) + if (Math.abs(normalized.scale) >= 16) { + return toExponential(normalized) + } + + const negative = normalized.value < bigint0 + const absolute = negative ? `${normalized.value}`.substring(1) : `${normalized.value}` + + let before: string + let after: string + + if (normalized.scale >= absolute.length) { + before = "0" + after = "0".repeat(normalized.scale - absolute.length) + absolute + } else { + const location = absolute.length - normalized.scale + if (location > absolute.length) { + const zeros = location - absolute.length + before = `${absolute}${"0".repeat(zeros)}` + after = "" + } else { + after = absolute.slice(location) + before = absolute.slice(0, location) + } + } + + const complete = after === "" ? before : `${before}.${after}` + return negative ? `-${complete}` : complete +} + +/** + * Formats a given `BigDecimal` as a `string` in scientific notation. + * + * **When to use** + * + * Use to render a `BigDecimal` in scientific notation. + * + * **Example** (Formatting decimals exponentially) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.toExponential(BigDecimal.make(123456n, -5)), "1.23456e+10") + * ``` + * + * @see {@link format} for plain decimal formatting when possible + * + * @category converting + * @since 3.11.0 + */ +export const toExponential = (n: BigDecimal): string => { + if (isZero(n)) { + return "0e+0" + } + + const normalized = normalize(n) + const digits = `${abs(normalized).value}` + const head = digits.slice(0, 1) + const tail = digits.slice(1) + + let output = `${isNegative(normalized) ? "-" : ""}${head}` + if (tail !== "") { + output += `.${tail}` + } + + const exp = tail.length - normalized.scale + return `${output}e${exp >= 0 ? "+" : ""}${exp}` +} + +/** + * Converts a `BigDecimal` to a JavaScript `number`. + * + * **When to use** + * + * Use when you need a JavaScript number at an interop boundary where precision + * loss is acceptable. + * + * **Gotchas** + * + * This conversion is unsafe because the result can lose integer or fractional + * precision, round to a nearby representable value, or become `Infinity` when + * the decimal cannot be represented as a finite JavaScript `number`. + * + * **Example** (Converting decimals to numbers) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.toNumberUnsafe(BigDecimal.fromStringUnsafe("123.456")), 123.456) + * ``` + * + * @see {@link format} for preserving decimal precision as text + * + * @category converting + * @since 4.0.0 + */ +export const toNumberUnsafe = (n: BigDecimal): number => Number(format(n)) + +/** + * Checks whether a given `BigDecimal` is an integer. + * + * **When to use** + * + * Use to test whether a `BigDecimal` has no fractional decimal part. + * + * **Example** (Checking integer decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("0")), true) + * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1")), true) + * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1.1")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isInteger = (n: BigDecimal): boolean => normalize(n).scale <= 0 + +/** + * Checks whether a given `BigDecimal` is `0`. + * + * **When to use** + * + * Use to test whether a `BigDecimal` is exactly zero. + * + * **Example** (Checking zero decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.isZero(BigDecimal.fromStringUnsafe("0")), true) + * assert.deepStrictEqual(BigDecimal.isZero(BigDecimal.fromStringUnsafe("1")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isZero = (n: BigDecimal): boolean => n.value === bigint0 + +/** + * Checks whether a given `BigDecimal` is negative. + * + * **When to use** + * + * Use to test whether a `BigDecimal` is less than zero. + * + * **Example** (Checking negative decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("-1")), true) + * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("0")), false) + * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("1")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isNegative = (n: BigDecimal): boolean => n.value < bigint0 + +/** + * Checks whether a given `BigDecimal` is positive. + * + * **When to use** + * + * Use to test whether a `BigDecimal` is greater than zero. + * + * **Example** (Checking positive decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("-1")), false) + * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("0")), false) + * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("1")), true) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isPositive = (n: BigDecimal): boolean => n.value > bigint0 + +const isBigDecimalArgs = (args: IArguments) => isBigDecimal(args[0]) + +/** + * Rounding modes for `BigDecimal`. + * + * **When to use** + * + * Use with `round` to choose how discarded digits affect a `BigDecimal` + * rounded to a target scale. + * + * **Details** + * + * - `ceil`: round towards positive infinity + * - `floor`: round towards negative infinity + * - `to-zero`: round towards zero + * - `from-zero`: round away from zero + * - `half-ceil`: round to the nearest neighbor; if equidistant round towards positive infinity + * - `half-floor`: round to the nearest neighbor; if equidistant round towards negative infinity + * - `half-to-zero`: round to the nearest neighbor; if equidistant round towards zero + * - `half-from-zero`: round to the nearest neighbor; if equidistant round away from zero + * - `half-even`: round to the nearest neighbor; if equidistant round to the neighbor with an even digit + * - `half-odd`: round to the nearest neighbor; if equidistant round to the neighbor with an odd digit + * + * @see {@link round} for configurable rounding with a `RoundingMode` + * @see {@link ceil} for fixed rounding toward positive infinity + * @see {@link floor} for fixed rounding toward negative infinity + * @see {@link truncate} for fixed rounding toward zero + * + * @category math + * @since 3.16.0 + */ +export type RoundingMode = + | "ceil" + | "floor" + | "to-zero" + | "from-zero" + | "half-ceil" + | "half-floor" + | "half-to-zero" + | "half-from-zero" + | "half-even" + | "half-odd" + +/** + * Computes a rounded `BigDecimal` at the given scale with the specified rounding mode. + * + * **When to use** + * + * Use to round a decimal at a requested scale with an explicit rounding mode. + * + * **Example** (Rounding decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.round(BigDecimal.fromStringUnsafe("145"), { mode: "from-zero", scale: -1 }), + * BigDecimal.fromStringUnsafe("150") + * ) + * assert.deepStrictEqual( + * BigDecimal.round(BigDecimal.fromStringUnsafe("-14.5")), + * BigDecimal.fromStringUnsafe("-15") + * ) + * ``` + * + * @see {@link ceil} for fixed rounding toward positive infinity + * @see {@link floor} for fixed rounding toward negative infinity + * @see {@link truncate} for fixed rounding toward zero + * + * @category math + * @since 3.16.0 + */ +export const round: { + (options: { scale?: number; mode?: RoundingMode }): (self: BigDecimal) => BigDecimal + (n: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, options?: { scale?: number; mode?: RoundingMode }): BigDecimal => { + const mode = options?.mode ?? "half-from-zero" + const scale = options?.scale ?? 0 + + switch (mode) { + case "ceil": + return ceil(self, scale) + + case "floor": + return floor(self, scale) + + case "to-zero": + return truncate(self, scale) + + case "from-zero": + return (isPositive(self) ? ceil(self, scale) : floor(self, scale)) + + case "half-ceil": + return floor(sum(self, make(bigint5, scale + 1)), scale) + + case "half-floor": + return ceil(sum(self, make(bigint_5, scale + 1)), scale) + + case "half-to-zero": + return isNegative(self) + ? floor(sum(self, make(bigint5, scale + 1)), scale) + : ceil(sum(self, make(bigint_5, scale + 1)), scale) + + case "half-from-zero": + return isNegative(self) + ? ceil(sum(self, make(bigint_5, scale + 1)), scale) + : floor(sum(self, make(bigint5, scale + 1)), scale) + } + + const halfCeil = floor(sum(self, make(bigint5, scale + 1)), scale) + const halfFloor = ceil(sum(self, make(bigint_5, scale + 1)), scale) + const digit = digitAt(halfCeil, scale) + + switch (mode) { + case "half-even": + return equals(halfCeil, halfFloor) ? halfCeil : (digit % bigint2 === bigint0) ? halfCeil : halfFloor + + case "half-odd": + return equals(halfCeil, halfFloor) ? halfCeil : (digit % bigint2 === bigint0) ? halfFloor : halfCeil + } +}) + +/** + * Computes a truncated `BigDecimal` at the given scale. This removes fractional digits beyond the scale, + * rounding toward zero. + * + * **When to use** + * + * Use when you need to discard fractional digits beyond a scale rather than + * round half up, half down, or toward an infinity. + * + * **Example** (Truncating decimals) + * + * ```ts + * import { BigDecimal } from "effect" + * + * console.log(BigDecimal.truncate(BigDecimal.fromStringUnsafe("145"), -1)) // BigDecimal(140) + * console.log(BigDecimal.truncate(BigDecimal.fromStringUnsafe("-14.5"))) // BigDecimal(-14) + * ``` + * + * @see {@link round} for configurable rounding modes + * @see {@link ceil} for rounding toward positive infinity + * @see {@link floor} for rounding toward negative infinity + * + * @category math + * @since 3.16.0 + */ +export const truncate: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + if (self.scale <= scale) { + return self + } + + // BigInt division truncates towards zero + return make(self.value / (bigint10 ** BigInt(self.scale - scale)), scale) +}) + +/** + * Computes the ceiling of a `BigDecimal` at the given scale. + * + * **When to use** + * + * Use to round a decimal toward positive infinity at a requested scale. + * + * **Details** + * + * The default scale is `0`. Positive scales keep digits to the right of the + * decimal point, and negative scales round positions to the left of the decimal + * point. + * + * @see {@link floor} for rounding toward negative infinity + * @see {@link truncate} for rounding toward zero + * @see {@link round} for configurable rounding modes + * + * **Example** (Rounding decimals up) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.ceil(BigDecimal.fromStringUnsafe("145"), -1), + * BigDecimal.fromStringUnsafe("150") + * ) + * assert.deepStrictEqual(BigDecimal.ceil(BigDecimal.fromStringUnsafe("-14.5")), BigDecimal.fromStringUnsafe("-14")) + * ``` + * + * @category math + * @since 3.16.0 + */ +export const ceil: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + const truncated = truncate(self, scale) + + if (isPositive(self) && isLessThan(truncated, self)) { + return sum(truncated, make(bigint1, scale)) + } + + return truncated +}) + +/** + * Internal function used by `round` for `half-even` and `half-odd` rounding modes. + * + * Returns the digit at the position of the given `scale` within the `BigDecimal`. + * + * @internal + */ +export const digitAt: { + (scale: number): (self: BigDecimal) => bigint + (self: BigDecimal, scale: number): bigint +} = dual(2, (self: BigDecimal, scale: number): bigint => { + if (self.scale < scale) { + return bigint0 + } + + const scaled = self.value / (bigint10 ** BigInt(self.scale - scale)) + return scaled % bigint10 +}) + +/** + * Computes the floor of a `BigDecimal` at the given scale. + * + * **When to use** + * + * Use to round a decimal toward negative infinity at a requested scale. + * + * **Example** (Rounding decimals down) + * + * ```ts + * import { BigDecimal } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * BigDecimal.floor(BigDecimal.fromStringUnsafe("145"), -1), + * BigDecimal.fromStringUnsafe("140") + * ) + * assert.deepStrictEqual( + * BigDecimal.floor(BigDecimal.fromStringUnsafe("-14.5")), + * BigDecimal.fromStringUnsafe("-15") + * ) + * ``` + * + * @see {@link ceil} for rounding toward positive infinity + * @see {@link truncate} for rounding toward zero + * @see {@link round} for configurable rounding modes + * + * @category math + * @since 3.16.0 + */ +export const floor: { + (scale: number): (self: BigDecimal) => BigDecimal + (self: BigDecimal, scale?: number): BigDecimal +} = dual(isBigDecimalArgs, (self: BigDecimal, scale: number = 0): BigDecimal => { + const truncated = truncate(self, scale) + + if (isNegative(self) && isGreaterThan(truncated, self)) { + return sum(truncated, make(bigint_1, scale)) + } + + return truncated +}) diff --git a/repos/effect/packages/effect/src/BigInt.ts b/repos/effect/packages/effect/src/BigInt.ts new file mode 100644 index 0000000..34a9736 --- /dev/null +++ b/repos/effect/packages/effect/src/BigInt.ts @@ -0,0 +1,1029 @@ +/** + * Works with JavaScript `bigint` values. + * + * This module exposes the native `BigInt` constructor together with helpers for + * checking, arithmetic, comparison, range checks, safe parsing and conversions + * that return `Option`, integer square roots, aggregation, ordering, + * equivalence, reducers, and combiners. + * + * @since 2.0.0 + */ + +import * as Combiner from "./Combiner.ts" +import * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import type { Ordering } from "./Ordering.ts" +import * as predicate from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Exposes the global bigint constructor for JavaScript bigint coercion. + * + * **When to use** + * + * Use to access native JavaScript bigint constructor coercion from the Effect + * module namespace. + * + * **Gotchas** + * + * This follows native `BigInt` coercion rules. It throws for invalid strings or + * non-integral numbers, and whitespace-only strings coerce to `0n`. + * + * @see {@link fromString} for parsing strings into an `Option` + * @see {@link fromNumber} for converting safe integers into an `Option` + * + * **Example** (Constructing bigints) + * + * ```ts + * import { BigInt } from "effect" + * + * const bigInt = BigInt.BigInt(123) + * console.log(bigInt) // 123n + * + * const fromString = BigInt.BigInt("456") + * console.log(fromString) // 456n + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const BigInt = globalThis.BigInt + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const bigint2 = BigInt(2) + +/** + * Checks whether a value is a `bigint`. + * + * **When to use** + * + * Use to validate unknown input and narrow it to `bigint`. + * + * **Example** (Checking for bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.isBigInt(1n), true) + * assert.deepStrictEqual(BigInt.isBigInt(1), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBigInt: (u: unknown) => u is bigint = predicate.isBigInt + +/** + * Provides an addition operation on `bigint`s. + * + * **When to use** + * + * Use when you need a binary addition function for piping or higher-order APIs + * instead of the infix addition operator. + * + * **Example** (Adding bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.sum(2n, 3n), 5n) + * ``` + * + * @see {@link sumAll} for summing an iterable of `bigint` values + * + * @category math + * @since 2.0.0 + */ +export const sum: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self + that) + +/** + * Provides a multiplication operation on `bigint`s. + * + * **When to use** + * + * Use to multiply two `bigint` values. + * + * **Example** (Multiplying bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.multiply(2n, 3n), 6n) + * ``` + * + * @see {@link multiplyAll} for multiplying an iterable of `bigint` values + * + * @category math + * @since 2.0.0 + */ +export const multiply: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self * that) + +/** + * Provides a subtraction operation on `bigint`s. + * + * **When to use** + * + * Use to subtract one `bigint` value from another. + * + * **Example** (Subtracting bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.subtract(2n, 3n), -1n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const subtract: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self - that) + +/** + * Divides one `bigint` by another safely. + * + * **When to use** + * + * Use to divide `bigint` values while representing division by zero as + * `Option.none`. + * + * **Details** + * + * Uses JavaScript `bigint` division, so non-exact quotients are truncated + * toward zero. Returns `Option.none()` when the divisor is `0n`. + * + * **Example** (Dividing bigints safely) + * + * ```ts + * import { BigInt, Option } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.divide(6n, 3n), Option.some(2n)) + * assert.deepStrictEqual(BigInt.divide(6n, 0n), Option.none()) + * ``` + * + * @see {@link divideUnsafe} for division that throws when the divisor is `0n` + * @see {@link remainder} for the JavaScript remainder operation + * + * @category math + * @since 2.0.0 + */ +export const divide: { + (that: bigint): (self: bigint) => Option.Option + (self: bigint, that: bigint): Option.Option +} = dual( + 2, + (self: bigint, that: bigint): Option.Option => that === bigint0 ? Option.none() : Option.some(self / that) +) + +/** + * Divides one `bigint` by another, throwing if the divisor is zero. + * + * **When to use** + * + * Use to divide `bigint` values where the divisor is known to be non-zero and + * division by zero should be a thrown exception. + * + * **Details** + * + * Uses JavaScript `bigint` division, so non-exact quotients are truncated + * toward zero. + * + * **Gotchas** + * + * Throws a `RangeError` when the divisor is `0n`. + * + * **Example** (Dividing bigints unsafely) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.divideUnsafe(6n, 3n), 2n) + * assert.deepStrictEqual(BigInt.divideUnsafe(6n, 4n), 1n) + * ``` + * + * @see {@link divide} for division that returns `Option.none` when the divisor is `0n` + * + * @category math + * @since 4.0.0 + */ +export const divideUnsafe: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => self / that) + +/** + * Returns the result of adding `1n` to a `bigint`. + * + * **When to use** + * + * Use to increment a `bigint` counter by one. + * + * **Example** (Incrementing a bigint) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.increment(2n), 3n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const increment = (n: bigint): bigint => n + bigint1 + +/** + * Returns the result of subtracting `1n` from a `bigint`. + * + * **When to use** + * + * Use to decrement a `bigint` counter by one. + * + * **Example** (Decrementing a bigint) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.decrement(3n), 2n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const decrement = (n: bigint): bigint => n - bigint1 + +/** + * Provides an `Order` instance for `bigint` that allows comparing and sorting BigInt values. + * + * **When to use** + * + * Use when you need to sort or compare bigint values through APIs that accept + * an ordering instance. + * + * **Example** (Comparing bigints with Order) + * + * ```ts + * import { BigInt } from "effect" + * + * const a = 123n + * const b = 456n + * const c = 123n + * + * console.log(BigInt.Order(a, b)) // -1 (a < b) + * console.log(BigInt.Order(b, a)) // 1 (b > a) + * console.log(BigInt.Order(a, c)) // 0 (a === c) + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.BigInt + +/** + * Equivalence instance for bigints using strict equality (`===`). + * + * **When to use** + * + * Use when checking bigint equality through APIs that accept an equivalence + * relation. + * + * **Example** (Comparing bigints for equivalence) + * + * ```ts + * import { BigInt } from "effect" + * + * console.log(BigInt.Equivalence(1n, 1n)) // true + * console.log(BigInt.Equivalence(1n, 2n)) // false + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.BigInt + +/** + * Returns `true` if the first argument is less than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one `bigint` is strictly less than another. + * + * **Example** (Checking less-than comparisons) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.isLessThan(2n, 3n), true) + * assert.deepStrictEqual(BigInt.isLessThan(3n, 3n), false) + * assert.deepStrictEqual(BigInt.isLessThan(4n, 3n), false) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThan: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.isLessThan(Order) + +/** + * Returns a function that checks if a given `bigint` is less than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one `bigint` is less than or equal to another. + * + * **Example** (Checking less-than-or-equal comparisons) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(2n, 3n), true) + * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(3n, 3n), true) + * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(4n, 3n), false) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.isLessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one `bigint` is strictly greater than another. + * + * **Example** (Checking greater-than comparisons) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.isGreaterThan(2n, 3n), false) + * assert.deepStrictEqual(BigInt.isGreaterThan(3n, 3n), false) + * assert.deepStrictEqual(BigInt.isGreaterThan(4n, 3n), true) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.isGreaterThan(Order) + +/** + * Returns a function that checks if a given `bigint` is greater than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one `bigint` is greater than or equal to another. + * + * **Example** (Checking greater-than-or-equal comparisons) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(2n, 3n), false) + * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(3n, 3n), true) + * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(4n, 3n), true) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: bigint): (self: bigint) => boolean + (self: bigint, that: bigint): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** + * Checks whether a `bigint` is between a `minimum` and `maximum` value (inclusive). + * + * **When to use** + * + * Use to test whether a `bigint` falls inside an inclusive range. + * + * **Example** (Checking whether a bigint is within bounds) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * const between = BigInt.between({ minimum: 0n, maximum: 5n }) + * + * assert.deepStrictEqual(between(3n), true) + * assert.deepStrictEqual(between(-1n), false) + * assert.deepStrictEqual(between(6n), false) + * ``` + * + * @see {@link clamp} for forcing a `bigint` into an inclusive range + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { + minimum: bigint + maximum: bigint + }): (self: bigint) => boolean + (self: bigint, options: { + minimum: bigint + maximum: bigint + }): boolean +} = order.isBetween(Order) + +/** + * Restricts the given `bigint` to be within the range specified by the `minimum` and `maximum` values. + * + * **When to use** + * + * Use to force a `bigint` into an inclusive range. + * + * **Details** + * + * If the `bigint` is less than the minimum, the function returns the minimum. + * If the `bigint` is greater than the maximum, the function returns the + * maximum. Otherwise, it returns the original `bigint`. + * + * **Example** (Clamping a bigint to bounds) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * const clamp = BigInt.clamp({ minimum: 1n, maximum: 5n }) + * + * assert.equal(clamp(3n), 3n) + * assert.equal(clamp(0n), 1n) + * assert.equal(clamp(6n), 5n) + * ``` + * + * @see {@link between} for checking whether a `bigint` is already inside a range + * + * @category math + * @since 2.0.0 + */ +export const clamp: { + (options: { + minimum: bigint + maximum: bigint + }): (self: bigint) => bigint + (self: bigint, options: { + minimum: bigint + maximum: bigint + }): bigint +} = order.clamp(Order) + +/** + * Returns the minimum between two `bigint`s. + * + * **When to use** + * + * Use to select the smaller of two `bigint` values. + * + * **Example** (Finding the minimum bigint) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.min(2n, 3n), 2n) + * ``` + * + * @see {@link max} for selecting the larger value + * + * @category math + * @since 2.0.0 + */ +export const min: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = order.min(Order) + +/** + * Returns the maximum between two `bigint`s. + * + * **When to use** + * + * Use to select the larger of two `bigint` values. + * + * **Example** (Finding the maximum bigint) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.max(2n, 3n), 3n) + * ``` + * + * @see {@link min} for selecting the smaller value + * + * @category math + * @since 2.0.0 + */ +export const max: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = order.max(Order) + +/** + * Determines the sign of a given `bigint`. + * + * **When to use** + * + * Use to classify a `bigint` as negative, zero, or positive. + * + * **Example** (Determining bigint signs) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.sign(-5n), -1) + * assert.deepStrictEqual(BigInt.sign(0n), 0) + * assert.deepStrictEqual(BigInt.sign(5n), 1) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sign = (n: bigint): Ordering => order.BigInt(n, bigint0) + +/** + * Determines the absolute value of a given `bigint`. + * + * **When to use** + * + * Use to remove the sign from a `bigint` while preserving its magnitude. + * + * **Example** (Calculating absolute values) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.abs(-5n), 5n) + * assert.deepStrictEqual(BigInt.abs(0n), 0n) + * assert.deepStrictEqual(BigInt.abs(5n), 5n) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const abs = (n: bigint): bigint => (n < bigint0 ? -n : n) + +/** + * Determines the greatest common divisor of two `bigint`s. + * + * **When to use** + * + * Use to compute the greatest common divisor of two integer values. + * + * **Example** (Calculating greatest common divisors) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.gcd(2n, 3n), 1n) + * assert.deepStrictEqual(BigInt.gcd(2n, 4n), 2n) + * assert.deepStrictEqual(BigInt.gcd(16n, 24n), 8n) + * ``` + * + * @see {@link lcm} for computing the least common multiple + * + * @category math + * @since 2.0.0 + */ +export const gcd: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => { + while (that !== bigint0) { + const t = that + that = self % that + self = t + } + return self +}) + +/** + * Determines the least common multiple of two `bigint`s. + * + * **When to use** + * + * Use to compute the least common multiple of two integer values. + * + * **Example** (Calculating least common multiples) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.lcm(2n, 3n), 6n) + * assert.deepStrictEqual(BigInt.lcm(2n, 4n), 4n) + * assert.deepStrictEqual(BigInt.lcm(16n, 24n), 48n) + * ``` + * + * @see {@link gcd} for computing the greatest common divisor + * + * @category math + * @since 2.0.0 + */ +export const lcm: { + (that: bigint): (self: bigint) => bigint + (self: bigint, that: bigint): bigint +} = dual(2, (self: bigint, that: bigint): bigint => (self * that) / gcd(self, that)) + +/** + * Returns the integer square root of a non-negative `bigint`. + * + * **When to use** + * + * Use when you need to compute an integer square root for a `bigint` that has + * already been validated as non-negative, and you want negative input to throw + * instead of returning `Option.none`. + * + * **Details** + * + * For non-perfect squares, returns the largest `bigint` whose square is less + * than or equal to the input. + * + * **Gotchas** + * + * Throws a `RangeError` if the input is negative. + * + * **Example** (Calculating square roots unsafely) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.sqrtUnsafe(4n), 2n) + * assert.deepStrictEqual(BigInt.sqrtUnsafe(9n), 3n) + * assert.deepStrictEqual(BigInt.sqrtUnsafe(16n), 4n) + * ``` + * + * @see {@link sqrt} for returning `Option.none` when the input is negative + * + * @category math + * @since 4.0.0 + */ +export const sqrtUnsafe = (n: bigint): bigint => { + if (n < bigint0) { + throw new RangeError("Cannot take the square root of a negative number") + } + if (n < bigint2) { + return n + } + let x = n / bigint2 + while (x * x > n) { + x = ((n / x) + x) / bigint2 + } + return x +} + +/** + * Computes the integer square root of a `bigint` safely. + * + * **When to use** + * + * Use to compute an integer square root while representing negative input as + * `Option.none`. + * + * **Details** + * + * For non-perfect squares, returns the largest `bigint` whose square is less + * than or equal to the input. Returns `Option.none()` when the input is + * negative. + * + * **Example** (Calculating square roots safely) + * + * ```ts + * import { BigInt } from "effect" + * + * BigInt.sqrt(4n) // Option.some(2n) + * BigInt.sqrt(9n) // Option.some(3n) + * BigInt.sqrt(16n) // Option.some(4n) + * BigInt.sqrt(-1n) // Option.none() + * ``` + * + * @see {@link sqrtUnsafe} for square root computation that throws on negative input + * + * @category math + * @since 2.0.0 + */ +export const sqrt = (n: bigint): Option.Option => + isGreaterThanOrEqualTo(n, bigint0) ? Option.some(sqrtUnsafe(n)) : Option.none() + +/** + * Takes an `Iterable` of `bigint`s and returns their sum as a single `bigint`. Returns `0n` for an empty iterable. + * + * **When to use** + * + * Use when you want an immediate aggregate from an iterable instead of a + * folding reducer owned by another API. + * + * **Example** (Summing iterable bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.sumAll([2n, 3n, 4n]), 9n) + * ``` + * + * @see {@link sum} for adding two `bigint` values + * @see {@link ReducerSum} for summing through APIs that consume a `Reducer` + * + * @category math + * @since 2.0.0 + */ +export const sumAll = (collection: Iterable): bigint => { + let out = bigint0 + for (const n of collection) { + out += n + } + return out +} + +/** + * Takes an `Iterable` of `bigint`s and returns their product as a single `bigint`. Returns `1n` for an empty iterable. + * + * **When to use** + * + * Use to multiply all `bigint` values in an iterable. + * + * **Example** (Multiplying iterable bigints) + * + * ```ts + * import { BigInt } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(BigInt.multiplyAll([2n, 3n, 4n]), 24n) + * ``` + * + * @see {@link multiply} for multiplying two `bigint` values + * @see {@link ReducerMultiply} for multiplying through APIs that consume a `Reducer` + * + * @category math + * @since 2.0.0 + */ +export const multiplyAll = (collection: Iterable): bigint => { + let out = bigint1 + for (const n of collection) { + if (n === bigint0) { + return bigint0 + } + out *= n + } + return out +} + +/** + * Converts a `bigint` to a `number` safely. + * + * **When to use** + * + * Use to convert a `bigint` to a JavaScript number only when it is a safe + * integer. + * + * **Details** + * + * If the `bigint` is outside the safe integer range for JavaScript (`Number.MAX_SAFE_INTEGER` + * and `Number.MIN_SAFE_INTEGER`), it returns `Option.none()`. + * + * **Example** (Converting bigints to numbers) + * + * ```ts + * import { BigInt as BI } from "effect" + * + * BI.toNumber(42n) // Option.some(42) + * BI.toNumber(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // Option.none() + * BI.toNumber(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // Option.none() + * ``` + * + * @see {@link fromNumber} for converting a safe integer number to `bigint` + * + * @category converting + * @since 2.0.0 + */ +export const toNumber = (b: bigint): Option.Option => { + if (b > BigInt(Number.MAX_SAFE_INTEGER) || b < BigInt(Number.MIN_SAFE_INTEGER)) { + return Option.none() + } + return Option.some(Number(b)) +} + +/** + * Parses a string into a `bigint` safely. + * + * **When to use** + * + * Use to parse a string as a `bigint` without throwing on invalid input. + * + * **Details** + * + * If the string is empty or contains characters that cannot be converted into a + * `bigint`, it returns `Option.none()`. + * + * **Example** (Parsing strings as bigints) + * + * ```ts + * import { BigInt } from "effect" + * + * BigInt.fromString("42") // Option.some(42n) + * BigInt.fromString(" ") // Option.none() + * BigInt.fromString("a") // Option.none() + * ``` + * + * @see {@link BigInt} for native constructor coercion that throws on invalid input + * + * @category converting + * @since 2.4.12 + */ +export const fromString = (s: string): Option.Option => { + try { + return s.trim() === "" + ? Option.none() + : Option.some(BigInt(s)) + } catch { + return Option.none() + } +} + +/** + * Converts a number to a `bigint`. + * + * **When to use** + * + * Use to convert a JavaScript number to `bigint` only when it is a safe integer. + * + * **Details** + * + * If the number is outside the safe integer range for JavaScript + * (`Number.MAX_SAFE_INTEGER` and `Number.MIN_SAFE_INTEGER`) or if the number is + * not a valid `bigint`, it returns `Option.none()`. + * + * **Example** (Converting numbers to bigints) + * + * ```ts + * import { BigInt } from "effect" + * + * BigInt.fromNumber(42) // Option.some(42n) + * + * BigInt.fromNumber(Number.MAX_SAFE_INTEGER + 1) // Option.none() + * BigInt.fromNumber(Number.MIN_SAFE_INTEGER - 1) // Option.none() + * ``` + * + * @see {@link toNumber} for converting `bigint` values back to safe integer numbers + * @see {@link BigInt} for native constructor coercion + * + * @category converting + * @since 2.4.12 + */ +export function fromNumber(n: number): Option.Option { + if (n > Number.MAX_SAFE_INTEGER || n < Number.MIN_SAFE_INTEGER) { + return Option.none() + } + + try { + return Option.some(BigInt(n)) + } catch { + return Option.none() + } +} + +/** + * Returns the JavaScript remainder of dividing one `bigint` by another. + * + * **When to use** + * + * Use when you want native remainder semantics, including signed remainders and + * a thrown division-by-zero error. + * + * **Gotchas** + * + * Throws a `RangeError` when the divisor is `0n`. + * + * **Example** (Calculating remainders) + * + * ```ts + * import { BigInt } from "effect" + * + * BigInt.remainder(10n, 3n) // 1n + * + * BigInt.remainder(15n, 4n) // 3n + * ``` + * + * @see {@link divide} for quotient calculation with division-by-zero represented as `Option.none` + * + * @category math + * @since 4.0.0 + */ +export const remainder: { + (divisor: bigint): (self: bigint) => bigint + (self: bigint, divisor: bigint): bigint +} = dual(2, (self: bigint, divisor: bigint): bigint => self % divisor) + +/** + * Reducer for combining `bigint`s using addition. + * + * **When to use** + * + * Use to sum many `bigint` values through APIs that consume a `Reducer`. + * + * **Details** + * + * The initial value is `0n`, so `combineAll([])` returns `0n`. + * + * @see {@link sumAll} for summing an iterable directly + * @see {@link ReducerMultiply} for multiplying `bigint` values + * + * @category math + * @since 4.0.0 + */ +export const ReducerSum: Reducer.Reducer = Reducer.make((a, b) => a + b, bigint0) + +/** + * Reducer for combining `bigint`s using multiplication. + * + * **When to use** + * + * Use to multiply many `bigint` values through APIs that consume a `Reducer`. + * + * **Details** + * + * The initial value is `1n`, so `combineAll([])` returns `1n`. + * + * @see {@link multiplyAll} for multiplying an iterable directly + * @see {@link ReducerSum} for summing `bigint` values + * + * @category math + * @since 4.0.0 + */ +export const ReducerMultiply: Reducer.Reducer = Reducer.make((a, b) => a * b, bigint1, (collection) => { + let acc = bigint1 + for (const n of collection) { + if (n === bigint0) return bigint0 + acc *= n + } + return acc +}) + +/** + * Combiner that returns the maximum `bigint`. + * + * **When to use** + * + * Use to keep the largest `bigint` when an API consumes a `Combiner`. + * + * @see {@link CombinerMin} for keeping the smallest `bigint` + * @see {@link max} for comparing two `bigint` values directly + * + * @category math + * @since 4.0.0 + */ +export const CombinerMax: Combiner.Combiner = Combiner.max(Order) + +/** + * Combiner that returns the minimum `bigint`. + * + * **When to use** + * + * Use to keep the smallest `bigint` through APIs that consume a `Combiner`. + * + * @see {@link CombinerMax} for keeping the largest `bigint` + * @see {@link min} for comparing two `bigint` values directly + * + * @category math + * @since 4.0.0 + */ +export const CombinerMin: Combiner.Combiner = Combiner.min(Order) diff --git a/repos/effect/packages/effect/src/Boolean.ts b/repos/effect/packages/effect/src/Boolean.ts new file mode 100644 index 0000000..d69e52a --- /dev/null +++ b/repos/effect/packages/effect/src/Boolean.ts @@ -0,0 +1,481 @@ +/** + * Works with TypeScript `boolean` values. + * + * This module exposes the native `Boolean` constructor together with helpers + * for checking values, choosing between lazy branches, combining booleans with + * logical operations, checking collections with `every` or `some`, ordering + * booleans, and reducing boolean values. + * + * @since 2.0.0 + */ +import * as Equ from "./Equivalence.ts" +import type { LazyArg } from "./Function.ts" +import { dual } from "./Function.ts" +import * as order from "./Order.ts" +import * as predicate from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Exposes the global boolean constructor for JavaScript truthiness + * coercion. + * + * **When to use** + * + * Use to access native JavaScript truthiness coercion from the Effect module + * namespace. + * + * **Gotchas** + * + * This follows native truthiness rules. For example, non-empty strings such as + * `"false"` coerce to `true`. + * + * **Example** (Coercing values to booleans) + * + * ```ts + * import { Boolean } from "effect" + * + * const bool = Boolean.Boolean(1) + * console.log(bool) // true + * + * const fromString = Boolean.Boolean("false") + * console.log(fromString) // true (non-empty string) + * + * const fromZero = Boolean.Boolean(0) + * console.log(fromZero) // false + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const Boolean = globalThis.Boolean + +/** + * Checks whether a value is a `boolean`. + * + * **When to use** + * + * Use to validate unknown input and narrow it to `boolean`. + * + * **Example** (Checking for booleans) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.isBoolean(true), true) + * assert.deepStrictEqual(Boolean.isBoolean("true"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isBoolean: (input: unknown) => input is boolean = predicate.isBoolean + +/** + * Chooses between two lazy branches based on a boolean value. + * + * **When to use** + * + * Use to choose between two lazy branches based on a boolean value. + * + * **Example** (Pattern matching on booleans) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Boolean.match(true, { + * onFalse: () => "It's false!", + * onTrue: () => "It's true!" + * }), + * "It's true!" + * ) + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg + }): (value: boolean) => A | B + (value: boolean, options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg + }): A | B +} = dual(2, (value: boolean, options: { + readonly onFalse: LazyArg + readonly onTrue: LazyArg +}): A | B => value ? options.onTrue() : options.onFalse()) + +/** + * Provides an `Order` instance for `boolean` that allows comparing and sorting boolean values. + * In this ordering, `false` is considered less than `true`. + * + * **When to use** + * + * Use when you need to sort or compare boolean values through APIs that accept + * an ordering instance where `false` comes before `true`. + * + * **Example** (Comparing booleans) + * + * ```ts + * import { Boolean } from "effect" + * + * console.log(Boolean.Order(false, true)) // -1 (false < true) + * console.log(Boolean.Order(true, false)) // 1 (true > false) + * console.log(Boolean.Order(true, true)) // 0 (true === true) + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.Boolean + +/** + * Equivalence instance for booleans using strict equality (`===`). + * + * **When to use** + * + * Use when checking boolean equality through APIs that accept an equivalence + * relation. + * + * **Example** (Comparing booleans for equivalence) + * + * ```ts + * import { Boolean } from "effect" + * + * console.log(Boolean.Equivalence(true, true)) // true + * console.log(Boolean.Equivalence(true, false)) // false + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.Boolean + +/** + * Negates the given boolean: `!self` + * + * **When to use** + * + * Use to invert a boolean value. + * + * **Example** (Negating booleans) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.not(true), false) + * assert.deepStrictEqual(Boolean.not(false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const not = (self: boolean): boolean => !self + +/** + * Combines two booleans using logical AND: `self && that`. + * + * **When to use** + * + * Use to require both boolean operands to be `true`. + * + * **Details** + * + * Supports both data-first and data-last forms. + * + * **Example** (Combining booleans with AND) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.and(true, true), true) + * assert.deepStrictEqual(Boolean.and(true, false), false) + * assert.deepStrictEqual(Boolean.and(false, true), false) + * assert.deepStrictEqual(Boolean.and(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const and: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => self && that) + +/** + * Combines two booleans using NAND: `!(self && that)`. + * + * **When to use** + * + * Use to negate a logical AND result. + * + * **Example** (Combining booleans with NAND) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.nand(true, true), false) + * assert.deepStrictEqual(Boolean.nand(true, false), true) + * assert.deepStrictEqual(Boolean.nand(false, true), true) + * assert.deepStrictEqual(Boolean.nand(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const nand: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !(self && that)) + +/** + * Combines two booleans using OR: `self || that`. + * + * **When to use** + * + * Use to accept when either boolean operand is `true`. + * + * **Example** (Combining booleans with OR) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.or(true, true), true) + * assert.deepStrictEqual(Boolean.or(true, false), true) + * assert.deepStrictEqual(Boolean.or(false, true), true) + * assert.deepStrictEqual(Boolean.or(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const or: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => self || that) + +/** + * Combines two booleans using NOR: `!(self || that)`. + * + * **When to use** + * + * Use to accept only when both boolean operands are `false`. + * + * **Example** (Combining booleans with NOR) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.nor(true, true), false) + * assert.deepStrictEqual(Boolean.nor(true, false), false) + * assert.deepStrictEqual(Boolean.nor(false, true), false) + * assert.deepStrictEqual(Boolean.nor(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const nor: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !(self || that)) + +/** + * Combines two booleans using XOR: `(!self && that) || (self && !that)`. + * + * **When to use** + * + * Use to accept when exactly one boolean operand is `true`. + * + * **Example** (Combining booleans with XOR) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.xor(true, true), false) + * assert.deepStrictEqual(Boolean.xor(true, false), true) + * assert.deepStrictEqual(Boolean.xor(false, true), true) + * assert.deepStrictEqual(Boolean.xor(false, false), false) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const xor: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => (!self && that) || (self && !that)) + +/** + * Combines two booleans using EQV (aka XNOR): `!xor(self, that)`. + * + * **When to use** + * + * Use to accept when both boolean operands have the same truth value. + * + * **Example** (Checking boolean equivalence) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.eqv(true, true), true) + * assert.deepStrictEqual(Boolean.eqv(true, false), false) + * assert.deepStrictEqual(Boolean.eqv(false, true), false) + * assert.deepStrictEqual(Boolean.eqv(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const eqv: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self: boolean, that: boolean): boolean => !xor(self, that)) + +/** + * Combines two booleans using an implication: `(!self || that)`. + * + * **When to use** + * + * Use to model logical implication between a condition and a consequence. + * + * **Example** (Checking boolean implication) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.implies(true, true), true) + * assert.deepStrictEqual(Boolean.implies(true, false), false) + * assert.deepStrictEqual(Boolean.implies(false, true), true) + * assert.deepStrictEqual(Boolean.implies(false, false), true) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const implies: { + (that: boolean): (self: boolean) => boolean + (self: boolean, that: boolean): boolean +} = dual(2, (self, that) => self ? that : true) + +/** + * Checks whether every boolean in a collection is `true`. + * + * **When to use** + * + * Use to check that every boolean in an iterable is `true`. + * + * **Example** (Checking every boolean) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.every([true, true, true]), true) + * assert.deepStrictEqual(Boolean.every([true, false, true]), false) + * ``` + * + * @see {@link some} for checking whether at least one value is `true` + * @see {@link ReducerAnd} for reducing booleans with AND through a `Reducer` + * + * @category predicates + * @since 2.0.0 + */ +export const every = (collection: Iterable): boolean => { + for (const b of collection) { + if (!b) { + return false + } + } + return true +} + +/** + * Checks whether at least one boolean in a collection is `true`. + * + * **When to use** + * + * Use to check that at least one boolean in an iterable is `true`. + * + * **Example** (Checking some booleans) + * + * ```ts + * import { Boolean } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Boolean.some([true, false, true]), true) + * assert.deepStrictEqual(Boolean.some([false, false, false]), false) + * ``` + * + * @see {@link every} for checking whether all values are `true` + * @see {@link ReducerOr} for reducing booleans with OR through a `Reducer` + * + * @category predicates + * @since 2.0.0 + */ +export const some = (collection: Iterable): boolean => { + for (const b of collection) { + if (b) { + return true + } + } + return false +} + +/** + * Reducer for combining `boolean`s using AND. + * + * **When to use** + * + * Use to require every accumulated boolean to be `true` through APIs that + * consume a `Reducer`. + * + * **Details** + * + * The `initialValue` is `true`, so `combineAll([])` returns `true`. + * + * **Gotchas** + * + * `combineAll` uses the default left-to-right `Reducer.make` fold and does not + * short-circuit on `false`. + * + * @see {@link ReducerOr} for reducing with OR semantics + * @see {@link every} for checking an iterable directly + * + * @category math + * @since 4.0.0 + */ +export const ReducerAnd: Reducer.Reducer = Reducer.make((a, b) => a && b, true) + +/** + * Reducer for combining `boolean`s using OR. + * + * **When to use** + * + * Use to reduce boolean values where the result should be `true` if any + * combined value is `true`. + * + * **Details** + * + * The `initialValue` is `false`. + * + * @see {@link ReducerAnd} for reducing with AND semantics + * @see {@link some} for checking an iterable directly + * + * @category math + * @since 4.0.0 + */ +export const ReducerOr: Reducer.Reducer = Reducer.make((a, b) => a || b, false) diff --git a/repos/effect/packages/effect/src/Brand.ts b/repos/effect/packages/effect/src/Brand.ts new file mode 100644 index 0000000..39789b2 --- /dev/null +++ b/repos/effect/packages/effect/src/Brand.ts @@ -0,0 +1,318 @@ +/** + * The `Brand` module adds compile-time names to ordinary TypeScript values so + * structurally identical values cannot be mixed accidentally. A branded value + * has the same runtime representation as its unbranded value; the extra + * information lives in the type system unless you choose a validating + * constructor. + * + * @since 2.0.0 + */ +import * as Arr from "./Array.ts" +import * as Option from "./Option.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import * as SchemaAST from "./SchemaAST.ts" +import type * as SchemaIssue from "./SchemaIssue.ts" +import type * as Types from "./Types.ts" + +const TypeId = "~effect/Brand" + +/** + * A generic interface that defines a branded type. + * + * **When to use** + * + * Use to define a branded type such as `number & Brand<"Positive">` when + * TypeScript should keep structurally identical values separate without + * changing their runtime value. + * + * @see {@link Branded} for applying a brand key to a base type + * @see {@link Constructor} for validating or constructing branded values + * + * @category models + * @since 2.0.0 + */ +export interface Brand { + readonly [TypeId]: { + readonly [K in Keys]: Keys + } +} + +/** + * A constructor for a branded type that provides validation and safe + * construction methods. + * + * **When to use** + * + * Use as the shared callable interface for branded values when an API accepts + * or returns a brand constructor and callers need throwing, `Option`, `Result`, + * or type-guard validation forms. + * + * @see {@link nominal} for a constructor without runtime validation + * @see {@link make} for creating a constructor from a validation predicate + * @see {@link check} for creating a constructor from schema checks + * @see {@link all} for combining brand constructors + * + * @category models + * @since 2.0.0 + */ +export interface Constructor> { + /** + * Constructs a branded type from a value of type `Unbranded`, throwing an + * error if the provided value is not valid. + */ + (unbranded: Brand.Unbranded): B + /** + * Constructs a branded type from a value of type `Unbranded`, returning + * `Some` if the provided value is valid, `None` otherwise. + */ + option(unbranded: Brand.Unbranded): Option.Option + /** + * Constructs a branded type from a value of type `Unbranded`, returning + * `Success` if the provided value is valid, `Failure` + * otherwise. + */ + result(unbranded: Brand.Unbranded): Result.Result + /** + * Attempts to refine the provided value of type `Unbranded`, returning + * `true` if the provided value is a valid branded type, `false` otherwise. + */ + is(unbranded: Brand.Unbranded): unbranded is Brand.Unbranded & B + + /** + * The checks that are applied to the branded type. + * + * @internal + */ + checks?: readonly [SchemaAST.Check>, ...Array>>] | undefined +} + +/** + * Error returned when a branded type is constructed from an invalid value. + * + * **Details** + * + * The error wraps a `SchemaIssue.Issue`, exposes `message` through + * `issue.toString()`, and formats as `BrandError()`. + * + * **Gotchas** + * + * `BrandError` is an error-like model with `_tag`, `name`, `message`, and + * `toString`; it does not extend JavaScript `Error`. + * + * @category errors + * @since 4.0.0 + */ +export class BrandError { + constructor(issue: SchemaIssue.Issue) { + this.issue = issue + } + /** + * Discriminant used to identify brand construction failures. + * + * @since 4.0.0 + */ + readonly _tag = "BrandError" + /** + * Error name used by tools that inspect JavaScript error-like objects. + * + * @since 4.0.0 + */ + readonly name: string = "BrandError" + /** + * Schema issue describing why brand validation failed. + * + * @since 4.0.0 + */ + readonly issue: SchemaIssue.Issue + /** + * Human-readable rendering of the validation issue. + * + * @since 4.0.0 + */ + get message() { + return this.issue.toString() + } + /** + * Formats the brand error together with its validation message. + * + * @since 4.0.0 + */ + toString() { + return `BrandError(${this.message})` + } +} + +/** + * Namespace containing type-level helpers for working with branded types and + * brand constructors. + * + * @since 2.0.0 + */ +export declare namespace Brand { + /** + * A utility type to extract a branded type from a `Constructor`. + * + * @category utility types + * @since 2.0.0 + */ + export type FromConstructor = C extends Constructor ? B : never + + /** + * A utility type to extract the unbranded value type from a brand. + * + * @category utility types + * @since 2.0.0 + */ + export type Unbranded> = B extends infer U & Brands ? U : B + + /** + * A utility type to extract the keys of a branded type. + * + * @category utility types + * @since 4.0.0 + */ + export type Keys> = keyof B[typeof TypeId] + + /** + * A utility type to extract the brands from a branded type. + * + * @category utility types + * @since 2.0.0 + */ + export type Brands> = Types.UnionToIntersection< + { [K in Keys]: K extends string ? Brand : never }[Keys] + > + + /** + * A utility type that checks that all brands have the same base type. + * + * @category utility types + * @since 2.0.0 + */ + export type EnsureCommonBase< + Brands extends readonly [Constructor, ...Array>] + > = { + [B in keyof Brands]: Brand.Unbranded> extends + Brand.Unbranded> + ? Brand.Unbranded> extends Brand.Unbranded> + ? Brands[B] + : Brands[B] + : "ERROR: All brands should have the same base type" + } +} + +/** + * A type alias for creating branded types more concisely. + * + * @category utility types + * @since 2.0.0 + */ +export type Branded = A & Brand + +/** + * Returns a `Constructor` that **does not apply any runtime checks** and just + * returns the provided value. + * + * **When to use** + * + * Use to create nominal types that allow distinguishing between two values + * of the same type but with different meanings. + * + * @see {@link make} for constructing branded values with validation. + * @see {@link check} for constructing branded values from schema checks. + * + * @category constructors + * @since 2.0.0 + */ +export function nominal>(): Constructor { + return Object.assign((input: Brand.Unbranded) => input as A, { + option: (input: Brand.Unbranded) => Option.some(input as A), + result: (input: Brand.Unbranded) => Result.succeed(input as A), + is: (_: Brand.Unbranded): _ is Brand.Unbranded & A => true + }) +} + +/** + * Returns a `Constructor` that can construct a branded type from an unbranded + * value using the provided `filter` predicate as validation of the input data. + * + * **When to use** + * + * Use when you want validation while constructing the branded type. + * + * @see {@link nominal} for a brand constructor that performs no validation. + * + * @category constructors + * @since 4.0.0 + */ +export function make>( + filter: (unbranded: Brand.Unbranded) => Schema.FilterOutput +): Constructor { + return check(SchemaAST.makeFilter(filter)) +} + +/** + * Creates a branded type `Constructor` from one or more schema checks. + * + * **When to use** + * + * Use when you need a branded type constructor that performs runtime validation + * via schema checks. + * + * **Details** + * + * Calling the returned constructor validates the unbranded value and throws on + * failure. Use the returned `option`, `result`, or `is` methods for + * non-throwing validation. + * + * @see {@link nominal} for a brand constructor without runtime validation + * @see {@link all} for combining multiple brand constructors + * @category constructors + * @since 4.0.0 + */ +export function check>( + ...checks: readonly [ + SchemaAST.Check>, + ...Array>> + ] +): Constructor { + const result = (input: Brand.Unbranded): Result.Result => { + return Result.mapError(SchemaAST.runChecks(checks, input), (issue) => new BrandError(issue)) as any + } + return Object.assign((input: Brand.Unbranded) => Result.getOrThrow(result(input)), { + option: (input: Brand.Unbranded) => Option.getSuccess(result(input)), + result, + is: (input: Brand.Unbranded): input is Brand.Unbranded & A => Result.isSuccess(result(input)), + checks + }) +} + +/** + * Combines one or more brand constructors to form a single branded type. + * + * **When to use** + * + * Use to require an input to satisfy every runtime check collected by the + * provided brand constructors. + * + * **Details** + * + * If the provided constructors contain runtime checks, the combined + * constructor succeeds only when all checks pass. If no runtime checks are + * present, it behaves as a nominal constructor. + * + * @category combining + * @since 2.0.0 + */ +export function all, ...Array>]>( + ...brands: Brand.EnsureCommonBase +): Constructor< + Types.UnionToIntersection<{ [B in keyof Brands]: Brand.FromConstructor }[number]> extends + infer X extends Brand ? X : Brand +> { + const checks = brands.flatMap((brand) => brand.checks ?? []) + return Arr.isArrayNonEmpty(checks) ? + check(...checks) : + nominal() +} diff --git a/repos/effect/packages/effect/src/Cache.ts b/repos/effect/packages/effect/src/Cache.ts new file mode 100644 index 0000000..21fb232 --- /dev/null +++ b/repos/effect/packages/effect/src/Cache.ts @@ -0,0 +1,1295 @@ +/** + * Caches values loaded by an Effect lookup function. + * + * A cache stores successful and failed lookup results, shares an in-progress + * lookup when multiple callers request the same missing key, and limits entries + * by capacity and optional time-to-live rules. This module includes helpers for + * reading, setting, refreshing, invalidating, and inspecting cache contents. + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Duration from "./Duration.ts" +import type * as Effect from "./Effect.ts" +import type * as Exit from "./Exit.ts" +import type * as Fiber from "./Fiber.ts" +import { dual } from "./Function.ts" +import * as core from "./internal/core.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as Iterable from "./Iterable.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Predicate } from "./Predicate.ts" +import * as Result from "./Result.ts" + +const TypeId = "~effect/Cache" + +/** + * A cache interface that provides a mutable key-value store with automatic TTL management, + * capacity limits, and lookup functions for cache misses. + * + * **Example** (Creating a basic cache) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Basic cache with string keys and number values + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Cache operations + * const value1 = yield* Cache.get(cache, "hello") // 5 + * const value2 = yield* Cache.get(cache, "world") // 5 + * const value3 = yield* Cache.get(cache, "hello") // 5 (cached) + * + * return [value1, value2, value3] + * }) + * ``` + * + * **Example** (Handling lookup failures) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Cache with error handling + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => + * key === "error" + * ? Effect.fail("Lookup failed") + * : Effect.succeed(key.length) + * }) + * + * // Handle successful and failed lookups + * const success = yield* Cache.get(cache, "test") // 4 + * const failure = yield* Effect.exit(Cache.get(cache, "error")) // Exit.fail + * + * return { success, failure } + * }) + * ``` + * + * **Example** (Using complex keys with TTL) + * + * ```ts + * import { Cache, Data, Duration, Effect } from "effect" + * + * // Cache with complex key types and TTL + * class UserId extends Data.Class<{ id: number }> {} + * + * const program = Effect.gen(function*() { + * const userCache = yield* Cache.make({ + * capacity: 1000, + * lookup: (userId: UserId) => Effect.succeed(`User-${userId.id}`), + * timeToLive: Duration.minutes(5) + * }) + * + * const userId = new UserId({ id: 123 }) + * const userName = yield* Cache.get(userCache, userId) + * + * return userName // "User-123" + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Cache extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly map: MutableHashMap.MutableHashMap> + readonly capacity: number + readonly lookup: (key: Key) => Effect.Effect + readonly timeToLive: (exit: Exit.Exit, key: Key) => Duration.Duration +} + +/** + * Represents a low-level cache entry containing a deferred lookup result and + * an optional expiration timestamp. + * + * **When to use** + * + * Use when inspecting a `Cache`'s low-level map and you need the stored + * deferred lookup result or expiration timestamp for a key. + * + * **Details** + * + * An `expiresAt` value of `undefined` means the entry does not expire. + * + * @see {@link Cache} for the public cache API that manages entries through + * combinators + * + * @category models + * @since 4.0.0 + */ +export interface Entry { + expiresAt: number | undefined + readonly deferred: Deferred.Deferred +} + +/** + * Creates a cache with dynamic time-to-live based on the result and key. + * + * **When to use** + * + * Use when you need different cache entry lifetimes based on the lookup result + * or key characteristics. + * + * **Details** + * + * The timeToLive function receives both the exit result and the key, allowing + * for flexible TTL policies based on success/failure state and key characteristics. + * + * **Example** (Configuring dynamic time to live) + * + * ```ts + * import { Cache, Effect, Exit } from "effect" + * + * // Cache with TTL based on computed value + * const userCache = Effect.gen(function*() { + * const cache = yield* Cache.makeWith( + * (id: number) => Effect.succeed({ id, active: id % 2 === 0 }), + * { + * capacity: 1000, + * timeToLive(exit) { + * if (Exit.isSuccess(exit)) { + * const user = exit.value + * return user.active ? "1 hour" : "5 minutes" + * } + * return "30 seconds" + * } + * } + * ) + * + * return cache + * }) + * ``` + * + * @see {@link make} for a simpler cache constructor with a fixed time-to-live for all entries + * @category constructors + * @since 2.0.0 + */ +export const makeWith = < + Key, + A, + E = never, + R = never, + ServiceMode extends "lookup" | "construction" = never +>(lookup: (key: Key) => Effect.Effect, options: { + readonly capacity: number + readonly timeToLive?: ((exit: Exit.Exit, key: Key) => Duration.Input) | undefined + readonly requireServicesAt?: ServiceMode | undefined +}): Effect.Effect< + Cache, + never, + "lookup" extends ServiceMode ? never : R +> => + effect.contextWith((context: Context.Context) => { + const self = Object.create(Proto) + self.lookup = (key: Key): Effect.Effect => + effect.updateContext( + lookup(key), + (input) => Context.merge(context, input) + ) + self.map = MutableHashMap.make() + self.capacity = options.capacity + self.timeToLive = options.timeToLive + ? (exit: Exit.Exit, key: Key) => Duration.fromInputUnsafe(options.timeToLive!(exit, key)) + : defaultTimeToLive + return effect.succeed(self as Cache) + }) + +/** + * Creates a cache with a fixed time-to-live for all entries. + * + * **Details** + * + * This is the basic cache constructor where all entries share the same TTL. + * The lookup function will be called when a key is not found or has expired. + * + * **Example** (Creating a basic cache) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Basic cache with string keys + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key) => Effect.succeed(key.length) + * }) + * + * const result1 = yield* Cache.get(cache, "hello") + * const result2 = yield* Cache.get(cache, "world") + * console.log({ result1, result2 }) // { result1: 5, result2: 5 } + * }) + * ``` + * + * **Example** (Creating a cache with TTL) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const users = new Map([ + * [123, { name: "Ada", email: "ada@example.com" }], + * [456, { name: "Grace", email: "grace@example.com" }] + * ]) + * + * const cache = yield* Cache.make< + * number, + * { name: string; email: string }, + * string + * >({ + * capacity: 500, + * lookup: (userId) => + * Effect.suspend(() => { + * const user = users.get(userId) + * return user === undefined + * ? Effect.fail(`User ${userId} not found`) + * : Effect.succeed(user) + * }), + * timeToLive: "15 minutes" + * }) + * + * const user1 = yield* Cache.get(cache, 123) + * console.log(user1) // { name: "Ada", email: "ada@example.com" } + * + * const user2 = yield* Cache.get(cache, 123) + * console.log(user2) // { name: "Ada", email: "ada@example.com" } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = < + Key, + A, + E = never, + R = never, + ServiceMode extends "lookup" | "construction" = never +>( + options: { + readonly lookup: (key: Key) => Effect.Effect + readonly capacity: number + readonly timeToLive?: Duration.Input | undefined + readonly requireServicesAt?: ServiceMode | undefined + } +): Effect.Effect< + Cache, + never, + "lookup" extends ServiceMode ? never : R +> => + makeWith(options.lookup, { + ...options, + timeToLive: options.timeToLive ? () => options.timeToLive! : defaultTimeToLive + }) + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + toJSON(this: Cache) { + return { + _id: "Cache", + capacity: this.capacity, + map: this.map + } + } +} + +const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Duration => Duration.infinity + +/** + * Retrieves the value for a key, invoking the lookup function on a cache miss + * or expired entry. + * + * **Details** + * + * Concurrent `get` calls for the same missing key share the same pending + * lookup. The cache stores the lookup `Exit`, so failed lookups are cached and + * will fail again until the entry expires, is invalidated, or is refreshed. + * + * **Example** (Getting cached values) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Cache miss - triggers lookup function + * const result1 = yield* Cache.get(cache, "hello") + * console.log(result1) // 5 + * + * // Cache hit - returns cached value without lookup + * const result2 = yield* Cache.get(cache, "hello") + * console.log(result2) // 5 (from cache) + * + * return { result1, result2 } + * }) + * ``` + * + * **Example** (Handling lookup failures) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Error handling when lookup fails + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => + * key === "error" + * ? Effect.fail("Lookup failed") + * : Effect.succeed(key.length) + * }) + * + * // Successful lookup + * const success = yield* Cache.get(cache, "hello") + * console.log(success) // 5 + * + * // Failed lookup - returns error + * const failure = yield* Effect.exit(Cache.get(cache, "error")) + * console.log(failure) // Exit.fail("Lookup failed") + * }) + * ``` + * + * **Example** (Sharing concurrent lookups) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Concurrent access - multiple gets of same key only invoke lookup once + * const program = Effect.gen(function*() { + * let lookupCount = 0 + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => + * Effect.sync(() => { + * lookupCount++ + * return key.length + * }) + * }) + * + * // Multiple concurrent gets + * const results = yield* Effect.all([ + * Cache.get(cache, "hello"), + * Cache.get(cache, "hello"), + * Cache.get(cache, "hello") + * ], { concurrency: "unbounded" }) + * + * console.log(results) // [5, 5, 5] + * console.log(lookupCount) // 1 (lookup called only once) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const get: { + (key: Key): (self: Cache) => Effect.Effect + (self: Cache, key: Key): Effect.Effect +} = dual( + 2, + (self: Cache, key: Key): Effect.Effect => + core.withFiber((fiber) => { + const oentry = MutableHashMap.get(self.map, key) + if (Option.isSome(oentry) && !hasExpired(oentry.value, fiber)) { + // Move the entry to the end of the map to keep it fresh + MutableHashMap.remove(self.map, key) + MutableHashMap.set(self.map, key, oentry.value) + return Deferred.await(oentry.value.deferred) + } + const deferred = Deferred.makeUnsafe() + const entry: Entry = { + expiresAt: undefined, + deferred + } + MutableHashMap.set(self.map, key, entry) + if (Number.isFinite(self.capacity)) { + checkCapacity(self) + } + return effect.onExit(self.lookup(key), (exit) => { + Deferred.doneUnsafe(deferred, exit) + const ttl = self.timeToLive(exit, key) + if (Duration.isFinite(ttl)) { + entry.expiresAt = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + } else if (Duration.isZero(ttl)) { + MutableHashMap.remove(self.map, key) + } + return effect.void + }) + }) +) + +const hasExpired = (entry: Entry, fiber: Fiber.Fiber): boolean => { + if (entry.expiresAt === undefined) { + return false + } + return fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() >= entry.expiresAt +} + +const checkCapacity = (self: Cache) => { + let diff = MutableHashMap.size(self.map) - self.capacity + if (diff <= 0) return + // MutableHashMap has insertion order, so we can remove the oldest entries + for (const [key] of self.map) { + MutableHashMap.remove(self.map, key) + diff-- + if (diff === 0) return + } +} + +/** + * Reads an existing cache entry without invoking the lookup function. + * + * **Details** + * + * Returns `Option.none()` when the key is missing or expired, and `Option.some` + * when a cached lookup has succeeded. If the entry is still pending, waits for + * it to complete. If the cached or pending lookup fails, this effect fails with + * the same error. + * + * **Example** (Reading cached values without lookup) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // No value in cache yet - returns None without lookup + * const empty = yield* Cache.getOption(cache, "hello") + * console.log(empty) // Option.none() + * + * // Populate cache using get + * yield* Cache.get(cache, "hello") + * + * // Now getOption returns the cached value + * const cached = yield* Cache.getOption(cache, "hello") + * console.log(cached) // Option.some(5) + * + * return { empty, cached } + * }) + * ``` + * + * **Example** (Skipping expired entries) + * + * ```ts + * import { Cache, Effect } from "effect" + * import { TestClock } from "effect/testing" + * + * // Expired entries return None + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length), + * timeToLive: "1 hour" + * }) + * + * // Add value to cache + * yield* Cache.get(cache, "hello") + * + * // Value exists before expiration + * const beforeExpiry = yield* Cache.getOption(cache, "hello") + * console.log(beforeExpiry) // Option.some(5) + * + * // Simulate time passing + * yield* TestClock.adjust("2 hours") + * + * // Value expired - returns None + * const afterExpiry = yield* Cache.getOption(cache, "hello") + * console.log(afterExpiry) // Option.none() + * }) + * ``` + * + * **Example** (Waiting for pending lookups) + * + * ```ts + * import { Cache, Deferred, Effect, Fiber } from "effect" + * + * // Waits for ongoing computation to complete + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (_key: string) => Deferred.await(deferred).pipe(Effect.as(42)) + * }) + * + * // Start lookup in background + * const getFiber = yield* Effect.forkChild(Cache.get(cache, "key")) + * + * // getOption waits for ongoing computation + * const optionFiber = yield* Effect.forkChild(Cache.getOption(cache, "key")) + * + * // Complete the computation + * yield* Deferred.succeed(deferred, void 0) + * + * const result = yield* Fiber.join(optionFiber) + * console.log(result) // Option.some(42) + * + * const value = yield* Fiber.join(getFiber) + * console.log(value) // 42 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const getOption: { + (key: Key): (self: Cache) => Effect.Effect, E> + (self: Cache, key: Key): Effect.Effect, E> +} = dual( + 2, + (self: Cache, key: Key): Effect.Effect, E> => + core.withFiber((fiber) => { + const entry = getImpl(self, key, fiber) + return entry ? effect.asSome(Deferred.await(entry.deferred)) : effect.succeedNone + }) +) + +const getImpl = ( + self: Cache, + key: Key, + fiber: Fiber.Fiber, + isRead = true +): Entry | undefined => { + const oentry = MutableHashMap.get(self.map, key) + if (Option.isNone(oentry)) { + return undefined + } else if (hasExpired(oentry.value, fiber)) { + MutableHashMap.remove(self.map, key) + return undefined + } else if (isRead) { + MutableHashMap.remove(self.map, key) + MutableHashMap.set(self.map, key, oentry.value) + } + return oentry.value +} + +/** + * Retrieves the value associated with the specified key from the cache, only if + * it contains a resolved successful value. + * + * **Details** + * + * This checks only an existing non-expired entry. It returns `Option.some` when + * the entry has already resolved successfully, and `Option.none` for missing, + * expired, failed, or still-pending entries. + * + * @see {@link get} for triggering or awaiting the cache lookup + * @see {@link getOption} for reading an existing entry as an optional effect + * + * @category combinators + * @since 4.0.0 + */ +export const getSuccess: { + (key: Key): (self: Cache) => Effect.Effect> + (self: Cache, key: Key): Effect.Effect> +} = dual( + 2, + (self: Cache, key: Key): Effect.Effect> => + core.withFiber((fiber) => { + const exit = getImpl(self, key, fiber)?.deferred.effect as Exit.Exit | undefined + if (exit && effect.exitIsSuccess(exit)) { + return effect.succeedSome(exit.value) + } + return effect.succeedNone + }) +) + +/** + * Sets the value associated with the specified key in the cache. This will + * overwrite any existing value for that key, skipping the lookup function. + * + * **Example** (Setting values directly) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Set a value directly without invoking lookup + * yield* Cache.set(cache, "hello", 42) + * const result = yield* Cache.get(cache, "hello") + * console.log(result) // 42 (not 5 from lookup) + * }) + * ``` + * + * **Example** (Overwriting cached values) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Overwriting existing cached values + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // First get populates via lookup + * const original = yield* Cache.get(cache, "test") // 4 + * + * // Set overwrites the cached value + * yield* Cache.set(cache, "test", 999) + * const updated = yield* Cache.get(cache, "test") // 999 + * + * console.log({ original, updated }) + * }) + * ``` + * + * **Example** (Applying TTL to set values) + * + * ```ts + * import { Cache, Effect } from "effect" + * import { TestClock } from "effect/testing" + * + * // TTL behavior with set operations + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length), + * timeToLive: "1 hour" + * }) + * + * // Set value with TTL applied + * yield* Cache.set(cache, "temporary", 123) + * console.log(yield* Cache.has(cache, "temporary")) // true + * + * // Advance time past TTL + * yield* TestClock.adjust("2 hours") + * console.log(yield* Cache.has(cache, "temporary")) // false + * }) + * ``` + * + * **Example** (Enforcing capacity when setting values) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Capacity enforcement with set operations + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 2, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Fill cache to capacity + * yield* Cache.set(cache, "a", 1) + * yield* Cache.set(cache, "b", 2) + * console.log(yield* Cache.size(cache)) // 2 + * + * // Adding another entry evicts oldest + * yield* Cache.set(cache, "c", 3) + * console.log(yield* Cache.size(cache)) // 2 + * console.log(yield* Cache.has(cache, "a")) // false (evicted) + * console.log(yield* Cache.has(cache, "c")) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const set: { + (key: Key, value: A): (self: Cache) => Effect.Effect + (self: Cache, key: Key, value: A): Effect.Effect +} = dual( + 3, + (self: Cache, key: Key, value: A): Effect.Effect => + core.withFiber((fiber) => { + const exit = core.exitSucceed(value) + const deferred = Deferred.makeUnsafe() + Deferred.doneUnsafe(deferred, exit) + const ttl = self.timeToLive(exit, key) + if (Duration.isZero(ttl)) { + MutableHashMap.remove(self.map, key) + return effect.void + } + MutableHashMap.set(self.map, key, { + deferred, + expiresAt: Duration.isFinite(ttl) + ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + : undefined + }) + checkCapacity(self) + return effect.void + }) +) + +/** + * Checks whether the cache contains an entry for the specified key. + * + * **Details** + * + * This checks for an existing non-expired entry without invoking the cache + * lookup function. Expired entries are treated as absent. + * + * **Example** (Checking for cached keys) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Check non-existent key + * console.log(yield* Cache.has(cache, "missing")) // false + * + * // Add entry and check existence + * yield* Cache.get(cache, "hello") + * console.log(yield* Cache.has(cache, "hello")) // true + * }) + * ``` + * + * **Example** (Checking TTL expiration) + * + * ```ts + * import { Cache, Effect } from "effect" + * import { TestClock } from "effect/testing" + * + * // TTL expiration behavior + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length), + * timeToLive: "1 hour" + * }) + * + * // Add entry with TTL + * yield* Cache.get(cache, "expires") + * console.log(yield* Cache.has(cache, "expires")) // true + * + * // Still valid before expiration + * yield* TestClock.adjust("30 minutes") + * console.log(yield* Cache.has(cache, "expires")) // true + * + * // Expired after TTL + * yield* TestClock.adjust("31 minutes") + * console.log(yield* Cache.has(cache, "expires")) // false + * }) + * ``` + * + * **Example** (Checking multiple keys) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Checking multiple keys efficiently + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 100, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Populate some entries + * yield* Cache.set(cache, "apple", 5) + * yield* Cache.set(cache, "banana", 6) + * + * // Check multiple keys + * const keys = ["apple", "banana", "cherry", "date"] + * for (const key of keys) { + * const exists = yield* Cache.has(cache, key) + * console.log(`${key}: ${exists}`) + * } + * // Output: + * // apple: true + * // banana: true + * // cherry: false + * // date: false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const has: { + (key: Key): (self: Cache) => Effect.Effect + (self: Cache, key: Key): Effect.Effect +} = dual( + 2, + (self: Cache, key: Key): Effect.Effect => + core.withFiber((fiber) => { + const oentry = getImpl(self, key, fiber, false) + return effect.succeed(oentry !== undefined) + }) +) + +/** + * Invalidates the entry associated with the specified key in the cache. + * + * **Example** (Invalidating cached entries) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Add a value to the cache + * yield* Cache.get(cache, "hello") + * console.log(yield* Cache.has(cache, "hello")) // true + * + * // Invalidate the entry + * yield* Cache.invalidate(cache, "hello") + * console.log(yield* Cache.has(cache, "hello")) // false + * + * // Invalidating non-existent keys doesn't error + * yield* Cache.invalidate(cache, "nonexistent") + * + * // Get after invalidation will invoke lookup again + * let lookupCount = 0 + * const cache2 = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => + * Effect.sync(() => { + * lookupCount++ + * return key.length + * }) + * }) + * + * yield* Cache.get(cache2, "test") // lookupCount = 1 + * yield* Cache.invalidate(cache2, "test") + * yield* Cache.get(cache2, "test") // lookupCount = 2 (lookup called again) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const invalidate: { + (key: Key): (self: Cache) => Effect.Effect + (self: Cache, key: Key): Effect.Effect +} = dual(2, (self: Cache, key: Key): Effect.Effect => + effect.sync(() => { + MutableHashMap.remove(self.map, key) + })) + +/** + * Invalidates the entry associated with the specified key in the cache when the + * predicate returns true for the cached value. + * + * **Example** (Invalidating entries conditionally) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Add values to the cache + * yield* Cache.get(cache, "hello") // value = 5 + * yield* Cache.get(cache, "hi") // value = 2 + * + * // Invalidate when value equals 5 + * const invalidated1 = yield* Cache.invalidateWhen( + * cache, + * "hello", + * (value) => value === 5 + * ) + * console.log(invalidated1) // true + * console.log(yield* Cache.has(cache, "hello")) // false + * + * // Don't invalidate when predicate doesn't match + * const invalidated2 = yield* Cache.invalidateWhen( + * cache, + * "hi", + * (value) => value === 5 + * ) + * console.log(invalidated2) // false + * console.log(yield* Cache.has(cache, "hi")) // true (still present) + * + * // Returns false for non-existent keys + * const invalidated3 = yield* Cache.invalidateWhen( + * cache, + * "nonexistent", + * () => true + * ) + * console.log(invalidated3) // false + * + * // Returns false for failed cached values + * const cacheWithErrors = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => + * key === "fail" ? Effect.fail("error") : Effect.succeed(key.length) + * }) + * + * yield* Effect.exit(Cache.get(cacheWithErrors, "fail")) + * const invalidated4 = yield* Cache.invalidateWhen( + * cacheWithErrors, + * "fail", + * () => true + * ) + * console.log(invalidated4) // false (can't invalidate failed values) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const invalidateWhen: { + (key: Key, f: Predicate): (self: Cache) => Effect.Effect + (self: Cache, key: Key, f: Predicate): Effect.Effect +} = dual( + 3, + (self: Cache, key: Key, f: Predicate): Effect.Effect => + core.withFiber((fiber) => { + const oentry = getImpl(self, key, fiber, false) + if (oentry === undefined) { + return effect.succeed(false) + } + return Deferred.await(oentry.deferred).pipe( + effect.map((value) => { + if (f(value)) { + MutableHashMap.remove(self.map, key) + return true + } + return false + }), + effect.catchCause(() => effect.succeed(false)) + ) + }) +) + +/** + * Forces a refresh of the value associated with the specified key in the cache. + * + * **Details** + * + * It will always invoke the lookup function to construct a new value, + * overwriting any existing value for that key. + * + * **Example** (Refreshing cached values) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Force refresh of existing cached values + * const program = Effect.gen(function*() { + * let counter = 0 + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.sync(() => `${key}-${++counter}`) + * }) + * + * // Initial cache population + * const value1 = yield* Cache.get(cache, "user") + * console.log(value1) // "user-1" + * + * // Get from cache (no lookup) + * const value2 = yield* Cache.get(cache, "user") + * console.log(value2) // "user-1" (same value) + * + * // Force refresh - always calls lookup + * const refreshed = yield* Cache.refresh(cache, "user") + * console.log(refreshed) // "user-2" (new value) + * + * // Subsequent gets return refreshed value + * const value3 = yield* Cache.get(cache, "user") + * console.log(value3) // "user-2" + * }) + * ``` + * + * **Example** (Resetting TTL on refresh) + * + * ```ts + * import { Cache, Effect } from "effect" + * import { TestClock } from "effect/testing" + * + * // Refresh resets TTL (Time To Live) + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length), + * timeToLive: "1 hour" + * }) + * + * yield* Cache.get(cache, "test") + * yield* TestClock.adjust("45 minutes") + * + * // Entry would normally expire in 15 minutes + * console.log(yield* Cache.has(cache, "test")) // true + * + * // Refresh resets the TTL to full 1 hour + * yield* Cache.refresh(cache, "test") + * yield* TestClock.adjust("30 minutes") + * + * // Still valid because TTL was reset + * console.log(yield* Cache.has(cache, "test")) // true + * }) + * ``` + * + * **Example** (Refreshing missing keys) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Refresh non-existent keys + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(`value-for-${key}`) + * }) + * + * // Refresh non-existent key creates new entry + * const result = yield* Cache.refresh(cache, "newKey") + * console.log(result) // "value-for-newKey" + * + * // Verify it's now cached + * console.log(yield* Cache.has(cache, "newKey")) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const refresh: { + (key: Key): (self: Cache) => Effect.Effect + (self: Cache, key: Key): Effect.Effect +} = dual( + 2, + (self: Cache, key: Key): Effect.Effect => + core.withFiber((fiber) => { + const deferred = Deferred.makeUnsafe() + const entry: Entry = { + expiresAt: undefined, + deferred + } + const existing = getImpl(self, key, fiber, false) !== undefined + if (!existing) { + MutableHashMap.set(self.map, key, entry) + checkCapacity(self) + } + return effect.onExit(self.lookup(key), (exit) => { + Deferred.doneUnsafe(deferred, exit) + const ttl = self.timeToLive(exit, key) + if (Duration.isZero(ttl)) { + MutableHashMap.remove(self.map, key) + return effect.void + } + entry.expiresAt = Duration.isFinite(ttl) + ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + : undefined + if (existing) { + MutableHashMap.set(self.map, key, entry) + } + return effect.void + }) + }) +) + +/** + * Invalidates all entries in the cache. + * + * **Example** (Invalidating all entries) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Clear all cached entries at once + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Populate cache with multiple entries + * yield* Cache.get(cache, "apple") + * yield* Cache.get(cache, "banana") + * yield* Cache.get(cache, "cherry") + * + * console.log(yield* Cache.size(cache)) // 3 + * console.log(yield* Cache.has(cache, "apple")) // true + * + * // Clear all entries + * yield* Cache.invalidateAll(cache) + * + * // Verify cache is empty + * console.log(yield* Cache.size(cache)) // 0 + * console.log(yield* Cache.has(cache, "apple")) // false + * console.log(yield* Cache.has(cache, "banana")) // false + * console.log(yield* Cache.has(cache, "cherry")) // false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const invalidateAll = (self: Cache): Effect.Effect => + effect.sync(() => { + MutableHashMap.clear(self.map) + }) + +/** + * Retrieves the approximate number of entries in the cache. + * + * **Details** + * + * Note that expired entries are counted until they are accessed and removed. + * The size reflects the current number of entries stored, not the number + * of valid entries. + * + * **Example** (Reading cache size) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Empty cache has size 0 + * const emptySize = yield* Cache.size(cache) + * console.log(emptySize) // 0 + * + * // Add entries and check size + * yield* Cache.get(cache, "hello") + * yield* Cache.get(cache, "world") + * const sizeAfterAdding = yield* Cache.size(cache) + * console.log(sizeAfterAdding) // 2 + * + * // Size decreases after invalidation + * yield* Cache.invalidate(cache, "hello") + * const sizeAfterInvalidation = yield* Cache.size(cache) + * console.log(sizeAfterInvalidation) // 1 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const size = (self: Cache): Effect.Effect => + effect.sync(() => MutableHashMap.size(self.map)) + +/** + * Retrieves all active keys from the cache, automatically filtering out expired entries. + * + * **Example** (Reading active keys) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * // Basic key enumeration + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Add some entries to the cache + * yield* Cache.get(cache, "hello") + * yield* Cache.get(cache, "world") + * yield* Cache.get(cache, "cache") + * + * // Retrieve all active keys + * const keys = yield* Cache.keys(cache) + * + * console.log(Array.from(keys).sort()) // ["cache", "hello", "world"] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const keys = (self: Cache): Effect.Effect> => + core.withFiber((fiber) => { + const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + return effect.succeed(Iterable.filterMap(self.map, ([key, entry]) => { + if (entry.expiresAt === undefined || entry.expiresAt > now) { + return Result.succeed(key) + } + MutableHashMap.remove(self.map, key) + return Result.failVoid + })) + }) + +/** + * Retrieves all successfully cached values from the cache, excluding failed + * lookups and expired entries. + * + * **Example** (Reading all cached values) + * + * ```ts + * import { Cache, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* Cache.make({ + * capacity: 10, + * lookup: (key: string) => Effect.succeed(key.length) + * }) + * + * // Add some values to the cache + * yield* Cache.get(cache, "a") + * yield* Cache.get(cache, "ab") + * yield* Cache.get(cache, "abc") + * + * // Retrieve all cached values + * const values = yield* Cache.values(cache) + * const valuesArray = Array.from(values).sort() + * + * console.log(valuesArray) // [1, 2, 3] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const values = (self: Cache): Effect.Effect> => + effect.map(entries(self), Iterable.map(([, value]) => value)) + +/** + * Retrieves all key-value pairs from the cache as an iterable. This function + * only returns entries with successfully resolved values, filtering out any + * failed lookups or expired entries. + * + * **Gotchas** + * + * Expired entries are removed from the cache while `entries` filters them out. + * + * @see {@link keys} for retrieving only cached keys + * @see {@link values} for retrieving only cached values + * + * @category combinators + * @since 4.0.0 + */ +export const entries = (self: Cache): Effect.Effect> => + core.withFiber((fiber) => { + const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + return effect.succeed(Iterable.filterMap(self.map, ([key, entry]) => { + if (entry.expiresAt === undefined || entry.expiresAt > now) { + const exit = entry.deferred.effect + return !core.isExit(exit) || effect.exitIsFailure(exit) + ? Result.failVoid + : Result.succeed([key, exit.value as A]) + } + MutableHashMap.remove(self.map, key) + return Result.failVoid + })) + }) diff --git a/repos/effect/packages/effect/src/Cause.ts b/repos/effect/packages/effect/src/Cause.ts new file mode 100644 index 0000000..2d0fda1 --- /dev/null +++ b/repos/effect/packages/effect/src/Cause.ts @@ -0,0 +1,1941 @@ +/** + * Records the full reason an `Effect` failed. + * + * A `Cause` can contain typed failures, unexpected defects, interruptions, + * and annotations. Keeping those details together lets code inspect or format + * failures without first collapsing them to a single error value. This module + * includes the `Cause` and `Reason` data types, helpers for building and + * checking causes, and small error types used by several Effect APIs. + * + * @since 2.0.0 + */ +import * as Context from "./Context.ts" +import type * as Effect from "./Effect.ts" +import type { Equal } from "./Equal.ts" +import type { Fiber } from "./Fiber.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as core from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import type { Option } from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { StackFrame } from "./References.ts" +import type * as Result from "./Result.ts" +import type * as Types from "./Types.ts" + +/** + * Unique brand for `Cause` values, used for runtime type checks via {@link isCause}. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: "~effect/Cause" = core.CauseTypeId + +/** + * Unique brand for `Reason` values, used for runtime type checks via {@link isReason}. + * + * @category type IDs + * @since 4.0.0 + */ +export const ReasonTypeId: "~effect/Cause/Reason" = core.CauseReasonTypeId + +/** + * A structured representation of how an Effect failed. + * + * **When to use** + * + * Use to preserve the full structured failure information for an effect instead + * of collapsing it to a single error value. + * + * **Details** + * + * Access the individual failure entries through the `reasons` array, then + * narrow each entry with {@link isFailReason}, {@link isDieReason}, or + * {@link isInterruptReason}. + * + * - Use {@link hasFails} / {@link hasDies} / {@link hasInterrupts} to test + * for the presence of specific reason kinds without iterating. + * - Use {@link findError} / {@link findDefect} to extract the first value + * of a given kind. + * - Use {@link combine} to merge two causes. + * + * `Cause` implements `Equal` — two causes with the same reasons (by value) + * compare as equal. + * + * **Example** (Creating and inspecting a cause) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail("Something went wrong") + * console.log(cause.reasons.length) // 1 + * console.log(Cause.isFailReason(cause.reasons[0])) // true + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Cause extends Pipeable, Inspectable, Equal { + readonly [TypeId]: typeof TypeId + readonly reasons: ReadonlyArray> +} + +/** + * Checks whether an arbitrary value is a `Cause`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isCause(Cause.fail("error"))) // true + * console.log(Cause.isCause("not a cause")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isCause: (self: unknown) => self is Cause = core.isCause + +/** + * Checks whether an arbitrary value is a `Reason` (`Fail`, `Die`, or `Interrupt`). + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * const reason = Cause.fail("error").reasons[0] + * console.log(Cause.isReason(reason)) // true + * console.log(Cause.isReason("not a reason")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isReason: (self: unknown) => self is Reason = core.isCauseReason + +/** + * A single entry inside a `Cause`'s `reasons` array. + * + * **Details** + * + * Narrow to a concrete type with {@link isFailReason}, {@link isDieReason}, + * or {@link isInterruptReason}. + * + * - `Fail` — typed error, access via `.error` + * - `Die` — untyped defect, access via `.defect` + * - `Interrupt` — fiber interruption, access via `.fiberId` + * + * Every reason carries an `annotations` map and an `annotate` method for + * attaching tracing metadata. + * + * **Example** (Narrowing a reason) + * + * ```ts + * import { Cause } from "effect" + * + * const reason = Cause.fail("error").reasons[0] + * if (Cause.isFailReason(reason)) { + * console.log(reason.error) // "error" + * } + * ``` + * + * @category models + * @since 4.0.0 + */ +export type Reason = Fail | Die | Interrupt + +/** + * Narrows a `Reason` to `Fail`. + * + * **When to use** + * + * Use as a predicate for `Array.filter` to pick out typed `Fail` reasons when + * iterating over `cause.reasons`. + * + * **Example** (Filtering fail reasons) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail("error") + * const fails = cause.reasons.filter(Cause.isFailReason) + * console.log(fails[0].error) // "error" + * ``` + * + * @see {@link isDieReason} — narrow to `Die` + * @see {@link isInterruptReason} — narrow to `Interrupt` + * + * @category guards + * @since 4.0.0 + */ +export const isFailReason: (self: Reason) => self is Fail = core.isFailReason + +/** + * Narrows a `Reason` to `Die`. + * + * **When to use** + * + * Use as a predicate for `Array.filter` to pick out `Die` (defect) reasons when + * iterating over `cause.reasons`. + * + * **Example** (Filtering die reasons) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.die("defect") + * const dies = cause.reasons.filter(Cause.isDieReason) + * console.log(dies[0].defect) // "defect" + * ``` + * + * @see {@link isFailReason} — narrow to `Fail` + * @see {@link isInterruptReason} — narrow to `Interrupt` + * + * @category guards + * @since 4.0.0 + */ +export const isDieReason: (self: Reason) => self is Die = core.isDieReason + +/** + * Narrows a `Reason` to `Interrupt`. + * + * **When to use** + * + * Use as a predicate for `Array.filter` to pick out `Interrupt` reasons when + * iterating over `cause.reasons`. + * + * **Example** (Filtering interrupt reasons) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.interrupt(123) + * const interrupts = cause.reasons.filter(Cause.isInterruptReason) + * console.log(interrupts[0].fiberId) // 123 + * ``` + * + * @see {@link isFailReason} — narrow to `Fail` + * @see {@link isDieReason} — narrow to `Die` + * + * @category guards + * @since 4.0.0 + */ +export const isInterruptReason: (self: Reason) => self is Interrupt = core.isInterruptReason + +/** + * Companion namespace for the `Cause` interface. + * + * @since 2.0.0 + */ +export declare namespace Cause { + /** + * Extracts the error type `E` from a `Cause`. + * + * **Example** (Extracting the error type) + * + * ```ts + * import type { Cause } from "effect" + * + * // string + * type E = Cause.Cause.Error> + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Error = T extends Cause ? E : never + + /** + * Base interface shared by all reason types (`Fail`, `Die`, `Interrupt`). + * + * **Details** + * + * Every reason carries: + * - `_tag` — discriminant string (`"Fail"`, `"Die"`, or `"Interrupt"`) + * - `annotations` — tracing metadata attached by the runtime + * - `annotate()` — returns a copy with additional annotations + * + * @category models + * @since 4.0.0 + */ + export interface ReasonProto extends Inspectable, Equal { + readonly [ReasonTypeId]: typeof ReasonTypeId + readonly _tag: Tag + readonly annotations: ReadonlyMap + annotate(annotations: Context.Context | ReadonlyMap, options?: { + readonly overwrite?: boolean | undefined + }): this + } +} + +/** + * Companion namespace for the `Reason` type. + * + * @since 4.0.0 + */ +export declare namespace Reason { + /** + * Extracts the error type `E` from a `Reason`. + * + * **Example** (Extracting the error type) + * + * ```ts + * import type { Cause } from "effect" + * + * // string + * type E = Cause.Reason.Error> + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Error = T extends Reason ? E : never +} + +/** + * An untyped defect — typically a programming error or an uncaught exception. + * + * **When to use** + * + * Use when inspecting `Cause` reasons that represent defects instead of typed + * failures or interruptions. + * + * **Details** + * + * The `defect` property is `unknown` because defects are not part of the + * typed error channel. Use {@link isDieReason} to narrow a `Reason` + * to this type. + * + * **Example** (Accessing the defect) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.die("Unexpected") + * const reason = cause.reasons[0] + * if (Cause.isDieReason(reason)) { + * console.log(reason.defect) // "Unexpected" + * } + * ``` + * + * @see {@link die} for constructing a cause with a single `Die` reason + * @see {@link isDieReason} for narrowing a `Reason` to `Die` + * + * @category models + * @since 2.0.0 + */ +export interface Die extends Cause.ReasonProto<"Die"> { + readonly defect: unknown +} + +/** + * A typed, expected error produced by `Effect.fail`. + * + * **When to use** + * + * Use when inspecting `Cause` reasons that represent expected failures from the + * typed error channel. + * + * **Details** + * + * The `error` property carries the typed value `E`. Use {@link isFailReason} + * to narrow a `Reason` to this type. + * + * **Example** (Accessing the error) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail("Something went wrong") + * const reason = cause.reasons[0] + * if (Cause.isFailReason(reason)) { + * console.log(reason.error) // "Something went wrong" + * } + * ``` + * + * @see {@link fail} for constructing a cause with a single `Fail` reason + * @see {@link isFailReason} for narrowing a `Reason` to `Fail` + * + * @category models + * @since 2.0.0 + */ +export interface Fail extends Cause.ReasonProto<"Fail"> { + readonly error: E +} + +/** + * A fiber interruption signal, optionally carrying the ID of the fiber that + * initiated the interruption. + * + * **Details** + * + * Use {@link isInterruptReason} to narrow a `Reason` to this type. + * + * **Example** (Accessing the fiber ID) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.interrupt(123) + * const reason = cause.reasons[0] + * if (Cause.isInterruptReason(reason)) { + * console.log(reason.fiberId) // 123 + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Interrupt extends Cause.ReasonProto<"Interrupt"> { + readonly fiberId: number | undefined +} + +/** + * Creates a `Cause` from an array of `Reason` values. + * + * **When to use** + * + * Use when you already have individual reasons (e.g. from filtering or + * transforming another cause's `reasons` array) and need to wrap them back + * into a `Cause`. + * + * **Details** + * + * - Returns a new `Cause`. + * - An empty array produces a cause equivalent to `empty`. + * + * **Gotchas** + * + * The `reasons` array is stored as provided. Treat the array as immutable + * after passing it to this function. + * + * **Example** (Building a cause from reasons) + * + * ```ts + * import { Cause } from "effect" + * + * const reasons = [ + * Cause.makeFailReason("err1"), + * Cause.makeFailReason("err2") + * ] + * const cause = Cause.fromReasons(reasons) + * console.log(cause.reasons.length) // 2 + * ``` + * + * @see {@link combine} — merge two existing causes + * + * @category constructors + * @since 4.0.0 + */ +export const fromReasons: ( + reasons: ReadonlyArray> +) => Cause = core.causeFromReasons + +/** + * Represents a `Cause` with an empty `reasons` array. + * + * **When to use** + * + * Use to represent the absence of failure when constructing or combining + * causes. + * + * **Details** + * + * Represents the absence of failure. Combining any cause with `empty` via + * {@link combine} returns the original cause unchanged. + * + * **Example** (Combining with the empty cause) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.combine(Cause.empty, Cause.fail("boom")) + * + * console.log(cause.reasons.length) // 1 + * console.log(Cause.hasFails(cause)) // true + * ``` + * + * @see {@link combine} for merging causes where `empty` acts as the identity + * + * @category constructors + * @since 2.0.0 + */ +export const empty: Cause = core.causeEmpty + +/** + * Creates a `Cause` containing a single `Fail` reason with the + * given typed error. + * + * **When to use** + * + * Use to construct a cause from an expected typed error. + * + * **Example** (Creating a fail cause) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail("Something went wrong") + * console.log(cause.reasons.length) // 1 + * console.log(Cause.isFailReason(cause.reasons[0])) // true + * ``` + * + * @see {@link die} — for untyped defects + * @see {@link interrupt} — for fiber interruptions + * + * @category constructors + * @since 2.0.0 + */ +export const fail: (error: E) => Cause = core.causeFail + +/** + * Creates a `Cause` containing a single `Die` reason with the + * given defect. + * + * **When to use** + * + * Use to construct a cause from an untyped defect or unexpected thrown value. + * + * **Example** (Creating a die cause) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.die("Unexpected") + * console.log(cause.reasons.length) // 1 + * console.log(Cause.isDieReason(cause.reasons[0])) // true + * ``` + * + * @see {@link fail} — for typed errors + * @see {@link interrupt} — for fiber interruptions + * + * @category constructors + * @since 2.0.0 + */ +export const die: (defect: unknown) => Cause = core.causeDie + +/** + * Creates a `Cause` containing a single `Interrupt` reason, + * optionally carrying the interrupting fiber's ID. + * + * **Example** (Creating an interrupt cause) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.interrupt(123) + * console.log(cause.reasons.length) // 1 + * console.log(Cause.isInterruptReason(cause.reasons[0])) // true + * ``` + * + * @see {@link fail} — for typed errors + * @see {@link die} — for untyped defects + * + * @category constructors + * @since 2.0.0 + */ +export const interrupt: (fiberId?: number | undefined) => Cause = effect.causeInterrupt + +/** + * Creates a standalone `Fail` reason (not wrapped in a `Cause`). + * + * **When to use** + * + * Use when constructing a standalone typed failure reason for + * {@link fromReasons} or direct comparison. + * + * **Example** (Creating a Fail reason) + * + * ```ts + * import { Cause } from "effect" + * + * const reason = Cause.makeFailReason("error") + * console.log(reason._tag) // "Fail" + * console.log(reason.error) // "error" + * ``` + * + * @see {@link makeDieReason} — create a `Die` reason + * @see {@link makeInterruptReason} — create an `Interrupt` reason + * + * @category constructors + * @since 4.0.0 + */ +export const makeFailReason = (error: E): Fail => new core.Fail(error) + +/** + * Creates a standalone `Die` reason (not wrapped in a `Cause`). + * + * **When to use** + * + * Use when constructing a standalone defect reason for {@link fromReasons} or + * direct comparison. + * + * **Example** (Creating a Die reason) + * + * ```ts + * import { Cause } from "effect" + * + * const reason = Cause.makeDieReason("bug") + * console.log(reason._tag) // "Die" + * console.log(reason.defect) // "bug" + * ``` + * + * @see {@link makeFailReason} — create a `Fail` reason + * @see {@link makeInterruptReason} — create an `Interrupt` reason + * + * @category constructors + * @since 4.0.0 + */ +export const makeDieReason = (defect: unknown): Die => new core.Die(defect) + +/** + * Creates a standalone `Interrupt` reason (not wrapped in a `Cause`), + * optionally carrying the interrupting fiber's ID. + * + * **When to use** + * + * Use when constructing a standalone interrupt reason for {@link fromReasons} + * or direct comparison. + * + * **Example** (Creating an Interrupt reason) + * + * ```ts + * import { Cause } from "effect" + * + * const reason = Cause.makeInterruptReason(42) + * console.log(reason._tag) // "Interrupt" + * console.log(reason.fiberId) // 42 + * ``` + * + * @see {@link makeFailReason} — create a `Fail` reason + * @see {@link makeDieReason} — create a `Die` reason + * + * @category constructors + * @since 4.0.0 + */ +export const makeInterruptReason: (fiberId?: number | undefined) => Interrupt = effect.makeInterruptReason + +/** + * Returns `true` if every reason in the cause is an `Interrupt` (and + * there is at least one reason). + * + * **When to use** + * + * Use when you need to detect failures caused only by interruption. + * + * **Example** (Checking interrupt-only causes) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.hasInterruptsOnly(Cause.interrupt(123))) // true + * console.log(Cause.hasInterruptsOnly(Cause.fail("error"))) // false + * console.log(Cause.hasInterruptsOnly(Cause.empty)) // false + * ``` + * + * @see {@link hasInterrupts} — `true` if the cause contains *any* interrupts + * + * @category predicates + * @since 4.0.0 + */ +export const hasInterruptsOnly: (self: Cause) => boolean = effect.hasInterruptsOnly + +/** + * Transforms the typed error values inside a `Cause` using the + * provided function. Only `Fail` reasons are affected; `Die` and `Interrupt` + * reasons pass through unchanged. + * + * **When to use** + * + * Use to transform expected typed failures while preserving defects and + * interruptions unchanged. + * + * **Details** + * + * If at least one `Fail` reason exists, this returns a new `Cause` + * containing the mapped failures. If the cause has no `Fail` reasons, the + * original cause is returned unchanged. + * + * **Example** (Mapping errors to uppercase) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail("error") + * const mapped = Cause.map(cause, (e) => e.toUpperCase()) + * const reason = mapped.reasons[0] + * if (Cause.isFailReason(reason)) { + * console.log(reason.error) // "ERROR" + * } + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (error: Types.NoInfer) => E2): (self: Cause) => Cause + (self: Cause, f: (error: Types.NoInfer) => E2): Cause +} = effect.causeMap + +/** + * Merges two causes into a single cause whose `reasons` array is the union + * of both inputs (de-duplicated by value equality). + * + * **When to use** + * + * Use to merge independent causes into one structured failure value. + * + * **Details** + * + * - Combining with `empty` returns the other cause unchanged. + * - If the result is structurally equal to `self`, `self` is returned + * (referential shortcut). + * + * **Example** (Combining two causes) + * + * ```ts + * import { Cause } from "effect" + * + * const cause1 = Cause.fail("error1") + * const cause2 = Cause.fail("error2") + * const combined = Cause.combine(cause1, cause2) + * console.log(combined.reasons.length) // 2 + * ``` + * + * @see {@link fromReasons} — build a cause from an array of reasons + * @see {@link empty} for the identity cause used when combining + * + * @category combining + * @since 4.0.0 + */ +export const combine: { + (that: Cause): (self: Cause) => Cause + (self: Cause, that: Cause): Cause +} = effect.causeCombine + +/** + * Collapses a `Cause` into a single `unknown` value, picking the "most + * important" failure in this order: + * + * **When to use** + * + * Use to collapse a structured cause to the single value that synchronous and + * promise runners would throw. + * + * **Details** + * + * 1. First `Fail` error (the `E` value) + * 2. First `Die` defect + * 3. A generic `Error("All fibers interrupted without error")` for interrupt-only causes + * 4. A generic `Error("Empty cause")` for `empty` + * + * This is the function used by `Effect.runPromise` and `Effect.runSync` to + * decide what to throw. + * + * **Gotchas** + * + * This function is lossy. Use {@link prettyErrors} or iterate `cause.reasons` + * when you need all failures. + * + * **Example** (Squashing a cause) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.squash(Cause.fail("error"))) // "error" + * console.log(Cause.squash(Cause.die("defect"))) // "defect" + * ``` + * + * @see {@link prettyErrors} — non-lossy conversion to `Array` + * @see {@link pretty} — human-readable string rendering + * + * @category destructors + * @since 2.0.0 + */ +export const squash: (self: Cause) => unknown = effect.causeSquash + +/** + * Returns `true` if the cause contains at least one `Fail` reason. + * + * **When to use** + * + * Use to check whether a cause includes typed failures before extracting, + * mapping, or rendering them. + * + * **Example** (Checking for typed errors) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.hasFails(Cause.fail("error"))) // true + * console.log(Cause.hasFails(Cause.die("defect"))) // false + * ``` + * + * @see {@link hasDies} — check for defects + * @see {@link hasInterrupts} — check for interruptions + * + * @category predicates + * @since 4.0.0 + */ +export const hasFails: (self: Cause) => boolean = effect.hasFails + +/** + * Returns a `Result` whose success value is the first `Fail` reason in + * the cause, including its annotations. If the cause has no `Fail` reason, the + * failure value is the original cause narrowed to `Cause`, because it + * contains no typed error reasons. + * + * **When to use** + * + * Use when you need the full `Fail` reason from a `Cause`, including + * annotations. + * + * **Example** (Extracting the first Fail reason) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.findFail(Cause.fail("error")) + * if (!Result.isFailure(result)) { + * console.log(result.success.error) // "error" + * } + * ``` + * + * @see {@link findError} — extract the unwrapped `E` value + * @see {@link findDie} — extract the first `Die` reason + * + * @category filtering + * @since 4.0.0 + */ +export const findFail: (self: Cause) => Result.Result, Cause> = effect.findFail + +/** + * Returns a `Result` whose success value is the first typed error value `E` + * from a `Fail` reason in the cause. If the cause has no `Fail` reason, + * the failure value is the original cause narrowed to `Cause`, because + * it contains no typed error reasons. + * + * **When to use** + * + * Use when you need the first typed error value from a `Cause` as a `Result` + * that preserves the original cause when no match is found. + * + * **Example** (Extracting the first error value) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.findError(Cause.fail("error")) + * if (!Result.isFailure(result)) { + * console.log(result.success) // "error" + * } + * ``` + * + * @see {@link findFail} — extract the full `Fail` reason + * @see {@link findErrorOption} — `Option`-based variant + * + * @category filtering + * @since 4.0.0 + */ +export const findError: (self: Cause) => Result.Result> = effect.findError + +/** + * Returns the first typed error value `E` from a cause wrapped in + * `Option.some`, or `Option.none` if no `Fail` reason exists. + * + * **When to use** + * + * Use when you need the first typed error value from a `Cause` as an `Option`, + * discarding the original cause. + * + * **Example** (Extracting an error as Option) + * + * ```ts + * import { Cause, Option } from "effect" + * + * const some = Cause.findErrorOption(Cause.fail("error")) + * console.log(Option.isSome(some)) // true + * + * const none = Cause.findErrorOption(Cause.die("defect")) + * console.log(Option.isNone(none)) // true + * ``` + * + * @see {@link findError} — `Result`-based variant + * + * @category filtering + * @since 4.0.0 + */ +export const findErrorOption: (input: Cause) => Option = effect.findErrorOption + +/** + * Returns `true` if the cause contains at least one `Die` reason. + * + * **When to use** + * + * Use to check whether a cause includes defects before extracting or rendering + * them. + * + * **Example** (Checking for defects) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.hasDies(Cause.die("defect"))) // true + * console.log(Cause.hasDies(Cause.fail("error"))) // false + * ``` + * + * @see {@link hasFails} — check for typed errors + * @see {@link hasInterrupts} — check for interruptions + * + * @category predicates + * @since 4.0.0 + */ +export const hasDies: (self: Cause) => boolean = effect.hasDies + +/** + * Returns a `Result` whose success value is the first `Die` reason in + * the cause, including its annotations. If the cause has no `Die` reason, the + * failure value is the original cause. + * + * **When to use** + * + * Use when you need the full `Die` reason from a `Cause`, including + * annotations. + * + * **Example** (Extracting the first Die reason) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.findDie(Cause.die("defect")) + * if (!Result.isFailure(result)) { + * console.log(result.success.defect) // "defect" + * } + * ``` + * + * @see {@link findDefect} — extract the unwrapped defect value + * @see {@link findFail} — extract the first `Fail` reason + * + * @category filtering + * @since 4.0.0 + */ +export const findDie: (self: Cause) => Result.Result> = effect.findDie + +/** + * Returns a `Result` whose success value is the first defect value from a + * `Die` reason in the cause. If the cause has no `Die` reason, the + * failure value is the original cause. + * + * **When to use** + * + * Use when you need the first defect value from a `Cause` as a `Result`, + * without the full `Die` reason. + * + * **Example** (Extracting the first defect) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.findDefect(Cause.die("defect")) + * if (!Result.isFailure(result)) { + * console.log(result.success) // "defect" + * } + * ``` + * + * @see {@link findDie} — extract the full `Die` reason + * @see {@link findError} — extract the first typed error + * + * @category filtering + * @since 4.0.0 + */ +export const findDefect: (self: Cause) => Result.Result> = effect.findDefect + +/** + * Returns `true` if the cause contains at least one `Interrupt` reason. + * + * **Example** (Checking for interruptions) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.hasInterrupts(Cause.interrupt(123))) // true + * console.log(Cause.hasInterrupts(Cause.fail("error"))) // false + * ``` + * + * @see {@link hasInterruptsOnly} — `true` only when *all* reasons are interrupts + * @see {@link hasFails} — check for typed errors + * @see {@link hasDies} — check for defects + * + * @category predicates + * @since 4.0.0 + */ +export const hasInterrupts: (self: Cause) => boolean = effect.hasInterrupts + +/** + * Returns a `Result` whose success value is the first `Interrupt` reason + * in the cause, including its annotations. If the cause has no `Interrupt` + * reason, the failure value is the original cause. + * + * **When to use** + * + * Use when you need the first `Interrupt` reason from a `Cause`, including the + * fiber ID and annotations. + * + * **Example** (Extracting the first interrupt) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.findInterrupt(Cause.interrupt(42)) + * if (!Result.isFailure(result)) { + * console.log(result.success.fiberId) // 42 + * } + * ``` + * + * @see {@link interruptors} — collect all interrupting fiber IDs as a `Set` + * + * @category filtering + * @since 4.0.0 + */ +export const findInterrupt: (self: Cause) => Result.Result> = effect.findInterrupt + +/** + * Collects the defined fiber IDs from all `Interrupt` reasons in the + * cause into a `ReadonlySet`. Interrupt reasons without a `fiberId` are + * ignored. Returns an empty set when the cause has no interrupting fiber IDs. + * + * **When to use** + * + * Use when you need interrupting fiber IDs as a set, with absence represented + * as an empty set. + * + * **Example** (Collecting interruptors) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.combine( + * Cause.interrupt(1), + * Cause.interrupt(2) + * ) + * + * console.log(Cause.interruptors(cause)) // Set(2) { 1, 2 } + * ``` + * + * @see {@link filterInterruptors} — `Result`-based variant + * + * @category accessors + * @since 2.0.0 + */ +export const interruptors: (self: Cause) => ReadonlySet = effect.causeInterruptors + +/** + * Returns a `Result` whose success value is the set of defined fiber IDs from + * the cause's `Interrupt` reasons. If the cause has no `Interrupt` + * reason, the failure value is the original cause. + * + * **When to use** + * + * Use when you need absence of interrupt reasons to fail with the original + * cause. + * + * **Gotchas** + * + * Interrupt reasons without a `fiberId` still count as interrupts, so the + * function succeeds with an empty `Set` when every interrupt reason has an + * undefined fiber ID. + * + * **Example** (Extracting interruptors with Result) + * + * ```ts + * import { Cause, Result } from "effect" + * + * const result = Cause.filterInterruptors(Cause.interrupt(1)) + * if (!Result.isFailure(result)) { + * console.log(result.success) // Set(1) { 1 } + * } + * ``` + * + * @see {@link interruptors} — always-succeeding variant + * + * @category filtering + * @since 4.0.0 + */ +export const filterInterruptors: (self: Cause) => Result.Result, Cause> = + effect.causeFilterInterruptors + +/** + * Converts a `Cause` into an `Array` suitable for logging or + * rethrowing. + * + * **When to use** + * + * Use to convert every renderable failure in a cause into individual `Error` + * values before logging or rethrowing. + * + * **Details** + * + * Each `Fail` and `Die` reason is converted into a standard + * `Error`: + * + * - **Objects / Error instances** — `message`, `name`, `stack`, and `cause` + * are preserved. Extra enumerable properties are copied. Stack traces are + * cleaned up and enriched with span annotations when available. + * - **Strings** — used directly as the `Error` message. + * - **Other primitives** (`null`, `undefined`, numbers, …) — wrapped in an + * `Error` with message `"Unknown error: "`. + * + * `Interrupt` reasons are collected separately. If the cause contains + * **only** interrupts (no `Fail` or `Die`), a single `InterruptError` is + * returned whose `cause` lists the interrupting fiber IDs. + * + * An empty cause returns an empty array. + * + * **Example** (Converting a cause to errors) + * + * ```ts + * import { Cause } from "effect" + * + * const cause = Cause.fail(new Error("boom")) + * const errors = Cause.prettyErrors(cause) + * console.log(errors[0].message) // "boom" + * ``` + * + * @see {@link pretty} — renders the cause as a single string + * @see {@link squash} — lossy collapse to a single thrown value + * + * @category rendering + * @since 3.2.0 + */ +export const prettyErrors: (self: Cause, options?: { + readonly includeCauseInStack?: boolean | undefined +}) => Array = effect.causePrettyErrors + +/** + * Formats a `Cause` as a human-readable string for logging or debugging. + * + * **When to use** + * + * Use to render a whole cause as one human-readable string for logs or + * diagnostics. + * + * **Details** + * + * Delegates to {@link prettyErrors} to convert each reason to an `Error`, + * then joins their stack traces with newlines. Nested `Error.cause` chains + * are rendered inline with indentation: + * + * ```text + * ErrorName: message + * at ... + * at ... { + * [cause]: NestedError: message + * at ... + * } + * ``` + * + * Span annotations are appended to the relevant stack frames when available. + * + * **Gotchas** + * + * Rendering an empty cause produces an empty string because there are no + * errors to render. + * + * **Example** (Rendering a cause) + * + * ```ts + * import { Cause } from "effect" + * + * const rendered = Cause.pretty(Cause.fail("something went wrong")) + * console.log(rendered.includes("something went wrong")) // true + * ``` + * + * @see {@link prettyErrors} — get the individual `Error` instances + * + * @category rendering + * @since 2.0.0 + */ +export const pretty: (cause: Cause) => string = effect.causePretty + +/** + * Base interface for error classes that can be yielded directly inside + * `Effect.gen`. Yielding one of these errors fails the generator with that + * error as the typed failure value. + * + * **Details** + * + * All built-in error classes in this module (`NoSuchElementError`, + * `TimeoutError`, `IllegalArgumentError`, `ExceededCapacityError`, + * `AsyncFiberError`, and `UnknownError`) implement this interface. + * + * **Example** (Yielding an error in Effect.gen) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const error = new Cause.NoSuchElementError("not found") + * + * const program = Effect.gen(function*() { + * return yield* error // fails the effect with NoSuchElementError + * }) + * ``` + * + * @category errors + * @since 2.0.0 + */ +export interface YieldableError extends Error, Pipeable, Inspectable { + readonly [Effect.TypeId]: Effect.Variance + [Symbol.iterator](): Effect.EffectIterator> +} + +/** + * Checks whether an arbitrary value is a `NoSuchElementError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isNoSuchElementError(new Cause.NoSuchElementError())) // true + * console.log(Cause.isNoSuchElementError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isNoSuchElementError: (u: unknown) => u is NoSuchElementError = core.isNoSuchElementError + +/** + * Unique brand for `NoSuchElementError`. + * + * @category type IDs + * @since 4.0.0 + */ +export const NoSuchElementErrorTypeId: "~effect/Cause/NoSuchElementError" = core.NoSuchElementErrorTypeId + +/** + * An error indicating that an expected value was absent. + * + * **When to use** + * + * Use to model APIs that intentionally turn absence into an error. + * + * **Details** + * + * Used by APIs that convert absence into an exception or effect failure, such + * as `Option.getOrThrow`. Implements `YieldableError` so it can be + * yielded directly in `Effect.gen`. + * + * **Gotchas** + * + * Prefer APIs that return `Option` or a typed failure when absence is an + * expected case. This error is mainly for APIs that intentionally turn absence + * into a thrown value or failed effect. + * + * **Example** (Creating and checking a NoSuchElementError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.NoSuchElementError("Element not found") + * console.log(error._tag) // "NoSuchElementError" + * console.log(error.message) // "Element not found" + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface NoSuchElementError extends YieldableError { + readonly [NoSuchElementErrorTypeId]: typeof NoSuchElementErrorTypeId + readonly _tag: "NoSuchElementError" +} + +/** + * Constructs a `NoSuchElementError` with an optional message. + * + * **When to use** + * + * Use to create the error value for APIs that intentionally fail when an + * expected element is absent. + * + * **Example** (Creating a NoSuchElementError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.NoSuchElementError("Element not found") + * console.log(error.message) // "Element not found" + * ``` + * + * @see {@link isNoSuchElementError} for checking unknown values + * + * @category constructors + * @since 4.0.0 + */ +export const NoSuchElementError: new(message?: string) => NoSuchElementError = core.NoSuchElementError + +/** + * Checks whether an arbitrary value is a `Done` signal. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isDone(Cause.Done())) // true + * console.log(Cause.isDone("not done")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isDone: (u: unknown) => u is Done = core.isDone + +/** + * Unique brand for `Done` values. + * + * @category type IDs + * @since 4.0.0 + */ +export const DoneTypeId: "~effect/Cause/Done" = core.DoneTypeId + +/** + * A graceful completion signal for queues and streams. + * + * **When to use** + * + * Use to model normal producer completion through a stream or queue error + * channel. + * + * **Details** + * + * `Done` indicates that a producer has finished normally — no more elements + * will arrive. It is distinct from an error or interruption; it represents + * successful completion. The optional `value` field can carry a final + * leftover payload. + * + * **Example** (Signaling queue completion) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * yield* Queue.offer(queue, 1) + * yield* Queue.end(queue) + * + * const result = yield* Effect.flip(Queue.take(queue)) + * console.log(Cause.isDone(result)) // true + * }) + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface Done { + readonly [DoneTypeId]: typeof DoneTypeId + readonly _tag: "Done" + readonly value: A +} + +/** + * Companion namespace for the `Done` interface. + * + * @since 4.0.0 + */ +export declare namespace Done { + /** + * Extracts the value type `A` from a `Done` that may be nested in an + * error union. + * + * @category utility types + * @since 4.0.0 + */ + export type Extract = E extends Done ? L : never + + /** + * Filters a type union to only keep `Done` members. + * + * @category filtering + * @since 4.0.0 + */ + export type Only = E extends Done ? Done : never +} + +/** + * Creates a `Done` signal with an optional value. + * + * **When to use** + * + * Use when you need to construct a low-level pull completion signal directly. + * + * @see {@link done} — create a failing `Effect` with `Done` + * + * @category constructors + * @since 4.0.0 + */ +export const Done: (value?: A) => Done = core.Done + +/** + * Creates an Effect that fails with a `Done` error. Shorthand for + * `Effect.fail(Cause.Done(value))`. + * + * **When to use** + * + * Use when you model stream or queue completion through the error channel. + * + * **Example** (Failing with Done) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const program = Cause.done("finished") + * + * Effect.runPromiseExit(program).then((exit) => { + * console.log(exit._tag) // "Failure" + * }) + * ``` + * + * @see {@link Done} — create the signal value without an Effect + * + * @category constructors + * @since 4.0.0 + */ +export const done: (value?: A) => Effect.Effect> = core.done + +/** + * Unique brand for `TimeoutError`. + * + * @category type IDs + * @since 4.0.0 + */ +export const TimeoutErrorTypeId: "~effect/Cause/TimeoutError" = effect.TimeoutErrorTypeId + +/** + * Checks whether an arbitrary value is a `TimeoutError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isTimeoutError(new Cause.TimeoutError())) // true + * console.log(Cause.isTimeoutError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTimeoutError: (u: unknown) => u is TimeoutError = effect.isTimeoutError + +/** + * An error indicating that an operation exceeded its time limit. + * + * **Details** + * + * Produced by `Effect.timeout` and related APIs. Implements + * `YieldableError`. + * + * **Example** (Creating and checking a TimeoutError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.TimeoutError("Operation timed out") + * console.log(error._tag) // "TimeoutError" + * console.log(error.message) // "Operation timed out" + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface TimeoutError extends YieldableError { + readonly [TimeoutErrorTypeId]: typeof TimeoutErrorTypeId + readonly _tag: "TimeoutError" +} + +/** + * Constructs a `TimeoutError` with an optional message. + * + * **Example** (Creating a TimeoutError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.TimeoutError("Operation timed out") + * console.log(error.message) // "Operation timed out" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const TimeoutError: new(message?: string) => TimeoutError = effect.TimeoutError + +/** + * Unique brand for `IllegalArgumentError`. + * + * @category type IDs + * @since 4.0.0 + */ +export const IllegalArgumentErrorTypeId: "~effect/Cause/IllegalArgumentError" = effect.IllegalArgumentErrorTypeId + +/** + * Checks whether an arbitrary value is an `IllegalArgumentError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isIllegalArgumentError(new Cause.IllegalArgumentError())) // true + * console.log(Cause.isIllegalArgumentError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isIllegalArgumentError: (u: unknown) => u is IllegalArgumentError = effect.isIllegalArgumentError + +/** + * An error indicating that a function received an argument that violates + * its contract (e.g. negative where positive was expected). + * + * **Details** + * + * Implements `YieldableError`. + * + * **Example** (Creating and checking an IllegalArgumentError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.IllegalArgumentError("Expected positive number") + * console.log(error._tag) // "IllegalArgumentError" + * console.log(error.message) // "Expected positive number" + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface IllegalArgumentError extends YieldableError { + readonly [IllegalArgumentErrorTypeId]: typeof IllegalArgumentErrorTypeId + readonly _tag: "IllegalArgumentError" +} + +/** + * Constructs an `IllegalArgumentError` with an optional message. + * + * **Example** (Creating an IllegalArgumentError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.IllegalArgumentError("Invalid argument") + * console.log(error.message) // "Invalid argument" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const IllegalArgumentError: new(message?: string) => IllegalArgumentError = effect.IllegalArgumentError + +/** + * Checks whether an arbitrary value is an `ExceededCapacityError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isExceededCapacityError(new Cause.ExceededCapacityError())) // true + * console.log(Cause.isExceededCapacityError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isExceededCapacityError: (u: unknown) => u is ExceededCapacityError = effect.isExceededCapacityError + +/** + * Unique brand for `ExceededCapacityError`. + * + * @category type IDs + * @since 4.0.0 + */ +export const ExceededCapacityErrorTypeId: "~effect/Cause/ExceededCapacityError" = effect.ExceededCapacityErrorTypeId + +/** + * An error indicating that a bounded resource (queue, pool, semaphore, etc.) + * has exceeded its capacity. + * + * **When to use** + * + * Use to model bounded-resource failures where an operation cannot proceed + * because capacity has been exhausted. + * + * **Details** + * + * Implements `YieldableError`. + * + * **Example** (Creating and checking an ExceededCapacityError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.ExceededCapacityError("Queue full") + * console.log(error._tag) // "ExceededCapacityError" + * console.log(error.message) // "Queue full" + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface ExceededCapacityError extends YieldableError { + readonly [ExceededCapacityErrorTypeId]: typeof ExceededCapacityErrorTypeId + readonly _tag: "ExceededCapacityError" +} + +/** + * Constructs an `ExceededCapacityError` with an optional message. + * + * **When to use** + * + * Use to create the error value for bounded-resource capacity failures. + * + * **Example** (Creating an ExceededCapacityError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.ExceededCapacityError("Queue full") + * console.log(error.message) // "Queue full" + * ``` + * + * @see {@link isExceededCapacityError} for checking unknown values + * + * @category constructors + * @since 4.0.0 + */ +export const ExceededCapacityError: new(message?: string) => ExceededCapacityError = effect.ExceededCapacityError + +/** + * Unique brand present on `AsyncFiberError` values and used by + * `isAsyncFiberError` for runtime checks. + * + * @category type IDs + * @since 4.0.0 + */ +export const AsyncFiberErrorTypeId: "~effect/Cause/AsyncFiberError" = effect.AsyncFiberErrorTypeId + +/** + * Checks whether an arbitrary value is an `AsyncFiberError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * import type { Fiber } from "effect" + * + * declare const fiber: Fiber.Fiber + * + * const error = new Cause.AsyncFiberError(fiber) + * console.log(Cause.isAsyncFiberError(error)) // true + * console.log(Cause.isAsyncFiberError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isAsyncFiberError: (u: unknown) => u is AsyncFiberError = effect.isAsyncFiberError + +/** + * An error that occurs when trying to run an async fiber with Effect.runSync. + * + * **When to use** + * + * Use to inspect failures produced when synchronous runners encounter an effect + * that cannot complete synchronously. + * + * **Details** + * + * The `fiber` property stores the fiber that could not be synchronously + * resolved. This error implements `YieldableError`. + * + * **Example** (Accessing the fiber) + * + * ```ts + * import { Cause } from "effect" + * import type { Fiber } from "effect" + * + * declare const fiber: Fiber.Fiber + * + * const error = new Cause.AsyncFiberError(fiber) + * console.log(error._tag) // "AsyncFiberError" + * console.log(error.fiber === fiber) // true + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface AsyncFiberError extends YieldableError { + readonly [AsyncFiberErrorTypeId]: typeof AsyncFiberErrorTypeId + readonly _tag: "AsyncFiberError" + readonly fiber: Fiber +} + +/** + * Constructs an `AsyncFiberError` for a fiber that could not be resolved + * synchronously. + * + * **When to use** + * + * Use to create the error value for a fiber that could not be completed by a + * synchronous runner. + * + * **Example** (Creating an AsyncFiberError) + * + * ```ts + * import { Cause } from "effect" + * import type { Fiber } from "effect" + * + * declare const fiber: Fiber.Fiber + * + * const error = new Cause.AsyncFiberError(fiber) + * console.log(error.message) // "An asynchronous Effect was executed with Effect.runSync" + * ``` + * + * @see {@link isAsyncFiberError} for checking unknown values + * + * @category constructors + * @since 4.0.0 + */ +export const AsyncFiberError: new(fiber: Fiber) => AsyncFiberError = effect.AsyncFiberError + +/** + * Unique brand for `UnknownError`. + * + * @category type IDs + * @since 4.0.0 + */ +export const UnknownErrorTypeId: "~effect/Cause/UnknownError" = effect.UnknownErrorTypeId + +/** + * Checks whether an arbitrary value is an `UnknownError`. + * + * **Example** (Checking the runtime type) + * + * ```ts + * import { Cause } from "effect" + * + * console.log(Cause.isUnknownError(new Cause.UnknownError("x"))) // true + * console.log(Cause.isUnknownError("nope")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isUnknownError: (u: unknown) => u is UnknownError = effect.isUnknownError + +/** + * A wrapper for errors whose type is not statically known. + * + * **Details** + * + * Used when a thrown or rejected value is not represented by a more specific + * typed error. The original value is stored in the `cause` property inherited + * from `Error`. Implements `YieldableError`. + * + * **Example** (Creating and checking an UnknownError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.UnknownError("original", "Something unknown") + * console.log(error._tag) // "UnknownError" + * console.log(error.message) // "Something unknown" + * ``` + * + * @category errors + * @since 4.0.0 + */ +export interface UnknownError extends YieldableError { + readonly [UnknownErrorTypeId]: typeof UnknownErrorTypeId + readonly _tag: "UnknownError" +} + +/** + * Constructs an `UnknownError`. The first argument is the original + * cause (stored in `Error.cause`); the second is an optional human-readable + * message. + * + * **Example** (Creating an UnknownError) + * + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.UnknownError({ raw: true }, "Unexpected value") + * console.log(error.message) // "Unexpected value" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const UnknownError: new(cause: unknown, message?: string) => UnknownError = effect.UnknownError + +/** + * Attaches metadata to every reason in a `Cause`. + * + * **When to use** + * + * Use to attach diagnostic metadata to every reason in a cause. + * + * **Details** + * + * Annotations are stored as a `Context` on each reason and can be + * retrieved later via {@link reasonAnnotations} or {@link annotations}. + * The runtime uses this to attach stack traces and spans. + * + * - Returns a new `Cause`. + * - By default, existing keys are preserved. Pass `{ overwrite: true }` to + * replace them. + * + * **Example** (Annotating a cause) + * + * ```ts + * import { Cause, Context } from "effect" + * + * class RequestId extends Context.Service()("RequestId") {} + * + * const cause = Cause.fail("error") + * const annotated = Cause.annotate(cause, Context.make(RequestId, "req-1")) + * + * console.log(Context.getOrUndefined(Cause.annotations(annotated), RequestId)) // "req-1" + * ``` + * + * @see {@link annotations} for reading merged annotations from a cause + * @see {@link reasonAnnotations} for reading annotations from a single reason + * + * @category annotations + * @since 4.0.0 + */ +export const annotate: { + ( + annotations: Context.Context, + options?: { readonly overwrite?: boolean | undefined } + ): (self: Cause) => Cause + ( + self: Cause, + annotations: Context.Context, + options?: { readonly overwrite?: boolean | undefined } + ): Cause +} = core.causeAnnotate + +/** + * Reads the annotations from a single `Reason` as a `Context`. + * + * **When to use** + * + * Use when you need tracing metadata (e.g. `StackTrace`) from + * a specific reason rather than the whole cause. + * + * **Example** (Reading reason annotations) + * + * ```ts + * import { Cause, Context } from "effect" + * + * class RequestId extends Context.Service()("RequestId") {} + * + * const reason = Cause.makeFailReason("error") + * const annotated = reason.annotate(Context.make(RequestId, "req-1")) + * + * console.log(Context.getOrUndefined(Cause.reasonAnnotations(annotated), RequestId)) // "req-1" + * ``` + * + * @see {@link annotations} — merged annotations from all reasons in a cause + * + * @category annotations + * @since 4.0.0 + */ +export const reasonAnnotations: (self: Reason) => Context.Context = effect.reasonAnnotations + +/** + * Reads the merged annotations from all reasons in a `Cause`. + * + * **When to use** + * + * Use to read diagnostic metadata merged from the whole cause. + * + * **Gotchas** + * + * When multiple reasons contain the same annotation key, the value from the + * later reason wins. + * + * **Example** (Reading merged annotations) + * + * ```ts + * import { Cause, Context } from "effect" + * + * class RequestId extends Context.Service()("RequestId") {} + * + * const cause = Cause.annotate( + * Cause.fail("error"), + * Context.make(RequestId, "req-1") + * ) + * + * console.log(Context.getOrUndefined(Cause.annotations(cause), RequestId)) // "req-1" + * ``` + * + * @see {@link reasonAnnotations} — annotations from a single reason + * + * @category annotations + * @since 4.0.0 + */ +export const annotations: (self: Cause) => Context.Context = effect.causeAnnotations + +/** + * Context annotation used to store the stack frame captured at the point of failure. + * + * **When to use** + * + * Use to read the failure stack-frame annotation from a `Reason` when building + * diagnostics, logging, or custom cause renderers. + * + * **Details** + * + * The runtime annotates every reason with this when a stack frame is + * available. Retrieve it via + * `Context.get(Cause.reasonAnnotations(reason), Cause.StackTrace)`. + * + * @see {@link reasonAnnotations} for reading annotations from a single reason + * @see {@link annotations} for reading merged annotations from a cause + * @see {@link InterruptorStackTrace} for the interrupt-specific stack-frame annotation + * + * @category annotations + * @since 4.0.0 + */ +export class StackTrace extends Context.Service()("effect/Cause/StackTrace") {} + +/** + * Context annotation used to store the stack frame captured at the point of + * interruption. + * + * **When to use** + * + * Use when you need the stack-frame annotation used by interrupt-only cause + * rendering. + * + * **Details** + * + * Similar to `StackTrace` but specific to `Interrupt` reasons. + * + * @see {@link StackTrace} for stack frames attached to failures + * @see {@link reasonAnnotations} for reading annotations from a single reason + * @see {@link annotate} for attaching annotations to a cause + * + * @category annotations + * @since 4.0.0 + */ +export class InterruptorStackTrace + extends Context.Service()("effect/Cause/InterruptorStackTrace") +{} diff --git a/repos/effect/packages/effect/src/Channel.ts b/repos/effect/packages/effect/src/Channel.ts new file mode 100644 index 0000000..f094d9b --- /dev/null +++ b/repos/effect/packages/effect/src/Channel.ts @@ -0,0 +1,8615 @@ +/** + * Provides low-level building blocks for streaming data through Effect. + * + * A `Channel` can read input elements, write output elements, fail with a typed + * error, and finish with a typed result while managing resources safely. + * Streams and sinks are built on channels, so most application code uses those + * higher-level modules instead. This module is useful when implementing stream + * operators or specialized streaming workflows. + * + * @since 2.0.0 + */ +// @effect-diagnostics returnEffectInGen:off +import * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Chunk from "./Chunk.ts" +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import type * as Filter from "./Filter.ts" +import type { LazyArg } from "./Function.ts" +import { constant, constTrue, constVoid, dual, identity as identity_ } from "./Function.ts" +import { ClockRef, endSpan } from "./internal/effect.ts" +import { addSpanStackTrace } from "./internal/tracer.ts" +import * as Iterable from "./Iterable.ts" +import * as Latch from "./Latch.ts" +import * as Layer from "./Layer.ts" +import type { Severity } from "./LogLevel.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import type * as Predicate from "./Predicate.ts" +import { hasProperty, isTagged } from "./Predicate.ts" +import * as PubSub from "./PubSub.ts" +import * as Pull from "./Pull.ts" +import * as Queue from "./Queue.ts" +import { TracerTimingEnabled } from "./References.ts" +import * as Result from "./Result.ts" +import * as Schedule from "./Schedule.ts" +import * as Scope from "./Scope.ts" +import * as Semaphore from "./Semaphore.ts" +import * as String from "./String.ts" +import * as Take from "./Take.ts" +import { ParentSpan, type SpanOptions } from "./Tracer.ts" +import type * as Types from "./Types.ts" +import type * as Unify from "./Unify.ts" + +/** + * String literal type used as the unique brand for `Channel` values. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/Channel" + +/** + * Runtime identifier stored on `Channel` values and used by `isChannel` to + * recognize them. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~effect/Channel" + +/** + * Checks whether a value is a `Channel`. + * + * **Example** (Checking for channels) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.succeed(42) + * console.log(Channel.isChannel(channel)) // true + * console.log(Channel.isChannel("not a channel")) // false + * ``` + * + * @category guards + * @since 3.5.4 + */ +export const isChannel = ( + u: unknown +): u is Channel => hasProperty(u, TypeId) + +/** + * A `Channel` is a nexus of I/O operations, which supports both reading and + * writing. A channel may read values of type `InElem` and write values of type + * `OutElem`. When the channel finishes, it yields a value of type `OutDone`. A + * channel may fail with a value of type `OutErr`. + * + * **Details** + * + * Channels are the foundation of Streams: both streams and sinks are built on + * channels. Most users shouldn't have to use channels directly, as streams and + * sinks are much more convenient and cover all common use cases. However, when + * adding new stream and sink operators, or doing something highly specialized, + * it may be useful to use channels directly. + * + * Channels compose in a variety of ways: + * + * - **Piping**: One channel can be piped to another channel, assuming the + * input type of the second is the same as the output type of the first. + * - **Sequencing**: The terminal value of one channel can be used to create + * another channel, and both the first channel and the function that makes + * the second channel can be composed into a channel. + * - **Concatenating**: The output of one channel can be used to create other + * channels, which are all concatenated together. The first channel and the + * function that makes the other channels can be composed into a channel. + * + * **Example** (Typing channels) + * + * ```ts + * import type { Channel } from "effect" + * + * // A channel that outputs numbers and requires no environment + * type NumberChannel = Channel.Channel + * + * // A channel that outputs strings, can fail with Error, completes with boolean + * type StringChannel = Channel.Channel + * + * // A channel with all type parameters specified + * type FullChannel = Channel.Channel< + * string, // OutElem - output elements + * Error, // OutErr - output errors + * number, // OutDone - completion value + * number, // InElem - input elements + * string, // InErr - input errors + * boolean, // InDone - input completion + * { db: string } // Env - required environment + * > + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Channel< + out OutElem, + out OutErr = never, + out OutDone = void, + in InElem = unknown, + in InErr = unknown, + in InDone = unknown, + out Env = never +> extends Variance, Pipeable { + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ChannelUnify + [Unify.ignoreSymbol]?: ChannelUnifyIgnore +} + +/** + * Type-level unification support for `Channel` values. + * + * **Details** + * + * This preserves all `Channel` type parameters when `Unify` normalizes unions + * or generic return types that include channels. Users normally do not need to + * reference this interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface ChannelUnify extends Effect.EffectUnify { + Channel?: () => A[Unify.typeSymbol] extends + | Channel + | infer _ ? Channel + : never +} + +/** + * Marker used by `Unify` while resolving `Channel` values. + * + * **Details** + * + * It prevents the inherited `Effect` unifier from being selected when the + * channel-specific unifier should preserve `Channel` input, output, and + * environment type parameters. Users normally do not need to reference this + * interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface ChannelUnifyIgnore { + Effect?: true +} + +type TagsWithReason = { + [T in Types.Tags]: Types.ReasonTags> extends never ? never : T +}[Types.Tags] + +/** + * Phantom variance marker for the type parameters of `Channel`. + * + * **Details** + * + * Output element, output error, output done, and environment types are + * covariant. Input element, input error, and input done types are + * contravariant. This is type-level machinery and is not used directly at + * runtime. + * + * @category models + * @since 2.0.0 + */ +export interface Variance< + out OutElem, + out OutErr, + out OutDone, + in InElem, + in InErr, + in InDone, + out Env +> { + readonly [TypeId]: VarianceStruct +} +/** + * Structural encoding used by `Variance` to record each `Channel` type + * parameter's variance. + * + * **Details** + * + * The `_OutElem`, `_OutErr`, `_OutDone`, and `_Env` fields are covariant; the + * `_InElem`, `_InErr`, and `_InDone` fields are contravariant. Users normally + * do not need to reference this interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface VarianceStruct< + out OutElem, + out OutErr, + out OutDone, + in InElem, + in InErr, + in InDone, + out Env +> { + _Env: Types.Covariant + _InErr: Types.Contravariant + _InElem: Types.Contravariant + _InDone: Types.Contravariant + _OutErr: Types.Covariant + _OutElem: Types.Covariant + _OutDone: Types.Covariant +} + +const ChannelProto = { + [TypeId]: { + _Env: identity_, + _InErr: identity_, + _InElem: identity_, + _OutErr: identity_, + _OutElem: identity_ + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +// ----------------------------------------------------------------------------- +// Constructors +// ----------------------------------------------------------------------------- + +/** + * Creates a `Channel` from a transformation function that operates on upstream pulls. + * + * **Example** (Creating channels from transforms) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * const channel = Channel.fromTransform((upstream, scope) => + * Effect.succeed(upstream) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromTransform = ( + transform: ( + upstream: Pull.Pull, + scope: Scope.Scope + ) => Effect.Effect, EX, Env> +): Channel< + OutElem, + Pull.ExcludeDone | EX, + OutDone, + InElem, + InErr, + InDone, + Env | EnvX +> => { + const self = Object.create(ChannelProto) + self.transform = (upstream: any, scope: Scope.Scope) => + Effect.catchCause(transform(upstream, scope), (cause) => Effect.succeed(Effect.failCause(cause))) + return self +} + +/** + * Transforms a Channel by applying a function to its Pull implementation. + * + * **Example** (Transforming pull behavior) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * // Transform a channel by modifying its pull behavior + * const originalChannel = Channel.fromIterable([1, 2, 3]) + * + * const transformedChannel = Channel.transformPull( + * originalChannel, + * (pull, scope) => + * Effect.succeed( + * Effect.map(pull, (value) => value * 2) + * ) + * ) + * // Outputs: 2, 4, 6 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const transformPull = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + Env2, + OutErrX, + EnvX +>( + self: Channel, + f: ( + pull: Pull.Pull, + scope: Scope.Scope + ) => Effect.Effect, OutErrX, EnvX> +): Channel< + OutElem2, + Pull.ExcludeDone | OutErrX, + OutDone2, + InElem, + InErr, + InDone, + Env | Env2 | EnvX +> => fromTransform((upstream, scope) => Effect.flatMap(toTransform(self)(upstream, scope), (pull) => f(pull, scope))) + +/** + * Creates a `Channel` from an `Effect` that produces a `Pull`. + * + * **Example** (Creating channels from pulls) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * const channel = Channel.fromPull( + * Effect.succeed(Effect.succeed(42)) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromPull = ( + effect: Effect.Effect, EX, Env> +): Channel | EX, OutDone, unknown, unknown, unknown, Env | EnvX> => + fromTransform((_, __) => effect) as any + +/** + * Creates a `Channel` from a transformation function that operates on upstream + * pulls, but also provides a forked scope that closes when the resulting + * Channel completes. + * + * **When to use** + * + * Use when building channels that require scoped resource lifecycle management, + * providing both the channel scope and a forked scope that automatically closes + * when the channel completes. + * + * @see {@link fromTransform} for a simpler transformation without a forked scope + * @category constructors + * @since 4.0.0 + */ +export const fromTransformBracket = ( + f: ( + upstream: Pull.Pull, + scope: Scope.Scope, + forkedScope: Scope.Scope + ) => Effect.Effect, EX, Env> +): Channel | EX, OutDone, InElem, InErr, InDone, Env | EnvX> => + fromTransform( + Effect.fnUntraced(function*(upstream, scope) { + const closableScope = Scope.forkUnsafe(scope) + const onCause = (cause: Cause.Cause>) => + Scope.close(closableScope, Pull.doneExitFromCause(cause)) + const pull = yield* Effect.onError( + f(upstream, scope, closableScope), + onCause + ) + return Effect.onError(pull, onCause) + }) + ) + +/** + * Converts a `Channel` back to its underlying transformation function. + * + * **Example** (Extracting channel transforms) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.succeed(42) + * const transform = Channel.toTransform(channel) + * // transform can now be used directly + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toTransform = ( + channel: Channel +): ( + upstream: Pull.Pull, + scope: Scope.Scope +) => Effect.Effect, never, Env> => (channel as any).transform + +/** + * The default chunk size used by channels for batching operations. + * + * **Example** (Reading the default chunk size) + * + * ```ts + * import { Channel } from "effect" + * + * console.log(Channel.DefaultChunkSize) // 4096 + * ``` + * + * @category constants + * @since 4.0.0 + */ +export const DefaultChunkSize: number = 4096 + +const asyncQueue = ( + scope: Scope.Scope, + f: (queue: Queue.Queue) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + } +) => + Queue.make({ + capacity: options?.bufferSize, + strategy: options?.strategy + }).pipe( + Effect.tap((queue) => Scope.addFinalizer(scope, Queue.shutdown(queue))), + Effect.tap((queue) => Effect.forkIn(Scope.provide(f(queue), scope), scope)) + ) + +/** + * Creates a `Channel` that interacts with a callback function using a queue. + * + * **Example** (Creating channels from callbacks) + * + * ```ts + * import { Channel, Effect, Queue } from "effect" + * + * const channel = Channel.callback((queue) => + * Effect.gen(function*() { + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * yield* Queue.offer(queue, 3) + * }) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const callback = ( + f: (queue: Queue.Queue) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + } +): Channel> => + fromTransform((_, scope) => Effect.map(asyncQueue(scope, f, options), Queue.take)) + +/** + * Creates a `Channel` that interacts with a callback function using a queue, emitting arrays. + * + * **Example** (Creating array channels from callbacks) + * + * ```ts + * import { Channel, Effect, Queue } from "effect" + * + * const channel = Channel.callbackArray(Effect.fn(function*(queue) { + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * })) + * // Emits arrays of numbers instead of individual numbers + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const callbackArray = ( + f: (queue: Queue.Queue) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + } +): Channel, E, void, unknown, unknown, unknown, Exclude> => + fromTransform((_, scope) => Effect.map(asyncQueue(scope, f, options), Queue.takeAll)) + +/** + * Creates a `Channel` that lazily evaluates to another channel. + * + * **Example** (Suspending channel creation) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.suspend(() => Channel.succeed(42)) + * // The inner channel is not created until the suspended channel is run + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const suspend = ( + evaluate: LazyArg> +): Channel => + fromTransform((upstream, scope) => Effect.suspend(() => toTransform(evaluate())(upstream, scope))) + +/** + * Acquires a resource, uses it to build a `Channel`, and guarantees that + * `release` runs with the channel's `Exit` when the channel completes, fails, + * or is interrupted. + * + * **Details** + * + * Acquisition is uninterruptible. If acquisition fails, `use` is not run and + * `release` is not registered. + * + * **Example** (Managing resources with acquire-use-release) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * const channel = Channel.acquireUseRelease( + * Effect.succeed("resource"), + * (resource) => Channel.succeed(resource.toUpperCase()), + * (resource, exit) => Effect.log(`Released: ${resource}`) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const acquireUseRelease = ( + acquire: Effect.Effect, + use: (a: A) => Channel, + release: (a: A, exit: Exit.Exit) => Effect.Effect +): Channel => + fromTransformBracket( + Effect.fnUntraced(function*(upstream, scope, forkedScope) { + let option = Option.none() + yield* Scope.addFinalizerExit(forkedScope, (exit) => + Option.isSome(option) + ? release(option.value, exit as any) + : Effect.void) + const value = yield* Effect.uninterruptible(acquire) + option = Option.some(value) + return yield* toTransform(use(value))(upstream, scope) + }) + ) + +/** + * Acquires a resource, emits the acquired value as a single channel element, + * and registers `release` in the channel scope. + * + * **Details** + * + * The release action runs when the channel scope closes and receives the scope + * exit. If acquisition fails, no element is emitted and `release` is not + * registered. + * + * **Example** (Managing resources with acquire-release) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * const channel = Channel.acquireRelease( + * Effect.succeed("resource"), + * (resource, exit) => Effect.log(`Released: ${resource}`) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const acquireRelease: { + ( + release: (z: Z, e: Exit.Exit) => Effect.Effect + ): (self: Effect.Effect) => Channel + ( + self: Effect.Effect, + release: (z: Z, e: Exit.Exit) => Effect.Effect + ): Channel +} = dual(2, ( + self: Effect.Effect, + release: (z: Z, e: Exit.Exit) => Effect.Effect +): Channel => + unwrap(Effect.map( + Effect.acquireRelease(self, release), + succeed + ))) + +/** + * Creates a `Channel` from an iterator. + * + * **Example** (Creating channels from iterators) + * + * ```ts + * import { Channel } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const channel = Channel.fromIterator(() => numbers[Symbol.iterator]()) + * // Emits: 1, 2, 3, 4, 5 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIterator = (iterator: LazyArg>): Channel => + fromPull( + Effect.sync(() => { + const iter = iterator() + return Effect.suspend(() => { + const state = iter.next() + return state.done ? Cause.done(state.value) : Effect.succeed(state.value) + }) + }) + ) + +/** + * Creates a `Channel` that emits all elements from an array. + * + * **Example** (Creating channels from arrays) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.fromArray([1, 2, 3, 4, 5]) + * // Emits: 1, 2, 3, 4, 5 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromArray = (array: ReadonlyArray): Channel => + fromPull(Effect.sync(() => { + let index = 0 + return Effect.suspend(() => index >= array.length ? Cause.done() : Effect.succeed(array[index++])) + })) + +/** + * Creates a `Channel` that emits all elements from a chunk. + * + * **Example** (Creating channels from chunks) + * + * ```ts + * import { Channel, Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const channel = Channel.fromChunk(chunk) + * // Emits: 1, 2, 3 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chunk.toReadonlyArray(chunk)) + +/** + * Creates a `Channel` from an iterator that emits arrays of elements. + * + * **Example** (Batching iterator output) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel from a simple iterator + * const numberIterator = (): Iterator => { + * let count = 0 + * return { + * next: () => { + * if (count < 3) { + * return { value: count++, done: false } + * } + * return { value: "finished", done: true } + * } + * } + * } + * + * const channel = Channel.fromIteratorArray(() => numberIterator(), 2) + * // This will emit arrays: [0, 1], [2], then complete with "finished" + * ``` + * + * **Example** (Batching generator output) + * + * ```ts + * import { Channel } from "effect" + * + * // Create channel from a generator function + * function* fibonacci(): Generator { + * let a = 0, b = 1 + * for (let i = 0; i < 5; i++) { + * yield a + * ;[a, b] = [b, a + b] + * } + * } + * + * const fibChannel = Channel.fromIteratorArray(() => fibonacci(), 3) + * // Emits: [0, 1, 1], [2, 3], then completes + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIteratorArray = ( + iterator: LazyArg>, + chunkSize = DefaultChunkSize +): Channel, never, L> => + fromPull( + Effect.sync(() => { + const iter = iterator() + let done = Option.none() + return Effect.suspend(() => { + if (done._tag === "Some") return Cause.done(done.value) + const buffer: Array = [] + while (buffer.length < chunkSize) { + const state = iter.next() + if (state.done) { + if (buffer.length === 0) { + return Cause.done(state.value) + } + done = Option.some(state.value) + break + } + buffer.push(state.value) + } + return Effect.succeed(buffer as any as Arr.NonEmptyReadonlyArray) + }) + }) + ) + +/** + * Creates a `Channel` that emits all elements from an iterable. + * + * **Example** (Creating channels from iterables) + * + * ```ts + * import { Channel } from "effect" + * + * const set = new Set([1, 2, 3]) + * const channel = Channel.fromIterable(set) + * // Emits: 1, 2, 3 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIterable = (iterable: Iterable): Channel => + fromIterator(() => iterable[Symbol.iterator]()) + +/** + * Creates a `Channel` that emits arrays of elements from an iterable. + * + * **Example** (Batching iterable output) + * + * ```ts + * import { Channel } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const channel = Channel.fromIterableArray(numbers) + * // Emits arrays like: [1, 2, 3, 4], [5] (based on chunk size) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIterableArray = ( + iterable: Iterable, + chunkSize = DefaultChunkSize +): Channel, never, L> => fromIteratorArray(() => iterable[Symbol.iterator](), chunkSize) + +/** + * Creates a `Channel` that emits a single value and then ends. + * + * **Example** (Creating channels that succeed) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.succeed(42) + * // Emits: 42 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const succeed = (value: A): Channel => fromEffect(Effect.succeed(value)) + +/** + * Creates a `Channel` that immediately ends with the specified value. + * + * **Example** (Ending with a value) + * + * ```ts + * import { Channel } from "effect" + * + * const channel = Channel.end("done") + * // Ends immediately with "done", emits nothing + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const end = (value: A): Channel => fromPull(Effect.succeed(Cause.done(value))) + +/** + * Creates a `Channel` that immediately ends with the lazily evaluated value. + * + * @category constructors + * @since 4.0.0 + */ +export const endSync = (evaluate: LazyArg): Channel => + fromPull(Effect.sync(() => Cause.done(evaluate()))) + +/** + * Creates a `Channel` that emits a single value computed by a lazy evaluation. + * + * **Example** (Computing values lazily) + * + * ```ts + * import { Channel } from "effect" + * + * let requests = 0 + * + * const channel = Channel.sync(() => { + * requests += 1 + * return `request-${requests}` + * }) + * // Emits "request-1" when the channel runs for the first time + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sync = (evaluate: LazyArg): Channel => fromEffect(Effect.sync(evaluate)) + +/** + * Represents a `Channel` that emits no elements. + * + * **Example** (Creating empty channels) + * + * ```ts + * import { Channel } from "effect" + * + * // Create an empty channel + * const emptyChannel = Channel.empty + * + * // Use empty channel in composition + * const combined = Channel.concatWith(emptyChannel, () => Channel.succeed(42)) + * // Will immediately provide the second channel's output + * + * // Empty channel can be used as a no-op in conditional logic + * const conditionalChannel = (shouldEmit: boolean) => + * shouldEmit ? Channel.succeed("data") : Channel.empty + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const empty: Channel = fromPull(Effect.succeed(Cause.done())) + +/** + * Represents a `Channel` that never completes. + * + * **Example** (Creating non-terminating channels) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel that never completes + * const neverChannel = Channel.never + * + * // Use in conditional logic + * const withFallback = Channel.concatWith( + * neverChannel, + * () => Channel.succeed("fallback") + * ) + * + * // Never channel is useful for testing or as a placeholder + * const conditionalChannel = (shouldComplete: boolean) => + * shouldComplete ? Channel.succeed("done") : Channel.never + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const never: Channel = fromPull(Effect.succeed(Effect.never)) + +/** + * Constructs a channel that fails immediately with the specified error. + * + * **Example** (Failing with an error) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel that fails with a string error + * const failedChannel = Channel.fail("Something went wrong") + * + * // Create a channel that fails with a custom error + * class CustomError extends Error { + * constructor(message: string) { + * super(message) + * this.name = "CustomError" + * } + * } + * const customErrorChannel = Channel.fail(new CustomError("Custom error")) + * + * // Use in error handling by piping to another channel + * const channelWithFallback = Channel.concatWith( + * failedChannel, + * () => Channel.succeed("fallback value") + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fail = (error: E): Channel => fromPull(Effect.succeed(Effect.fail(error))) + +/** + * Constructs a channel that fails immediately with the specified lazily + * evaluated error. + * + * **When to use** + * + * Use when the error value should be computed each time the channel runs instead + * of when the channel is constructed. + * + * **Example** (Failing with a lazy error) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel that fails with a lazily computed error + * const failedChannel = Channel.failSync(() => { + * console.log("Computing error...") + * return new Error("Computed at runtime") + * }) + * + * // The error computation is deferred until the channel runs + * let attempts = 0 + * const conditionalError = Channel.failSync(() => { + * attempts += 1 + * return `Error after attempt ${attempts}` + * }) + * + * // Use with expensive error construction + * const expensiveError = Channel.failSync(() => { + * const requestId = "request-123" + * return new Error(`Failed while processing ${requestId}`) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failSync = (evaluate: LazyArg): Channel => fromPull(Effect.failSync(evaluate)) + +/** + * Constructs a channel that fails immediately with the specified `Cause`. + * + * **When to use** + * + * Use when the channel failure must preserve a full `Cause`, such as defects, + * interruptions, or combined failures. + * + * **Example** (Failing with causes) + * + * ```ts + * import { Cause, Channel } from "effect" + * + * // Create a channel that fails with a simple cause + * const simpleCause = Cause.fail("Simple error") + * const failedChannel = Channel.failCause(simpleCause) + * + * // Create a channel with a die cause + * const dieCause = Cause.die(new Error("System error")) + * const dieFailure = Channel.failCause(dieCause) + * + * // Create a channel with a simple fail cause + * const failCause = Cause.fail("Simple error") + * const simpleFail = Channel.failCause(failCause) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCause = (cause: Cause.Cause): Channel => fromPull(Effect.failCause(cause)) + +/** + * Constructs a channel that fails immediately with the specified lazily + * evaluated `Cause`. + * + * **Example** (Failing with lazy causes) + * + * ```ts + * import { Cause, Channel } from "effect" + * + * // Create a channel that fails with a lazily computed cause + * let attempts = 0 + * const failedChannel = Channel.failCauseSync(() => { + * attempts += 1 + * return Cause.fail(`Runtime error after attempt ${attempts}`) + * }) + * + * // Create a channel with die cause computation + * const dieCauseChannel = Channel.failCauseSync(() => { + * const operation = "load-profile" + * return Cause.die(`Unexpected defect during ${operation}`) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCauseSync = ( + evaluate: LazyArg> +): Channel => fromPull(Effect.failCauseSync(evaluate)) + +/** + * Constructs a channel that fails immediately with the specified defect. + * + * **Example** (Dying with defects) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel that dies with a string defect + * const diedChannel = Channel.die("Unrecoverable error") + * + * // Create a channel that dies with an Error object + * const errorDefect = Channel.die(new Error("System failure")) + * + * // Die with any value as a defect + * const objectDefect = Channel.die({ + * code: "SYSTEM_FAILURE", + * details: "Critical system component failed" + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const die = (defect: unknown): Channel => failCause(Cause.die(defect)) + +/** + * Uses an effect to write a single value to the channel. + * + * **Example** (Creating channels from effects) + * + * ```ts + * import { Channel, Data, Effect } from "effect" + * + * class DatabaseError extends Data.TaggedError("DatabaseError")<{ + * readonly message: string + * }> {} + * + * // Create a channel from a successful effect + * const successChannel = Channel.fromEffect( + * Effect.succeed("Hello from effect!") + * ) + * + * // Create a channel from an effect that might fail + * const fetchUserChannel = Channel.fromEffect( + * Effect.tryPromise({ + * try: () => fetch("/api/user").then((res) => res.json()), + * catch: (error) => new DatabaseError({ message: String(error) }) + * }) + * ) + * + * // Channel from effect with async computation + * const asyncChannel = Channel.fromEffect( + * Effect.gen(function*() { + * yield* Effect.sleep("100 millis") + * return "Async result" + * }) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromEffect = ( + effect: Effect.Effect +): Channel, void, unknown, unknown, unknown, R> => + fromPull( + Effect.sync(() => { + let done = false + return Effect.suspend((): Pull.Pull => { + if (done) return Cause.done() + done = true + return effect + }) + }) + ) + +/** + * Creates a channel that evaluates an effect and uses its successful value as + * the channel's done value without emitting any output elements. + * + * **Details** + * + * If the effect fails, the channel fails with the effect's error. + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectDone = ( + effect: Effect.Effect +): Channel, A, unknown, unknown, unknown, R> => + fromPull(Effect.succeed(Effect.flatMap(effect, Cause.done))) + +/** + * Uses an effect and discards its result. + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectDrain = ( + effect: Effect.Effect +): Channel => fromPull(Effect.flatMap(effect, () => Cause.done())) as any + +/** + * Creates a channel from an effect that produces a `Take`. + * + * **Details** + * + * A successful `Take` emits a non-empty array of output elements. A failed + * `Take` fails the channel. A done `Take` completes the channel with its done + * value. + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectTake = ( + effect: Effect.Effect, E2, R> +): Channel, E | E2, Done, unknown, unknown, unknown, R> => + fromPull(Effect.succeed(Effect.flatMap(effect, Take.toPull))) + +/** + * Creates a channel from a queue. + * + * **Example** (Creating channels from queues) + * + * ```ts + * import { Channel, Data, Effect, Queue } from "effect" + * + * class QueueError extends Data.TaggedError("QueueError")<{ + * readonly reason: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a bounded queue + * const queue = yield* Queue.bounded(10) + * + * // Add some items to the queue + * yield* Queue.offer(queue, "item1") + * yield* Queue.offer(queue, "item2") + * yield* Queue.offer(queue, "item3") + * + * // Create a channel from the queue + * const channel = Channel.fromQueue(queue) + * + * // The channel will read items from the queue one by one + * return channel + * }) + * + * // Sliding queue example + * const slidingProgram = Effect.gen(function*() { + * const slidingQueue = yield* Queue.sliding(5) + * yield* Queue.offerAll(slidingQueue, [1, 2, 3, 4, 5, 6]) + * return Channel.fromQueue(slidingQueue) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromQueue = ( + queue: Queue.Dequeue +): Channel> => fromPull(Effect.succeed(Queue.take(queue))) + +/** + * Creates a channel from a queue that emits arrays of elements. + * + * **Example** (Creating batched channels from queues) + * + * ```ts + * import { Channel, Data, Effect, Queue } from "effect" + * + * class ProcessingError extends Data.TaggedError("ProcessingError")<{ + * readonly stage: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a queue for batch processing + * const queue = yield* Queue.bounded(100) + * + * // Fill queue with data + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + * + * // Create a channel that reads arrays from the queue + * const arrayChannel = Channel.fromQueueArray(queue) + * + * // This will emit non-empty arrays of elements instead of individual items + * // Useful for batch processing scenarios + * return arrayChannel + * }) + * + * // High-throughput processing example + * const batchProcessor = Effect.gen(function*() { + * const dataQueue = yield* Queue.dropping(1000) + * const batchChannel = Channel.fromQueueArray(dataQueue) + * + * // Process data in batches for better performance + * return Channel.map( + * batchChannel, + * (batch) => batch.map((item) => item.toUpperCase()) + * ) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromQueueArray = ( + queue: Queue.Dequeue +): Channel, Exclude> => fromPull(Effect.succeed(Queue.takeAll(queue))) + +/** + * Creates a channel that forwards upstream input elements, input errors, and + * the upstream done value unchanged. + * + * @category constructors + * @since 2.0.0 + */ +export const identity = (): Channel => + fromTransform((upstream, _scope) => Effect.succeed(upstream)) + +/** + * Creates a channel from a PubSub subscription. + * + * **Example** (Creating channels from subscriptions) + * + * ```ts + * import { Channel, Data, Effect, PubSub } from "effect" + * + * class SubscriptionError extends Data.TaggedError("SubscriptionError")<{ + * readonly reason: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a PubSub + * const pubsub = yield* PubSub.bounded(32) + * + * // Create a subscription + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish some messages + * yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.publish(pubsub, "World") + * yield* PubSub.publish(pubsub, "from") + * yield* PubSub.publish(pubsub, "PubSub") + * + * // Create a channel from the subscription + * const channel = Channel.fromSubscription(subscription) + * + * // The channel will receive all published messages + * return channel + * }) + * + * // Real-time notifications example + * const notificationChannel = Effect.gen(function*() { + * const eventBus = yield* PubSub.unbounded<{ type: string; payload: any }>() + * const userSubscription = yield* PubSub.subscribe(eventBus) + * + * return Channel.fromSubscription(userSubscription) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromSubscription = ( + subscription: PubSub.Subscription +): Channel => fromPull(Effect.succeed(Effect.onInterrupt(PubSub.take(subscription), () => Cause.done()))) + +/** + * Creates a channel from a PubSub subscription that outputs arrays of values. + * + * **Details** + * + * This constructor creates a channel that reads from a PubSub subscription and outputs + * arrays of values in chunks. It's useful when you want to process multiple values at once + * for better performance. + * + * **Example** (Batching subscription values) + * + * ```ts + * import { Channel, Data, Effect, PubSub } from "effect" + * + * class StreamError extends Data.TaggedError("StreamError")<{ + * readonly message: string + * }> {} + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(16) + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Create a channel that reads arrays of values + * const channel = Channel.fromSubscriptionArray(subscription) + * + * // Publish some values + * yield* PubSub.publish(pubsub, 1) + * yield* PubSub.publish(pubsub, 2) + * yield* PubSub.publish(pubsub, 3) + * yield* PubSub.publish(pubsub, 4) + * + * // The channel will output arrays like [1, 2, 3] and [4] + * return channel + * }) + * ``` + * + * **Example** (Processing subscription values in batches) + * + * ```ts + * import { Channel, Data, Effect, PubSub } from "effect" + * + * class BatchProcessingError extends Data.TaggedError("BatchProcessingError")<{ + * readonly reason: string + * }> {} + * + * const batchProcessor = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(32) + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Create a channel that processes items in batches + * const batchChannel = Channel.fromSubscriptionArray(subscription) + * + * // Transform to process each batch + * const processedChannel = Channel.map(batchChannel, (batch) => { + * console.log(`Processing batch of ${batch.length} items:`, batch) + * return batch.map((item) => item.toUpperCase()) + * }) + * + * return processedChannel + * }) + * ``` + * + * **Example** (Aggregating subscription metrics) + * + * ```ts + * import { Channel, Effect, PubSub } from "effect" + * + * const metricsAggregator = Effect.gen(function*() { + * const metricsPubSub = yield* PubSub.bounded< + * { timestamp: number; value: number } + * >(100) + * const subscription = yield* PubSub.subscribe(metricsPubSub) + * + * // Create a channel that collects metrics in chunks + * const metricsChannel = Channel.fromSubscriptionArray(subscription) + * + * // Transform to calculate aggregate statistics + * const aggregatedChannel = Channel.map(metricsChannel, (metrics) => { + * const values = metrics.map((m) => m.value) + * const sum = values.reduce((a, b) => a + b, 0) + * const avg = sum / values.length + * const min = Math.min(...values) + * const max = Math.max(...values) + * + * return { + * count: values.length, + * sum, + * average: avg, + * min, + * max, + * firstTimestamp: Math.min(...metrics.map((m) => m.timestamp)), + * lastTimestamp: Math.max(...metrics.map((m) => m.timestamp)) + * } + * }) + * + * return aggregatedChannel + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromSubscriptionArray = ( + subscription: PubSub.Subscription +): Channel> => + fromPull(Effect.succeed(Effect.onInterrupt(PubSub.takeAll(subscription), () => Cause.done()))) + +/** + * Creates a channel from a PubSub that outputs individual values. + * + * **Details** + * + * This constructor creates a channel that reads from a PubSub by automatically + * subscribing to it. The channel outputs individual values as they are published + * to the PubSub, making it ideal for real-time streaming scenarios. + * + * **Example** (Creating channels from PubSubs) + * + * ```ts + * import { Channel, Data, Effect, PubSub } from "effect" + * + * class StreamError extends Data.TaggedError("StreamError")<{ + * readonly message: string + * }> {} + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(16) + * + * // Create a channel that reads individual values + * const channel = Channel.fromPubSub(pubsub) + * + * // Publish some values + * yield* PubSub.publish(pubsub, 1) + * yield* PubSub.publish(pubsub, 2) + * yield* PubSub.publish(pubsub, 3) + * + * // The channel will output: 1, 2, 3 (individual values) + * return channel + * }) + * ``` + * + * **Example** (Streaming PubSub notifications) + * + * ```ts + * import { Channel, Effect, PubSub } from "effect" + * + * const notificationService = Effect.gen(function*() { + * const notificationPubSub = yield* PubSub.bounded(50) + * + * // Create a channel for real-time notifications + * const notificationChannel = Channel.fromPubSub(notificationPubSub) + * + * // Transform notifications to add timestamps + * const receivedAt = "2024-01-01T00:00:00.000Z" + * const timestampedChannel = Channel.map(notificationChannel, (message) => ({ + * message, + * receivedAt, + * id: `notification:${message}` + * })) + * + * return timestampedChannel + * }) + * ``` + * + * **Example** (Processing PubSub events) + * + * ```ts + * import { Channel, Effect, PubSub } from "effect" + * + * interface DomainEvent { + * readonly type: string + * readonly payload: unknown + * readonly timestamp: number + * } + * + * const eventProcessor = Effect.gen(function*() { + * const eventPubSub = yield* PubSub.bounded(100) + * + * // Create a channel for processing domain events + * const eventChannel = Channel.fromPubSub(eventPubSub) + * + * // Filter and transform events + * const processedChannel = Channel.map(eventChannel, (event) => { + * if (event.type === "user.created") { + * return { + * ...event, + * processed: true, + * processedAt: event.timestamp + 1 + * } + * } + * return event + * }) + * + * return processedChannel + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromPubSub = ( + pubsub: PubSub.PubSub +): Channel => unwrap(Effect.map(PubSub.subscribe(pubsub), fromSubscription)) + +/** + * Creates a channel from a PubSub that outputs arrays of values. + * + * **Details** + * + * This constructor creates a channel that reads from a PubSub by automatically + * subscribing to it and collecting values into arrays. The channel outputs + * arrays of values in chunks, making it ideal for batch processing scenarios. + * + * **Example** (Batching PubSub values) + * + * ```ts + * import { Channel, Data, Effect, PubSub } from "effect" + * + * class BatchError extends Data.TaggedError("BatchError")<{ + * readonly message: string + * }> {} + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(16) + * + * // Create a channel that reads arrays of values + * const channel = Channel.fromPubSubArray(pubsub) + * + * // Publish some values + * yield* PubSub.publish(pubsub, 1) + * yield* PubSub.publish(pubsub, 2) + * yield* PubSub.publish(pubsub, 3) + * yield* PubSub.publish(pubsub, 4) + * + * // The channel will output arrays like [1, 2, 3] and [4] + * return channel + * }) + * ``` + * + * **Example** (Processing PubSub orders in batches) + * + * ```ts + * import { Channel, Effect, PubSub } from "effect" + * + * interface Order { + * readonly id: string + * readonly customerId: string + * readonly items: ReadonlyArray + * readonly total: number + * readonly submittedAt: number + * } + * + * const orderBatchProcessor = Effect.gen(function*() { + * const orderPubSub = yield* PubSub.bounded(100) + * + * // Create a channel that processes orders in batches + * const orderChannel = Channel.fromPubSubArray(orderPubSub) + * + * // Transform to process each batch of orders + * const processedChannel = Channel.map(orderChannel, (orderBatch) => { + * const totalRevenue = orderBatch.reduce((sum, order) => sum + order.total, 0) + * const customerCount = new Set(orderBatch.map((order) => + * order.customerId + * )).size + * + * return { + * batchSize: orderBatch.length, + * totalRevenue, + * uniqueCustomers: customerCount, + * firstSubmittedAt: Math.min(...orderBatch.map((order) => order.submittedAt)), + * orders: orderBatch + * } + * }) + * + * return processedChannel + * }) + * ``` + * + * **Example** (Processing PubSub logs in batches) + * + * ```ts + * import { Channel, Effect, PubSub } from "effect" + * + * interface LogEntry { + * readonly timestamp: number + * readonly level: "info" | "warn" | "error" + * readonly message: string + * readonly source: string + * } + * + * const logAggregator = Effect.gen(function*() { + * const logPubSub = yield* PubSub.bounded(500) + * + * // Create a channel that collects logs in batches + * const logChannel = Channel.fromPubSubArray(logPubSub) + * + * // Transform to analyze log batches + * const analysisChannel = Channel.map(logChannel, (logBatch) => { + * const errorCount = logBatch.filter((log) => log.level === "error").length + * const warnCount = logBatch.filter((log) => log.level === "warn").length + * const infoCount = logBatch.filter((log) => log.level === "info").length + * + * const timeRange = { + * start: Math.min(...logBatch.map((log) => log.timestamp)), + * end: Math.max(...logBatch.map((log) => log.timestamp)) + * } + * + * return { + * batchId: `${timeRange.start}-${timeRange.end}`, + * totalEntries: logBatch.length, + * errorCount, + * warnCount, + * infoCount, + * timeRange, + * sources: [...new Set(logBatch.map((log) => log.source))] + * } + * }) + * + * return analysisChannel + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromPubSubArray = (pubsub: PubSub.PubSub): Channel> => + unwrap(Effect.map(PubSub.subscribe(pubsub), fromSubscriptionArray)) + +/** + * Subscribes to a `PubSub` of `Take` values and exposes them as a channel. + * + * **Details** + * + * Output `Take` values are emitted as non-empty arrays. Failed `Take` values + * fail the channel. Done `Take` values complete the channel. + * + * @category constructors + * @since 4.0.0 + */ +export const fromPubSubTake = ( + pubsub: PubSub.PubSub> +): Channel, E, Done> => + unwrap(Effect.map(PubSub.subscribe(pubsub), (sub) => fromEffectTake(PubSub.take(sub)))) + +/** + * Creates a Channel from a Schedule. + * + * @category constructors + * @since 4.0.0 + */ +export const fromSchedule = ( + schedule: Schedule.Schedule +): Channel => + fromPull(Effect.map(Schedule.toStepWithSleep(schedule), (step) => step(void 0))) + +/** + * Creates a channel that pulls values from an `AsyncIterable`. + * + * **Details** + * + * Each yielded value is emitted as an output element. The iterator's return + * value becomes the channel's done value. Thrown or rejected iterator errors + * are converted with `onError`. If the channel scope closes early and the + * iterator has a `return` method, that method is called. + * + * @category constructors + * @since 4.0.0 + */ +export const fromAsyncIterable = ( + iterable: AsyncIterable, + onError: (error: unknown) => E +): Channel => + fromTransform(Effect.fnUntraced(function*(_, scope) { + const iter = iterable[Symbol.asyncIterator]() + if (iter.return) { + yield* Scope.addFinalizer(scope, Effect.promise(() => iter.return!())) + } + return Effect.flatMap( + Effect.tryPromise({ + try: () => iter.next(), + catch: onError + }), + (result) => result.done ? Cause.done(result.value) : Effect.succeed(result.value) + ) + })) + +/** + * Creates a channel from an `AsyncIterable`, emitting each yielded value as a + * single-element non-empty array. + * + * **Details** + * + * The iterator's return value becomes the channel's done value. Thrown or + * rejected iterator errors are converted with `onError`. If the channel scope + * closes early and the iterator has a `return` method, that method is called. + * + * @category constructors + * @since 4.0.0 + */ +export const fromAsyncIterableArray = ( + iterable: AsyncIterable, + onError: (error: unknown) => E +): Channel, E, D> => map(fromAsyncIterable(iterable, onError), Arr.of) + +/** + * Maps the output of this channel using the specified function. + * + * **Example** (Mapping channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class TransformError extends Data.TaggedError("TransformError")<{ + * readonly reason: string + * }> {} + * + * // Basic mapping of channel values + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * const doubledChannel = Channel.map(numbersChannel, (n) => n * 2) + * // Outputs: 2, 4, 6, 8, 10 + * + * // Transform string data + * const wordsChannel = Channel.fromIterable(["hello", "world", "effect"]) + * const upperCaseChannel = Channel.map(wordsChannel, (word) => word.toUpperCase()) + * // Outputs: "HELLO", "WORLD", "EFFECT" + * + * // Complex object transformation + * type User = { id: number; name: string } + * type UserDisplay = { displayName: string; isActive: boolean } + * + * const usersChannel = Channel.fromIterable([ + * { id: 1, name: "Alice" }, + * { id: 2, name: "Bob" } + * ]) + * const displayChannel = Channel.map(usersChannel, (user): UserDisplay => ({ + * displayName: `User: ${user.name}`, + * isActive: true + * })) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const map: { + ( + f: (o: OutElem, i: number) => OutElem2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutElem, i: number) => OutElem2 + ): Channel +} = dual( + 2, + ( + self: Channel, + f: (o: OutElem, i: number) => OutElem2 + ): Channel => + transformPull(self, (pull) => + Effect.sync(() => { + let i = 0 + return Effect.map(pull, (o) => f(o, i++)) + })) +) + +/** + * Maps the done value of this channel using the specified function. + * + * @category sequencing + * @since 4.0.0 + */ +export const mapDone: { + ( + f: (o: OutDone) => OutDone2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutDone) => OutDone2 + ): Channel +} = dual( + 2, + ( + self: Channel, + f: (o: OutDone) => OutDone2 + ): Channel => mapDoneEffect(self, (o) => Effect.succeed(f(o))) +) + +/** + * Maps the done value of this channel using the specified effectful function. + * + * **When to use** + * + * Use when the terminal done value transformation needs services or can fail, + * while emitted elements should pass through unchanged. + * + * @category sequencing + * @since 4.0.0 + */ +export const mapDoneEffect: { + ( + f: (o: OutDone) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (o: OutDone) => Effect.Effect + ): Channel +} = dual( + 2, + ( + self: Channel, + f: (o: OutDone) => Effect.Effect + ): Channel => + transformPull(self, (pull) => + Effect.succeed(Pull.catchDone( + pull, + (done) => Effect.flatMap(f(done as OutDone), Cause.done) + ))) +) + +const concurrencyIsSequential = ( + concurrency: number | "unbounded" | undefined +) => concurrency === undefined || (concurrency !== "unbounded" && concurrency <= 1) + +/** + * Maps each output element with an effectful function, preserving the source + * channel's done value. + * + * **When to use** + * + * Use when transforming each channel output needs an Effect, service + * dependency, failure channel, or configured concurrency. + * + * **Details** + * + * The mapping function receives the output element and its zero-based index. + * By default elements are mapped sequentially. Use `options.concurrency` to + * map multiple elements concurrently, and `options.unordered` to allow + * concurrently mapped outputs to be emitted as soon as they complete. + * + * **Example** (Mapping channel output with effects) + * + * ```ts + * import { Channel, Data, Effect } from "effect" + * + * class NetworkError extends Data.TaggedError("NetworkError")<{ + * readonly url: string + * }> {} + * + * // Transform values using effectful operations + * const urlsChannel = Channel.fromIterable([ + * "/api/users/1", + * "/api/users/2", + * "/api/users/3" + * ]) + * + * const fetchDataChannel = Channel.mapEffect( + * urlsChannel, + * (url) => + * Effect.tryPromise({ + * try: () => fetch(url).then((res) => res.json()), + * catch: () => new NetworkError({ url }) + * }) + * ) + * + * // Concurrent processing with options + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * const processedChannel = Channel.mapEffect( + * numbersChannel, + * (n) => + * Effect.gen(function*() { + * yield* Effect.sleep("100 millis") // Simulate async work + * return n * n + * }), + * { concurrency: 3, unordered: true } + * ) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const mapEffect: { + ( + f: (d: OutElem, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (d: OutElem, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ): Channel +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + f: (d: OutElem, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } + ): Channel => + concurrencyIsSequential(options?.concurrency) + ? mapEffectSequential(self, f) + : mapEffectConcurrent(self, f, options as any) +) + +const mapEffectSequential = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem2, + EX, + RX +>( + self: Channel, + f: (o: OutElem, i: number) => Effect.Effect +): Channel => + fromTransform((upstream, scope) => { + let i = 0 + return Effect.map(toTransform(self)(upstream, scope), Effect.flatMap((o) => f(o, i++))) + }) + +const mapEffectConcurrent = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem2, + EX, + RX +>( + self: Channel, + f: (o: OutElem, i: number) => Effect.Effect, + options: { + readonly concurrency: number | "unbounded" + readonly unordered?: boolean | undefined + } +): Channel => + fromTransformBracket( + Effect.fnUntraced(function*(upstream, scope, forkedScope) { + let i = 0 + const pull = yield* toTransform(self)(upstream, scope) + const concurrencyN = options.concurrency === "unbounded" + ? Number.MAX_SAFE_INTEGER + : options.concurrency + const queue = yield* Queue.bounded>(0) + yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) + + const runFork = Effect.runForkWith(yield* Effect.context()) + const trackFiber = Fiber.runIn(forkedScope) + + if (options.unordered) { + const semaphore = Semaphore.makeUnsafe(concurrencyN) + const release = constant(semaphore.release(1)) + const handle = Effect.matchCauseEffect({ + onFailure: (cause: Cause.Cause) => Effect.flatMap(Queue.failCause(queue, cause), release), + onSuccess: (value: OutElem2) => Effect.flatMap(Queue.offer(queue, value), release) + }) + yield* semaphore.take(1).pipe( + Effect.flatMap(() => pull), + Effect.flatMap((value) => { + trackFiber(runFork(handle(f(value, i++)))) + return Effect.void + }), + Effect.forever({ disableYield: true }), + Effect.catchCause((cause) => + semaphore.withPermits(concurrencyN - 1)( + Queue.failCause(queue, cause) + ) + ), + Effect.forkIn(forkedScope) + ) + } else { + // capacity is n - 2 because + // - 1 for the offer *after* starting a fiber + // - 1 for the current processing fiber + const effects = yield* Queue.bounded< + Effect.Effect>, + OutErr | EX | Cause.Done + >(concurrencyN - 2) + yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) + + yield* Queue.take(effects).pipe( + Effect.flatten, + Effect.flatMap((value) => Queue.offer(queue, value)), + Effect.forever({ disableYield: true }), + Effect.catchCause((cause) => Queue.failCause(queue, cause)), + Effect.forkIn(forkedScope) + ) + + let errorCause: Cause.Cause | undefined + const onExit = (exit: Exit.Exit) => { + if (exit._tag === "Success") return + errorCause = exit.cause + Queue.failCauseUnsafe(queue, exit.cause) + } + yield* pull.pipe( + Effect.flatMap((value) => { + if (errorCause) return Effect.failCause(errorCause) + const fiber = runFork(f(value, i++)) + trackFiber(fiber) + fiber.addObserver(onExit) + return Queue.offer(effects, Fiber.join(fiber)) + }), + Effect.forever({ disableYield: true }), + Effect.catchCause((cause) => + Queue.offer(effects, Exit.failCause(cause)).pipe( + Effect.andThen(Queue.failCause(effects, cause)) + ) + ), + Effect.forkIn(forkedScope) + ) + } + + return Queue.take(queue) + }) + ) + +/** + * Returns a new channel which is the same as this one but applies the given + * function to the input channel’s input elements. + * + * @category sequencing + * @since 2.0.0 + */ +export const mapInput: { + ( + f: (i: InElem2) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (i: InElem2) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + f: (i: InElem2) => Effect.Effect +): Channel => + fromTransform((upstream, scope) => + toTransform(self)( + Effect.flatMap(upstream, (el) => f(el)) as Pull.Pull, + scope + ) + )) + +/** + * Returns a new channel which is the same as this one but applies the given + * function to the input errors. + * + * @category sequencing + * @since 2.0.0 + */ +export const mapInputError: { + ( + f: (i: InErr2) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (i: InErr2) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + f: (i: InErr2) => Effect.Effect +): Channel => + fromTransform((upstream, scope) => + toTransform(self)( + Effect.catch(upstream, (err): Pull.Pull => { + if (Cause.isDone(err)) return Effect.fail(err) + return Effect.flatMap(f(err), Effect.fail) as Pull.Pull + }), + scope + ) + )) + +/** + * Applies a side effect function to each output element of the channel, + * returning a new channel that emits the same elements. + * + * **Details** + * + * The `tap` function allows you to perform side effects (like logging or + * debugging) on each element emitted by a channel without modifying the + * elements themselves. + * + * **Example** (Tapping channel output) + * + * ```ts + * import { Channel, Console, Data } from "effect" + * + * class LogError extends Data.TaggedError("LogError")<{ + * readonly message: string + * }> {} + * + * // Create a channel that outputs numbers + * const numberChannel = Channel.fromIterable([1, 2, 3]) + * + * // Tap into each output element to perform side effects + * const tappedChannel = Channel.tap( + * numberChannel, + * (n) => Console.log(`Processing number: ${n}`) + * ) + * + * // The channel still outputs the same elements but logs each one + * // Outputs: 1, 2, 3 (while logging each) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const tap: { + ( + f: (d: Types.NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (d: Types.NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } + ): Channel +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + f: (d: Types.NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } + ): Channel => + mapEffect(self, (a) => Effect.as(f(a), a), options) +) + +/** + * Maps each output element to a channel and flattens the child channel + * outputs. + * + * **Details** + * + * The source channel's done value is preserved. Child channel done values are + * used only for child-channel completion. By default child channels are run + * sequentially. Use `options.concurrency` and `options.bufferSize` to run child + * channels concurrently. + * + * **Example** (Flat mapping channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class ProcessError extends Data.TaggedError("ProcessError")<{ + * readonly cause: string + * }> {} + * + * // Create a channel that outputs numbers + * const numberChannel = Channel.fromIterable([1, 2, 3]) + * + * // FlatMap each number to create new channels + * const flatMappedChannel = Channel.flatMap( + * numberChannel, + * (n) => + * Channel.fromIterable(Array.from({ length: n }, (_, i) => `item-${n}-${i}`)) + * ) + * + * // Flattens nested channels into a single stream + * // Outputs: "item-1-0", "item-2-0", "item-2-1", "item-3-0", "item-3-1", "item-3-2" + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): ( + self: Channel + ) => Channel< + OutElem1, + OutErr1 | OutErr, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual( + (args) => isChannel(args[0]), + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > => + concurrencyIsSequential(options?.concurrency) + ? flatMapSequential(self, f) + : flatMapConcurrent(self, f, options as any) +) + +const flatMapSequential = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (d: OutElem) => Channel +): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => + fromTransform((upstream, scope) => + Effect.map(toTransform(self)(upstream, scope), (pull) => { + let childPull: Effect.Effect | undefined + let childScope: Scope.Closeable | undefined + const makePull: Pull.Pull< + OutElem1, + OutErr | OutErr1, + OutDone, + Env1 + > = Effect.flatMap(pull, (value) => { + childScope ??= Scope.forkUnsafe(scope) + return Effect.flatMapEager(toTransform(f(value))(upstream, childScope), (pull) => { + childPull = catchHalt(pull) as any + return childPull! + }) + }) + const catchHalt = Pull.catchDone((_) => { + childPull = undefined + // we can reuse the scope if the only finalizer is the "fork" one + if (childScope!.state._tag === "Open" && childScope!.state.finalizers.size === 1) { + return makePull + } + const close = Scope.close(childScope!, Exit.void) + childScope = undefined + return Effect.flatMap(close, () => makePull) + }) + return Effect.suspend(() => childPull ?? makePull) + }) + ) + +const flatMapConcurrent = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (d: OutElem) => Channel, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } +): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => self.pipe(map(f), mergeAll(options)) + +/** + * Concatenates this channel with another channel created from the terminal value + * of this channel. The new channel is created using the provided function. + * + * **Example** (Concatenating with completion values) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class ConcatError extends Data.TaggedError("ConcatError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel that outputs numbers and terminates with sum + * const numberChannel = Channel.fromIterable([1, 2, 3]).pipe( + * Channel.concatWith((sum: void) => Channel.succeed(`Completed processing`)) + * ) + * + * // Concatenates additional channel based on completion value + * // Outputs: 1, 2, 3, then "Completed processing" + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const concatWith: { + ( + f: (leftover: Types.NoInfer) => Channel + ): ( + self: Channel + ) => Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (leftover: Types.NoInfer) => Channel + ): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (leftover: Types.NoInfer) => Channel +): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env +> => + fromTransform((upstream, scope) => + Effect.sync(() => { + let currentPull: Pull.Pull | undefined + const forkedScope = Scope.forkUnsafe(scope) + const makePull = Effect.flatMap(toTransform(self)(upstream, forkedScope), (pull) => { + currentPull = Pull.catchDone(pull, (leftover) => { + return Scope.close(forkedScope, Exit.void).pipe( + Effect.flatMap(() => toTransform(f(leftover as OutDone))(upstream, scope)), + Effect.flatMap((pull) => { + currentPull = pull + return pull + }) + ) + }) + return currentPull + }) + return Effect.suspend(() => currentPull ?? makePull) + }) + )) + +/** + * Concatenates this channel with another channel, so that the second channel + * starts emitting values after the first channel has completed. + * + * **Example** (Concatenating channels) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class ConcatError extends Data.TaggedError("ConcatError")<{ + * readonly reason: string + * }> {} + * + * // Create two channels + * const firstChannel = Channel.fromIterable([1, 2, 3]) + * const secondChannel = Channel.fromIterable(["a", "b", "c"]) + * + * // Concatenate them + * const concatenatedChannel = Channel.concat(firstChannel, secondChannel) + * + * // Outputs: 1, 2, 3, "a", "b", "c" + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const concat: { + ( + that: Channel + ): ( + self: Channel + ) => Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + that: Channel + ): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + that: Channel +): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env +> => concatWith(self, (_) => that)) + +/** + * Combines two channels with a stateful pull function. + * + * **When to use** + * + * Use to coordinate pulling from two channels when each output element depends + * on both sides and local state. + * + * **Details** + * + * The combining function receives the current state and pull functions for the + * left and right channels. It returns the next output element together with the + * next state. + * + * @category sequencing + * @since 4.0.0 + */ +export const combine: { + ( + that: Channel, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect + ): (self: Channel) => Channel< + A, + Pull.ExcludeDone, + Cause.Done.Extract, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env | Env2 | R + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + InElem2, + InErr2, + InDone2, + Env2, + S, + A, + E, + R + >( + self: Channel, + that: Channel, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect + ): Channel< + A, + Pull.ExcludeDone, + Cause.Done.Extract, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env | Env2 | R + > +} = dual(4, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem2, + OutErr2, + OutDone2, + InElem2, + InErr2, + InDone2, + Env2, + S, + A, + E, + R +>( + self: Channel, + that: Channel, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect +): Channel< + A, + Pull.ExcludeDone, + Cause.Done.Extract, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env | Env2 | R +> => + fromTransform(Effect.fnUntraced(function*(upstream, scope) { + const leftPull = yield* toTransform(self)(upstream, scope) + const rightPull = yield* toTransform(that)(upstream, scope) + let state = s() + return Effect.suspend(() => { + const combinedPull = f(state, leftPull, rightPull) + return Effect.map(combinedPull, ([a, s1]) => { + state = s1 + return a + }) + }) + }))) + +/** + * Runs a fallback channel if this channel completes without emitting any + * output elements. + * + * **Details** + * + * If the source emits at least one element, the source is used unchanged. If + * the source completes before emitting an element, the fallback function + * receives the source done value and returns the replacement channel. + * + * @category sequencing + * @since 4.0.0 + */ +export const orElseIfEmpty: { + ( + f: (leftover: Types.NoInfer) => Channel + ): ( + self: Channel + ) => Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (leftover: Types.NoInfer) => Channel + ): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (leftover: Types.NoInfer) => Channel +): Channel< + OutElem | OutElem1, + OutErr1 | OutErr, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env +> => + fromTransform((upstream, scope) => + Effect.sync(() => { + let currentPull: Pull.Pull | undefined + const forkedScope = Scope.forkUnsafe(scope) + const makePull = Effect.flatMap(toTransform(self)(upstream, forkedScope), (pull) => { + const next = pull.pipe( + Effect.tap(() => { + currentPull = pull + return Effect.void + }), + Pull.catchDone((leftover) => + Scope.close(forkedScope, Exit.succeed(leftover)).pipe( + Effect.andThen(toTransform(f(leftover as OutDone))(upstream, scope)), + Effect.flatMap((pull) => { + currentPull = pull + return pull + }) + ) + ) + ) + currentPull = next + return next + }) + return Effect.suspend(() => currentPull ?? makePull) + }) + )) + +/** + * Flattens a channel of channels. + * + * **Example** (Flattening nested channels) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class FlattenError extends Data.TaggedError("FlattenError")<{ + * readonly cause: string + * }> {} + * + * // Create a channel that outputs channels + * const nestedChannels = Channel.fromIterable([ + * Channel.fromIterable([1, 2]), + * Channel.fromIterable([3, 4]), + * Channel.fromIterable([5, 6]) + * ]) + * + * // Flatten the nested channels + * const flattenedChannel = Channel.flatten(nestedChannels) + * + * // Outputs: 1, 2, 3, 4, 5, 6 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const flatten = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + channels: Channel< + Channel, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + > +): Channel => + flatMap(channels, identity_) + +/** + * Flattens a channel that outputs arrays into a channel that outputs individual elements. + * + * **Example** (Flattening arrays of channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class FlattenError extends Data.TaggedError("FlattenError")<{ + * readonly message: string + * }> {} + * + * // Create a channel that outputs arrays + * const arrayChannel = Channel.fromIterable([ + * [1, 2, 3], + * [4, 5], + * [6, 7, 8, 9] + * ]) + * + * // Flatten the arrays into individual elements + * const flattenedChannel = Channel.flattenArray(arrayChannel) + * + * // Outputs: 1, 2, 3, 4, 5, 6, 7, 8, 9 + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export const flattenArray = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env +>( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> +): Channel => + transformPull(self, (pull) => { + let array: ReadonlyArray | undefined + let index = 0 + const pump = Effect.suspend(function loop(): Pull.Pull { + if (array === undefined) { + return Effect.flatMap(pull, (array_) => { + switch (array_.length) { + case 0: + return loop() + case 1: + return Effect.succeed(array_[0]) + default: { + array = array_ + return Effect.succeed(array_[index++]) + } + } + }) + } + const next = array[index++] + if (index >= array.length) { + array = undefined + index = 0 + } + return Effect.succeed(next) + }) + return Effect.succeed(pump) + }) + +/** + * Flattens a channel that emits `Take` values into a channel that emits the + * `Take` outputs directly. + * + * **Details** + * + * Output `Take` values are emitted as non-empty arrays. Failed `Take` values + * fail the returned channel. Done `Take` values complete the returned channel. + * + * @category transforming + * @since 4.0.0 + */ +export const flattenTake = < + OutElem, + OutErr, + OutDone, + OutErr2, + OutDone2, + InElem, + InErr, + InDone, + Env +>( + self: Channel, OutErr2, OutDone2, InElem, InErr, InDone, Env> +): Channel, OutErr | OutErr2, OutDone, InElem, InErr, InDone, Env> => + mapEffectSequential(self, Take.toPull) as any + +/** + * Creates a new channel that consumes all output from the source channel + * but emits nothing, preserving only the completion value. + * + * **Example** (Draining channel output) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel that outputs values + * const sourceChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Drain all output, keeping only the completion + * const drainedChannel = Channel.drain(sourceChannel) + * + * // The channel completes but emits no values + * // Useful for consuming side effects without collecting output + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const drain = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env +>( + self: Channel< + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + > +): Channel => + transformPull(self, (pull) => Effect.succeed(Effect.forever(pull, { disableYield: true }))) + +/** + * Repeats this channel according to the provided schedule. + * + * @category repetition + * @since 4.0.0 + */ +export const repeat: { + ( + schedule: + | Schedule.Schedule, SE, SR> + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) + ): ( + self: Channel + ) => Channel + ( + self: Channel, + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) + ): Channel +} = dual(2, ( + self: Channel, + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) +): Channel => + Schedule.toStepWithMetadata(typeof schedule === "function" ? schedule(identity_) : schedule).pipe( + Effect.map((step) => { + let meta = Schedule.CurrentMetadata.defaultValue() + const loop: Channel< + OutElem, + OutErr | SE, + OutDone, + InElem, + InErr, + InDone, + Env | SR + > = concatWith( + provideServiceEffect(self, Schedule.CurrentMetadata, Effect.sync(() => meta)), + (done) => + step(done).pipe( + Effect.map((meta_) => { + meta = meta_ + return loop + }), + Pull.catchDone(() => Effect.succeed(end(done))), + unwrap + ) + ) + return loop + }), + unwrap + )) + +/** + * Repeats this channel forever. + * + * @category repetition + * @since 4.0.0 + */ +export const forever = ( + self: Channel +): Channel => concatWith(self, () => forever(self)) + +/** + * Runs a schedule step for each output element while preserving the emitted + * elements. + * + * **Details** + * + * The schedule receives each output element as input. Schedule delays are + * applied between emitted elements. If the schedule fails, the returned channel + * fails. If the schedule finishes, the returned channel completes with the + * schedule output. + * + * @category sequencing + * @since 4.0.0 + */ +export const schedule: { + ( + schedule: Schedule.Schedule, SE, SR> + ): ( + self: Channel + ) => Channel + ( + self: Channel, + schedule: Schedule.Schedule + ): Channel +} = dual(2, ( + self: Channel, + schedule: Schedule.Schedule +): Channel => + transformPull( + self, + (pull, _scope) => + Effect.map( + Schedule.toStepWithSleep(schedule), + (step) => { + const pullWithStep: Pull.Pull< + OutElem, + OutErr | SE, + OutDone | SO, + SR + > = Effect.tap(pull, step) + return pullWithStep + } + ) + )) + +/** + * Filters the output elements of a channel using a predicate function. + * Elements that don't match the predicate are discarded. + * + * **Example** (Filtering channel output) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel with mixed numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5, 6, 7, 8]) + * + * // Filter to keep only even numbers + * const evenChannel = Channel.filter(numbersChannel, (n) => n % 2 === 0) + * // Outputs: 2, 4, 6, 8 + * + * // Filter with type refinement + * const mixedChannel = Channel.fromIterable([1, "hello", 2, "world", 3]) + * const numbersOnlyChannel = Channel.filter( + * mixedChannel, + * (value): value is number => typeof value === "number" + * ) + * // Outputs: 1, 2, 3 (all typed as numbers) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const filter: { + ( + refinement: Predicate.Refinement + ): ( + self: Channel + ) => Channel + ( + predicate: Predicate.Predicate + ): ( + self: Channel + ) => Channel + ( + self: Channel, + refinement: Predicate.Refinement + ): Channel + ( + self: Channel, + predicate: Predicate.Predicate + ): Channel +} = dual(2, ( + self: Channel, + predicate: Predicate.Predicate +): Channel => + fromTransform((upstream, scope) => + Effect.map( + toTransform(self)(upstream, scope), + (pull) => + Effect.flatMap(pull, function loop(elem): Pull.Pull { + return predicate(elem) + ? Effect.succeed(elem) + : Effect.flatMap(pull, loop) + }) + ) + )) + +/** + * Filters and maps output elements using a `Filter`. + * + * **When to use** + * + * Use to keep only channel output elements accepted by a `Filter` and emit + * each filter success value. + * + * **Details** + * + * Successful filter results are emitted as mapped values. Failed filter + * results are discarded. The source channel's errors and done value are + * preserved. + * + * @see {@link filter} for keeping original output elements with a predicate + * @see {@link filterMapEffect} for using an effectful `Filter` + * @see {@link filterMapArray} for filtering arrays of output elements + * + * @category filtering + * @since 4.0.0 + */ +export const filterMap: { + ( + filter: Filter.Filter + ): ( + self: Channel + ) => Channel + ( + self: Channel, + filter: Filter.Filter + ): Channel +} = dual(2, ( + self: Channel, + filter: Filter.Filter +): Channel => + fromTransform((upstream, scope) => + Effect.map( + toTransform(self)(upstream, scope), + (pull) => + Effect.flatMap(pull, function loop(elem): Pull.Pull { + const result = filter(elem) + return Result.isFailure(result) + ? Effect.flatMap(pull, loop) + : Effect.succeed(result.success) + }) + ) + )) + +/** + * Filters output elements with an effectful predicate. + * + * **When to use** + * + * Use when the keep/discard decision depends on an Effect or service and + * predicate failures should fail the returned channel. + * + * **Details** + * + * Elements for which the predicate succeeds with `true` are emitted. Elements + * for which the predicate succeeds with `false` are discarded. Predicate + * failures fail the returned channel. + * + * @category filtering + * @since 4.0.0 + */ +export const filterEffect: { + ( + predicate: (a: OutElem) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + predicate: (a: OutElem) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + predicate: (a: OutElem) => Effect.Effect +): Channel => + fromTransform((upstream, scope) => + Effect.map( + toTransform(self)(upstream, scope), + (pull) => + Effect.flatMap(pull, function loop(elem): Pull.Pull { + return Effect.flatMap( + predicate(elem), + (passes) => + passes + ? Effect.succeed(elem) + : Effect.flatMap(pull, loop) + ) + }) + ) + )) + +/** + * Filters and maps output elements using an effectful `Filter`. + * + * **When to use** + * + * Use to apply effectful logic that can discard channel output elements and + * emit transformed values for the elements that pass. + * + * **Details** + * + * Successful filter results are emitted as mapped values. Failed filter + * results are discarded. Failures from the effectful filter fail the returned + * channel. + * + * @see {@link filterMap} for using a synchronous `Filter` + * @see {@link filterEffect} for effectfully keeping original output elements + * @see {@link mapEffect} for effectfully transforming every output element + * @see {@link filterMapArrayEffect} for effectful filtering of array outputs + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapEffect: { + ( + filter: Filter.FilterEffect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + filter: Filter.FilterEffect + ): Channel +} = dual(2, ( + self: Channel, + filter: Filter.FilterEffect +): Channel => + fromTransform((upstream, scope) => + Effect.map( + toTransform(self)(upstream, scope), + (pull) => + Effect.flatMap(pull, function loop(elem): Pull.Pull { + return Effect.flatMap( + filter(elem), + (result) => + Result.isFailure(result) + ? Effect.flatMap(pull, loop) + : Effect.succeed(result.success) + ) + }) + ) + )) + +/** + * Filters arrays of elements emitted by a channel, applying the filter + * to each element within the arrays and only emitting non-empty filtered arrays. + * + * **Example** (Filtering array output) + * + * ```ts + * import { Array, Channel } from "effect" + * + * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty + * + * // Create a channel that outputs arrays of mixed data + * const arrayChannel = Channel.fromIterable([ + * Array.make(1, 2, 3, 4, 5), + * Array.make(6, 7, 8, 9, 10), + * Array.make(11, 12, 13, 14, 15) + * ]).pipe(Channel.filter(nonEmptyArrayPredicate)) + * + * // Filter arrays to keep only even numbers + * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0) + * // Outputs: [2, 4], [6, 8, 10], [12, 14] + * // Note: Only non-empty filtered arrays are emitted + * + * // Arrays that would become empty after filtering are discarded entirely + * const oddChannel = Channel.fromIterable([ + * Array.make(1, 3, 5), + * Array.make(2, 4), + * Array.make(7, 9) + * ]).pipe(Channel.filter(nonEmptyArrayPredicate)) + * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0) + * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const filterArray: { + ( + refinement: Predicate.Refinement + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ( + predicate: Predicate.Predicate> + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + refinement: Predicate.Refinement + ): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + predicate: Predicate.Predicate> + ): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> +} = dual(2, ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + predicate: Predicate.Predicate> +): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> => + transformPull(self, (pull) => + Effect.succeed(Effect.flatMap( + pull, + function loop(arr): Pull.Pull, OutErr, OutDone> { + const passes: Array = [] + for (let i = 0; i < arr.length; i++) { + if (predicate(arr[i] as Types.NoInfer)) { + passes.push(arr[i]) + } + } + return Arr.isReadonlyArrayNonEmpty(passes) + ? Effect.succeed(passes) + : Effect.flatMap(pull, loop) + } + )))) + +/** + * Filters and maps each element inside emitted non-empty arrays using a + * `Filter`. + * + * **Details** + * + * Successful filter results are kept as mapped values. Failed filter results + * are removed from the array. Arrays that become empty are discarded. + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapArray: { + ( + filter: Filter.Filter, B, X> + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + filter: Filter.Filter + ): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> +} = dual(2, ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + filter: Filter.Filter +): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> => + transformPull(self, (pull) => + Effect.succeed(Effect.flatMap( + pull, + function loop(arr): Pull.Pull, OutErr, OutDone> { + const passes: Array = [] + for (let i = 0; i < arr.length; i++) { + const result = filter(arr[i]) + if (Result.isSuccess(result)) { + passes.push(result.success) + } + } + return Arr.isReadonlyArrayNonEmpty(passes) + ? Effect.succeed(passes) + : Effect.flatMap(pull, loop) + } + )))) + +/** + * Filters each element inside emitted non-empty arrays with an effectful + * predicate. + * + * **When to use** + * + * Use when filtering array-valued channel outputs requires Effects or services, + * and arrays that become empty should be skipped. + * + * **Details** + * + * The predicate receives the element and its index within the array. Elements + * for which the predicate succeeds with `true` are kept. Arrays that become + * empty are discarded. Predicate failures fail the returned channel. + * + * @category filtering + * @since 4.0.0 + */ +export const filterArrayEffect: { + ( + predicate: (a: Types.NoInfer, index: number) => Effect.Effect + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr | E, OutDone, InElem, InErr, InDone, Env | R> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + predicate: (a: Types.NoInfer, index: number) => Effect.Effect + ): Channel, OutErr | E, OutDone, InElem, InErr, InDone, Env | R> +} = dual(2, ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + predicate: (a: Types.NoInfer, index: number) => Effect.Effect +): Channel, OutErr | E, OutDone, InElem, InErr, InDone, Env | R> => + transformPull(self, (pull) => { + const f = Effect.flatMap(pull, (arr) => Effect.filter(arr, predicate)) + return Effect.succeed(Effect.flatMap( + f, + function loop(arr): Pull.Pull, OutErr | E, OutDone, R> { + return Arr.isReadonlyArrayNonEmpty(arr) ? Effect.succeed(arr) : Effect.flatMap(f, loop) + } + )) + })) + +/** + * Filters and maps each element inside emitted non-empty arrays using an + * effectful `Filter`. + * + * **When to use** + * + * Use when array-valued channel outputs need an effectful filter-map that can + * fail and can discard arrays that become empty. + * + * **Details** + * + * Successful filter results are kept as mapped values. Failed filter results + * are removed from the array. Arrays that become empty are discarded. Failures + * from the effectful filter fail the returned channel. + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapArrayEffect: { + ( + filter: Filter.FilterEffect, B, X, EX, RX> + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr | EX, OutDone, InElem, InErr, InDone, Env | RX> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + filter: Filter.FilterEffect + ): Channel, OutErr | EX, OutDone, InElem, InErr, InDone, Env | RX> +} = dual(2, ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + filter: Filter.FilterEffect +): Channel, OutErr | EX, OutDone, InElem, InErr, InDone, Env | RX> => + transformPull(self, (pull) => + Effect.succeed(Effect.flatMap( + pull, + function loop(arr): Pull.Pull, OutErr | EX, OutDone, RX> { + return Effect.flatMap( + Effect.filterMapEffect(arr, filter as any), + (passes) => + Arr.isReadonlyArrayNonEmpty(passes) + ? Effect.succeed(passes as Arr.NonEmptyReadonlyArray) + : Effect.flatMap(pull, loop) + ) + } + )))) + +/** + * Maps over a channel statefully with an accumulator, where each element can produce multiple output values. + * + * **Example** (Mapping with accumulated state) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * // Create a channel with numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4]) + * + * // Use mapAccum to create running sums and emit both current and sum + * const runningSum = Channel.mapAccum( + * numbersChannel, + * () => 0, // initial accumulator state + * (sum, current) => { + * const newSum = sum + current + * // Return [newState, outputValues] + * return [newSum, [current, newSum]] as const + * } + * ) + * // Outputs: 1, 1, 2, 3, 3, 6, 4, 10 + * + * // Using with Effect for async processing + * const asyncMapAccum = Channel.mapAccum( + * numbersChannel, + * () => "", + * (acc, value) => + * Effect.gen(function*() { + * const newAcc = acc + value.toString() + * return [newAcc, [`${value}-processed`, newAcc]] as const + * }) + * ) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const mapAccum: { + ( + initial: LazyArg, + f: ( + s: S, + a: Types.NoInfer + ) => + | Effect.Effect], E, R> + | readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => Array) | undefined + } + ): < + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + B, + OutErr | E, + OutDone, + InElem, + InErr, + InDone, + Env | R + > + ( + self: Channel, + initial: LazyArg, + f: ( + s: S, + a: Types.NoInfer + ) => + | Effect.Effect], E, R> + | readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => Array) | undefined + } + ): Channel +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + initial: LazyArg, + f: ( + s: S, + a: Types.NoInfer + ) => + | Effect.Effect], E, R> + | readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): Channel => + fromTransform((upstream, scope) => + Effect.map(toTransform(self)(upstream, scope), (pull) => { + let state = initial() + let current: ReadonlyArray | undefined + let index = 0 + let cause: Cause.Cause> | undefined + const pullNext = Effect.matchCauseEffect(pull, { + onFailure(cause_) { + cause = cause_ + const b = options?.onHalt && options.onHalt(state) + return b && b.length > 0 + ? Effect.succeed([state, b] as const) + : Effect.failCause(cause_) + }, + onSuccess(a): Effect.Effect], E, R> { + const b = f(state, a) + return Arr.isArray(b) + ? Effect.succeed(b as any) + : b as any + } + }) + const pump = Effect.suspend(function loop(): Pull.Pull { + if (current === undefined) { + if (cause) return Effect.failCause(cause) + return Effect.flatMap(pullNext, ([newState, values]) => { + state = newState + if (values.length === 0) { + return loop() + } else if (values.length === 1) { + return Effect.succeed(values[0]) + } + current = values + return loop() + }) + } + const next = current[index++] + if (index >= current.length) { + current = undefined + index = 0 + } + return Effect.succeed(next) + }) + return pump + }) + ) +) + +/** + * Transforms a channel statefully by scanning over its output with an accumulator function. + * Emits the intermediate results of the scan operation. + * + * **Example** (Scanning channel output) + * + * ```ts + * import { Channel } from "effect" + * + * // Create a channel with numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Scan to create running sum + * const runningSumChannel = Channel.scan(numbersChannel, 0, (sum, n) => sum + n) + * // Outputs: 0, 1, 3, 6, 10, 15 + * // Note: emits the initial value and each intermediate result + * + * // Scan with string concatenation + * const wordsChannel = Channel.fromIterable(["hello", "world", "from", "effect"]) + * const sentenceChannel = Channel.scan( + * wordsChannel, + * "", + * (sentence, word) => sentence === "" ? word : `${sentence} ${word}` + * ) + * // Outputs: "", "hello", "hello world", "hello world from", "hello world from effect" + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const scan: { + (initial: S, f: (s: S, a: Types.NoInfer) => S): < + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + S, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + > + ( + self: Channel, + initial: S, + f: (s: S, a: Types.NoInfer) => S + ): Channel +} = dual(3, ( + self: Channel, + initial: S, + f: (s: S, a: Types.NoInfer) => S +): Channel => + scanEffect(self, initial, (s, a) => Effect.succeed(f(s, a)))) + +/** + * Transforms a channel statefully by scanning over its output with an effectful accumulator function. + * Emits the intermediate results of the scan operation. + * + * **When to use** + * + * Use when maintaining accumulated state over channel output requires Effects + * or can fail, while still emitting each intermediate state. + * + * **Example** (Scanning channel output with effects) + * + * ```ts + * import { Channel, Data, Effect } from "effect" + * + * class ScanError extends Data.TaggedError("ScanError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel with numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4]) + * + * // Effectful scan with async operations + * const asyncScanChannel = Channel.scanEffect( + * numbersChannel, + * "", + * (acc, value) => + * Effect.gen(function*() { + * // Simulate async work + * yield* Effect.sleep("10 millis") + * return acc + value.toString() + * }) + * ) + * // Outputs: "", "1", "12", "123", "1234" + * + * // Scan with error handling + * const errorHandlingScan = Channel.scanEffect( + * numbersChannel, + * 0, + * (sum, n) => { + * if (n < 0) { + * return Effect.fail(new ScanError({ reason: "negative number" })) + * } + * return Effect.succeed(sum + n) + * } + * ) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const scanEffect: { + (initial: S, f: (s: S, a: Types.NoInfer) => Effect.Effect): < + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + S, + OutErr | E, + OutDone, + InElem, + InErr, + InDone, + Env | R + > + ( + self: Channel, + initial: S, + f: (s: S, a: Types.NoInfer) => Effect.Effect + ): Channel +} = dual(3, ( + self: Channel, + initial: S, + f: (s: S, a: Types.NoInfer) => Effect.Effect +): Channel => + fromTransform((upstream, scope) => + Effect.map(toTransform(self)(upstream, scope), (pull) => { + let state = initial + let isFirst = true + return Effect.suspend(() => { + if (isFirst) { + isFirst = false + return Effect.succeed(state) + } + return Effect.map( + Effect.flatMap(pull, (a) => f(state, a)), + (newState) => { + state = newState + return state + } + ) + }) + }) + )) + +/** + * Catches any cause of failure from the channel and allows recovery by + * creating a new channel based on the caught cause. + * + * **Example** (Recovering from failure causes) + * + * ```ts + * import { Cause, Channel, Data } from "effect" + * + * class ProcessError extends Data.TaggedError("ProcessError")<{ + * readonly reason: string + * }> {} + * + * class RecoveryError extends Data.TaggedError("RecoveryError")<{ + * readonly message: string + * }> {} + * + * // Create a failing channel + * const failingChannel = Channel.fail( + * new ProcessError({ reason: "network error" }) + * ) + * + * // Catch the cause and provide recovery + * const recoveredChannel = Channel.catchCause(failingChannel, (cause) => { + * if (Cause.hasFails(cause)) { + * return Channel.succeed("Recovered from failure") + * } + * return Channel.succeed("Recovered from interruption") + * }) + * + * // The channel recovers gracefully from errors + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchCause: { + ( + f: (d: Cause.Cause) => Channel + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: Cause.Cause) => Channel + ): Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (d: Cause.Cause) => Channel +): Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => + fromTransform((upstream, scope) => { + let forkedScope = Scope.forkUnsafe(scope) + return Effect.map(toTransform(self)(upstream, forkedScope), (pull) => { + let currentPull: Pull.Pull = pull.pipe( + Effect.catchCause((cause): Pull.Pull => { + if (Pull.isDoneCause(cause)) { + return Effect.failCause(cause as Cause.Cause>) + } + const toClose = forkedScope + forkedScope = Scope.forkUnsafe(scope) + return Scope.close(toClose, Exit.failCause(cause)).pipe( + Effect.andThen(toTransform(f(cause as Cause.Cause))(upstream, forkedScope)), + Effect.flatMap((childPull) => { + currentPull = childPull + return childPull + }) + ) + }) + ) + return Effect.suspend(() => currentPull) + }) + })) + +/** + * Runs an effect with the full failure `Cause` when the channel fails, then + * fails the returned channel with the original cause. + * + * **When to use** + * + * Use when observing the full channel failure `Cause` is needed without + * changing successful output or replacing the original cause. + * + * **Details** + * + * Use this for observing failures, such as logging or metrics. If the observer + * effect fails, that failure can fail the returned channel. + * + * @category error handling + * @since 4.0.0 + */ +export const tapCause: { + ( + f: (d: Cause.Cause) => Effect.Effect + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + A, + E, + R + >( + self: Channel, + f: (d: Cause.Cause) => Effect.Effect + ): Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + A, + E, + R +>( + self: Channel, + f: (d: Cause.Cause) => Effect.Effect +): Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R +> => catchCause(self, (cause) => fromEffectDrain(Effect.flatMap(f(cause), (_) => Effect.failCause(cause))))) + +/** + * Catches causes of failure that match a specific filter, allowing + * conditional error recovery based on the type of failure. + * + * **When to use** + * + * Use to recover a channel only when its full `Cause` satisfies a boolean + * predicate. + * + * **Details** + * + * When the predicate matches, the recovery function receives the original + * cause. When it does not match, the returned channel fails with the original + * cause. + * + * @see {@link catchCauseFilter} for selecting causes with a `Filter` + * @see {@link catchCause} for recovering from every cause + * @see {@link catchIf} for recovering from typed channel errors + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseIf: { + < + OutErr, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Channel + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Channel + ): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(3, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Channel +): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => + catchCause( + self, + ( + cause + ): Channel< + OutElem1, + OutErr | OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + > => { + return predicate(cause) + ? f(cause) + : failCause(cause as any) + } + )) + +/** + * Recovers from channel failures whose full `Cause` is selected by a `Filter`. + * + * **When to use** + * + * Use when you need to recover a channel only from causes selected by a + * `Filter`, while giving the recovery both the selected value and the original + * `Cause`. + * + * **Details** + * + * When the filter succeeds, the recovery function receives the selected value + * and the original cause. When the filter fails, the returned channel fails + * with the residual cause produced by the filter. + * + * @see {@link catchCauseIf} for selecting causes with a predicate + * @see {@link catchFilter} for selecting typed errors with a `Filter` + * @see {@link catchCause} for recovering from every cause + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseFilter: { + < + OutErr, + EB, + X extends Cause.Cause, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + filter: Filter.Filter, EB, X>, + f: ( + failure: EB, + cause: Cause.Cause + ) => Channel + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1, + Cause.Cause.Error | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + EB, + X extends Cause.Cause, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + filter: Filter.Filter, EB, X>, + f: ( + failure: EB, + cause: Cause.Cause + ) => Channel + ): Channel< + OutElem | OutElem1, + Cause.Cause.Error | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(3, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + EB, + X extends Cause.Cause, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + filter: Filter.Filter, EB, X>, + f: ( + failure: EB, + cause: Cause.Cause + ) => Channel +): Channel< + OutElem | OutElem1, + Cause.Cause.Error | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => + catchCause( + self, + ( + cause + ): Channel< + OutElem1, + Cause.Cause.Error | OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + > => { + const result = filter(cause) + return Result.isFailure(result) + ? failCause(result.failure) + : f(result.success, cause) + } + )) + +const catch_: { + ( + f: (d: OutErr) => Channel + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: OutErr) => Channel + ): Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + self: Channel, + f: (d: OutErr) => Channel +): Channel< + OutElem | OutElem1, + OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => catchCauseFilter(self, Cause.findError, (e) => f(e))) + +export { + /** + * Recovers from typed channel errors by running a fallback channel. + * + * @category error handling + * @since 4.0.0 + */ + catch_ as catch +} + +/** + * Runs an effect when the channel fails with a typed error, then preserves the + * original channel failure. + * + * **Details** + * + * The effect is not run for normal channel completion. If the observer effect + * fails, that failure can fail the returned channel. + * + * @category error handling + * @since 4.0.0 + */ +export const tapError: { + ( + f: (d: OutErr) => Effect.Effect + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + A, + E, + R + >( + self: Channel, + f: (d: OutErr) => Effect.Effect + ): Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + A, + E, + R +>( + self: Channel, + f: (d: OutErr) => Effect.Effect +): Channel< + OutElem, + OutErr | E, + OutDone | void, + InElem, + InErr, + InDone, + Env | R +> => + transformPull( + self, + (pull) => + Effect.succeed(Effect.tapError( + pull, + (err) => Cause.isDone(err) ? Effect.void : Effect.asVoid(f(err)) + )) + )) + +/** + * Recovers from typed channel errors that match a predicate or refinement. + * + * **When to use** + * + * Use to recover from typed channel errors when a predicate or refinement + * selects the failures that should switch to a recovery channel. + * + * **Details** + * + * Matching errors are handled by the recovery function. Non-matching errors + * are handled by `orElse` when provided. Without `orElse`, non-matching errors + * are re-failed. + * + * @see {@link catch_ catch} for recovering from every typed channel error + * @see {@link catchFilter} for selecting typed errors with a `Filter` + * @see {@link catchTag} for selecting tagged typed errors + * @see {@link catchCauseFilter} for selecting full causes with a `Filter` + * + * @category error handling + * @since 4.0.0 + */ +export const catchIf: { + < + OutErr, + EB extends OutErr, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + refinement: Predicate.Refinement, + f: (failure: EB) => Channel, + orElse?: + | (( + failure: Exclude + ) => Channel) + | undefined + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? Exclude : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutErr, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + predicate: Predicate.Predicate, + f: (failure: OutErr) => Channel, + orElse?: + | (( + failure: OutErr + ) => Channel) + | undefined + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? OutErr : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + EB extends OutErr, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + refinement: Predicate.Refinement, + f: (failure: EB) => Channel, + orElse?: + | (( + failure: Exclude + ) => Channel) + | undefined + ): Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? Exclude : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + predicate: Predicate.Predicate, + f: (failure: OutErr) => Channel, + orElse?: + | (( + failure: OutErr + ) => Channel) + | undefined + ): Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? OutErr : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = dual((args) => isChannel(args[0]), < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = never, + OutErr2 = OutErr, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never +>( + self: Channel, + predicate: Predicate.Predicate, + f: (failure: OutErr) => Channel, + orElse?: + | (( + failure: OutErr + ) => Channel) + | undefined +): Channel< + OutElem | OutElem1 | OutElem2, + OutErr1 | OutErr2, + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 +> => + catch_( + self, + (err): Channel< + OutElem1 | OutElem2, + OutErr1 | OutErr2, + OutDone1 | OutDone2, + InElem1 & InElem2, + InErr1 & InErr2, + InDone1 & InDone2, + Env1 | Env2 + > => { + return predicate(err) + ? f(err) + : orElse + ? orElse(err) + : fail(err as any) as any + } + )) + +/** + * Recovers from typed channel errors selected by a `Filter`. + * + * **When to use** + * + * Use to recover from channel errors with a reusable `Filter` when matching + * can also narrow or transform the error before choosing the recovery channel. + * + * **Details** + * + * Successful filter results are handled by the recovery function. Failed + * filter results are handled by `orElse` when provided. Without `orElse`, + * failed filter results are re-failed. + * + * @see {@link catchIf} for selecting typed errors with a predicate + * @see {@link catchTag} for selecting tagged typed errors + * @see {@link catchCauseFilter} for selecting full causes with a `Filter` + * + * @category error handling + * @since 4.0.0 + */ +export const catchFilter: { + < + OutErr, + EB, + X, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + filter: Filter.Filter, + f: (failure: EB) => Channel, + orElse?: + | (( + failure: X + ) => Channel) + | undefined + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? X : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + EB, + X, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + filter: Filter.Filter, + f: (failure: EB) => Channel, + orElse?: + | (( + failure: X + ) => Channel) + | undefined + ): Channel< + OutElem | OutElem1 | Exclude, + OutErr1 | OutErr2 | (OutElem2 extends Types.unassigned ? X : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = dual((args) => isChannel(args[0]), < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + EB, + X, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = never, + OutErr2 = X, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never +>( + self: Channel, + filter: Filter.Filter, + f: (failure: EB) => Channel, + orElse?: + | (( + failure: X + ) => Channel) + | undefined +): Channel< + OutElem | OutElem1 | OutElem2, + OutErr1 | OutErr2, + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 +> => + catch_( + self, + (err): Channel< + OutElem1 | OutElem2, + OutErr1 | OutErr2, + OutDone1 | OutDone2, + InElem1 & InElem2, + InErr1 & InErr2, + InDone1 & InDone2, + Env1 | Env2 + > => { + const result = filter(err) + return Result.isFailure(result) + ? orElse + ? orElse(result.failure) + : fail(result.failure as any) as any + : f(result.success) + } + )) + +/** + * Recovers from tagged channel errors whose `_tag` matches one or more tags. + * + * **Details** + * + * Matching tagged errors are handled by the recovery function. Non-matching + * errors are handled by `orElse` when provided. Without `orElse`, + * non-matching errors are re-failed. + * + * @category error handling + * @since 4.0.0 + */ +export const catchTag: { + < + OutErr, + const K extends Types.Tags | Arr.NonEmptyReadonlyArray>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + k: K, + f: ( + e: Types.ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel, + orElse?: + | (( + e: Types.ExcludeTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel) + | undefined + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >(self: Channel) => Channel< + OutElem | OutElem1 | Exclude, + | OutErr1 + | OutErr2 + | (OutElem2 extends Types.unassigned + ? Types.ExcludeTag ? K[number] : K> + : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + const K extends Types.Tags | Arr.NonEmptyReadonlyArray>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + k: K, + f: ( + e: Types.ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel, + orElse?: + | (( + e: Types.ExcludeTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel) + | undefined + ): Channel< + OutElem | OutElem1 | Exclude, + | OutErr1 + | OutErr2 + | (OutElem2 extends Types.unassigned + ? Types.ExcludeTag ? K[number] : K> + : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = dual((args) => isChannel(args[0]), < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + const K extends Types.Tags | Arr.NonEmptyReadonlyArray>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = never, + OutErr2 = Types.ExcludeTag ? K[number] : K>, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never +>( + self: Channel, + k: K, + f: ( + e: Types.ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel, + orElse?: + | (( + e: Types.ExcludeTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Channel) + | undefined +): Channel< + OutElem | OutElem1 | OutElem2, + OutErr1 | OutErr2, + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 +> => { + const pred = Array.isArray(k) + ? ((e: OutErr): e is any => hasProperty(e, "_tag") && k.includes(e._tag)) + : isTagged(k as string) + return catchIf(self, pred, f, orElse as any) as any +}) + +/** + * Catches a specific reason within a tagged error. + * + * **Example** (Recovering from nested reasons) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * const channel = Channel.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) + * ) + * + * const recovered = channel.pipe( + * Channel.catchReason("AiError", "RateLimitError", (reason) => + * Channel.succeed(`retry: ${reason.retryAfter}`) + * ) + * ) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchReason: { + < + OutErr, + K extends Types.Tags, + RK extends Types.ReasonTags, K>>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + errorTag: K, + reasonTag: RK, + f: ( + reason: Types.ExtractReason, K>, RK>, + error: Types.NarrowReason, K>, RK> + ) => Channel, + orElse?: + | (( + reason: Types.ExcludeReason, K>, RK>, + error: Types.OmitReason, K>, RK> + ) => Channel) + | undefined + ): < + OutElem, + OutDone, + InElem, + InErr, + InDone, + Env + >( + self: Channel + ) => Channel< + OutElem | OutElem1 | Exclude, + | Types.ExcludeTag + | OutErr1 + | OutErr2 + | (OutElem2 extends Types.unassigned ? Types.ExtractTag : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + K extends Types.Tags, + RK extends Types.ReasonTags, K>>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + errorTag: K, + reasonTag: RK, + f: ( + reason: Types.ExtractReason, K>, RK>, + error: Types.NarrowReason, K>, RK> + ) => Channel, + orElse?: + | (( + reason: Types.ExcludeReason, K>, RK>, + error: Types.OmitReason, K>, RK> + ) => Channel) + | undefined + ): Channel< + OutElem | OutElem1 | Exclude, + | Types.ExcludeTag + | OutErr1 + | OutErr2 + | (OutElem2 extends Types.unassigned ? Types.ExtractTag : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 + > +} = dual((args) => isChannel(args[0]), < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + K extends Types.Tags, + RK extends Types.ReasonTags, K>>, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never +>( + self: Channel, + errorTag: K, + reasonTag: RK, + f: ( + reason: Types.ExtractReason, K>, RK>, + error: Types.NarrowReason, K>, RK> + ) => Channel, + orElse?: + | (( + reason: Types.ExcludeReason, K>, RK>, + error: Types.OmitReason, K>, RK> + ) => Channel) + | undefined +): Channel< + OutElem | OutElem1 | Exclude, + | Types.ExcludeTag + | OutErr1 + | OutErr2 + | (OutElem2 extends Types.unassigned ? Types.ExtractTag : never), + OutDone | OutDone1 | OutDone2, + InElem & InElem1 & InElem2, + InErr & InErr1 & InErr2, + InDone & InDone1 & InDone2, + Env | Env1 | Env2 +> => + catch_( + self, + (error): Channel< + OutElem1 | Exclude, + OutErr1 | OutErr2, + OutDone1 | OutDone2, + InElem1 & InElem2, + InErr1 & InErr2, + InDone1 & InDone2, + Env1 | Env2 + > => { + if (isTagged(error, errorTag) && hasProperty(error, "reason")) { + const reason = error.reason as Types.ExcludeReason, K>, RK> + if (isTagged(reason, reasonTag)) { + return f(reason as any, error as any) + } + return orElse ? orElse(reason, error as any) as any : fail(error) as any + } + return fail(error) as any + } + )) + +/** + * Catches multiple reasons within a tagged error using an object of handlers. + * + * @category error handling + * @since 4.0.0 + */ +export const catchReasons: { + < + K extends Types.Tags, + OutErr, + Cases extends { + [RK in Types.ReasonTags, K>>]+?: ( + reason: Types.ExtractReason, K>, RK>, + error: Types.NarrowReason, K>, RK> + ) => Channel + }, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: Types.ExcludeReason, K>, Extract>, + error: Types.OmitReason, K>, Extract> + ) => Channel) + | undefined + ): ( + self: Channel + ) => Channel< + | OutElem + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutElem1 : never + }[keyof Cases], + | Types.ExcludeTag + | OutErr2 + | (OutElem2 extends Types.unassigned ? Types.ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutErr1 : never + }[keyof Cases], + | OutDone + | OutDone2 + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutDone1 : never + }[keyof Cases], + & InElem + & InElem2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InElem1 : never + }[keyof Cases], + & InErr + & InErr2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InErr1 : never + }[keyof Cases], + & InDone + & InDone2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InDone1 : never + }[keyof Cases], + | Env + | Env2 + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? Env1 : never + }[keyof Cases] + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + K extends Types.Tags, + Cases extends { + [RK in Types.ReasonTags>]+?: ( + reason: Types.ExtractReason, RK>, + error: Types.NarrowReason, RK> + ) => Channel + }, + OutElem2 = Types.unassigned, + OutErr2 = never, + OutDone2 = never, + InElem2 = unknown, + InErr2 = unknown, + InDone2 = unknown, + Env2 = never + >( + self: Channel, + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: Types.ExcludeReason, K>, Extract>, + error: Types.OmitReason, K>, Extract> + ) => Channel) + | undefined + ): Channel< + | OutElem + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutElem1 : never + }[keyof Cases], + | Types.ExcludeTag + | OutErr2 + | (OutElem2 extends Types.unassigned ? Types.ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutErr1 : never + }[keyof Cases], + | OutDone + | OutDone2 + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? OutDone1 : never + }[keyof Cases], + & InElem + & InElem2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InElem1 : never + }[keyof Cases], + & InErr + & InErr2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InErr1 : never + }[keyof Cases], + & InDone + & InDone2 + & { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? InDone1 : never + }[keyof Cases], + | Env + | Env2 + | { + [RK in keyof Cases]: Cases[RK] extends + (...args: Array) => Channel ? Env1 : never + }[keyof Cases] + > +} = dual((args) => isChannel(args[0]), (self, errorTag, cases, orElse) => { + let keys: Set + return catch_(self, (error) => { + if ( + isTagged(error, errorTag) && + hasProperty(error, "reason") && + hasProperty(error.reason, "_tag") && + String.isString(error.reason._tag) + ) { + const reason = error.reason as { readonly _tag: string } + keys ??= new Set(Object.keys(cases)) + if (keys.has(reason._tag)) { + return (cases as any)[reason._tag](reason as any, error) + } + return orElse ? orElse(reason, error) as any : fail(error) as any + } + return fail(error) as any + }) +}) + +/** + * Promotes nested reason errors into the channel error, replacing the parent error. + * + * **Example** (Promoting nested reasons) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * const channel = Channel.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) + * ) + * + * const unwrapped = channel.pipe(Channel.unwrapReason("AiError")) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const unwrapReason: { + < + K extends TagsWithReason, + OutErr + >( + errorTag: K + ): ( + self: Channel + ) => Channel< + OutElem, + Types.ExcludeTag | Types.ReasonOf>, + OutDone, + InElem, + InErr, + InDone, + Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + K extends TagsWithReason + >( + self: Channel, + errorTag: K + ): Channel< + OutElem, + Types.ExcludeTag | Types.ReasonOf>, + OutDone, + InElem, + InErr, + InDone, + Env + > +} = dual(2, < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + K extends TagsWithReason +>( + self: Channel, + errorTag: K +): Channel< + OutElem, + Types.ExcludeTag | Types.ReasonOf>, + OutDone, + InElem, + InErr, + InDone, + Env +> => + catchFilter( + self, + (error) => + isTagged(error, errorTag) && hasProperty(error, "reason") ? Result.succeed(error.reason) : Result.fail(error), + fail + ) as any) + +/** + * Returns a new channel, which is the same as this one, except the failure + * value of the returned channel is created by applying the specified function + * to the failure value of this channel. + * + * @category error handling + * @since 2.0.0 + */ +export const mapError: { + ( + f: (err: OutErr) => OutErr2 + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (err: OutErr) => OutErr2 + ): Channel +} = dual(2, ( + self: Channel, + f: (err: OutErr) => OutErr2 +): Channel => catch_(self, (err) => fail(f(err)))) + +/** + * Converts all errors in the channel to defects (unrecoverable failures). + * This is useful when you want to treat errors as programming errors. + * + * **Example** (Converting failures to defects) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class ValidationError extends Data.TaggedError("ValidationError")<{ + * readonly field: string + * }> {} + * + * // Create a channel that might fail + * const failingChannel = Channel.fail(new ValidationError({ field: "email" })) + * + * // Convert failures to defects + * const fatalChannel = Channel.orDie(failingChannel) + * + * // Any failure will now become a defect (uncaught exception) + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const orDie = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env +>( + self: Channel +): Channel => catch_(self, die) + +/** + * Ignores all errors in the channel, converting them to an empty channel. + * + * **Details** + * + * Use the `log` option to emit the full {@link Cause} when the channel fails. + * + * @category error handling + * @since 4.0.0 + */ +export const ignore: < + Arg extends Channel | { + readonly log?: boolean | Severity | undefined + } | undefined = { + readonly log?: boolean | Severity | undefined + } +>( + selfOrOptions: Arg, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined +) => [Arg] extends + [Channel] + ? Channel + : ( + self: Channel + ) => Channel = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined + ): Channel => { + if (!options?.log) { + return catch_(self, () => empty) + } + const logEffect = Effect.logWithLevel(options.log === true ? undefined : options.log) + return catch_( + tapCause(self, (cause) => Cause.hasFails(cause) ? logEffect(cause) : Effect.void), + () => empty + ) + } + ) + +const ignoreCause_ = < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env +>( + self: Channel +): Channel => catchCause(self, () => empty) + +/** + * Ignores all errors in the channel including defects, converting them to an empty channel. + * + * **When to use** + * + * Use when a channel should become best-effort and all failure causes, including + * defects and interruptions, can be converted to empty output. + * + * **Details** + * + * Use the `log` option to emit the full {@link Cause} when the channel fails. + * + * @category error handling + * @since 4.0.0 + */ +export const ignoreCause: < + Arg extends Channel | { + readonly log?: boolean | Severity | undefined + } | undefined = { + readonly log?: boolean | Severity | undefined + } +>( + selfOrOptions: Arg, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined +) => [Arg] extends + [Channel] + ? Channel + : ( + self: Channel + ) => Channel = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + options?: { readonly log?: boolean | Severity | undefined } | undefined + ): Channel => { + if (!options?.log) return ignoreCause_(self) + const logEffect = Effect.logWithLevel(options.log === true ? undefined : options.log) + return ignoreCause_(tapCause(self, (cause) => logEffect(cause))) + } + ) + +/** + * Returns a new channel that retries this channel according to the specified + * schedule whenever it fails. + * + * @category error handling + * @since 4.0.0 + */ +export const retry: { + ( + schedule: + | Schedule.Schedule, SE, SR> + | (( + $: ( + _: Schedule.Schedule, SE, SR> + ) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) + ): ( + self: Channel + ) => Channel + ( + self: Channel, + schedule: + | Schedule.Schedule + | (( + $: ( + _: Schedule.Schedule, SE, SR> + ) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) + ): Channel +} = dual(2, ( + self: Channel, + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule, SE, R>) => Schedule.Schedule + ) => Schedule.Schedule, SE, SR>) +): Channel => + suspend(() => { + let step: ((input: OutErr) => Pull.Pull, SE, SO, SR>) | undefined = undefined + let meta = Schedule.CurrentMetadata.defaultValue() + const selfWithMeta = provideServiceEffect(self, Schedule.CurrentMetadata, Effect.sync(() => meta)) + const withReset = onFirst(selfWithMeta, () => { + step = undefined + return Effect.void + }) + const resolvedSchedule = typeof schedule === "function" ? schedule(identity_) : schedule + const loop: Channel< + OutElem, + OutErr | SE, + OutDone, + InElem, + InErr, + InDone, + Env | SR + > = catch_( + withReset, + Effect.fnUntraced( + function*(error) { + if (!step) { + step = yield* Schedule.toStepWithMetadata(resolvedSchedule) + } + meta = yield* step(error) + return loop + }, + (effect, error) => Pull.catchDone(effect, () => Effect.succeed(fail(error))), + unwrap + ) + ) + return loop + })) + +/** + * Maps each output element to a channel and emits values from the most recent + * active child channels. + * + * **Details** + * + * With the default concurrency of `1`, starting a new child channel interrupts + * the previous child channel. Use `options.concurrency` to allow more active + * child channels. The source channel's done value is preserved. + * + * **Example** (Switching mapped channels) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class SwitchError extends Data.TaggedError("SwitchError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel that outputs numbers + * const numberChannel = Channel.fromIterable([1, 2, 3]) + * + * // Switch to new channels based on each value + * const switchedChannel = Channel.switchMap( + * numberChannel, + * (n) => Channel.fromIterable([`value-${n}`]) + * ) + * + * // Outputs: "value-1", "value-2", "value-3" + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const switchMap: { + ( + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): ( + self: Channel + ) => Channel< + OutElem1, + OutErr1 | OutErr, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual( + (args) => isChannel(args[0]), + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + self: Channel, + f: (d: OutElem) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): Channel< + OutElem1, + OutErr | OutErr1, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > => + self.pipe( + map(f), + mergeAll({ + ...options, + concurrency: options?.concurrency ?? 1, + switch: true + }) + ) +) + +/** + * Merges multiple channels with specified concurrency and buffering options. + * + * **When to use** + * + * Use when channel outputs are themselves channels and multiple inner channels + * should run with configured concurrency and buffering. + * + * **Example** (Merging nested channels) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class MergeAllError extends Data.TaggedError("MergeAllError")<{ + * readonly reason: string + * }> {} + * + * // Create channels that output other channels + * const nestedChannels = Channel.fromIterable([ + * Channel.fromIterable([1, 2]), + * Channel.fromIterable([3, 4]), + * Channel.fromIterable([5, 6]) + * ]) + * + * // Merge all channels with bounded concurrency + * const mergedChannel = Channel.mergeAll({ + * concurrency: 2, + * bufferSize: 16 + * })(nestedChannels) + * + * // Outputs: 1, 2, 3, 4, 5, 6 (order may vary due to concurrency) + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const mergeAll: { + (options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + }): ( + channels: Channel< + Channel, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + > + ) => Channel< + OutElem, + OutErr1 | OutErr, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + ( + channels: Channel< + Channel, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + >, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } + ): Channel< + OutElem, + OutErr1 | OutErr, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > +} = dual( + 2, + ( + channels: Channel< + Channel, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + >, + { bufferSize = 16, concurrency, switch: switch_ = false }: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + readonly switch?: boolean | undefined + } + ): Channel< + OutElem, + OutErr1 | OutErr, + OutDone, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > => + fromTransformBracket( + Effect.fnUntraced(function*(upstream, scope, forkedScope) { + const concurrencyN = concurrency === "unbounded" + ? Number.MAX_SAFE_INTEGER + : Math.max(1, concurrency) + const semaphore = switch_ ? undefined : Semaphore.makeUnsafe(concurrencyN) + const doneLatch = yield* Latch.make(true) + const fibers = new Set>() + + const queue = yield* Queue.bounded>( + bufferSize + ) + yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) + + const pull = yield* toTransform(channels)(upstream, scope) + + yield* Effect.gen(function*() { + while (true) { + if (semaphore) yield* semaphore.take(1) + const channel = yield* pull + const childScope = Scope.forkUnsafe(forkedScope) + const childPull = yield* toTransform(channel)(upstream, childScope) + + while (fibers.size >= concurrencyN) { + const fiber = Iterable.headUnsafe(fibers) + fibers.delete(fiber) + if (fibers.size === 0) yield* doneLatch.open + yield* Fiber.interrupt(fiber) + } + + const fiber = yield* childPull.pipe( + Effect.tap(() => Effect.yieldNow), + Effect.flatMap((value) => Queue.offer(queue, value)), + Effect.forever({ disableYield: true }), + Effect.onError(Effect.fnUntraced(function*(cause) { + const halt = Pull.filterDone(cause) + yield* Effect.exit(Scope.close( + childScope, + !Result.isFailure(halt) ? Exit.succeed(halt.success.value) : Exit.failCause(halt.failure) + )) + if (!fibers.has(fiber)) return + fibers.delete(fiber) + if (semaphore) yield* semaphore.release(1) + if (fibers.size === 0) yield* doneLatch.open + if (Result.isSuccess(halt)) return + return yield* Queue.failCause(queue, cause as any) + })), + Effect.forkChild + ) + + doneLatch.closeUnsafe() + fibers.add(fiber) + } + }).pipe( + Effect.catchCause((cause) => doneLatch.whenOpen(Queue.failCause(queue, cause))), + Effect.forkIn(forkedScope) + ) + + return Queue.take(queue) + }) + ) +) + +/** + * Represents strategies for halting merged channels when one completes or fails. + * + * **Example** (Choosing merge halt strategies) + * + * ```ts + * import type { Channel } from "effect" + * + * // Different halt strategies for channel merging + * const leftFirst: Channel.HaltStrategy = "left" // Stop when left channel halts + * const rightFirst: Channel.HaltStrategy = "right" // Stop when right channel halts + * const both: Channel.HaltStrategy = "both" // Stop when both channels halt + * const either: Channel.HaltStrategy = "either" // Stop when either channel halts + * ``` + * + * @category models + * @since 4.0.0 + */ +export type HaltStrategy = "left" | "right" | "both" | "either" + +/** + * Returns a new channel, which is the merge of this channel and the specified + * channel. + * + * **Example** (Merging channels) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class MergeError extends Data.TaggedError("MergeError")<{ + * readonly source: string + * }> {} + * + * // Create two channels + * const leftChannel = Channel.fromIterable([1, 2, 3]) + * const rightChannel = Channel.fromIterable(["a", "b", "c"]) + * + * // Merge them with "either" halt strategy + * const mergedChannel = Channel.merge(leftChannel, rightChannel, { + * haltStrategy: "either" + * }) + * + * // Outputs elements from both channels concurrently + * // Order may vary: 1, "a", 2, "b", 3, "c" + * ``` + * + * @category combining + * @since 4.0.0 + */ +export const merge: { + ( + right: Channel, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined + ): ( + left: Channel + ) => Channel< + OutElem1 | OutElem, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env1 | Env + > + < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 + >( + left: Channel, + right: Channel, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined + ): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + > +} = dual((args) => isChannel(args[0]) && isChannel(args[1]), < + OutElem, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + OutElem1, + OutErr1, + OutDone1, + InElem1, + InErr1, + InDone1, + Env1 +>( + left: Channel, + right: Channel, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined +): Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 +> => + fromTransformBracket(Effect.fnUntraced(function*(upstream, _scope, forkedScope) { + const strategy = options?.haltStrategy ?? "both" + const queue = yield* Queue.bounded>(0) + yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) + let done = 0 + function onExit( + side: "left" | "right", + cause: Cause.Cause> + ): Effect.Effect { + done++ + if (!Pull.isDoneCause(cause)) { + return Queue.failCause(queue, cause) + } + switch (strategy) { + case "both": { + return done === 2 ? Queue.failCause(queue, cause) : Effect.void + } + case "left": + case "right": { + return side === strategy ? Queue.failCause(queue, cause) : Effect.void + } + case "either": { + return Queue.failCause(queue, cause) + } + } + } + const runSide = ( + side: "left" | "right", + channel: Channel< + OutElem | OutElem1, + OutErr | OutErr1, + OutDone | OutDone1, + InElem & InElem1, + InErr & InErr1, + InDone & InDone1, + Env | Env1 + >, + scope: Scope.Closeable + ) => + toTransform(channel)(upstream, scope).pipe( + Effect.flatMap((pull) => + pull.pipe( + Effect.flatMap((value) => Queue.offer(queue, value)), + Effect.forever + ) + ), + Effect.onError((cause) => + Effect.andThen( + Scope.close(scope, Pull.doneExitFromCause(cause)), + onExit(side, cause) + ) + ), + Effect.forkIn(forkedScope) + ) + yield* runSide("left", left, Scope.forkUnsafe(forkedScope)) + yield* runSide("right", right, Scope.forkUnsafe(forkedScope)) + return Queue.take(queue) + }))) + +/** + * Runs an effect concurrently with a channel while emitting only the channel's + * output elements. + * + * **When to use** + * + * Use when a side effect should run for the lifetime of a channel and only the + * channel's output elements should be emitted. + * + * **Details** + * + * The effect's successful value is ignored. If the effect fails while the + * channel is running, the returned channel fails with that error. + * + * @category combining + * @since 4.0.0 + */ +export const mergeEffect: { + ( + effect: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + effect: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + effect: Effect.Effect +): Channel => + merge( + self, + fromEffectDrain(effect), + { haltStrategy: "left" } + ) as any) + +/** + * Splits upstream string chunks into lines, recognizing `\n`, `\r\n`, and + * standalone `\r` as line terminators. The behavior matches + * `String.linesIterator` regardless of how the input is chunked. + * + * **Details** + * + * A line terminator at the very end of the stream does **not** produce a + * trailing empty line (consistent with `String.linesIterator`). Conversely, + * if the stream ends without a terminator the final partial line is still + * emitted. + * + * **Example** (Splitting string chunks into lines) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function*() { + * const result = yield* Stream.runCollect( + * Stream.splitLines(Stream.make("hel", "lo\r\nwor", "ld\n")) + * ) + * console.log(result) + * // [ 'hello', 'world' ] + * })) + * ``` + * + * @category String manipulation + * @since 2.0.0 + */ +export const splitLines = (): Channel< + Arr.NonEmptyReadonlyArray, + Err, + Done, + Arr.NonEmptyReadonlyArray, + Err, + Done +> => + fromTransform((upstream, _scope) => + Effect.sync(() => { + // Accumulates text that has not yet been terminated by a line break. + // Content is carried across chunks until a terminator is found. + let stringBuilder = "" + // Set when a chunk ends with \r so the next chunk can check whether + // the following character is \n (completing a \r\n pair) or not + // (standalone \r, which is itself a line terminator). + let midCRLF = false + // Remembers the upstream Done value after the first time the upstream + // signals completion, so subsequent pulls return Done immediately + // without pulling upstream again. + let done = Option.none() + + function splitLinesArray(chunk: Arr.NonEmptyReadonlyArray): Arr.NonEmptyReadonlyArray | null { + const chunkBuilder: Array = [] + + function pushLine(segment: string): void { + if (stringBuilder.length === 0) { + chunkBuilder.push(segment) + } else { + chunkBuilder.push(stringBuilder + segment) + stringBuilder = "" + } + } + + for (let i = 0; i < chunk.length; i++) { + const str = chunk[i] + if (str.length !== 0) { + let from = 0 + let indexOfCR = str.indexOf("\r") + let indexOfLF = str.indexOf("\n") + if (midCRLF) { + if (indexOfLF === 0) { + pushLine("") + from = 1 + indexOfLF = str.indexOf("\n", from) + } else { + pushLine("") + } + midCRLF = false + } + while (indexOfCR !== -1 || indexOfLF !== -1) { + if (indexOfCR === -1 || (indexOfLF !== -1 && indexOfLF < indexOfCR)) { + pushLine(str.substring(from, indexOfLF)) + from = indexOfLF + 1 + indexOfLF = str.indexOf("\n", from) + } else { + if (str.length === indexOfCR + 1) { + midCRLF = true + indexOfCR = -1 + } else { + pushLine(str.substring(from, indexOfCR)) + from = indexOfCR + (indexOfLF === indexOfCR + 1 ? 2 : 1) + indexOfCR = str.indexOf("\r", from) + indexOfLF = str.indexOf("\n", from) + } + } + } + stringBuilder = stringBuilder + str.substring(from, str.length - (midCRLF ? 1 : 0)) + } + } + return Arr.isReadonlyArrayNonEmpty(chunkBuilder) ? chunkBuilder : null + } + + const pullOrFlush: Pull.Pull, Err, Done> = Effect.suspend(() => { + if (done._tag === "Some") { + return Cause.done(done.value) + } + return Pull.matchEffect(upstream, { + onSuccess: loop, + onFailure: Effect.failCause, + onDone: (leftover) => { + done = Option.some(leftover) + if (stringBuilder.length > 0 || midCRLF) { + const last = stringBuilder + stringBuilder = "" + midCRLF = false + return Effect.succeed([last] as Arr.NonEmptyReadonlyArray) + } + return Cause.done(leftover) + } + }) + }) + + function loop(chunk: Arr.NonEmptyReadonlyArray): Pull.Pull, Err, Done> { + const lines = splitLinesArray(chunk) + return lines !== null ? Effect.succeed(lines) : pullOrFlush + } + + return pullOrFlush + }) + ) + +/** + * Decodes incoming `Uint8Array` chunks into strings using `TextDecoder`. + * + * **Details** + * + * Input chunks are decoded with streaming enabled so multi-byte characters may + * span `Uint8Array` boundaries. The optional `encoding` and `options` are + * passed to `TextDecoder`. + * + * @category String manipulation + * @since 4.0.0 + */ +export const decodeText = (encoding?: string, options?: TextDecoderOptions): Channel< + Arr.NonEmptyReadonlyArray, + Err, + Done, + Arr.NonEmptyReadonlyArray, + Err, + Done +> => + fromTransform((upstream, _scope) => + Effect.sync(() => { + const decoder = new TextDecoder(encoding, options) + const streamOptions = { stream: true } + return Effect.map(upstream, Arr.map((line) => decoder.decode(line, streamOptions))) + }) + ) + +/** + * Encodes incoming string chunks into `Uint8Array` values using `TextEncoder`. + * + * **Details** + * + * Each string inside an emitted array is encoded independently. + * + * @category String manipulation + * @since 4.0.0 + */ +export const encodeText = (): Channel< + Arr.NonEmptyReadonlyArray, + Err, + Done, + Arr.NonEmptyReadonlyArray, + Err, + Done +> => + fromTransform((upstream, _scope) => + Effect.sync(() => { + const encoder = new TextEncoder() + return Effect.map(upstream, Arr.map((line) => encoder.encode(line))) + }) + ) + +/** + * Returns a new channel that pipes the output of this channel into the + * specified channel. The returned channel has the input type of this channel, + * and the output type of the specified channel, terminating with the value of + * the specified channel. + * + * **Example** (Piping one channel into another) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class PipeError extends Data.TaggedError("PipeError")<{ + * readonly stage: string + * }> {} + * + * // Create source and transform channels + * const sourceChannel = Channel.fromIterable([1, 2, 3]) + * const transformChannel = Channel.map(sourceChannel, (n: number) => n * 2) + * + * // Pipe the source into the transform + * const pipedChannel = Channel.pipeTo(sourceChannel, transformChannel) + * + * // Outputs: 2, 4, 6 + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const pipeTo: { + ( + that: Channel + ): ( + self: Channel + ) => Channel + ( + self: Channel, + that: Channel + ): Channel +} = dual( + 2, + ( + self: Channel, + that: Channel + ): Channel => + fromTransform((upstream, scope) => + Effect.flatMap(toTransform(self)(upstream, scope), (upstream) => toTransform(that)(upstream, scope)) + ) +) + +/** + * Returns a new channel that pipes the output of this channel into the + * specified channel and preserves this channel's failures without providing + * them to the other channel for observation. + * + * **Example** (Piping while preserving failures) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class SourceError extends Data.TaggedError("SourceError")<{ + * readonly code: number + * }> {} + * + * // Create a failing source channel + * const failingSource = Channel.fail(new SourceError({ code: 404 })) + * const safeTransform = Channel.succeed("transformed") + * + * // Pipe while preserving source failures + * const safePipedChannel = Channel.pipeToOrFail(failingSource, safeTransform) + * + * // Source errors are preserved and not sent to transform channel + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const pipeToOrFail: { + ( + that: Channel + ): ( + self: Channel + ) => Channel + ( + self: Channel, + that: Channel + ): Channel +} = dual( + 2, + ( + self: Channel, + that: Channel + ): Channel => + fromTransform((upstream, scope) => + Effect.flatMap(toTransform(self)(upstream, scope), (upstream) => { + const upstreamPull = Effect.catchCause( + upstream, + (cause) => Pull.isDoneCause(cause) ? Effect.failCause(cause) : Effect.die(Cause.Done(cause)) + ) as Pull.Pull + + return Effect.map( + toTransform(that)(upstreamPull, scope), + (pull) => + Effect.catchDefect( + pull, + (defect) => + Cause.isDone(defect) ? Effect.failCause(defect.value as Cause.Cause) : Effect.die(defect) + ) + ) + }) + ) +) + +/** + * Constructs a `Channel` from a scoped effect that will result in a + * `Channel` if successful. + * + * **Example** (Unwrapping channel effects) + * + * ```ts + * import { Channel, Data, Effect } from "effect" + * + * class UnwrapError extends Data.TaggedError("UnwrapError")<{ + * readonly reason: string + * }> {} + * + * // Create an effect that produces a channel + * const channelEffect = Effect.succeed( + * Channel.fromIterable([1, 2, 3]) + * ) + * + * // Unwrap the effect to get the channel + * const unwrappedChannel = Channel.unwrap(channelEffect) + * + * // The resulting channel outputs: 1, 2, 3 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unwrap = ( + channel: Effect.Effect, E, R> +): Channel | R2> => + fromTransform((upstream, scope) => { + let pull: Pull.Pull | undefined + return Effect.succeed(Effect.suspend(() => { + if (pull) return pull + return channel.pipe( + Scope.provide(scope), + Effect.flatMap((channel) => toTransform(channel)(upstream, scope)), + Effect.flatMap((pull_) => pull = pull_) + ) + })) + }) + +/** + * Runs a channel with a scope provided for the duration of the channel + * execution, removing the channel's `Scope` requirement. + * + * @category resource management + * @since 2.0.0 + */ +export const scoped = ( + self: Channel +): Channel> => + fromTransformBracket((upstream, scope, forkedScope) => + Effect.map( + Scope.provide(toTransform(self)(upstream, scope), forkedScope), + Scope.provide(forkedScope) + ) + ) + +/** + * Runs an input handler against the upstream pull while the wrapped channel + * runs without receiving upstream input directly. + * + * **Details** + * + * The input handler is forked in the channel scope. The wrapped channel is run + * with an already-completed input. + * + * **Example** (Embedding custom input handling) + * + * ```ts + * import { Channel, Effect } from "effect" + * + * // Create a base channel + * const baseChannel = Channel.fromIterable([1, 2, 3]) + * + * // Drain the embedded input while the base channel runs + * const embeddedChannel = Channel.embedInput( + * baseChannel, + * (upstream) => + * upstream.pipe( + * Effect.tap((message) => + * Effect.sync(() => console.log(message)) + * ), + * Effect.forever, + * Effect.ignore + * ) + * ) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const embedInput: { + ( + input: ( + upstream: Pull.Pull + ) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + input: ( + upstream: Pull.Pull + ) => Effect.Effect + ): Channel +} = dual( + 2, + ( + self: Channel, + input: ( + upstream: Pull.Pull + ) => Effect.Effect + ): Channel => + fromTransformBracket((upstream, scope, forkedScope) => + Effect.andThen( + Effect.forkIn(input(upstream), forkedScope), + toTransform(self)(Cause.done(), scope) + ) + ) +) + +/** + * Buffers individual output elements in a queue with the configured `capacity` + * so a faster producer can progress independently of a slower consumer. + * + * **When to use** + * + * Use when output elements can be decoupled from downstream demand and the + * configured backpressure or loss strategy is acceptable. + * + * **Details** + * + * Finite queues use the `strategy` option. The default `"suspend"` strategy + * applies backpressure, while `"dropping"` and `"sliding"` can discard output + * elements when the queue is full. `"unbounded"` capacity does not use a finite + * capacity strategy. + * + * **Gotchas** + * + * Dropping and sliding strategies can lose output elements under backpressure. + * + * @see {@link bufferArray} for buffering elements from array outputs + * + * @category Buffering + * @since 2.0.0 + */ +export const buffer: { + ( + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): ( + self: Channel + ) => Channel + ( + self: Channel, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Channel +} = dual(2, ( + self: Channel, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } +): Channel => + fromTransform(Effect.fnUntraced(function*(upstream, scope) { + const pull = yield* toTransform(self)(upstream, scope) + const queue = yield* Queue.make>({ + capacity: options.capacity === "unbounded" ? undefined : options.capacity, + strategy: options.capacity === "unbounded" ? undefined : options.strategy + }) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + yield* pull.pipe( + Effect.flatMap((value) => Queue.offer(queue, value)), + Effect.forever({ disableYield: true }), + Effect.onError((cause) => Queue.failCause(queue, cause)), + Effect.forkIn(scope) + ) + return Queue.take(queue) + }))) + +/** + * Buffers array output elements in a queue with the configured `capacity` so a + * faster producer can progress independently of a slower consumer. + * + * **When to use** + * + * Use when emitted arrays are batches of elements and it is acceptable for + * buffering to flatten and rebuild those batches. + * + * **Details** + * + * Finite queues use the `strategy` option. The default `"suspend"` strategy + * applies backpressure, while `"dropping"` and `"sliding"` can discard output + * elements when the queue is full. `"unbounded"` capacity does not use a finite + * capacity strategy. + * + * **Gotchas** + * + * Input arrays are offered to the queue element-by-element and outputs are + * rebuilt from the currently available queued elements, so upstream array + * boundaries are not preserved. + * + * @see {@link buffer} for buffering output elements without flattening arrays + * + * @category Buffering + * @since 4.0.0 + */ +export const bufferArray: { + ( + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ) => Channel, OutErr, OutDone, InElem, InErr, InDone, Env> + ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> +} = dual(2, ( + self: Channel, OutErr, OutDone, InElem, InErr, InDone, Env>, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } +): Channel, OutErr, OutDone, InElem, InErr, InDone, Env> => + fromTransform(Effect.fnUntraced(function*(upstream, scope) { + const pull = yield* toTransform(self)(upstream, scope) + const queue = yield* Queue.make>({ + capacity: options.capacity === "unbounded" ? undefined : options.capacity, + strategy: options.capacity === "unbounded" ? undefined : options.strategy + }) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + yield* pull.pipe( + Effect.flatMap((value) => Queue.offerAll(queue, value)), + Effect.forever({ disableYield: true }), + Effect.onError((cause) => Queue.failCause(queue, cause)), + Effect.forkIn(scope) + ) + return Queue.takeAll(queue) + }))) + +/** + * Interrupts a channel when another effect completes. + * + * **When to use** + * + * Use to race channel execution against an external effect whose success can + * become the channel's done value. + * + * **Details** + * + * If the effect completes first, its success value becomes the returned + * channel's done value. If the channel completes first, the original channel's + * done value is preserved. + * + * @category interruption + * @since 2.0.0 + */ +export const interruptWhen: { + ( + effect: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + effect: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + effect: Effect.Effect +): Channel => + merge( + self, + fromPull(Effect.succeed(Effect.flatMap(effect, Cause.done))), + { haltStrategy: "either" } + )) + +/** + * Stops a channel when the specified effect completes or fails. + * + * **Details** + * + * If the effect completes before the channel is done, its success value becomes + * the returned channel's done value. If the effect fails, the returned channel + * fails with that error. If the channel completes first, the channel's done + * value is preserved. + * + * @category interruption + * @since 4.0.0 + */ +export const haltWhen: { + ( + effect: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + effect: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + effect: Effect.Effect +): Channel => + fromTransformBracket(Effect.fnUntraced(function*(upstream, scope, forkedScope) { + const pull = yield* toTransform(self)(upstream, scope) + let haltCause: Cause.Cause> | undefined = undefined + yield* effect.pipe( + Effect.catchCause((cause) => { + haltCause = cause + return Effect.void + }), + Effect.forkIn(forkedScope) + ) + return Effect.suspend((): Pull.Pull => + haltCause ? Effect.failCause(haltCause) : pull + ) + }))) + +/** + * Attaches a finalizer that runs only when the channel exits with failure. + * + * **Details** + * + * The finalizer receives the failure `Cause`. The original channel failure is + * preserved. The finalizer itself must not fail. + * + * @category error handling + * @since 4.0.0 + */ +export const onError: { + ( + finalizer: (cause: Cause.Cause) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + finalizer: (cause: Cause.Cause) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + finalizer: (cause: Cause.Cause) => Effect.Effect +): Channel => + onExit(self, (exit) => Exit.isFailure(exit) ? finalizer(exit.cause) : Effect.void)) + +/** + * Returns a channel with an exit-aware finalizer that is guaranteed to run once + * the channel begins execution, whether it succeeds or fails. + * + * **Example** (Running exit finalizers) + * + * ```ts + * import { Channel, Console, Data, Exit } from "effect" + * + * class ExitError extends Data.TaggedError("ExitError")<{ + * readonly stage: string + * }> {} + * + * // Create a channel + * const dataChannel = Channel.fromIterable([1, 2, 3]) + * + * // Attach exit handler + * const channelWithExit = Channel.onExit(dataChannel, (exit) => { + * if (Exit.isSuccess(exit)) { + * return Console.log(`Channel completed successfully with: ${exit.value}`) + * } else { + * return Console.log(`Channel failed with: ${exit.cause}`) + * } + * }) + * ``` + * + * @category resource management + * @since 4.0.0 + */ +export const onExit: { + ( + finalizer: (e: Exit.Exit) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + finalizer: (e: Exit.Exit) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + finalizer: (e: Exit.Exit) => Effect.Effect +): Channel => + fromTransformBracket((upstream, scope, forkedScope) => + Scope.addFinalizerExit(forkedScope, finalizer as any).pipe( + Effect.andThen(toTransform(self)(upstream, scope)) + ) + )) + +/** + * Runs an effect before the channel starts. + * + * **Details** + * + * The effect's successful value is ignored. If the effect fails, the returned + * channel fails before running the source channel. + * + * @category hooks + * @since 4.0.0 + */ +export const onStart: { + ( + onStart: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + onStart: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + onStart: Effect.Effect +): Channel => unwrap(Effect.as(onStart, self))) + +/** + * Runs an effect the first time the channel emits an output element. + * + * **When to use** + * + * Use when initialization depends on the first output element rather than only + * on channel startup. + * + * **Details** + * + * The effect receives the first emitted element. The first element is still + * emitted unchanged. The effect is not run if the channel completes without + * emitting an element. + * + * @category hooks + * @since 4.0.0 + */ +export const onFirst: { + ( + onFirst: (element: Types.NoInfer) => Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + onFirst: (element: Types.NoInfer) => Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + onFirst: (element: Types.NoInfer) => Effect.Effect +): Channel => + transformPull(self, (pull) => + Effect.sync(() => { + let isFirst = true + const pullFirst = Effect.tap(pull, (element) => { + isFirst = false + return onFirst(element) + }) + return Effect.suspend(() => isFirst ? pullFirst : pull) + }))) + +/** + * Runs an effect when the channel completes successfully. + * + * **Details** + * + * The effect runs before the original done value is propagated. The effect is + * not run when the channel fails. If the effect fails, the returned channel + * fails with that error. + * + * @category hooks + * @since 4.0.0 + */ +export const onEnd: { + ( + onEnd: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + onEnd: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + onEnd: Effect.Effect +): Channel => + transformPull(self, (pull) => + Effect.succeed(Pull.catchDone( + pull, + (leftover) => Effect.flatMap(onEnd, () => Cause.done(leftover as OutDone)) + )))) + +/** + * Returns a channel with a finalizer effect that is guaranteed to run once the + * channel begins execution, whether it succeeds or fails. + * + * **Example** (Ensuring cleanup runs) + * + * ```ts + * import { Channel, Console, Data } from "effect" + * + * class EnsureError extends Data.TaggedError("EnsureError")<{ + * readonly operation: string + * }> {} + * + * // Create a channel + * const dataChannel = Channel.fromIterable([1, 2, 3]) + * + * // Ensure cleanup always runs + * const channelWithCleanup = Channel.ensuring( + * dataChannel, + * Console.log("Cleanup executed regardless of success or failure") + * ) + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const ensuring: { + ( + finalizer: Effect.Effect + ): ( + self: Channel + ) => Channel + ( + self: Channel, + finalizer: Effect.Effect + ): Channel +} = dual(2, ( + self: Channel, + finalizer: Effect.Effect +): Channel => onExit(self, (_) => finalizer)) + +const runWith = < + OutElem, + OutErr, + OutDone, + Env, + EX, + RX, + AH = OutDone, + EH = never, + RH = never +>( + self: Channel, + f: (pull: Pull.Pull) => Effect.Effect, + onHalt?: (leftover: OutDone) => Effect.Effect +): Effect.Effect | EH, Env | RX | RH> => + Effect.suspend(() => { + const scope = Scope.makeUnsafe() + const makePull = toTransform(self)(Cause.done(), scope) + return Pull.catchDone(Effect.flatMap(makePull, f), onHalt ? onHalt : Effect.succeed as any).pipe( + Effect.onExit((exit) => Scope.close(scope, exit)) + ) as any + }) + +/** + * Creates a channel from the specified services. + * + * @category services + * @since 2.0.0 + */ +export const contextWith = ( + f: (context: Context.Context) => Channel +): Channel => + fromTransform((upstream, scope) => + Effect.contextWith((context: Context.Context) => toTransform(f(context))(upstream, scope)) + ) + +/** + * Provides a `Context` to the channel, removing the corresponding service + * requirements from the returned channel. + * + * @category services + * @since 2.0.0 + */ +export const provideContext: { + ( + context: Context.Context + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + context: Context.Context + ): Channel> +} = dual(2, ( + self: Channel, + context: Context.Context +): Channel> => + fromTransform((upstream, scope) => + Effect.map( + Effect.provideContext(toTransform(self)(upstream, scope), context), + Effect.provideContext(context) + ) + )) + +/** + * Provides a concrete service for a context key, removing that service + * requirement from the returned channel. + * + * @category services + * @since 2.0.0 + */ +export const provideService: { + ( + key: Context.Key, + service: NoInfer + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + key: Context.Key, + service: NoInfer + ): Channel> +} = dual(3, ( + self: Channel, + key: Context.Key, + service: NoInfer +): Channel> => + fromTransform((upstream, scope) => + Effect.map( + Effect.provideService(toTransform(self)(upstream, scope), key, service), + Effect.provideService(key, service) + ) + )) + +/** + * Provides a service to the channel after obtaining it from an effect. + * + * **When to use** + * + * Use to supply a channel dependency when constructing the service itself is + * effectful or can fail. + * + * **Details** + * + * If the service effect fails, the returned channel fails. The provided service + * removes the corresponding service requirement from the returned channel. + * + * @category services + * @since 4.0.0 + */ +export const provideServiceEffect: { + ( + key: Context.Key, + service: Effect.Effect, ES, RS> + ): ( + self: Channel + ) => Channel | RS> + ( + self: Channel, + key: Context.Key, + service: Effect.Effect, ES, RS> + ): Channel | RS> +} = dual(3, ( + self: Channel, + key: Context.Key, + service: Effect.Effect, ES, RS> +): Channel | RS> => + fromTransform((upstream, scope) => + Effect.flatMap( + service, + (s) => toTransform(provideService(self, key, s))(upstream, scope) + ) + )) + +/** + * Provides a `Layer` or `Context` to the channel, removing the corresponding + * service requirements. + * + * **Details** + * + * Providing a `Context` delegates to `provideContext`. Providing a `Layer` + * builds the layer in the channel scope. Use `options.local` to build a fresh + * layer instance for this provision. + * + * @category services + * @since 4.0.0 + */ +export const provide: { + ( + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined + ): ( + self: Channel + ) => Channel | R> + ( + self: Channel, + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined + ): Channel | R> +} = dual((args) => isChannel(args[0]), ( + self: Channel, + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined +): Channel | R> => + Context.isContext(layer) ? provideContext(self, layer) : fromTransform((upstream, scope) => + Effect.flatMap( + options?.local + ? Layer.buildWithMemoMap(layer, Layer.makeMemoMapUnsafe(), scope) + : Layer.buildWithScope(layer, scope), + (context) => + Effect.map( + Effect.provideContext(toTransform(self)(upstream, scope), context), + Effect.provideContext(context) + ) + ) + )) + +/** + * Transforms the current context before running the channel. + * + * **Details** + * + * The function receives the surrounding context and returns the context to + * provide to the channel. The returned channel requires the services needed to + * build that context. + * + * @category services + * @since 4.0.0 + */ +export const updateContext: { + ( + f: (context: Context.Context) => Context.Context + ): ( + self: Channel + ) => Channel + ( + self: Channel, + f: (context: Context.Context) => Context.Context + ): Channel +} = dual(2, ( + self: Channel, + f: (context: Context.Context) => Context.Context +): Channel => + fromTransform((upstream, scope) => + Effect.contextWith((context) => { + const toProvide = f(context) + return toTransform(provideContext(self, toProvide))(upstream, scope) + }) + )) + +/** + * Updates a service in the current context before running the channel. + * + * **Details** + * + * The existing service is read from the context. The updated service is + * provided to the channel under the same key. + * + * @category services + * @since 2.0.0 + */ +export const updateService: { + ( + key: Context.Key, + f: (service: NoInfer) => S + ): ( + self: Channel + ) => Channel + ( + self: Channel, + service: Context.Key, + f: (service: NoInfer) => S + ): Channel +} = dual(3, ( + self: Channel, + service: Context.Key, + f: (service: NoInfer) => S +): Channel => + updateContext(self, (context) => + Context.add( + context, + service, + f(Context.get(context, service)) + ))) + +/** + * Runs the channel inside a tracing span with the specified name and options. + * + * **Details** + * + * The created span is provided as the current parent span while the channel + * runs. The span is ended with the channel's exit value. + * + * @category tracing + * @since 2.0.0 + */ +export const withSpan: { + ( + name: string, + options?: SpanOptions + ): ( + self: Channel + ) => Channel> + ( + self: Channel, + name: string, + options?: SpanOptions + ): Channel> +} = function() { + const dataFirst = isChannel(arguments[0]) + const name = dataFirst ? arguments[1] : arguments[0] + const options = addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return withSpanImpl(self, name, options) + } + return (self: any) => withSpanImpl(self, name, options) +} as any + +const withSpanImpl = ( + self: Channel, + name: string, + options?: SpanOptions +): Channel> => + acquireUseRelease( + Effect.makeSpan(name, options), + (span) => provideService(self, ParentSpan, span), + (span, exit) => + Effect.withFiber((fiber) => { + const clock = fiber.getRef(ClockRef) + const timingEnabled = fiber.getRef(TracerTimingEnabled) + return endSpan(span, exit, clock, timingEnabled) + }) + ) + +/** + * The starting channel for Do notation, emitting an empty object. + * + * @category do notation + * @since 4.0.0 + */ +export const Do: Channel<{}> = succeed({}) + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): ( + self: Channel + ) => Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + R + > + ( + self: Channel, + name: Exclude, + f: (a: NoInfer) => B + ): Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + R + > +} = dual(3, ( + self: Channel, + name: Exclude, + f: (a: NoInfer) => B +): Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + R +> => + map(self, (elem) => (({ + ...elem, + [name]: f(elem) + }) as any))) +export { + /** + * Adds a computed field to each object emitted by a channel. + * + * @category do notation + * @since 4.0.0 + */ + let_ as let +} + +/** + * Adds a field to each object emitted by a channel by running another channel + * derived from that object. + * + * **Details** + * + * The field name must not already exist on the emitted object. The derived + * channel's output becomes the value of the new field. `options.concurrency` + * and `options.bufferSize` control how derived channels are flattened. + * + * @category do notation + * @since 4.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): ( + self: Channel + ) => Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr2 | OutErr, + OutDone, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env2 | Env + > + < + OutElem extends object, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + N extends string, + B, + OutErr2, + OutDone2, + InElem2, + InErr2, + InDone2, + Env2 + >( + self: Channel, + name: Exclude, + f: (a: NoInfer) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } + ): Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr2 | OutErr, + OutDone, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env2 | Env + > +} = dual((args) => isChannel(args[0]), < + OutElem extends object, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env, + N extends string, + B, + OutErr2, + OutDone2, + InElem2, + InErr2, + InDone2, + Env2 +>( + self: Channel, + name: Exclude, + f: (a: NoInfer) => Channel, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } +): Channel< + { [K in N | keyof OutElem]: K extends keyof OutElem ? OutElem[K] : B }, + OutErr2 | OutErr, + OutDone, + InElem & InElem2, + InErr & InErr2, + InDone & InDone2, + Env2 | Env +> => + flatMap( + self, + (elem) => map(f(elem), (b) => ({ ...elem, [name]: b } as any)), + options + )) + +/** + * Wraps each output element in an object under the specified field name. + * + * **When to use** + * + * Use when you need to start a Channel Do-notation chain from an existing + * output value by assigning that value to a field name. + * + * @see {@link Do} for starting Do notation from an empty object + * @see {@link bind} for adding a field produced by another channel + * @see {@link let_ let} for adding a computed field + * + * @category do notation + * @since 4.0.0 + */ +export const bindTo: { + (name: N): ( + self: Channel + ) => Channel< + { [K in N]: OutElem }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + > + ( + self: Channel, + name: N + ): Channel< + { [K in N]: OutElem }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env + > +} = dual(2, ( + self: Channel, + name: N +): Channel< + { [K in N]: OutElem }, + OutErr, + OutDone, + InElem, + InErr, + InDone, + Env +> => map(self, (elem) => ({ [name]: elem } as any))) + +/** + * Runs a channel and counts the number of elements it outputs. + * + * **Example** (Counting channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class CountError extends Data.TaggedError("CountError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel with multiple elements + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Count the elements + * const countEffect = Channel.runCount(numbersChannel) + * + * // Effect.runSync(countEffect) // Returns: 5 + * ``` + * + * @category execution + * @since 4.0.0 + */ +export const runCount = ( + self: Channel +): Effect.Effect => runFold(self, () => 0, (acc) => acc + 1) + +/** + * Runs a channel and discards all output elements, returning only the final result. + * + * **Example** (Draining channel output at runtime) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class DrainError extends Data.TaggedError("DrainError")<{ + * readonly stage: string + * }> {} + * + * // Create a channel that outputs elements and completes with a result + * const resultChannel = Channel.fromIterable([1, 2, 3]) + * const completedChannel = Channel.concatWith( + * resultChannel, + * () => Channel.succeed("completed") + * ) + * + * // Drain all elements and get only the final result + * const drainEffect = Channel.runDrain(completedChannel) + * + * // Effect.runSync(drainEffect) // Returns: "completed" + * ``` + * + * @category execution + * @since 2.0.0 + */ +export const runDrain = ( + self: Channel +): Effect.Effect => runWith(self, (pull) => Effect.forever(pull, { disableYield: true })) + +/** + * Runs a channel and applies an effect to each output element. + * + * **Example** (Running effects for each output) + * + * ```ts + * import { Channel, Console, Data } from "effect" + * + * class ForEachError extends Data.TaggedError("ForEachError")<{ + * readonly element: unknown + * }> {} + * + * // Create a channel with numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3]) + * + * // Run forEach to log each element + * const forEachEffect = Channel.runForEach( + * numbersChannel, + * (n) => Console.log(`Processing: ${n}`) + * ) + * + * // Logs: "Processing: 1", "Processing: 2", "Processing: 3" + * ``` + * + * @category execution + * @since 4.0.0 + */ +export const runForEach: { + ( + f: (o: OutElem) => Effect.Effect + ): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + f: (o: OutElem) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Channel, + f: (o: OutElem) => Effect.Effect + ): Effect.Effect => + runWith(self, (pull) => Effect.forever(Effect.flatMap(pull, f), { disableYield: true })) +) + +/** + * Runs a channel and applies an effectful predicate to each output element + * until the predicate returns `false`. + * + * **Details** + * + * Returning `true` continues consuming the channel. Returning `false` stops + * consumption early. The returned effect completes with `void`. + * + * @category execution + * @since 4.0.0 + */ +export const runForEachWhile: { + ( + f: (o: OutElem) => Effect.Effect + ): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + f: (o: OutElem) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Channel, + f: (o: OutElem) => Effect.Effect + ): Effect.Effect => + runWith(self, (pull) => + pull.pipe( + Effect.flatMap(f), + Effect.flatMap((cont) => (cont ? Effect.void : Cause.done())), + Effect.forever({ disableYield: true }) + )) +) + +/** + * Runs a channel and collects all output elements into an array. + * + * **Example** (Collecting channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class CollectError extends Data.TaggedError("CollectError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel with elements + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Collect all elements into an array + * const collectEffect = Channel.runCollect(numbersChannel) + * + * // Effect.runSync(collectEffect) // Returns: [1, 2, 3, 4, 5] + * ``` + * + * @category execution + * @since 2.0.0 + */ +export const runCollect = ( + self: Channel +): Effect.Effect, OutErr, Env> => + runFold(self, () => [] as Array, (acc, o) => { + acc.push(o) + return acc + }) + +/** + * Runs a channel and outputs the done value. + * + * @category execution + * @since 4.0.0 + */ +export const runDone = ( + self: Channel +): Effect.Effect => runWith(self, identity_, Effect.succeed) + +/** + * Runs a channel until the first output element is available, returning it in + * an `Option`. + * + * **Details** + * + * Returns `Option.some` with the first output element, or `Option.none` if the + * channel completes without emitting output. + * + * @category execution + * @since 4.0.0 + */ +export const runHead = ( + self: Channel +): Effect.Effect, OutErr, Env> => + Effect.suspend(() => { + let head = Option.none() + return runWith(self, (pull) => + pull.pipe( + Effect.asSome, + Effect.flatMap((head_) => { + head = head_ + return Cause.done() + }) + ), () => Effect.succeed(head)) + }) + +/** + * Runs a channel to completion and returns the last output element in an + * `Option`. + * + * **Details** + * + * Returns `Option.some` with the last emitted element, or `Option.none` if the + * channel completes without emitting output. + * + * @category execution + * @since 4.0.0 + */ +export const runLast = ( + self: Channel +): Effect.Effect, OutErr, Env> => + Effect.suspend(() => { + const absent = Symbol() // Prevent boxing + let last: typeof absent | OutElem = absent + return runWith( + self, + (pull) => + Effect.forever( + Effect.flatMap(pull, (item) => { + last = item + return Effect.void + }), + { disableYield: true } + ), + () => last === absent ? Effect.succeedNone : Effect.succeedSome(last) + ) + }) + +/** + * Runs a channel and folds over all output elements with an accumulator. + * + * **Example** (Folding channel output) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class FoldError extends Data.TaggedError("FoldError")<{ + * readonly operation: string + * }> {} + * + * // Create a channel with numbers + * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Fold to calculate sum + * const sumEffect = Channel.runFold(numbersChannel, () => 0, (acc, n) => acc + n) + * + * // Effect.runSync(sumEffect) // Returns: 15 + * ``` + * + * @category execution + * @since 4.0.0 + */ +export const runFold: { + ( + initial: LazyArg, + f: (acc: Z, o: OutElem) => Z + ): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + initial: LazyArg, + f: (acc: Z, o: OutElem) => Z + ): Effect.Effect +} = dual(3, ( + self: Channel, + initial: LazyArg, + f: (acc: Z, o: OutElem) => Z +): Effect.Effect => + Effect.suspend(() => { + let state = initial() + return runWith( + self, + (pull) => + Effect.whileLoop({ + while: constTrue, + body: () => pull, + step: (value) => { + state = f(state, value) + } + }), + () => Effect.succeed(state) + ) + })) + +/** + * Runs a channel and effectfully folds all output elements with an accumulator. + * + * **When to use** + * + * Use when folding channel output needs effects, services, or an additional + * failure channel during accumulation. + * + * **Details** + * + * The initial accumulator is evaluated lazily. Each output element is passed to + * the effectful accumulator function. The returned effect succeeds with the + * final accumulator value. + * + * @category execution + * @since 4.0.0 + */ +export const runFoldEffect: { + ( + initial: LazyArg, + f: (acc: Z, o: OutElem) => Effect.Effect + ): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + initial: LazyArg, + f: (acc: Z, o: OutElem) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Channel, + initial: LazyArg, + f: (acc: Z, o: OutElem) => Effect.Effect +): Effect.Effect => + Effect.suspend(() => { + let state = initial() + return runWith( + self, + (pull) => + Effect.whileLoop({ + while: constTrue, + body: constant(pull.pipe( + Effect.flatMap((o) => f(state, o)), + Effect.map((s) => { + state = s + }) + )), + step: constVoid + }), + () => Effect.succeed(state) + ) + })) + +/** + * Converts a channel to a scoped `Pull` for low-level consumption. + * + * **Details** + * + * The effect requires a `Scope`. The returned pull should be consumed only + * while that scope remains open. Pulls are serialized so only one pull is + * evaluated at a time. + * + * **Example** (Converting channels to pulls) + * + * ```ts + * import { Channel, Data, Effect } from "effect" + * + * class PullError extends Data.TaggedError("PullError")<{ + * readonly step: string + * }> {} + * + * // Create a channel + * const numbersChannel = Channel.fromIterable([1, 2, 3]) + * + * // Convert to Pull within a scope + * const pullEffect = Effect.scoped( + * Channel.toPull(numbersChannel) + * ) + * + * // Use the Pull to manually consume elements + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toPull: ( + self: Channel +) => Effect.Effect< + Pull.Pull, + never, + Env | Scope.Scope +> = Effect.fnUntraced( + function*( + self: Channel + ) { + const semaphore = Semaphore.makeUnsafe(1) + const context = yield* Effect.context() + const scope = Context.get(context, Scope.Scope) + const pull = yield* toTransform(self)(Cause.done(), scope) + return pull.pipe( + Effect.provideContext(context), + semaphore.withPermits(1) + ) + }, + // ensure errors are redirected to the pull effect + Effect.catchCause((cause) => Effect.succeed(Effect.failCause(cause))) +) as any + +/** + * Converts a channel to a Pull within an existing scope. + * + * **Example** (Converting channels to scoped pulls) + * + * ```ts + * import { Channel, Data, Effect, Scope } from "effect" + * + * class ScopedPullError extends Data.TaggedError("ScopedPullError")<{ + * readonly reason: string + * }> {} + * + * // Create a channel + * const numbersChannel = Channel.fromIterable([1, 2, 3]) + * + * // Convert to Pull with explicit scope + * const scopedPullEffect = Effect.gen(function*() { + * const scope = yield* Scope.make() + * const pull = yield* Channel.toPullScoped(numbersChannel, scope) + * return pull + * }) + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toPullScoped = ( + self: Channel, + scope: Scope.Scope +): Effect.Effect, never, Env> => toTransform(self)(Cause.done(), scope) + +/** + * Runs a channel and offers each output element into a queue. + * + * **Details** + * + * When the channel completes, the queue is ended. When the channel fails, the + * queue is failed with the channel's cause. The returned effect itself + * completes with `void`. + * + * @category destructors + * @since 4.0.0 + */ +export const runIntoQueue: { + (queue: Queue.Queue): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + queue: Queue.Queue + ): Effect.Effect +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + queue: Queue.Queue + ): Effect.Effect => + Effect.uninterruptibleMask((restore) => + runForEach(self, (value) => Queue.offer(queue, value)).pipe( + restore, + Effect.exit, + Effect.flatMap((exit) => { + if (Exit.isSuccess(exit)) { + Queue.endUnsafe(queue) + } else { + Queue.failCauseUnsafe(queue, exit.cause) + } + return Effect.void + }) + ) + ) +) + +/** + * Runs a channel that emits non-empty arrays and offers each array element into + * a queue. + * + * **Details** + * + * When the channel completes, the queue is ended. When the channel fails, the + * queue is failed with the channel's cause. The returned effect itself + * completes with `void`. + * + * @category destructors + * @since 4.0.0 + */ +export const runIntoQueueArray: { + (queue: Queue.Queue): ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> + ) => Effect.Effect + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + queue: Queue.Queue + ): Effect.Effect +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + queue: Queue.Queue + ): Effect.Effect => + Effect.uninterruptibleMask((restore) => + runForEach(self, (value) => Queue.offerAll(queue, value)).pipe( + restore, + Effect.exit, + Effect.flatMap((exit) => { + if (Exit.isSuccess(exit)) { + Queue.endUnsafe(queue) + } else { + Queue.failCauseUnsafe(queue, exit.cause) + } + return Effect.void + }) + ) + ) +) + +/** + * Creates a scoped queue and forks the channel to feed it for concurrent + * consumption. + * + * **Details** + * + * Output elements are offered to the queue. Channel completion and failure are + * signaled through the queue. The queue is shut down when the surrounding scope + * closes. + * + * **Example** (Converting channels to queues) + * + * ```ts + * import { Channel, Data } from "effect" + * + * class QueueError extends Data.TaggedError("QueueError")<{ + * readonly operation: string + * }> {} + * + * // Create a channel with data + * const dataChannel = Channel.fromIterable([1, 2, 3, 4, 5]) + * + * // Convert to queue for concurrent processing + * const queueEffect = Channel.toQueue(dataChannel, { capacity: 32 }) + * + * // The queue can be used for concurrent consumption + * // Multiple consumers can read from the queue + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toQueue: { + ( + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): ( + self: Channel + ) => Effect.Effect, never, Env | Scope.Scope> + ( + self: Channel, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Effect.Effect, never, Env | Scope.Scope> +} = dual( + (args) => isChannel(args[0]), + Effect.fnUntraced(function*( + self: Channel, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ) { + const scope = yield* Effect.scope + const queue = yield* Queue.make({ + capacity: typeof options.capacity === "number" ? options.capacity : undefined, + strategy: typeof options.capacity === "number" ? options.strategy : undefined + }) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + yield* Effect.forkIn(runIntoQueue(self, queue), scope) + return queue + }) +) + +/** + * Creates a scoped queue and forks an array-emitting channel to feed it. + * + * **Details** + * + * Each element inside emitted non-empty arrays is offered to the queue. Channel + * completion and failure are signaled through the queue. The queue is shut down + * when the surrounding scope closes. + * + * @category destructors + * @since 4.0.0 + */ +export const toQueueArray: { + ( + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> + ) => Effect.Effect, never, Env | Scope.Scope> + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Effect.Effect, never, Env | Scope.Scope> +} = dual( + (args) => isChannel(args[0]), + Effect.fnUntraced(function*( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ) { + const scope = yield* Effect.scope + const queue = yield* Queue.make({ + capacity: typeof options.capacity === "number" ? options.capacity : undefined, + strategy: typeof options.capacity === "number" ? options.strategy : undefined + }) + yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) + yield* Effect.forkIn(runIntoQueueArray(self, queue), scope) + return queue + }) +) + +/** + * Converts a channel to a PubSub for concurrent consumption. + * + * **Details** + * + * `shutdownOnEnd` indicates whether the PubSub should be shut down when the + * channel ends. By default this is `true`. + * + * @category destructors + * @since 2.0.0 + */ +export const toPubSub: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): ( + self: Channel + ) => Effect.Effect, never, Env | Scope.Scope> + ( + self: Channel, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): Effect.Effect, never, Env | Scope.Scope> +} = dual( + 2, + Effect.fnUntraced(function*( + self: Channel, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ) { + const pubsub = yield* makePubSub(options) + yield* Effect.forkScoped(runIntoPubSub(self, pubsub, { + shutdownOnEnd: options.shutdownOnEnd !== false + })) + return pubsub + }) +) + +/** + * Runs a channel and publishes each output element to a `PubSub`. + * + * **Details** + * + * The channel's output values are published as individual PubSub messages. Use + * `options.shutdownOnEnd` to shut down the PubSub when channel execution ends. + * + * @category destructors + * @since 4.0.0 + */ +export const runIntoPubSub: { + ( + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): ( + self: Channel + ) => Effect.Effect + ( + self: Channel, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ) => + runForEach(self, (value) => PubSub.publish(pubsub, value)).pipe( + options?.shutdownOnEnd === true ? Effect.ensuring(PubSub.shutdown(pubsub)) : identity_ + ) +) + +const makePubSub = ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } +) => + Effect.acquireRelease( + options.capacity === "unbounded" + ? PubSub.unbounded(options) + : options.strategy === "dropping" + ? PubSub.dropping(options) + : options.strategy === "sliding" + ? PubSub.sliding(options) + : PubSub.bounded(options), + PubSub.shutdown + ) + +/** + * Converts an array-emitting channel to a scoped `PubSub` for concurrent + * consumption. + * + * **Details** + * + * Each element inside emitted non-empty arrays is published as an individual + * PubSub message. `shutdownOnEnd` indicates whether the PubSub should be shut + * down when the channel ends. By default this is `true`. + * + * @category destructors + * @since 4.0.0 + */ +export const toPubSubArray: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> + ) => Effect.Effect, never, Env | Scope.Scope> + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): Effect.Effect, never, Env | Scope.Scope> +} = dual( + 2, + Effect.fnUntraced(function*( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ) { + const pubsub = yield* makePubSub(options) + yield* Effect.forkScoped(runIntoPubSubArray(self, pubsub, { + shutdownOnEnd: options.shutdownOnEnd !== false + })) + return pubsub + }) +) + +/** + * Runs an array-emitting channel and publishes each array element to a + * `PubSub`. + * + * **Details** + * + * Each element inside emitted non-empty arrays is published as an individual + * PubSub message. Use `options.shutdownOnEnd` to shut down the PubSub when + * channel execution ends. + * + * @category destructors + * @since 4.0.0 + */ +export const runIntoPubSubArray: { + ( + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> + ) => Effect.Effect + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual( + (args) => isChannel(args[0]), + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ) => + runForEach(self, (value) => PubSub.publishAll(pubsub, value)).pipe( + options?.shutdownOnEnd === true ? Effect.ensuring(PubSub.shutdown(pubsub)) : identity_ + ) +) + +/** + * Converts a channel to a scoped `PubSub` of `Take` values. + * + * **Details** + * + * Emitted non-empty arrays are published as output `Take` values. When the + * channel ends, its final `Exit` is published so subscribers can observe + * completion or failure. + * + * @category destructors + * @since 4.0.0 + */ +export const toPubSubTake: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ): ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> + ) => Effect.Effect< + PubSub.PubSub>, + never, + Env | Scope.Scope + > + ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect< + PubSub.PubSub>, + never, + Env | Scope.Scope + > +} = dual( + 2, + Effect.fnUntraced(function*( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env>, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ) { + const pubsub = yield* makePubSub>(options) + yield* runForEach(self, (value) => PubSub.publish(pubsub, value)).pipe( + Effect.onExit((exit) => PubSub.publish(pubsub, exit)), + Effect.forkScoped + ) + return pubsub + }) +) diff --git a/repos/effect/packages/effect/src/ChannelSchema.ts b/repos/effect/packages/effect/src/ChannelSchema.ts new file mode 100644 index 0000000..fb2b486 --- /dev/null +++ b/repos/effect/packages/effect/src/ChannelSchema.ts @@ -0,0 +1,308 @@ +/** + * Schema adapters for `Channel` boundaries. The helpers encode typed channel + * chunks before they cross an encoded boundary, decode encoded chunks before + * application code receives them, and wrap bidirectional channels so callers + * work with schema-typed input and output while the inner channel uses encoded + * values. + * + * @since 4.0.0 + */ +import type * as Arr from "./Array.ts" +import * as Channel from "./Channel.ts" +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import * as Schema from "./Schema.ts" + +/** + * Creates a channel that encodes non-empty chunks of schema values into the + * schema's encoded representation. + * + * **When to use** + * + * Use to encode typed channel input into the schema's encoded representation + * before passing chunks to an encoded downstream boundary. + * + * **Details** + * + * Encoding failures are emitted as `SchemaError`, and any encoding services + * required by the schema become channel requirements. + * + * @see {@link encodeUnknown} for encoded output chunks that should be typed as `unknown` + * @see {@link decode} for the inverse channel that decodes encoded chunks into schema values + * + * @category constructors + * @since 4.0.0 + */ +export const encode = ( + schema: S +) => +(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | Schema.SchemaError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["EncodingServices"] +> => { + const encode = Schema.encodeEffect(Schema.NonEmptyArray(schema)) + return Channel.fromTransform((upstream, _scope) => Effect.succeed(Effect.flatMap(upstream, (chunk) => encode(chunk)))) +} + +/** + * Creates an `encode` channel variant whose encoded output chunks are typed as + * `unknown`. + * + * **When to use** + * + * Use when a channel boundary should encode typed input chunks while the encoded + * output representation is intentionally untyped. + * + * @see {@link encode} for the variant that preserves the schema encoded type + * + * @category constructors + * @since 4.0.0 + */ +export const encodeUnknown: ( + schema: S +) => () => Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | Schema.SchemaError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["EncodingServices"] +> = encode + +/** + * Creates a channel that decodes non-empty chunks from the schema's encoded + * representation into schema values. + * + * **When to use** + * + * Use to validate and decode encoded channel output into typed schema values + * before application code consumes it. + * + * **Details** + * + * Decoding failures are emitted as `SchemaError`, and any decoding services + * required by the schema become channel requirements. + * + * @see {@link decodeUnknown} for boundaries where the encoded input side is intentionally untyped + * @see {@link encode} for the inverse adapter that encodes typed schema values + * + * @category constructors + * @since 4.0.0 + */ +export const decode = ( + schema: S +) => +(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | Schema.SchemaError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["DecodingServices"] +> => { + const decode = Schema.decodeEffect(Schema.NonEmptyArray(schema)) + return Channel.fromTransform((upstream, _scope) => Effect.succeed(Effect.flatMap(upstream, (chunk) => decode(chunk)))) +} + +/** + * Creates a `decode` channel variant for schema-decoding channel boundaries. + * + * **When to use** + * + * Use when you need an intentionally unknown or untyped encoded input while + * keeping only the decoded output statically typed according to the schema. + * + * **Details** + * + * The channel decodes non-empty encoded chunks into schema values, emits + * `SchemaError` when decoding fails, and requires the schema's decoding + * services. + * + * @see {@link decode} for the typed variant that preserves the schema's encoded type + * + * @category constructors + * @since 4.0.0 + */ +export const decodeUnknown: ( + schema: S +) => () => Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | Schema.SchemaError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["DecodingServices"] +> = decode + +/** + * Wraps a channel so callers work with typed input and output chunks while the + * wrapped channel uses encoded chunks. + * + * **When to use** + * + * Use to expose typed input and output at a bidirectional channel boundary + * while the wrapped channel continues to operate on schema-encoded chunks. + * + * **Details** + * + * Values sent into the resulting channel are encoded with `inputSchema` before + * reaching the wrapped channel. Values emitted by the wrapped channel are + * decoded with `outputSchema` before they are emitted downstream. Schema + * failures are surfaced as `SchemaError`. + * + * @see {@link duplexUnknown} for the variant whose encoded side is intentionally untyped + * @see {@link encode} for encoding typed chunks at one-way channel boundaries + * @see {@link decode} for decoding encoded chunks at one-way channel boundaries + * + * @category combinators + * @since 4.0.0 + */ +export const duplex: { + (options: { + readonly inputSchema: In + readonly outputSchema: Out + }): ( + self: Channel.Channel< + Arr.NonEmptyReadonlyArray, + OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | InErr, + InDone, + R + > + ) => Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + InErr, + InDone, + R | In["EncodingServices"] | Out["DecodingServices"] + > + ( + self: Channel.Channel< + Arr.NonEmptyReadonlyArray, + OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | InErr, + InDone, + R + >, + options: { + readonly inputSchema: In + readonly outputSchema: Out + } + ): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + InErr, + InDone, + R | In["EncodingServices"] | Out["DecodingServices"] + > +} = dual(2, ( + self: Channel.Channel< + Arr.NonEmptyReadonlyArray, + OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | InErr, + InDone, + R + >, + options: { + readonly inputSchema: In + readonly outputSchema: Out + } +): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + InErr, + InDone, + R | In["EncodingServices"] | Out["DecodingServices"] +> => + encode(options.inputSchema)().pipe( + Channel.pipeTo(self), + Channel.pipeTo(decode(options.outputSchema)()) + )) + +/** + * Wraps a bidirectional channel whose encoded chunks are typed as `unknown`. + * + * **When to use** + * + * Use when you need a bidirectional channel to cross an encoded boundary whose + * chunk types are intentionally erased, while callers send and receive + * schema-typed chunks. + * + * **Details** + * + * The resulting channel accepts typed input chunks, encodes them with + * `inputSchema`, decodes unknown output chunks with `outputSchema`, and + * surfaces schema failures as `SchemaError`. + * + * @see {@link duplex} for the variant that preserves the schema encoded types on the wrapped channel + * + * @category combinators + * @since 4.0.0 + */ +export const duplexUnknown: { + (options: { + readonly inputSchema: In + readonly outputSchema: Out + }): ( + self: Channel.Channel< + Arr.NonEmptyReadonlyArray, + OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | InErr, + InDone, + R + > + ) => Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + InErr, + InDone, + R | In["EncodingServices"] | Out["DecodingServices"] + > + ( + self: Channel.Channel< + Arr.NonEmptyReadonlyArray, + OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | InErr, + InDone, + R + >, + options: { + readonly inputSchema: In + readonly outputSchema: Out + } + ): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | OutErr, + OutDone, + Arr.NonEmptyReadonlyArray, + InErr, + InDone, + R | In["EncodingServices"] | Out["DecodingServices"] + > +} = duplex diff --git a/repos/effect/packages/effect/src/Chunk.ts b/repos/effect/packages/effect/src/Chunk.ts new file mode 100644 index 0000000..67b2f99 --- /dev/null +++ b/repos/effect/packages/effect/src/Chunk.ts @@ -0,0 +1,2976 @@ +/** + * Stores many values in an immutable ordered collection. + * + * A `Chunk` is useful when you need to build or transform collections + * without changing the original collection. It is designed for efficient + * append, prepend, and concatenation. This module includes helpers for + * creating, reading, slicing, mapping, filtering, sorting, zipping, combining, + * and converting chunks to and from arrays and iterables. + * + * @since 2.0.0 + */ +import * as RA from "./Array.ts" +import type { NonEmptyReadonlyArray } from "./Array.ts" +import * as Equal from "./Equal.ts" +import * as Equivalence from "./Equivalence.ts" +import type * as Filter from "./Filter.ts" +import { format } from "./Formatter.ts" +import { dual, identity, pipe } from "./Function.ts" +import * as Hash from "./Hash.ts" +import type { TypeLambda } from "./HKT.ts" +import { type Inspectable, NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { NonEmptyIterable } from "./NonEmptyIterable.ts" +import type { Option } from "./Option.ts" +import * as O from "./Option.ts" +import * as Order from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty, type Predicate, type Refinement } from "./Predicate.ts" +import * as R from "./Result.ts" +import type { Result } from "./Result.ts" +import type { Covariant, NoInfer } from "./Types.ts" + +const TypeId = "~effect/collections/Chunk" + +/** + * A Chunk is an immutable, ordered collection optimized for efficient concatenation and access patterns. + * + * **Example** (Inspecting chunk values) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk: Chunk.Chunk = Chunk.make(1, 2, 3) + * console.log(chunk.length) // 3 + * console.log(Chunk.toArray(chunk)) // [1, 2, 3] + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Chunk extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _A: Covariant + } + readonly length: number + right: Chunk + left: Chunk + backing: Backing + depth: number +} + +/** + * A non-empty Chunk guaranteed to contain at least one element. + * + * **Example** (Working with non-empty chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const nonEmptyChunk: Chunk.NonEmptyChunk = Chunk.make(1, 2, 3) + * console.log(Chunk.headNonEmpty(nonEmptyChunk)) // 1 + * console.log(Chunk.lastNonEmpty(nonEmptyChunk)) // 3 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface NonEmptyChunk extends Chunk, NonEmptyIterable {} + +/** + * Type lambda for Chunk, used for higher-kinded type operations. + * + * **Example** (Applying the Chunk type lambda) + * + * ```ts + * import type { Chunk, HKT } from "effect" + * + * // Create a Chunk type using the type lambda + * type NumberChunk = HKT.Kind + * // Equivalent to: Chunk + * ``` + * + * @category type lambdas + * @since 2.0.0 + */ +export interface ChunkTypeLambda extends TypeLambda { + readonly type: Chunk +} + +type Backing = + | IArray + | IConcat + | ISingleton + | IEmpty + | ISlice + +interface IArray { + readonly _tag: "IArray" + readonly array: ReadonlyArray +} + +interface IConcat { + readonly _tag: "IConcat" + readonly left: Chunk + readonly right: Chunk +} + +interface ISingleton { + readonly _tag: "ISingleton" + readonly a: A +} + +interface IEmpty { + readonly _tag: "IEmpty" +} + +interface ISlice { + readonly _tag: "ISlice" + readonly chunk: Chunk + readonly offset: number + readonly length: number +} + +function copy( + src: ReadonlyArray, + srcPos: number, + dest: Array, + destPos: number, + len: number +) { + for (let i = srcPos; i < Math.min(src.length, srcPos + len); i++) { + dest[destPos + i - srcPos] = src[i]! + } + return dest +} + +const emptyArray: ReadonlyArray = [] + +/** + * Creates an `Equivalence` for chunks that compares chunk lengths and then + * compares corresponding elements with the provided element equivalence. + * + * **Example** (Comparing chunks for equivalence) + * + * ```ts + * import { Chunk, Equivalence } from "effect" + * + * const chunk1 = Chunk.make(1, 2, 3) + * const chunk2 = Chunk.make(1, 2, 3) + * const chunk3 = Chunk.make(1, 2, 4) + * + * const eq = Chunk.makeEquivalence(Equivalence.strictEqual()) + * console.log(eq(chunk1, chunk2)) // true + * console.log(eq(chunk1, chunk3)) // false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((self, that) => + self.length === that.length && toReadonlyArray(self).every((value, i) => isEquivalent(value, getUnsafe(that, i))) + ) + +const _equivalence = makeEquivalence(Equal.equals) + +const ChunkProto: Omit, "backing" | "depth" | "left" | "length" | "right"> = { + [TypeId]: { + _A: (_: never) => _ + }, + toString(this: Chunk) { + return `Chunk(${format(toReadonlyArray(this))})` + }, + toJSON(this: Chunk) { + return { + _id: "Chunk", + values: toJson(toReadonlyArray(this)) + } + }, + [NodeInspectSymbol](this: Chunk) { + return this.toJSON() + }, + [Equal.symbol](this: Chunk, that: unknown): boolean { + return isChunk(that) && _equivalence(this, that) + }, + [Hash.symbol](this: Chunk): number { + return Hash.array(toReadonlyArray(this)) + }, + [Symbol.iterator](this: Chunk): Iterator { + switch (this.backing._tag) { + case "IArray": { + return this.backing.array[Symbol.iterator]() + } + case "IEmpty": { + return emptyArray[Symbol.iterator]() + } + default: { + return toReadonlyArray(this)[Symbol.iterator]() + } + } + }, + pipe(this: Chunk) { + return pipeArguments(this, arguments) + } +} + +const makeChunk = (backing: Backing): Chunk => { + const chunk = Object.create(ChunkProto) + chunk.backing = backing + switch (backing._tag) { + case "IEmpty": { + chunk.length = 0 + chunk.depth = 0 + chunk.left = chunk + chunk.right = chunk + break + } + case "IConcat": { + chunk.length = backing.left.length + backing.right.length + chunk.depth = 1 + Math.max(backing.left.depth, backing.right.depth) + chunk.left = backing.left + chunk.right = backing.right + break + } + case "IArray": { + chunk.length = backing.array.length + chunk.depth = 0 + chunk.left = _empty + chunk.right = _empty + break + } + case "ISingleton": { + chunk.length = 1 + chunk.depth = 0 + chunk.left = _empty + chunk.right = _empty + break + } + case "ISlice": { + chunk.length = backing.length + chunk.depth = backing.chunk.depth + 1 + chunk.left = _empty + chunk.right = _empty + break + } + } + return chunk +} + +/** + * Checks whether `u` is a `Chunk` + * + * **Example** (Checking for chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const array = [1, 2, 3] + * + * console.log(Chunk.isChunk(chunk)) // true + * console.log(Chunk.isChunk(array)) // false + * console.log(Chunk.isChunk("string")) // false + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const isChunk: { + (u: Iterable): u is Chunk + (u: unknown): u is Chunk +} = (u: unknown): u is Chunk => hasProperty(u, TypeId) + +const _empty = makeChunk({ _tag: "IEmpty" }) + +/** + * Creates an empty `Chunk`. + * + * **Example** (Creating an empty chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const emptyChunk = Chunk.empty() + * console.log(Chunk.size(emptyChunk)) // 0 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => Chunk = () => _empty + +/** + * Builds a `NonEmptyChunk` from an non-empty collection of elements. + * + * **Example** (Creating a non-empty chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = ]>(...as: As): NonEmptyChunk => + fromNonEmptyArrayUnsafe(as) + +/** + * Builds a `NonEmptyChunk` from a single element. + * + * **Example** (Creating a single-element chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.of("hello") + * console.log(Chunk.toArray(chunk)) // ["hello"] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): NonEmptyChunk => makeChunk({ _tag: "ISingleton", a }) as any + +/** + * Creates a new `Chunk` from an iterable collection of values. + * + * **Example** (Creating chunks from iterables) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.fromIterable([1, 2, 3]) + * console.log(Chunk.toArray(chunk)) // [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (self: Iterable): Chunk => + isChunk(self) ? self : fromArrayUnsafe(RA.fromIterable(self)) + +const copyToArray = (self: Chunk, array: Array, initial: number): void => { + switch (self.backing._tag) { + case "IArray": { + copy(self.backing.array, 0, array, initial, self.length) + break + } + case "IConcat": { + copyToArray(self.left, array, initial) + copyToArray(self.right, array, initial + self.left.length) + break + } + case "ISingleton": { + array[initial] = self.backing.a + break + } + case "ISlice": { + let i = 0 + let j = initial + while (i < self.length) { + array[j] = getUnsafe(self, i) + i += 1 + j += 1 + } + break + } + } +} + +const toArray_ = (self: Chunk): Array => toReadonlyArray(self).slice() + +/** + * Converts a `Chunk` into an `Array`. If the provided `Chunk` is non-empty + * (`NonEmptyChunk`), the function will return a `NonEmptyArray`, ensuring the + * non-empty property is preserved. + * + * **Example** (Converting chunks to mutable arrays) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const array = Chunk.toArray(chunk) + * console.log(array) // [1, 2, 3] + * console.log(Array.isArray(array)) // true + * + * // With empty chunk + * const emptyChunk = Chunk.empty() + * console.log(Chunk.toArray(emptyChunk)) // [] + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const toArray: >( + self: S +) => S extends NonEmptyChunk ? RA.NonEmptyArray> : Array> = toArray_ as any + +const toReadonlyArray_ = (self: Chunk): ReadonlyArray => { + switch (self.backing._tag) { + case "IEmpty": { + return emptyArray + } + case "IArray": { + return self.backing.array + } + default: { + const arr = new Array(self.length) + copyToArray(self, arr, 0) + self.backing = { + _tag: "IArray", + array: arr + } + self.left = _empty + self.right = _empty + self.depth = 0 + return arr + } + } +} + +/** + * Converts a `Chunk` into a `ReadonlyArray`. If the provided `Chunk` is + * non-empty (`NonEmptyChunk`), the function will return a + * `NonEmptyReadonlyArray`, ensuring the non-empty property is preserved. + * + * **Example** (Converting chunks to readonly arrays) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const readonlyArray = Chunk.toReadonlyArray(chunk) + * console.log(readonlyArray) // [1, 2, 3] + * + * // The result is read-only, modifications would cause TypeScript errors + * // readonlyArray[0] = 10 // TypeScript error + * + * // With empty chunk + * const emptyChunk = Chunk.empty() + * console.log(Chunk.toReadonlyArray(emptyChunk)) // [] + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const toReadonlyArray: >( + self: S +) => S extends NonEmptyChunk ? RA.NonEmptyReadonlyArray> : ReadonlyArray> = + toReadonlyArray_ as any + +const reverseChunk = (self: Chunk): Chunk => { + switch (self.backing._tag) { + case "IEmpty": + case "ISingleton": + return self + case "IArray": { + return makeChunk({ _tag: "IArray", array: RA.reverse(self.backing.array) }) + } + case "IConcat": { + return makeChunk({ _tag: "IConcat", left: reverse(self.backing.right), right: reverse(self.backing.left) }) + } + case "ISlice": + return fromArrayUnsafe(RA.reverse(toReadonlyArray(self))) + } +} + +/** + * Reverses the order of elements in a `Chunk`. + * + * **When to use** + * + * Use to read or process chunk elements in reverse order. + * + * **Details** + * + * If the input chunk is a `NonEmptyChunk`, the reversed chunk is also a + * `NonEmptyChunk`. + * + * **Example** (Reversing chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const result = Chunk.reverse(chunk) + * + * console.log(Chunk.toArray(result)) // [3, 2, 1] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const reverse: >(self: S) => Chunk.With> = reverseChunk as any + +/** + * Gets the value at an index in a `Chunk` safely, returning `None` when the index is + * out of bounds. + * + * **Example** (Accessing elements safely) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make("a", "b", "c", "d") + * + * console.log(Chunk.get(chunk, 1)) // Option.some("b") + * console.log(Chunk.get(chunk, 10)) // Option.none() + * console.log(Chunk.get(chunk, -1)) // Option.none() + * + * // Using pipe syntax + * const result = chunk.pipe(Chunk.get(2)) + * console.log(result) // Option.some("c") + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const get: { + (index: number): (self: Chunk) => Option + (self: Chunk, index: number): Option +} = dual( + 2, + (self: Chunk, index: number): Option => + index < 0 || index >= self.length ? O.none() : O.some(getUnsafe(self, index)) +) + +/** + * Wraps an array into a chunk without copying. + * + * **When to use** + * + * Use when the input array can be shared with the resulting `Chunk` and avoiding + * a copy matters. + * + * **Gotchas** + * + * Mutating the source array after wrapping can mutate the resulting `Chunk`. + * + * **Example** (Creating chunks without copying arrays) + * + * ```ts + * import { Chunk } from "effect" + * + * const array = [1, 2, 3, 4, 5] + * const chunk = Chunk.fromArrayUnsafe(array) + * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * + * // Warning: Since this doesn't copy the array, mutations affect the chunk + * array[0] = 999 + * console.log(Chunk.toArray(chunk)) // [999, 2, 3, 4, 5] + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const fromArrayUnsafe = (self: ReadonlyArray): Chunk => + self.length === 0 ? empty() : self.length === 1 ? of(self[0]) : makeChunk({ _tag: "IArray", array: self }) + +/** + * Wraps a non-empty array into a non-empty chunk without copying. + * + * **When to use** + * + * Use when the input array is already known to be non-empty, can be shared with + * the resulting `Chunk`, and avoiding a copy matters. + * + * **Gotchas** + * + * Mutating the source array after wrapping can mutate the resulting `Chunk`. + * + * **Example** (Creating non-empty chunks without copying arrays) + * + * ```ts + * import { Array, Chunk } from "effect" + * + * const nonEmptyArray = Array.make(1, 2, 3, 4, 5) + * const chunk = Chunk.fromNonEmptyArrayUnsafe(nonEmptyArray) + * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * + * // The result is guaranteed to be non-empty + * console.log(Chunk.isNonEmpty(chunk)) // true + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const fromNonEmptyArrayUnsafe = (self: NonEmptyReadonlyArray): NonEmptyChunk => + fromArrayUnsafe(self) as any + +/** + * Gets an element at the specified index without returning an `Option`. + * + * **When to use** + * + * Use when reading from a `Chunk` at an index known to be in bounds and direct + * element access is preferred over handling `Option.none`. + * + * **Gotchas** + * + * Throws if the index is out of bounds. + * + * **Example** (Accessing elements unsafely) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make("a", "b", "c", "d") + * + * console.log(Chunk.getUnsafe(chunk, 1)) // "b" + * console.log(Chunk.getUnsafe(chunk, 3)) // "d" + * + * // Use Chunk.get when the index may be out of bounds + * console.log(Option.isNone(Chunk.get(chunk, 10))) // true + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const getUnsafe: { + (index: number): (self: Chunk) => A + (self: Chunk, index: number): A +} = dual(2, (self: Chunk, index: number): A => { + const i = Math.floor(index) + switch (self.backing._tag) { + case "IEmpty": { + throw new Error(`Index out of bounds: ${i}`) + } + case "ISingleton": { + if (index !== 0) { + throw new Error(`Index out of bounds: ${i}`) + } + return self.backing.a + } + case "IArray": { + if (i >= self.length || i < 0) { + throw new Error(`Index out of bounds: ${i}`) + } + return self.backing.array[i]! + } + case "IConcat": { + return i < self.left.length + ? getUnsafe(self.left, i) + : getUnsafe(self.right, i - self.left.length) + } + case "ISlice": { + return getUnsafe(self.backing.chunk, i + self.backing.offset) + } + } +}) + +/** + * Appends the specified element to the end of the `Chunk`. + * + * **When to use** + * + * Use to add one element after the existing chunk elements and return a + * `NonEmptyChunk`. + * + * **Example** (Appending an element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const newChunk = Chunk.append(chunk, 4) + * console.log(Chunk.toArray(newChunk)) // [1, 2, 3, 4] + * + * // Appending to empty chunk + * const emptyChunk = Chunk.empty() + * const singleElement = Chunk.append(emptyChunk, 42) + * console.log(Chunk.toArray(singleElement)) // [42] + * ``` + * + * @see {@link prepend} for adding one element before the existing elements + * @see {@link appendAll} for appending all elements from another chunk + * + * @category combining + * @since 2.0.0 + */ +export const append: { + (a: A2): (self: Chunk) => NonEmptyChunk + (self: Chunk, a: A2): NonEmptyChunk +} = dual(2, (self: Chunk, a: A2): NonEmptyChunk => appendAll(self, of(a))) + +/** + * Prepends an element to the front of a `Chunk`, creating a new `NonEmptyChunk`. + * + * **Example** (Prepending an element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(2, 3, 4) + * const newChunk = Chunk.prepend(chunk, 1) + * console.log(Chunk.toArray(newChunk)) // [1, 2, 3, 4] + * + * // Prepending to empty chunk + * const emptyChunk = Chunk.empty() + * const singleElement = Chunk.prepend(emptyChunk, "first") + * console.log(Chunk.toArray(singleElement)) // ["first"] + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const prepend: { + (elem: B): (self: Chunk) => NonEmptyChunk + (self: Chunk, elem: B): NonEmptyChunk +} = dual(2, (self: Chunk, elem: B): NonEmptyChunk => appendAll(of(elem), self)) + +/** + * Takes the first up to `n` elements from the chunk. + * + * **Example** (Taking elements from the start) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.take(chunk, 3) + * console.log(Chunk.toArray(result)) // [1, 2, 3] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => { + if (n <= 0) { + return _empty + } else if (n >= self.length) { + return self + } else { + switch (self.backing._tag) { + case "ISlice": { + return makeChunk({ + _tag: "ISlice", + chunk: self.backing.chunk, + length: n, + offset: self.backing.offset + }) + } + case "IConcat": { + if (n > self.left.length) { + return makeChunk({ + _tag: "IConcat", + left: self.left, + right: take(self.right, n - self.left.length) + }) + } + + return take(self.left, n) + } + default: { + return makeChunk({ + _tag: "ISlice", + chunk: self, + offset: 0, + length: n + }) + } + } + } +}) + +/** + * Drops the first up to `n` elements from the chunk. + * + * **Example** (Dropping elements from the start) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.drop(chunk, 2) + * console.log(Chunk.toArray(result)) // [3, 4, 5] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => { + if (n <= 0) { + return self + } else if (n >= self.length) { + return _empty + } else { + switch (self.backing._tag) { + case "ISlice": { + return makeChunk({ + _tag: "ISlice", + chunk: self.backing.chunk, + offset: self.backing.offset + n, + length: self.backing.length - n + }) + } + case "IConcat": { + if (n > self.left.length) { + return drop(self.right, n - self.left.length) + } + return makeChunk({ + _tag: "IConcat", + left: drop(self.left, n), + right: self.right + }) + } + default: { + return makeChunk({ + _tag: "ISlice", + chunk: self, + offset: n, + length: self.length - n + }) + } + } + } +}) + +/** + * Drops the last `n` elements. + * + * **Example** (Dropping elements from the end) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.dropRight(chunk, 2) + * console.log(Chunk.toArray(result)) // [1, 2, 3] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const dropRight: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => take(self, Math.max(0, self.length - n))) + +/** + * Drops all elements so long as the predicate returns true. + * + * **Example** (Dropping elements while a predicate matches) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.dropWhile(chunk, (n) => n < 3) + * console.log(Chunk.toArray(result)) // [3, 4, 5] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const dropWhile: { + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual(2, (self: Chunk, predicate: Predicate): Chunk => { + const arr = toReadonlyArray(self) + const len = arr.length + let i = 0 + while (i < len && predicate(arr[i]!)) { + i++ + } + return drop(self, i) +}) + +/** + * Prepends the specified prefix chunk to the beginning of the specified chunk. + * If either chunk is non-empty, the result is also a non-empty chunk. + * + * **Example** (Prepending all elements) + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.make(1, 2).pipe( + * Chunk.prependAll(Chunk.make("a", "b")), + * Chunk.toArray + * ) + * + * console.log(result) + * // [ "a", "b", 1, 2 ] + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const prependAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: NonEmptyChunk, that: Chunk): Chunk => appendAll(that, self)) + +/** + * Concatenates two chunks, combining their elements. + * If either chunk is non-empty, the result is also a non-empty chunk. + * + * **When to use** + * + * Use to concatenate two chunks when the second chunk's elements should come + * after the first. + * + * **Example** (Appending all elements) + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.make(1, 2).pipe( + * Chunk.appendAll(Chunk.make("a", "b")), + * Chunk.toArray + * ) + * + * console.log(result) + * // [ 1, 2, "a", "b" ] + * ``` + * + * @see {@link prependAll} for concatenating chunks in the opposite order + * @see {@link append} for adding a single element to the end + * + * @category combining + * @since 2.0.0 + */ +export const appendAll: { + , T extends Chunk>( + that: T + ): (self: S) => Chunk.OrNonEmpty | Chunk.Infer> + (self: Chunk, that: NonEmptyChunk): NonEmptyChunk + (self: NonEmptyChunk, that: Chunk): NonEmptyChunk + (self: Chunk, that: Chunk): Chunk +} = dual(2, (self: Chunk, that: Chunk): Chunk => { + if (self.backing._tag === "IEmpty") { + return that + } + if (that.backing._tag === "IEmpty") { + return self + } + const diff = that.depth - self.depth + if (Math.abs(diff) <= 1) { + return makeChunk({ _tag: "IConcat", left: self, right: that }) + } else if (diff < -1) { + if (self.left.depth >= self.right.depth) { + const nr = appendAll(self.right, that) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nrr = appendAll(self.right.right, that) + if (nrr.depth === self.depth - 3) { + const nr = makeChunk({ _tag: "IConcat", left: self.right.left, right: nrr }) + return makeChunk({ _tag: "IConcat", left: self.left, right: nr }) + } else { + const nl = makeChunk({ _tag: "IConcat", left: self.left, right: self.right.left }) + return makeChunk({ _tag: "IConcat", left: nl, right: nrr }) + } + } + } else { + if (that.right.depth >= that.left.depth) { + const nl = appendAll(self, that.left) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nll = appendAll(self, that.left.left) + if (nll.depth === that.depth - 3) { + const nl = makeChunk({ _tag: "IConcat", left: nll, right: that.left.right }) + return makeChunk({ _tag: "IConcat", left: nl, right: that.right }) + } else { + const nr = makeChunk({ _tag: "IConcat", left: that.left.right, right: that.right }) + return makeChunk({ _tag: "IConcat", left: nll, right: nr }) + } + } + } +}) + +/** + * Returns a filtered and mapped subset of the elements. + * + * **Example** (Filtering and mapping values) + * + * ```ts + * import { Chunk, Result } from "effect" + * + * const chunk = Chunk.make("1", "2", "hello", "3", "world") + * const numbers = Chunk.filterMap(chunk, (str) => { + * const num = parseInt(str) + * return isNaN(num) ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Chunk.toArray(numbers)) // [1, 2, 3] + * + * // With index parameter + * const evenIndexNumbers = Chunk.filterMap(chunk, (str, i) => { + * const num = parseInt(str) + * return isNaN(num) || i % 2 !== 0 ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Chunk.toArray(evenIndexNumbers)) // [1] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (input: A, i: number) => Result): (self: Chunk) => Chunk + (self: Chunk, f: (input: A, i: number) => Result): Chunk +} = dual( + 2, + (self: Chunk, f: (input: A, i: number) => Result): Chunk => { + const as = RA.fromIterable(self) + const out: Array = [] + for (let i = 0; i < as.length; i++) { + const result = f(as[i], i) + if (R.isSuccess(result)) { + out.push(result.success) + } + } + return fromArrayUnsafe(out) + } +) + +/** + * Returns a filtered subset of the elements. + * + * **Example** (Filtering values) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) + * const evenNumbers = Chunk.filter(chunk, (n) => n % 2 === 0) + * console.log(Chunk.toArray(evenNumbers)) // [2, 4, 6] + * + * // With refinement + * const mixed = Chunk.make("hello", 42, "world", 100) + * const numbers = Chunk.filter(mixed, (x): x is number => typeof x === "number") + * console.log(Chunk.toArray(numbers)) // [42, 100] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: Refinement, B>): (self: Chunk) => Chunk + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, refinement: Refinement): Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual( + 2, + (self: Chunk, predicate: Predicate): Chunk => fromArrayUnsafe(RA.filter(self, predicate)) +) + +/** + * Transforms all elements of the chunk for as long as the specified function succeeds. + * + * **Example** (Filtering and mapping while values match) + * + * ```ts + * import { Chunk, Result } from "effect" + * + * const chunk = Chunk.make("1", "2", "hello", "3", "4") + * const result = Chunk.filterMapWhile(chunk, (s) => { + * const num = parseInt(s) + * return isNaN(num) ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Chunk.toArray(result)) // [1, 2] + * // Stops at "hello" and doesn't process "3", "4" + * + * // Compare with regular filterMap + * const allNumbers = Chunk.filterMap(chunk, (s) => { + * const num = parseInt(s) + * return isNaN(num) ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Chunk.toArray(allNumbers)) // [1, 2, 3, 4] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMapWhile: { + (f: Filter.Filter): (self: Chunk) => Chunk + (self: Chunk, f: Filter.Filter): Chunk +} = dual(2, (self: Chunk, f: Filter.Filter): Chunk => { + const out: Array = [] + for (const a of self) { + const result = f(a) + if (R.isSuccess(result)) { + out.push(result.success) + } else { + break + } + } + return fromArrayUnsafe(out) +}) + +/** + * Filters out optional values + * + * **Example** (Compacting optional values) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make(Option.some(1), Option.none(), Option.some(3)) + * const result = Chunk.compact(chunk) + * console.log(Chunk.toArray(result)) // [1, 3] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const compact = (self: Chunk>): Chunk => { + const out: Array = [] + for (const option of self) { + if (O.isSome(option)) { + out.push(option.value) + } + } + return fromArrayUnsafe(out) +} + +/** + * Applies a function to each element in a chunk and returns a new chunk containing the concatenated mapped elements. + * + * **Example** (Flat mapping chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * const duplicated = Chunk.flatMap(chunk, (n) => Chunk.make(n, n)) + * console.log(Chunk.toArray(duplicated)) // [1, 1, 2, 2, 3, 3] + * + * // Flattening nested arrays + * const words = Chunk.make("hello", "world") + * const letters = Chunk.flatMap( + * words, + * (word) => Chunk.fromIterable(word.split("")) + * ) + * console.log(Chunk.toArray(letters)) // ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"] + * + * // With index parameter + * const indexed = Chunk.flatMap(chunk, (n, i) => Chunk.make(n + i)) + * console.log(Chunk.toArray(indexed)) // [1, 3, 5] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + , T extends Chunk>( + f: (a: Chunk.Infer, i: number) => T + ): (self: S) => Chunk.AndNonEmpty> + (self: NonEmptyChunk, f: (a: A, i: number) => NonEmptyChunk): NonEmptyChunk + (self: Chunk, f: (a: A, i: number) => Chunk): Chunk +} = dual(2, (self: Chunk, f: (a: A, i: number) => Chunk) => { + if (self.backing._tag === "ISingleton") { + return f(self.backing.a, 0) + } + let out: Chunk = _empty + let i = 0 + for (const k of self) { + out = appendAll(out, f(k, i++)) + } + return out +}) + +/** + * Iterates over each element of a `Chunk` and applies a function to it. + * + * **Details** + * + * This function processes every element of the given `Chunk`, calling the + * provided function `f` on each element. It does not return a new value; + * instead, it is primarily used for side effects, such as logging or + * accumulating data in an external variable. + * + * **Example** (Iterating over chunk values) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * + * // Log each element + * Chunk.forEach(chunk, (n) => console.log(`Value: ${n}`)) + * // Output: + * // Value: 1 + * // Value: 2 + * // Value: 3 + * // Value: 4 + * + * // With index parameter + * Chunk.forEach(chunk, (n, i) => console.log(`Index ${i}: ${n}`)) + * // Output: + * // Index 0: 1 + * // Index 1: 2 + * // Index 2: 3 + * // Index 3: 4 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const forEach: { + (f: (a: A, index: number) => B): (self: Chunk) => void + (self: Chunk, f: (a: A, index: number) => B): void +} = dual(2, (self: Chunk, f: (a: A) => B): void => toReadonlyArray(self).forEach(f)) + +/** + * Flattens a chunk of chunks into a single chunk by concatenating all chunks. + * + * **Example** (Flattening nested chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const nested = Chunk.make( + * Chunk.make(1, 2), + * Chunk.make(3, 4, 5), + * Chunk.make(6) + * ) + * const flattened = Chunk.flatten(nested) + * console.log(Chunk.toArray(flattened)) // [1, 2, 3, 4, 5, 6] + * + * // With empty chunks + * const withEmpty = Chunk.make( + * Chunk.make(1, 2), + * Chunk.empty(), + * Chunk.make(3, 4) + * ) + * console.log(Chunk.toArray(Chunk.flatten(withEmpty))) // [1, 2, 3, 4] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten: >>(self: S) => Chunk.Flatten = flatMap(identity) as any + +/** + * Groups elements in chunks of up to `n` elements. + * + * **When to use** + * + * Use to divide a chunk into ordered, non-overlapping chunks with at most `n` + * elements each. + * + * **Details** + * + * The final chunk may contain fewer than `n` elements. Empty input produces an + * empty chunk of chunks. + * + * **Gotchas** + * + * Values of `n` less than or equal to zero produce singleton chunks. + * + * **Example** (Splitting into fixed-size chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8, 9) + * const chunked = Chunk.chunksOf(chunk, 3) + * + * console.log(Chunk.toArray(chunked).map(Chunk.toArray)) + * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * + * // When length is not evenly divisible + * const chunk2 = Chunk.make(1, 2, 3, 4, 5) + * const chunked2 = Chunk.chunksOf(chunk2, 2) + * console.log(Chunk.toArray(chunked2).map(Chunk.toArray)) + * // [[1, 2], [3, 4], [5]] + * ``` + * + * @see {@link split} for splitting into a target number of chunks instead of a fixed chunk size + * + * @category elements + * @since 2.0.0 + */ +export const chunksOf: { + (n: number): (self: Chunk) => Chunk> + (self: Chunk, n: number): Chunk> +} = dual(2, (self: Chunk, n: number) => { + const gr: Array> = [] + let current: Array = [] + toReadonlyArray(self).forEach((a) => { + current.push(a) + if (current.length >= n) { + gr.push(fromArrayUnsafe(current)) + current = [] + } + }) + if (current.length > 0) { + gr.push(fromArrayUnsafe(current)) + } + return fromArrayUnsafe(gr) +}) + +/** + * Creates a `Chunk` of values that are included in both chunks. + * + * **Details** + * + * The order and references of result values are determined by the first chunk. + * + * **Example** (Intersecting chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk1 = Chunk.make(1, 2, 3, 4) + * const chunk2 = Chunk.make(3, 4, 5, 6) + * const result = Chunk.intersection(chunk1, chunk2) + * console.log(Chunk.toArray(result)) // [3, 4] + * + * // With strings + * const words1 = Chunk.make("hello", "world", "foo") + * const words2 = Chunk.make("world", "bar", "foo") + * console.log(Chunk.toArray(Chunk.intersection(words1, words2))) // ["world", "foo"] + * + * // No intersection + * const chunk3 = Chunk.make(1, 2) + * const chunk4 = Chunk.make(3, 4) + * console.log(Chunk.toArray(Chunk.intersection(chunk3, chunk4))) // [] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const intersection: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk => + fromArrayUnsafe(RA.intersection(toReadonlyArray(self), toReadonlyArray(that))) +) + +/** + * Determines if the chunk is empty. + * + * **Example** (Checking for empty chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * console.log(Chunk.isEmpty(Chunk.empty())) // true + * console.log(Chunk.isEmpty(Chunk.make(1, 2, 3))) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const isEmpty = (self: Chunk): boolean => self.length === 0 + +/** + * Determines if the chunk is not empty. + * + * **Example** (Checking for non-empty chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * console.log(Chunk.isNonEmpty(Chunk.empty())) // false + * console.log(Chunk.isNonEmpty(Chunk.make(1, 2, 3))) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const isNonEmpty = (self: Chunk): self is NonEmptyChunk => self.length > 0 + +/** + * Returns the first element of this chunk safely if it exists. + * + * **Example** (Getting the first element) + * + * ```ts + * import { Chunk } from "effect" + * + * console.log(Chunk.head(Chunk.empty())) // { _tag: "None" } + * console.log(Chunk.head(Chunk.make(1, 2, 3))) // { _tag: "Some", value: 1 } + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const head: (self: Chunk) => Option = get(0) + +/** + * Returns the first element of this chunk. + * + * **When to use** + * + * Use when you know the chunk is non-empty and need the first element directly + * without handling `Option.none`. + * + * **Gotchas** + * + * Throws an error if the chunk is empty. + * + * **Example** (Getting the first element unsafely) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.headUnsafe(chunk)) // 1 + * + * const singleElement = Chunk.make("hello") + * console.log(Chunk.headUnsafe(singleElement)) // "hello" + * + * // Use Chunk.head when the chunk may be empty + * console.log(Option.isNone(Chunk.head(Chunk.empty()))) // true + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const headUnsafe = (self: Chunk): A => getUnsafe(self, 0) + +/** + * Returns the first element of this non empty chunk. + * + * **Example** (Getting the first element of a non-empty chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.headNonEmpty(nonEmptyChunk)) // 1 + * + * const singleElement = Chunk.make("hello") + * console.log(Chunk.headNonEmpty(singleElement)) // "hello" + * + * // Type safety: this function only accepts NonEmptyChunk + * // Chunk.headNonEmpty(Chunk.empty()) // TypeScript error + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const headNonEmpty: (self: NonEmptyChunk) => A = headUnsafe + +/** + * Returns the last element of this chunk safely if it exists. + * + * **Example** (Getting the last element) + * + * ```ts + * import { Chunk } from "effect" + * + * console.log(Chunk.last(Chunk.empty())) // { _tag: "None" } + * console.log(Chunk.last(Chunk.make(1, 2, 3))) // { _tag: "Some", value: 3 } + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const last = (self: Chunk): Option => get(self, self.length - 1) + +/** + * Returns the last element of this chunk. + * + * **When to use** + * + * Use when you know the chunk is non-empty and need the last element directly + * without handling `Option.none`. + * + * **Gotchas** + * + * Throws an error if the chunk is empty. + * + * **Example** (Getting the last element unsafely) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.lastUnsafe(chunk)) // 4 + * + * const singleElement = Chunk.make("hello") + * console.log(Chunk.lastUnsafe(singleElement)) // "hello" + * + * // Use Chunk.last when the chunk may be empty + * console.log(Option.isNone(Chunk.last(Chunk.empty()))) // true + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const lastUnsafe = (self: Chunk): A => getUnsafe(self, self.length - 1) + +/** + * Returns the last element of this non empty chunk. + * + * **Example** (Getting the last element of a non-empty chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.lastNonEmpty(nonEmptyChunk)) // 4 + * + * const singleElement = Chunk.make("hello") + * console.log(Chunk.lastNonEmpty(singleElement)) // "hello" + * + * // Type safety: this function only accepts NonEmptyChunk + * // Chunk.lastNonEmpty(Chunk.empty()) // TypeScript error + * ``` + * + * @category elements + * @since 3.4.0 + */ +export const lastNonEmpty: (self: NonEmptyChunk) => A = lastUnsafe + +/** + * A namespace containing utility types for Chunk operations. + * + * **Example** (Working with Chunk utility types) + * + * ```ts + * import type { Chunk } from "effect" + * + * // Extract the element type from a Chunk + * declare const chunk: Chunk.Chunk + * type ElementType = Chunk.Chunk.Infer // string + * + * // Create a preserving non-emptiness + * declare const nonEmptyChunk: Chunk.NonEmptyChunk + * type WithString = Chunk.Chunk.With // Chunk.NonEmptyChunk + * ``` + * + * @since 2.0.0 + */ +export declare namespace Chunk { + /** + * Infers the element type of a Chunk. + * + * **Example** (Inferring element types) + * + * ```ts + * import type { Chunk } from "effect" + * + * declare const numberChunk: Chunk.Chunk + * declare const stringChunk: Chunk.Chunk + * + * type NumberType = Chunk.Chunk.Infer // number + * type StringType = Chunk.Chunk.Infer // string + * ``` + * + * @category types + * @since 2.0.0 + */ + export type Infer> = S extends Chunk ? A : never + + /** + * Constructs a Chunk type preserving non-emptiness. + * + * **Example** (Preserving non-emptiness) + * + * ```ts + * import type { Chunk } from "effect" + * + * declare const regularChunk: Chunk.Chunk + * declare const nonEmptyChunk: Chunk.NonEmptyChunk + * + * type WithString1 = Chunk.Chunk.With // Chunk.Chunk + * type WithString2 = Chunk.Chunk.With // Chunk.NonEmptyChunk + * ``` + * + * @category types + * @since 2.0.0 + */ + export type With, A> = S extends NonEmptyChunk ? NonEmptyChunk : Chunk + + /** + * Creates a non-empty Chunk if either input is non-empty. + * + * **Example** (Preserving non-emptiness from either input) + * + * ```ts + * import type { Chunk } from "effect" + * + * declare const emptyChunk: Chunk.Chunk + * declare const nonEmptyChunk: Chunk.NonEmptyChunk + * + * type Result1 = Chunk.Chunk.OrNonEmpty< + * typeof emptyChunk, + * typeof emptyChunk, + * string + * > // Chunk.Chunk + * type Result2 = Chunk.Chunk.OrNonEmpty< + * typeof emptyChunk, + * typeof nonEmptyChunk, + * string + * > // Chunk.NonEmptyChunk + * type Result3 = Chunk.Chunk.OrNonEmpty< + * typeof nonEmptyChunk, + * typeof emptyChunk, + * string + * > // Chunk.NonEmptyChunk + * ``` + * + * @category types + * @since 2.0.0 + */ + export type OrNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? + NonEmptyChunk + : T extends NonEmptyChunk ? NonEmptyChunk + : Chunk + + /** + * Creates a non-empty Chunk only if both inputs are non-empty. + * + * **Example** (Requiring non-emptiness from both inputs) + * + * ```ts + * import type { Chunk } from "effect" + * + * declare const emptyChunk: Chunk.Chunk + * declare const nonEmptyChunk: Chunk.NonEmptyChunk + * + * type Result1 = Chunk.Chunk.AndNonEmpty< + * typeof emptyChunk, + * typeof emptyChunk, + * string + * > // Chunk.Chunk + * type Result2 = Chunk.Chunk.AndNonEmpty< + * typeof emptyChunk, + * typeof nonEmptyChunk, + * string + * > // Chunk.Chunk + * type Result3 = Chunk.Chunk.AndNonEmpty< + * typeof nonEmptyChunk, + * typeof nonEmptyChunk, + * string + * > // Chunk.NonEmptyChunk + * ``` + * + * @category types + * @since 2.0.0 + */ + export type AndNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? + T extends NonEmptyChunk ? NonEmptyChunk + : Chunk : + Chunk + + /** + * Flattens a nested Chunk type. + * + * **Example** (Flattening nested chunk types) + * + * ```ts + * import type { Chunk } from "effect" + * + * declare const nestedChunk: Chunk.Chunk> + * declare const nestedNonEmpty: Chunk.NonEmptyChunk> + * + * type Flattened1 = Chunk.Chunk.Flatten // Chunk.Chunk + * type Flattened2 = Chunk.Chunk.Flatten // Chunk.NonEmptyChunk + * ``` + * + * @category types + * @since 2.0.0 + */ + export type Flatten>> = T extends NonEmptyChunk> ? NonEmptyChunk + : T extends Chunk> ? Chunk + : never +} + +/** + * Transforms the elements of a chunk using the specified mapping function. + * If the input chunk is non-empty, the resulting chunk will also be non-empty. + * + * **Example** (Mapping values) + * + * ```ts + * import { Chunk } from "effect" + * + * const result = Chunk.map(Chunk.make(1, 2), (n) => n + 1) + * + * console.log(Chunk.toArray(result)) // [2, 3] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + , B>(f: (a: Chunk.Infer, i: number) => B): (self: S) => Chunk.With + (self: NonEmptyChunk, f: (a: A, i: number) => B): NonEmptyChunk + (self: Chunk, f: (a: A, i: number) => B): Chunk +} = dual(2, (self: Chunk, f: (a: A, i: number) => B): Chunk => + self.backing._tag === "ISingleton" ? + of(f(self.backing.a, 0)) : + fromArrayUnsafe(pipe(toReadonlyArray(self), RA.map((a, i) => f(a, i))))) + +/** + * Maps over the chunk statefully, producing new elements of type `B`. + * + * **Example** (Mapping with accumulated state) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const [finalState, mapped] = Chunk.mapAccum(chunk, 0, (state, current) => [ + * state + current, // accumulate sum + * state + current // output running sum + * ]) + * + * console.log(finalState) // 15 (final accumulated sum) + * console.log(Chunk.toArray(mapped)) // [1, 3, 6, 10, 15] (running sums) + * + * // Building a string with indices + * const words = Chunk.make("hello", "world", "effect") + * const [count, indexed] = Chunk.mapAccum(words, 0, (index, word) => [ + * index + 1, + * `${index}: ${word}` + * ]) + * console.log(count) // 3 + * console.log(Chunk.toArray(indexed)) // ["0: hello", "1: world", "2: effect"] + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const mapAccum: { + (s: S, f: (s: S, a: A) => readonly [S, B]): (self: Chunk) => [S, Chunk] + (self: Chunk, s: S, f: (s: S, a: A) => readonly [S, B]): [S, Chunk] +} = dual(3, (self: Chunk, s: S, f: (s: S, a: A) => readonly [S, B]): [S, Chunk] => { + const [s1, as] = RA.mapAccum(self, s, f) + return [s1, fromArrayUnsafe(as)] +}) + +/** + * Splits a chunk using a `Filter` into failures and successes. + * + * **Details** + * + * Returns `[excluded, satisfying]`. The filter receives `(element, index)`. + * + * **Example** (Partitioning with a Result) + * + * ```ts + * import { Chunk, Result } from "effect" + * + * const [excluded, satisfying] = Chunk.partition(Chunk.make(1, -2, 3), (n, i) => + * n > 0 ? Result.succeed(n + i) : Result.fail(`negative:${n}`) + * ) + * + * console.log(Chunk.toArray(excluded)) // ["negative:-2"] + * console.log(Chunk.toArray(satisfying)) // [1, 5] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + ( + f: (input: NoInfer, i: number) => Result + ): (self: Chunk) => [excluded: Chunk, satisfying: Chunk] + ( + self: Chunk, + f: (input: A, i: number) => Result + ): [excluded: Chunk, satisfying: Chunk] +} = dual( + 2, + ( + self: Chunk, + f: (input: A, i: number) => Result + ): [excluded: Chunk, satisfying: Chunk] => { + const [excluded, satisfying] = RA.partition(self, f) + return [fromArrayUnsafe(excluded), fromArrayUnsafe(satisfying)] + } +) + +/** + * Separates a chunk of `Result` values into a chunk of failures and a chunk of + * successes. + * + * **Details** + * + * The returned tuple is `[failures, successes]`, preserving the original order + * within each side. + * + * **Example** (Separating failures and successes) + * + * ```ts + * import { Chunk, Result } from "effect" + * + * const chunk = Chunk.make( + * Result.succeed(1), + * Result.fail("error1"), + * Result.succeed(2), + * Result.fail("error2"), + * Result.succeed(3) + * ) + * + * const [errors, values] = Chunk.separate(chunk) + * console.log(Chunk.toArray(errors)) // ["error1", "error2"] + * console.log(Chunk.toArray(values)) // [1, 2, 3] + * + * // All successes + * const allSuccesses = Chunk.make(Result.succeed(1), Result.succeed(2)) + * const [noErrors, allValues] = Chunk.separate(allSuccesses) + * console.log(Chunk.toArray(noErrors)) // [] + * console.log(Chunk.toArray(allValues)) // [1, 2] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const separate = (self: Chunk>): [Chunk, Chunk] => + pipe( + RA.separate(toReadonlyArray(self)), + ([l, r]) => [fromArrayUnsafe(l), fromArrayUnsafe(r)] + ) + +/** + * Retrieves the size of the chunk. + * + * **Example** (Getting chunk size) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3) + * console.log(Chunk.size(chunk)) // 3 + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const size = (self: Chunk): number => self.length + +/** + * Sorts the elements of a `Chunk` in increasing order, creating a new `Chunk`. + * + * **Example** (Sorting chunks) + * + * ```ts + * import { Chunk, Order } from "effect" + * + * const numbers = Chunk.make(3, 1, 4, 1, 5, 9, 2, 6) + * const sorted = Chunk.sort(numbers, Order.Number) + * console.log(Chunk.toArray(sorted)) // [1, 1, 2, 3, 4, 5, 6, 9] + * + * // Reverse order + * const reverseSorted = Chunk.sort(numbers, Order.flip(Order.Number)) + * console.log(Chunk.toArray(reverseSorted)) // [9, 6, 5, 4, 3, 2, 1, 1] + * + * // String sorting + * const words = Chunk.make("banana", "apple", "cherry") + * const sortedWords = Chunk.sort(words, Order.String) + * console.log(Chunk.toArray(sortedWords)) // ["apple", "banana", "cherry"] + * ``` + * + * @category sorting + * @since 2.0.0 + */ +export const sort: { + (O: Order.Order): (self: Chunk) => Chunk + (self: Chunk, O: Order.Order): Chunk +} = dual( + 2, + (self: Chunk, O: Order.Order): Chunk => fromArrayUnsafe(RA.sort(toReadonlyArray(self), O)) +) + +/** + * Sorts the elements of a `Chunk` based on a projection function. + * + * **Example** (Sorting chunks by a derived value) + * + * ```ts + * import { Chunk, Order } from "effect" + * + * const people = Chunk.make( + * { name: "Alice", age: 30 }, + * { name: "Bob", age: 25 }, + * { name: "Charlie", age: 35 } + * ) + * + * // Sort by age + * const byAge = Chunk.sortWith(people, (person) => person.age, Order.Number) + * console.log(Chunk.toArray(byAge)) + * // [{ name: "Bob", age: 25 }, { name: "Alice", age: 30 }, { name: "Charlie", age: 35 }] + * + * // Sort by name + * const byName = Chunk.sortWith(people, (person) => person.name, Order.String) + * console.log(Chunk.toArray(byName)) + * // [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }, { name: "Charlie", age: 35 }] + * + * // Sort by string length + * const words = Chunk.make("a", "abc", "ab") + * const byLength = Chunk.sortWith(words, (word) => word.length, Order.Number) + * console.log(Chunk.toArray(byLength)) // ["a", "ab", "abc"] + * ``` + * + * @category sorting + * @since 2.0.0 + */ +export const sortWith: { + (f: (a: A) => B, order: Order.Order): (self: Chunk) => Chunk + (self: Chunk, f: (a: A) => B, order: Order.Order): Chunk +} = dual( + 3, + (self: Chunk, f: (a: A) => B, order: Order.Order): Chunk => sort(self, Order.mapInput(order, f)) +) + +/** + * Returns two splits of this chunk at the specified index. + * + * **Example** (Splitting at an index) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) + * const [before, after] = Chunk.splitAt(chunk, 3) + * console.log(Chunk.toArray(before)) // [1, 2, 3] + * console.log(Chunk.toArray(after)) // [4, 5, 6] + * + * // Split at index 0 + * const [empty, all] = Chunk.splitAt(chunk, 0) + * console.log(Chunk.toArray(empty)) // [] + * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] + * + * // Split beyond length + * const [allElements, empty2] = Chunk.splitAt(chunk, 10) + * console.log(Chunk.toArray(allElements)) // [1, 2, 3, 4, 5, 6] + * console.log(Chunk.toArray(empty2)) // [] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitAt: { + (n: number): (self: Chunk) => [beforeIndex: Chunk, fromIndex: Chunk] + (self: Chunk, n: number): [beforeIndex: Chunk, fromIndex: Chunk] +} = dual(2, (self: Chunk, n: number): [Chunk, Chunk] => [take(self, n), drop(self, n)]) + +/** + * Splits a `NonEmptyChunk` at `n`, returning a non-empty prefix and the + * remaining suffix. + * + * **Details** + * + * `n` is floored and normalized to at least `1`. If `n` is greater than or + * equal to the chunk length, the first result is the original chunk and the + * second result is empty. + * + * **Example** (Splitting non-empty chunks at an index) + * + * ```ts + * import { Chunk } from "effect" + * + * const nonEmptyChunk = Chunk.make(1, 2, 3, 4, 5, 6) + * const [before, after] = Chunk.splitNonEmptyAt(nonEmptyChunk, 3) + * console.log(Chunk.toArray(before)) // [1, 2, 3] + * console.log(Chunk.toArray(after)) // [4, 5, 6] + * + * // Split at 1 (minimum) + * const [first, rest] = Chunk.splitNonEmptyAt(nonEmptyChunk, 1) + * console.log(Chunk.toArray(first)) // [1] + * console.log(Chunk.toArray(rest)) // [2, 3, 4, 5, 6] + * + * // The first part is guaranteed to be NonEmptyChunk + * // while the second part may be empty + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitNonEmptyAt: { + (n: number): (self: NonEmptyChunk) => [beforeIndex: NonEmptyChunk, fromIndex: Chunk] + (self: NonEmptyChunk, n: number): [beforeIndex: NonEmptyChunk, fromIndex: Chunk] +} = dual(2, (self: NonEmptyChunk, n: number): [Chunk, Chunk] => { + const _n = Math.max(1, Math.floor(n)) + return _n >= self.length ? + [self, empty()] : + [take(self, _n), drop(self, _n)] +}) + +/** + * Splits a chunk into up to `n` chunks, distributing elements in order. + * + * **Details** + * + * The chunk size is derived from the input length and `n`; the final chunk may + * contain fewer elements than the others. + * + * **Example** (Splitting chunks into groups) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8, 9) + * const chunks = Chunk.split(chunk, 3) + * console.log(Chunk.toArray(chunks).map(Chunk.toArray)) + * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * + * // Uneven split + * const chunk2 = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8) + * const chunks2 = Chunk.split(chunk2, 3) + * console.log(Chunk.toArray(chunks2).map(Chunk.toArray)) + * // [[1, 2, 3], [4, 5, 6], [7, 8]] + * + * // Split into 1 chunk + * const chunks3 = Chunk.split(chunk, 1) + * console.log(Chunk.toArray(chunks3).map(Chunk.toArray)) + * // [[1, 2, 3, 4, 5, 6, 7, 8, 9]] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const split: { + (n: number): (self: Chunk) => Chunk> + (self: Chunk, n: number): Chunk> +} = dual(2, (self: Chunk, n: number) => chunksOf(self, Math.ceil(self.length / Math.floor(n)))) + +/** + * Splits this chunk on the first element that matches this predicate. + * Returns a tuple containing two chunks: the first one is before the match, and the second one is from the match onward. + * + * **Example** (Splitting at a matching element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) + * const [before, fromMatch] = Chunk.splitWhere(chunk, (n) => n > 3) + * console.log(Chunk.toArray(before)) // [1, 2, 3] + * console.log(Chunk.toArray(fromMatch)) // [4, 5, 6] + * + * // No match found + * const [all, empty] = Chunk.splitWhere(chunk, (n) => n > 10) + * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] + * console.log(Chunk.toArray(empty)) // [] + * + * // Match on first element + * const [emptyBefore, allFromFirst] = Chunk.splitWhere(chunk, (n) => n === 1) + * console.log(Chunk.toArray(emptyBefore)) // [] + * console.log(Chunk.toArray(allFromFirst)) // [1, 2, 3, 4, 5, 6] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const splitWhere: { + (predicate: Predicate>): (self: Chunk) => [beforeMatch: Chunk, fromMatch: Chunk] + (self: Chunk, predicate: Predicate): [beforeMatch: Chunk, fromMatch: Chunk] +} = dual(2, (self: Chunk, predicate: Predicate): [beforeMatch: Chunk, fromMatch: Chunk] => { + let i = 0 + for (const a of toReadonlyArray(self)) { + if (predicate(a)) { + break + } else { + i++ + } + } + return splitAt(self, i) +}) + +/** + * Returns every element after the first safely, or `None` when the chunk is empty. + * + * **Example** (Getting the tail safely) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * console.log(Chunk.tail(chunk)) // Option.some(Chunk.make(2, 3, 4)) + * + * const singleElement = Chunk.make(1) + * console.log(Chunk.tail(singleElement)) // Option.some(Chunk.empty()) + * + * const empty = Chunk.empty() + * console.log(Chunk.tail(empty)) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const tail = (self: Chunk): O.Option> => self.length > 0 ? O.some(drop(self, 1)) : O.none() + +/** + * Returns every element after the first from a non-empty chunk. + * + * **Example** (Getting the tail of a non-empty chunk) + * + * ```ts + * import { Chunk } from "effect" + * + * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) + * const result = Chunk.tailNonEmpty(nonEmptyChunk) + * console.log(Chunk.toArray(result)) // [2, 3, 4] + * + * const singleElement = Chunk.make(1) + * const resultSingle = Chunk.tailNonEmpty(singleElement) + * console.log(Chunk.toArray(resultSingle)) // [] + * + * // Type safety: this function only accepts NonEmptyChunk + * // Chunk.tailNonEmpty(Chunk.empty()) // TypeScript error + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, 1) + +/** + * Takes the last `n` elements. + * + * **Example** (Taking elements from the end) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) + * const lastThree = Chunk.takeRight(chunk, 3) + * console.log(Chunk.toArray(lastThree)) // [4, 5, 6] + * + * // Take more than available + * const all = Chunk.takeRight(chunk, 10) + * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] + * + * // Take zero + * const none = Chunk.takeRight(chunk, 0) + * console.log(Chunk.toArray(none)) // [] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: Chunk) => Chunk + (self: Chunk, n: number): Chunk +} = dual(2, (self: Chunk, n: number): Chunk => drop(self, self.length - n)) + +/** + * Takes all elements so long as the predicate returns true. + * + * **Example** (Taking elements while a predicate matches) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 3, 2, 1) + * const result = Chunk.takeWhile(chunk, (n) => n < 4) + * console.log(Chunk.toArray(result)) // [1, 2, 3] + * + * // Empty if first element doesn't match + * const none = Chunk.takeWhile(chunk, (n) => n > 5) + * console.log(Chunk.toArray(none)) // [] + * + * // Takes all if all match + * const small = Chunk.make(1, 2, 3) + * const all = Chunk.takeWhile(small, (n) => n < 10) + * console.log(Chunk.toArray(all)) // [1, 2, 3] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: Refinement, B>): (self: Chunk) => Chunk + (predicate: Predicate>): (self: Chunk) => Chunk + (self: Chunk, refinement: Refinement): Chunk + (self: Chunk, predicate: Predicate): Chunk +} = dual(2, (self: Chunk, predicate: Predicate): Chunk => { + const out: Array = [] + for (const a of toReadonlyArray(self)) { + if (predicate(a)) { + out.push(a) + } else { + break + } + } + return fromArrayUnsafe(out) +}) + +/** + * Creates a Chunks of unique values, in order, from all given Chunks. + * + * **Example** (Unioning chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk1 = Chunk.make(1, 2, 3) + * const chunk2 = Chunk.make(3, 4, 5) + * const result = Chunk.union(chunk1, chunk2) + * console.log(Chunk.toArray(result)) // [1, 2, 3, 4, 5] + * + * // Handles duplicates within the same chunk + * const withDupes1 = Chunk.make(1, 1, 2) + * const withDupes2 = Chunk.make(2, 3, 3) + * const unified = Chunk.union(withDupes1, withDupes2) + * console.log(Chunk.toArray(unified)) // [1, 2, 3] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const union: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk) => fromArrayUnsafe(RA.union(toReadonlyArray(self), toReadonlyArray(that))) +) + +/** + * Removes duplicate elements from a `Chunk`, preserving the first occurrence + * of each value. + * + * **Example** (Removing duplicate values) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 2, 3, 1, 4, 3) + * const result = Chunk.dedupe(chunk) + * console.log(Chunk.toArray(result)) // [1, 2, 3, 4] + * + * // Empty chunk + * const empty = Chunk.empty() + * const emptyDeduped = Chunk.dedupe(empty) + * console.log(Chunk.toArray(emptyDeduped)) // [] + * + * // No duplicates + * const unique = Chunk.make(1, 2, 3) + * const uniqueDeduped = Chunk.dedupe(unique) + * console.log(Chunk.toArray(uniqueDeduped)) // [1, 2, 3] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const dedupe = (self: Chunk): Chunk => fromArrayUnsafe(RA.dedupe(toReadonlyArray(self))) + +/** + * Deduplicates adjacent elements that are identical. + * + * **Example** (Removing adjacent duplicates) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 1, 2, 2, 2, 3, 1, 1) + * const result = Chunk.dedupeAdjacent(chunk) + * console.log(Chunk.toArray(result)) // [1, 2, 3, 1] + * + * // Only removes adjacent duplicates, not all duplicates + * const mixed = Chunk.make("a", "a", "b", "a", "a") + * const mixedResult = Chunk.dedupeAdjacent(mixed) + * console.log(Chunk.toArray(mixedResult)) // ["a", "b", "a"] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dedupeAdjacent = (self: Chunk): Chunk => fromArrayUnsafe(RA.dedupeAdjacent(self)) + +/** + * Takes a `Chunk` of pairs and returns two corresponding `Chunk`s. + * + * **Details** + * + * This function is the reverse of `zip`. + * + * **Example** (Unzipping pairs) + * + * ```ts + * import { Chunk } from "effect" + * + * const pairs = Chunk.make( + * [1, "a"] as const, + * [2, "b"] as const, + * [3, "c"] as const + * ) + * const [numbers, letters] = Chunk.unzip(pairs) + * console.log(Chunk.toArray(numbers)) // [1, 2, 3] + * console.log(Chunk.toArray(letters)) // ["a", "b", "c"] + * + * // Empty chunk + * const empty = Chunk.empty<[number, string]>() + * const [emptyNums, emptyStrs] = Chunk.unzip(empty) + * console.log(Chunk.toArray(emptyNums)) // [] + * console.log(Chunk.toArray(emptyStrs)) // [] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const unzip = (self: Chunk): [Chunk, Chunk] => { + const [left, right] = RA.unzip(self) + return [fromArrayUnsafe(left), fromArrayUnsafe(right)] +} + +/** + * Zips this chunk pointwise with the specified chunk using the specified combiner. + * + * **Example** (Zipping chunks with a function) + * + * ```ts + * import { Chunk } from "effect" + * + * const numbers = Chunk.make(1, 2, 3) + * const letters = Chunk.make("a", "b", "c") + * const result = Chunk.zipWith(numbers, letters, (n, l) => `${n}-${l}`) + * console.log(Chunk.toArray(result)) // ["1-a", "2-b", "3-c"] + * + * // Different lengths - takes minimum + * const short = Chunk.make(1, 2) + * const long = Chunk.make("a", "b", "c", "d") + * const mixed = Chunk.zipWith(short, long, (n, l) => [n, l]) + * console.log(Chunk.toArray(mixed)) // [[1, "a"], [2, "b"]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: Chunk, f: (a: A, b: B) => C): (self: Chunk) => Chunk + (self: Chunk, that: Chunk, f: (a: A, b: B) => C): Chunk +} = dual( + 3, + (self: Chunk, that: Chunk, f: (a: A, b: B) => C): Chunk => + fromArrayUnsafe(RA.zipWith(self, that, f)) +) + +/** + * Zips this chunk pointwise with the specified chunk. + * + * **Example** (Zipping chunks) + * + * ```ts + * import { Chunk } from "effect" + * + * const numbers = Chunk.make(1, 2, 3) + * const letters = Chunk.make("a", "b", "c") + * const result = Chunk.zip(numbers, letters) + * console.log(Chunk.toArray(result)) // [[1, "a"], [2, "b"], [3, "c"]] + * + * // Different lengths - takes minimum length + * const short = Chunk.make(1, 2) + * const long = Chunk.make("a", "b", "c", "d") + * const zipped = Chunk.zip(short, long) + * console.log(Chunk.toArray(zipped)) // [[1, "a"], [2, "b"]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: Chunk): (self: Chunk) => Chunk<[A, B]> + (self: Chunk, that: Chunk): Chunk<[A, B]> +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk<[A, B]> => zipWith(self, that, (a, b) => [a, b]) +) + +/** + * Deletes the element at the specified index, creating a new `Chunk`. + * + * **Example** (Removing an element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make("a", "b", "c", "d") + * const result = Chunk.remove(chunk, 1) + * console.log(Chunk.toArray(result)) // ["a", "c", "d"] + * + * // Remove first element + * const removeFirst = Chunk.remove(chunk, 0) + * console.log(Chunk.toArray(removeFirst)) // ["b", "c", "d"] + * + * // Index out of bounds returns same chunk + * const outOfBounds = Chunk.remove(chunk, 10) + * console.log(Chunk.toArray(outOfBounds)) // ["a", "b", "c", "d"] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const remove: { + (i: number): (self: Chunk) => Chunk + (self: Chunk, i: number): Chunk +} = dual( + 2, + (self: Chunk, i: number): Chunk => fromArrayUnsafe(RA.remove(toReadonlyArray(self), i)) +) + +/** + * Applies a function to the element at the specified index safely, creating a new `Chunk`, + * or returns `None` if the index is out of bounds. + * + * **Example** (Modifying an element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * const result = Chunk.modify(chunk, 1, (n) => n * 10) + * console.log(result) // Option.some(Chunk.make(1, 20, 3, 4)) + * + * // Index out of bounds returns None + * const outOfBounds = chunk.pipe(Chunk.modify(10, (n) => n * 10)) + * console.log(outOfBounds) // Option.none() + * + * // Negative index returns None + * const negative = chunk.pipe(Chunk.modify(-1, (n) => n * 10)) + * console.log(negative) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const modify: { + (i: number, f: (a: A) => B): (self: Chunk) => O.Option> + (self: Chunk, i: number, f: (a: A) => B): O.Option> +} = dual( + 3, + (self: Chunk, i: number, f: (a: A) => B): O.Option> => + pipe(RA.modify(toReadonlyArray(self), i, f), O.map(fromArrayUnsafe)) +) + +/** + * Changes the element at the specified index safely, creating a new `Chunk`, + * or returns `None` if the index is out of bounds. + * + * **Example** (Replacing an element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make("a", "b", "c", "d") + * const result = Chunk.replace(chunk, 1, "X") + * console.log(result) // Option.some(Chunk.make("a", "X", "c", "d")) + * + * // Index out of bounds returns None + * const outOfBounds = chunk.pipe(Chunk.replace(10, "Y")) + * console.log(outOfBounds) // Option.none() + * + * // Negative index returns None + * const negative = chunk.pipe(Chunk.replace(-1, "Z")) + * console.log(negative) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const replace: { + (i: number, b: B): (self: Chunk) => O.Option> + (self: Chunk, i: number, b: B): O.Option> +} = dual(3, (self: Chunk, i: number, b: B): O.Option> => modify(self, i, () => b)) + +/** + * Returns a non-empty `Chunk` of length `n` with element `i` initialized by `f(i)`. + * + * **Details** + * + * `n` is normalized to an integer greater than or equal to `1`. + * + * **Example** (Generating chunks from indices) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.makeBy(5, (i) => i * 2) + * console.log(Chunk.toArray(chunk)) // [0, 2, 4, 6, 8] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy: { + (f: (i: number) => A): (n: number) => NonEmptyChunk + (n: number, f: (i: number) => A): NonEmptyChunk +} = dual(2, (n, f) => fromIterable(RA.makeBy(n, f))) + +/** + * Creates a non-empty `Chunk` of consecutive integers from `start` through + * `end`, inclusive. + * + * **Details** + * + * If `start` is greater than `end`, returns a single-element chunk containing + * `start`. + * + * **Example** (Creating a range) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.range(1, 5) + * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end: number): NonEmptyChunk => + start <= end ? makeBy(end - start + 1, (i) => start + i) : of(start) + +// ------------------------------------------------------------------------------------- +// re-exports from ReadonlyArray +// ------------------------------------------------------------------------------------- + +/** + * Returns a function that checks if a `Chunk` contains a given value using the default `Equivalence`. + * + * **Example** (Checking membership) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * console.log(Chunk.contains(chunk, 3)) // true + * console.log(Chunk.contains(chunk, 6)) // false + * + * // Works with strings + * const words = Chunk.make("apple", "banana", "cherry") + * console.log(Chunk.contains(words, "banana")) // true + * console.log(Chunk.contains(words, "grape")) // false + * + * // Empty chunk + * const empty = Chunk.empty() + * console.log(Chunk.contains(empty, 1)) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Chunk) => boolean + (self: Chunk, a: A): boolean +} = RA.contains + +/** + * Returns a function that checks if a `Chunk` contains a given value using a provided `isEquivalent` function. + * + * **Example** (Checking membership with custom equivalence) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make({ id: 1, name: "Alice" }, { id: 2, name: "Bob" }) + * + * // Custom equivalence by id + * const containsById = Chunk.containsWith<{ id: number; name: string }>((a, b) => + * a.id === b.id + * ) + * console.log(containsById(chunk, { id: 1, name: "Different" })) // true + * console.log(containsById(chunk, { id: 3, name: "Charlie" })) // false + * + * // Case-insensitive string comparison + * const words = Chunk.make("Apple", "Banana", "Cherry") + * const containsCaseInsensitive = Chunk.containsWith((a, b) => + * a.toLowerCase() === b.toLowerCase() + * ) + * console.log(containsCaseInsensitive(words, "apple")) // true + * console.log(containsCaseInsensitive(words, "grape")) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const containsWith: ( + isEquivalent: (self: A, that: A) => boolean +) => { + (a: A): (self: Chunk) => boolean + (self: Chunk, a: A): boolean +} = RA.containsWith + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * **Example** (Finding the first matching element) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.findFirst(chunk, (n) => n > 3) + * console.log(Option.isSome(result)) // true + * console.log(Option.getOrElse(result, () => 0)) // 4 + * + * // No match found + * const notFound = Chunk.findFirst(chunk, (n) => n > 10) + * console.log(Option.isNone(notFound)) // true + * + * // With type refinement + * const mixed = Chunk.make(1, "hello", 2, "world", 3) + * const firstString = Chunk.findFirst( + * mixed, + * (x): x is string => typeof x === "string" + * ) + * console.log(Option.getOrElse(firstString, () => "")) // "hello" + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (refinement: Refinement, B>): (self: Chunk) => Option + (predicate: Predicate>): (self: Chunk) => Option + (self: Chunk, refinement: Refinement): Option + (self: Chunk, predicate: Predicate): Option +} = RA.findFirst + +/** + * Returns the first index for which a predicate holds. + * + * **Example** (Finding the first matching index) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.findFirstIndex(chunk, (n) => n > 3) + * console.log(result) // Option.some(3) + * + * // No match found + * const notFound = Chunk.findFirstIndex(chunk, (n) => n > 10) + * console.log(notFound) // Option.none() + * + * // Find first even number + * const firstEven = Chunk.findFirstIndex(chunk, (n) => n % 2 === 0) + * console.log(firstEven) // Option.some(1) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirstIndex: { + (predicate: Predicate): (self: Chunk) => O.Option + (self: Chunk, predicate: Predicate): O.Option +} = dual( + 2, + (self: Chunk, predicate: Predicate): O.Option => RA.findFirstIndex(self, predicate) +) + +/** + * Finds the last element for which a predicate holds. + * + * **Example** (Finding the last matching element) + * + * ```ts + * import { Chunk, Option } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.findLast(chunk, (n) => n < 4) + * console.log(Option.isSome(result)) // true + * console.log(Option.getOrElse(result, () => 0)) // 3 + * + * // No match found + * const notFound = Chunk.findLast(chunk, (n) => n > 10) + * console.log(Option.isNone(notFound)) // true + * + * // Find last even number + * const lastEven = Chunk.findLast(chunk, (n) => n % 2 === 0) + * console.log(Option.getOrElse(lastEven, () => 0)) // 4 + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (refinement: Refinement, B>): (self: Chunk) => Option + (predicate: Predicate>): (self: Chunk) => Option + (self: Chunk, refinement: Refinement): Option + (self: Chunk, predicate: Predicate): Option +} = RA.findLast + +/** + * Returns the last index for which a predicate holds. + * + * **Example** (Finding the last matching index) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const result = Chunk.findLastIndex(chunk, (n) => n < 4) + * console.log(result) // Option.some(2) + * + * // No match found + * const notFound = Chunk.findLastIndex(chunk, (n) => n > 10) + * console.log(notFound) // Option.none() + * + * // Find last even number index + * const lastEven = Chunk.findLastIndex(chunk, (n) => n % 2 === 0) + * console.log(lastEven) // Option.some(3) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findLastIndex: { + (predicate: Predicate): (self: Chunk) => O.Option + (self: Chunk, predicate: Predicate): O.Option +} = dual( + 2, + (self: Chunk, predicate: Predicate): O.Option => RA.findLastIndex(self, predicate) +) + +/** + * Checks whether a predicate holds true for every `Chunk` element. + * + * **Example** (Checking every element) + * + * ```ts + * import { Chunk } from "effect" + * + * const allPositive = Chunk.make(1, 2, 3, 4, 5) + * console.log(Chunk.every(allPositive, (n) => n > 0)) // true + * console.log(Chunk.every(allPositive, (n) => n > 3)) // false + * + * // Empty chunk returns true + * const empty = Chunk.empty() + * console.log(Chunk.every(empty, (n) => n > 0)) // true + * + * // Type refinement + * const mixed = Chunk.make(1, 2, 3) + * if (Chunk.every(mixed, (x): x is number => typeof x === "number")) { + * // mixed is now typed as Chunk + * console.log("All elements are numbers") + * } + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const every: { + (refinement: Refinement, B>): (self: Chunk) => self is Chunk + (predicate: Predicate): (self: Chunk) => boolean + (self: Chunk, refinement: Refinement): self is Chunk + (self: Chunk, predicate: Predicate): boolean +} = dual( + 2, + (self: Chunk, refinement: Refinement): self is Chunk => + RA.fromIterable(self).every(refinement) +) + +/** + * Checks whether a predicate holds true for some `Chunk` element. + * + * **Example** (Checking for some matching element) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * console.log(Chunk.some(chunk, (n) => n > 4)) // true + * console.log(Chunk.some(chunk, (n) => n > 10)) // false + * + * // Empty chunk returns false + * const empty = Chunk.empty() + * console.log(Chunk.some(empty, (n) => n > 0)) // false + * + * // Check for specific value + * const words = Chunk.make("apple", "banana", "cherry") + * console.log(Chunk.some(words, (word) => word.includes("ban"))) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const some: { + (predicate: Predicate>): (self: Chunk) => self is NonEmptyChunk + (self: Chunk, predicate: Predicate): self is NonEmptyChunk +} = dual( + 2, + (self: Chunk, predicate: Predicate): self is NonEmptyChunk => RA.fromIterable(self).some(predicate) +) + +/** + * Joins the elements together with "sep" in the middle. + * + * **Example** (Joining chunks into a string) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make("apple", "banana", "cherry") + * const result = Chunk.join(chunk, ", ") + * console.log(result) // "apple, banana, cherry" + * + * // With different separator + * const withPipe = Chunk.join(chunk, " | ") + * console.log(withPipe) // "apple | banana | cherry" + * + * // Empty chunk + * const empty = Chunk.empty() + * console.log(Chunk.join(empty, ", ")) // "" + * + * // Single element + * const single = Chunk.make("hello") + * console.log(Chunk.join(single, ", ")) // "hello" + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const join: { + (sep: string): (self: Chunk) => string + (self: Chunk, sep: string): string +} = RA.join + +/** + * Reduces the elements of a chunk from left to right. + * + * **Example** (Reducing from the left) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4, 5) + * const sum = Chunk.reduce(chunk, 0, (acc, n) => acc + n) + * console.log(sum) // 15 + * + * // String concatenation with index + * const words = Chunk.make("a", "b", "c") + * const result = Chunk.reduce(words, "", (acc, word, i) => acc + `${i}:${word} `) + * console.log(result) // "0:a 1:b 2:c " + * + * // Find maximum + * const max = Chunk.reduce(chunk, -Infinity, (acc, n) => Math.max(acc, n)) + * console.log(max) // 5 + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Chunk) => B + (self: Chunk, b: B, f: (b: B, a: A, i: number) => B): B +} = RA.reduce + +/** + * Reduces the elements of a chunk from right to left. + * + * **Example** (Reducing from the right) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk = Chunk.make(1, 2, 3, 4) + * const result = Chunk.reduceRight(chunk, 0, (acc, n) => acc + n) + * console.log(result) // 10 + * + * // String building (right to left) + * const words = Chunk.make("a", "b", "c") + * const reversed = Chunk.reduceRight( + * words, + * "", + * (acc, word, i) => acc + `${i}:${word} ` + * ) + * console.log(reversed) // "2:c 1:b 0:a " + * + * // Subtract from right to left + * const subtraction = Chunk.reduceRight(chunk, 0, (acc, n) => n - acc) + * console.log(subtraction) // -2 (4 - (3 - (2 - (1 - 0)))) + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduceRight: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Chunk) => B + (self: Chunk, b: B, f: (b: B, a: A, i: number) => B): B +} = RA.reduceRight + +/** + * Creates a `Chunk` of values not included in the other given `Chunk` using the provided `isEquivalent` function. + * The order and references of result values are determined by the first `Chunk`. + * + * **Example** (Computing difference with custom equivalence) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk1 = Chunk.make({ id: 1, name: "Alice" }, { id: 2, name: "Bob" }) + * const chunk2 = Chunk.make({ id: 1, name: "Alice" }, { id: 3, name: "Charlie" }) + * + * // Custom equivalence by id + * const byId = Chunk.differenceWith<{ id: number; name: string }>((a, b) => + * a.id === b.id + * ) + * const result = byId(chunk1, chunk2) + * console.log(Chunk.toArray(result)) // [{ id: 2, name: "Bob" }] + * + * // String comparison case-insensitive + * const words1 = Chunk.make("Apple", "Banana", "Cherry") + * const words2 = Chunk.make("apple", "grape") + * const caseInsensitive = Chunk.differenceWith((a, b) => + * a.toLowerCase() === b.toLowerCase() + * ) + * const wordDiff = caseInsensitive(words1, words2) + * console.log(Chunk.toArray(wordDiff)) // ["Banana", "Cherry"] + * ``` + * + * @category filtering + * @since 3.2.0 + */ +export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} => { + return dual( + 2, + (self: Chunk, that: Chunk): Chunk => fromArrayUnsafe(RA.differenceWith(isEquivalent)(self, that)) + ) +} + +/** + * Creates a `Chunk` of values not included in the other given `Chunk`. + * The order and references of result values are determined by the first `Chunk`. + * + * **Example** (Computing chunk difference) + * + * ```ts + * import { Chunk } from "effect" + * + * const chunk1 = Chunk.make(1, 2, 3, 4, 5) + * const chunk2 = Chunk.make(3, 4, 6, 7) + * const result = Chunk.difference(chunk1, chunk2) + * console.log(Chunk.toArray(result)) // [1, 2, 5] + * + * // String difference + * const words1 = Chunk.make("apple", "banana", "cherry") + * const words2 = Chunk.make("banana", "grape") + * const wordDiff = Chunk.difference(words1, words2) + * console.log(Chunk.toArray(wordDiff)) // ["apple", "cherry"] + * + * // Empty second chunk returns original + * const empty = Chunk.empty() + * const unchanged = Chunk.difference(chunk1, empty) + * console.log(Chunk.toArray(unchanged)) // [1, 2, 3, 4, 5] + * ``` + * + * @category filtering + * @since 3.2.0 + */ +export const difference: { + (that: Chunk): (self: Chunk) => Chunk + (self: Chunk, that: Chunk): Chunk +} = dual( + 2, + (self: Chunk, that: Chunk): Chunk => fromArrayUnsafe(RA.difference(self, that)) +) diff --git a/repos/effect/packages/effect/src/Clock.ts b/repos/effect/packages/effect/src/Clock.ts new file mode 100644 index 0000000..03a8b21 --- /dev/null +++ b/repos/effect/packages/effect/src/Clock.ts @@ -0,0 +1,194 @@ +/** + * Service and helpers for reading time and sleeping inside Effect programs. + * The active `Clock` provides current time in milliseconds or nanoseconds and a + * `sleep` operation for delaying work. Because time is accessed through a + * service, tests can replace the clock with a controlled implementation. + * + * @since 2.0.0 + */ +import type * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import type { Effect } from "./Effect.ts" +import * as effect from "./internal/effect.ts" + +/** + * Represents a time-based clock which provides functionality related to time + * and scheduling. + * + * **When to use** + * + * Use to define or provide a clock service for current-time and sleep + * operations. + * + * **Example** (Reading current time) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * const clockOperations = Effect.gen(function*() { + * const currentTime = yield* Clock.currentTimeMillis + * const currentTimeNanos = yield* Clock.currentTimeNanos + * + * console.log(`Current time (ms): ${currentTime}`) + * console.log(`Current time (ns): ${currentTimeNanos}`) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Clock { + /** + * Returns the current time in milliseconds unsafely. + * + * **When to use** + * + * Use to read millisecond time synchronously when you already have a `Clock` + * service and can accept non-effectful access. + */ + currentTimeMillisUnsafe(): number + /** + * Returns the current time in milliseconds. + * + * **When to use** + * + * Use to read millisecond time through this `Clock` service in `Effect`. + */ + readonly currentTimeMillis: Effect + /** + * Returns the current time in nanoseconds unsafely. + * + * **When to use** + * + * Use to read nanosecond time synchronously when you already have a `Clock` + * service and can accept non-effectful access. + */ + currentTimeNanosUnsafe(): bigint + /** + * Returns the current time in nanoseconds. + * + * **When to use** + * + * Use to read nanosecond time through this `Clock` service in `Effect`. + */ + readonly currentTimeNanos: Effect + /** + * Asynchronously sleeps for the specified duration. + * + * **When to use** + * + * Use to delay an `Effect` workflow by a duration through this `Clock` service. + */ + sleep(duration: Duration.Duration): Effect +} + +/** + * Context reference for the active time service in the environment. + * + * **When to use** + * + * Use when you need to access or provide the full time service, including sleep + * operations, rather than a single timestamp accessor. + * + * **Example** (Accessing the Clock service) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const clock = yield* Clock.Clock + * return clock.currentTimeMillisUnsafe() + * }) + * ``` + * + * @see {@link clockWith} for using the current Clock service inside an effect + * @see {@link currentTimeMillis} for reading the current time in milliseconds + * @see {@link currentTimeNanos} for reading the current time in nanoseconds + * + * @category references + * @since 2.0.0 + */ +export const Clock: Context.Reference = effect.ClockRef + +/** + * Accesses the current Clock service and uses it to run the provided function. + * + * **When to use** + * + * Use when you need the full Clock service interface to perform multiple time + * operations or call unsafe variants within a single effect. + * + * **Example** (Accessing the current Clock service) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * const program = Clock.clockWith((clock) => + * Effect.sync(() => { + * const currentTime = clock.currentTimeMillisUnsafe() + * console.log(`Current time: ${currentTime}`) + * return currentTime + * }) + * ) + * ``` + * + * @see {@link Clock} for the service reference + * @see {@link currentTimeMillis} for convenience accessor that returns milliseconds + * @see {@link currentTimeNanos} for convenience accessor that returns nanoseconds + * @category constructors + * @since 2.0.0 + */ +export const clockWith: (f: (clock: Clock) => Effect) => Effect = effect.clockWith + +/** + * Returns an Effect that succeeds with the current time in milliseconds. + * + * **When to use** + * + * Use to read wall-clock time from the active Clock service with millisecond + * precision. + * + * **Example** (Reading milliseconds) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const currentTime = yield* Clock.currentTimeMillis + * console.log(`Current time: ${currentTime}ms`) + * return currentTime + * }) + * ``` + * + * @see {@link currentTimeNanos} for nanosecond precision + * @see {@link clockWith} for accessing the full Clock service + * + * @category constructors + * @since 2.0.0 + */ +export const currentTimeMillis: Effect = effect.currentTimeMillis + +/** + * Returns an Effect that succeeds with the current time in nanoseconds. + * + * **When to use** + * + * Use to read wall-clock time from the active `Clock` service with nanosecond + * precision. + * + * **Example** (Reading nanoseconds) + * + * ```ts + * import { Clock, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const currentTime = yield* Clock.currentTimeNanos + * console.log(`Current time: ${currentTime}ns`) + * return currentTime + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const currentTimeNanos: Effect = effect.currentTimeNanos diff --git a/repos/effect/packages/effect/src/Combiner.ts b/repos/effect/packages/effect/src/Combiner.ts new file mode 100644 index 0000000..1e9ac7c --- /dev/null +++ b/repos/effect/packages/effect/src/Combiner.ts @@ -0,0 +1,316 @@ +/** + * Defines reusable rules for merging two values of the same type. + * + * A `Combiner` contains one operation, `combine(self, that)`, which returns + * the merged value. It does not define an initial value for reducing a + * collection; use a `Reducer` when you need that. This module includes the + * `Combiner` interface, a constructor for custom combining logic, and common + * combiners for choosing or ordering values. + * + * @since 4.0.0 + */ +import type * as Order from "./Order.ts" + +/** + * Represents a strategy for combining two values of the same type `A`. A + * `Combiner` contains a single `combine` method that takes two values and + * returns a merged result. It does not include an identity/empty value; use + * `Reducer` when you need one. + * + * **When to use** + * + * Use when you need to describe how two values of the same type + * merge, pass a reusable combining strategy to library functions like + * `Struct.makeCombiner` or `Option.makeCombinerFailFast`, or define the + * combining step for a `Reducer`. + * + * **Example** (Combining numbers with addition) + * + * ```ts + * import { Combiner } from "effect" + * + * const Sum = Combiner.make((self, that) => self + that) + * + * console.log(Sum.combine(3, 4)) + * // Output: 7 + * ``` + * + * @see {@link make} – create a `Combiner` from a function + * @category models + * @since 4.0.0 + */ +export interface Combiner { + /** + * Combines two values into a new value. + * + * **When to use** + * + * Use to merge two values according to this combining strategy. + */ + readonly combine: (self: A, that: A) => A +} + +/** + * Creates a `Combiner` from a binary function. + * + * **When to use** + * + * Use when you have a custom combining operation that is not covered by + * the built-in constructors (`min`, `max`, `first`, `last`, `constant`). + * + * **Details** + * + * The returned combiner's `combine` method delegates to the provided function. + * Any purity, associativity, or mutation behavior comes from that function. + * + * **Example** (Multiplying numbers) + * + * ```ts + * import { Combiner } from "effect" + * + * const Product = Combiner.make((self, that) => self * that) + * + * console.log(Product.combine(3, 5)) + * // Output: 15 + * ``` + * + * @see {@link Combiner} – the interface this creates + * @category constructors + * @since 4.0.0 + */ +export function make(combine: (self: A, that: A) => A): Combiner { + return { combine } +} + +/** + * Reverses the argument order of a combiner's `combine` method. + * + * **When to use** + * + * Use when you want the right-hand value to act as the accumulator, or need to + * reverse a non-commutative combiner such as string concatenation. + * + * **Details** + * + * Returns a new `Combiner` where `combine(self, that)` calls the original + * combiner as `combine(that, self)`. + * + * **Example** (Reversing string concatenation) + * + * ```ts + * import { Combiner, String } from "effect" + * + * const Prepend = Combiner.flip(String.ReducerConcat) + * + * console.log(Prepend.combine("a", "b")) + * // Output: "ba" + * ``` + * + * @see {@link make} + * @category combinators + * @since 4.0.0 + */ +export function flip(combiner: Combiner): Combiner { + return make((self, that) => combiner.combine(that, self)) +} + +/** + * Creates a `Combiner` that returns the smaller of two values according to + * the provided `Order`. + * + * **When to use** + * + * Use when you want to accumulate the minimum value across a collection or + * build a `Reducer` that tracks the running minimum. + * + * **Details** + * + * The combiner compares values using the given `Order`. When values are equal, + * it returns `that` (the second argument). + * + * **Example** (Selecting the minimum of two numbers) + * + * ```ts + * import { Combiner, Number } from "effect" + * + * const Min = Combiner.min(Number.Order) + * + * console.log(Min.combine(3, 1)) + * // Output: 1 + * + * console.log(Min.combine(1, 3)) + * // Output: 1 + * ``` + * + * @see {@link max} + * @category constructors + * @since 4.0.0 + */ +export function min(order: Order.Order): Combiner { + return make((self, that) => order(self, that) === -1 ? self : that) +} + +/** + * Creates a `Combiner` that returns the larger of two values according to + * the provided `Order`. + * + * **When to use** + * + * Use when you want to accumulate the maximum value across a collection or + * build a `Reducer` that tracks the running maximum. + * + * **Details** + * + * The combiner compares values using the given `Order`. When values are equal, + * it returns `that` (the second argument). + * + * **Example** (Selecting the maximum of two numbers) + * + * ```ts + * import { Combiner, Number } from "effect" + * + * const Max = Combiner.max(Number.Order) + * + * console.log(Max.combine(3, 1)) + * // Output: 3 + * + * console.log(Max.combine(1, 3)) + * // Output: 3 + * ``` + * + * @see {@link min} + * @category constructors + * @since 4.0.0 + */ +export function max(order: Order.Order): Combiner { + return make((self, that) => order(self, that) === 1 ? self : that) +} + +/** + * Creates a `Combiner` that always returns the first (left) argument. + * + * **When to use** + * + * Use when you want "first write wins" semantics while merging values. + * + * **Details** + * + * `combine(self, that)` returns `self` and ignores `that`. + * + * **Example** (Keeping the first value) + * + * ```ts + * import { Combiner } from "effect" + * + * const First = Combiner.first() + * + * console.log(First.combine(1, 2)) + * // Output: 1 + * ``` + * + * @see {@link last} + * @category constructors + * @since 4.0.0 + */ +export function first(): Combiner { + return make((self, _) => self) +} + +/** + * Creates a `Combiner` that always returns the last (right) argument. + * + * **When to use** + * + * Use when you want "last write wins" semantics while merging values. + * + * **Details** + * + * `combine(self, that)` returns `that` and ignores `self`. + * + * **Example** (Keeping the last value) + * + * ```ts + * import { Combiner } from "effect" + * + * const Last = Combiner.last() + * + * console.log(Last.combine(1, 2)) + * // Output: 2 + * ``` + * + * @see {@link first} + * @category constructors + * @since 4.0.0 + */ +export function last(): Combiner { + return make((_, that) => that) +} + +/** + * Creates a `Combiner` that ignores both arguments and always returns the + * given constant value. + * + * **When to use** + * + * Use when you need a combiner that always returns a fixed value, including + * when a generic API requires a combiner but the result is predetermined. + * + * **Details** + * + * `combine(self, that)` returns the constant `a` and ignores both arguments. + * + * **Example** (Always returning zero) + * + * ```ts + * import { Combiner } from "effect" + * + * const Zero = Combiner.constant(0) + * + * console.log(Zero.combine(42, 99)) + * // Output: 0 + * ``` + * + * @see {@link first} + * @see {@link last} + * @category constructors + * @since 4.0.0 + */ +export function constant(a: A): Combiner { + return make(() => a) +} + +/** + * Wraps a `Combiner` so that a separator value is inserted between every + * pair of combined elements. + * + * **When to use** + * + * Use when you need to inject a fixed separator between accumulated values, + * such as when building delimited strings, paths, or CSV-like output by + * repeated combination. + * + * **Details** + * + * `intercalate(middle)(combiner).combine(self, that)` is equivalent to + * `combiner.combine(self, combiner.combine(middle, that))`. This function is + * curried: first provide the separator, then the base combiner. + * + * **Example** (Joining strings with a separator) + * + * ```ts + * import { Combiner, String } from "effect" + * + * const commaSep = Combiner.intercalate(",")(String.ReducerConcat) + * + * console.log(commaSep.combine("a", "b")) + * // Output: "a,b" + * ``` + * + * @see {@link make} + * @category combinators + * @since 4.0.0 + */ +export function intercalate(middle: A) { + return (combiner: Combiner): Combiner => + make((self, that) => combiner.combine(self, combiner.combine(middle, that))) +} diff --git a/repos/effect/packages/effect/src/Config.ts b/repos/effect/packages/effect/src/Config.ts new file mode 100644 index 0000000..026b4e0 --- /dev/null +++ b/repos/effect/packages/effect/src/Config.ts @@ -0,0 +1,1425 @@ +/** + * Descriptions of configuration values that can be read from a + * `ConfigProvider`. A `Config` explains which keys to read, how to decode + * and validate them, and how to combine defaults, fallbacks, nested paths, and + * multiple settings. Configs are also Effects, so they can be yielded in + * `Effect.gen` after a provider has been supplied. + * + * @since 4.0.0 + */ +import type { Path, SourceError } from "./ConfigProvider.ts" +import * as ConfigProvider from "./ConfigProvider.ts" +import * as Effect from "./Effect.ts" +import * as Effectable from "./Effectable.ts" +import { dual } from "./Function.ts" +import * as LogLevel_ from "./LogLevel.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Rec from "./Record.ts" +import * as Schema from "./Schema.ts" +import * as SchemaAST from "./SchemaAST.ts" +import * as SchemaGetter from "./SchemaGetter.ts" +import * as SchemaIssue from "./SchemaIssue.ts" +import * as SchemaParser from "./SchemaParser.ts" +import * as SchemaTransformation from "./SchemaTransformation.ts" + +const TypeId = "~effect/Config" + +/** + * Returns `true` if `u` is a `Config` instance. + * + * **When to use** + * + * Use when you need to distinguish a `Config` from an unknown value before + * calling `.parse` or {@link unwrap}. + * + * **Example** (Checking Config values) + * + * ```ts + * import { Config } from "effect" + * + * console.log(Config.isConfig(Config.string("HOST"))) // true + * console.log(Config.isConfig("not a config")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isConfig = (u: unknown): u is Config => Predicate.hasProperty(u, TypeId) + +/** + * Represents the error type produced when config loading or validation fails. + * + * **When to use** + * + * Use when you need to inspect config loading or validation failures. + * + * **Details** + * + * Wraps either: + * - A `SourceError` — the provider could not read data (I/O failure). + * - A `SchemaError` — the data was found but did not match the schema + * (wrong type, out of range, missing key, etc.). + * + * @see {@link orElse} – recover from a ConfigError + * @see {@link withDefault} – provide a fallback for missing-data errors + * + * @category errors + * @since 4.0.0 + */ +export class ConfigError { + readonly _tag = "ConfigError" + readonly name: string = "ConfigError" + readonly cause: SourceError | Schema.SchemaError + constructor(cause: SourceError | Schema.SchemaError) { + this.cause = cause + } + get message() { + return this.cause.toString() + } + toString() { + return `ConfigError(${this.message})` + } +} + +/** + * A recipe for extracting a typed value `T` from a `ConfigProvider`. + * + * **When to use** + * + * Use to describe typed configuration that can be parsed from a provider or + * yielded inside `Effect.gen`. + * + * **Details** + * + * Key members: + * - `parse(provider, pathPrefix?)` – runs the config against a specific provider. + * The optional path prefix is the logical scope accumulated from outer + * `Config.nested` calls. + * - Yieldable – can be yielded inside `Effect.gen`, which automatically + * resolves the current `ConfigProvider` from the context. + * - Pipeable – supports `.pipe(Config.map(...))` etc. + * + * @see {@link schema} – the main way to create a Config + * + * @category models + * @since 2.0.0 + */ +export interface Config extends Effect.Effect { + readonly [TypeId]: typeof TypeId + readonly parse: ( + provider: ConfigProvider.ConfigProvider, + pathPrefix?: Path + ) => Effect.Effect +} + +const Proto = { + ...Effectable.Prototype>({ + label: "Config", + evaluate(fiber) { + return this.parse(fiber.getRef(ConfigProvider.ConfigProvider)) + } + }), + [TypeId]: TypeId, + toJSON(this: Config) { + return { + _id: "Config" + } + } +} + +function make( + parse: (provider: ConfigProvider.ConfigProvider, pathPrefix: Path) => Effect.Effect +): Config { + const self = Object.create(Proto) + self.parse = (provider: ConfigProvider.ConfigProvider, pathPrefix: Path = []) => parse(provider, pathPrefix) + return self +} + +/** + * Transforms the parsed value of a config with a pure function. + * + * **When to use** + * + * Use when you need to transform a parsed config value with a function that + * cannot fail. + * + * **Example** (Uppercasing a string config) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const upper = Config.string("name").pipe( + * Config.map((s) => s.toUpperCase()) + * ) + * + * const provider = ConfigProvider.fromUnknown({ name: "alice" }) + * // Effect.runSync(upper.parse(provider)) // "ALICE" + * ``` + * + * @see {@link mapOrFail} – when the transformation can fail + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => B): (self: Config) => Config + (self: Config, f: (a: A) => B): Config +} = dual(2, (self: Config, f: (a: A) => B): Config => { + return make((provider, pathPrefix) => Effect.map(self.parse(provider, pathPrefix), f)) +}) + +/** + * Transforms the parsed value with a function that may fail. + * + * **When to use** + * + * Use when you need to transform a parsed config value with a function that can + * produce a `ConfigError` (e.g. parsing a URL, checking a range). + * + * **Example** (Wrapping a value in an effectful transformation) + * + * ```ts + * import { Config, Effect } from "effect" + * + * const trimmed = Config.string("name").pipe( + * Config.mapOrFail((s) => Effect.succeed(s.trim())) + * ) + * ``` + * + * @see {@link map} – when the transformation cannot fail + * + * @category mapping + * @since 2.0.0 + */ +export const mapOrFail: { + (f: (a: A) => Effect.Effect): (self: Config) => Config + (self: Config, f: (a: A) => Effect.Effect): Config +} = dual(2, (self: Config, f: (a: A) => Effect.Effect): Config => { + return make((provider, pathPrefix) => Effect.flatMap(self.parse(provider, pathPrefix), f)) +}) + +/** + * Provides a fallback config when parsing fails with a `ConfigError`. + * + * **When to use** + * + * Use when you need to try an alternative config source after the primary one + * fails. + * + * **Details** + * + * Unlike {@link withDefault}, this catches **all** `ConfigError`s (not just + * missing data). The fallback function receives the error and returns a new + * `Config`. + * + * **Example** (Falling back to a literal) + * + * ```ts + * import { Config } from "effect" + * + * const hostConfig = Config.string("HOST").pipe( + * Config.orElse(() => Config.succeed("localhost")) + * ) + * ``` + * + * @see {@link withDefault} – fallback only on missing data + * + * @category combinators + * @since 2.0.0 + */ +export const orElse: { + (that: (error: ConfigError) => Config): (self: Config) => Config + (self: Config, that: (error: ConfigError) => Config): Config +} = dual(2, (self: Config, that: (error: ConfigError) => Config): Config => { + return make((provider, pathPrefix) => + Effect.catch(self.parse(provider, pathPrefix), (error) => that(error).parse(provider, pathPrefix)) + ) +}) + +/** + * Combines multiple configs into a single config that parses all of them. + * + * **When to use** + * + * Use when you need to group related configs into a tuple or named struct. + * + * **Details** + * + * Accepts a tuple (preserves positions), an iterable, or a record of configs. + * Returns a config whose parsed value mirrors the input shape. + * + * **Example** (Combining configs as a struct) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const dbConfig = Config.all({ + * host: Config.string("host"), + * port: Config.number("port") + * }) + * + * const provider = ConfigProvider.fromUnknown({ host: "localhost", port: 5432 }) + * // Effect.runSync(dbConfig.parse(provider)) + * // { host: "localhost", port: 5432 } + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export function all> | Record>>( + arg: Arg +): Config< + [Arg] extends [ReadonlyArray>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config] ? A : never + } + : [Arg] extends [Iterable>] ? Array + : [Arg] extends [Record>] ? { + -readonly [K in keyof Arg]: [Arg[K]] extends [Config] ? A : never + } + : never +> { + const configs: Array> | Record> = Array.isArray(arg) + ? arg + : Symbol.iterator in arg + ? [...arg as any] + : arg + if (Array.isArray(configs)) { + return make((provider, pathPrefix) => + Effect.all(configs.map((config) => config.parse(provider, pathPrefix))) + ) as any + } else { + return make((provider, pathPrefix) => + Effect.all(Rec.map(configs, (config) => config.parse(provider, pathPrefix))) + ) as any + } +} + +function isMissingDataOnly(issue: SchemaIssue.Issue): boolean { + switch (issue._tag) { + case "MissingKey": + return true + case "InvalidType": + case "InvalidValue": + return Option.isNone(issue.actual) || (Option.isSome(issue.actual) && issue.actual.value === undefined) + case "OneOf": + return issue.actual === undefined + case "Encoding": + return Option.isNone(issue.actual) || (Option.isSome(issue.actual) && issue.actual.value === undefined) + ? true + : isMissingDataOnly(issue.issue) + case "Pointer": + return isMissingDataOnly(issue.issue) + case "Filter": + case "UnexpectedKey": + case "Forbidden": + return false + case "Composite": + return issue.issues.every(isMissingDataOnly) + case "AnyOf": + if (issue.issues.length === 0) { + return issue.actual === undefined + } + return issue.issues.every(isMissingDataOnly) + } +} + +/** + * Provides a fallback value when the config fails due to missing data. + * + * **When to use** + * + * Use when you need to make a config key optional with a sensible default. + * + * **Gotchas** + * + * Only applies when the error is a `SchemaError` caused exclusively by + * missing data (missing keys, undefined values). Validation errors (wrong + * type, out of range) still propagate. + * + * **Example** (Defaulting a missing port) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const port = Config.number("port").pipe(Config.withDefault(3000)) + * + * const provider = ConfigProvider.fromUnknown({}) + * // Effect.runSync(port.parse(provider)) // 3000 + * ``` + * + * @see {@link option} – returns `Option` instead of a default value + * @see {@link orElse} – catches all errors, not just missing data + * + * @category combinators + * @since 2.0.0 + */ +export const withDefault: { + (defaultValue: A2): (self: Config) => Config + (self: Config, defaultValue: A2): Config +} = dual(2, (self: Config, defaultValue: A2): Config => { + return orElse(self, (err) => { + if (Schema.isSchemaError(err.cause)) { + const issue = err.cause.issue + if (isMissingDataOnly(issue)) { + return succeed(defaultValue) + } + } + return fail(err.cause) + }) +}) + +/** + * Makes a config optional: returns `Some(value)` on success and `None` when + * data is missing. + * + * **When to use** + * + * Use when you need to handle a config key that may or may not be present. + * + * **Gotchas** + * + * Like {@link withDefault}, only missing-data errors produce `None`. + * Validation errors still propagate. + * + * **Example** (Reading optional config) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const maybePort = Config.option(Config.number("port")) + * + * const provider = ConfigProvider.fromUnknown({}) + * // Effect.runSync(maybePort.parse(provider)) // { _tag: "None" } + * ``` + * + * @see {@link withDefault} – provide a concrete fallback value instead + * + * @category combinators + * @since 2.0.0 + */ +export const option = (self: Config): Config> => + self.pipe(map(Option.some), withDefault(Option.none())) + +/** + * Extracts the successfully parsed value type from a `Config`. + * + * **When to use** + * + * Use to derive the parsed value type from an existing `Config` value when + * declaring reusable config-driven types. + * + * @see {@link Config} for the config type whose parsed value is extracted + * @see {@link Effect.Success} for extracting the success type from any `Effect` + * + * @category utility types + * @since 2.5.0 + */ +export type Success = [T] extends [Config] ? A : never + +/** + * Utility type that recursively replaces primitives with `Config` in a nested + * structure. + * + * **When to use** + * + * Use when typing the input of {@link unwrap} so callers can pass either a `Config` + * or a record of `Config`s. + * + * **Details** + * + * `Config.Wrap<{ key: string }>` becomes `{ key: Config } | Config<{ key: string }>` + * + * @see {@link unwrap} – construct a `Config` from a `Wrap` + * + * @category Wrap + * @since 2.0.0 + */ +export type Wrap = [NonNullable] extends [infer T] ? [IsPlainObject] extends [true] ? + | { readonly [K in keyof A]: Wrap } + | Config + : Config + : Config + +type IsPlainObject = [A] extends [Record] + ? [keyof A] extends [never] ? false : [keyof A] extends [string] ? true : false + : false + +/** + * Constructs a `Config` from a value matching `Wrap`. + * + * **When to use** + * + * Use when accepting config from callers who may pass either a single `Config` or a + * record of individual `Config`s. + * + * **Details** + * + * If the input is already a `Config`, it is returned as-is. Otherwise, each + * key is recursively unwrapped and combined. + * + * **Example** (Unwrapping a record of configs) + * + * ```ts + * import { Config } from "effect" + * + * interface Options { + * key: string + * } + * + * const makeConfig = (config: Config.Wrap): Config.Config => + * Config.unwrap(config) + * ``` + * + * @see {@link Wrap} – the utility type accepted by this function + * + * @category Wrap + * @since 2.0.0 + */ +export const unwrap = (wrapped: Wrap): Config => { + if (isConfig(wrapped)) return wrapped + return make((provider, pathPrefix) => { + const entries = Object.entries(wrapped) + const configs = entries.map(([key, config]) => + unwrap(config as any).parse(provider, pathPrefix).pipe(Effect.map((value) => [key, value] as const)) + ) + return Effect.all(configs).pipe(Effect.map(Object.fromEntries)) + }) +} + +// ----------------------------------------------------------------------------- +// schema +// ----------------------------------------------------------------------------- + +const dump: ( + provider: ConfigProvider.ConfigProvider, + path: Path +) => Effect.Effect = Effect.fnUntraced(function*( + provider, + path +) { + const stat = yield* provider.load(path) + if (stat === undefined) return undefined + switch (stat._tag) { + case "Value": + return stat.value + case "Record": { + if (stat.value !== undefined) return stat.value + const out: Record = {} + for (const key of stat.keys) { + const child = yield* dump(provider, [...path, key]) + if (child !== undefined) out[key] = child + } + return out + } + case "Array": { + if (stat.value !== undefined) return stat.value + const out: Array = [] + for (let i = 0; i < stat.length; i++) { + out.push(yield* dump(provider, [...path, i])) + } + return out + } + } +}) + +const recur: ( + ast: SchemaAST.AST, + provider: ConfigProvider.ConfigProvider, + path: Path +) => Effect.Effect = Effect.fnUntraced( + function*(ast, provider, path) { + switch (ast._tag) { + case "Objects": { + const stat = yield* provider.load(path) + if (stat === undefined && path.length > 0) return undefined + const out: Record = {} + for (const ps of ast.propertySignatures) { + const name = ps.name + if (typeof name === "string") { + const value = yield* recur(ps.type, provider, [...path, name]) + if (value !== undefined) out[name] = value + } + } + if (ast.indexSignatures.length > 0) { + if (stat && stat._tag === "Record") { + for (const is of ast.indexSignatures) { + const matches = SchemaParser._is(is.parameter) + for (const key of stat.keys) { + if (!Object.hasOwn(out, key) && matches(key)) { + const value = yield* recur(is.type, provider, [...path, key]) + if (value !== undefined) out[key] = value + } + } + } + } + } + return out + } + case "Arrays": { + const stat = yield* provider.load(path) + if (stat === undefined) return undefined + if (stat && stat._tag === "Value") return stat.value === "" ? [] : stat.value.split(",") + if (stat && stat._tag === "Array" && stat.value !== undefined) { + return stat.value === "" ? [] : stat.value.split(",") + } + const out: Array = [] + const length = stat && stat._tag === "Array" ? stat.length : ast.elements.length + for (let i = 0; i < length; i++) { + const element = ast.elements[i] ?? ast.rest[0] + if (element !== undefined) { + out.push(yield* recur(element, provider, [...path, i])) + } + } + return out + } + case "Union": + // Let downstream decoding decide; dump can return a string, object, or array. + return yield* dump(provider, path) + case "Suspend": + return yield* recur(ast.thunk(), provider, path) + default: { + // Base primitives / string-like encoded nodes. + const stat = yield* provider.load(path) + if (stat === undefined) return undefined + if (stat._tag === "Value") return stat.value + if (stat._tag === "Record" && stat.value !== undefined) return stat.value + if (stat._tag === "Array" && stat.value !== undefined) return stat.value + // Container without a co-located value cannot satisfy a scalar request. + return undefined + } + } + } +) + +/** + * Creates a `Config` from a `Schema.Codec`. + * + * **When to use** + * + * Use when you need to read structured or schema-validated configuration. + * + * **Details** + * + * The optional `path` sets the local path segment(s) for the config lookup. + * It is appended to the logical path prefix accumulated from outer + * {@link nested} calls. Pass a single string for a flat key or an array for + * nested paths. + * + * Convenience constructors such as `string`, `number`, and `boolean` delegate + * to this API. + * + * The codec is used to decode the raw `StringTree` produced by the provider + * into `T`. Schema validation errors are wrapped in `ConfigError`. + * + * **Example** (Reading a structured config) + * + * ```ts + * import { Config, ConfigProvider, Effect, Schema } from "effect" + * + * const DbConfig = Config.schema( + * Schema.Struct({ + * host: Schema.String, + * port: Schema.Int + * }), + * "db" + * ) + * + * const provider = ConfigProvider.fromUnknown({ + * db: { host: "localhost", port: 5432 } + * }) + * + * // Effect.runSync(DbConfig.parse(provider)) + * // { host: "localhost", port: 5432 } + * ``` + * + * @see {@link string} / {@link number} / {@link boolean} – shortcuts for + * single-value configs + * + * @category schemas + * @since 4.0.0 + */ +export function schema(codec: Schema.ConstraintCodec, path?: string | ConfigProvider.Path): Config { + const codecStringTree = Schema.toCodecStringTree(codec) + const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(codecStringTree) + const codecStringTreeEncoded = SchemaAST.toEncoded(codecStringTree.ast) + const localPath = typeof path === "string" ? [path] : path ?? [] + return make((provider, pathPrefix) => { + const fullPath = [...pathPrefix, ...localPath] + return recur(codecStringTreeEncoded, provider, fullPath).pipe( + Effect.flatMapEager((tree) => + decodeUnknownEffect(tree).pipe( + Effect.mapErrorEager((issue) => + new Schema.SchemaError(fullPath.length > 0 ? new SchemaIssue.Pointer(fullPath, issue) : issue) + ) + ) + ), + Effect.mapErrorEager((cause) => new ConfigError(cause)) + ) + }) +} + +/** @internal */ +export const TrueValues = Schema.Literals(["true", "yes", "on", "1", "y"]) + +/** @internal */ +export const FalseValues = Schema.Literals(["false", "no", "off", "0", "n"]) + +/** + * Schema for boolean values encoded as strings. + * + * **When to use** + * + * Use when you need the reusable boolean schema value for `Config.schema` with + * custom paths. + * + * **Details** + * + * Accepted string values: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, + * `0`, `y`, `n` (case-sensitive). + * + * @see {@link boolean} – convenience constructor + * + * @category schemas + * @since 4.0.0 + */ +export const Boolean = Schema.Literals([...TrueValues.literals, ...FalseValues.literals]).pipe( + Schema.decodeTo( + Schema.Boolean, + SchemaTransformation.transform({ + decode: (value) => value === "true" || value === "yes" || value === "on" || value === "1" || value === "y", + encode: (value) => value ? "true" : "false" + }) + ) +) + +/** + * Schema for port numbers (integers in 1–65535). + * + * **When to use** + * + * Use when you need the reusable port schema value for `Config.schema` with + * custom paths. + * + * @see {@link port} – convenience constructor + * + * @category schemas + * @since 4.0.0 + */ +export const Port = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })) + +/** + * Schema for `LogLevel` string literals. + * + * **When to use** + * + * Use when you need the reusable log-level schema value for `Config.schema` + * with custom paths. + * + * **Details** + * + * Accepted values: `"All"`, `"Fatal"`, `"Error"`, `"Warn"`, `"Info"`, + * `"Debug"`, `"Trace"`, `"None"`. + * + * @see {@link logLevel} – convenience constructor + * + * @category schemas + * @since 4.0.0 + */ +export const LogLevel = Schema.Literals(LogLevel_.values) + +/** + * Schema for key-value record types that can also be parsed from + * a flat comma-separated string. + * + * **When to use** + * + * Use when reading key-value maps from a single env var (e.g. OpenTelemetry + * resource attributes). + * + * **Details** + * + * Accepts either a JSON-like record from the provider or a flat string like + * `"key1=val1,key2=val2"`. The `separator` (default `","`) and + * `keyValueSeparator` (default `"="`) can be customized. + * + * **Example** (Parsing a comma-separated record) + * + * ```ts + * import { Config, ConfigProvider, Effect, Schema } from "effect" + * + * const schema = Config.Record(Schema.String, Schema.String) + * const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * OTEL_RESOURCE_ATTRIBUTES: + * "service.name=my-service,service.version=1.0.0,custom.attribute=value" + * } + * }) + * + * console.dir(Effect.runSync(config.parse(provider))) + * // { + * // 'service.name': 'my-service', + * // 'service.version': '1.0.0', + * // 'custom.attribute': 'value' + * // } + * ``` + * + * @category schemas + * @since 4.0.0 + */ +export const Record = (key: K, value: V, options?: { + readonly separator?: string | undefined + readonly keyValueSeparator?: string | undefined +}) => { + const record = Schema.Record(key, value) + const recordString = Schema.String.pipe( + Schema.decodeTo( + Schema.Record(Schema.String, Schema.String), + SchemaTransformation.splitKeyValue(options) + ), + Schema.decodeTo(record) + ) + + return Schema.Union([record, recordString]) +} + +/** + * @category schemas + * @since 4.0.0 + */ +const ArrayConfig = (value: V, options?: { + readonly separator?: string | undefined +}) => { + const array = Schema.Array(value) + const separator = options?.separator ?? "," + const arrayString = Schema.String.pipe( + Schema.decodeTo( + Schema.Array(Schema.String), + { + decode: SchemaGetter.split(options), + encode: SchemaGetter.transform((input: ReadonlyArray) => input.join(separator)) + } + ), + Schema.decodeTo(array) + ) + + return Schema.Union([arrayString, array]) +} + +export { + /** + * Schema for array types that can also be parsed from a flat separated string. + * + * **When to use** + * + * Use when reading array values from a single env var, such as comma-separated + * exporter names. + * + * **Details** + * + * Accepts either a JSON-like array from the provider or a flat string like + * `"a,b,c"`. The `separator` defaults to `","` and can be customized. + * + * @category schemas + * @since 4.0.0 + */ + ArrayConfig as Array +} + +// ----------------------------------------------------------------------------- +// constructors +// ----------------------------------------------------------------------------- + +/** + * Creates a config that always fails with the given error. + * + * **When to use** + * + * Use when you need to re-raise a specific config error, such as inside + * {@link orElse}. + * + * @category constructors + * @since 2.0.0 + */ +export function fail(err: SourceError | Schema.SchemaError) { + return make(() => Effect.fail(new ConfigError(err))) +} + +/** + * Creates a config that always succeeds with the given value, ignoring the + * provider entirely. + * + * **When to use** + * + * Use when you need a hardcoded config value, such as inside {@link orElse} or + * tests. + * + * **Example** (Returning a constant fallback) + * + * ```ts + * import { Config } from "effect" + * + * const host = Config.string("HOST").pipe( + * Config.orElse(() => Config.succeed("localhost")) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export function succeed(value: T) { + return make(() => Effect.succeed(value)) +} + +/** + * Creates a config for a single string value. + * + * **When to use** + * + * Use when reading a single string env var or config key. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.String, name)`. + * + * **Example** (Reading a string config) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const host = Config.string("HOST") + * + * const provider = ConfigProvider.fromUnknown({ HOST: "localhost" }) + * // Effect.runSync(host.parse(provider)) // "localhost" + * ``` + * + * @see {@link nonEmptyString} – rejects empty strings + * @see {@link schema} – for more complex types + * + * @category constructors + * @since 2.0.0 + */ +export function string(name?: string) { + return schema(Schema.String, name) +} + +/** + * Creates a config for a non-empty string value. Fails if the value is an + * empty string. + * + * **When to use** + * + * Use to read a string config value that must contain at least one character. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.NonEmptyString, name)`. + * + * @see {@link string} for allowing empty strings + * + * @category constructors + * @since 3.7.0 + */ +export function nonEmptyString(name?: string) { + return schema(Schema.NonEmptyString, name) +} + +/** + * Creates a config for a numeric value (including `NaN`, `Infinity`). + * + * **When to use** + * + * Use when you need config input to accept JavaScript's full number domain, + * including NaN and infinities, rather than reject non-finite values. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Number, name)`. + * + * @see {@link finite} for rejecting `NaN` and `Infinity` + * @see {@link int} for accepting only integers + * + * @category constructors + * @since 2.0.0 + */ +export function number(name?: string) { + return schema(Schema.Number, name) +} + +/** + * Creates a config for a finite number (rejects `NaN` and `Infinity`). + * + * **When to use** + * + * Use to read a numeric config value that must be finite. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Finite, name)`. + * + * @see {@link number} for accepting `NaN` and `Infinity` + * @see {@link int} for accepting only integers + * + * @category constructors + * @since 4.0.0 + */ +export function finite(name?: string) { + return schema(Schema.Finite, name) +} + +/** + * Creates a config for an integer value. Rejects floats. + * + * **When to use** + * + * Use to read a numeric config value that must be an integer. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Int, name)`. + * + * @see {@link number} for accepting any number + * @see {@link port} for accepting only integers in `1` through `65535` + * + * @category constructors + * @since 4.0.0 + */ +export function int(name?: string) { + return schema(Schema.Int, name) +} + +/** + * Creates a config that only accepts a specific literal value. + * + * **When to use** + * + * Use to restrict a config to a single, specific literal value. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Literal(literal), name)`. + * + * **Example** (Restricting to a literal) + * + * ```ts + * import { Config } from "effect" + * + * const env = Config.literal("production", "ENV") + * ``` + * + * @see {@link literals} – accepts multiple literal values + * @category constructors + * @since 2.0.0 + */ +export function literal(literal: L, name?: string) { + return schema(Schema.Literal(literal), name) +} + +/** + * Creates a config that only accepts one of the specified literal values. + * + * **When to use** + * + * Use to restrict a config to a fixed set of allowed literal values. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Literals(literals), name)`. + * + * **Example** (Restricting to a set of literals) + * + * ```ts + * import { Config } from "effect" + * + * const env = Config.literals(["development", "production"], "ENV") + * ``` + * + * @see {@link literal} for accepting one specific literal value + * + * @category constructors + * @since 4.0.0 + */ +export function literals>(literals: L, name?: string) { + return schema(Schema.Literals(literals), name) +} + +/** + * Creates a config for a boolean value parsed from common string + * representations. + * + * **When to use** + * + * Use to read boolean flags from string-like config sources. + * + * **Details** + * + * Shortcut for `Config.schema(Config.Boolean, name)`. + * + * Accepted values: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`, + * `y`, `n`. + * + * **Example** (Reading a boolean flag) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const flag = yield* Config.boolean("FEATURE_FLAG") + * console.log(flag) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * FEATURE_FLAG: "yes" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: true + * ``` + * + * @see {@link Boolean} for the underlying boolean codec + * + * @category constructors + * @since 2.0.0 + */ +export function boolean(name?: string) { + return schema(Boolean, name) +} + +/** + * Creates a config for a `Duration` value parsed from a human-readable + * string. + * + * **When to use** + * + * Use to read time duration settings such as timeouts, intervals, or TTLs. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.DurationFromString, name)`. + * + * Accepts any string that `Duration.fromInput` can parse (e.g. + * `"10 seconds"`, `"500 millis"`, `"Infinity"`, `"-Infinity"`). + * + * **Example** (Reading a duration) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const duration = yield* Config.duration("DURATION") + * console.log(duration) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * DURATION: "10 seconds" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: Duration { _tag: "millis", value: 10000 } + * ``` + * + * @see {@link schema} for decoding configuration values with a custom codec + * + * @category constructors + * @since 2.5.0 + */ +export function duration(name?: string) { + return schema(Schema.DurationFromString, name) +} + +/** + * Creates a config for a port number (integer in 1–65535). + * + * **When to use** + * + * Use to read network port settings that must be valid port numbers. + * + * **Details** + * + * Shortcut for `Config.schema(Config.Port, name)`. + * + * **Example** (Reading a port) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const port = yield* Config.port("PORT") + * console.log(port) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * PORT: "8080" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: 8080 + * ``` + * + * @see {@link int} for integer config values outside the port range + * @see {@link Port} for the underlying port codec + * + * @category constructors + * @since 3.16.0 + */ +export function port(name?: string) { + return schema(Port, name) +} + +/** + * Creates a config for a log level string. + * + * **When to use** + * + * Use to read Effect log-level settings from configuration. + * + * **Details** + * + * Shortcut for `Config.schema(Config.LogLevel, name)`. + * + * Accepted values: `"All"`, `"Fatal"`, `"Error"`, `"Warn"`, `"Info"`, + * `"Debug"`, `"Trace"`, `"None"`. + * + * **Example** (Reading a log level) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const logLevel = yield* Config.logLevel("LOG_LEVEL") + * console.log(logLevel) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * LOG_LEVEL: "Info" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: "Info" + * ``` + * + * @see {@link LogLevel} for the underlying log-level codec + * + * @category constructors + * @since 2.0.0 + */ +export function logLevel(name?: string) { + return schema(LogLevel, name) +} + +/** + * Creates a config for a redacted string value. The parsed result is wrapped + * in a `Redacted` container that hides the value from logs and `toString`. + * + * **When to use** + * + * Use to read secret string settings that should not be exposed in logs or + * string output. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.Redacted(Schema.String), name)`. + * + * **Example** (Reading a secret) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const apiKey = yield* Config.redacted("API_KEY") + * console.log(apiKey) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * API_KEY: "sk-1234567890abcdef" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: + * ``` + * + * @see {@link string} for non-secret string settings + * + * @category constructors + * @since 2.0.0 + */ +export function redacted(name?: string) { + return schema(Schema.Redacted(Schema.String), name) +} + +/** + * Creates a config for a `URL` value parsed from a string. + * + * **When to use** + * + * Use to read configuration values that must be valid URL strings. + * + * **Details** + * + * This is a shortcut for `Config.schema(Schema.URL, name)`. + * + * **Gotchas** + * + * Fails if the string cannot be parsed by the `URL` constructor. + * + * **Example** (Reading a URL) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const url = yield* Config.url("URL") + * console.log(url) + * }) + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * URL: "https://example.com" + * } + * }) + * + * Effect.runSync( + * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) + * ) + * // Output: + * // URL { + * // href: 'https://example.com/', + * // origin: 'https://example.com', + * // protocol: 'https:', + * // username: '', + * // password: '', + * // host: 'example.com', + * // hostname: 'example.com', + * // port: '', + * // pathname: '/', + * // search: '', + * // searchParams: URLSearchParams {}, + * // hash: '' + * // } + * ``` + * + * @see {@link schema} for decoding configuration values with a custom codec + * + * @category constructors + * @since 3.11.0 + */ +export function url(name?: string) { + return schema(Schema.URL, name) +} + +/** + * Creates a config for a `Date` value parsed from a string. + * + * **When to use** + * + * Use to read date settings that must parse to valid `Date` values. + * + * **Details** + * + * Shortcut for `Config.schema(Schema.DateValid, name)`. + * + * **Gotchas** + * + * Fails with a `SchemaError` if the string produces an invalid `Date`. + * + * **Example** (Reading a date) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const createdAt = Config.date("CREATED_AT") + * + * const provider = ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" }) + * // Effect.runSync(createdAt.parse(provider)) + * // Date("2024-01-15T00:00:00.000Z") + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export function date(name?: string) { + return schema(Schema.DateValid, name) +} + +/** + * Scopes a config under a named prefix. + * + * **When to use** + * + * Use when you need to group related config keys under a common namespace. + * + * **Details** + * + * The prefix is prepended to every key the inner config reads. With + * `fromUnknown` this means an extra object level; with `fromEnv` it means + * a `_`-separated prefix on env var names. + * + * Multiple `nested` calls compose: the outermost name becomes the + * outermost path segment. + * + * **Example** (Nesting a struct config under `"database"`) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const dbConfig = Config.all({ + * host: Config.string("host"), + * port: Config.number("port") + * }).pipe(Config.nested("database")) + * + * const provider = ConfigProvider.fromUnknown({ + * database: { host: "localhost", port: "5432" } + * }) + * // Effect.runSync(dbConfig.parse(provider)) + * // { host: "localhost", port: 5432 } + * ``` + * + * **Example** (Reading env vars with a nested prefix) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const host = Config.string("host").pipe(Config.nested("database")) + * + * const provider = ConfigProvider.fromEnv({ + * env: { database_host: "localhost" } + * }) + * // Effect.runSync(host.parse(provider)) // "localhost" + * ``` + * + * @see {@link all} – combine multiple configs into a struct + * @see {@link schema} – read structured config from a schema + * + * @category combinators + * @since 2.0.0 + */ +export const nested: { + (name: string): (self: Config) => Config + (self: Config, name: string): Config +} = dual( + 2, + (self: Config, name: string): Config => + make((provider, pathPrefix) => self.parse(provider, [...pathPrefix, name])) +) diff --git a/repos/effect/packages/effect/src/ConfigProvider.ts b/repos/effect/packages/effect/src/ConfigProvider.ts new file mode 100644 index 0000000..3f12825 --- /dev/null +++ b/repos/effect/packages/effect/src/ConfigProvider.ts @@ -0,0 +1,1212 @@ +/** + * Data sources used by `Config` to load raw configuration values. A + * `ConfigProvider` reads paths from places such as environment variables, + * JavaScript objects, `.env` contents, or directories, and returns a uniform + * `Node` shape that config schemas can decode. The module also includes helpers + * for composing providers, changing paths, and installing providers through + * layers. + * + * @since 4.0.0 + */ + +import * as Context from "./Context.ts" +import * as Data from "./Data.ts" +import * as Effect from "./Effect.ts" +import * as FileSystem from "./FileSystem.ts" +import { format } from "./Formatter.ts" +import { dual, flow } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Layer from "./Layer.ts" +import * as Path_ from "./Path.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { PlatformError } from "./PlatformError.ts" +import * as Predicate from "./Predicate.ts" +import type { Scope } from "./Scope.ts" +import * as Str from "./String.ts" + +/** + * A discriminated union describing the shape of a configuration value at a + * given path. + * + * **When to use** + * + * Use when implementing a custom `ConfigProvider` by returning raw + * nodes from the `get` callback passed to {@link make}, or when inspecting raw + * provider output before schema parsing. + * + * **Details** + * + * `Value` is a terminal string leaf. `Record` is an object-like container + * whose immediate child keys are known and may carry an optional co-located + * `value`. `Array` is an indexed container with a known `length` and may also + * carry an optional co-located `value`. + * + * @see {@link makeValue} – construct a `Value` node + * @see {@link makeRecord} – construct a `Record` node + * @see {@link makeArray} – construct an `Array` node + * + * @category models + * @since 4.0.0 + */ +export type Node = + /** A terminal string value */ + | { + readonly _tag: "Value" + readonly value: string + } + /** An object; keys are unordered */ + | { + readonly _tag: "Record" + readonly keys: ReadonlySet + readonly value: string | undefined + } + /** An array-like container; length is the number of elements */ + | { + readonly _tag: "Array" + readonly length: number + readonly value: string | undefined + } + +/** + * Creates a `Value` node representing a terminal string leaf. + * + * **When to use** + * + * Use when building nodes inside a custom `ConfigProvider`'s `get` + * callback. + * + * **Details** + * + * The function returns a new plain object. + * + * **Example** (Creating a value node) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const node = ConfigProvider.makeValue("3000") + * // { _tag: "Value", value: "3000" } + * ``` + * + * @see {@link makeRecord} – for object-like containers + * @see {@link makeArray} – for array-like containers + * + * @category constructors + * @since 4.0.0 + */ +export function makeValue(value: string): Node { + return { _tag: "Value", value } +} + +/** + * Creates a `Record` node representing an object-like container with known + * child keys. + * + * **When to use** + * + * Use when you need to describe a directory or JSON object inside a custom + * provider. + * + * **Details** + * + * The optional `value` allows a node to be both a container and a leaf at the + * same time (for example, an env var `A=x` that also has children `A_FOO` and + * `A_BAR`). + * + * **Example** (Creating a record node) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const node = ConfigProvider.makeRecord(new Set(["host", "port"])) + * // { _tag: "Record", keys: Set(["host", "port"]), value: undefined } + * ``` + * + * @see {@link makeValue} – for terminal leaves + * @see {@link makeArray} – for array-like containers + * + * @category constructors + * @since 4.0.0 + */ +export function makeRecord(keys: ReadonlySet, value?: string): Node { + return { _tag: "Record", keys, value } +} + +/** + * Creates an `Array` node representing an indexed container with a known + * length. + * + * **When to use** + * + * Use when you need to describe a JSON array or numerically indexed env vars + * inside a custom provider. + * + * **Details** + * + * The optional `value` allows a node to be both a container and a leaf at the + * same time. + * + * **Example** (Creating an array node) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const node = ConfigProvider.makeArray(3) + * // { _tag: "Array", length: 3, value: undefined } + * ``` + * + * @see {@link makeValue} – for terminal leaves + * @see {@link makeRecord} – for object-like containers + * + * @category constructors + * @since 4.0.0 + */ +export function makeArray(length: number, value?: string): Node { + return { _tag: "Array", length, value } +} + +/** + * Typed error indicating that a configuration source could not be read. + * + * **When to use** + * + * Use when you need to report that a custom provider's underlying store is + * unreachable or produced an I/O error while reading configuration data. + * + * **Gotchas** + * + * Do not use `SourceError` for "key not found". That case is represented by + * returning `undefined` from `load`. + * + * **Example** (Failing with a SourceError) + * + * ```ts + * import { ConfigProvider, Effect } from "effect" + * + * const provider = ConfigProvider.make((_path) => + * Effect.fail( + * new ConfigProvider.SourceError({ message: "connection refused" }) + * ) + * ) + * ``` + * + * @see {@link ConfigProvider} – the interface whose `load` may fail with this + * error + * + * @category models + * @since 4.0.0 + */ +export class SourceError extends Data.TaggedError("SourceError")<{ + readonly message: string + readonly cause?: unknown +}> {} + +/** + * An ordered sequence of string or numeric segments that addresses a node in + * the configuration tree. String segments name object keys; numeric segments + * index into arrays. + * + * **When to use** + * + * Use to address raw configuration nodes when implementing or transforming a + * `ConfigProvider`. + * + * **Example** (A typical config path) + * + * ```ts + * import type { ConfigProvider } from "effect" + * + * const path: ConfigProvider.Path = ["database", "replicas", 0, "host"] + * ``` + * + * @category models + * @since 4.0.0 + */ +export type Path = ReadonlyArray + +/** + * The core interface for loading raw configuration data. + * + * **When to use** + * + * Use to type-annotate variables that hold a provider or to implement a + * custom provider via {@link make}. + * + * **Details** + * + * `load(path)` is the semantic lookup operation used by the `Config` module. + * It applies provider transformations and composition before consulting the + * underlying source. `undefined` means "not found" and `SourceError` means the + * source itself failed. + * + * @see {@link make} – construct a provider from a lookup function + * @see {@link orElse} – compose providers with fallback + * + * @category models + * @since 2.0.0 + */ +export interface ConfigProvider extends Pipeable { + /** + * Returns the node found at `path`, or `undefined` if it does not exist. + * Fails with `SourceError` when the underlying source cannot be read. + * + * **When to use** + * + * Use to resolve a path through this provider's path transformations before + * reading the backing source. + */ + readonly load: (path: Path) => Effect.Effect + + /** @internal */ + readonly state: ProviderState +} + +/** + * Context reference for the active raw configuration provider, registered in the context with a + * default value of `fromEnv()`. Because it is a `Context.Reference`, it is + * available without explicit provision; `Config` schemas automatically resolve + * it. + * + * **When to use** + * + * Use to override the active raw configuration provider for an entire program, + * or retrieve the current provider inside an Effect. + * + * **Example** (Providing a custom provider) + * + * ```ts + * import { ConfigProvider, Effect } from "effect" + * + * const provider = ConfigProvider.fromUnknown({ port: 8080 }) + * + * const program = Effect.gen(function*() { + * const current = yield* ConfigProvider.ConfigProvider + * return current + * }).pipe( + * Effect.provideService(ConfigProvider.ConfigProvider, provider) + * ) + * ``` + * + * @see {@link layer} – install a provider as a Layer + * @see {@link layerAdd} – add a fallback provider as a Layer + * + * @category services + * @since 2.0.0 + */ +export const ConfigProvider: Context.Reference = Context.Reference( + "effect/ConfigProvider", + { defaultValue: () => fromEnv() } +) + +const Proto = { + ...PipeInspectableProto, + toJSON(this: ConfigProvider) { + return { + _id: "ConfigProvider" + } + } +} + +type SourceState = { + readonly _tag: "Source" + readonly get: (path: Path) => Effect.Effect + readonly transform: (path: Path) => Path +} + +type OrElseState = { + readonly _tag: "OrElse" + readonly first: ConfigProvider + readonly second: ConfigProvider +} + +type ProviderState = SourceState | OrElseState + +const identityPath = (path: Path): Path => path + +function makeProvider( + state: ProviderState, + load: (path: Path) => Effect.Effect +): ConfigProvider { + const self = Object.create(Proto) + self.state = state + self.load = load + return self +} + +function makeSource( + get: (path: Path) => Effect.Effect, + transform: (path: Path) => Path +): ConfigProvider { + const state: SourceState = { + _tag: "Source", + get, + transform + } + return makeProvider(state, (path) => state.get(state.transform(path))) +} + +function makeOrElse(first: ConfigProvider, second: ConfigProvider): ConfigProvider { + const state: OrElseState = { + _tag: "OrElse", + first, + second + } + return makeProvider(state, (path) => + Effect.flatMap( + state.first.load(path), + (node) => node ? Effect.succeed(node) : state.second.load(path) + )) +} + +/** + * Creates a `ConfigProvider` from a raw lookup function. + * + * **When to use** + * + * Use when implementing a provider backed by a custom store, such as a + * database, remote API, or in-memory map. + * + * **Details** + * + * The `get` callback receives a `Path` and must return + * `Effect`. Return `undefined` when the path + * does not exist; fail with `SourceError` only for actual I/O errors. + * + * **Example** (Creating a simple in-memory provider) + * + * ```ts + * import { ConfigProvider, Effect } from "effect" + * + * const data: Record = { + * host: "localhost", + * port: "5432" + * } + * + * const provider = ConfigProvider.make((path) => { + * const key = path.join(".") + * const value = data[key] + * return Effect.succeed( + * value !== undefined ? ConfigProvider.makeValue(value) : undefined + * ) + * }) + * ``` + * + * @see {@link fromEnv} – pre-built provider for environment variables + * @see {@link fromUnknown} – pre-built provider for JSON objects + * + * @category constructors + * @since 2.0.0 + */ +export function make(get: (path: Path) => Effect.Effect): ConfigProvider { + return makeSource(get, identityPath) +} + +/** + * Returns a provider that falls back to `that` when `self` returns `undefined` + * for a path. + * + * **When to use** + * + * Use to layer multiple config sources, such as env vars plus a defaults file, + * or provide partial overrides on top of a base config. + * + * **Details** + * + * Each provider keeps its own path transformations. If the combined provider + * is later transformed with {@link mapInput} or {@link nested}, the + * transformation is applied to both sides. + * + * **Gotchas** + * + * The fallback only runs when the path is not found (`undefined`). A + * `SourceError` from `self` is not caught; it propagates immediately. + * + * **Example** (Falling back to a default provider) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const envProvider = ConfigProvider.fromEnv({ + * env: { HOST: "prod.example.com" } + * }) + * const defaults = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000" }) + * + * const combined = ConfigProvider.orElse(envProvider, defaults) + * ``` + * + * @see {@link layerAdd} – install a fallback provider via a Layer + * + * @category combinators + * @since 2.0.0 + */ +export const orElse: { + (that: ConfigProvider): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, that: ConfigProvider): ConfigProvider +} = dual( + 2, + (self: ConfigProvider, that: ConfigProvider): ConfigProvider => makeOrElse(self, that) +) + +/** + * Transforms the path segments before they reach the underlying store. + * + * **When to use** + * + * Use when you need to rename, re-case, or otherwise transform config path + * segments before lookup. + * + * **Details** + * + * The function `f` receives the whole path produced by earlier provider + * transformations and must return a new path. Lookup path transformations + * compose in application order: the existing transformation runs first, then + * `f` runs. For providers composed with {@link orElse}, the transformation is + * applied to each operand. + * + * **Example** (Uppercasing path segments) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const provider = ConfigProvider.fromEnv({ + * env: { APP_HOST: "localhost" } + * }) + * + * const upper = ConfigProvider.mapInput(provider, (path) => + * path.map((seg) => + * typeof seg === "string" ? seg.toUpperCase() : seg + * ) + * ) + * ``` + * + * @see {@link constantCase} – a preset that converts to `CONSTANT_CASE` + * @see {@link nested} – for prepending a prefix instead of transforming + * + * @category combinators + * @since 4.0.0 + */ +export const mapInput: { + (f: (path: Path) => Path): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider +} = dual( + 2, + (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider => { + const state = self.state + switch (state._tag) { + case "Source": + return makeSource(state.get, flow(state.transform, f)) + case "OrElse": + return makeOrElse(mapInput(state.first, f), mapInput(state.second, f)) + } + } +) + +/** + * Converts all string path segments to `CONSTANT_CASE` before lookup. + * + * **When to use** + * + * Use to bridge camelCase schema keys to `SCREAMING_SNAKE_CASE` + * environment variables. + * + * **Details** + * + * Numeric segments are left unchanged. String segments use `String.configCase` + * so numeric word groups such as `v2` are preserved for environment variable + * names. This is a specialization of {@link mapInput}. + * + * **Example** (Resolving camelCase keys to env vars) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const provider = ConfigProvider.fromEnv({ + * env: { DATABASE_HOST: "localhost" } + * }).pipe(ConfigProvider.constantCase) + * + * // path ["databaseHost"] now resolves to env var DATABASE_HOST + * ``` + * + * @see {@link mapInput} – for arbitrary path transformations + * + * @category combinators + * @since 2.0.0 + */ +export const constantCase: (self: ConfigProvider) => ConfigProvider = mapInput((path) => + path.map((seg) => typeof seg === "number" ? seg : Str.configCase(seg)) +) + +/** + * Scopes a provider so that all lookups are prefixed with the given path + * segments. + * + * **When to use** + * + * Use to namespace config under a prefix like `"app"` or `"database"`, or + * reuse the same provider shape for multiple sub-configs. + * + * **Details** + * + * Accepts a single string or a full `Path` array. For providers composed with + * {@link orElse}, the prefix is applied to each operand. Supports both + * data-last and data-first calling conventions. + * + * **Gotchas** + * + * Ordering matters when composing with {@link mapInput} or + * {@link constantCase}. Later provider transformations run after earlier ones: + * a later `nested` becomes the outer prefix, and a later `mapInput` sees the + * whole path produced by previous transformations. + * + * **Example** (Nesting under a prefix) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const provider = ConfigProvider.fromEnv({ + * env: { APP_HOST: "localhost", APP_PORT: "3000" } + * }) + * + * // Lookups for ["HOST"] now resolve to ["APP", "HOST"] + * const scoped = ConfigProvider.nested(provider, "APP") + * ``` + * + * @see {@link mapInput} – for arbitrary path transformations + * + * @category combinators + * @since 2.0.0 + */ +export const nested: { + (prefix: string | Path): (self: ConfigProvider) => ConfigProvider + (self: ConfigProvider, prefix: string | Path): ConfigProvider +} = dual( + 2, + (self: ConfigProvider, prefix: string | Path): ConfigProvider => { + const path = typeof prefix === "string" ? [prefix] : prefix + const state = self.state + switch (state._tag) { + case "Source": + return makeSource(state.get, flow(state.transform, (input) => [...path, ...input])) + case "OrElse": + return makeOrElse(nested(state.first, path), nested(state.second, path)) + } + } +) + +/** + * Provides a layer that installs a `ConfigProvider` as the active provider for + * all downstream effects, replacing any previously installed provider. + * + * **When to use** + * + * Use to set the config source for an entire application or test suite. + * + * **Details** + * + * Accepts either a plain `ConfigProvider` or an `Effect` that produces one. + * When given an Effect, it is evaluated once when the layer is built. + * + * **Example** (Reading config from a JSON object) + * + * ```ts + * import { Config, ConfigProvider, Effect, Layer } from "effect" + * + * const TestLayer = ConfigProvider.layer( + * ConfigProvider.fromUnknown({ port: 8080 }) + * ) + * + * const program = Effect.gen(function*() { + * const port = yield* Config.number("port") + * return port + * }) + * + * // Effect.runSync(Effect.provide(program, TestLayer)) // 8080 + * ``` + * + * @see {@link layerAdd} – add a provider without replacing the existing one + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + self: ConfigProvider | Effect.Effect +): Layer.Layer> => + Effect.isEffect(self) ? Layer.effect(ConfigProvider)(self) : Layer.succeed(ConfigProvider)(self) + +/** + * Creates a Layer that composes a new `ConfigProvider` with the currently + * active one, rather than replacing it. + * + * **When to use** + * + * Use to add defaults that should only apply when the primary provider has no + * value for a path, or override specific keys while keeping the rest from the + * existing provider by setting `asPrimary: true`. + * + * **Details** + * + * By default, the new provider acts as a fallback and is consulted only when + * the current provider returns `undefined`. Set `asPrimary: true` to make the + * new provider the primary source, with the existing one as fallback. + * + * **Example** (Adding default values) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const defaults = ConfigProvider.fromUnknown({ + * HOST: "localhost", + * PORT: "3000" + * }) + * + * // The current env provider is tried first; `defaults` is the fallback + * const DefaultsLayer = ConfigProvider.layerAdd(defaults) + * ``` + * + * @see {@link layer} – replace the provider entirely + * @see {@link orElse} – compose providers without layers + * + * @category layers + * @since 4.0.0 + */ +export const layerAdd = ( + self: ConfigProvider | Effect.Effect, + options?: { + readonly asPrimary?: boolean | undefined + } | undefined +): Layer.Layer> => + Layer.effect(ConfigProvider)( + Effect.gen(function*() { + const current = yield* ConfigProvider + const configProvider = Effect.isEffect(self) ? yield* self : self + return options?.asPrimary ? orElse(configProvider, current) : orElse(current, configProvider) + }) + ) + +/** + * Creates a `ConfigProvider` backed by an in-memory JavaScript value + * (typically a parsed JSON object). + * + * **When to use** + * + * Use when you need deterministic config from an in-memory JavaScript value, + * such as in tests, embedded config, or parsed JSON. + * + * **Details** + * + * Path traversal follows standard JS rules: string segments index into object + * keys, numeric segments index into arrays. Returns `undefined` for any path + * that cannot be resolved. Never fails with `SourceError`. + * + * Primitive values (`number`, `boolean`, `bigint`) are stringified via + * `String(...)`. + * + * Literal empty strings are treated as missing values when loaded as values by + * default. Pass `{ preserveEmptyStrings: true }` to keep empty strings as + * explicit values. + * + * **Gotchas** + * + * Object keys and array lengths reflect the original input shape. A leaf value + * of `""` is treated as missing when that leaf is loaded, but the parent + * container still reports its original keys or length. + * + * **Example** (Providing config from a plain object) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const provider = ConfigProvider.fromUnknown({ + * database: { + * host: "localhost", + * port: 5432 + * } + * }) + * + * const host = Config.string("host").parse( + * provider.pipe(ConfigProvider.nested("database")) + * ) + * + * // Effect.runSync(host) // "localhost" + * ``` + * + * @see {@link fromEnv} – for environment variables + * @see {@link make} – for custom backing stores + * + * @category ConfigProviders + * @since 4.0.0 + */ +export function fromUnknown(root: unknown, options?: { + readonly preserveEmptyStrings?: boolean | undefined +}): ConfigProvider { + const preserveEmptyStrings = options?.preserveEmptyStrings === true + return make((path) => Effect.succeed(nodeAtJson(root, path, preserveEmptyStrings))) +} + +function nodeAtJson(root: unknown, path: Path, preserveEmptyStrings: boolean): Node | undefined { + let cur: unknown = root + + for (const seg of path) { + if (cur === null || cur === undefined) return undefined + + if (Array.isArray(cur)) { + if (typeof seg !== "number" || !Number.isInteger(seg) || seg < 0 || seg >= cur.length) return undefined + cur = cur[seg] + continue + } + + if (Predicate.isObject(cur)) { + if (typeof seg !== "string") return undefined + if (!Object.hasOwn(cur, seg)) return undefined + cur = cur[seg] + continue + } + + // cannot descend + return undefined + } + + return describeUnknown(cur, preserveEmptyStrings) +} + +function describeUnknown(u: unknown, preserveEmptyStrings: boolean): Node | undefined { + if (u === undefined || u === null) return undefined + if (typeof u === "string") return stringNode(u, preserveEmptyStrings) + if (typeof u === "number" || typeof u === "boolean" || typeof u === "bigint") { + return makeValue(String(u)) + } + if (Array.isArray(u)) return makeArray(u.length) + if (Predicate.isObject(u)) { + return makeRecord(new Set(Object.keys(u))) + } + // unknown values + return makeValue(format(u)) +} + +function stringNode(value: string, preserveEmptyStrings: boolean): Node | undefined { + const normalized = emptyStringAsMissing(value, preserveEmptyStrings) + return normalized === undefined ? undefined : makeValue(normalized) +} + +function emptyStringAsMissing(value: string | undefined, preserveEmptyStrings: boolean): string | undefined { + return value === "" && !preserveEmptyStrings ? undefined : value +} + +/** + * Creates a `ConfigProvider` backed by environment variables. + * + * **When to use** + * + * Use to read configuration from `process.env`, which is the default when no + * provider is explicitly set, or pass a custom env record for testing or + * non-Node runtimes. + * + * **Details** + * + * Path segments are joined with `_` for direct lookup, and env var names are + * also split on `_` to build a trie for child key discovery. This means + * `DATABASE_HOST=localhost` is accessible at both path `["DATABASE_HOST"]` + * and `["DATABASE", "HOST"]`. If all immediate children of a trie node have + * purely numeric names, the node is reported as an `Array`; otherwise as a + * `Record`. + * + * The default environment merges `process.env` and `import.meta.env` (when + * available). Override by passing `{ env: { ... } }`. + * + * Literal empty strings are treated as missing values when loaded as values by + * default. Pass `{ preserveEmptyStrings: true }` to keep empty strings as + * explicit values. Child discovery still reflects the environment variable + * names present in the source. + * + * Never fails with `SourceError` — all lookups are synchronous. + * + * **Example** (Reading from a custom env record) + * + * ```ts + * import { Config, ConfigProvider, Effect } from "effect" + * + * const provider = ConfigProvider.fromEnv({ + * env: { + * DATABASE_HOST: "localhost", + * DATABASE_PORT: "5432" + * } + * }) + * + * const host = Config.string("HOST").parse( + * provider.pipe(ConfigProvider.nested("DATABASE")) + * ) + * + * // Effect.runSync(host) // "localhost" + * ``` + * + * @see {@link fromUnknown} – for JSON objects + * @see {@link constantCase} – bridge camelCase keys to SCREAMING_SNAKE_CASE + * + * @category ConfigProviders + * @since 2.0.0 + */ +export function fromEnv(options?: { + readonly env?: Record | undefined + readonly preserveEmptyStrings?: boolean | undefined +}): ConfigProvider { + const env: Record = options?.env ?? { + ...globalThis?.process?.env, + ...(import.meta as any)?.env + } + const preserveEmptyStrings = options?.preserveEmptyStrings === true + const trie = buildEnvTrie(env) + + return make((path) => Effect.succeed(nodeAtEnv(trie, env, path, preserveEmptyStrings))) +} + +type EnvTrieNode = { + children?: Record +} + +function buildEnvTrie(env: Record): EnvTrieNode { + const trie: EnvTrieNode = {} + + for (const [name, value] of Object.entries(env)) { + if (value === undefined) continue + + // Split on "_" and keep empty segments (no special handling for "__") + const segments = name.split("_") + + let node = trie + for (const seg of segments) { + node.children ??= {} + node = node.children[seg] ??= {} + } + } + + return trie +} + +const NUMERIC_INDEX = /^(0|[1-9][0-9]*)$/ + +function nodeAtEnv( + trie: EnvTrieNode, + env: Record, + path: Path, + preserveEmptyStrings: boolean +): Node | undefined { + const key = path.map(String).join("_") + const leafValue = emptyStringAsMissing(env[key], preserveEmptyStrings) + + const trieNode = trieNodeAt(trie, path) + const children = trieNode?.children ? Object.keys(trieNode.children) : [] + + if (children.length === 0) { + return leafValue === undefined ? undefined : makeValue(leafValue) + } + + const allNumeric = children.every((k) => NUMERIC_INDEX.test(k)) + if (allNumeric) { + const length = Math.max(...children.map((k) => parseInt(k, 10))) + 1 + return makeArray(length, leafValue) + } + + return makeRecord(new Set(children), leafValue) +} + +function trieNodeAt(root: EnvTrieNode, path: Path): EnvTrieNode | undefined { + if (path.length === 0) return root + + // Convert path segments to strings and navigate through the trie + let node: EnvTrieNode | undefined = root + for (const seg of path) { + node = node?.children?.[String(seg)] + if (!node) return undefined + } + return node +} + +/** + * Creates a `ConfigProvider` by parsing the string contents of a `.env` file. + * + * **When to use** + * + * Use when you already have the `.env` contents as a string, such as contents + * fetched from a remote store or embedded in a test. + * + * **Details** + * + * Supports `export` prefixes, single/double/backtick quoting, inline comments, + * and escaped newlines. Variable expansion (for example, `${VAR}`) is disabled + * by default; enable with `{ expandVariables: true }`. + * + * Literal empty strings are treated as missing values when loaded as values by + * default. Pass `{ preserveEmptyStrings: true }` to keep empty strings as + * explicit values. Child discovery still reflects the keys present in the + * parsed `.env` source. + * + * Parsing is based on the `dotenv` / `dotenv-expand` algorithm. + * + * Internally delegates to {@link fromEnv} with the parsed key-value pairs. + * + * **Example** (Parsing .env contents) + * + * ```ts + * import { ConfigProvider } from "effect" + * + * const contents = ` + * HOST=localhost + * PORT=3000 + * # this is a comment + * ` + * + * const provider = ConfigProvider.fromDotEnvContents(contents) + * ``` + * + * @see {@link fromDotEnv} – loads a `.env` file from disk + * @see {@link fromEnv} – for raw environment variable access + * + * @category ConfigProviders + * @since 4.0.0 + */ +export function fromDotEnvContents(lines: string, options?: { + readonly expandVariables?: boolean | undefined + readonly preserveEmptyStrings?: boolean | undefined +}): ConfigProvider { + let env = parseDotEnvContents(lines) + if (options?.expandVariables) { + env = dotEnvExpand(env) + } + return fromEnv({ env, preserveEmptyStrings: options?.preserveEmptyStrings }) +} + +const DOT_ENV_LINE = + /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg + +function parseDotEnvContents(lines: string): Record { + const obj: Record = {} + + // Convert line breaks to same format + lines = lines.replace(/\r\n?/gm, "\n") + + let match: RegExpExecArray | null + while ((match = DOT_ENV_LINE.exec(lines)) != null) { + const key = match[1] + + // Default undefined or null to empty string + let value = match[2] || "" + + // Remove whitespace + value = value.trim() + + // Check if double quoted + const maybeQuote = value[0] + + // Remove surrounding quotes + value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2") + + // Expand newlines if double quoted + if (maybeQuote === "\"") { + value = value.replace(/\\n/g, "\n") + value = value.replace(/\\r/g, "\r") + } + + // Add to object + obj[key] = value + } + + return obj +} + +function dotEnvExpand(parsed: Record): Record { + const newParsed: Record = {} + + for (const configKey in parsed) { + // resolve escape sequences + newParsed[configKey] = interpolate(parsed[configKey], parsed).replace(/\\\$/g, "$") + } + + return newParsed +} + +function interpolate(envValue: string, parsed: Record): string { + // find the last unescaped dollar sign in the + // value so that we can evaluate it + const lastUnescapedDollarSignIndex = searchLast(envValue, /(?!(?<=\\))\$/g) + + // If we couldn't match any unescaped dollar sign + // let's return the string as is + if (lastUnescapedDollarSignIndex === -1) return envValue + + // This is the right-most group of variables in the string + const rightMostGroup = envValue.slice(lastUnescapedDollarSignIndex) + + /** + * This finds the inner most variable/group divided + * by variable name and default value (if present) + * ( + * (?!(?<=\\))\$ // only match dollar signs that are not escaped + * {? // optional opening curly brace + * ([\w]+) // match the variable name + * (?::-([^}\\]*))? // match an optional default value + * }? // optional closing curly brace + * ) + */ + const matchGroup = /((?!(?<=\\))\${?([\w]+)(?::-([^}\\]*))?}?)/ + const match = rightMostGroup.match(matchGroup) + + if (match !== null) { + const [_, group, variableName, defaultValue] = match + + return interpolate( + envValue.replace(group, defaultValue || parsed[variableName] || ""), + parsed + ) + } + + return envValue +} + +function searchLast(str: string, rgx: RegExp): number { + const matches = Array.from(str.matchAll(rgx)) + return matches.length > 0 ? matches.slice(-1)[0].index : -1 +} + +/** + * Creates a `ConfigProvider` by reading and parsing a `.env` file from the + * file system. + * + * **When to use** + * + * Use to load environment config from a `.env` file at application startup. + * + * **Details** + * + * Requires `FileSystem` in the Effect context. Defaults to reading `".env"` in + * the current directory; override with `{ path: "/custom/.env" }`. + * Variable expansion (for example, `${VAR}`) is disabled by default; enable + * with `{ expandVariables: true }`. + * + * Literal empty strings are treated as missing values when loaded as values by + * default. Pass `{ preserveEmptyStrings: true }` to keep empty strings as + * explicit values. Child discovery still reflects the keys present in the + * parsed `.env` source. + * + * Returns an `Effect` that resolves to a `ConfigProvider`. Fails with a + * `PlatformError` if the file cannot be read. + * + * **Example** (Loading a .env file) + * + * ```ts + * import { ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const provider = yield* ConfigProvider.fromDotEnv() + * return provider + * }) + * ``` + * + * @see {@link fromDotEnvContents} – parse a `.env` string directly + * @see {@link fromEnv} – read from the runtime environment + * + * @category ConfigProviders + * @since 4.0.0 + */ +export const fromDotEnv: (options?: { + readonly path?: string | undefined + readonly expandVariables?: boolean | undefined + readonly preserveEmptyStrings?: boolean | undefined +}) => Effect.Effect = Effect.fnUntraced( + function*(options) { + const fs = yield* FileSystem.FileSystem + const content = yield* fs.readFileString(options?.path ?? ".env") + return fromDotEnvContents(content, options) + } +) + +/** + * Creates a `ConfigProvider` that reads configuration from a directory tree + * on disk, where each file is a leaf value and each directory is a container. + * + * **When to use** + * + * Use when you expose each config key as a file under a directory, such as + * Kubernetes ConfigMap or Secret volume mounts. + * + * **Details** + * + * Resolution tries a regular file first and returns a `Value` node for + * non-empty trimmed file contents. If the file read fails, it tries a directory + * and returns a `Record` node with immediate child names as keys. If both fail + * with `NotFound`, it returns `undefined`. Other platform failures return + * `SourceError`. + * + * Requires `Path` and `FileSystem` in the Effect context. Defaults to root + * path `/`; override with `{ rootPath: "/etc/config" }`. + * + * Literal empty strings are treated as missing values by default after file + * contents are trimmed. Pass `{ preserveEmptyStrings: true }` to keep empty + * strings as explicit values. Directory listings still reflect the file names + * present on disk. + * + * **Example** (Reading config from a directory) + * + * ```ts + * import { ConfigProvider, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const provider = yield* ConfigProvider.fromDir({ + * rootPath: "/etc/myapp" + * }) + * return provider + * }) + * ``` + * + * @see {@link fromEnv} – for environment variables + * @see {@link fromDotEnv} – for `.env` files + * + * @category ConfigProviders + * @since 4.0.0 + */ +export const fromDir: (options?: { + readonly rootPath?: string | undefined + readonly preserveEmptyStrings?: boolean | undefined +}) => Effect.Effect< + ConfigProvider, + never, + Path_.Path | FileSystem.FileSystem +> = Effect.fnUntraced(function*(options) { + const platformPath = yield* Path_.Path + const fs = yield* FileSystem.FileSystem + const rootPath = options?.rootPath ?? "/" + const preserveEmptyStrings = options?.preserveEmptyStrings === true + + return make((path) => { + const fullPath = platformPath.join(rootPath, ...path.map(String)) + + // Try reading as a *file* + const asFile = fs.readFileString(fullPath).pipe( + Effect.map((content) => stringNode(content.trim(), preserveEmptyStrings)) + ) + + // If not a file, try reading as a *directory* + const asDirectory = fs.readDirectory(fullPath).pipe( + Effect.map((entries) => makeRecord(new Set(entries.map((entry) => platformPath.basename(entry))))) + ) + + return asFile.pipe( + Effect.catch((fileCause) => + asDirectory.pipe( + Effect.catch((dirCause) => + isNotFound(fileCause) && isNotFound(dirCause) + ? Effect.succeed(undefined) + : Effect.fail(isNotFound(fileCause) ? dirCause : fileCause) + ) + ) + ), + Effect.mapError((cause: PlatformError) => + new SourceError({ + message: `Failed to read file at ${platformPath.join(rootPath, ...path.map(String))}`, + cause + }) + ) + ) + }) +}) + +const isNotFound = (cause: PlatformError) => cause.reason._tag === "NotFound" diff --git a/repos/effect/packages/effect/src/Console.ts b/repos/effect/packages/effect/src/Console.ts new file mode 100644 index 0000000..72f8bcf --- /dev/null +++ b/repos/effect/packages/effect/src/Console.ts @@ -0,0 +1,653 @@ +/** + * Wraps console operations in Effect. + * + * The `Console` service exposes common console methods such as logging, + * warnings, errors, groups, counters, tables, and timers. Because console access + * goes through a service, programs can use custom console implementations in + * tests or other environments. This module also includes scoped helpers that + * close console groups or timers automatically. + * + * @since 2.0.0 + */ +import type * as Context from "./Context.ts" +import type * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import * as core from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import type { Scope } from "./Scope.ts" + +/** + * Represents a console interface for logging, debugging, timing, and grouping output. + * + * @category models + * @since 2.0.0 + */ +export interface Console { + assert(condition: boolean, ...args: ReadonlyArray): void + clear(): void + count(label?: string): void + countReset(label?: string): void + debug(...args: ReadonlyArray): void + dir(item: any, options?: any): void + dirxml(...args: ReadonlyArray): void + error(...args: ReadonlyArray): void + group(...args: ReadonlyArray): void + groupCollapsed(...args: ReadonlyArray): void + groupEnd(): void + info(...args: ReadonlyArray): void + log(...args: ReadonlyArray): void + table(tabularData: any, properties?: ReadonlyArray): void + time(label?: string): void + timeEnd(label?: string): void + timeLog(label?: string, ...args: ReadonlyArray): void + trace(...args: ReadonlyArray): void + warn(...args: ReadonlyArray): void +} + +/** + * Context reference for the current console service in the Effect system, allowing access to the active console implementation from within the Effect context. + * + * **When to use** + * + * Use when you need an effect to run against a provided console implementation, + * such as tests or alternate runtimes, rather than the default console. + * + * **Details** + * + * When no override is provided, the reference resolves to `globalThis.console`. + * + * **Example** (Accessing the current console) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Console.consoleWith((console) => + * Effect.sync(() => { + * console.log("Hello from current console!") + * }) + * ) + * ``` + * + * @see {@link consoleWith} for using the current console service inside an effect + * + * @category references + * @since 2.0.0 + */ +export const Console: Context.Reference = effect.ConsoleRef + +/** + * Creates an Effect that provides access to the current console service and lets you perform operations with it within an Effect context. + * + * **Example** (Accessing the current console service) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Console.consoleWith((console) => + * Effect.sync(() => { + * console.log("Hello, world!") + * console.error("This is an error message") + * }) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const consoleWith = (f: (console: Console) => Effect.Effect): Effect.Effect => + core.withFiber((fiber) => f(fiber.getRef(Console))) + +/** + * Writes the supplied assertion message to the console as an error when `condition` is false; when `condition` is true, no console output is produced. + * + * **Example** (Logging failed assertions) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.assert(2 + 2 === 4, "Math is working correctly") + * yield* Console.assert(2 + 2 === 5, "This will be logged as an error") + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const assert = (condition: boolean, ...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.assert(condition, ...args) + }) + ) + +/** + * Runs the current console service's clear operation. + * + * **When to use** + * + * Use to request that the active console implementation clear its visible + * output. + * + * **Gotchas** + * + * The clearing behavior depends on the active console implementation and host + * environment. + * + * **Example** (Clearing console output) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.log("This will be cleared") + * yield* Console.clear + * yield* Console.log("This appears after clearing") + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const clear: Effect.Effect = consoleWith((console) => + effect.sync(() => { + console.clear() + }) +) + +/** + * Logs and increments the counter associated with `label`, using the console's default counter when no label is provided. + * + * **Example** (Counting repeated calls) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.count("my-counter") + * yield* Console.count("my-counter") // Will show: my-counter: 2 + * yield* Console.count() // Default counter + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const count = (label?: string): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.count(label) + }) + ) + +/** + * Resets the counter associated with the specified label back to zero. + * + * **Example** (Resetting a counter) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.count("my-counter") + * yield* Console.count("my-counter") // Will show: my-counter: 2 + * yield* Console.countReset("my-counter") + * yield* Console.count("my-counter") // Will show: my-counter: 1 + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const countReset = (label?: string): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.countReset(label) + }) + ) + +/** + * Writes a debug message through the current `Console` service. + * + * **Details** + * + * The arguments are passed to the service's `debug` method when the returned + * Effect is executed. Any filtering behavior depends on the active console + * implementation. + * + * **Example** (Writing debug messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.debug("Debug info:", { userId: 123, action: "login" }) + * yield* Console.debug("Processing step", 1, "of", 5) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const debug = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.debug(...args) + }) + ) + +/** + * Displays an interactive list of the properties of the specified object, optionally using console-specific inspection options for debugging complex data structures. + * + * **Example** (Inspecting an object) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const obj = { name: "John", age: 30, nested: { city: "New York" } } + * yield* Console.dir(obj) + * yield* Console.dir(obj, { depth: 2, colors: true }) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const dir = (item: any, options?: any): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.dir(item, options) + }) + ) + +/** + * Displays an interactive tree of descendant XML or HTML elements, which is particularly useful for inspecting DOM elements in browser environments. + * + * **Example** (Inspecting XML-like data) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.dirxml("Ada") + * }) + * + * Effect.runSync(program) + * // Ada + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const dirxml = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.dirxml(...args) + }) + ) + +/** + * Writes an error-level message to the console, typically displayed with error + * styling by the active console implementation. + * + * **Example** (Writing error messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.error("Something went wrong!") + * yield* Console.error("Error details:", { + * code: 500, + * message: "Internal Server Error" + * }) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const error = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.error(...args) + }) + ) + +/** + * Creates a scoped console group, optionally collapsed and labeled, and closes it automatically when the Effect scope is finalized. + * + * **Example** (Grouping scoped output) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.scoped( + * Effect.gen(function*() { + * yield* Console.group({ label: "User Processing" }) + * yield* Console.log("Loading user data...") + * yield* Console.log("Validating user...") + * yield* Console.log("User processed successfully") + * }) + * ) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const group = ( + options?: { label?: string | undefined; collapsed?: boolean | undefined } | undefined +): Effect.Effect => + consoleWith((console) => + effect.acquireRelease( + effect.sync(() => { + if (options?.collapsed) { + console.groupCollapsed(options.label) + } else { + console.group(options?.label) + } + }), + () => + effect.sync(() => { + console.groupEnd() + }) + ) + ) + +/** + * Writes an informational message to the console, typically displayed with info + * styling by the active console implementation. + * + * **Example** (Writing informational messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.info("Application started successfully") + * yield* Console.info("Server configuration:", { + * port: 3000, + * env: "development" + * }) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const info = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.info(...args) + }) + ) + +/** + * Logs a general-purpose message to the console. + * + * **Example** (Writing log messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.log("Hello, world!") + * yield* Console.log("User data:", { name: "John", age: 30 }) + * yield* Console.log("Processing", 42, "items") + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const log = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.log(...args) + }) + ) + +/** + * Displays tabular data as a formatted table in the console, optionally limited to selected properties. + * + * **Example** (Displaying tabular data) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const users = [ + * { name: "John", age: 30, city: "New York" }, + * { name: "Jane", age: 25, city: "London" }, + * { name: "Bob", age: 35, city: "Paris" } + * ] + * yield* Console.table(users) + * yield* Console.table(users, ["name", "age"]) // Only show specific columns + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const table = (tabularData: any, properties?: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.table(tabularData, properties) + }) + ) + +/** + * Starts a scoped timer for `label` and automatically ends it when the Effect scope is finalized. + * + * **Example** (Timing scoped work) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.scoped( + * Effect.gen(function*() { + * yield* Console.time("operation-timer") + * yield* Effect.sleep("1 second") + * yield* Console.log("Operation completed") + * // Timer ends automatically when scope closes + * }) + * ) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const time = (label?: string | undefined): Effect.Effect => + consoleWith((console) => + effect.acquireRelease( + effect.sync(() => { + console.time(label) + }), + () => + effect.sync(() => { + console.timeEnd(label) + }) + ) + ) + +/** + * Logs the elapsed time for an existing timer without stopping it, allowing progress reports for long-running operations. + * + * **Example** (Logging timer progress) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.scoped( + * Effect.gen(function*() { + * yield* Console.time("long-operation") + * yield* Effect.sleep("500 millis") + * yield* Console.timeLog("long-operation", "Halfway done") + * yield* Effect.sleep("500 millis") + * // Timer ends when scope closes + * }) + * ) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const timeLog = (label?: string, ...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.timeLog(label, ...args) + }) + ) + +/** + * Writes the current stack trace to the console to show how the current point in + * the code was reached. + * + * **Example** (Writing stack traces) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.trace("Debug trace point") + * yield* Console.trace("Function call:", { functionName: "processData" }) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const trace = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.trace(...args) + }) + ) + +/** + * Writes a warning-level message to the console, typically displayed with + * warning styling by the active console implementation. + * + * **Example** (Writing warning messages) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.warn("This feature is deprecated") + * yield* Console.warn("Performance warning:", { + * slowQuery: "SELECT * FROM large_table" + * }) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const warn = (...args: ReadonlyArray): Effect.Effect => + consoleWith((console) => + effect.sync(() => { + console.warn(...args) + }) + ) + +/** + * Runs an Effect inside an optionally labeled or collapsed console group, starting the group before execution and ending it after the Effect completes. + * + * **Example** (Wrapping an effect in a group) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.withGroup( + * Effect.gen(function*() { + * yield* Console.log("Step 1: Initialize") + * yield* Console.log("Step 2: Process") + * yield* Console.log("Step 3: Complete") + * }), + * { label: "Processing Steps", collapsed: false } + * ) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const withGroup = dual< + ( + options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + } + ) => (self: Effect.Effect) => Effect.Effect, + ( + self: Effect.Effect, + options?: { + readonly label?: string | undefined + readonly collapsed?: boolean | undefined + } + ) => Effect.Effect +>((args) => core.isEffect(args[0]), (self, options) => + consoleWith((console) => + effect.acquireUseRelease( + effect.sync(() => { + if (options?.collapsed) { + console.groupCollapsed(options.label) + } else { + console.group(options?.label) + } + }), + () => self, + () => + effect.sync(() => { + console.groupEnd() + }) + ) + )) + +/** + * Runs an Effect with a console timer, starting the timer before execution and ending it after the Effect completes. + * + * **Example** (Timing an effect) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.withTime( + * Effect.gen(function*() { + * yield* Effect.sleep("1 second") + * yield* Console.log("Operation completed") + * }), + * "my-operation" + * ) + * }) + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const withTime = dual< + (label?: string) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, label?: string) => Effect.Effect +>((args) => core.isEffect(args[0]), (self, label) => + consoleWith((console) => + effect.acquireUseRelease( + effect.sync(() => { + console.time(label) + }), + () => self, + () => + effect.sync(() => { + console.timeEnd(label) + }) + ) + )) diff --git a/repos/effect/packages/effect/src/Context.ts b/repos/effect/packages/effect/src/Context.ts new file mode 100644 index 0000000..cfc8cd9 --- /dev/null +++ b/repos/effect/packages/effect/src/Context.ts @@ -0,0 +1,1338 @@ +/** + * Stores Effect services in typed maps. + * + * A `Context` holds service implementations under `Context.Service` or + * `Context.Reference` keys, and its type records which keys are present. + * Effects use contexts as their environment, so services can be provided once + * instead of passed through every function call. This module includes helpers + * for creating keys, building contexts, adding and reading services, merging + * contexts, and selecting or removing services. + * + * @since 4.0.0 + */ +import type { Effect, EffectIterator } from "./Effect.ts" +import * as Effectable from "./Effectable.ts" +import * as Equal from "./Equal.ts" +import { dual, type LazyArg } from "./Function.ts" +import * as Hash from "./Hash.ts" +import type { Inspectable } from "./Inspectable.ts" +import { exitSucceed, PipeInspectableProto, withFiber } from "./internal/core.ts" +import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Types from "./Types.ts" + +/** + * String literal type used as the runtime type identifier for `Context` + * service keys. + * + * @category type IDs + * @since 4.0.0 + */ +export type ServiceTypeId = "~effect/Context/Service" + +/** + * Runtime type identifier attached to `Context` service keys and used by + * `isKey` to recognize them. + * + * @category type IDs + * @since 4.0.0 + */ +export const ServiceTypeId: ServiceTypeId = "~effect/Context/Service" + +/** + * Typed identifier for a service stored in a `Context`. + * + * **When to use** + * + * Use as the typed handle for storing, retrieving, and requiring a specific + * service in a `Context`. + * + * **Details** + * + * `Identifier` tracks the requirement in Effect types, while `Shape` is the + * service implementation retrieved by the key. A key is also an Effect value, + * so yielding it inside `Effect.gen` retrieves the service from the current + * fiber context. + * + * @see {@link Service} for creating required service keys + * @see {@link Reference} for creating service keys with default values + * + * @category models + * @since 4.0.0 + */ +export interface Key extends Effect { + readonly [ServiceTypeId]: ServiceTypeId + readonly Service: Shape + readonly Identifier: Identifier + readonly key: string + readonly stack?: string | undefined +} + +/** + * Context key with helper methods for working with a service. + * + * **Details** + * + * `context` creates a one-service `Context`, `use` and `useSync` retrieve the + * service from the current Effect context before applying a function, and `of` + * is a type-level helper for service values. + * + * **Example** (Defining a service key) + * + * ```ts + * import { Context } from "effect" + * + * // Define an identifier for a database service + * const Database = Context.Service<{ query: (sql: string) => string }>( + * "Database" + * ) + * + * // The key can be used to store and retrieve services + * const context = Context.make(Database, { query: (sql) => `Result: ${sql}` }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Service extends Key { + of(this: void, self: Shape): Shape + context(self: Shape): Context + use(f: (service: Shape) => Effect): Effect + useSync(f: (service: Shape) => A): Effect +} + +/** + * Class-style service key produced by `Context.Service()("Id")`. + * + * **When to use** + * + * Use when declaring a service as a class so the class value can serve as the + * `Context` key. + * + * **Details** + * + * The class itself is the `Context` key, and its string `key` identifies the + * service at runtime. + * + * @see {@link Service} for creating function-style keys or class-style service keys + * + * @category models + * @since 4.0.0 + */ +export interface ServiceClass + extends Service +{ + new(_: never): ServiceClass.Shape + readonly key: Identifier +} + +/** + * Namespace containing helper types for class-style `Context.Service` + * declarations. + * + * @since 4.0.0 + */ +export declare namespace ServiceClass { + /** + * Runtime and type-level metadata carried by a class-style service key, + * including its service type identifier, string key, and service shape. + * + * @category models + * @since 4.0.0 + */ + export interface Shape { + readonly [ServiceTypeId]: typeof ServiceTypeId + readonly key: Identifier + readonly Service: Service + } +} + +/** + * Creates a `Context` service key. + * + * **When to use** + * + * Use when you need to define a context service key for a dependency that must + * be provided by the surrounding context. + * + * **Details** + * + * Call `Context.Service("Key")` for a function-style key, or use the two-stage + * form `Context.Service()("Key")` for class-style service + * declarations. The returned key can be yielded as an Effect and passed to + * `Context.make`, `Context.add`, and the Context getter functions. + * + * **Gotchas** + * + * The string key is the runtime identity of the service. Reusing the same key + * string for unrelated services makes them occupy the same slot in a + * `Context`. + * + * **Example** (Creating service keys) + * + * ```ts + * import { Context } from "effect" + * + * // Create a simple service + * const Database = Context.Service<{ + * query: (sql: string) => string + * }>("Database") + * + * // Create a service class + * class Config extends Context.Service()("Config") {} + * + * // Use the services to create contexts + * const db = Context.make(Database, { + * query: (sql) => `Result: ${sql}` + * }) + * const config = Context.make(Config, { port: 8080 }) + * ``` + * + * @see {@link Reference} for service keys with default values + * + * @category constructors + * @since 4.0.0 + */ +export const Service: { + (key: string): Service + (): < + const Identifier extends string, + E, + R = Types.unassigned, + Args extends ReadonlyArray = never + >( + id: Identifier, + options?: { + readonly make: ((...args: Args) => Effect) | Effect | undefined + } | undefined + ) => + & ServiceClass + & ([Types.unassigned] extends [R] ? unknown + : { readonly make: [Args] extends [never] ? Effect : (...args: Args) => Effect }) + (): < + const Identifier extends string, + Make extends Effect | ((...args: any) => Effect) + >( + id: Identifier, + options: { + readonly make: Make + } + ) => + & ServiceClass< + Self, + Identifier, + Make extends + Effect | ((...args: infer _Args) => Effect) ? _A + : never + > + & { readonly make: Make } +} = function() { + const prevLimit = getStackTraceLimit() + setStackTraceLimit(2) + const err = new Error() + setStackTraceLimit(prevLimit) + function KeyClass() {} + const self = KeyClass as any as Types.Mutable> + Object.setPrototypeOf(self, ServiceProto) + // @effect-diagnostics-next-line floatingEffect:off + Object.defineProperty(self, "stack", { + get() { + return err.stack + } + }) + if (arguments.length > 0) { + self.key = arguments[0] + if (arguments[1]?.defaultValue) { + self[ReferenceTypeId] = ReferenceTypeId + self.defaultValue = arguments[1].defaultValue + } + return self + } + return function(key: string, options?: { + readonly make?: any + }) { + self.key = key + if (options?.make) { + ;(self as any).make = options.make + } + return self + } +} as any + +const ServiceProto: any = { + [ServiceTypeId]: ServiceTypeId, + ...Effectable.Prototype>({ + label: "Service", + evaluate(fiber) { + return exitSucceed(get(fiber.context, this)) + } + }), + toJSON(this: Service) { + return { + _id: "Service", + key: this.key, + stack: this.stack + } + }, + of(this: void, self: Service): Service { + return self + }, + context( + this: Service, + self: Shape + ): Context { + return make(this, self) + }, + use(this: Service, f: (service: any) => Effect): Effect { + return withFiber((fiber) => f(get(fiber.context, this))) + }, + useSync(this: Service, f: (service: any) => A): Effect { + return withFiber((fiber) => exitSucceed(f(get(fiber.context, this)))) + } +} + +const ReferenceTypeId = "~effect/Context/Reference" as const + +/** + * Service key with a lazily computed default value. + * + * **Details** + * + * When a `Reference` is requested from a `Context` that does not contain an + * override, Context getters that resolve references return the cached default + * value instead of failing. + * + * **Example** (Defining a reference with a default value) + * + * ```ts + * import { Context } from "effect" + * + * // Define a reference with a default value + * const LoggerRef: Context.Reference<{ log: (msg: string) => void }> = + * Context.Reference("Logger", { + * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * }) + * + * // The reference can be used without explicit provision + * const context = Context.empty() + * const logger = Context.get(context, LoggerRef) // Uses default value + * ``` + * + * @category models + * @since 3.11.0 + */ +export interface Reference extends Service { + readonly [ReferenceTypeId]: typeof ReferenceTypeId + readonly defaultValue: () => Shape + [Symbol.iterator](): EffectIterator> + new(_: never): {} +} + +/** + * Namespace containing utility types for `Context` service keys. + * + * **Example** (Extracting service types) + * + * ```ts + * import { Context } from "effect" + * + * const Database = Context.Service<{ + * query: (sql: string) => string + * }>("Database") + * + * // Extract service type from a key + * type DatabaseService = Context.Service.Shape + * + * // Extract identifier type from a key + * type DatabaseId = Context.Service.Identifier + * ``` + * + * @since 2.0.0 + */ +export declare namespace Service { + /** + * Type that matches any `Context` service key regardless of its identifier or + * service shape. + * + * **Example** (Typing any service key) + * + * ```ts + * import { Context } from "effect" + * + * // Any represents any possible service type + * const services: Array = [ + * Context.Service<{ log: (msg: string) => void }>("Logger"), + * Context.Service<{ query: (sql: string) => string }>("Database") + * ] + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Any = Key | Key + + /** + * Extracts the service implementation type stored behind a `Context` service + * key. + * + * **Example** (Extracting a service shape) + * + * ```ts + * import { Context } from "effect" + * + * const Database = Context.Service<{ query: (sql: string) => string }>( + * "Database" + * ) + * + * // Extract the service shape from the service + * type DatabaseService = Context.Service.Shape + * // DatabaseService is { query: (sql: string) => string } + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Shape = T extends Key ? S : never + + /** + * Extracts the identifier, or requirement type, associated with a `Context` + * service key. + * + * **Example** (Extracting a service identifier) + * + * ```ts + * import { Context } from "effect" + * + * const Database = Context.Service<{ query: (sql: string) => string }>( + * "Database" + * ) + * + * // Extract the identifier type from a key + * type DatabaseId = Context.Service.Identifier + * // DatabaseId is the identifier type + * ``` + * + * @category models + * @since 2.0.0 + */ + export type Identifier = T extends Key ? I : never +} + +const TypeId = "~effect/Context" as const + +/** + * Immutable collection of service implementations used for dependency + * injection in Effect programs. + * + * **Details** + * + * The type parameter tracks the service identifiers available in the context. + * At runtime, services are stored by each key's string `key`. + * + * **Example** (Creating a context with multiple services) + * + * ```ts + * import { Context } from "effect" + * + * // Create a context with multiple services + * const Logger = Context.Service<{ log: (msg: string) => void }>("Logger") + * const Database = Context.Service<{ query: (sql: string) => string }>( + * "Database" + * ) + * + * const context = Context.make(Logger, { + * log: (msg: string) => console.log(msg) + * }) + * .pipe(Context.add(Database, { query: (sql) => `Result: ${sql}` })) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Context extends Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _Services: Types.Contravariant + } + readonly mapUnsafe: ReadonlyMap + mutable: boolean +} + +/** + * Creates a `Context` from an existing service map. + * + * **When to use** + * + * Use when constructing a low-level `Context` from a trusted map whose lifecycle + * you control. + * + * **Gotchas** + * + * This is unsafe because later mutation of the provided map can affect the + * created `Context`. Prefer `empty`, `make`, `add`, or `merge` for normal + * Context construction. + * + * **Example** (Creating a context from a map) + * + * ```ts + * import { Context } from "effect" + * + * // Create a context from a Map (unsafe) + * const map = new Map([ + * ["Logger", { log: (msg: string) => console.log(msg) }] + * ]) + * + * const context = Context.makeUnsafe(map) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (mapUnsafe: ReadonlyMap): Context => { + const self = Object.create(Proto) + self.mapUnsafe = mapUnsafe + self.mutable = false + return self +} + +const Proto: Omit, "mapUnsafe" | "mutable"> = { + ...PipeInspectableProto, + [TypeId]: { + _Services: (_: never) => _ + }, + toJSON(this: Context) { + return { + _id: "Context", + services: Array.from(this.mapUnsafe).map(([key, value]) => ({ key, value })) + } + }, + [Equal.symbol](this: Context, that: unknown): boolean { + if ( + !isContext(that) + || this.mapUnsafe.size !== that.mapUnsafe.size + ) return false + for (const k of this.mapUnsafe.keys()) { + if ( + !that.mapUnsafe.has(k) || + !Equal.equals(this.mapUnsafe.get(k), that.mapUnsafe.get(k)) + ) { + return false + } + } + return true + }, + [Hash.symbol](this: Context): number { + return Hash.number(this.mapUnsafe.size) + } +} + +/** + * Checks whether the provided argument is a `Context`. + * + * **When to use** + * + * Use to narrow an unknown value before passing it to APIs that require a + * `Context`. + * + * **Details** + * + * This checks the runtime `Context` marker and does not inspect which services + * the context contains. + * + * **Gotchas** + * + * This guard only proves that the value is a `Context`; it does not prove that + * any specific service is present. + * + * **Example** (Checking for contexts) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * assert.strictEqual(Context.isContext(Context.empty()), true) + * ``` + * + * @see {@link isKey} for checking service keys + * @see {@link isReference} for checking references with defaults + * + * @category guards + * @since 2.0.0 + */ +export const isContext = (u: unknown): u is Context => hasProperty(u, TypeId) + +/** + * Checks whether the provided argument is a `Key`. + * + * **Example** (Checking for keys) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * assert.strictEqual(Context.isKey(Context.Service("Service")), true) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isKey = (u: unknown): u is Key => hasProperty(u, ServiceTypeId) + +/** + * Checks whether the provided argument is a `Reference`. + * + * **Example** (Checking for references) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * const LoggerRef = Context.Reference("Logger", { + * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * }) + * + * assert.strictEqual(Context.isReference(LoggerRef), true) + * assert.strictEqual(Context.isReference(Context.Service("Key")), false) + * ``` + * + * @category guards + * @since 3.11.0 + */ +export const isReference = (u: unknown): u is Reference => hasProperty(u, ReferenceTypeId) + +/** + * Returns an empty `Context`. + * + * **Example** (Creating an empty context) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * assert.strictEqual(Context.isContext(Context.empty()), true) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Context => emptyContext +const emptyContext = makeUnsafe(new Map()) + +/** + * Creates a new `Context` with a single service associated to the key. + * + * **Example** (Creating a context with one service) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * + * const context = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = ( + key: Key, + service: Types.NoInfer +): Context => makeUnsafe(new Map([[key.key, service]])) + +/** + * Adds a service to a given `Context`. + * + * **When to use** + * + * Use when you need to store a known service value in a `Context`. + * + * **Details** + * + * If the context already contains the same service key, the new service + * replaces the previous one. + * + * **Example** (Adding a service to a context) + * + * ```ts + * import { Context, pipe } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const someContext = Context.make(Port, { PORT: 8080 }) + * + * const context = pipe( + * someContext, + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @see {@link addOrOmit} for adding or removing a service from an `Option` + * + * @category adders + * @since 2.0.0 + */ +export const add: { + ( + key: Key, + service: Types.NoInfer + ): (self: Context) => Context + ( + self: Context, + key: Key, + service: Types.NoInfer + ): Context +} = dual(3, ( + self: Context, + key: Key, + service: Types.NoInfer +): Context => + withMapUnsafe(self, (map) => { + map.set(key.key, service) + })) + +/** + * Adds or removes a service depending on an `Option`. + * + * **When to use** + * + * Use when you need to add or omit a `Context` service based on an `Option`. + * + * **Details** + * + * When `service` is `Option.some`, the value is stored for the key. When it is + * `Option.none`, the key is removed from the returned `Context`. + * + * **Example** (Adding optional services) + * + * ```ts + * import { Context, Option } from "effect" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * + * const withPort = Context.empty().pipe( + * Context.addOrOmit(Port, Option.some({ PORT: 8080 })) + * ) + * + * const withoutPort = withPort.pipe( + * Context.addOrOmit(Port, Option.none()) + * ) + * ``` + * + * @see {@link add} for always storing a service value + * + * @category adders + * @since 4.0.0 + */ +export const addOrOmit: { + ( + key: Key, + service: Option.Option> + ): (self: Context) => Context + ( + self: Context, + key: Key, + service: Option.Option> + ): Context +} = dual(3, ( + self: Context, + key: Key, + service: Option.Option> +): Context => + withMapUnsafe(self, (map) => { + if (service._tag === "None") { + map.delete(key.key) + } else { + map.set(key.key, service.value) + } + })) + +/** + * Gets the service for a key, or evaluates the fallback when a non-reference + * key is absent. + * + * **When to use** + * + * Use when you need a fallback for a missing `Context.Service` key while still + * resolving `Context.Reference` defaults. + * + * **Details** + * + * If the key is a `Context.Reference` and no override is stored in the + * context, its cached default value is returned instead of the fallback. + * + * **Gotchas** + * + * The fallback is not evaluated for missing `Context.Reference` keys because + * references resolve to their default value. + * + * **Example** (Falling back for missing services) + * + * ```ts + * import { Context } from "effect" + * + * const Logger = Context.Service<{ log: (msg: string) => void }>("Logger") + * const Database = Context.Service<{ query: (sql: string) => string }>( + * "Database" + * ) + * + * const context = Context.make(Logger, { + * log: (msg: string) => console.log(msg) + * }) + * + * const logger = Context.getOrElse(context, Logger, () => ({ log: () => {} })) + * const database = Context.getOrElse( + * context, + * Database, + * () => ({ query: () => "fallback" }) + * ) + * + * console.log(logger === Context.get(context, Logger)) // true + * console.log(database.query("SELECT 1")) // "fallback" + * ``` + * + * @see {@link getOption} for returning `Option.none` when a non-reference key is missing + * + * @category getters + * @since 3.7.0 + */ +export const getOrElse: { + (key: Key, orElse: LazyArg): (self: Context) => S | B + (self: Context, key: Key, orElse: LazyArg): S | B +} = dual(3, (self: Context, key: Key, orElse: LazyArg): S | B => { + if (self.mapUnsafe.has(key.key)) { + return self.mapUnsafe.get(key.key)! as any + } + return isReference(key) ? getDefaultValue(key) : orElse() +}) + +/** + * Returns the service currently stored for a key, or `undefined` when the key + * is absent. + * + * **When to use** + * + * Use when you need to read the service stored for a key without resolving + * `Context.Reference` defaults. + * + * **Gotchas** + * + * This is a raw lookup and does not resolve default values for + * `Context.Reference` keys. + * + * @see {@link getOption} for a reference-aware optional lookup + * + * @category getters + * @since 4.0.0 + */ +export const getOrUndefined: { + (key: Key): (self: Context) => S | undefined + (self: Context, key: Key): S | undefined +} = dual( + 2, + (self: Context, key: Key): S | undefined => self.mapUnsafe.get(key.key) +) + +/** + * Gets the service for a key, throwing if an absent non-reference key cannot be + * resolved. + * + * **When to use** + * + * Use when you need to read a service from a context whose type does not prove + * the service is present. + * + * **Details** + * + * If the key is a `Context.Reference` and no override is stored in the + * context, its cached default value is returned. For absent non-reference keys, + * this function throws a runtime error. + * + * **Example** (Getting services unsafely) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const context = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual(Context.getUnsafe(context, Port), { PORT: 8080 }) + * assert.throws(() => Context.getUnsafe(context, Timeout)) + * ``` + * + * @see {@link get} for type-checked service access + * @see {@link getOption} for optional service access + * + * @category unsafe + * @since 4.0.0 + */ +export const getUnsafe: { + (service: Key): (self: Context) => S + (self: Context, services: Key): S +} = dual( + 2, + (self: Context, service: Key): S => { + if (!self.mapUnsafe.has(service.key)) { + if (ReferenceTypeId in service) return getDefaultValue(service as any) + throw serviceNotFoundError(service) + } + return self.mapUnsafe.get(service.key)! as any + } +) + +/** + * Gets a service from the context that corresponds to the given key. + * + * **When to use** + * + * Use when you need type-checked access to a service already included in the + * context type. + * + * **Example** (Getting a service from a context) + * + * ```ts + * import { Context, pipe } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const context = pipe( + * Context.make(Port, { PORT: 8080 }), + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @see {@link getOption} for optional service access + * @see {@link getOrElse} for fallback values + * + * @category getters + * @since 2.0.0 + */ +export const get: { + (service: Key): (self: Context) => S + (self: Context, service: Key): S +} = getUnsafe + +/** + * Gets the value for a `Context.Reference`, returning its cached default when + * the context does not contain an override. + * + * **When to use** + * + * Use when you need a `Context.Reference` value resolved from either a stored + * override or the reference's default value. + * + * **Details** + * + * Stored overrides take precedence. If no override is present, the reference's + * default value is computed lazily and cached on the reference itself. + * + * **Gotchas** + * + * Mutable default values can be shared across contexts unless an override is + * provided, because the default is cached on the `Context.Reference`. + * + * **Example** (Getting reference defaults unsafely) + * + * ```ts + * import { Context } from "effect" + * + * const LoggerRef = Context.Reference("Logger", { + * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * }) + * + * const context = Context.empty() + * const logger = Context.getReferenceUnsafe(context, LoggerRef) + * + * console.log(typeof logger.log) // "function" + * ``` + * + * @see {@link getUnsafe} for unsafe access with any service key + * @see {@link get} for type-checked reference-aware access + * @see {@link getOption} for optional access to non-reference keys + * + * @category unsafe + * @since 4.0.0 + */ +export const getReferenceUnsafe = (self: Context, service: Reference): S => { + if (!self.mapUnsafe.has(service.key)) { + return getDefaultValue(service as any) + } + return self.mapUnsafe.get(service.key)! as any +} + +const defaultValueCacheKey = "~effect/Context/defaultValue" as const + +const getDefaultValue = (ref: Reference) => { + if (defaultValueCacheKey in ref) { + return ref[defaultValueCacheKey] as any + } + return (ref as any)[defaultValueCacheKey] = ref.defaultValue() +} + +const serviceNotFoundError = (service: Key) => { + const error = new Error( + `Service not found${service.key ? `: ${String(service.key)}` : ""}` + ) + if (service.stack) { + const lines = service.stack.split("\n") + if (lines.length > 2) { + const afterAt = lines[2].match(/at (.*)/) + if (afterAt) { + error.message = error.message + ` (defined at ${afterAt[1]})` + } + } + } + if (error.stack) { + const lines = error.stack.split("\n") + lines.splice(1, 3) + error.stack = lines.join("\n") + } + return error +} + +/** + * Gets the service for a key safely wrapped in an `Option`. + * + * **When to use** + * + * Use when you need to read a `Context` service as an `Option` so absence is + * represented as data. + * + * **Details** + * + * Returns `Option.some` when the service is stored in the context. If the key + * is a `Context.Reference` and no override is stored, returns `Option.some` of + * the cached default value. Missing non-reference keys return `Option.none`. + * + * **Example** (Getting optional services) + * + * ```ts + * import { Context, Option } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const context = Context.make(Port, { PORT: 8080 }) + * + * assert.deepStrictEqual( + * Context.getOption(context, Port), + * Option.some({ PORT: 8080 }) + * ) + * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * ``` + * + * @see {@link getOrElse} for returning a fallback value directly + * + * @category getters + * @since 2.0.0 + */ +export const getOption: { + (service: Key): (self: Context) => Option.Option + (self: Context, service: Key): Option.Option +} = dual(2, (self: Context, service: Key): Option.Option => { + if (self.mapUnsafe.has(service.key)) { + return Option.some(self.mapUnsafe.get(service.key)! as any) + } + return isReference(service) ? Option.some(getDefaultValue(service as any)) : Option.none() +}) + +/** + * Merges two `Context`s into one. + * + * **When to use** + * + * Use when you need to combine two contexts. + * + * **Details** + * + * When both contexts contain the same service key, the service from `that` + * overrides the service from `self`. + * + * **Example** (Merging two contexts) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const firstContext = Context.make(Port, { PORT: 8080 }) + * const secondContext = Context.make(Timeout, { TIMEOUT: 5000 }) + * + * const context = Context.merge(firstContext, secondContext) + * + * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * ``` + * + * @see {@link mergeAll} for merging more than two contexts at once + * + * @category combining + * @since 2.0.0 + */ +export const merge: { + (that: Context): (self: Context) => Context + (self: Context, that: Context): Context +} = dual(2, (self: Context, that: Context): Context => { + if (self.mapUnsafe.size === 0) return that as any + if (that.mapUnsafe.size === 0) return self as any + return withMapUnsafe(self, (map) => { + that.mapUnsafe.forEach((value, key) => map.set(key, value)) + }) +}) + +/** + * Merges any number of `Context`s into one. + * + * **When to use** + * + * Use when you need to combine a variadic list of contexts. + * + * **Details** + * + * When multiple contexts contain the same service key, the service from the + * last context with that key is kept. + * + * **Example** (Merging multiple contexts) + * + * ```ts + * import { Context } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * const Host = Context.Service<{ HOST: string }>("Host") + * + * const firstContext = Context.make(Port, { PORT: 8080 }) + * const secondContext = Context.make(Timeout, { TIMEOUT: 5000 }) + * const thirdContext = Context.make(Host, { HOST: "localhost" }) + * + * const context = Context.mergeAll( + * firstContext, + * secondContext, + * thirdContext + * ) + * + * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) + * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * assert.deepStrictEqual(Context.get(context, Host), { HOST: "localhost" }) + * ``` + * + * @see {@link merge} for merging two contexts + * + * @category combining + * @since 3.12.0 + */ +export const mergeAll = >( + ...ctxs: [...{ [K in keyof T]: Context }] +): Context => { + const map = new Map() + for (let i = 0; i < ctxs.length; i++) { + ctxs[i].mapUnsafe.forEach((value, key) => { + map.set(key, value) + }) + } + return makeUnsafe(map) +} + +/** + * Returns a new `Context` that contains only the specified services. + * + * **When to use** + * + * Use when you want to keep an allowlist of services in a `Context`. + * + * **Example** (Picking services from a context) + * + * ```ts + * import { Context, Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const someContext = pipe( + * Context.make(Port, { PORT: 8080 }), + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * const context = pipe(someContext, Context.pick(Port)) + * + * assert.deepStrictEqual( + * Context.getOption(context, Port), + * Option.some({ PORT: 8080 }) + * ) + * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * ``` + * + * @see {@link omit} for removing selected services + * + * @category filtering + * @since 2.0.0 + */ +export const pick = >>( + ...services: S +) => +(self: Context): Context> => + withMapUnsafe(self, (map) => { + const keySet = new Set(services.map((key) => key.key)) + map.forEach((_, key) => { + if (keySet.has(key)) return + map.delete(key) + }) + }) + +/** + * Returns a new `Context` with the specified service keys removed. + * + * **When to use** + * + * Use when you want to remove a denylist of services from a `Context`. + * + * **Example** (Omitting services from a context) + * + * ```ts + * import { Context, Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const Port = Context.Service<{ PORT: number }>("Port") + * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") + * + * const someContext = pipe( + * Context.make(Port, { PORT: 8080 }), + * Context.add(Timeout, { TIMEOUT: 5000 }) + * ) + * + * const context = pipe(someContext, Context.omit(Timeout)) + * + * assert.deepStrictEqual( + * Context.getOption(context, Port), + * Option.some({ PORT: 8080 }) + * ) + * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * ``` + * + * @see {@link pick} for keeping selected services + * + * @category filtering + * @since 2.0.0 + */ +export const omit = >>( + ...keys: S +) => +(self: Context): Context>> => + withMapUnsafe(self, (map) => { + for (let i = 0; i < keys.length; i++) { + map.delete(keys[i].key) + } + }) + +/** + * Performs a series of mutations on a `Context`. Prevents unnecessary copying + * of the underlying map when multiple mutations are needed. + * + * **When to use** + * + * Use to apply several `Context` transformations in one callback while copying + * the underlying service map only once. + * + * @see {@link add} for adding or replacing a service + * @see {@link addOrOmit} for adding or removing a service from an `Option` + * @see {@link merge} for combining two contexts + * @see {@link pick} for keeping selected services + * @see {@link omit} for removing selected services + * + * @category mutations + * @since 4.0.0 + */ +export const mutate: { + ( + f: (context: Context) => Context + ): (self: Context) => Context + (self: Context, f: (context: Context) => Context): Context +} = dual( + 2, + (self: Context, f: (context: Context) => Context): Context => { + const next = makeUnsafe(new Map(self.mapUnsafe)) + next.mutable = true + const result = f(next) + result.mutable = false + return result + } +) + +const withMapUnsafe = (self: Context, f: (map: Map) => void): Context => { + if (self.mutable) { + f(self.mapUnsafe as any) + return self as any + } + const map = new Map(self.mapUnsafe) + f(map) + return makeUnsafe(map) +} + +/** + * Creates a context key with a default value. + * + * **When to use** + * + * Use when you need to define a context key with a lazily computed default + * value. + * + * **Details** + * + * `Context.Reference` allows you to create a key that can hold a value. You + * can provide a default value for the service, which will automatically be used + * when the context is accessed, or override it with a custom implementation + * when needed. The default value is computed lazily and cached on the + * reference. + * + * **Example** (Creating references with default values) + * + * ```ts + * import { Context } from "effect" + * + * // Create a reference with a default value + * const LoggerRef = Context.Reference("Logger", { + * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * }) + * + * // The reference provides the default value when accessed from an empty context + * const context = Context.empty() + * const logger = Context.get(context, LoggerRef) + * + * // You can also override the default value + * const customContext = Context.make(LoggerRef, { + * log: (msg: string) => `Custom: ${msg}` + * }) + * const customLogger = Context.get(customContext, LoggerRef) + * ``` + * + * @see {@link Service} for required services without default values + * + * @category references + * @since 3.11.0 + */ +export const Reference: ( + key: string, + options: { readonly defaultValue: () => Service } +) => Reference = Service as any diff --git a/repos/effect/packages/effect/src/Cron.ts b/repos/effect/packages/effect/src/Cron.ts new file mode 100644 index 0000000..b2312dc --- /dev/null +++ b/repos/effect/packages/effect/src/Cron.ts @@ -0,0 +1,1264 @@ +/** + * Utilities for recurring calendar schedules written as cron expressions or + * explicit field constraints. A `Cron` value stores allowed seconds, minutes, + * hours, days of month, months, weekdays, and an optional time zone. The module + * can create or parse schedules, compare them, test whether a date matches, and + * find previous or next scheduled occurrences. + * + * @since 2.0.0 + */ +import * as Arr from "./Array.ts" +import * as Data from "./Data.ts" +import type * as DateTime from "./DateTime.ts" +import * as Equal from "./Equal.ts" +import * as Equ from "./Equivalence.ts" +import { format } from "./Formatter.ts" +import { constVoid, dual, pipe } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" +import * as dateTime from "./internal/dateTime.ts" +import * as N from "./Number.ts" +import * as Option from "./Option.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as Result from "./Result.ts" +import * as String from "./String.ts" +import type { Mutable } from "./Types.ts" + +const TypeId = "~effect/time/Cron" + +/** + * Represents a cron schedule with time constraints and timezone information. + * + * **When to use** + * + * Use to represent a recurring calendar schedule that can be matched against + * dates or used to compute scheduled occurrences. + * + * **Details** + * + * A `Cron` instance defines when a scheduled task should run, supporting + * seconds, minutes, hours, days, months, and weekday constraints. It also + * supports timezone-aware scheduling. + * + * **Example** (Creating a cron schedule) + * + * ```ts + * import { Cron } from "effect" + * + * // Create a cron that runs at 9 AM on weekdays + * const weekdayMorning = Cron.make({ + * minutes: [0], + * hours: [9], + * days: [], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] // Monday to Friday + * }) + * + * // Check if a date matches the schedule + * const matches = Cron.match(weekdayMorning, new Date("2023-06-05T09:00:00")) + * console.log(matches) // true if it's 9 AM on a weekday + * ``` + * + * @see {@link make} for creating a schedule from explicit field constraints + * @see {@link parse} for creating a schedule from a cron expression string + * @see {@link match} for testing a date against a schedule + * @see {@link next} for finding the next scheduled occurrence + * + * @category models + * @since 2.0.0 + */ +export interface Cron extends Pipeable, Equal.Equal, Inspectable { + readonly [TypeId]: typeof TypeId + readonly tz: Option.Option + readonly seconds: ReadonlySet + readonly minutes: ReadonlySet + readonly hours: ReadonlySet + readonly days: ReadonlySet + readonly months: ReadonlySet + readonly weekdays: ReadonlySet + /** @internal */ + readonly and: boolean + /** @internal */ + readonly first: { + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly weekday: number + } + /** @internal */ + readonly last: { + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly weekday: number + } + /** @internal */ + readonly next: { + readonly second: ReadonlyArray + readonly minute: ReadonlyArray + readonly hour: ReadonlyArray + readonly day: ReadonlyArray + readonly month: ReadonlyArray + readonly weekday: ReadonlyArray + } + /** @internal */ + readonly prev: { + readonly second: ReadonlyArray + readonly minute: ReadonlyArray + readonly hour: ReadonlyArray + readonly day: ReadonlyArray + readonly month: ReadonlyArray + readonly weekday: ReadonlyArray + } +} + +function toPojo(cron: Cron): Record { + const out: Record = { + tz: cron.tz, + and: cron.and, + seconds: Arr.fromIterable(cron.seconds), + minutes: Arr.fromIterable(cron.minutes), + hours: Arr.fromIterable(cron.hours), + days: Arr.fromIterable(cron.days), + months: Arr.fromIterable(cron.months), + weekdays: Arr.fromIterable(cron.weekdays) + } + return out +} + +const CronProto = { + [TypeId]: TypeId, + [Equal.symbol](this: Cron, that: unknown) { + return isCron(that) && equals(this, that) + }, + [Hash.symbol](this: Cron): number { + return pipe( + Hash.hash(this.tz), + Hash.combine(Hash.hash(this.and)), + Hash.combine(Hash.array(Arr.fromIterable(this.seconds))), + Hash.combine(Hash.array(Arr.fromIterable(this.minutes))), + Hash.combine(Hash.array(Arr.fromIterable(this.hours))), + Hash.combine(Hash.array(Arr.fromIterable(this.days))), + Hash.combine(Hash.array(Arr.fromIterable(this.months))), + Hash.combine(Hash.array(Arr.fromIterable(this.weekdays))) + ) + }, + toObject(this: Cron) { + return toPojo(this) + }, + toString(this: Cron) { + return `Cron(${format(toPojo(this))})` + }, + toJSON(this: Cron) { + const out = toPojo(this) + out["_id"] = "Cron" + return out + }, + [NodeInspectSymbol](this: Cron) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Checks whether a given value is a Cron instance. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `Cron` schedule. + * + * **Details** + * + * This function is a type guard that determines whether the provided + * value is a valid Cron instance by checking for the presence of the + * Cron type identifier. + * + * **Example** (Checking cron values) + * + * ```ts + * import { Cron } from "effect" + * + * const cron = Cron.make({ + * minutes: [0], + * hours: [9], + * days: [1, 15], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] + * }) + * + * console.log(Cron.isCron(cron)) // true + * console.log(Cron.isCron({})) // false + * console.log(Cron.isCron("not a cron")) // false + * ``` + * + * @see {@link make} for constructing a `Cron` value directly + * @see {@link parse} for constructing a `Cron` value from a string + * + * @category guards + * @since 2.0.0 + */ +export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId) + +/** + * Creates a Cron instance from time constraints. + * + * **When to use** + * + * Use to build a cron schedule from explicit sets of allowed time-field values. + * + * **Details** + * + * Constructs a cron schedule by specifying which seconds, minutes, hours, + * days, months, and weekdays the schedule should match. Empty arrays leave a + * time unit unrestricted. If only days or weekdays are restricted, that field + * must match. When both are restricted, the default matches either field; set + * `and: true` to require both fields to match. Weekdays range from `0` (Sunday) + * to `7` (also Sunday). The constructor throws a `RangeError` when a field + * contains a non-integer or out-of-range value. + * + * **Example** (Creating schedules from constraints) + * + * ```ts + * import { Cron } from "effect" + * + * // Every day at midnight + * const midnight = Cron.make({ + * minutes: [0], + * hours: [0], + * days: [ + * 1, + * 2, + * 3, + * 4, + * 5, + * 6, + * 7, + * 8, + * 9, + * 10, + * 11, + * 12, + * 13, + * 14, + * 15, + * 16, + * 17, + * 18, + * 19, + * 20, + * 21, + * 22, + * 23, + * 24, + * 25, + * 26, + * 27, + * 28, + * 29, + * 30, + * 31 + * ], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [0, 1, 2, 3, 4, 5, 6] + * }) + * + * // Every 15 minutes during business hours on weekdays + * const businessHours = Cron.make({ + * minutes: [0, 15, 30, 45], + * hours: [9, 10, 11, 12, 13, 14, 15, 16, 17], + * days: [], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] // Monday to Friday + * }) + * ``` + * + * @see {@link parse} for building a schedule from a cron expression string + * + * @category constructors + * @since 2.0.0 + */ +export const make = (values: { + readonly seconds?: Iterable | undefined + readonly minutes: Iterable + readonly hours: Iterable + readonly days: Iterable + readonly months: Iterable + readonly weekdays: Iterable + readonly and?: boolean | undefined + readonly tz?: DateTime.TimeZone | undefined +}): Cron => { + const o: Mutable = Object.create(CronProto) + o.seconds = restrictions.seconds(values.seconds ?? [0]) + o.minutes = restrictions.minutes(values.minutes) + o.hours = restrictions.hours(values.hours) + o.days = restrictions.days(values.days) + o.months = restrictions.months(values.months) + o.weekdays = restrictions.weekdays(values.weekdays) + o.and = values.and === true + o.tz = Option.fromUndefinedOr(values.tz) + + const seconds = Array.from(o.seconds) + const minutes = Array.from(o.minutes) + const hours = Array.from(o.hours) + const days = Array.from(o.days) + const months = Array.from(o.months) + const weekdays = Array.from(o.weekdays) + + o.first = { + second: seconds[0] ?? 0, + minute: minutes[0] ?? 0, + hour: hours[0] ?? 0, + day: days[0] ?? 1, + month: (months[0] ?? 1) - 1, + weekday: weekdays[0] ?? 0 + } + + o.last = { + second: seconds[seconds.length - 1] ?? 59, + minute: minutes[minutes.length - 1] ?? 59, + hour: hours[hours.length - 1] ?? 23, + day: days[days.length - 1] ?? 31, + month: (months[months.length - 1] ?? 12) - 1, + weekday: weekdays[weekdays.length - 1] ?? 6 + } + + o.next = { + second: lookup.next.second(seconds), + minute: lookup.next.minute(minutes), + hour: lookup.next.hour(hours), + day: lookup.next.day(days), + month: lookup.next.month(months), + weekday: lookup.next.weekday(weekdays) + } + + o.prev = { + second: lookup.prev.second(seconds), + minute: lookup.prev.minute(minutes), + hour: lookup.prev.hour(hours), + day: lookup.prev.day(days), + month: lookup.prev.month(months), + weekday: lookup.prev.weekday(weekdays) + } + + return o +} + +const makeRestrictions = ( + field: string, + min: number, + max: number, + normalize: (value: number) => number = (value) => value +): (values: Iterable) => Set => +(values) => { + const restrictions: Array = [] + for (const value of values) { + if (!Number.isInteger(value) || value < min || value > max) { + throw new RangeError(`${field} must contain only integers between ${min} and ${max}`) + } + restrictions.push(normalize(value)) + } + return new Set(Arr.sort(restrictions, N.Order)) +} + +const restrictions = { + seconds: makeRestrictions("seconds", 0, 59), + minutes: makeRestrictions("minutes", 0, 59), + hours: makeRestrictions("hours", 0, 23), + days: makeRestrictions("days", 1, 31), + months: makeRestrictions("months", 1, 12), + weekdays: makeRestrictions("weekdays", 0, 7, (value) => value === 7 ? 0 : value) +} + +const makeLookupTable = ( + size: number, + dir: "next" | "prev" +): (values: ReadonlyArray) => Array => +(values) => { + const result = new Array(size).fill(undefined) + if (values.length === 0) { + return result + } + + let current: number | undefined = undefined + + if (dir === "next") { + let index = values.length - 1 + for (let i = size - 1; i >= 0; i--) { + while (index >= 0 && values[index] >= i) { + current = values[index--] + } + result[i] = current + } + } else { + let index = 0 + for (let i = 0; i < size; i++) { + while (index < values.length && values[index] <= i) { + current = values[index++] + } + result[i] = current + } + } + + return result +} + +const lookup = { + prev: { + second: makeLookupTable(60, "prev"), + minute: makeLookupTable(60, "prev"), + hour: makeLookupTable(24, "prev"), + day: makeLookupTable(32, "prev"), + month: makeLookupTable(13, "prev"), + weekday: makeLookupTable(7, "prev") + }, + next: { + second: makeLookupTable(60, "next"), + minute: makeLookupTable(60, "next"), + hour: makeLookupTable(24, "next"), + day: makeLookupTable(32, "next"), + month: makeLookupTable(13, "next"), + weekday: makeLookupTable(7, "next") + } +} + +const CronParseErrorTypeId = "~effect/time/Cron/CronParseError" + +/** + * Represents an error that occurs when parsing a cron expression fails. + * + * **When to use** + * + * Use to handle invalid cron expression failures returned by `parse`. + * + * **Details** + * + * This error provides information about what went wrong during parsing, + * including the error message and optionally the input that caused the error. + * + * **Example** (Handling cron parse failures) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const result = Cron.parse("invalid expression") + * if (Result.isFailure(result)) { + * const error: Cron.CronParseError = result.failure + * console.log(error.message) // "Invalid number of segments in cron expression" + * console.log(error.input) // "invalid expression" + * } + * ``` + * + * @see {@link parse} for the parser that returns this error in `Result.fail` + * @see {@link isCronParseError} for narrowing unknown values to this error type + * + * @category errors + * @since 4.0.0 + */ +export class CronParseError extends Data.TaggedError("CronParseError")<{ + readonly message: string + readonly input?: string +}> { + readonly [CronParseErrorTypeId]: typeof CronParseErrorTypeId = CronParseErrorTypeId +} + +/** + * Checks whether a given value is a CronParseError instance. + * + * **When to use** + * + * Use to narrow an unknown failure before handling it as a cron parse error. + * + * **Details** + * + * This function is a type guard that determines whether the provided + * value is a CronParseError by checking for the presence of the + * CronParseError type identifier. + * + * **Example** (Checking cron parse errors) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const result = Cron.parse("invalid cron expression") + * if (Result.isFailure(result)) { + * const error = result.failure + * console.log(Cron.isCronParseError(error)) // true + * } + * + * console.log(Cron.isCronParseError(new Error("regular error"))) // false + * console.log(Cron.isCronParseError("not an error")) // false + * ``` + * + * @see {@link CronParseError} for the parse error type + * @see {@link parse} for producing `CronParseError` values on invalid input + * + * @category guards + * @since 4.0.0 + */ +export const isCronParseError = (u: unknown): u is CronParseError => hasProperty(u, CronParseErrorTypeId) + +/** + * Parses a cron expression safely into a `Cron` instance, returning a `Result` + * instead of throwing. + * + * **When to use** + * + * Use to parse cron expressions from configuration or user input while handling + * invalid input as a `Result`. + * + * **Details** + * + * The expression may contain five fields, where seconds default to `0`, or six + * fields including seconds. Fields support `*`, comma-separated values, ranges, + * steps, and month or weekday aliases. Invalid expressions fail with + * `CronParseError`. When both the day-of-month and weekday fields are + * restricted, a date matches if either field matches. When either field starts + * with `*`, both fields must match; an unrestricted field always matches. + * + * **Example** (Parsing cron expressions) + * + * ```ts + * import { Cron, Result } from "effect" + * import * as assert from "node:assert" + * + * // At 04:00 on every day-of-month from 8 through 14. + * assert.deepStrictEqual( + * Cron.parse("0 0 4 8-14 * *"), + * Result.succeed(Cron.make({ + * seconds: [0], + * minutes: [0], + * hours: [4], + * days: [8, 9, 10, 11, 12, 13, 14], + * months: [], + * weekdays: [] + * })) + * ) + * ``` + * + * @see {@link parseUnsafe} for throwing on invalid cron expressions + * @see {@link make} for constructing a schedule from explicit field constraints + * + * @category constructors + * @since 2.0.0 + */ +export const parse = (cron: string, tz?: DateTime.TimeZone | string): Result.Result => { + const segments = cron.trim().split(/\s+/).filter(String.isNonEmpty) + if (segments.length !== 5 && segments.length !== 6) { + return Result.fail(new CronParseError({ message: `Invalid number of segments in cron expression`, input: cron })) + } + + if (segments.length === 5) { + segments.unshift("0") + } + + const [seconds, minutes, hours, days, months, weekdays] = segments + const zone = tz === undefined || dateTime.isTimeZone(tz) ? + Result.succeed(tz) : + Result.fromOption( + dateTime.zoneFromString(tz), + () => new CronParseError({ message: `Invalid time zone in cron expression`, input: tz }) + ) + + return Result.all({ + tz: zone, + seconds: parseSegment(seconds, secondOptions), + minutes: parseSegment(minutes, minuteOptions), + hours: parseSegment(hours, hourOptions), + days: parseSegment(days, dayOptions), + months: parseSegment(months, monthOptions), + weekdays: parseSegment(weekdays, weekdayOptions) + }).pipe(Result.map(({ tz, seconds, minutes, hours, days, months, weekdays }) => + make({ + tz, + seconds: seconds.values, + minutes: minutes.values, + hours: hours.values, + days: days.values, + months: months.values, + weekdays: weekdays.values, + and: (days.wildcard || weekdays.wildcard) && days.values.size !== 0 && weekdays.values.size !== 0 + }) + )) +} + +/** + * Parses a cron expression into a `Cron` instance, throwing on failure. + * + * **When to use** + * + * Use when you expect the input to be valid and want to avoid handling the + * `Result` type. + * + * **Example** (Parsing cron expressions unsafely) + * + * ```ts + * import { Cron } from "effect" + * + * // At 04:00 on every day-of-month from 8 through 14 + * const cron = Cron.parseUnsafe("0 0 4 8-14 * *") + * + * // With timezone + * const cronWithTz = Cron.parseUnsafe("0 0 9 * * *", "America/New_York") + * + * // This would throw an error + * // const invalid = Cron.parseUnsafe("invalid expression") + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const parseUnsafe = (cron: string, tz?: DateTime.TimeZone | string): Cron => Result.getOrThrow(parse(cron, tz)) + +/** + * Returns `true` when a date/time matches a `Cron` schedule. + * + * **When to use** + * + * Use to test whether a specific date/time satisfies a cron schedule. + * + * **Details** + * + * The schedule's timezone determines which calendar fields are read from the + * input; the host system's timezone is used when the schedule has no timezone. + * Seconds, minutes, hours, and months are checked against their restrictions; + * an empty set leaves that field unrestricted. If only `days` or `weekdays` is + * restricted, that field must match. If both are restricted, either may match + * unless the schedule was created with `and: true`, which requires both to + * match. + * + * **Example** (Matching dates against a schedule) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *", "UTC")) + * + * // Check if specific dates match + * const matches1 = Cron.match(cron, new Date("2021-01-08T04:00:00Z")) + * console.log(matches1) // true - 4 AM on the 8th + * + * const matches2 = Cron.match(cron, new Date("2021-01-08T05:00:00Z")) + * console.log(matches2) // false - wrong hour + * + * const matches3 = Cron.match(cron, new Date("2021-01-07T04:00:00Z")) + * console.log(matches3) // false - wrong day + * ``` + * + * @see {@link next} for finding the next matching date/time + * @see {@link prev} for finding the previous matching date/time + * + * @category predicates + * @since 2.0.0 + */ +export const match = (cron: Cron, date: DateTime.DateTime.Input): boolean => { + const parts = dateTime.makeZonedUnsafe(date, { + timeZone: Option.getOrUndefined(cron.tz) + }).pipe(dateTime.toParts) + + if (cron.seconds.size !== 0 && !cron.seconds.has(parts.second)) { + return false + } + + if (cron.minutes.size !== 0 && !cron.minutes.has(parts.minute)) { + return false + } + + if (cron.hours.size !== 0 && !cron.hours.has(parts.hour)) { + return false + } + + if (cron.months.size !== 0 && !cron.months.has(parts.month)) { + return false + } + + if (cron.days.size === 0 && cron.weekdays.size === 0) { + return true + } + + if (cron.and) { + return (cron.days.size === 0 || cron.days.has(parts.day)) && + (cron.weekdays.size === 0 || cron.weekdays.has(parts.weekDay)) + } + + if (cron.weekdays.size === 0) { + return cron.days.has(parts.day) + } + + if (cron.days.size === 0) { + return cron.weekdays.has(parts.weekDay) + } + + return cron.days.has(parts.day) || cron.weekdays.has(parts.weekDay) +} + +const daysInMonth = (date: Date): number => + new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate() + +/** + * Returns the next scheduled date/time for the given Cron instance. + * + * **When to use** + * + * Use to find the next occurrence of a cron schedule after a specific date/time + * or after the current time. + * + * **Details** + * + * Searches for the next date and time when the cron schedule should trigger, + * starting after the specified date/time or after the current time when no + * date is provided. + * + * **Example** (Finding the next occurrence) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *", "UTC")) + * + * // Get next run after a specific date + * const after = new Date("2021-01-01T00:00:00Z") + * const nextRun = Cron.next(cron, after) + * console.log(nextRun.toISOString()) // 2021-01-08T04:00:00.000Z + * + * // Get next run from current time + * const nextFromNow = Cron.next(cron) + * console.log(nextFromNow) // Next occurrence from now + * ``` + * + * @see {@link prev} for finding the previous scheduled occurrence + * @see {@link sequence} for iterating future scheduled occurrences + * + * @category getters + * @since 2.0.0 + */ +export const next = (cron: Cron, now?: DateTime.DateTime.Input): Date => { + return stepCron(cron, now, "next") +} + +/** + * Returns the previous scheduled date/time for the given Cron instance. + * + * **When to use** + * + * Use to find the most recent occurrence of a cron schedule before a specific + * date/time or before the current time. + * + * **Details** + * + * When no date/time is provided, the search starts from the current time. + * + * **Gotchas** + * + * The search is strict: if the supplied date/time already matches the schedule, + * the result is the earlier occurrence. + * + * @see {@link next} for finding the next scheduled occurrence + * + * @category getters + * @since 3.20.0 + */ +export const prev = (cron: Cron, now?: DateTime.DateTime.Input): Date => { + return stepCron(cron, now, "prev") +} + +const stepCron = (cron: Cron, now: DateTime.DateTime.Input | undefined, direction: "next" | "prev"): Date => { + const tz = Option.getOrUndefined(cron.tz) + const zoned = dateTime.makeZonedUnsafe(now ?? new Date(), { + timeZone: tz + }) + + const reverse = direction === "prev" + const tick = reverse ? -1 : 1 + const table = cron[direction] + const boundary = reverse ? cron.last : cron.first + + const needsStep = reverse ? + (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.makeZonedUnsafe(current, { + timeZone: zoned.zone, + adjustForTimeZone: true, + disambiguation: reverse ? "later" : undefined + }).pipe(dateTime.toDate) + + const drift = current.getTime() - adjusted.getTime() + if (reverse ? drift !== 0 : drift > 0) { + current.setTime(reverse ? adjusted.getTime() : current.getTime() + drift) + } + } + + const result = dateTime.mutate(zoned, (current) => { + current.setUTCSeconds(current.getUTCSeconds() + tick, 0) + + for (let i = 0; i < 10_000; i++) { + if (cron.seconds.size !== 0) { + const currentSecond = current.getUTCSeconds() + const nextSecond = table.second[currentSecond] + if (nextSecond === undefined) { + current.setUTCMinutes(current.getUTCMinutes() + tick, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextSecond, currentSecond)) { + current.setUTCSeconds(nextSecond) + adjustDst(current) + continue + } + } + + if (cron.minutes.size !== 0) { + const currentMinute = current.getUTCMinutes() + const nextMinute = table.minute[currentMinute] + if (nextMinute === undefined) { + current.setUTCHours(current.getUTCHours() + tick, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextMinute, currentMinute)) { + current.setUTCMinutes(nextMinute, boundary.second) + adjustDst(current) + continue + } + } + + if (cron.hours.size !== 0) { + const currentHour = current.getUTCHours() + const nextHour = table.hour[currentHour] + if (nextHour === undefined) { + current.setUTCDate(current.getUTCDate() + tick) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextHour, currentHour)) { + current.setUTCHours(nextHour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + if (cron.weekdays.size !== 0 || cron.days.size !== 0) { + if (cron.and) { + const matchesDay = cron.days.size === 0 || cron.days.has(current.getUTCDate()) + const matchesWeekday = cron.weekdays.size === 0 || cron.weekdays.has(current.getUTCDay()) + if (!matchesDay || !matchesWeekday) { + current.setUTCDate(current.getUTCDate() + tick) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } else { + let a: number = reverse ? -Infinity : Infinity + let b: number = reverse ? -Infinity : Infinity + + if (cron.weekdays.size !== 0) { + const currentWeekday = current.getUTCDay() + const nextWeekday = table.weekday[currentWeekday] + if (nextWeekday === undefined) { + a = reverse ? + boundary.weekday - 7 - currentWeekday : + 7 - currentWeekday + boundary.weekday + } else { + a = nextWeekday - currentWeekday + } + } + + if (cron.days.size !== 0 && a !== 0) { + const currentDay = current.getUTCDate() + const nextDay = table.day[currentDay] + if (nextDay === undefined) { + if (reverse) { + const previous = new Date(current) + // Day zero is the previous month's last day. These two probes cover every + // valid day-of-month. + previous.setUTCDate(0) + let day = table.day[previous.getUTCDate()] + if (day === undefined) { + previous.setUTCDate(0) + day = table.day[previous.getUTCDate()] + } + if (day === undefined) { + throw new Error("Unable to find cron date") + } + previous.setUTCDate(day) + b = (previous.getTime() - current.getTime()) / 86_400_000 + } else { + b = daysInMonth(current) - currentDay + boundary.day + } + } else if (!reverse && nextDay > daysInMonth(current)) { + // The next matching day does not exist in the current month. Setting it + // directly would overflow and skip earlier matching days next month. + b = daysInMonth(current) - currentDay + boundary.day + } else { + b = nextDay - currentDay + } + } + + const addDays = reverse ? Math.max(a, b) : Math.min(a, b) + if (addDays !== 0) { + current.setUTCDate(current.getUTCDate() + addDays) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + } + + if (cron.months.size !== 0) { + const currentMonth = current.getUTCMonth() + 1 + const nextMonth = table.month[currentMonth] + const clampBoundaryDay = (targetMonthIndex: number): number => { + const maxDayInMonth = daysInMonth(new Date(Date.UTC(current.getUTCFullYear(), targetMonthIndex + 1, 0))) + if (cron.days.size !== 0 && cron.weekdays.size === 0) { + return reverse ? table.day[maxDayInMonth] ?? maxDayInMonth : boundary.day + } + return reverse ? maxDayInMonth : 1 + } + if (nextMonth === undefined) { + current.setUTCFullYear(current.getUTCFullYear() + tick) + current.setUTCMonth(boundary.month, clampBoundaryDay(boundary.month)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + if (needsStep(nextMonth, currentMonth)) { + const targetMonthIndex = nextMonth - 1 + current.setUTCMonth(targetMonthIndex, clampBoundaryDay(targetMonthIndex)) + current.setUTCHours(boundary.hour, boundary.minute, boundary.second) + adjustDst(current) + continue + } + } + + return + } + + throw new Error("Unable to find cron date") + }) + + return dateTime.toDateUtc(result) +} + +/** + * Returns an infinite iterator that yields dates matching the Cron schedule. + * + * **When to use** + * + * Use to lazily iterate future occurrences of a cron schedule. + * + * **Details** + * + * The iterator generates an infinite sequence of dates when the cron schedule + * should trigger, starting after the specified date/time or after the current + * time when no date is provided. + * + * **Example** (Iterating scheduled occurrences) + * + * ```ts + * import { Cron, Result } from "effect" + * + * const cron = Result.getOrThrow(Cron.parse("0 0 9 * * 1-5", "UTC")) // 9 AM weekdays + * + * // Get first 5 occurrences + * const iterator = Cron.sequence(cron, new Date("2023-01-01T00:00:00Z")) + * const next5 = Array.from({ length: 5 }, () => iterator.next().value.toISOString()) + * + * console.log(next5) + * // ["2023-01-02T09:00:00.000Z", "2023-01-03T09:00:00.000Z", ...] + * ``` + * + * @see {@link next} for computing one next occurrence + * + * @category sequencing + * @since 2.0.0 + */ +export const sequence = function*(cron: Cron, now?: DateTime.DateTime.Input): IterableIterator { + while (true) { + yield now = next(cron, now) + } +} + +/** + * Equivalence instance for comparing the timezone, field restrictions, and + * day-matching mode of two `Cron` schedules. + * + * **When to use** + * + * Use to compare cron schedules through APIs that accept an equivalence + * relation. + * + * **Details** + * + * This comparison checks the optional timezone, the `and` day-matching mode, + * seconds, minutes, hours, days, months, and weekdays. + * + * **Example** (Comparing schedules with equivalence) + * + * ```ts + * import { Cron } from "effect" + * + * const cron1 = Cron.make({ + * minutes: [0, 30], + * hours: [9], + * days: [1, 15], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] + * }) + * + * const cron2 = Cron.make({ + * minutes: [30, 0], // Different order + * hours: [9], + * days: [15, 1], // Different order + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] + * }) + * + * console.log(Cron.Equivalence(cron1, cron2)) // true + * ``` + * + * @see {@link equals} for directly comparing two `Cron` values + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.make((self, that) => + Equal.equals(self.tz, that.tz) && + self.and === that.and && + restrictionsEquals(self.seconds, that.seconds) && + restrictionsEquals(self.minutes, that.minutes) && + restrictionsEquals(self.hours, that.hours) && + restrictionsEquals(self.days, that.days) && + restrictionsEquals(self.months, that.months) && + restrictionsEquals(self.weekdays, that.weekdays) +) + +const restrictionsArrayEquals = Equ.Array(Equ.strictEqual()) +const restrictionsEquals = (self: ReadonlySet, that: ReadonlySet): boolean => + restrictionsArrayEquals(Arr.fromIterable(self), Arr.fromIterable(that)) + +/** + * Checks whether two `Cron` instances have equal timezone values, field + * restrictions, and day-matching modes. + * + * **When to use** + * + * Use to directly compare two cron schedules, including their timezones and + * day-matching modes. + * + * **Details** + * + * The comparison checks the optional timezone, the `and` day-matching mode, + * seconds, minutes, hours, days, months, and weekdays. + * + * **Example** (Checking schedule equality) + * + * ```ts + * import { Cron } from "effect" + * + * const cron1 = Cron.make({ + * minutes: [0], + * hours: [9], + * days: [1, 15], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] + * }) + * + * const cron2 = Cron.make({ + * minutes: [0], + * hours: [9], + * days: [1, 15], + * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + * weekdays: [1, 2, 3, 4, 5] + * }) + * + * console.log(Cron.equals(cron1, cron2)) // true + * console.log(Cron.equals(cron1)(cron2)) // true (curried form) + * ``` + * + * @see {@link Equivalence} for the reusable equivalence instance + * + * @category predicates + * @since 2.0.0 + */ +export const equals: { + (that: Cron): (self: Cron) => boolean + (self: Cron, that: Cron): boolean +} = dual(2, (self: Cron, that: Cron): boolean => Equivalence(self, that)) + +interface SegmentOptions { + min: number + max: number + aliases?: Record | undefined + normalize?: ((value: number) => number) | undefined +} + +interface ParsedSegment { + readonly values: Set + readonly wildcard: boolean +} + +const secondOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const minuteOptions: SegmentOptions = { + min: 0, + max: 59 +} + +const hourOptions: SegmentOptions = { + min: 0, + max: 23 +} + +const dayOptions: SegmentOptions = { + min: 1, + max: 31 +} + +const monthOptions: SegmentOptions = { + min: 1, + max: 12, + aliases: { + jan: 1, + feb: 2, + mar: 3, + apr: 4, + may: 5, + jun: 6, + jul: 7, + aug: 8, + sep: 9, + oct: 10, + nov: 11, + dec: 12 + } +} + +const weekdayOptions: SegmentOptions = { + min: 0, + max: 7, + normalize: (value) => value === 7 ? 0 : value, + aliases: { + sun: 0, + mon: 1, + tue: 2, + wed: 3, + thu: 4, + fri: 5, + sat: 6 + } +} + +const parseSegment = ( + input: string, + options: SegmentOptions +): Result.Result => { + const values = new Set() + const fields = input.split(",") + const first = splitStep(fields[0]!) + const wildcard = first[0] === "*" + const normalize = options.normalize ?? ((value: number) => value) + const add = wildcard && (first[1] === undefined || first[1] === 1) ? + constVoid : + (value: number) => { + values.add(normalize(value)) + } + + for (let index = 0; index < fields.length; index++) { + const field = fields[index]! + const [raw, step] = index === 0 ? first : splitStep(field) + if (step !== undefined) { + if (!Number.isInteger(step)) { + return Result.fail(new CronParseError({ message: `Expected step value to be a positive integer`, input })) + } + if (step < 1) { + return Result.fail(new CronParseError({ message: `Expected step value to be greater than 0`, input })) + } + if (step > options.max) { + return Result.fail( + new CronParseError({ message: `Expected step value to be less than or equal to ${options.max}`, input }) + ) + } + } + + if (raw === "*") { + if (index === 0 && (step === undefined || step === 1)) { + continue + } + for (let i = options.min; i <= options.max; i += step ?? 1) { + add(i) + } + } else { + const [left, right] = splitRange(raw, options.aliases) + if (!Number.isInteger(left)) { + return Result.fail(new CronParseError({ message: `Expected a positive integer`, input })) + } + if (left < options.min || left > options.max) { + return Result.fail( + new CronParseError({ message: `Expected a value between ${options.min} and ${options.max}`, input }) + ) + } + + if (right === undefined) { + for (let i = left; i <= (step === undefined ? left : options.max); i += step ?? 1) { + add(i) + } + } else { + if (!Number.isInteger(right)) { + return Result.fail(new CronParseError({ message: `Expected a positive integer`, input })) + } + if (right < options.min || right > options.max) { + return Result.fail( + new CronParseError({ message: `Expected a value between ${options.min} and ${options.max}`, input }) + ) + } + if (left > right) { + return Result.fail(new CronParseError({ message: `Invalid value range`, input })) + } + + for (let i = left; i <= right; i += step ?? 1) { + add(i) + } + } + } + } + + return Result.succeed({ values, wildcard }) +} + +const splitStep = (input: string): [string, number | undefined] => { + const separator = input.indexOf("/") + if (separator !== -1) { + const step = input.slice(separator + 1) + return [input.slice(0, separator), decimalRegex.test(step) ? Number(step) : NaN] + } + + return [input, undefined] +} + +const splitRange = (input: string, aliases?: Record): [number, number | undefined] => { + const separator = input.indexOf("-") + if (separator !== -1) { + return [aliasOrValue(input.slice(0, separator), aliases), aliasOrValue(input.slice(separator + 1), aliases)] + } + + return [aliasOrValue(input, aliases), undefined] +} + +function aliasOrValue(field: string, aliases?: Record): number { + return aliases?.[String.toLowerCase(field)] ?? (decimalRegex.test(field) ? Number(field) : NaN) +} + +const decimalRegex = /^\d+$/ diff --git a/repos/effect/packages/effect/src/Crypto.ts b/repos/effect/packages/effect/src/Crypto.ts new file mode 100644 index 0000000..d3cdc01 --- /dev/null +++ b/repos/effect/packages/effect/src/Crypto.ts @@ -0,0 +1,318 @@ +/** + * Defines a platform-independent service for cryptographic operations. + * + * Runtime packages provide concrete implementations backed by the host + * platform's cryptography APIs. This module defines the service interface and a + * constructor from random-byte and digest primitives. The service provides + * secure random bytes and numbers, UUIDv4 and UUIDv7 generation, shuffling, and + * SHA message digests. + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as PlatformError from "./PlatformError.ts" + +const TypeId = "~effect/platform/Crypto" + +/** + * Digest algorithms supported by the platform `Crypto` service. + * + * **Gotchas** + * + * SHA-1 is included for interoperability with existing protocols. Do not use + * SHA-1 for new security-sensitive designs. + * + * **Example** (Using a digest algorithm) + * + * ```ts + * import { Crypto } from "effect" + * + * const algorithm: Crypto.DigestAlgorithm = "SHA-256" + * ``` + * + * @category models + * @since 4.0.0 + */ +export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" + +/** + * Platform-agnostic cryptographic operations. + * + * **Details** + * + * `Crypto` implementations must use cryptographically secure platform APIs. + * The random generator helpers are derived by the `make` constructor from + * the random methods on this service. + * + * **Example** (Using cryptographic operations) + * + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * const bytes = yield* crypto.randomBytes(16) + * const uuidv4 = yield* crypto.randomUUIDv4 + * const uuidv7 = yield* crypto.randomUUIDv7 + * const hash = yield* crypto.digest("SHA-256", bytes) + * return { uuidv4, uuidv7, hash } + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Crypto { + readonly [TypeId]: typeof TypeId + + /** + * Generates a random integer in the range Number.MIN_SAFE_INTEGER to + * Number.MAX_SAFE_INTEGER (both inclusive). + */ + nextIntUnsafe(): number + + /** + * Generates a random number in the range 0 (inclusive) to 1 (exclusive). + */ + nextDoubleUnsafe(): number + + /** + * Generates cryptographically secure random bytes. + */ + randomBytes(size: number): Effect.Effect + + /** + * Computes a cryptographic digest for the supplied data. + */ + digest( + algorithm: DigestAlgorithm, + data: Uint8Array + ): Effect.Effect + + /** + * Generates a cryptographically secure random number between 0 (inclusive) + * and 1 (exclusive). + */ + readonly random: Effect.Effect + + /** + * Generates a cryptographically secure random boolean. + */ + readonly randomBoolean: Effect.Effect + + /** + * Generates a cryptographically secure random integer between + * `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` (both inclusive). + */ + readonly randomInt: Effect.Effect + + /** + * Generates a cryptographically secure random number between `min` + * (inclusive) and `max` (exclusive). + */ + randomBetween(min: number, max: number): Effect.Effect + + /** + * Generates a cryptographically secure random integer between `min` and `max`. + * + * **Details** + * + * The lower bound is rounded up with `Math.ceil` and the upper bound is + * rounded down with `Math.floor`. By default the range is inclusive; set + * `options.halfOpen: true` to exclude the upper bound. + */ + randomIntBetween(min: number, max: number, options?: { + readonly halfOpen?: boolean | undefined + }): Effect.Effect + + /** + * Uses the cryptographically secure random generator to shuffle the supplied + * iterable. + */ + randomShuffle(elements: Iterable): Effect.Effect> + + /** + * Generates a cryptographically secure UUIDv4 string. + */ + readonly randomUUIDv4: Effect.Effect + + /** + * Generates a cryptographically secure UUIDv7 string. + */ + readonly randomUUIDv7: Effect.Effect +} + +/** + * Service tag for platform cryptography. + * + * **When to use** + * + * Use when you need to provide or retrieve the full platform cryptography + * service from an effect's context. + * + * **Details** + * + * Providing this service supplies platform-agnostic cryptographic operations + * such as hashing, UUID generation, and secure random values. + * + * @see {@link make} for constructing a Crypto service from primitive operations + * + * @category services + * @since 4.0.0 + */ +export const Crypto: Context.Service = Context.Service("effect/Crypto") + +/** + * Creates a `Crypto` service from the primitive implementation, deriving the + * random generator helpers and UUID generation from those primitives. + * + * **When to use** + * + * Use to build a Crypto service for a platform integration, test layer, or + * custom runtime from primitive random-byte and digest operations. + * + * **Details** + * + * The constructor derives random numbers, booleans, integer ranges, shuffling, + * and UUID generation from `impl.randomBytes`. Digest operations delegate to + * `impl.digest`. + * + * **Gotchas** + * + * `impl.randomBytes` must return cryptographically secure bytes of the + * requested length. UUID formatting mutates the byte array returned for UUID + * generation, so the implementation should return a fresh array for each call. + * + * **Example** (Creating a Crypto service) + * + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + impl: { + readonly randomBytes: (size: number) => Uint8Array + readonly digest: ( + algorithm: DigestAlgorithm, + data: Uint8Array + ) => Effect.Effect + } +): Crypto => { + const randomBytesUnsafe = impl.randomBytes + + const randomBytes: Crypto["randomBytes"] = (size) => Effect.map(validateSize("randomBytes", size), randomBytesUnsafe) + + const nextDoubleUnsafe = (): number => { + const bytes = randomBytesUnsafe(7) + const value = ((bytes[0] & 0x1f) * 2 ** 48) + (bytes[1] * 2 ** 40) + (bytes[2] * 2 ** 32) + + (bytes[3] * 2 ** 24) + (bytes[4] * 2 ** 16) + (bytes[5] * 2 ** 8) + bytes[6] + return value / 2 ** 53 + } + + const nextIntUnsafe = (): number => + Math.floor(nextDoubleUnsafe() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER + 1)) + Number.MIN_SAFE_INTEGER + + return Crypto.of({ + [TypeId]: TypeId, + randomBytes, + nextDoubleUnsafe, + nextIntUnsafe, + digest: impl.digest, + random: Effect.sync(() => nextDoubleUnsafe()), + randomBoolean: Effect.sync(() => nextDoubleUnsafe() > 0.5), + randomInt: Effect.sync(() => nextIntUnsafe()), + randomBetween: (min, max) => Effect.sync(() => nextDoubleUnsafe() * (max - min) + min), + randomIntBetween(min, max, options) { + const extra = options?.halfOpen === true ? 0 : 1 + return Effect.sync(() => { + const minInt = Math.ceil(min) + const maxInt = Math.floor(max) + return Math.floor(nextDoubleUnsafe() * (maxInt - minInt + extra)) + minInt + }) + }, + randomShuffle: (elements) => + Effect.sync(() => { + const buffer = Array.from(elements) + for (let i = buffer.length - 1; i >= 1; i = i - 1) { + const index = Math.min(i, Math.floor(nextDoubleUnsafe() * (i + 1))) + const value = buffer[i]! + buffer[i] = buffer[index]! + buffer[index] = value + } + return buffer + }), + randomUUIDv4: Effect.sync(() => formatUUIDv4(randomBytesUnsafe(16))), + randomUUIDv7: Effect.clockWith((clock) => + Effect.succeed(formatUUIDv7(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(16))) + ) + }) +} + +const validateSize = (method: string, size: number): Effect.Effect => + Number.isSafeInteger(size) && size >= 0 + ? Effect.succeed(size) + : Effect.fail(PlatformError.badArgument({ + module: "Crypto", + method, + description: "size must be a non-negative safe integer" + })) + +const hex = (byte: number): string => byte.toString(16).padStart(2, "0") + +const formatUUID = (bytes: Uint8Array): string => { + const segments = [ + bytes.subarray(0, 4), + bytes.subarray(4, 6), + bytes.subarray(6, 8), + bytes.subarray(8, 10), + bytes.subarray(10, 16) + ] + + return segments.map((segment) => Array.from(segment, hex).join("")).join("-") +} + +const formatUUIDv4 = (bytes: Uint8Array): string => { + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return formatUUID(bytes) +} + +const maxUUIDv7Timestamp = 2 ** 48 - 1 + +const formatUUIDv7 = (timestampMillis: number, bytes: Uint8Array): string => { + const timestamp = Math.min(Math.max(0, Math.trunc(timestampMillis)), maxUUIDv7Timestamp) + + bytes[0] = Math.floor(timestamp / 2 ** 40) + bytes[1] = Math.floor(timestamp / 2 ** 32) & 0xff + bytes[2] = Math.floor(timestamp / 2 ** 24) & 0xff + bytes[3] = Math.floor(timestamp / 2 ** 16) & 0xff + bytes[4] = Math.floor(timestamp / 2 ** 8) & 0xff + bytes[5] = timestamp & 0xff + bytes[6] = (bytes[6] & 0x0f) | 0x70 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return formatUUID(bytes) +} diff --git a/repos/effect/packages/effect/src/Data.ts b/repos/effect/packages/effect/src/Data.ts new file mode 100644 index 0000000..2062977 --- /dev/null +++ b/repos/effect/packages/effect/src/Data.ts @@ -0,0 +1,774 @@ +/** + * Defines helpers for small immutable data models. + * + * This module helps create plain classes, tagged classes, tagged unions, and + * typed errors with readonly fields. Tagged values carry a `_tag` field, which + * makes them easy to narrow with pattern matching or simple checks. These + * helpers are commonly used for domain values and errors in Effect programs. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.ts" +import * as core from "./internal/core.ts" +import * as Pipeable from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type * as Types from "./Types.ts" +import type { Unify } from "./Unify.ts" + +/** + * Provides a base class for immutable data types. + * + * **When to use** + * + * Use when you need a lightweight immutable value type with `.pipe()` support. + * + * **Details** + * + * Extend `Class` with a type parameter to declare fields. The constructor + * accepts those fields as a single object argument. When there are no fields + * the argument is optional. Instances are `Readonly` and `Pipeable`. + * + * **Example** (Defining a value class) + * + * ```ts + * import { Data, Equal } from "effect" + * + * class Person extends Data.Class<{ readonly name: string }> {} + * + * const mike1 = new Person({ name: "Mike" }) + * const mike2 = new Person({ name: "Mike" }) + * + * console.log(Equal.equals(mike1, mike2)) + * // true + * ``` + * + * @see {@link TaggedClass} — adds a `_tag` field + * @see {@link Error} — yieldable error variant + * + * @category constructors + * @since 2.0.0 + */ +export const Class: new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => Readonly & Pipeable.Pipeable = class extends Pipeable.Class { + constructor(props: any) { + super() + if (props) { + Object.assign(this, props) + } + } +} as any + +/** + * Provides a base class for immutable data types with a `_tag` discriminator. + * + * **When to use** + * + * Use when you need a single-variant tagged type or an ad-hoc discriminator. + * + * **Details** + * + * Like {@link Class}, but the resulting instances also carry a + * `readonly _tag: Tag` property. The `_tag` is excluded from the constructor + * argument. + * + * **Example** (Defining a tagged class) + * + * ```ts + * import { Data } from "effect" + * + * class Person extends Data.TaggedClass("Person")<{ + * readonly name: string + * }> {} + * + * const mike = new Person({ name: "Mike" }) + * console.log(mike._tag) + * // "Person" + * ``` + * + * @see {@link Class} — without a `_tag` + * @see {@link TaggedError} — tagged error variant + * @see {@link TaggedEnum} — multi-variant unions + * + * @category constructors + * @since 2.0.0 + */ +export const TaggedClass = ( + tag: Tag +): new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +) => Readonly & { readonly _tag: Tag } & Pipeable.Pipeable => + class extends Class { + readonly _tag = tag + } as any + +/** + * Transforms a record of variant definitions into a discriminated union type. + * + * **When to use** + * + * Use when you have two or more variants that share a common `_tag` discriminator. + * + * **Details** + * + * Each key in the record becomes a variant with `readonly _tag` set to that + * key. Use with {@link taggedEnum} to get constructors and matchers. + * + * **Gotchas** + * + * Variant records must **not** include a `_tag` property; it is added automatically. + * + * **Example** (Defining a tagged enum) + * + * ```ts + * import { Data } from "effect" + * + * type HttpError = Data.TaggedEnum<{ + * BadRequest: { readonly status: 400; readonly message: string } + * NotFound: { readonly status: 404 } + * }> + * + * // Equivalent to: + * // | { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string } + * // | { readonly _tag: "NotFound"; readonly status: 404 } + * + * const { BadRequest, NotFound } = Data.taggedEnum() + * + * const err = BadRequest({ status: 400, message: "missing id" }) + * console.log(err._tag) + * // "BadRequest" + * ``` + * + * @see {@link taggedEnum} — constructors and matchers for a `TaggedEnum` + * @see {@link TaggedEnum.WithGenerics} — generic tagged enums + * @see {@link TaggedEnum.Constructor} — the constructor object type + * + * @category models + * @since 2.0.0 + */ +export type TaggedEnum< + A extends Record> & UntaggedChildren +> = keyof A extends infer Tag ? Tag extends keyof A ? Types.Simplify< + { readonly _tag: Tag } & { readonly [K in keyof A[Tag]]: A[Tag][K] } + > + : never + : never + +type ChildrenAreTagged = keyof A extends infer K ? K extends keyof A ? "_tag" extends keyof A[K] ? true + : false + : never + : never + +type UntaggedChildren = true extends ChildrenAreTagged + ? "It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property." + : unknown + +/** + * Namespace for `TaggedEnum` utility types. + * + * **When to use** + * + * Use to reference utility types for constructing, extracting, and matching + * `TaggedEnum` variants. + * + * **Details** + * + * Provides helper types for: + * - Generic tagged enums ({@link TaggedEnum.WithGenerics}, {@link TaggedEnum.Kind}) + * - Extracting constructor arguments ({@link TaggedEnum.Args}) and variant + * values ({@link TaggedEnum.Value}) + * - Full constructor objects ({@link TaggedEnum.Constructor}) + * + * @since 2.0.0 + */ +export declare namespace TaggedEnum { + /** + * Defines a tagged enum shape that accepts generic type parameters. + * + * **When to use** + * + * Use when variant payloads need to be parameterized, such as `Result`. + * + * **Details** + * + * Extend this interface and set `taggedEnum` to your union type, using + * `this["A"]`, `this["B"]`, etc. as placeholders for the generics. The + * `Count` parameter declares how many generics are used (up to 4). + * + * **Example** (Defining a generic tagged enum) + * + * ```ts + * import { Data } from "effect" + * + * type MyResult = Data.TaggedEnum<{ + * Failure: { readonly error: E } + * Success: { readonly value: A } + * }> + * + * interface MyResultDef extends Data.TaggedEnum.WithGenerics<2> { + * readonly taggedEnum: MyResult + * } + * + * const { Failure, Success } = Data.taggedEnum() + * + * const ok = Success({ value: 42 }) + * // ok: { readonly _tag: "Success"; readonly value: number } + * ``` + * + * @see {@link Kind} — apply concrete types to a `WithGenerics` definition + * @see {@link taggedEnum} — constructors and matchers + * + * @category models + * @since 2.0.0 + */ + export interface WithGenerics { + readonly taggedEnum: { readonly _tag: string } + readonly numberOfGenerics: Count + + readonly A: unknown + readonly B: unknown + readonly C: unknown + readonly D: unknown + } + + /** + * Applies concrete type arguments to a `WithGenerics` definition, producing + * the resulting tagged union type. + * + * **When to use** + * + * Use to refer to a specific instantiation of a generic tagged enum in type signatures. + * + * **Example** (Applying generics) + * + * ```ts + * import type { Data } from "effect" + * + * type Option = Data.TaggedEnum<{ + * None: {} + * Some: { readonly value: A } + * }> + * interface OptionDef extends Data.TaggedEnum.WithGenerics<1> { + * readonly taggedEnum: Option + * } + * + * // Resolve to the concrete union for `string` + * type StringOption = Data.TaggedEnum.Kind + * // { _tag: "None" } | { _tag: "Some"; value: string } + * ``` + * + * @see {@link WithGenerics} — define the generic shape + * + * @category utility types + * @since 2.0.0 + */ + export type Kind< + Z extends WithGenerics, + A = unknown, + B = unknown, + C = unknown, + D = unknown + > = (Z & { + readonly A: A + readonly B: B + readonly C: C + readonly D: D + })["taggedEnum"] + + /** + * Extracts the constructor argument type for a specific variant of a tagged + * union. + * + * **When to use** + * + * Use to derive the argument object expected by a constructor for one tagged + * union variant. + * + * **Details** + * + * Returns `void` if the variant has no fields beyond `_tag`. + * + * **Example** (Extracting variant args) + * + * ```ts + * import type { Data } from "effect" + * + * type Result = + * | { readonly _tag: "Ok"; readonly value: number } + * | { readonly _tag: "Err"; readonly error: string } + * + * type OkArgs = Data.TaggedEnum.Args + * // { readonly value: number } + * + * type ErrArgs = Data.TaggedEnum.Args + * // { readonly error: string } + * ``` + * + * @see {@link Value} — extracts the full variant type (including `_tag`) + * + * @category utility types + * @since 2.0.0 + */ + export type Args< + A extends { readonly _tag: string }, + K extends A["_tag"], + E = Extract + > = { + readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] + } extends infer T ? Types.VoidIfEmpty + : never + + /** + * Extracts the full variant type (including `_tag`) for a specific tag. + * + * **When to use** + * + * Use to select one full tagged-union variant by its `_tag` value. + * + * **Example** (Extracting a variant type) + * + * ```ts + * import type { Data } from "effect" + * + * type Result = + * | { readonly _tag: "Ok"; readonly value: number } + * | { readonly _tag: "Err"; readonly error: string } + * + * type OkVariant = Data.TaggedEnum.Value + * // { readonly _tag: "Ok"; readonly value: number } + * ``` + * + * @see {@link Args} — extracts fields without `_tag` + * + * @category utility types + * @since 2.0.0 + */ + export type Value< + A extends { readonly _tag: string }, + K extends A["_tag"] + > = Extract + + /** + * The full constructors-and-matchers object type returned by {@link taggedEnum}. + * + * **When to use** + * + * Use when you want to annotate an exported constructor bundle so downstream + * code keeps exact variant constructors and exhaustive matching. + * + * **Details** + * + * Includes: + * - A constructor function for each variant (keyed by tag name) + * - `$is(tag)` — returns a type-guard that checks only the `_tag` field; + * safe when the tag is globally unique and the value was produced by your + * constructors. For untrusted input, validate with the `Schema` module first. + * - `$match` — exhaustive pattern matching (data-last or data-first) + * + * **Example** (Using the constructor object) + * + * ```ts + * import { Data } from "effect" + * + * type Shape = + * | { readonly _tag: "Circle"; readonly radius: number } + * | { readonly _tag: "Rect"; readonly w: number; readonly h: number } + * + * const { Circle, Rect, $is, $match } = Data.taggedEnum() + * + * const shape = Circle({ radius: 10 }) + * + * // Type guard + * if ($is("Circle")(shape)) { + * console.log(shape.radius) + * } + * + * // Pattern matching + * const label = $match(shape, { + * Circle: (s) => `circle r=${s.radius}`, + * Rect: (s) => `rect ${s.w}x${s.h}` + * }) + * ``` + * + * @see {@link taggedEnum} — creates constructors and matchers + * + * @category types + * @since 3.1.0 + */ + export type Constructor = Types.Simplify< + { + readonly [Tag in A["_tag"]]: ConstructorFrom< + Extract, + "_tag" + > + } & { + readonly $is: ( + tag: Tag + ) => (u: unknown) => u is Extract + readonly $match: { + < + Cases extends { + readonly [Tag in A["_tag"]]: ( + args: Extract + ) => any + } + >( + cases: Cases + ): (value: A) => Unify> + < + Cases extends { + readonly [Tag in A["_tag"]]: ( + args: Extract + ) => any + } + >( + value: A, + cases: Cases + ): Unify> + } + } + > + + /** + * Function type that constructs a tagged-union variant from its fields, + * excluding the keys listed in `Tag`. + * + * **When to use** + * + * Use to type an individual constructor for one tagged-union variant. + * + * **Details** + * + * The constructor returns the full variant type `A`. If no fields remain + * after excluding `Tag` keys, the constructor argument type becomes `void`. + * + * @category utility types + * @since 4.0.0 + */ + export type ConstructorFrom = ( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends Tag ? never : P]: A[P] }> + ) => A + + /** + * Type-guard and pattern-matching interface for generic tagged enums. + * + * **When to use** + * + * Use to type the `$is` and `$match` helpers for generic tagged enums. + * + * **Details** + * + * This is the `$is` / `$match` portion of the object returned by + * {@link taggedEnum} when used with a {@link WithGenerics} definition. + * + * @see {@link Constructor} — the non-generic equivalent + * + * @category models + * @since 3.2.0 + */ + export interface GenericMatchers> { + readonly $is: ( + tag: Tag + ) => { + >( + u: T + ): u is T & { readonly _tag: Tag } + (u: unknown): u is Extract, { readonly _tag: Tag }> + } + readonly $match: { + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract< + TaggedEnum.Kind, + { readonly _tag: Tag } + > + ) => any + } + >( + cases: Cases + ): ( + self: TaggedEnum.Kind + ) => Unify> + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract< + TaggedEnum.Kind, + { readonly _tag: Tag } + > + ) => any + } + >( + self: TaggedEnum.Kind, + cases: Cases + ): Unify> + } + } +} + +/** + * Creates constructors and matchers for a `TaggedEnum` type. + * + * **When to use** + * + * Use when you model a closed union with plain data objects and want + * construction, tag checks, and exhaustive matching from the same definition. + * + * **Details** + * + * Returns an object with: + * - One constructor per variant (keyed by tag name) + * - `$is(tag)` — returns a type-guard function that checks only the `_tag` field + * - `$match` — exhaustive pattern matching (data-first or data-last) + * + * **Gotchas** + * + * - Constructors produce **plain objects**, not class instances. + * - `$is(tag)` only checks the `_tag` field, not the full structure. It relies + * on the tag being globally unique and the value being produced by your + * constructors. For untrusted input, validate with the `Schema` module first. + * + * **Example** (Creating and matching tagged enum values) + * + * ```ts + * import { Data } from "effect" + * + * type HttpError = Data.TaggedEnum<{ + * BadRequest: { readonly message: string } + * NotFound: { readonly url: string } + * }> + * + * const { BadRequest, NotFound, $is, $match } = Data.taggedEnum() + * + * const err = NotFound({ url: "/missing" }) + * + * // Type guard + * console.log($is("NotFound")(err)) // true + * + * // Pattern matching + * const msg = $match(err, { + * BadRequest: (e) => e.message, + * NotFound: (e) => `${e.url} not found` + * }) + * console.log(msg) // "/missing not found" + * ``` + * + * **Example** (Defining a generic tagged enum) + * + * ```ts + * import { Data } from "effect" + * + * type MyResult = Data.TaggedEnum<{ + * Failure: { readonly error: E } + * Success: { readonly value: A } + * }> + * interface MyResultDef extends Data.TaggedEnum.WithGenerics<2> { + * readonly taggedEnum: MyResult + * } + * const { Failure, Success } = Data.taggedEnum() + * + * const ok = Success({ value: 42 }) + * // ok: { readonly _tag: "Success"; readonly value: number } + * ``` + * + * @see {@link TaggedEnum} — the type-level companion + * @see {@link TaggedEnum.Constructor} — the returned object type + * @see {@link TaggedEnum.WithGenerics} — generic enum support + * + * @category constructors + * @since 2.0.0 + */ +export const taggedEnum: { + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > + + (): TaggedEnum.Constructor +} = () => + new Proxy( + {}, + { + get(_target, tag, _receiver) { + if (tag === "$is") { + return Predicate.isTagged + } else if (tag === "$match") { + return taggedMatch + } + return (props: any) => ({ ...props, _tag: tag }) + } + } + ) as any + +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(self: A, cases: Cases): ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(cases: Cases): (value: A) => ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(): any { + if (arguments.length === 1) { + const cases = arguments[0] as Cases + return function(value: A): ReturnType { + return cases[value._tag as A["_tag"]](value as any) + } + } + const value = arguments[0] as A + const cases = arguments[1] as Cases + return cases[value._tag as A["_tag"]](value as any) +} + +/** + * Provides a base class for yieldable errors. + * + * **When to use** + * + * Use when you need yieldable errors that do **not** need tag-based + * discrimination. + * + * **Details** + * + * Extends `Cause.YieldableError`, so instances can be yielded inside + * `Effect.gen` to fail the enclosing effect. Fields are passed as a single + * object; when there are no fields the argument is optional. If a `message` + * field is provided, it becomes the error's `.message`. + * + * **Example** (Defining a yieldable error) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class NetworkError extends Data.Error<{ + * readonly code: number + * readonly message: string + * }> {} + * + * const program = Effect.gen(function*() { + * return yield* new NetworkError({ code: 500, message: "timeout" }) + * }) + * + * // The effect fails with a NetworkError + * Effect.runSync(Effect.exit(program)) + * ``` + * + * @see {@link TaggedError} — adds a `_tag` for `Effect.catchTag` + * @see {@link Class} — non-error data class + * + * @category constructors + * @since 2.0.0 + */ +export const Error: new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => Cause.YieldableError & Readonly = core.Error + +/** + * Creates a tagged error class with a `_tag` discriminator. + * + * **When to use** + * + * Use when you need domain errors with discriminated-union handling. + * + * **Details** + * + * Like {@link Error}, but instances also carry a `readonly _tag` property, + * enabling `Effect.catchTag` and `Effect.catchTags` for tag-based recovery. + * The `_tag` is excluded from the constructor argument. Yielding an instance + * inside `Effect.gen` fails the effect with this error. + * + * **Example** (Recovering by tag) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class NotFound extends Data.TaggedError("NotFound")<{ + * readonly resource: string + * }> {} + * + * class Forbidden extends Data.TaggedError("Forbidden")<{ + * readonly reason: string + * }> {} + * + * const program = Effect.gen(function*() { + * return yield* new NotFound({ resource: "/users/42" }) + * }) + * + * const recovered = program.pipe( + * Effect.catchTag("NotFound", (e) => + * Effect.succeed(`missing: ${e.resource}`)) + * ) + * ``` + * + * @see {@link Error} — without a `_tag` + * @see {@link TaggedClass} — tagged class that is not an error + * + * @category constructors + * @since 2.0.0 + */ +export const TaggedError: ( + tag: Tag +) => new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +) => Cause.YieldableError & { readonly _tag: Tag } & Readonly = core.TaggedError as any diff --git a/repos/effect/packages/effect/src/DateTime.ts b/repos/effect/packages/effect/src/DateTime.ts new file mode 100644 index 0000000..36df2d8 --- /dev/null +++ b/repos/effect/packages/effect/src/DateTime.ts @@ -0,0 +1,2922 @@ +/** + * Works with absolute instants, UTC date-times, zoned date-times, and time + * zones. + * + * A `DateTime` always represents an absolute point in time with epoch + * milliseconds. It may also carry a `TimeZone` for calendar parts, formatting, + * and zone-aware transformations. This module includes constructors, time-zone + * helpers, comparisons, date arithmetic, current-time effects, and formatting + * functions. + * + * @since 3.6.0 + */ +import type { IllegalArgumentError } from "./Cause.ts" +import * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import type * as Equ from "./Equivalence.ts" +import { dual, flow, type LazyArg } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as Internal from "./internal/dateTime.ts" +import { provideService } from "./internal/effect.ts" +import * as Layer from "./Layer.ts" +import type * as Option from "./Option.ts" +import type * as order from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" + +const TypeId = Internal.TypeId +const TimeZoneTypeId = Internal.TimeZoneTypeId + +/** + * A `DateTime` represents a point in time. It can optionally have a time zone + * associated with it. + * + * @category models + * @since 3.6.0 + */ +export type DateTime = Utc | Zoned + +/** + * Represents a `DateTime` stored as an absolute UTC instant with no associated + * time zone. + * + * **Details** + * + * Use `DateTime.isUtc` to narrow a `DateTime` to this variant. + * + * @category models + * @since 3.6.0 + */ +export interface Utc extends DateTime.Proto { + readonly _tag: "Utc" + readonly epochMilliseconds: number + partsUtc: DateTime.PartsWithWeekday | undefined +} + +/** + * Represents a `DateTime` with an associated `TimeZone`. + * + * **Details** + * + * A zoned value still represents an absolute instant through + * `epochMilliseconds`, while the time zone is used for wall-clock parts, + * formatting, and zone-aware transformations. + * + * @category models + * @since 3.6.0 + */ +export interface Zoned extends DateTime.Proto { + readonly _tag: "Zoned" + readonly epochMilliseconds: number + readonly zone: TimeZone + adjustedEpochMilliseconds: number | undefined + partsAdjusted: DateTime.PartsWithWeekday | undefined + partsUtc: DateTime.PartsWithWeekday | undefined +} + +/** + * Companion namespace containing the public helper types used by `DateTime` + * constructors, parts APIs, formatting, and date/time arithmetic. + * + * @since 3.6.0 + */ +export declare namespace DateTime { + /** + * Input accepted by `DateTime.make`, `DateTime.makeUnsafe`, and the zoned + * constructors. + * + * **Details** + * + * Includes existing `DateTime` values, partial date parts, epoch-millisecond + * objects, epoch milliseconds, JavaScript `Date` instances, and parseable date + * strings. + * + * @category models + * @since 3.6.0 + */ + export type Input = DateTime | Partial | Instant | InstantWithZone | Date | number | string + + /** + * Type-level helper used by constructors to preserve a zoned input. + * + * **Details** + * + * When the input type is `DateTime.Zoned`, the result type is + * `DateTime.Zoned`; otherwise the result type is `DateTime.Utc`. + * + * @category models + * @since 3.6.0 + */ + export type PreserveZone = A extends Zoned ? Zoned : Utc + + /** + * Date and time unit name accepted by `DateTime` rounding and arithmetic + * APIs. + * + * **Details** + * + * Includes both singular units, such as `"day"`, and plural units, such as + * `"days"`. + * + * @category models + * @since 3.6.0 + */ + export type Unit = UnitSingular | UnitPlural + + /** + * Singular date and time unit names used by rounding APIs such as + * `DateTime.startOf`, `DateTime.endOf`, and `DateTime.nearest`. + * + * @category models + * @since 3.6.0 + */ + export type UnitSingular = + | "millisecond" + | "second" + | "minute" + | "hour" + | "day" + | "week" + | "month" + | "year" + + /** + * Plural date and time unit names used by `DateTime.PartsForMath` for + * amount-based arithmetic. + * + * @category models + * @since 3.6.0 + */ + export type UnitPlural = + | "milliseconds" + | "seconds" + | "minutes" + | "hours" + | "days" + | "weeks" + | "months" + | "years" + + /** + * Calendar and time components of a `DateTime`, including the weekday. + * + * **Details** + * + * `month` is one-based (`1` for January through `12` for December), and + * `weekDay` follows JavaScript `Date#getUTCDay` numbering (`0` for Sunday + * through `6` for Saturday). + * + * @category models + * @since 3.6.0 + */ + export interface PartsWithWeekday { + readonly millisecond: number + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly weekDay: number + readonly month: number + readonly year: number + } + + /** + * Calendar and time components of a `DateTime`, without weekday information. + * + * **Details** + * + * `month` is one-based (`1` for January through `12` for December). + * + * @category models + * @since 3.6.0 + */ + export interface Parts { + readonly millisecond: number + readonly second: number + readonly minute: number + readonly hour: number + readonly day: number + readonly month: number + readonly year: number + } + + /** + * Plural amount fields accepted by `DateTime.add` and `DateTime.subtract`. + * + * **Details** + * + * Each field represents the number of units to add or subtract for that part. + * + * @category models + * @since 3.6.0 + */ + export interface PartsForMath { + readonly milliseconds: number + readonly seconds: number + readonly minutes: number + readonly hours: number + readonly days: number + readonly weeks: number + readonly months: number + readonly years: number + } + + /** + * Object input representing an absolute instant as milliseconds since the Unix + * epoch. + * + * @category models + * @since 4.0.0 + */ + export interface Instant { + readonly epochMilliseconds: number + } + + /** + * Object input representing an absolute instant plus a time zone identifier. + * + * **Details** + * + * `DateTime.makeZoned` and `DateTime.makeZonedUnsafe` use `timeZoneId` when + * no explicit `timeZone` option is supplied. + * + * @category models + * @since 4.0.0 + */ + export interface InstantWithZone { + readonly timeZoneId: string + readonly epochMilliseconds: number + } + + /** + * Shared protocol implemented by all `DateTime` values. + * + * **Details** + * + * Provides the `DateTime` type identifier along with pipe and inspection + * support. + * + * @category models + * @since 3.6.0 + */ + export interface Proto extends Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + } +} + +/** + * Represents a time zone used by `DateTime.Zoned`. + * + * **Details** + * + * A `TimeZone` is either a fixed offset from UTC or a named IANA time zone. + * + * @category models + * @since 3.6.0 + */ +export type TimeZone = TimeZone.Offset | TimeZone.Named + +/** + * Companion namespace containing the public variant and protocol types for + * `TimeZone`. + * + * @since 3.6.0 + */ +export declare namespace TimeZone { + /** + * Shared protocol implemented by all `TimeZone` values. + * + * **Details** + * + * Provides the `TimeZone` type identifier and inspection support. + * + * @category models + * @since 3.6.0 + */ + export interface Proto extends Inspectable { + readonly [TimeZoneTypeId]: typeof TimeZoneTypeId + } + + /** + * Fixed-offset time zone. + * + * **Details** + * + * The `offset` is measured in milliseconds from UTC. Positive offsets are + * ahead of UTC, and negative offsets are behind UTC. + * + * @category models + * @since 3.6.0 + */ + export interface Offset extends Proto { + readonly _tag: "Offset" + readonly offset: number + } + + /** + * Named IANA time zone. + * + * **Details** + * + * The `id` field contains the resolved time zone identifier, such as + * `"Europe/London"` or `"America/New_York"`. + * + * @category models + * @since 3.6.0 + */ + export interface Named extends Proto { + readonly _tag: "Named" + readonly id: string + /** @internal */ + readonly format: Intl.DateTimeFormat + } +} + +/** + * A `Disambiguation` is used to resolve ambiguities when a `DateTime` is + * ambiguous, such as during a daylight saving time transition. + * + * **Details** + * + * For more information, see the [Temporal documentation](https://tc39.es/proposal-temporal/docs/timezone.html#ambiguity-due-to-dst-or-other-time-zone-offset-changes) + * + * - `"compatible"`: (default) Behavior matching Temporal API and legacy JavaScript Date and moment.js. + * For repeated times, chooses the earlier occurrence. For gap times, chooses the later interpretation. + * + * - `"earlier"`: For repeated times, always choose the earlier occurrence. + * For gap times, choose the time before the gap. + * + * - `"later"`: For repeated times, always choose the later occurrence. + * For gap times, choose the time after the gap. + * + * - `"reject"`: Throw an `RangeError` when encountering ambiguous or non-existent times. + * + * **Example** (Resolving ambiguous local times) + * + * ```ts + * import { DateTime } from "effect" + * + * // Fall-back example: 01:30 on Nov 2, 2025 in New York happens twice + * const ambiguousTime = { year: 2025, month: 11, day: 2, hours: 1, minutes: 30 } + * const timeZone = DateTime.zoneMakeNamedUnsafe("America/New_York") + * + * DateTime.makeZoned(ambiguousTime, { + * timeZone, + * adjustForTimeZone: true, + * disambiguation: "earlier" + * }) + * // Earlier occurrence (DST time): 2025-11-02T05:30:00.000Z + * + * DateTime.makeZoned(ambiguousTime, { + * timeZone, + * adjustForTimeZone: true, + * disambiguation: "later" + * }) + * // Later occurrence (standard time): 2025-11-02T06:30:00.000Z + * + * // Gap example: 02:30 on Mar 9, 2025 in New York doesn't exist + * const gapTime = { year: 2025, month: 3, day: 9, hours: 2, minutes: 30 } + * + * DateTime.makeZoned(gapTime, { + * timeZone, + * adjustForTimeZone: true, + * disambiguation: "earlier" + * }) + * // Time before gap: 2025-03-09T06:30:00.000Z (01:30 EST) + * + * DateTime.makeZoned(gapTime, { + * timeZone, + * adjustForTimeZone: true, + * disambiguation: "later" + * }) + * // Time after gap: 2025-03-09T07:30:00.000Z (03:30 EDT) + * ``` + * + * @category models + * @since 3.18.0 + */ +export type Disambiguation = "compatible" | "earlier" | "later" | "reject" + +// ============================================================================= +// guards +// ============================================================================= + +/** + * Checks whether a value is a `DateTime`. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `DateTime`. + * + * @see {@link isUtc} for narrowing a known `DateTime` to UTC + * @see {@link isZoned} for narrowing a known `DateTime` to zoned + * + * @category guards + * @since 3.6.0 + */ +export const isDateTime: (u: unknown) => u is DateTime = Internal.isDateTime + +/** + * Checks whether a value is a `TimeZone`. + * + * **When to use** + * + * Use to narrow unknown input to any `TimeZone` before passing it to APIs that + * accept either fixed-offset or named time zones. + * + * @see {@link isTimeZoneOffset} for narrowing to fixed-offset time zones + * @see {@link isTimeZoneNamed} for narrowing to named time zones + * + * @category guards + * @since 3.6.0 + */ +export const isTimeZone: (u: unknown) => u is TimeZone = Internal.isTimeZone + +/** + * Checks whether a value is an offset-based `TimeZone`. + * + * **When to use** + * + * Use when you need to narrow an unknown or union `TimeZone` value to the + * fixed-offset variant before reading its offset in milliseconds. + * + * @see {@link isTimeZone} for checking either time zone variant + * @see {@link isTimeZoneNamed} for narrowing to named time zones + * + * @category guards + * @since 3.6.0 + */ +export const isTimeZoneOffset: (u: unknown) => u is TimeZone.Offset = Internal.isTimeZoneOffset + +/** + * Checks whether a value is a named `TimeZone` (IANA time zone). + * + * **When to use** + * + * Use to narrow an unknown value to the `TimeZone.Named` variant before + * reading named-zone fields such as `id`. + * + * @see {@link isTimeZone} for checking either time zone variant + * @see {@link isTimeZoneOffset} for narrowing to fixed-offset time zones + * + * @category guards + * @since 3.6.0 + */ +export const isTimeZoneNamed: (u: unknown) => u is TimeZone.Named = Internal.isTimeZoneNamed + +/** + * Checks whether a `DateTime` is a UTC `DateTime` (no time zone information). + * + * **When to use** + * + * Use to narrow a `DateTime` before passing it to code that requires a UTC + * value without an associated time zone. + * + * @see {@link isZoned} for narrowing to zoned date-times + * @see {@link match} for handling both UTC and zoned cases + * + * @category guards + * @since 3.6.0 + */ +export const isUtc: (self: DateTime) => self is Utc = Internal.isUtc + +/** + * Checks whether a `DateTime` is a zoned `DateTime` (has time zone information). + * + * **When to use** + * + * Use to narrow a known `DateTime` before reading its zone or passing it to + * APIs that require `DateTime.Zoned`. + * + * @see {@link isUtc} for narrowing to UTC date-times + * @see {@link match} for handling both UTC and zoned cases + * + * @category guards + * @since 3.6.0 + */ +export const isZoned: (self: DateTime) => self is Zoned = Internal.isZoned + +// ============================================================================= +// instances +// ============================================================================= + +/** + * Provides an `Equivalence` for comparing two `DateTime` values for equality. + * + * **Details** + * + * Two `DateTime` values are considered equivalent if they represent the same + * point in time, regardless of their time zone. + * + * **Example** (Comparing DateTime values for equivalence) + * + * ```ts + * import { DateTime } from "effect" + * + * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: "Europe/London" + * }) + * + * console.log(DateTime.Equivalence(utc, zoned)) // true + * ``` + * + * @category instances + * @since 3.6.0 + */ +export const Equivalence: Equ.Equivalence = Internal.Equivalence + +/** + * Provides an `Order` for comparing and sorting `DateTime` values. + * + * **Details** + * + * `DateTime` values are ordered by their epoch milliseconds, so earlier times + * come before later times regardless of time zone. + * + * **Example** (Sorting DateTime values chronologically) + * + * ```ts + * import { Array, DateTime } from "effect" + * + * const dates = [ + * DateTime.makeUnsafe("2024-03-01"), + * DateTime.makeUnsafe("2024-01-01"), + * DateTime.makeUnsafe("2024-02-01") + * ] + * + * const sorted = Array.sort(dates, DateTime.Order) + * // Results in chronological order: 2024-01-01, 2024-02-01, 2024-03-01 + * ``` + * + * @category instances + * @since 3.6.0 + */ +export const Order: order.Order = Internal.Order + +/** + * Returns a `DateTime` constrained between a minimum and maximum value. + * + * **Details** + * + * If the `DateTime` is before the minimum, the minimum is returned. + * If the `DateTime` is after the maximum, the maximum is returned. + * Otherwise, the original `DateTime` is returned. + * + * **Example** (Clamping DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * const min = DateTime.makeUnsafe("2024-01-01") + * const max = DateTime.makeUnsafe("2024-12-31") + * const date = DateTime.makeUnsafe("2025-06-15") + * + * const clamped = DateTime.clamp(date, { minimum: min, maximum: max }) + * // clamped equals max (2024-12-31) + * ``` + * + * @category ordering + * @since 3.6.0 + */ +export const clamp: { + ( + options: { readonly minimum: Min; readonly maximum: Max } + ): (self: A) => A | Min | Max + ( + self: A, + options: { readonly minimum: Min; readonly maximum: Max } + ): A | Min | Max +} = Internal.clamp + +// ============================================================================= +// constructors +// ============================================================================= + +/** + * Create a `DateTime` from a `Date`. + * + * **Details** + * + * If the `Date` is invalid, an `IllegalArgumentError` will be thrown. + * + * **Example** (Creating DateTime values from Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const date = new Date("2024-01-01T12:00:00Z") + * const dateTime = DateTime.fromDateUnsafe(date) + * + * console.log(DateTime.formatIso(dateTime)) // "2024-01-01T12:00:00.000Z" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromDateUnsafe: (date: Date) => Utc = Internal.fromDateUnsafe + +/** + * Create a `DateTime` from supported input values. + * + * **When to use** + * + * Use when creating a `DateTime` from trusted input and construction failures + * should throw an `IllegalArgumentError` instead of returning `Option.none`. + * + * **Details** + * + * - A `DateTime` + * - A `Date` instance (invalid dates will throw an `IllegalArgumentError`) + * - The `number` of milliseconds since the Unix epoch + * - An object with the parts of a date + * - A `string` that can be parsed by `Date.parse` + * + * **Example** (Creating DateTime values unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * // from Date + * const fromDate = DateTime.makeUnsafe(new Date("2024-01-01T12:00:00Z")) + * console.log(DateTime.formatIso(fromDate)) // "2024-01-01T12:00:00.000Z" + * + * // from parts + * const fromParts = DateTime.makeUnsafe({ year: 2024 }) + * console.log(DateTime.formatIso(fromParts)) // "2024-01-01T00:00:00.000Z" + * + * // from string + * const fromString = DateTime.makeUnsafe("2024-01-01") + * console.log(DateTime.formatIso(fromString)) // "2024-01-01T00:00:00.000Z" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe: (input: A) => DateTime.PreserveZone = Internal.makeUnsafe + +/** + * Create a `DateTime.Zoned` using `DateTime.makeUnsafe` and a time zone. + * + * **When to use** + * + * Use when the date/time input and zone options are trusted and invalid or + * rejected ambiguous times should throw instead of returning `Option.none`. + * + * **Details** + * + * The input is treated as UTC and then the time zone is attached, unless + * `adjustForTimeZone` is set to `true`. In that case, the input is treated as + * already in the time zone. + * + * When `adjustForTimeZone` is true and ambiguous times occur during DST transitions, + * the `disambiguation` option controls how to resolve the ambiguity: + * - `compatible` (default): Choose earlier time for repeated times, later for gaps + * - `earlier`: Always choose the earlier of two possible times + * - `later`: Always choose the later of two possible times + * - `reject`: Throw an error when ambiguous times are encountered + * + * **Example** (Creating zoned DateTime values unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * const zoned = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { + * timeZone: "Europe/London" + * }) + * + * console.log(DateTime.formatIsoZoned(zoned)) // "2024-06-15T15:30:00.000+01:00[Europe/London]" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeZonedUnsafe: (input: DateTime.Input, options?: { + readonly timeZone?: number | string | TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined +}) => Zoned = Internal.makeZonedUnsafe + +/** + * Creates a `DateTime.Zoned` safely from an input and a time zone. + * + * **Details** + * + * By default, the input is interpreted as a UTC instant and the time zone is + * attached without changing that instant. When `adjustForTimeZone` is `true`, + * the input is interpreted as wall-clock time in the target zone. + * + * When `adjustForTimeZone` is `true`, `disambiguation` controls + * daylight-saving gaps and repeated times: + * + * - `"compatible"` (default): chooses the earlier occurrence for repeated + * times and the later interpretation for gaps + * - `"earlier"`: chooses the earlier possible instant + * - `"later"`: chooses the later possible instant + * - `"reject"`: rejects ambiguous or nonexistent wall-clock times + * + * Returns `Some` when construction succeeds, or `None` when the input, time + * zone, or disambiguation cannot be resolved. + * + * **Example** (Creating optional zoned DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * const result = DateTime.makeZoned("2024-06-15T14:30:00Z", { + * timeZone: "Europe/London" + * }) + * + * console.log(result._tag) // "Some" + * if (result._tag === "Some") { + * console.log(DateTime.formatIsoZoned(result.value)) // "2024-06-15T15:30:00.000+01:00[Europe/London]" + * } + * ``` + * + * @category constructors + * @since 3.6.0 + */ +export const makeZoned: ( + input: DateTime.Input, + options?: { + readonly timeZone?: number | string | TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + } +) => Option.Option = Internal.makeZoned + +/** + * Creates a `DateTime` safely from supported input values. + * + * **Details** + * + * - A `DateTime` + * - A JavaScript `Date` + * - The number of milliseconds since the Unix epoch + * - An object with date and time parts + * - A string that can be parsed as a date + * + * Returns `Some` with the constructed `DateTime` when the input is valid, or + * `None` when construction would fail, including invalid `Date` instances or + * unparseable strings. + * + * **Example** (Creating optional DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * // from Date + * const fromDate = DateTime.make(new Date("2024-01-01T12:00:00Z")) + * console.log(fromDate._tag) // "Some" + * + * // from parts + * const fromParts = DateTime.make({ year: 2024 }) + * console.log(fromParts._tag) // "Some" + * + * // from string + * const fromString = DateTime.make("2024-01-01") + * console.log(fromString._tag) // "Some" + * + * const invalid = DateTime.make("not a date") + * console.log(invalid._tag) // "None" + * ``` + * + * @category constructors + * @since 3.6.0 + */ +export const make: (input: A) => Option.Option> = Internal.make + +/** + * Parses an ISO zoned date-time string into a `DateTime.Zoned` safely. + * + * **Details** + * + * Accepts named-zone strings such as + * `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]` and offset-only strings such as + * `YYYY-MM-DDTHH:mm:ss.sss+HH:MM`. Returns `None` when the input cannot be + * parsed. + * + * **Example** (Parsing zoned DateTime strings) + * + * ```ts + * import { DateTime } from "effect" + * + * const result1 = DateTime.makeZonedFromString( + * "2024-01-01T12:00:00+02:00[Europe/Berlin]" + * ) + * console.log(result1._tag === "Some") // true + * + * const result2 = DateTime.makeZonedFromString("2024-01-01T12:00:00Z") + * console.log(result2._tag === "Some") // true + * + * const invalid = DateTime.makeZonedFromString("invalid") + * console.log(invalid._tag === "None") // true + * ``` + * + * @category constructors + * @since 3.6.0 + */ +export const makeZonedFromString: (input: string) => Option.Option = Internal.makeZonedFromString + +/** + * Gets the current time using the `Clock` service and convert it to a `DateTime`. + * + * **Example** (Getting the current DateTime) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.nowAsDate + * console.log(now instanceof Date) // true + * }) + * ``` + * + * @category constructors + * @since 3.6.0 + */ +export const now: Effect.Effect = Internal.now + +/** + * Gets the current time from the `Clock` service and returns it as a + * JavaScript `Date`. + * + * **Example** (Getting the current Date) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * }) + * ``` + * + * @category constructors + * @since 3.14.0 + */ +export const nowAsDate: Effect.Effect = Internal.nowAsDate + +/** + * Gets the current time using `Date.now`. + * + * **When to use** + * + * Use when synchronous wall-clock access outside an Effect program is + * acceptable and testability through the `Clock` service is not needed. + * + * **Details** + * + * This is a synchronous version of `now` that directly uses `Date.now()` + * instead of the Effect `Clock` service. + * + * **Example** (Getting the current DateTime unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * const now = DateTime.nowUnsafe() + * console.log(DateTime.formatIso(now)) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const nowUnsafe: LazyArg = Internal.nowUnsafe + +// ============================================================================= +// time zones +// ============================================================================= + +/** + * Converts a `DateTime` to a UTC `DateTime`. + * + * **When to use** + * + * Use to represent the same instant in UTC instead of its current time zone. + * + * **Details** + * + * The returned value keeps the same epoch milliseconds and changes only the + * `DateTime` representation to UTC. + * + * **Example** (Converting DateTime values to UTC) + * + * ```ts + * import { DateTime } from "effect" + * + * const now = DateTime.makeZonedUnsafe({ year: 2024 }, { + * timeZone: "Europe/London" + * }) + * + * // set as UTC + * const utc: DateTime.Utc = DateTime.toUtc(now) + * ``` + * + * @category time zones + * @since 3.13.0 + */ +export const toUtc: (self: DateTime) => Utc = Internal.toUtc + +/** + * Sets the time zone of a `DateTime`, returning a new `DateTime.Zoned`. + * + * **Example** (Setting time zones) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * + * // set the time zone + * const zoned: DateTime.Zoned = DateTime.setZone(now, zone) + * }) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const setZone: { + (zone: TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zone: TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.setZone + +/** + * Adds a fixed offset time zone to a `DateTime`. + * + * **Details** + * + * The offset is in milliseconds. + * + * **Example** (Setting fixed-offset time zones) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * + * // set the offset time zone in milliseconds + * const zoned: DateTime.Zoned = DateTime.setZoneOffset(now, 3 * 60 * 60 * 1000) + * }) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const setZoneOffset: { + (offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.setZoneOffset + +/** + * Attempts to create a named time zone from an IANA time zone identifier. + * + * **When to use** + * + * Use when the IANA zone id is trusted and invalid zones should throw instead + * of returning `Option.none` or failing in `Effect`. + * + * **Details** + * + * If the time zone is invalid, an `IllegalArgumentError` will be thrown. + * + * **Example** (Creating named time zones unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * const londonZone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * console.log(DateTime.zoneToString(londonZone)) // "Europe/London" + * + * const tokyoZone = DateTime.zoneMakeNamedUnsafe("Asia/Tokyo") + * console.log(DateTime.zoneToString(tokyoZone)) // "Asia/Tokyo" + * + * // This would throw an IllegalArgumentError: + * // DateTime.zoneMakeNamedUnsafe("Invalid/Zone") + * ``` + * + * @category time zones + * @since 4.0.0 + */ +export const zoneMakeNamedUnsafe: (zoneId: string) => TimeZone.Named = Internal.zoneMakeNamedUnsafe + +/** + * Create a fixed offset time zone. + * + * **Details** + * + * The offset is specified in milliseconds from UTC. Positive values are + * ahead of UTC, negative values are behind UTC. + * + * **Example** (Creating fixed-offset time zones) + * + * ```ts + * import { DateTime } from "effect" + * + * // Create a time zone with +3 hours offset + * const zone = DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) + * + * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: zone + * }) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneMakeOffset: (offset: number) => TimeZone.Offset = Internal.zoneMakeOffset + +/** + * Creates a named time zone safely from an IANA time zone identifier. + * + * **Details** + * + * If the time zone is invalid, `None` will be returned. + * + * **Example** (Creating optional named time zones) + * + * ```ts + * import { DateTime } from "effect" + * + * const validZone = DateTime.zoneMakeNamed("Europe/London") + * console.log(validZone._tag === "Some") // true + * + * const invalidZone = DateTime.zoneMakeNamed("Invalid/Zone") + * console.log(invalidZone._tag === "None") // true + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneMakeNamed: (zoneId: string) => Option.Option = Internal.zoneMakeNamed + +/** + * Creates a named time zone effectfully from an IANA time zone identifier. + * + * **When to use** + * + * Use when invalid IANA zone ids should fail in the Effect error channel + * instead of returning `Option.none` or throwing. + * + * **Example** (Creating named time zones effectfully) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const zone = yield* DateTime.zoneMakeNamedEffect("Europe/London") + * const now = yield* DateTime.now + * return DateTime.setZone(now, zone) + * }) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneMakeNamedEffect: (zoneId: string) => Effect.Effect = + Internal.zoneMakeNamedEffect + +/** + * Create a named time zone from the system's local time zone. + * + * **Details** + * + * This uses the system's configured time zone, which may vary depending + * on the runtime environment. + * + * **Example** (Creating local time zones) + * + * ```ts + * import { DateTime } from "effect" + * + * const localZone = DateTime.zoneMakeLocal() + * console.log(DateTime.zoneToString(localZone)) // Output depends on system time zone + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneMakeLocal: () => TimeZone.Named = Internal.zoneMakeLocal + +/** + * Tries to parse a `TimeZone` from a string safely. + * + * **Details** + * + * Supports both IANA time zone identifiers and offset formats like "+03:00". + * + * **Example** (Parsing time zones) + * + * ```ts + * import { DateTime } from "effect" + * + * const namedZone = DateTime.zoneFromString("Europe/London") + * const offsetZone = DateTime.zoneFromString("+03:00") + * const invalid = DateTime.zoneFromString("invalid") + * + * console.log(namedZone._tag === "Some") // true + * console.log(offsetZone._tag === "Some") // true + * console.log(invalid._tag === "None") // true + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneFromString: (zone: string) => Option.Option = Internal.zoneFromString + +/** + * Formats a `TimeZone` as a string. + * + * **Example** (Formatting time zones) + * + * ```ts + * import { DateTime } from "effect" + * + * // Outputs "+03:00" + * DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) + * + * // Outputs "Europe/London" + * DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Europe/London")) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const zoneToString: (self: TimeZone) => string = Internal.zoneToString + +/** + * Sets the time zone of a `DateTime` safely from an IANA time zone identifier. If the + * time zone is invalid, `None` will be returned. + * + * **Example** (Setting named time zones safely) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * // set the time zone, returns an Option + * DateTime.setZoneNamed(now, "Europe/London") + * }) + * ``` + * + * @category time zones + * @since 3.6.0 + */ +export const setZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Option.Option + (self: DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Option.Option +} = Internal.setZoneNamed + +/** + * Sets the time zone of a `DateTime` from an IANA time zone identifier. If the + * time zone is invalid, an `IllegalArgumentError` will be thrown. + * + * **Example** (Setting named time zones unsafely) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * // set the time zone + * DateTime.setZoneNamedUnsafe(now, "Europe/London") + * }) + * ``` + * + * @category time zones + * @since 4.0.0 + */ +export const setZoneNamedUnsafe: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): (self: DateTime) => Zoned + (self: DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: Disambiguation | undefined + }): Zoned +} = Internal.setZoneNamedUnsafe + +// ============================================================================= +// comparisons +// ============================================================================= + +/** + * Computes the difference between two `DateTime` values, returning a + * `Duration` representing the amount of time between them. + * + * **Details** + * + * If `other` is *after* `self`, the result will be a positive `Duration`. If + * `other` is *before* `self`, the result will be a negative `Duration`. If they + * are equal, the result will be a `Duration` of zero. + * + * **Example** (Measuring distance between DateTime values) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * const other = DateTime.add(now, { minutes: 1 }) + * + * // returns Duration.minutes(1) + * DateTime.distance(now, other) + * }) + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const distance: { + (other: DateTime): (self: DateTime) => Duration.Duration + (self: DateTime, other: DateTime): Duration.Duration +} = Internal.distance + +/** + * Returns the earlier of two `DateTime` values. + * + * **Example** (Selecting the earlier DateTime) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-01-01") + * const date2 = DateTime.makeUnsafe("2024-02-01") + * + * const earlier = DateTime.min(date1, date2) + * // earlier equals date1 (2024-01-01) + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const min: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = Internal.min + +/** + * Returns the later of two `DateTime` values. + * + * **Example** (Selecting the later DateTime) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-01-01") + * const date2 = DateTime.makeUnsafe("2024-02-01") + * + * const later = DateTime.max(date1, date2) + * // later equals date2 (2024-02-01) + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const max: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = Internal.max + +/** + * Checks whether the first `DateTime` is after the second `DateTime`. + * + * **Example** (Checking whether a DateTime is later) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-02-01") + * const date2 = DateTime.makeUnsafe("2024-01-01") + * + * console.log(DateTime.isGreaterThan(date1, date2)) // true + * console.log(DateTime.isGreaterThan(date2, date1)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.isGreaterThan + +/** + * Checks whether the first `DateTime` is after or equal to the second `DateTime`. + * + * **Example** (Checking whether a DateTime is later or equal) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-01-01") + * const date2 = DateTime.makeUnsafe("2024-01-01") + * const date3 = DateTime.makeUnsafe("2024-02-01") + * + * console.log(DateTime.isGreaterThanOrEqualTo(date1, date2)) // true + * console.log(DateTime.isGreaterThanOrEqualTo(date3, date1)) // true + * console.log(DateTime.isGreaterThanOrEqualTo(date1, date3)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.isGreaterThanOrEqualTo + +/** + * Checks whether the first `DateTime` is before the second `DateTime`. + * + * **Example** (Checking whether a DateTime is earlier) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-01-01") + * const date2 = DateTime.makeUnsafe("2024-02-01") + * + * console.log(DateTime.isLessThan(date1, date2)) // true + * console.log(DateTime.isLessThan(date2, date1)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isLessThan: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.isLessThan + +/** + * Checks whether the first `DateTime` is before or equal to the second `DateTime`. + * + * **Example** (Checking whether a DateTime is earlier or equal) + * + * ```ts + * import { DateTime } from "effect" + * + * const date1 = DateTime.makeUnsafe("2024-01-01") + * const date2 = DateTime.makeUnsafe("2024-01-01") + * const date3 = DateTime.makeUnsafe("2024-02-01") + * + * console.log(DateTime.isLessThanOrEqualTo(date1, date2)) // true + * console.log(DateTime.isLessThanOrEqualTo(date1, date3)) // true + * console.log(DateTime.isLessThanOrEqualTo(date3, date1)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: DateTime): (self: DateTime) => boolean + (self: DateTime, that: DateTime): boolean +} = Internal.isLessThanOrEqualTo + +/** + * Checks whether a `DateTime` is between two other `DateTime` values (inclusive). + * + * **Example** (Checking whether a DateTime is within bounds) + * + * ```ts + * import { DateTime } from "effect" + * + * const min = DateTime.makeUnsafe("2024-01-01") + * const max = DateTime.makeUnsafe("2024-12-31") + * const date = DateTime.makeUnsafe("2024-06-15") + * + * console.log(DateTime.between(date, { minimum: min, maximum: max })) // true + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const between: { + (options: { minimum: DateTime; maximum: DateTime }): (self: DateTime) => boolean + (self: DateTime, options: { minimum: DateTime; maximum: DateTime }): boolean +} = Internal.between + +/** + * Checks effectfully if a `DateTime` is in the future compared to the current time. + * + * **Details** + * + * This is an effectful operation that uses the current time from the `Clock` service. + * + * **Example** (Checking future DateTime values effectfully) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const futureDate = DateTime.add(yield* DateTime.now, { hours: 1 }) + * const isFuture = yield* DateTime.isFuture(futureDate) + * console.log(isFuture) // true + * }) + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const isFuture: (self: DateTime) => Effect.Effect = Internal.isFuture + +/** + * Checks synchronously if a `DateTime` is in the future compared to the current time. + * + * **When to use** + * + * Use when checking whether a `DateTime` is in the future with a synchronous + * live-clock read and `Clock`-based testability is not needed. + * + * **Details** + * + * This is a synchronous version that uses `Date.now()` directly. + * + * **Example** (Checking future DateTime values unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * const now = DateTime.nowUnsafe() + * const futureDate = DateTime.add(now, { hours: 1 }) + * + * console.log(DateTime.isFutureUnsafe(futureDate)) // true + * console.log(DateTime.isFutureUnsafe(now)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isFutureUnsafe: (self: DateTime) => boolean = Internal.isFutureUnsafe + +/** + * Checks effectfully if a `DateTime` is in the past compared to the current time. + * + * **Details** + * + * This is an effectful operation that uses the current time from the `Clock` service. + * + * **Example** (Checking past DateTime values effectfully) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const pastDate = DateTime.subtract(yield* DateTime.now, { hours: 1 }) + * const isPast = yield* DateTime.isPast(pastDate) + * console.log(isPast) // true + * }) + * ``` + * + * @category comparisons + * @since 3.6.0 + */ +export const isPast: (self: DateTime) => Effect.Effect = Internal.isPast + +/** + * Checks synchronously if a `DateTime` is in the past compared to the current time. + * + * **When to use** + * + * Use when checking whether a `DateTime` is in the past with a synchronous + * live-clock read and `Clock`-based testability is not needed. + * + * **Details** + * + * This is a synchronous version that uses `Date.now()` directly. + * + * **Example** (Checking past DateTime values unsafely) + * + * ```ts + * import { DateTime } from "effect" + * + * const now = DateTime.nowUnsafe() + * const pastDate = DateTime.subtract(now, { hours: 1 }) + * + * console.log(DateTime.isPastUnsafe(pastDate)) // true + * console.log(DateTime.isPastUnsafe(now)) // false + * ``` + * + * @category comparisons + * @since 4.0.0 + */ +export const isPastUnsafe: (self: DateTime) => boolean = Internal.isPastUnsafe + +// ============================================================================= +// conversions +// ============================================================================= + +/** + * Gets the UTC `Date` of a `DateTime`. + * + * **Details** + * + * This always returns the UTC representation, ignoring any time zone information. + * + * **Example** (Converting DateTime values to UTC Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: "Europe/London" + * }) + * + * const utcDate = DateTime.toDateUtc(dt) + * console.log(utcDate.toISOString()) // "2024-01-01T12:00:00.000Z" + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const toDateUtc: (self: DateTime) => Date = Internal.toDateUtc + +/** + * Converts a `DateTime` to a `Date`, applying the time zone first. + * + * **Details** + * + * For `DateTime.Zoned`, this adjusts for the time zone before converting. + * For `DateTime.Utc`, this is equivalent to `toDateUtc`. + * + * **Example** (Converting DateTime values to Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: "Europe/London" + * }) + * + * console.log(DateTime.toDate(utc).toISOString()) + * console.log(DateTime.toDate(zoned).toISOString()) + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const toDate: (self: DateTime) => Date = Internal.toDate + +/** + * Computes the time zone offset of a `DateTime.Zoned` in milliseconds. + * + * **Details** + * + * Returns the offset from UTC in milliseconds. Positive values indicate + * time zones ahead of UTC, negative values indicate time zones behind UTC. + * + * **Example** (Reading zoned offsets) + * + * ```ts + * import { DateTime } from "effect" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: "Europe/London" + * }) + * + * const offset = DateTime.zonedOffset(zoned) + * console.log(offset) // 0 (London is UTC+0 in winter) + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const zonedOffset: (self: Zoned) => number = Internal.zonedOffset + +/** + * Formats the time zone offset of a `DateTime.Zoned` as an ISO string. + * + * **Details** + * + * The offset is formatted as "±HH:MM". + * + * **Example** (Formatting zoned offsets) + * + * ```ts + * import { DateTime } from "effect" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) // +3 hours + * }) + * + * const offsetString = DateTime.zonedOffsetIso(zoned) + * console.log(offsetString) // "+03:00" + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const zonedOffsetIso: (self: Zoned) => string = Internal.zonedOffsetIso + +/** + * Gets the milliseconds since the Unix epoch of a `DateTime`. + * + * **Details** + * + * This returns the UTC timestamp regardless of any time zone information. + * + * **Example** (Reading epoch milliseconds) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + * const epochMillis = DateTime.toEpochMillis(dt) + * + * console.log(epochMillis) // 1704067200000 + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const toEpochMillis: (self: DateTime) => number = Internal.toEpochMillis + +/** + * Removes the time aspect of a `DateTime`, first adjusting for the time + * zone. It will return a `DateTime.Utc` only containing the date. + * + * **Example** (Removing time components) + * + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.makeZonedUnsafe("2024-01-01T05:00:00Z", { + * timeZone: "Pacific/Auckland", + * adjustForTimeZone: true + * }).pipe( + * DateTime.removeTime, + * DateTime.formatIso + * ) + * ``` + * + * @category converting + * @since 3.6.0 + */ +export const removeTime: (self: DateTime) => Utc = Internal.removeTime + +// ============================================================================= +// parts +// ============================================================================= + +/** + * Gets the time-zone-adjusted parts of a `DateTime` as an object. + * + * **Details** + * + * The parts will be time zone adjusted if the `DateTime` is zoned. + * + * **Example** (Reading DateTime parts) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T12:30:45.123Z") + * const parts = DateTime.toParts(dt) + * + * console.log(parts) + * // { + * // year: 2024, + * // month: 1, + * // day: 1, + * // hours: 12, + * // minutes: 30, + * // seconds: 45, + * // millis: 123, + * // weekDay: 1 // Monday + * // } + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const toParts: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toParts + +/** + * Gets the UTC parts of a `DateTime` as an object. + * + * **Details** + * + * The parts will always be in UTC, ignoring any time zone information. + * + * **Example** (Reading UTC DateTime parts) + * + * ```ts + * import { DateTime } from "effect" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:30:45.123Z", { + * timeZone: "Europe/London" + * }) + * const parts = DateTime.toPartsUtc(zoned) + * + * console.log(parts) + * // Always returns UTC parts regardless of time zone + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const toPartsUtc: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toPartsUtc + +/** + * Gets one UTC part of a `DateTime` as a number. + * + * **Details** + * + * The part will be in the UTC time zone. + * + * **Example** (Reading UTC DateTime parts by key) + * + * ```ts + * import { DateTime } from "effect" + * + * const dateTime = DateTime.makeUnsafe({ year: 2024 }) + * const year = DateTime.getPartUtc(dateTime, "year") + * console.log(year) // 2024 + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const getPartUtc: { + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = Internal.getPartUtc + +/** + * Gets one time-zone-adjusted part of a `DateTime` as a number. + * + * **Details** + * + * The part will be time zone adjusted. + * + * **Example** (Reading DateTime parts by key) + * + * ```ts + * import { DateTime } from "effect" + * + * const dateTime = DateTime.makeZonedUnsafe({ year: 2024 }, { + * timeZone: "Europe/London" + * }) + * const year = DateTime.getPart(dateTime, "year") + * console.log(year) // 2024 + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const getPart: { + (part: keyof DateTime.PartsWithWeekday): (self: DateTime) => number + (self: DateTime, part: keyof DateTime.PartsWithWeekday): number +} = Internal.getPart + +/** + * Sets time-zone-adjusted parts on a `DateTime`. + * + * **Details** + * + * The date will be time zone adjusted for `DateTime.Zoned`. + * + * **Example** (Updating DateTime parts) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * const updated = DateTime.setParts(dt, { + * year: 2025, + * month: 6, + * day: 15 + * }) + * + * console.log(DateTime.formatIso(updated)) // "2025-06-15T12:00:00.000Z" + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const setParts: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.setParts + +/** + * Sets UTC parts on a `DateTime`. + * + * **Details** + * + * The parts are always interpreted as UTC, ignoring any time zone information. + * + * **Example** (Updating UTC DateTime parts) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * const updated = DateTime.setPartsUtc(dt, { + * year: 2025, + * hour: 18 + * }) + * + * console.log(DateTime.formatIso(updated)) // "2025-01-01T18:00:00.000Z" + * ``` + * + * @category parts + * @since 3.6.0 + */ +export const setPartsUtc: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.setPartsUtc + +// ============================================================================= +// current time zone +// ============================================================================= + +/** + * Context service that supplies the ambient `TimeZone` for APIs that work in + * the current zone, such as `DateTime.setZoneCurrent` and + * `DateTime.nowInCurrentZone`. + * + * **Details** + * + * Provide it with `DateTime.withCurrentZone`, one of the `withCurrentZone*` + * helpers, or one of the `layerCurrentZone*` layers. + * + * **Example** (Accessing the current time zone service) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * // Access the current time zone service + * const zone = yield* DateTime.CurrentTimeZone + * console.log(DateTime.zoneToString(zone)) + * }) + * + * // Provide a time zone + * const layer = DateTime.layerCurrentZoneNamed("Europe/London") + * Effect.provide(program, layer) + * ``` + * + * @category current time zone + * @since 3.11.0 + */ +export class CurrentTimeZone extends Context.Service()( + "effect/DateTime/CurrentTimeZone" +) {} + +/** + * Sets the time zone of a `DateTime` to the current time zone, which is + * determined by the `CurrentTimeZone` service. + * + * **Example** (Setting the current time zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const now = yield* DateTime.now + * + * // set the time zone to "Europe/London" + * const zoned = yield* DateTime.setZoneCurrent(now) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const setZoneCurrent = (self: DateTime): Effect.Effect => + Effect.map(CurrentTimeZone, (zone) => setZone(self, zone)) + +/** + * Provides the `CurrentTimeZone` to an effect. + * + * **Example** (Providing the current time zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * + * Effect.gen(function*() { + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZone(zone)) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const withCurrentZone: { + (value: TimeZone): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, value: TimeZone): Effect.Effect> +} = provideService(CurrentTimeZone) + +/** + * Provides the `CurrentTimeZone` to an effect, using the system's local time + * zone. + * + * **Example** (Providing the local time zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * // will use the system's local time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneLocal) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const withCurrentZoneLocal = ( + effect: Effect.Effect +): Effect.Effect> => + Effect.provideServiceEffect(effect, CurrentTimeZone, Effect.sync(zoneMakeLocal)) + +/** + * Provides the `CurrentTimeZone` to an effect, using an offset. + * + * **Example** (Providing a fixed-offset time zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * const zone = yield* DateTime.CurrentTimeZone + * console.log(DateTime.zoneToString(zone)) // "+03:00" + * }).pipe(DateTime.withCurrentZoneOffset(3 * 60 * 60 * 1000)) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const withCurrentZoneOffset: { + (offset: number): ( + effect: Effect.Effect + ) => Effect.Effect> + (effect: Effect.Effect, offset: number): Effect.Effect> +} = dual( + 2, + (effect: Effect.Effect, offset: number): Effect.Effect> => + Effect.provideService(effect, CurrentTimeZone, zoneMakeOffset(offset)) +) + +/** + * Provides the `CurrentTimeZone` to an effect using an IANA time zone + * identifier. + * + * **Details** + * + * If the time zone is invalid, it will fail with an `IllegalArgumentError`. + * + * **Example** (Providing a named time zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const withCurrentZoneNamed: { + (zone: string): ( + effect: Effect.Effect + ) => Effect.Effect> + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> +} = dual( + 2, + ( + effect: Effect.Effect, + zone: string + ): Effect.Effect> => + Effect.provideServiceEffect(effect, CurrentTimeZone, zoneMakeNamedEffect(zone)) +) + +/** + * Gets the current time as a `DateTime.Zoned`, using the `CurrentTimeZone`. + * + * **Example** (Getting the current time in the current zone) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * Effect.gen(function*() { + * // will use the "Europe/London" time zone + * const now = yield* DateTime.nowInCurrentZone + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const nowInCurrentZone: Effect.Effect = Effect.flatMap(now, setZoneCurrent) + +// ============================================================================= +// mapping +// ============================================================================= + +/** + * Modifies a `DateTime` with a mutable local `Date` copy. + * + * **When to use** + * + * Use to adjust calendar fields in the `DateTime`'s own time zone with an + * existing `Date` mutation API. + * + * **Details** + * + * The `Date` will first have the time zone applied if possible, and then be + * converted back to a `DateTime` within the same time zone. + * + * Supports `disambiguation` when the new wall clock time is ambiguous. + * + * **Example** (Mutating DateTime values with Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * + * const modified = DateTime.mutate(dt, (date) => { + * date.setHours(15) // Set to 3 PM + * date.setMinutes(30) // Set to 30 minutes + * }) + * + * console.log(DateTime.formatIso(modified)) // "2024-01-01T15:30:00.000Z" + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const mutate: { + ( + f: (date: Date) => void, + options?: { + readonly disambiguation?: Disambiguation | undefined + } + ): (self: A) => A + ( + self: A, + f: (date: Date) => void, + options?: { + readonly disambiguation?: Disambiguation | undefined + } + ): A +} = Internal.mutate + +/** + * Modifies a `DateTime` with a mutable UTC `Date` copy. + * + * **When to use** + * + * Use to adjust the instant with an existing `Date` mutation API that works on + * UTC calendar fields. + * + * **Example** (Mutating DateTime values with UTC Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: "Europe/London" + * }) + * + * const modified = DateTime.mutateUtc(dt, (date) => { + * date.setUTCHours(18) // Set UTC time to 6 PM + * }) + * + * console.log(DateTime.formatIso(modified)) // "2024-01-01T18:00:00.000Z" + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const mutateUtc: { + (f: (date: Date) => void): (self: A) => A + (self: A, f: (date: Date) => void): A +} = Internal.mutateUtc + +/** + * Transforms a `DateTime` by applying a function to the number of milliseconds + * since the Unix epoch. + * + * **Example** (Mapping epoch milliseconds) + * + * ```ts + * import { DateTime } from "effect" + * + * // add 10 milliseconds + * DateTime.makeUnsafe(0).pipe( + * DateTime.mapEpochMillis((millis) => millis + 10) + * ) + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const mapEpochMillis: { + (f: (millis: number) => number): (self: A) => A + (self: A, f: (millis: number) => number): A +} = Internal.mapEpochMillis + +/** + * Applies a function to a JavaScript `Date` representing the `DateTime` and + * returns the function's result. + * + * **Details** + * + * The callback receives the time-zone-adjusted wall-clock date for + * `DateTime.Zoned` values. Use `DateTime.withDateUtc` when the callback should + * receive the UTC instant. + * + * **Example** (Applying time zone adjusted Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * // get the time zone adjusted date in milliseconds + * DateTime.makeZonedUnsafe(0, { timeZone: "Europe/London" }).pipe( + * DateTime.withDate((date) => date.getTime()) + * ) + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const withDate: { + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = Internal.withDate + +/** + * Applies a function to a JavaScript `Date` representing the `DateTime`'s UTC + * instant and returns the function's result. + * + * **Details** + * + * This ignores any associated time zone. Use `DateTime.withDate` when the + * callback should receive the time-zone-adjusted wall-clock date. + * + * **Example** (Applying UTC Dates) + * + * ```ts + * import { DateTime } from "effect" + * + * // get the date in milliseconds + * DateTime.makeUnsafe(0).pipe( + * DateTime.withDateUtc((date) => date.getTime()) + * ) + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const withDateUtc: { + (f: (date: Date) => A): (self: DateTime) => A + (self: DateTime, f: (date: Date) => A): A +} = Internal.withDateUtc + +/** + * Pattern match on a `DateTime` to handle `Utc` and `Zoned` cases differently. + * + * **Example** (Pattern matching DateTime variants) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt1 = DateTime.makeUnsafe("2024-01-01T12:00:00Z") // Utc + * const dt2 = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { + * timeZone: "Europe/London" + * }) // Zoned + * + * const result1 = DateTime.match(dt1, { + * onUtc: (utc) => `UTC: ${DateTime.formatIso(utc)}`, + * onZoned: (zoned) => `Zoned: ${DateTime.formatIsoZoned(zoned)}` + * }) + * + * const result2 = DateTime.match(dt2, { + * onUtc: (utc) => `UTC: ${DateTime.formatIso(utc)}`, + * onZoned: (zoned) => `Zoned: ${DateTime.formatIsoZoned(zoned)}` + * }) + * + * console.log(result1) // "UTC: 2024-01-01T12:00:00.000Z" + * console.log(result2) // "Zoned: 2024-06-15T15:30:00.000+01:00[Europe/London]" + * ``` + * + * @category mapping + * @since 3.6.0 + */ +export const match: { + (options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B + }): (self: DateTime) => A | B + (self: DateTime, options: { + readonly onUtc: (_: Utc) => A + readonly onZoned: (_: Zoned) => B + }): A | B +} = Internal.match + +// ============================================================================= +// math +// ============================================================================= + +/** + * Adds the given `Duration` to a `DateTime`. + * + * **When to use** + * + * Use to move a `DateTime` by an elapsed duration such as minutes, seconds, or + * milliseconds. + * + * **Details** + * + * The duration is converted to milliseconds and added to the epoch + * milliseconds. Zoned values keep their original time zone. + * + * **Gotchas** + * + * This is elapsed-time arithmetic, not calendar-aware local date arithmetic. + * Use `add` when adding days, weeks, months, or years should account for the + * date/time zone rules. + * + * **Example** (Adding durations) + * + * ```ts + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.makeUnsafe(0).pipe( + * DateTime.addDuration("5 minutes") + * ) + * ``` + * + * @see {@link add} for calendar-aware date/time part arithmetic + * @see {@link subtractDuration} for subtracting an elapsed duration + * + * @category math + * @since 3.6.0 + */ +export const addDuration: { + (duration: Duration.Input): (self: A) => A + (self: A, duration: Duration.Input): A +} = Internal.addDuration + +/** + * Subtracts the given `Duration` from a `DateTime`. + * + * **Example** (Subtracting durations) + * + * ```ts + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.makeUnsafe(0).pipe( + * DateTime.subtractDuration("5 minutes") + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const subtractDuration: { + (duration: Duration.Input): (self: A) => A + (self: A, duration: Duration.Input): A +} = Internal.subtractDuration + +/** + * Adds the given `amount` of `unit` to a `DateTime`. + * + * **Details** + * + * The time zone is taken into account when adding days, weeks, months, and + * years. + * + * **Example** (Adding date and time parts) + * + * ```ts + * import { DateTime } from "effect" + * + * // add 5 minutes + * DateTime.makeUnsafe(0).pipe( + * DateTime.add({ minutes: 5 }) + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const add: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.add + +/** + * Subtracts the given `amount` of `unit` from a `DateTime`. + * + * **Example** (Subtracting date and time parts) + * + * ```ts + * import { DateTime } from "effect" + * + * // subtract 5 minutes + * DateTime.makeUnsafe(0).pipe( + * DateTime.subtract({ minutes: 5 }) + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const subtract: { + (parts: Partial): (self: A) => A + (self: A, parts: Partial): A +} = Internal.subtract + +/** + * Converts a `DateTime` to the start of the given `part`. + * + * **Details** + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * **Example** (Rounding down DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T00:00:00Z" + * DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe( + * DateTime.startOf("day"), + * DateTime.formatIso + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const startOf: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.startOf + +/** + * Converts a `DateTime` to the end of the given `part`. + * + * **Details** + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * **Example** (Rounding up DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-01T23:59:59.999Z" + * DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe( + * DateTime.endOf("day"), + * DateTime.formatIso + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const endOf: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.endOf + +/** + * Converts a `DateTime` to the nearest given `part`. + * + * **Details** + * + * If the part is `week`, the `weekStartsOn` option can be used to specify the + * day of the week that the week starts on. The default is 0 (Sunday). + * + * **Example** (Rounding DateTime values to nearest units) + * + * ```ts + * import { DateTime } from "effect" + * + * // returns "2024-01-02T00:00:00Z" + * DateTime.makeUnsafe("2024-01-01T12:01:00Z").pipe( + * DateTime.nearest("day"), + * DateTime.formatIso + * ) + * ``` + * + * @category math + * @since 3.6.0 + */ +export const nearest: { + ( + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): (self: A) => A + ( + self: A, + part: DateTime.UnitSingular, + options?: { readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined } + ): A +} = Internal.nearest + +// ============================================================================= +// formatting +// ============================================================================= + +/** + * Formats a `DateTime` with `Intl.DateTimeFormat`. + * + * **Details** + * + * Unless a `timeZone` option is supplied, UTC values are formatted in UTC and + * zoned values are formatted in their named zone or fixed-offset zone. + * + * Fixed-offset zones depend on runtime support for offset `timeZone` + * identifiers. When unsupported, formatting falls back to UTC with the + * `DateTime` adjusted to the offset. + * + * **Example** (Formatting DateTime values with Intl options) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { + * timeZone: "Europe/London" + * }) + * + * const formatted = DateTime.format(dt, { + * dateStyle: "full", + * timeStyle: "short", + * locale: "en-US" + * }) + * + * console.log(formatted) // "Saturday, June 15, 2024 at 3:30 PM" + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const format: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = Internal.format + +/** + * Formats a `DateTime` with `Intl.DateTimeFormat` using the system local time + * zone and locale. + * + * **Example** (Formatting DateTime values locally) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-06-15T14:30:00Z") + * + * // Uses system local time zone and locale + * const local = DateTime.formatLocal(dt, { + * year: "numeric", + * month: "long", + * day: "numeric", + * hour: "2-digit", + * minute: "2-digit" + * }) + * + * console.log(local) // Output depends on system locale/timezone + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatLocal: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = Internal.formatLocal + +/** + * Formats a `DateTime` with `Intl.DateTimeFormat` using the UTC time zone. + * + * **Details** + * + * This forces the time zone to be UTC. + * + * **Example** (Formatting DateTime values in UTC) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { + * timeZone: "Europe/London" + * }) + * + * // Force UTC formatting regardless of time zone + * const utcFormatted = DateTime.formatUtc(dt, { + * year: "numeric", + * month: "2-digit", + * day: "2-digit", + * hour: "2-digit", + * minute: "2-digit", + * timeZoneName: "short" + * }) + * + * console.log(utcFormatted) // "06/15/2024, 02:30 PM UTC" + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatUtc: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): (self: DateTime) => string + ( + self: DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: string | undefined + } + | undefined + ): string +} = Internal.formatUtc + +/** + * Formats a `DateTime` as a string using the `Intl.DateTimeFormat` API. + * + * **When to use** + * + * Use when you already have an `Intl.DateTimeFormat` and want it to control the + * locale, time zone, and formatting options. + * + * **Details** + * + * The formatter receives the `DateTime` epoch milliseconds. Any time zone + * conversion comes from the supplied formatter. + * + * **Example** (Formatting DateTime values with custom formatters) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-06-15T14:30:00Z") + * + * // Create a custom formatter + * const formatter = new Intl.DateTimeFormat("de-DE", { + * year: "numeric", + * month: "long", + * day: "numeric", + * hour: "2-digit", + * minute: "2-digit", + * timeZone: "Europe/Berlin" + * }) + * + * const formatted = DateTime.formatIntl(dt, formatter) + * console.log(formatted.length > 0) // true + * ``` + * + * @see {@link formatUtc} for formatting with options forced to UTC + * @see {@link formatIso} for stable ISO formatting + * + * @category formatting + * @since 3.6.0 + */ +export const formatIntl: { + (format: Intl.DateTimeFormat): (self: DateTime) => string + (self: DateTime, format: Intl.DateTimeFormat): string +} = Internal.formatIntl + +/** + * Formats a `DateTime` as a UTC ISO string. + * + * **Details** + * + * Always returns the UTC representation in ISO 8601 format, ignoring any time zone. + * + * **Example** (Formatting DateTime values as ISO strings) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T12:30:45.123Z") + * console.log(DateTime.formatIso(dt)) // "2024-01-01T12:30:45.123Z" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:30:45.123Z", { + * timeZone: "Europe/London" + * }) + * console.log(DateTime.formatIso(zoned)) // "2024-01-01T12:30:45.123Z" + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatIso: (self: DateTime) => string = Internal.formatIso + +/** + * Formats a `DateTime` as a time zone adjusted ISO date string. + * + * **Details** + * + * Returns only the date part (YYYY-MM-DD) after applying time zone adjustments. + * + * **Example** (Formatting DateTime values as ISO dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T23:30:00Z") + * console.log(DateTime.formatIsoDate(dt)) // "2024-01-01" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T23:30:00Z", { + * timeZone: "Pacific/Auckland" // UTC+12/13 + * }) + * console.log(DateTime.formatIsoDate(zoned)) // "2024-01-02" (next day in Auckland) + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatIsoDate: (self: DateTime) => string = Internal.formatIsoDate + +/** + * Formats a `DateTime` as a UTC ISO date string. + * + * **Details** + * + * Returns only the date part (YYYY-MM-DD) in UTC, ignoring any time zone. + * + * **Example** (Formatting DateTime values as UTC ISO dates) + * + * ```ts + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T23:30:00Z") + * console.log(DateTime.formatIsoDateUtc(dt)) // "2024-01-01" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T23:30:00Z", { + * timeZone: "Pacific/Auckland" + * }) + * console.log(DateTime.formatIsoDateUtc(zoned)) // "2024-01-01" (always UTC) + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatIsoDateUtc: (self: DateTime) => string = Internal.formatIsoDateUtc + +/** + * Formats a `DateTime.Zoned` as an ISO string with an offset. + * + * **Details** + * + * For `DateTime.Utc`, returns the same as `formatIso`. For `DateTime.Zoned`, + * includes the time zone offset in the format. + * + * **Example** (Formatting DateTime values with offsets) + * + * ```ts + * import { DateTime } from "effect" + * + * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * console.log(DateTime.formatIsoOffset(utc)) // "2024-01-01T12:00:00.000Z" + * + * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { + * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) + * }) + * console.log(DateTime.formatIsoOffset(zoned)) // "2024-01-01T15:00:00.000+03:00" + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatIsoOffset: (self: DateTime) => string = Internal.formatIsoOffset + +/** + * Formats a `DateTime.Zoned` as a string. + * + * **Details** + * + * It uses the format: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]`. + * + * **Example** (Formatting zoned DateTime values) + * + * ```ts + * import { DateTime } from "effect" + * + * const zoned = DateTime.makeZonedUnsafe("2024-06-15T14:30:45.123Z", { + * timeZone: "Europe/London" + * }) + * + * const formatted = DateTime.formatIsoZoned(zoned) + * console.log(formatted) // "2024-06-15T15:30:45.123+01:00[Europe/London]" + * + * const offsetZone = DateTime.makeZonedUnsafe("2024-06-15T14:30:45.123Z", { + * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) + * }) + * + * const offsetFormatted = DateTime.formatIsoZoned(offsetZone) + * console.log(offsetFormatted) // "2024-06-15T17:30:45.123+03:00" + * ``` + * + * @category formatting + * @since 3.6.0 + */ +export const formatIsoZoned: (self: Zoned) => string = Internal.formatIsoZoned + +/** + * Create a Layer from the given time zone. + * + * **Details** + * + * This layer provides the `CurrentTimeZone` service with the specified time zone. + * + * **Example** (Providing current time zone layers) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * const layer = DateTime.layerCurrentZone(zone) + * + * const program = Effect.gen(function*() { + * const now = yield* DateTime.nowInCurrentZone + * return DateTime.formatIsoZoned(now) + * }) + * + * // Use the layer to provide the time zone + * Effect.provide(program, layer) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const layerCurrentZone: (resource: NoInfer) => Layer.Layer = Layer.succeed( + CurrentTimeZone +) + +/** + * Create a Layer from the given time zone offset. + * + * **Details** + * + * This layer provides the `CurrentTimeZone` service with a fixed offset time zone. + * + * **Example** (Providing fixed-offset time zone layers) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * // Create a layer for UTC+3 + * const layer = DateTime.layerCurrentZoneOffset(3 * 60 * 60 * 1000) + * + * const program = Effect.gen(function*() { + * const now = yield* DateTime.nowInCurrentZone + * return DateTime.formatIsoZoned(now) + * }) + * + * Effect.provide(program, layer) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const layerCurrentZoneOffset = (offset: number): Layer.Layer => + Layer.succeed(CurrentTimeZone)(Internal.zoneMakeOffset(offset)) + +/** + * Create a Layer from the given IANA time zone identifier. + * + * **Details** + * + * This layer provides the `CurrentTimeZone` service with a named time zone. + * If the time zone identifier is invalid, the layer will fail. + * + * **Example** (Providing named time zone layers) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const layer = DateTime.layerCurrentZoneNamed("Europe/London") + * + * const program = Effect.gen(function*() { + * const now = yield* DateTime.nowInCurrentZone + * return DateTime.formatIsoZoned(now) + * }) + * + * Effect.provide(program, layer) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const layerCurrentZoneNamed: (zoneId: string) => Layer.Layer< + CurrentTimeZone, + IllegalArgumentError +> = flow(Internal.zoneMakeNamedEffect, Layer.effect(CurrentTimeZone)) + +/** + * Create a Layer from the system's local time zone. + * + * **Details** + * + * This layer provides the `CurrentTimeZone` service using the system's + * configured local time zone. + * + * **Example** (Providing local time zone layers) + * + * ```ts + * import { DateTime, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const now = yield* DateTime.nowInCurrentZone + * return DateTime.formatIsoZoned(now) + * }) + * + * // Use the system's local time zone + * Effect.provide(program, DateTime.layerCurrentZoneLocal) + * ``` + * + * @category current time zone + * @since 3.6.0 + */ +export const layerCurrentZoneLocal: Layer.Layer = Layer.sync(CurrentTimeZone)(zoneMakeLocal) diff --git a/repos/effect/packages/effect/src/Deferred.ts b/repos/effect/packages/effect/src/Deferred.ts new file mode 100644 index 0000000..a2f6260 --- /dev/null +++ b/repos/effect/packages/effect/src/Deferred.ts @@ -0,0 +1,917 @@ +/** + * One-time coordination cells for Effect programs. A `Deferred` starts + * empty, can be completed exactly once with a success, failure, defect, or + * interruption, and lets any number of fibers wait for that result. Awaiting a + * `Deferred` suspends the fiber instead of blocking an operating-system thread, + * and every waiter observes the same completion. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.ts" +import type { Effect } from "./Effect.ts" +import type * as Exit from "./Exit.ts" +import { dual, identity, type LazyArg } from "./Function.ts" +import * as core from "./internal/core.ts" +import * as internalEffect from "./internal/effect.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Types from "./Types.ts" + +const TypeId = "~effect/Deferred" + +/** + * A `Deferred` represents an asynchronous variable that can be set exactly + * once, with the ability for an arbitrary number of fibers to suspend (by + * calling `Deferred.await`) and automatically resume when the variable is set. + * + * **When to use** + * + * Use to coordinate multiple fibers around a value or failure that will be + * supplied exactly once. + * + * **Example** (Creating a Deferred for inter-fiber communication) + * + * ```ts + * import { Deferred, Effect, Fiber } from "effect" + * + * // Create and use a Deferred for inter-fiber communication + * const program = Effect.gen(function*() { + * // Create a Deferred that will hold a string value + * const deferred: Deferred.Deferred = yield* Deferred.make() + * + * // Fork a fiber that will set the deferred value + * const producer = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("100 millis") + * yield* Deferred.succeed(deferred, "Hello, World!") + * }) + * ) + * + * // Fork a fiber that will await the deferred value + * const consumer = yield* Effect.forkChild( + * Effect.gen(function*() { + * const value = yield* Deferred.await(deferred) + * console.log("Received:", value) + * return value + * }) + * ) + * + * // Wait for both fibers to complete + * yield* Fiber.join(producer) + * const result = yield* Fiber.join(consumer) + * return result + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Deferred extends Deferred.Variance, Pipeable { + effect?: Effect + resumes?: Array<(effect: Effect) => void> | undefined +} + +/** + * Checks whether a value is a `Deferred`. + * + * **When to use** + * + * Use to validate unknown values at runtime boundaries before treating them as + * `Deferred` values. + * + * @category guards + * @since 4.0.0 + */ +export const isDeferred = (u: unknown): u is Deferred => hasProperty(u, TypeId) + +/** + * Companion namespace containing type-level metadata for `Deferred`. + * + * **When to use** + * + * Use to reference type-level metadata associated with `Deferred`. + * + * @since 2.0.0 + */ +export declare namespace Deferred { + /** + * Type-level variance marker for the value and error channels of `Deferred`. + * + * **When to use** + * + * Use to carry the value and error type parameters for `Deferred` in Effect's + * type machinery. + * + * **Details** + * + * This interface is part of the public type structure and is not intended to + * be constructed directly. + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Invariant + readonly _E: Types.Invariant + } + } +} + +const DeferredProto = { + [TypeId]: { + _A: identity, + _E: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Creates an empty `Deferred` synchronously outside the `Effect` runtime. + * + * **When to use** + * + * Use to allocate a `Deferred` synchronously when direct allocation outside + * `Effect` is required. + * + * **Example** (Creating a Deferred unsafely) + * + * ```ts + * import { Deferred } from "effect" + * + * const deferred = Deferred.makeUnsafe() + * console.log(deferred) + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const makeUnsafe = (): Deferred => { + const self = Object.create(DeferredProto) + self.resumes = undefined + self.effect = undefined + return self +} + +/** + * Creates a new `Deferred`. + * + * **When to use** + * + * Use to allocate an empty `Deferred` inside an `Effect` workflow. + * + * **Example** (Creating a Deferred) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * yield* Deferred.succeed(deferred, 42) + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect> => internalEffect.sync(() => makeUnsafe()) + +const _await = (self: Deferred): Effect => + internalEffect.callback((resume) => { + if (self.effect) return resume(self.effect) + self.resumes ??= [] + self.resumes.push(resume) + return internalEffect.sync(() => { + const index = self.resumes!.indexOf(resume) + self.resumes!.splice(index, 1) + }) + }) + +export { + /** + * Retrieves the value of the `Deferred`, suspending the fiber running the + * workflow until the result is available. + * + * **When to use** + * + * Use to wait for a `Deferred` to be completed and resume with its success, + * failure, defect, or interruption. + * + * **Details** + * + * Awaiters observe the completion effect stored in the `Deferred`. + * + * **Example** (Awaiting a Deferred value) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * yield* Deferred.succeed(deferred, 42) + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link complete} for completing from an effect and memoizing its result + * @see {@link completeWith} for completing with an effect directly + * + * @category getters + * @since 2.0.0 + */ + _await as await +} + +/** + * Runs the supplied `Effect` and attempts to complete the `Deferred` with its + * memoized result. + * + * **When to use** + * + * Use when completing a `Deferred` should run an effect once and share its + * result with all awaiters. + * + * **Details** + * + * The returned effect succeeds with `true` when this call completed the + * `Deferred`, or `false` if it was already completed. + * + * **Example** (Completing a Deferred from an effect) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const completed = yield* Deferred.complete(deferred, Effect.succeed(42)) + * console.log(completed) // true + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link completeWith} for storing an effect directly without memoizing its result + * + * @category completion + * @since 2.0.0 + */ +export const complete: { + (effect: Effect): (self: Deferred) => Effect + (self: Deferred, effect: Effect): Effect +} = dual( + 2, + (self: Deferred, effect: Effect): Effect => + internalEffect.suspend(() => self.effect ? internalEffect.succeed(false) : into(effect, self)) +) + +/** + * Attempts to complete the `Deferred` with the specified effect directly. + * + * **When to use** + * + * Use to store an already environment-free effect as the completion without + * running it during completion. + * + * **Details** + * + * The returned effect succeeds with `true` when this call completed the + * `Deferred`, or `false` if it was already completed. + * + * **Gotchas** + * + * The supplied effect is not memoized by `completeWith`; each awaiter may run + * the stored effect independently. + * + * **Example** (Completing a Deferred with an effect) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const completed = yield* Deferred.completeWith(deferred, Effect.succeed(42)) + * console.log(completed) // true + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link complete} for running an effect once and sharing its result + * @see {@link done} for completing from an already computed `Exit` + * + * @category completion + * @since 2.0.0 + */ +export const completeWith: { + (effect: Effect): (self: Deferred) => Effect + (self: Deferred, effect: Effect): Effect +} = dual( + 2, + (self: Deferred, effect: Effect): Effect => + internalEffect.sync(() => doneUnsafe(self, effect)) +) + +/** + * Completes the `Deferred` with the specified `Exit` value, which will be + * propagated to all fibers waiting on the value of the `Deferred`. + * + * **When to use** + * + * Use to complete a `Deferred` from an already computed `Exit`. + * + * **Details** + * + * The returned effect succeeds with `true` when this call completed the + * `Deferred`, or `false` if it was already completed. + * + * **Example** (Completing a Deferred with an Exit) + * + * ```ts + * import { Deferred, Effect, Exit } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * yield* Deferred.done(deferred, Exit.succeed(42)) + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link complete} for completing from an effect and memoizing its result + * @see {@link completeWith} for storing an effect directly + * @see {@link succeed} for completing with a success value + * @see {@link failCause} for completing with a failure cause + * + * @category completion + * @since 2.0.0 + */ +export const done: { + (exit: Exit.Exit): (self: Deferred) => Effect + (self: Deferred, exit: Exit.Exit): Effect +} = completeWith as any + +/** + * Attempts to complete the `Deferred` with the specified error. + * + * **When to use** + * + * Use to complete a `Deferred` with a typed failure value. + * + * **Details** + * + * Fibers waiting on the `Deferred` fail with that error only if this call + * completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Failing a Deferred with an error) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.fail(deferred, "Operation failed") + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const fail: { + (error: E): (self: Deferred) => Effect + (self: Deferred, error: E): Effect +} = dual(2, (self: Deferred, error: E): Effect => done(self, core.exitFail(error))) + +/** + * Computes an error when the returned effect is run, then attempts to complete + * the `Deferred` with that error. + * + * **When to use** + * + * Use to lazily compute a typed failure value when the `Deferred` completion + * effect runs. + * + * **Details** + * + * Fibers waiting on the `Deferred` fail with the computed error only if this + * call completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Failing a Deferred with a lazy error) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.failSync(deferred, () => "Lazy error") + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const failSync: { + (evaluate: LazyArg): (self: Deferred) => Effect + (self: Deferred, evaluate: LazyArg): Effect +} = dual( + 2, + (self: Deferred, evaluate: LazyArg): Effect => + internalEffect.suspend(() => fail(self, evaluate())) +) + +/** + * Attempts to complete the `Deferred` with the specified `Cause`. + * + * **When to use** + * + * Use to complete a `Deferred` with a full failure cause. + * + * **Details** + * + * Fibers waiting on the `Deferred` observe that cause only if this call + * completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Failing a Deferred with a Cause) + * + * ```ts + * import { Cause, Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.failCause( + * deferred, + * Cause.fail("Operation failed") + * ) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const failCause: { + (cause: Cause.Cause): (self: Deferred) => Effect + (self: Deferred, cause: Cause.Cause): Effect +} = dual( + 2, + (self: Deferred, cause: Cause.Cause): Effect => done(self, core.exitFailCause(cause)) +) + +/** + * Computes a `Cause` when the returned effect is run, then attempts to + * complete the `Deferred` with that cause. + * + * **When to use** + * + * Use to lazily compute a full failure cause when the `Deferred` completion + * effect runs. + * + * **Details** + * + * Fibers waiting on the `Deferred` observe the computed cause only if this + * call completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Failing a Deferred with a lazy Cause) + * + * ```ts + * import { Cause, Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.failCauseSync( + * deferred, + * () => Cause.fail("Lazy error") + * ) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const failCauseSync: { + (evaluate: LazyArg>): (self: Deferred) => Effect + (self: Deferred, evaluate: LazyArg>): Effect +} = dual( + 2, + (self: Deferred, evaluate: LazyArg>): Effect => + internalEffect.suspend(() => failCause(self, evaluate())) +) + +/** + * Attempts to complete the `Deferred` with a defect. + * + * **When to use** + * + * Use to complete a `Deferred` with an unexpected defect. + * + * **Details** + * + * Fibers waiting on the `Deferred` die with that defect only if this call + * completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Killing a Deferred with a defect) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.die( + * deferred, + * new Error("Something went wrong") + * ) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const die: { + (defect: unknown): (self: Deferred) => Effect + (self: Deferred, defect: unknown): Effect +} = dual(2, (self: Deferred, defect: unknown): Effect => done(self, core.exitDie(defect))) + +/** + * Computes a defect when the returned effect is run, then attempts to complete + * the `Deferred` with that defect. + * + * **When to use** + * + * Use to lazily compute an unexpected defect when the completion effect runs. + * + * **Details** + * + * Fibers waiting on the `Deferred` die with the computed defect only if this + * call completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Killing a Deferred with a lazy defect) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.dieSync( + * deferred, + * () => new Error("Lazy error") + * ) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const dieSync: { + (evaluate: LazyArg): (self: Deferred) => Effect + (self: Deferred, evaluate: LazyArg): Effect +} = dual( + 2, + (self: Deferred, evaluate: LazyArg): Effect => + internalEffect.suspend(() => die(self, evaluate())) +) + +/** + * Attempts to complete the `Deferred` with interruption by the current fiber. + * + * **When to use** + * + * Use to complete a `Deferred` as interrupted by the current fiber. + * + * **Details** + * + * Fibers waiting on the `Deferred` are interrupted with the current fiber id + * only if this call completes it. The returned effect succeeds with `true` + * when this call completed the `Deferred`, or `false` if it was already + * completed. + * + * **Example** (Interrupting a Deferred) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.interrupt(deferred) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const interrupt = (self: Deferred): Effect => + core.withFiber((fiber) => interruptWith(self, fiber.id)) + +/** + * Attempts to complete the `Deferred` with interruption by the specified + * `FiberId`. + * + * **When to use** + * + * Use to complete a `Deferred` as interrupted by a specific fiber id. + * + * **Details** + * + * Fibers waiting on the `Deferred` are interrupted with that fiber id only if + * this call completes it. The returned effect succeeds with `true` when this + * call completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Interrupting a Deferred with a fiber id) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const success = yield* Deferred.interruptWith(deferred, 42) + * console.log(success) // true + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const interruptWith: { + (fiberId: number): (self: Deferred) => Effect + (self: Deferred, fiberId: number): Effect +} = dual( + 2, + (self: Deferred, fiberId: number): Effect => + failCause(self, internalEffect.causeInterrupt(fiberId)) +) + +/** + * Returns `true` if this `Deferred` has already been completed with a value or + * an error, `false` otherwise. + * + * **When to use** + * + * Use to check completion status inside an `Effect` workflow. + * + * **Example** (Checking Deferred completion) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const beforeCompletion = yield* Deferred.isDone(deferred) + * console.log(beforeCompletion) // false + * + * yield* Deferred.succeed(deferred, 42) + * const afterCompletion = yield* Deferred.isDone(deferred) + * console.log(afterCompletion) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isDone = (self: Deferred): Effect => internalEffect.sync(() => isDoneUnsafe(self)) + +/** + * Returns whether this `Deferred` has already been completed synchronously. + * + * **When to use** + * + * Use to check `Deferred` completion synchronously in code that cannot return + * an `Effect`, such as low-level integration code. + * + * @see {@link isDone} for checking completion inside `Effect` + * @see {@link poll} for reading the completed effect when available + * + * @category getters + * @since 4.0.0 + */ +export const isDoneUnsafe = (self: Deferred): boolean => self.effect !== undefined + +/** + * Returns the current completion effect as an `Option`. This returns + * `Option.some(effect)` when the `Deferred` is completed, `Option.none()` + * otherwise. + * + * **When to use** + * + * Use to inspect whether a `Deferred` is already completed and retrieve its + * stored completion effect when available. + * + * **Example** (Polling Deferred completion) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * const beforeCompletion = yield* Deferred.poll(deferred) + * console.log(beforeCompletion._tag === "None") // true + * + * yield* Deferred.succeed(deferred, 42) + * const afterCompletion = yield* Deferred.poll(deferred) + * console.log(afterCompletion._tag === "Some") // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export function poll(self: Deferred): Effect>> { + return internalEffect.sync(() => Option.fromUndefinedOr(self.effect)) +} + +/** + * Attempts to complete the `Deferred` with the specified value. + * + * **When to use** + * + * Use to complete a `Deferred` with a successful value. + * + * **Details** + * + * Fibers waiting on the `Deferred` receive the value only if this call + * completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Completing a Deferred with a value) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * yield* Deferred.succeed(deferred, 42) + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const succeed: { + (value: A): (self: Deferred) => Effect + (self: Deferred, value: A): Effect +} = dual(2, (self: Deferred, value: A): Effect => done(self, core.exitSucceed(value))) + +/** + * Computes a value when the returned effect is run, then attempts to complete + * the `Deferred` with that value. + * + * **When to use** + * + * Use to lazily compute a successful value when the `Deferred` completion + * effect runs. + * + * **Details** + * + * Fibers waiting on the `Deferred` receive the computed value only if this call + * completes it. The returned effect succeeds with `true` when this call + * completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Completing a Deferred with a lazy value) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* Deferred.make() + * yield* Deferred.sync(deferred, () => 42) + * + * const value = yield* Deferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const sync: { + (evaluate: LazyArg): (self: Deferred) => Effect + (self: Deferred, evaluate: LazyArg): Effect +} = dual( + 2, + (self: Deferred, evaluate: LazyArg): Effect => + internalEffect.suspend(() => succeed(self, evaluate())) +) + +/** + * Attempts to complete the `Deferred` synchronously with the specified + * completion effect. + * + * **When to use** + * + * Use to complete a `Deferred` synchronously in low-level code that already has + * the completion effect. + * + * **Details** + * + * This mutates the `Deferred` directly and should be reserved for low-level + * code; prefer the effectful completion APIs when possible. Returns `true` if + * this call completed the `Deferred`, or `false` if it was already completed. + * + * **Example** (Completing a Deferred unsafely) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * const deferred = Deferred.makeUnsafe() + * const success = Deferred.doneUnsafe(deferred, Effect.succeed(42)) + * console.log(success) // true + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const doneUnsafe = (self: Deferred, effect: Effect): boolean => { + if (self.effect) return false + self.effect = effect + if (self.resumes) { + for (let i = 0; i < self.resumes.length; i++) { + self.resumes[i](effect) + } + self.resumes = undefined + } + return true +} + +/** + * Runs an `Effect` and attempts to complete a `Deferred` with the effect's + * result. + * + * **When to use** + * + * Use to pipe an effect result into a `Deferred` while preserving success, + * failure, defects, and interruption. + * + * **Details** + * + * If the effect succeeds, fails, dies, or is interrupted, that result is used + * as the attempted completion. The returned effect cannot fail; it succeeds + * with `true` if it completed the `Deferred`, or `false` if the `Deferred` was + * already completed. + * + * **Example** (Completing a Deferred from an effect result) + * + * ```ts + * import { Deferred, Effect } from "effect" + * + * // Define an effect that succeeds + * const successEffect = Effect.succeed(42) + * + * const program = Effect.gen(function*() { + * // Create a deferred + * const deferred = yield* Deferred.make() + * + * // Complete the deferred using the successEffect + * const isCompleted = yield* Deferred.into(successEffect, deferred) + * + * // Access the value of the deferred + * const value = yield* Deferred.await(deferred) + * console.log(value) + * + * return isCompleted + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // 42 + * // true + * ``` + * + * @category Synchronization Utilities + * @since 4.0.0 + */ +export const into: { + (deferred: Deferred): (self: Effect) => Effect + (self: Effect, deferred: Deferred): Effect +} = dual( + 2, + (self: Effect, deferred: Deferred): Effect => + internalEffect.uninterruptibleMask((restore) => + internalEffect.flatMap( + internalEffect.exit(restore(self)), + (exit) => done(deferred, exit) + ) + ) +) diff --git a/repos/effect/packages/effect/src/Differ.ts b/repos/effect/packages/effect/src/Differ.ts new file mode 100644 index 0000000..ba636ba --- /dev/null +++ b/repos/effect/packages/effect/src/Differ.ts @@ -0,0 +1,32 @@ +/** + * Interfaces for describing changes to a value as patches. A + * `Differ` compares an old and new value, creates a patch for the + * difference, combines patches in order, and applies a patch to an old value to + * produce an updated value. + * + * @since 4.0.0 + */ + +/** + * Describes how to compute, combine, and apply patches for values of type `T`. + * + * **When to use** + * + * Use to model patch-based updates for a value type when callers need to + * compute a patch from two values, combine patches, and apply a patch later. + * + * **Details** + * + * A `Differ` provides an empty patch, computes the patch between two values, + * combines patches, and applies a patch to an old value to produce an updated + * value. + * + * @category models + * @since 2.0.0 + */ +export interface Differ { + readonly empty: Patch + diff(oldValue: T, newValue: T): Patch + combine(first: Patch, second: Patch): Patch + patch(oldValue: T, patch: Patch): T +} diff --git a/repos/effect/packages/effect/src/Duration.ts b/repos/effect/packages/effect/src/Duration.ts new file mode 100644 index 0000000..d2fe8eb --- /dev/null +++ b/repos/effect/packages/effect/src/Duration.ts @@ -0,0 +1,1827 @@ +/** + * Represents immutable spans of time. + * + * A `Duration` can be finite, positive infinity, or negative infinity. It is + * the standard representation for delays, timeouts, intervals, and + * time-to-live values across Effect APIs. This module includes constructors + * from common input shapes, unit conversions, comparisons, arithmetic, + * formatting, and reusable reducer or combiner helpers. + * + * @since 2.0.0 + */ +import * as Combiner from "./Combiner.ts" +import * as Equal from "./Equal.ts" +import type * as Equ from "./Equivalence.ts" +import { dual, identity } from "./Function.ts" +import * as Hash from "./Hash.ts" +import type * as Inspectable from "./Inspectable.ts" +import { NodeInspectSymbol } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty, isNumber } from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" + +const TypeId = "~effect/time/Duration" + +const bigint0 = BigInt(0) +const bigint1 = BigInt(1) +const bigint24 = BigInt(24) +const bigint60 = BigInt(60) +const bigint1e3 = BigInt(1_000) +const bigint1e6 = BigInt(1_000_000) +const bigint1e9 = BigInt(1_000_000_000) + +const roundTiesAwayFromZero = (input: number): bigint => + BigInt(input < 0 ? Math.ceil(input - 0.5) : Math.floor(input + 0.5)) + +const roundMillisToNanos = (millis: number): bigint => roundTiesAwayFromZero(millis * 1_000_000) + +const parseNanos = (input: string, scale: bigint): bigint => + input.includes(".") ? roundTiesAwayFromZero(Number(input) * Number(scale)) : BigInt(input) * scale + +const nanosToHrTime = (nanos: bigint): [seconds: number, nanos: number] => { + const sign = nanos < bigint0 ? -bigint1 : bigint1 + const absolute = nanos < bigint0 ? -nanos : nanos + return [ + Number(sign * (absolute / bigint1e9)), + Number(sign * (absolute % bigint1e9)) + ] +} + +/** + * Represents a span of time with high precision, supporting operations from + * nanoseconds to weeks. + * + * **When to use** + * + * Use to model elapsed time, delays, timeouts, schedule intervals, and cache + * TTLs as immutable duration values. + * + * @see {@link Input} for values accepted by APIs that decode duration-like + * inputs + * @see {@link DurationValue} for the tagged representation exposed by the + * `value` field + * + * @category models + * @since 2.0.0 + */ +export interface Duration extends Equal.Equal, Pipeable, Inspectable.Inspectable { + readonly [TypeId]: typeof TypeId + readonly value: DurationValue +} + +/** + * Tagged representation of a `Duration` value. + * + * **When to use** + * + * Use when modeling or inspecting the exact tagged representation stored in a + * `Duration`, including finite millisecond or nanosecond values and infinite + * sentinels. + * + * **Details** + * + * A duration is represented as milliseconds, nanoseconds, positive infinity, + * or negative infinity. + * + * @see {@link Duration} for the public type whose `value` field contains this + * representation + * @see {@link match} for pattern matching without reading `value` directly + * + * @category models + * @since 2.0.0 + */ +export type DurationValue = + | { _tag: "Millis"; millis: number } + | { _tag: "Nanos"; nanos: bigint } + | { _tag: "Infinity" } + | { _tag: "NegativeInfinity" } + +/** + * Valid time units that can be used in duration string representations. + * + * **When to use** + * + * Use when typing the unit portion of duration string inputs accepted by + * `Duration.Input`. + * + * @see {@link Input} for the full duration input union + * + * @category models + * @since 2.0.0 + */ +export type Unit = + | "nano" + | "nanos" + | "micro" + | "micros" + | "milli" + | "millis" + | "second" + | "seconds" + | "minute" + | "minutes" + | "hour" + | "hours" + | "day" + | "days" + | "week" + | "weeks" + +/** + * Valid input types that can be converted to a Duration. + * + * **When to use** + * + * Use when an API should accept any value that Effect can convert into a + * `Duration`, including existing durations, millisecond numbers, nanosecond + * bigints, high-resolution tuples, duration strings, infinity strings, or + * duration objects. + * + * **Details** + * + * String inputs accept values like `"10 seconds"`, `"500 millis"`, + * `"Infinity"`, and `"-Infinity"`. Finite fractional values that are + * normalized to nanoseconds are rounded to the nearest nanosecond, with ties + * away from zero. + * + * @see {@link fromInput} for safe conversion to `Option` + * @see {@link fromInputUnsafe} for throwing conversion + * @see {@link DurationObject} for object-shaped duration input + * @see {@link Unit} for supported string units + * + * @category models + * @since 4.0.0 + */ +export type Input = + | Duration + | number // millis + | bigint // nanos + | readonly [seconds: number, nanos: number] + | `${number} ${Unit}` + | "Infinity" + | "-Infinity" + | DurationObject + +/** + * An object with optional duration components that can be combined to create + * a Duration. All fields are optional and additive. + * + * **Details** + * + * Compatible with Temporal.Duration-like objects. + * + * **Example** (Combining duration object fields) + * + * ```ts + * import { Duration } from "effect" + * + * Duration.fromInputUnsafe({ seconds: 30 }) + * Duration.fromInputUnsafe({ days: 1 }) + * Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface DurationObject { + readonly weeks?: number | undefined + readonly days?: number | undefined + readonly hours?: number | undefined + readonly minutes?: number | undefined + readonly seconds?: number | undefined + readonly milliseconds?: number | undefined + readonly microseconds?: number | undefined + readonly nanoseconds?: number | undefined +} + +const DURATION_REGEXP = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/ + +/** + * Decodes a `Duration.Input` into a `Duration`. + * + * **When to use** + * + * Use when the input has already been validated or comes from a trusted source + * and throwing is acceptable for invalid duration syntax. + * + * **Gotchas** + * + * If the input is not a valid `Duration.Input`, it throws an error. + * + * **Example** (Decoding duration inputs) + * + * ```ts + * import { Duration } from "effect" + * + * const duration1 = Duration.fromInputUnsafe(1000) // 1000 milliseconds + * const duration2 = Duration.fromInputUnsafe("5 seconds") + * const duration3 = Duration.fromInputUnsafe("Infinity") + * const duration4 = Duration.fromInputUnsafe([2, 500_000_000]) // 2 seconds and 500ms + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromInputUnsafe = (input: Input): Duration => { + switch (typeof input) { + case "number": + return millis(input) + case "bigint": + return nanos(input) + case "string": { + if (input === "Infinity") { + return infinity + } + if (input === "-Infinity") { + return negativeInfinity + } + const match = DURATION_REGEXP.exec(input) + if (!match) break + const [_, valueStr, unit] = match + if (unit === "nano" || unit === "nanos") { + return nanos(parseNanos(valueStr, bigint1)) + } + if (unit === "micro" || unit === "micros") { + return nanos(parseNanos(valueStr, bigint1e3)) + } + const value = Number(valueStr) + switch (unit) { + case "milli": + case "millis": + return millis(value) + case "second": + case "seconds": + return seconds(value) + case "minute": + case "minutes": + return minutes(value) + case "hour": + case "hours": + return hours(value) + case "day": + case "days": + return days(value) + case "week": + case "weeks": + return weeks(value) + } + break + } + case "object": { + if (input === null) break + if (TypeId in input) return input as Duration + if (Array.isArray(input)) { + if (input.length !== 2 || !input.every(isNumber)) { + return invalid(input) + } + if (Number.isNaN(input[0]) || Number.isNaN(input[1])) { + return zero + } + if (input[0] === -Infinity || input[1] === -Infinity) { + return negativeInfinity + } + if (input[0] === Infinity || input[1] === Infinity) { + return infinity + } + return make(roundTiesAwayFromZero(input[0] * 1_000_000_000 + input[1])) + } + const obj = input as DurationObject + let millis = 0 + // we can use truthy checks here, because 0 can be ignored + if (obj.weeks) millis += obj.weeks * 604_800_000 + if (obj.days) millis += obj.days * 86_400_000 + if (obj.hours) millis += obj.hours * 3_600_000 + if (obj.minutes) millis += obj.minutes * 60_000 + if (obj.seconds) millis += obj.seconds * 1_000 + if (obj.milliseconds) millis += obj.milliseconds + if (!obj.microseconds && !obj.nanoseconds) return make(millis) + return make(roundTiesAwayFromZero( + millis * 1_000_000 + (obj.microseconds ?? 0) * 1_000 + (obj.nanoseconds ?? 0) + )) + } + } + return invalid(input) +} + +const invalid = (input: unknown): never => { + throw new Error(`Invalid Input: ${input}`) +} + +/** + * Decodes a `Input` value into a `Duration` safely, returning + * `Option.none()` if decoding fails. + * + * **Example** (Safely decoding duration inputs) + * + * ```ts + * import { Duration, Option } from "effect" + * + * Duration.fromInput(1000).pipe(Option.map(Duration.toSeconds)) // Some(1) + * + * Duration.fromInput("invalid" as any) // None + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromInput: (u: Input) => Option.Option = Option.liftThrowable( + fromInputUnsafe +) + +const zeroDurationValue: DurationValue = { _tag: "Millis", millis: 0 } +const infinityDurationValue: DurationValue = { _tag: "Infinity" } +const negativeInfinityDurationValue: DurationValue = { _tag: "NegativeInfinity" } + +const DurationProto: Omit = { + [TypeId]: TypeId, + [Hash.symbol](this: Duration) { + return Hash.structure(this.value) + }, + [Equal.symbol](this: Duration, that: unknown): boolean { + return isDuration(that) && equals(this, that) + }, + toString(this: Duration) { + switch (this.value._tag) { + case "Infinity": + return "Infinity" + case "NegativeInfinity": + return "-Infinity" + case "Nanos": + return `${this.value.nanos} nanos` + case "Millis": + return `${this.value.millis} millis` + } + }, + toJSON(this: Duration) { + switch (this.value._tag) { + case "Millis": + return { _id: "Duration", _tag: "Millis", millis: this.value.millis } + case "Nanos": + return { _id: "Duration", _tag: "Nanos", nanos: String(this.value.nanos) } + case "Infinity": + return { _id: "Duration", _tag: "Infinity" } + case "NegativeInfinity": + return { _id: "Duration", _tag: "NegativeInfinity" } + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} as const + +const make = (input: number | bigint): Duration => { + const duration = Object.create(DurationProto) + if (typeof input === "number") { + if (isNaN(input) || input === 0 || Object.is(input, -0)) { + duration.value = zeroDurationValue + } else if (!Number.isFinite(input)) { + duration.value = input > 0 ? infinityDurationValue : negativeInfinityDurationValue + } else if (!Number.isInteger(input)) { + duration.value = { _tag: "Nanos", nanos: roundMillisToNanos(input) } + } else { + duration.value = { _tag: "Millis", millis: input } + } + } else if (input === bigint0) { + duration.value = zeroDurationValue + } else { + duration.value = { _tag: "Nanos", nanos: input } + } + return duration +} + +/** + * Checks whether a value is a Duration. + * + * **Example** (Checking for durations) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.isDuration(Duration.seconds(1))) // true + * console.log(Duration.isDuration(1000)) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId) + +/** + * Checks whether a Duration is finite (not infinite). + * + * **Example** (Checking finite durations) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.isFinite(Duration.seconds(5))) // true + * console.log(Duration.isFinite(Duration.infinity)) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isFinite = (self: Duration): boolean => + self.value._tag !== "Infinity" && self.value._tag !== "NegativeInfinity" + +/** + * Checks whether a Duration is zero. + * + * **Example** (Checking for zero durations) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.isZero(Duration.zero)) // true + * console.log(Duration.isZero(Duration.seconds(1))) // false + * ``` + * + * @category guards + * @since 3.5.0 + */ +export const isZero = (self: Duration): boolean => { + switch (self.value._tag) { + case "Millis": + return self.value.millis === 0 + case "Nanos": + return self.value.nanos === bigint0 + case "Infinity": + case "NegativeInfinity": + return false + } +} + +/** + * Returns `true` if the duration is negative (strictly less than zero). + * + * **Example** (Checking for negative durations) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.isNegative(Duration.seconds(-5))) // true + * console.log(Duration.isNegative(Duration.zero)) // false + * console.log(Duration.isNegative(Duration.negativeInfinity)) // true + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isNegative = (self: Duration): boolean => { + switch (self.value._tag) { + case "Millis": + return self.value.millis < 0 + case "Nanos": + return self.value.nanos < bigint0 + case "NegativeInfinity": + return true + case "Infinity": + return false + } +} + +/** + * Returns `true` if the duration is positive (strictly greater than zero). + * + * **Example** (Checking for positive durations) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.isPositive(Duration.seconds(5))) // true + * console.log(Duration.isPositive(Duration.zero)) // false + * console.log(Duration.isPositive(Duration.infinity)) // true + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isPositive = (self: Duration): boolean => { + switch (self.value._tag) { + case "Millis": + return self.value.millis > 0 + case "Nanos": + return self.value.nanos > bigint0 + case "Infinity": + return true + case "NegativeInfinity": + return false + } +} + +/** + * Returns the absolute value of the duration. + * + * **Example** (Taking absolute duration values) + * + * ```ts + * import { Duration } from "effect" + * + * Duration.toMillis(Duration.abs(Duration.seconds(-5))) // 5000 + * Duration.abs(Duration.negativeInfinity) === Duration.infinity // true + * ``` + * + * @category math + * @since 4.0.0 + */ +export const abs = (self: Duration): Duration => { + switch (self.value._tag) { + case "Infinity": + case "NegativeInfinity": + return infinity + case "Millis": + return self.value.millis < 0 ? make(-self.value.millis) : self + case "Nanos": + return self.value.nanos < bigint0 ? make(-self.value.nanos) : self + } +} + +/** + * Returns the negated duration. + * + * **Example** (Negating durations) + * + * ```ts + * import { Duration } from "effect" + * + * Duration.toMillis(Duration.negate(Duration.seconds(5))) // -5000 + * Duration.negate(Duration.infinity) === Duration.negativeInfinity // true + * ``` + * + * @category math + * @since 4.0.0 + */ +export const negate = (self: Duration): Duration => { + switch (self.value._tag) { + case "Infinity": + return negativeInfinity + case "NegativeInfinity": + return infinity + case "Millis": + return self.value.millis === 0 ? self : make(-self.value.millis) + case "Nanos": + return self.value.nanos === bigint0 ? self : make(-self.value.nanos) + } +} + +/** + * A Duration representing zero time. + * + * **Example** (Referencing the zero duration) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toMillis(Duration.zero)) // 0 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const zero: Duration = make(0) + +/** + * A Duration representing infinite time. + * + * **Example** (Referencing infinite duration) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toMillis(Duration.infinity)) // Infinity + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const infinity: Duration = make(Infinity) + +/** + * A Duration representing negative infinite time. + * + * **Example** (Referencing negative infinite duration) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toMillis(Duration.negativeInfinity)) // -Infinity + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const negativeInfinity: Duration = make(-Infinity) + +/** + * Creates a Duration from nanoseconds. + * + * **Example** (Creating durations from nanoseconds) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.nanos(BigInt(500_000_000)) + * console.log(Duration.toMillis(duration)) // 500 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const nanos = (nanos: bigint): Duration => make(nanos) + +/** + * Creates a Duration from microseconds. + * + * **Example** (Creating durations from microseconds) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.micros(BigInt(500_000)) + * console.log(Duration.toMillis(duration)) // 500 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const micros = (micros: bigint): Duration => make(micros * bigint1e3) + +/** + * Creates a Duration from milliseconds. + * + * **Example** (Creating durations from milliseconds) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.millis(1000) + * console.log(Duration.toMillis(duration)) // 1000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const millis = (millis: number): Duration => make(millis) + +/** + * Creates a Duration from seconds. + * + * **Example** (Creating durations from seconds) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.seconds(30) + * console.log(Duration.toMillis(duration)) // 30000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const seconds = (seconds: number): Duration => make(seconds * 1000) + +/** + * Creates a Duration from minutes. + * + * **Example** (Creating durations from minutes) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.minutes(5) + * console.log(Duration.toMillis(duration)) // 300000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const minutes = (minutes: number): Duration => make(minutes * 60_000) + +/** + * Creates a Duration from hours. + * + * **Example** (Creating durations from hours) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.hours(2) + * console.log(Duration.toMillis(duration)) // 7200000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const hours = (hours: number): Duration => make(hours * 3_600_000) + +/** + * Creates a Duration from days. + * + * **Example** (Creating durations from days) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.days(1) + * console.log(Duration.toMillis(duration)) // 86400000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const days = (days: number): Duration => make(days * 86_400_000) + +/** + * Creates a Duration from weeks. + * + * **Example** (Creating durations from weeks) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.weeks(1) + * console.log(Duration.toMillis(duration)) // 604800000 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const weeks = (weeks: number): Duration => make(weeks * 604_800_000) + +/** + * Converts a Duration to milliseconds. + * + * **Example** (Converting durations to milliseconds) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toMillis(Duration.seconds(5))) // 5000 + * console.log(Duration.toMillis(Duration.minutes(2))) // 120000 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toMillis = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: identity, + onNanos: (nanos) => Number(nanos) / 1_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Converts a Duration to seconds. + * + * **Example** (Converting durations to seconds) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toSeconds(Duration.millis(5000))) // 5 + * console.log(Duration.toSeconds(Duration.minutes(2))) // 120 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toSeconds = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: (millis) => millis / 1_000, + onNanos: (nanos) => Number(nanos) / 1_000_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Converts a Duration to minutes. + * + * **Example** (Converting durations to minutes) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toMinutes(Duration.seconds(120))) // 2 + * console.log(Duration.toMinutes(Duration.hours(1))) // 60 + * ``` + * + * @category getters + * @since 3.8.0 + */ +export const toMinutes = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: (millis) => millis / 60_000, + onNanos: (nanos) => Number(nanos) / 60_000_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Converts a Duration to hours. + * + * **Example** (Converting durations to hours) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toHours(Duration.minutes(120))) // 2 + * console.log(Duration.toHours(Duration.days(1))) // 24 + * ``` + * + * @category getters + * @since 3.8.0 + */ +export const toHours = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: (millis) => millis / 3_600_000, + onNanos: (nanos) => Number(nanos) / 3_600_000_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Converts a Duration to days. + * + * **Example** (Converting durations to days) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toDays(Duration.hours(48))) // 2 + * console.log(Duration.toDays(Duration.weeks(1))) // 7 + * ``` + * + * @category getters + * @since 3.8.0 + */ +export const toDays = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: (millis) => millis / 86_400_000, + onNanos: (nanos) => Number(nanos) / 86_400_000_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Converts a Duration to weeks. + * + * **Example** (Converting durations to weeks) + * + * ```ts + * import { Duration } from "effect" + * + * console.log(Duration.toWeeks(Duration.days(14))) // 2 + * console.log(Duration.toWeeks(Duration.days(7))) // 1 + * ``` + * + * @category getters + * @since 3.8.0 + */ +export const toWeeks = (self: Input): number => + match(fromInputUnsafe(self), { + onMillis: (millis) => millis / 604_800_000, + onNanos: (nanos) => Number(nanos) / 604_800_000_000_000, + onInfinity: () => Infinity, + onNegativeInfinity: () => -Infinity + }) + +/** + * Gets the duration in nanoseconds as a bigint. + * + * **When to use** + * + * Use when the duration is known to be finite and you need the nanosecond value + * as a `bigint`. + * + * **Details** + * + * Millisecond-backed fractional durations are rounded to the nearest + * nanosecond, with ties away from zero. + * + * **Gotchas** + * + * If the duration is infinite, it throws an error. + * + * **Example** (Reading nanoseconds unsafely) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.seconds(2) + * const nanos = Duration.toNanosUnsafe(duration) + * console.log(nanos) // 2000000000n + * + * // Duration.toNanosUnsafe(Duration.infinity) + * // throws Error: "Cannot convert infinite duration to nanos" + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const toNanosUnsafe = (input: Input): bigint => { + const self = fromInputUnsafe(input) + switch (self.value._tag) { + case "Infinity": + case "NegativeInfinity": + throw new Error("Cannot convert infinite duration to nanos") + case "Nanos": + return self.value.nanos + case "Millis": + return roundMillisToNanos(self.value.millis) + } +} + +/** + * Gets the duration in nanoseconds safely as an `Option`. + * + * **Details** + * + * If the duration is infinite, returns `Option.none()`. + * + * **Example** (Safely reading nanoseconds) + * + * ```ts + * import { Duration, Option } from "effect" + * + * Duration.toNanos(Duration.seconds(1)) // Some(1000000000n) + * + * Duration.toNanos(Duration.infinity) // None + * Option.getOrUndefined(Duration.toNanos(Duration.infinity)) // undefined + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toNanos: (self: Input) => Option.Option = Option.liftThrowable(toNanosUnsafe) + +/** + * Converts a Duration to high-resolution time format [seconds, nanoseconds]. + * + * **Example** (Converting durations to high-resolution time) + * + * ```ts + * import { Duration } from "effect" + * + * const duration = Duration.millis(1500) + * const hrtime = Duration.toHrTime(duration) + * console.log(hrtime) // [1, 500000000] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toHrTime = (input: Input): [seconds: number, nanos: number] => { + const self = fromInputUnsafe(input) + switch (self.value._tag) { + case "Infinity": + return [Infinity, 0] + case "NegativeInfinity": + return [-Infinity, 0] + case "Nanos": + return nanosToHrTime(self.value.nanos) + case "Millis": + return nanosToHrTime(roundMillisToNanos(self.value.millis)) + } +} + +/** + * Pattern matches on the representation of a `Duration`. + * + * **Details** + * + * Provide handlers for millisecond-backed values, nanosecond-backed values, + * and positive infinity. Use `onNegativeInfinity` to handle negative infinity + * separately; otherwise negative infinity is handled by `onInfinity`. + * + * **Example** (Pattern matching on duration representations) + * + * ```ts + * import { Duration } from "effect" + * + * const result = Duration.match(Duration.seconds(5), { + * onMillis: (millis) => `${millis} milliseconds`, + * onNanos: (nanos) => `${nanos} nanoseconds`, + * onInfinity: () => "infinite" + * }) + * console.log(result) // "5000 milliseconds" + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + ( + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + readonly onInfinity: () => C + readonly onNegativeInfinity?: () => D + } + ): (self: Duration) => A | B | C | D + ( + self: Duration, + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + readonly onInfinity: () => C + readonly onNegativeInfinity?: () => D + } + ): A | B | C | D +} = dual(2, ( + self: Duration, + options: { + readonly onMillis: (millis: number) => A + readonly onNanos: (nanos: bigint) => B + readonly onInfinity: () => C + readonly onNegativeInfinity?: () => D + } +): A | B | C | D => { + switch (self.value._tag) { + case "Millis": + return options.onMillis(self.value.millis) + case "Nanos": + return options.onNanos(self.value.nanos) + case "Infinity": + return options.onInfinity() + case "NegativeInfinity": + return (options.onNegativeInfinity ?? options.onInfinity as unknown as () => D)() + } +}) + +/** + * Pattern matches on two `Duration`s, providing handlers that receive both values. + * + * **Example** (Pattern matching on duration pairs) + * + * ```ts + * import { Duration } from "effect" + * + * const sum = Duration.matchPair(Duration.seconds(3), Duration.seconds(2), { + * onMillis: (a, b) => a + b, + * onNanos: (a, b) => Number(a + b), + * onInfinity: () => Infinity + * }) + * console.log(sum) // 5000 + * ``` + * + * @category pattern matching + * @since 4.0.0 + */ +export const matchPair: { + ( + that: Duration, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + readonly onInfinity: (self: Duration, that: Duration) => C + } + ): (self: Duration) => A | B | C + ( + self: Duration, + that: Duration, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + readonly onInfinity: (self: Duration, that: Duration) => C + } + ): A | B | C +} = dual(3, ( + self: Duration, + that: Duration, + options: { + readonly onMillis: (self: number, that: number) => A + readonly onNanos: (self: bigint, that: bigint) => B + readonly onInfinity: (self: Duration, that: Duration) => C + } +): A | B | C => { + if ( + self.value._tag === "Infinity" || self.value._tag === "NegativeInfinity" || + that.value._tag === "Infinity" || that.value._tag === "NegativeInfinity" + ) return options.onInfinity(self, that) + if (self.value._tag === "Millis") { + return that.value._tag === "Millis" + ? options.onMillis(self.value.millis, that.value.millis) + : options.onNanos(toNanosUnsafe(self), that.value.nanos) + } else { + return options.onNanos(self.value.nanos, toNanosUnsafe(that)) + } +}) + +/** + * Provides an `Order` instance for comparing `Duration` values. + * + * **Details** + * + * `NegativeInfinity` < any finite value < `Infinity`. + * + * **Example** (Sorting durations) + * + * ```ts + * import { Duration } from "effect" + * + * const durations = [ + * Duration.seconds(3), + * Duration.seconds(1), + * Duration.seconds(2) + * ] + * const sorted = durations.sort((a, b) => Duration.Order(a, b)) + * console.log(sorted.map(Duration.toSeconds)) // [1, 2, 3] + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.make((self, that) => + matchPair(self, that, { + onMillis: (self, that) => (self < that ? -1 : self > that ? 1 : 0), + onNanos: (self, that) => (self < that ? -1 : self > that ? 1 : 0), + onInfinity: (self, that) => { + if (self.value._tag === that.value._tag) return 0 + if (self.value._tag === "Infinity") return 1 + if (self.value._tag === "NegativeInfinity") return -1 + // self is finite + if (that.value._tag === "Infinity") return -1 + // that is NegativeInfinity + return 1 + } + }) +) + +/** + * Returns `true` if a `Duration` is greater than or equal to `minimum` and + * less than or equal to `maximum`, according to `Duration.Order`. + * + * **When to use** + * + * Use to test whether a duration is inside an inclusive range. + * + * **Details** + * + * Both bounds are inclusive and compared with `Duration.Order`. + * + * **Gotchas** + * + * The bounds are not normalized. If `minimum` is greater than `maximum`, the + * predicate returns `false` for every duration. + * + * **Example** (Checking duration ranges) + * + * ```ts + * import { Duration } from "effect" + * + * const isInRange = Duration.between(Duration.seconds(3), { + * minimum: Duration.seconds(2), + * maximum: Duration.seconds(5) + * }) + * console.log(isInRange) // true + * ``` + * + * @see {@link clamp} for constraining a duration to a range + * @see {@link isGreaterThanOrEqualTo} for checking only the lower bound + * @see {@link isLessThanOrEqualTo} for checking only the upper bound + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { minimum: Duration; maximum: Duration }): (self: Duration) => boolean + (self: Duration, options: { minimum: Duration; maximum: Duration }): boolean +} = order.isBetween(Order) + +/** + * Provides an `Equivalence` instance for comparing `Duration` values. + * + * **Example** (Comparing durations for equivalence) + * + * ```ts + * import { Duration } from "effect" + * + * const isEqual = Duration.Equivalence(Duration.seconds(5), Duration.millis(5000)) + * console.log(isEqual) // true + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = (self, that) => + matchPair(self, that, { + onMillis: (self, that) => self === that, + onNanos: (self, that) => self === that, + onInfinity: (self, that) => self.value._tag === that.value._tag + }) + +/** + * Returns the smaller of two Durations. + * + * **Example** (Selecting the shorter duration) + * + * ```ts + * import { Duration } from "effect" + * + * const shorter = Duration.min(Duration.seconds(5), Duration.seconds(3)) + * console.log(Duration.toSeconds(shorter)) // 3 + * ``` + * + * @category ordering + * @since 2.0.0 + */ +export const min: { + (that: Duration): (self: Duration) => Duration + (self: Duration, that: Duration): Duration +} = order.min(Order) + +/** + * Returns the larger of two Durations. + * + * **Example** (Selecting the longer duration) + * + * ```ts + * import { Duration } from "effect" + * + * const longer = Duration.max(Duration.seconds(5), Duration.seconds(3)) + * console.log(Duration.toSeconds(longer)) // 5 + * ``` + * + * @category ordering + * @since 2.0.0 + */ +export const max: { + (that: Duration): (self: Duration) => Duration + (self: Duration, that: Duration): Duration +} = order.max(Order) + +/** + * Returns a `Duration` constrained between a minimum and maximum value. + * + * **Example** (Clamping durations to a range) + * + * ```ts + * import { Duration } from "effect" + * + * const clamped = Duration.clamp(Duration.seconds(10), { + * minimum: Duration.seconds(2), + * maximum: Duration.seconds(5) + * }) + * console.log(Duration.toSeconds(clamped)) // 5 + * ``` + * + * @category ordering + * @since 2.0.0 + */ +export const clamp: { + (options: { minimum: Duration; maximum: Duration }): (self: Duration) => Duration + (self: Duration, options: { minimum: Duration; maximum: Duration }): Duration +} = order.clamp(Order) + +/** + * Divides a `Duration` by a finite, non-zero number safely. + * + * **Details** + * + * Returns `Option.none()` for zero, negative zero, or non-finite divisors. For + * nanosecond-backed durations, also returns `Option.none()` when the divisor + * cannot be converted to a `bigint`, such as a fractional divisor. + * + * **Example** (Safely dividing durations) + * + * ```ts + * import { Duration, Option } from "effect" + * + * const d = Duration.divide(Duration.seconds(10), 2) + * console.log(Option.map(d, Duration.toSeconds)) // Some(5) + * + * Duration.divide(Duration.seconds(10), 0) // None + * ``` + * + * @category math + * @since 2.4.19 + */ +export const divide: { + (by: number): (self: Duration) => Option.Option + (self: Duration, by: number): Option.Option +} = dual( + 2, + (self: Duration, by: number): Option.Option => { + if (!Number.isFinite(by)) return Option.none() + if (by === 0 || Object.is(by, -0)) return Option.none() + return match(self, { + onMillis: (millis) => Option.some(make(millis / by)), + onNanos: (nanos) => { + try { + return Option.some(make(nanos / BigInt(by))) + } catch { + return Option.none() + } + }, + onInfinity: () => Option.some(by > 0 ? infinity : negativeInfinity), + onNegativeInfinity: () => Option.some(by > 0 ? negativeInfinity : infinity) + }) + } +) + +/** + * Divides a `Duration` by a number using fallback rules instead of returning + * an `Option`. + * + * **When to use** + * + * Use when dividing a `Duration` should return `Duration.zero` or signed + * infinity for invalid cases instead of forcing callers to handle `Option.none`. + * + * **Details** + * + * Non-finite divisors return `Duration.zero`. Division by positive or negative + * zero can produce signed infinity for non-zero finite durations, while zero + * or infinite durations divided by zero produce `Duration.zero`. + * Nanosecond-backed durations return `Duration.zero` when the divisor cannot + * be converted to a `bigint`. + * + * **Example** (Dividing durations unsafely) + * + * ```ts + * import { Duration } from "effect" + * + * const half = Duration.divideUnsafe(Duration.seconds(10), 2) + * console.log(Duration.toSeconds(half)) // 5 + * + * const infinite = Duration.divideUnsafe(Duration.seconds(10), 0) + * console.log(Duration.toMillis(infinite)) // Infinity + * ``` + * + * @category math + * @since 4.0.0 + */ +export const divideUnsafe: { + (by: number): (self: Duration) => Duration + (self: Duration, by: number): Duration +} = dual( + 2, + (self: Duration, by: number): Duration => { + if (!Number.isFinite(by)) return zero + return match(self, { + onMillis: (millis) => make(millis / by), + onNanos: (nanos) => { + if (Object.is(by, 0) || Object.is(by, -0)) { + if (nanos === bigint0) return zero + // match IEEE 754: same sign → +infinity, different sign → -infinity + const positiveNanos = nanos > bigint0 + const positiveZero = Object.is(by, 0) + return (positiveNanos === positiveZero) ? infinity : negativeInfinity + } + try { + return make(nanos / BigInt(by)) + } catch { + return zero + } + }, + onInfinity: () => by > 0 ? infinity : by < 0 ? negativeInfinity : zero, + onNegativeInfinity: () => by > 0 ? negativeInfinity : by < 0 ? infinity : zero + }) + } +) + +/** + * Returns a `Duration` multiplied by a number. + * + * **Details** + * + * For nanosecond-backed durations, the multiplier must be convertible to a + * `bigint`; fractional or non-finite multipliers can throw. Infinite + * durations return positive infinity, negative infinity, or zero depending on + * the multiplier sign. + * + * **Example** (Multiplying durations) + * + * ```ts + * import { Duration } from "effect" + * + * const doubled = Duration.times(Duration.seconds(5), 2) + * console.log(Duration.toSeconds(doubled)) // 10 + * ``` + * + * @category math + * @since 2.0.0 + */ +export const times: { + (times: number): (self: Duration) => Duration + (self: Duration, times: number): Duration +} = dual( + 2, + (self: Duration, times: number): Duration => + match(self, { + onMillis: (millis) => make(millis * times), + onNanos: (nanos) => make(nanos * BigInt(times)), + onInfinity: () => times > 0 ? infinity : times < 0 ? negativeInfinity : zero, + onNegativeInfinity: () => times > 0 ? negativeInfinity : times < 0 ? infinity : zero + }) +) + +/** + * Subtracts one Duration from another. The result can be negative. + * + * **Details** + * + * Infinity subtraction follows signed-infinity arithmetic. Subtracting the + * same infinity from itself returns zero. Positive infinity minus negative + * infinity or any finite duration remains positive infinity. Negative infinity + * minus positive infinity or any finite duration remains negative infinity. + * Finite durations minus positive infinity produce negative infinity, and + * finite durations minus negative infinity produce positive infinity. + * + * **Example** (Subtracting durations) + * + * ```ts + * import { Duration } from "effect" + * + * const result = Duration.subtract(Duration.seconds(10), Duration.seconds(3)) + * console.log(Duration.toSeconds(result)) // 7 + * ``` + * + * @category math + * @since 2.0.0 + */ +export const subtract: { + (that: Duration): (self: Duration) => Duration + (self: Duration, that: Duration): Duration +} = dual( + 2, + (self: Duration, that: Duration): Duration => + matchPair(self, that, { + onMillis: (self, that) => make(self - that), + onNanos: (self, that) => make(self - that), + onInfinity: (self, that) => { + const s = self.value._tag + const t = that.value._tag + if (s === "Infinity") return t === "Infinity" ? zero : infinity + if (s === "NegativeInfinity") return t === "NegativeInfinity" ? zero : negativeInfinity + return t === "Infinity" ? negativeInfinity : infinity + } + }) +) + +/** + * Adds two Durations together. + * + * **Details** + * + * Infinity addition follows these rules: + * + * - infinity + infinity = infinity + * - infinity + negativeInfinity = zero + * - infinity + finite = infinity + * - negativeInfinity + negativeInfinity = negativeInfinity + * - negativeInfinity + finite = negativeInfinity + * + * **Example** (Adding durations) + * + * ```ts + * import { Duration } from "effect" + * + * const total = Duration.sum(Duration.seconds(5), Duration.seconds(3)) + * console.log(Duration.toSeconds(total)) // 8 + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sum: { + (that: Duration): (self: Duration) => Duration + (self: Duration, that: Duration): Duration +} = dual( + 2, + (self: Duration, that: Duration): Duration => + matchPair(self, that, { + onMillis: (self, that) => make(self + that), + onNanos: (self, that) => make(self + that), + onInfinity: (self, that) => { + const s = self.value._tag + const t = that.value._tag + if (s === "Infinity" && t === "NegativeInfinity") return zero + if (s === "NegativeInfinity" && t === "Infinity") return zero + if (s === "Infinity" || t === "Infinity") return infinity + if (s === "NegativeInfinity" || t === "NegativeInfinity") return negativeInfinity + // unreachable, but satisfy TS + return zero + } + }) +) + +/** + * Checks whether the first Duration is less than the second. + * + * **Example** (Comparing durations with less than) + * + * ```ts + * import { Duration } from "effect" + * + * const isLess = Duration.isLessThan(Duration.seconds(3), Duration.seconds(5)) + * console.log(isLess) // true + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThan: { + (that: Duration): (self: Duration) => boolean + (self: Duration, that: Duration): boolean +} = order.isLessThan(Order) + +/** + * Checks whether the first Duration is less than or equal to the second. + * + * **Example** (Comparing durations with less than or equal) + * + * ```ts + * import { Duration } from "effect" + * + * const isLessOrEqual = Duration.isLessThanOrEqualTo( + * Duration.seconds(5), + * Duration.seconds(5) + * ) + * console.log(isLessOrEqual) // true + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: Duration): (self: Duration) => boolean + (self: Duration, that: Duration): boolean +} = order.isLessThanOrEqualTo(Order) + +/** + * Checks whether the first Duration is greater than the second. + * + * **Example** (Comparing durations with greater than) + * + * ```ts + * import { Duration } from "effect" + * + * const isGreater = Duration.isGreaterThan(Duration.seconds(5), Duration.seconds(3)) + * console.log(isGreater) // true + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: Duration): (self: Duration) => boolean + (self: Duration, that: Duration): boolean +} = order.isGreaterThan(Order) + +/** + * Checks whether the first Duration is greater than or equal to the second. + * + * **Example** (Comparing durations with greater than or equal) + * + * ```ts + * import { Duration } from "effect" + * + * const isGreaterOrEqual = Duration.isGreaterThanOrEqualTo( + * Duration.seconds(5), + * Duration.seconds(5) + * ) + * console.log(isGreaterOrEqual) // true + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: Duration): (self: Duration) => boolean + (self: Duration, that: Duration): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** + * Checks whether two Durations are equal. + * + * **Example** (Checking duration equality) + * + * ```ts + * import { Duration } from "effect" + * + * const isEqual = Duration.equals(Duration.seconds(5), Duration.millis(5000)) + * console.log(isEqual) // true + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const equals: { + (that: Duration): (self: Duration) => boolean + (self: Duration, that: Duration): boolean +} = dual(2, (self: Duration, that: Duration): boolean => Equivalence(self, that)) + +/** + * Decomposes a `Duration` into normalized signed components. + * + * **Details** + * + * Finite durations are returned as `{ days, hours, minutes, seconds, millis, + * nanos }`. Infinite durations return every component as `Infinity` or + * `-Infinity`. + * + * **Example** (Decomposing durations into parts) + * + * ```ts + * import { Duration } from "effect" + * + * // Create a complex duration by adding multiple parts + * const duration = Duration.sum( + * Duration.sum( + * Duration.sum(Duration.days(1), Duration.hours(2)), + * Duration.sum(Duration.minutes(30), Duration.seconds(45)) + * ), + * Duration.millis(123) + * ) + * const components = Duration.parts(duration) + * console.log(components) + * // { + * // days: 1, + * // hours: 2, + * // minutes: 30, + * // seconds: 45, + * // millis: 123, + * // nanos: 0 + * // } + * + * const complex = Duration.sum(Duration.hours(25), Duration.minutes(90)) + * const complexParts = Duration.parts(complex) + * console.log(complexParts) + * // { + * // days: 1, + * // hours: 2, + * // minutes: 30, + * // seconds: 0, + * // millis: 0, + * // nanos: 0 + * // } + * ``` + * + * @category converting + * @since 3.8.0 + */ +export const parts = (self: Duration): { + days: number + hours: number + minutes: number + seconds: number + millis: number + nanos: number +} => { + if (self.value._tag === "Infinity") { + return { + days: Infinity, + hours: Infinity, + minutes: Infinity, + seconds: Infinity, + millis: Infinity, + nanos: Infinity + } + } + if (self.value._tag === "NegativeInfinity") { + return { + days: -Infinity, + hours: -Infinity, + minutes: -Infinity, + seconds: -Infinity, + millis: -Infinity, + nanos: -Infinity + } + } + + const n = toNanosUnsafe(self) + const neg = n < bigint0 + const a = neg ? -n : n + const ms = a / bigint1e6 + const sec = ms / bigint1e3 + const min = sec / bigint60 + const hr = min / bigint60 + const d = hr / bigint24 + const sign = neg ? -1 : 1 + + return { + days: sign * Number(d), + hours: sign * Number(hr % bigint24), + minutes: sign * Number(min % bigint60), + seconds: sign * Number(sec % bigint60), + millis: sign * Number(ms % bigint1e3), + nanos: sign * Number(a % bigint1e6) + } +} + +/** + * Converts a `Duration` to a human readable string. + * + * **Example** (Formatting durations) + * + * ```ts + * import { Duration } from "effect" + * + * Duration.format(Duration.millis(1000)) // "1s" + * Duration.format(Duration.millis(1001)) // "1s 1ms" + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const format = (self: Duration): string => { + if (self.value._tag === "Infinity") { + return "Infinity" + } + if (self.value._tag === "NegativeInfinity") { + return "-Infinity" + } + if (isZero(self)) { + return "0" + } + if (isNegative(self)) { + return "-" + format(abs(self)) + } + + const fragments = parts(self) + const pieces = [] + if (fragments.days !== 0) { + pieces.push(`${fragments.days}d`) + } + + if (fragments.hours !== 0) { + pieces.push(`${fragments.hours}h`) + } + + if (fragments.minutes !== 0) { + pieces.push(`${fragments.minutes}m`) + } + + if (fragments.seconds !== 0) { + pieces.push(`${fragments.seconds}s`) + } + + if (fragments.millis !== 0) { + pieces.push(`${fragments.millis}ms`) + } + + if (fragments.nanos !== 0) { + pieces.push(`${fragments.nanos}ns`) + } + + return pieces.join(" ") +} + +/** + * Reducer for summing `Duration`s. + * + * **When to use** + * + * Use to sum many `Duration` values through APIs that consume a `Reducer`. + * + * **Details** + * + * `ReducerSum` uses `sum` and starts from `zero`, so `combineAll([])` returns + * `zero`. + * + * @see {@link sum} for adding two duration values directly + * @see {@link CombinerMax} for keeping the longest duration instead of summing + * @see {@link CombinerMin} for keeping the shortest duration instead of summing + * + * @category math + * @since 4.0.0 + */ +export const ReducerSum: Reducer.Reducer = Reducer.make(sum, zero) + +/** + * Combiner that returns the maximum `Duration`. + * + * **When to use** + * + * Use to keep the longest `Duration` when an API consumes a `Combiner`. + * + * @see {@link CombinerMin} for keeping the shortest `Duration` + * @see {@link max} for comparing two `Duration` values directly + * + * @category math + * @since 4.0.0 + */ +export const CombinerMax: Combiner.Combiner = Combiner.max(Order) + +/** + * Combiner that returns the minimum `Duration`. + * + * **When to use** + * + * Use to keep the shortest `Duration` through APIs that consume a `Combiner`. + * + * @see {@link CombinerMax} for keeping the longest `Duration` + * @see {@link min} for comparing two `Duration` values directly + * + * @category math + * @since 4.0.0 + */ +export const CombinerMin: Combiner.Combiner = Combiner.min(Order) diff --git a/repos/effect/packages/effect/src/Effect.ts b/repos/effect/packages/effect/src/Effect.ts new file mode 100644 index 0000000..7c4e042 --- /dev/null +++ b/repos/effect/packages/effect/src/Effect.ts @@ -0,0 +1,15444 @@ +/** + * Describes workflows that run only when executed by the Effect runtime. + * + * An `Effect` can succeed with an `A`, fail with an `E`, and require + * services `R`. Creating an effect does not perform the work; it builds a value + * that can be composed, provided with services, retried, interrupted, run + * concurrently, or inspected by the runtime. This module is the main API for + * creating effects, combining them, handling failures, managing resources, and + * running effect programs. + * + * @since 2.0.0 + */ +import type * as Arr from "./Array.ts" +import type * as Cause from "./Cause.ts" +import type { Clock } from "./Clock.ts" +import * as Context from "./Context.ts" +import * as Duration from "./Duration.ts" +import type { ExecutionPlan } from "./ExecutionPlan.ts" +import * as Exit from "./Exit.ts" +import type { Fiber } from "./Fiber.ts" +import type * as Filter from "./Filter.ts" +import { constant, dual, type LazyArg } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as core from "./internal/core.ts" +import * as internal from "./internal/effect.ts" +import * as internalExecutionPlan from "./internal/executionPlan.ts" +import * as internalLayer from "./internal/layer.ts" +import * as internalRequest from "./internal/request.ts" +import * as internalSchedule from "./internal/schedule.ts" +import type * as Layer from "./Layer.ts" +import type { Logger } from "./Logger.ts" +import type { Severity } from "./LogLevel.ts" +import * as Metric from "./Metric.ts" +import type { Option } from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type * as Predicate from "./Predicate.ts" +import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts" +import type * as Request from "./Request.ts" +import type { RequestResolver } from "./RequestResolver.ts" +import type * as Result from "./Result.ts" +import type { Schedule } from "./Schedule.ts" +import type { Scheduler } from "./Scheduler.ts" +import type { Scope } from "./Scope.ts" +import type { + AnySpan, + ParentSpan, + Span, + SpanLink, + SpanOptions, + SpanOptionsNoTrace, + TraceOptions, + Tracer +} from "./Tracer.ts" +import type { TxRef } from "./TxRef.ts" +import type { + Concurrency, + Covariant, + EqualsWith, + ExcludeReason, + ExcludeTag, + ExtractReason, + ExtractTag, + NarrowReason, + NoInfer, + OmitReason, + ReasonOf, + ReasonTags, + Simplify, + Tags, + unassigned +} from "./Types.ts" +import type * as Unify from "./Unify.ts" +import { internalCall } from "./Utils.ts" + +/** + * Type-level identifier for `Effect` values. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/Effect" + +/** + * Runtime identifier used to recognize `Effect` values. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = core.EffectTypeId + +/** + * The `Effect` interface defines a value that lazily describes a workflow or + * job. The workflow requires some context `R`, and may fail with an error of + * type `E`, or succeed with a value of type `A`. + * + * **When to use** + * + * Use when you need to represent a lazy, composable workflow that can require + * services, fail with a typed error, or succeed with a typed value. + * + * **Details** + * + * `Effect` values model resourceful interaction with the outside world, + * including synchronous, asynchronous, concurrent, and parallel interaction. + * They use a fiber-based concurrency model, with built-in support for + * scheduling, fine-grained interruption, structured concurrency, and high + * scalability. + * + * To run an `Effect` value, you need a `Runtime`, which is a type that is + * capable of executing `Effect` values. + * + * @category models + * @since 2.0.0 + */ +export interface Effect extends Pipeable, Inspectable { + readonly [TypeId]: Variance + [Symbol.iterator](): EffectIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: EffectUnify + [Unify.ignoreSymbol]?: {} +} + +/** + * Type-level unification support for `Effect` values. + * + * @category models + * @since 2.0.0 + */ +export interface EffectUnify { + Effect?: () => A[Unify.typeSymbol] extends + | Effect + | infer _ ? Effect + : never +} + +/** + * Type lambda used to represent `Effect` in higher-kinded APIs. + * + * @category type lambdas + * @since 2.0.0 + */ +export interface EffectTypeLambda extends TypeLambda { + readonly type: Effect +} + +/** + * Variance interface for Effect, encoding the type parameters' variance. + * + * @category models + * @since 2.0.0 + */ +export interface Variance { + _A: Covariant + _E: Covariant + _R: Covariant +} + +/** + * Extracts the success type from an `Effect`. + * + * **When to use** + * + * Use to derive the value produced by an existing effect when declaring + * reusable type aliases, service interfaces, or function signatures. + * + * @see {@link Error} for extracting the failure type from the same `Effect` + * @see {@link Services} for extracting the required services from the same `Effect` + * + * @category models + * @since 2.0.0 + */ +export type Success = T extends Effect ? _A + : never + +/** + * Extracts the error type from an `Effect`. + * + * **When to use** + * + * Use to derive the error type from an existing `Effect` type when declaring + * helper types, wrappers, or APIs that preserve the effect's failure channel. + * + * **Details** + * + * Non-`Effect` inputs resolve to `never`. + * + * @see {@link Success} for extracting the success value type instead + * @see {@link Services} for extracting the required services type instead + * + * @category models + * @since 2.0.0 + */ +export type Error = T extends Effect ? _E + : never + +/** + * Extracts the required services type from an `Effect`. + * + * **When to use** + * + * Use to derive the context requirements of a generic or inferred `Effect` + * without restating its `R` type parameter. + * + * @see {@link Success} for extracting the success value type instead + * @see {@link Error} for extracting the failure type instead + * + * @category models + * @since 4.0.0 + */ +export type Services = T extends Effect ? _R + : never + +/** + * Checks whether a value is an `Effect`. + * + * **Example** (Checking whether a value is an Effect) + * + * ```ts + * import { Effect } from "effect" + * + * console.log(Effect.isEffect(Effect.succeed(1))) // true + * console.log(Effect.isEffect("hello")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEffect: (u: unknown) => u is Effect = core.isEffect + +/** + * Iterator interface for Effect generators, enabling Effect values to work with generator functions. + * + * **When to use** + * + * Use when defining or typing `[Symbol.iterator]()` for values typed as + * `Effect`s so `yield*` can pass their success type back into `Effect.gen`. + * + * @see {@link gen} for writing generator-based `Effect` programs that consume this iterator protocol + * + * @category models + * @since 4.0.0 + */ +export interface EffectIterator> { + next( + ...args: ReadonlyArray + ): IteratorResult> +} + +// ======================================================================== +// Collecting +// ======================================================================== + +/** + * Namespace containing type utilities for the `Effect.all` function, which handles + * collecting multiple effects into various output structures. + * + * @since 2.0.0 + */ +export declare namespace All { + /** + * Alias for any `Effect` value accepted by `Effect.all`. + * + * @category utility types + * @since 2.0.0 + */ + export type EffectAny = Effect + + /** + * Computes the return type for `Effect.all` when collecting an iterable. + * + * @category utility types + * @since 2.0.0 + */ + export type ReturnIterable< + T extends Iterable, + Discard extends boolean, + Mode extends boolean = false + > = [T] extends [Iterable>] ? Effect< + Discard extends true ? void : Array : A>, + Mode extends true ? never : E, + R + > + : never + + /** + * Computes the return type for `Effect.all` when collecting a tuple. + * + * @category utility types + * @since 2.0.0 + */ + export type ReturnTuple< + T extends ReadonlyArray, + Discard extends boolean, + Mode extends boolean = false + > = Effect< + Discard extends true ? void + : T[number] extends never ? [] + : { + -readonly [K in keyof T]: T[K] extends Effect< + infer _A, + infer _E, + infer _R + > ? Mode extends true ? Result.Result<_A, _E> : _A + : never + }, + Mode extends true ? never + : T[number] extends never ? never + : T[number] extends Effect ? _E + : never, + T[number] extends never ? never + : T[number] extends Effect ? _R + : never + > extends infer X ? X + : never + + /** + * Computes the return type for `Effect.all` when collecting a record. + * + * @category utility types + * @since 2.0.0 + */ + export type ReturnObject = [T] extends [ + Record + ] ? Effect< + Discard extends true ? void + : { + -readonly [K in keyof T]: [T[K]] extends [ + Effect + ] ? Mode extends true ? Result.Result<_A, _E> : _A + : never + }, + Mode extends true ? never + : keyof T extends never ? never + : T[keyof T] extends Effect ? _E + : never, + keyof T extends never ? never + : T[keyof T] extends Effect ? _R + : never + > + : never + + /** + * Detects whether `Effect.all` should discard collected values. + * + * @category utility types + * @since 2.0.0 + */ + export type IsDiscard = [Extract] extends [ + never + ] ? false + : true + + /** + * Detects whether `Effect.all` should collect results in `Result` mode. + * + * @category utility types + * @since 4.0.0 + */ + export type IsResult = [Extract] extends [never] ? false : true + + /** + * Computes the return type for `Effect.all` from its input and options. + * + * @category utility types + * @since 2.0.0 + */ + export type Return< + Arg extends Iterable | Record, + O extends { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "result" | undefined + } + > = [Arg] extends [ReadonlyArray] ? ReturnTuple, IsResult> + : [Arg] extends [Iterable] ? ReturnIterable, IsResult> + : [Arg] extends [Record] ? ReturnObject, IsResult> + : never +} + +/** + * Combines an iterable or record of effects into one effect whose success shape + * follows the input. + * + * **When to use** + * + * Use to run a known collection of effects and collect results in the same + * tuple, iterable, or record shape. + * + * **Details** + * + * Tuple and iterable inputs collect results in order. Record inputs collect + * results under the same keys. By default, the combined effect fails on the + * first failure; with concurrent execution, effects that have already started + * may be interrupted, while effects not yet started are skipped. + * + * Options: + * + * Use `concurrency` to control sequential or concurrent execution. Use + * `mode: "result"` to run every effect and collect each success or failure as a + * `Result` in the same output shape. Use `discard: true` to ignore successful + * values and return `void`. + * + * **Example** (Collecting tuple results in order) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const tupleOfEffects = [ + * Effect.succeed(42).pipe(Effect.tap(Console.log)), + * Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * ] as const + * + * // ┌─── Effect<[number, string], never, never> + * // ▼ + * const resultsAsTuple = Effect.all(tupleOfEffects) + * + * Effect.runPromise(resultsAsTuple).then(console.log) + * // Output: + * // 42 + * // Hello + * // [ 42, 'Hello' ] + * ``` + * + * **Example** (Collecting iterable results in order) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const iterableOfEffects: Iterable> = [1, 2, 3].map( + * (n) => Effect.succeed(n).pipe(Effect.tap(Console.log)) + * ) + * + * // ┌─── Effect + * // ▼ + * const resultsAsArray = Effect.all(iterableOfEffects) + * + * Effect.runPromise(resultsAsArray).then(console.log) + * // Output: + * // 1 + * // 2 + * // 3 + * // [ 1, 2, 3 ] + * ``` + * + * **Example** (Collecting struct results by key) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const structOfEffects = { + * a: Effect.succeed(42).pipe(Effect.tap(Console.log)), + * b: Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * } + * + * // ┌─── Effect<{ a: number; b: string; }, never, never> + * // ▼ + * const resultsAsStruct = Effect.all(structOfEffects) + * + * Effect.runPromise(resultsAsStruct).then(console.log) + * // Output: + * // 42 + * // Hello + * // { a: 42, b: 'Hello' } + * ``` + * + * **Example** (Collecting record results by key) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const recordOfEffects: Record> = { + * key1: Effect.succeed(1).pipe(Effect.tap(Console.log)), + * key2: Effect.succeed(2).pipe(Effect.tap(Console.log)) + * } + * + * // ┌─── Effect<{ [x: string]: number; }, never, never> + * // ▼ + * const resultsAsRecord = Effect.all(recordOfEffects) + * + * Effect.runPromise(resultsAsRecord).then(console.log) + * // Output: + * // 1 + * // 2 + * // { key1: 1, key2: 2 } + * ``` + * + * **Example** (Stopping on the first failure) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.all([ + * Effect.succeed("Task1").pipe(Effect.tap(Console.log)), + * Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), + * // Won't execute due to earlier failure + * Effect.succeed("Task3").pipe(Effect.tap(Console.log)) + * ]) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: + * // Task1 + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Task2: Oh no!' } + * // } + * ``` + * + * @see {@link forEach} for iterating over elements and applying an effect. + * @category collecting + * @since 2.0.0 + */ +export const all: < + const Arg extends + | Iterable> + | Record>, + O extends { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "result" | undefined + } +>( + arg: Arg, + options?: O +) => All.Return = internal.all + +/** + * Applies an effectful function to each element and partitions failures and + * successes. + * + * **Details** + * + * The returned tuple is `[excluded, satisfying]`, where `excluded` contains + * all failures and `satisfying` contains all successes. + * + * This function runs every effect and never fails. Use `concurrency` to control + * parallelism. + * + * **Example** (Separating successes and failures) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.partition([0, 1, 2, 3], (n) => + * n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n) + * ) + * + * Effect.runPromise(program).then(console.log) + * // [ ["0 is even", "2 is even"], [1, 3] ] + * ``` + * + * @category collecting + * @since 2.0.0 + */ +export const partition: { + ( + f: (a: A, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): (elements: Iterable) => Effect<[excluded: Array, satisfying: Array], never, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect<[excluded: Array, satisfying: Array], never, R> +} = internal.partition + +/** + * Applies an effectful function to each element and accumulates all failures. + * + * **Details** + * + * This function always evaluates every element. If at least one effect fails, + * all failures are returned as a non-empty array and successes are discarded. + * If all effects succeed, it returns all collected successes. + * + * Use `discard: true` to ignore successful values while still validating all + * elements. + * + * **Example** (Validating every element) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.validate([0, 1, 2, 3], (n) => + * n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // reasons: [ + * // { _id: 'Reason', _tag: 'Fail', error: '0 is even' }, + * // { _id: 'Reason', _tag: 'Fail', error: '2 is even' } + * // ] + * // } + * // } + * ``` + * + * @category error accumulation + * @since 2.0.0 + */ +export const validate: { + ( + f: (a: A, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } | undefined + ): (elements: Iterable) => Effect, Arr.NonEmptyArray, R> + ( + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): (elements: Iterable) => Effect, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } | undefined + ): Effect, Arr.NonEmptyArray, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Effect, R> +} = internal.validate + +/** + * Returns the first element that satisfies an effectful predicate. + * + * **Details** + * + * The predicate receives the element and its index. Evaluation short-circuits + * as soon as an element matches. + * + * **Example** (Finding the first successful match) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2)) + * + * Effect.runPromise(program).then(console.log) + * // { _id: 'Option', _tag: 'Some', value: 3 } + * ``` + * + * @category collecting + * @since 2.0.0 + */ +export const findFirst: { + ( + predicate: (a: NoInfer, i: number) => Effect + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect + ): Effect, E, R> +} = internal.findFirst + +/** + * Returns the first value that passes an effectful `FilterEffect`. + * + * **When to use** + * + * Use when you need to find the first element that satisfies an effectful + * filter returning a `Result`, which also transforms the matching element. + * + * **Details** + * + * The filter receives the element and index. Evaluation short-circuits on the + * first `Result.succeed` and returns the transformed value in `Option.some`. + * + * @see {@link findFirst} for the simpler effectful predicate-based variant + * + * @category collecting + * @since 4.0.0 + */ +export const findFirstFilter: { + ( + filter: (input: NoInfer, i: number) => Effect, E, R> + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + filter: (input: NoInfer, i: number) => Effect, E, R> + ): Effect, E, R> +} = internal.findFirstFilter + +/** + * Executes an effectful operation for each element in an `Iterable`. + * + * **When to use** + * + * Use to traverse an iterable with an effectful function while preserving + * element order in the collected results. + * + * **Details** + * + * The `forEach` function applies a provided operation to each element in the + * iterable, producing a new effect that returns an array of results. + * + * If any effect fails, the iteration stops immediately (short-circuiting), and + * the error is propagated. + * + * Concurrency: + * + * The `concurrency` option controls how many operations are performed + * concurrently. By default, the operations are performed sequentially. + * + * Discarding Results: + * + * If the `discard` option is set to `true`, the intermediate results are not + * collected, and the final result of the operation is `void`. + * + * **Example** (Mapping over an iterable with effects) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const result = Effect.forEach( + * [1, 2, 3, 4, 5], + * (n, index) => + * Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)) + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: + * // Currently at index 0 + * // Currently at index 1 + * // Currently at index 2 + * // Currently at index 3 + * // Currently at index 4 + * // [ 2, 4, 6, 8, 10 ] + * ``` + * + * **Example** (Running effects without collecting results) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Apply effects but discard the results + * const result = Effect.forEach( + * [1, 2, 3, 4, 5], + * (n, index) => + * Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)), + * { discard: true } + * ) + * + * Effect.runPromise(result).then(console.log) + * // Output: + * // Currently at index 0 + * // Currently at index 1 + * // Currently at index 2 + * // Currently at index 3 + * // Currently at index 4 + * // undefined + * ``` + * + * @see {@link all} for combining multiple effects into one. + * @category collecting + * @since 2.0.0 + */ +export const forEach: { + , const Discard extends boolean = false>( + f: (a: Arr.ReadonlyArray.Infer, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined; readonly discard?: Discard | undefined } | undefined + ): (self: S) => Effect : void, E, R> + , const Discard extends boolean = false>( + self: S, + f: (a: Arr.ReadonlyArray.Infer, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined; readonly discard?: Discard | undefined } | undefined + ): Effect : void, E, R> +} = internal.forEach + +/** + * Executes a body effect repeatedly while a condition holds true. + * + * **Example** (Repeating an effectful loop) + * + * ```ts + * import { Effect } from "effect" + * + * let counter = 0 + * + * const program = Effect.whileLoop({ + * while: () => counter < 5, + * body: () => Effect.sync(() => ++counter), + * step: (n) => console.log(`Current count: ${n}`) + * }) + * + * Effect.runPromise(program) + * // Output: + * // Current count: 1 + * // Current count: 2 + * // Current count: 3 + * // Current count: 4 + * // Current count: 5 + * ``` + * + * @category collecting + * @since 2.0.0 + */ +export const whileLoop: (options: { + readonly while: LazyArg + readonly body: LazyArg> + readonly step: (a: A) => void +}) => Effect = internal.whileLoop + +// ----------------------------------------------------------------------------- +// Creating Effects +// ----------------------------------------------------------------------------- + +/** + * Creates an `Effect` that represents an asynchronous computation guaranteed to + * succeed. + * + * **When to use** + * + * Use to convert a `Promise` into an `Effect` when the async operation is + * guaranteed to succeed and will not reject. + * + * **Details** + * + * An optional `AbortSignal` can be provided to allow for interruption of the + * wrapped `Promise` API. + * + * **Gotchas** + * + * The `Promise` must not reject. If it rejects, the rejection is treated as a + * defect, not as a typed failure. Use `tryPromise` when rejection is expected. + * + * Interruption aborts the provided `AbortSignal`, but the underlying + * asynchronous operation only stops if it observes that signal. + * + * **Example** (Wrapping a non-rejecting Promise) + * + * ```ts + * import { Effect } from "effect" + * + * const delay = (message: string) => + * Effect.promise( + * () => + * new Promise((resolve) => { + * setTimeout(() => { + * resolve(message) + * }, 2000) + * }) + * ) + * + * // ┌─── Effect + * // ▼ + * const program = delay("Async operation completed successfully!") + * ``` + * + * @see {@link tryPromise} for a version that can handle failures. + * @category constructors + * @since 2.0.0 + */ +export const promise: ( + evaluate: (signal: AbortSignal) => PromiseLike +) => Effect = internal.promise + +/** + * Creates an `Effect` from an asynchronous computation that may throw or + * reject, mapping failures into the error channel. + * + * **When to use** + * + * Use when you need to perform asynchronous operations that might fail, such + * as fetching data from an API, and want thrown exceptions or rejected promises + * captured as Effect errors. + * + * **Details** + * + * The promise thunk is evaluated when the effect runs. If it returns a promise + * that resolves, the resolved value becomes the success value. If the thunk + * throws before returning a promise, or if the returned promise rejects, the + * thrown or rejected value is mapped into the error channel. + * + * Passing the thunk directly maps failures to {@link Cause.UnknownError}. + * Passing `{ try, catch }` uses `catch` to map failures to an error of type + * `E`. + * + * The thunk receives an `AbortSignal` that is aborted if the effect is + * interrupted. The underlying asynchronous operation only stops if it observes + * that signal. + * + * **Gotchas** + * + * If `catch` throws while mapping the error, that thrown value is treated as a + * defect. Return the error value you want in the error channel instead of + * throwing it. + * + * **Example** (Wrapping a fetch request that may fail) + * + * ```ts + * import { Effect } from "effect" + * + * const getTodo = (id: number) => + * // Will catch any errors and propagate them as UnknownError + * Effect.tryPromise(() => + * fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) + * ) + * + * // ┌─── Effect + * // ▼ + * const program = getTodo(1) + * ``` + * + * **Example** (Mapping Promise rejections to a tagged error) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class TodoFetchError extends Data.TaggedError("TodoFetchError")<{ readonly cause: unknown }> {} + * + * const getTodo = (id: number) => + * Effect.tryPromise({ + * try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`), + * // remap the error + * catch: (cause) => new TodoFetchError({ cause }) + * }) + * + * // ┌─── Effect + * // ▼ + * const program = getTodo(1) + * ``` + * + * @see {@link promise} if the effectful computation is asynchronous and does not throw errors. + * @category constructors + * @since 2.0.0 + */ +export const tryPromise: ( + options: + | { readonly try: (signal: AbortSignal) => PromiseLike; readonly catch: (error: unknown) => E } + | ((signal: AbortSignal) => PromiseLike) +) => Effect = internal.tryPromise + +/** + * Creates an `Effect` that always succeeds with a given value. + * + * **When to use** + * + * Use when an effect should complete successfully with a specific value without any errors + * or external dependencies. + * + * **Example** (Creating a successful effect) + * + * ```ts + * import { Effect } from "effect" + * + * // Creating an effect that represents a successful scenario + * // + * // ┌─── Effect + * // ▼ + * const success = Effect.succeed(42) + * ``` + * + * @see {@link fail} to create an effect that represents a failure. + * @category constructors + * @since 2.0.0 + */ +export const succeed: (value: A) => Effect = internal.succeed + +/** + * Returns an effect which succeeds with `None`. + * + * **Example** (Succeeding with Option.none) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.succeedNone + * + * Effect.runPromise(program).then(console.log) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const succeedNone: Effect> = internal.succeedNone + +/** + * Returns an effect which succeeds with the value wrapped in a `Some`. + * + * **Example** (Succeeding with Option.some) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.succeedSome(42) + * + * Effect.runPromise(program).then(console.log) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const succeedSome: (value: A) => Effect> = internal.succeedSome + +/** + * Creates an `Effect` lazily, delaying construction until it is needed. + * + * **When to use** + * + * Use when you need to defer the evaluation of an effect until it is required. + * + * **Details** + * + * `suspend` takes a thunk that represents an effect and delays creating it + * until the suspended effect is evaluated. This is useful for optimizing + * expensive computations, managing circular dependencies such as recursive + * functions, and helping TypeScript unify return types when branches construct + * different effects. Any side effects or scoped captures inside the thunk are + * re-executed on each invocation. + * + * **Example** (Lazily evaluating side effects) + * + * ```ts + * import { Effect } from "effect" + * + * let i = 0 + * + * const bad = Effect.succeed(i++) + * + * const good = Effect.suspend(() => Effect.succeed(i++)) + * + * console.log(Effect.runSync(bad)) // Output: 0 + * console.log(Effect.runSync(bad)) // Output: 0 + * + * console.log(Effect.runSync(good)) // Output: 1 + * console.log(Effect.runSync(good)) // Output: 2 + * ``` + * + * **Example** (Suspending recursive Fibonacci evaluation) + * + * ```ts + * import { Effect } from "effect" + * + * const blowsUp = (n: number): Effect.Effect => + * n < 2 + * ? Effect.succeed(1) + * : Effect.zipWith(blowsUp(n - 1), blowsUp(n - 2), (a, b) => a + b) + * + * // console.log(Effect.runSync(blowsUp(32))) + * // crash: JavaScript heap out of memory + * + * const allGood = (n: number): Effect.Effect => + * n < 2 + * ? Effect.succeed(1) + * : Effect.zipWith( + * Effect.suspend(() => allGood(n - 1)), + * Effect.suspend(() => allGood(n - 2)), + * (a, b) => a + b + * ) + * + * console.log(Effect.runSync(allGood(32))) + * // Output: 3524578 + * ``` + * + * **Example** (Helping TypeScript infer recursive effect types) + * + * ```ts + * import { Effect } from "effect" + * + * // Without suspend, TypeScript may struggle with type inference. + * // Inferred type: + * // (a: number, b: number) => + * // Effect | Effect + * const withoutSuspend = (a: number, b: number) => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // Using suspend to unify return types. + * // Inferred type: + * // (a: number, b: number) => Effect + * const withSuspend = (a: number, b: number) => + * Effect.suspend(() => + * b === 0 + * ? Effect.fail(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const suspend: ( + effect: LazyArg> +) => Effect = internal.suspend + +/** + * Creates an `Effect` that represents a synchronous side-effectful computation. + * + * **When to use** + * + * Use when you need to wrap a synchronous side-effectful operation that is not + * expected to throw. + * + * **Details** + * + * The provided function is evaluated lazily when the effect runs. + * + * **Gotchas** + * + * The function must not throw. If it throws, the thrown value is treated as a + * defect, not as a typed failure. Use `try` when throwing is expected. + * + * **Example** (Capturing synchronous logging in an Effect) + * + * ```ts + * import { Effect } from "effect" + * + * const log = (message: string) => + * Effect.sync(() => { + * console.log(message) // side effect + * }) + * + * // ┌─── Effect + * // ▼ + * const program = log("Hello, World!") + * ``` + * + * @see {@link try_ | try} for a version that can handle failures. + * @category constructors + * @since 2.0.0 + */ +export const sync: (thunk: LazyArg) => Effect = internal.sync + +const void_: Effect = internal.void +export { + /** + * Returns an effect that succeeds with `void`. + * + * @category constructors + * @since 2.0.0 + */ + void_ as void +} + +const undefined_: Effect = internal.undefined +export { + /** + * Returns an effect that succeeds with `undefined`. + * + * @category constructors + * @since 4.0.0 + */ + undefined_ as undefined +} + +/** + * Creates an `Effect` from a callback-based asynchronous API. + * + * **When to use** + * + * Use when you need to integrate APIs that complete through callbacks instead + * of returning a `Promise`. + * + * **Details** + * + * The registration function receives a `resume` callback and, when requested, + * an `AbortSignal`. Call `resume` at most once with the effect that should + * complete the fiber; later calls are ignored. Return an optional cleanup + * effect from the registration function to run if the fiber is interrupted. + * + * **Example** (Integrating callback APIs) + * + * ```ts + * import { Effect } from "effect" + * + * const delay = (ms: number) => + * Effect.callback((resume) => { + * const timeoutId = setTimeout(() => { + * resume(Effect.void) + * }, ms) + * // Cleanup function for interruption + * return Effect.sync(() => clearTimeout(timeoutId)) + * }) + * + * const program = delay(1000) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const callback: ( + register: ( + this: Scheduler, + resume: (effect: Effect) => void, + signal: AbortSignal + ) => void | Effect +) => Effect = internal.callback + +/** + * Returns an effect that will never produce anything. The moral equivalent of + * `while(true) {}`, only without the wasted CPU cycles. + * + * **Example** (Creating a never-ending effect) + * + * ```ts + * import { Effect } from "effect" + * + * // This effect will never complete + * const program = Effect.never + * + * // This will run forever (or until interrupted) + * // Effect.runPromise(program) // Never resolves + * + * // Use with timeout for practical applications + * const timedProgram = Effect.timeout(program, "1 second") + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const never: Effect = internal.never + +/** + * Effect that succeeds with an empty record `{}`, used as the starting point + * for do notation chains. + * + * **Example** (Starting do notation) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const program = pipe( + * Effect.Do, + * Effect.bind("x", () => Effect.succeed(2)), + * Effect.bind("y", ({ x }) => Effect.succeed(x + 1)), + * Effect.let("sum", ({ x, y }) => x + y) + * ) + * ``` + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Effect<{}> = internal.Do + +/** + * Gives a name to the success value of an `Effect`, creating a single-key + * record used in do notation pipelines. + * + * **When to use** + * + * Use to start a do-notation pipeline from an existing `Effect` when its + * success value should become the first named field in the accumulated record. + * + * @see {@link Do} for starting from an empty accumulated record + * @see {@link bind} for adding fields produced by effects + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Effect) => Effect<{ [K in N]: A }, E, R> + (self: Effect, name: N): Effect<{ [K in N]: A }, E, R> +} = internal.bindTo + +const let_: { + , B>( + name: N, + f: (a: NoInfer) => B + ): ( + self: Effect + ) => Effect & Record>, E, R> + , E, R, B, N extends string>( + self: Effect, + name: N, + f: (a: NoInfer) => B + ): Effect & Record>, E, R> +} = internal.let + +export { + /** + * Adds a computed plain value to the do notation record. + * + * **When to use** + * + * Use to add a derived, synchronous value to a do-notation pipeline when it + * depends on fields already accumulated in the record and does not need to run + * another `Effect`. + * + * **Details** + * + * The new field is added with object spreading. If the name already exists in + * the record, the computed value replaces it in the returned type. + * + * @see {@link bind} for adding fields produced by effects + * @see {@link bindTo} for naming an existing success value + * @see {@link Do} for starting from an empty accumulated record + * @see {@link gen} for sequencing without accumulating a record + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Adds an `Effect` value to the do notation record under a given name. + * + * **When to use** + * + * Use to sequence an effectful step in a do-notation pipeline when that step + * depends on fields already accumulated in the record and its success value + * should be stored under a name. + * + * **Details** + * + * The function receives the current record, runs the returned effect after the + * input effect succeeds, and inserts its success value under `name`. The + * resulting effect combines the error and service requirements of both steps. + * + * **Gotchas** + * + * Binding a name that already exists replaces that field in the resulting + * record. + * + * @see {@link Do} for starting from an empty do-notation record + * @see {@link bindTo} for naming the success value of an existing effect + * @see {@link gen} for generator-based sequencing without accumulating a record + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + , B, E2, R2>( + name: N, + f: (a: NoInfer) => Effect + ): ( + self: Effect + ) => Effect & Record>, E | E2, R | R2> + , E, R, B, E2, R2, N extends string>( + self: Effect, + name: N, + f: (a: NoInfer) => Effect + ): Effect & Record>, E | E2, R | R2> +} = internal.bind + +/** + * Provides a way to write effectful code using generator functions, simplifying + * control flow and error handling. + * + * **When to use** + * + * Use when you want to write effectful code that looks and behaves like + * synchronous code, while still handling asynchronous tasks, errors, and complex + * control flow such as loops and conditions. + * + * Generator functions work similarly to `async/await` but keep errors, + * requirements, and interruption in the Effect type. You can `yield*` values + * from effects and return the final result at the end. + * + * **Example** (Sequencing effects with generators) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} + * + * const addServiceCharge = (amount: number) => amount + 1 + * + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new DiscountRateError()) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) + * + * export const program = Effect.gen(function*() { + * const transactionAmount = yield* fetchTransactionAmount + * const discountRate = yield* fetchDiscountRate + * const discountedAmount = yield* applyDiscount( + * transactionAmount, + * discountRate + * ) + * const finalAmount = addServiceCharge(discountedAmount) + * return `Final amount to charge: ${finalAmount}` + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const gen: { + , AEff>( + f: () => Generator + ): Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + , AEff>( + options: { + readonly self: Self + }, + f: (this: Self) => Generator + ): Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > +} = internal.gen + +/** + * Type helpers for `Effect.gen` generator return signatures. + * + * @since 2.0.0 + */ +export declare namespace gen { + /** + * Generator return type accepted by `Effect.gen`. + * + * @category constructors + * @since 4.0.0 + */ + export type Return = Generator, A, any> +} + +/** + * Creates an `Effect` that represents a recoverable error. + * + * **When to use** + * + * Use to explicitly signal a recoverable error in an `Effect`. + * + * **Details** + * + * The error keeps propagating unless it is handled. You can handle tagged + * errors with functions like {@link catchTag} or {@link catchTags}. + * + * **Example** (Creating a failed effect) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} + * + * // ┌─── Effect + * // ▼ + * const failure = Effect.fail( + * new OperationFailedError() + * ) + * ``` + * + * @see {@link succeed} to create an effect that represents a successful value. + * @category constructors + * @since 2.0.0 + */ +export const fail: (error: E) => Effect = internal.fail + +/** + * Creates an `Effect` that represents a recoverable error using a lazy evaluation. + * + * **When to use** + * + * Use to defer computing a recoverable error value until the effect is run. + * + * **Details** + * + * The error-producing function is evaluated each time the effect is executed. + * + * **Example** (Lazily creating failures) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class ProgramError extends Data.TaggedError("ProgramError")<{ readonly failedAt: Date }> {} + * + * const program = Effect.failSync(() => new ProgramError({ failedAt: new Date() })) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failSync: (evaluate: LazyArg) => Effect = internal.failSync + +/** + * Creates an `Effect` that represents a failure with a specific `Cause`. + * + * **When to use** + * + * Use when you already have a full `Cause` and need to preserve defects, + * interruptions, annotations, or combined failures in the effect's failure + * channel. + * + * **Details** + * + * This function allows you to create effects that fail with complex error + * structures, including multiple errors, defects, interruptions, and more. + * + * **Example** (Failing with a full Cause) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const program = Effect.failCause( + * Cause.fail("Network error") + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCause: (cause: Cause.Cause) => Effect = internal.failCause + +/** + * Creates an `Effect` that represents a failure with a `Cause` computed lazily. + * + * **When to use** + * + * Use to defer computing a full `Cause` until the effect is run. + * + * **Details** + * + * The cause-producing function is evaluated each time the effect is executed. + * + * **Example** (Lazily creating a Cause) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const program = Effect.failCauseSync(() => + * Cause.fail("Error computed at runtime") + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCauseSync: ( + evaluate: LazyArg> +) => Effect = internal.failCauseSync + +/** + * Creates an effect that terminates a fiber with a specified error. + * + * **When to use** + * + * Use when you need an `Effect` to report an unrecoverable defect instead of a + * typed error. + * + * **Details** + * + * The `die` function is used to signal a defect, which represents a critical + * and unexpected error in the code. When invoked, it produces an effect that + * does not handle the error and instead terminates the fiber. + * + * The error channel of the resulting effect is of type `never`, indicating that + * it cannot recover from this failure. + * + * **Example** (Failing on division by zero) + * + * ```ts + * import { Effect } from "effect" + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.die(new Error("Cannot divide by zero")) + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = divide(1, 0) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) Error: Cannot divide by zero + * // ...stack trace... + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const die: (defect: unknown) => Effect = internal.die + +const try_: ( + options: { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } | LazyArg +) => Effect = internal.try + +export { + /** + * Creates an `Effect` from a synchronous computation that may throw, mapping + * thrown values into the error channel. + * + * **When to use** + * + * Use when you need to perform synchronous operations that might throw, such + * as parsing JSON, and want thrown exceptions captured as Effect errors. + * + * **Details** + * + * The thunk is evaluated when the effect runs. If it returns normally, the + * returned value becomes the success value. If it throws, the thrown value is + * mapped into the error channel. + * + * Passing the thunk directly maps failures to {@link Cause.UnknownError}. + * Passing `{ try, catch }` uses `catch` to map failures to an error of type + * `E`. + * + * **Gotchas** + * + * If `catch` throws while mapping the error, that thrown value is treated as + * a defect. Return the error value you want in the error channel instead of + * throwing it. + * + * **Example** (Parsing JSON) + * + * ```ts + * import { Effect } from "effect" + * + * const parseJSON = (input: string) => + * Effect.try(() => JSON.parse(input)) + * + * // Success case + * Effect.runPromise(parseJSON("{\"name\": \"Alice\"}")).then(console.log) + * // Output: { name: "Alice" } + * + * // Failure case maps the thrown value to UnknownError + * Effect.runPromiseExit(parseJSON("invalid json")).then(console.log) + * ``` + * + * **Example** (Mapping exceptions to a tagged error) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class JsonParsingError extends Data.TaggedError("JsonParsingError")<{ readonly cause: unknown }> {} + * + * const parseJSON = (input: string) => + * Effect.try({ + * try: () => JSON.parse(input), + * catch: (cause) => new JsonParsingError({ cause }) + * }) + * + * Effect.runPromiseExit(parseJSON("invalid json")).then(console.log) + * // Output: Exit.failure with custom Error message + * ``` + * + * @see {@link sync} if the effectful computation is synchronous and does not + * throw errors. + * @category constructors + * @since 2.0.0 + */ + try_ as try +} + +/** + * Yields control back to the Effect runtime, allowing other fibers to execute. + * + * **Example** (Yielding to other fibers) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * console.log("Before yield") + * yield* Effect.yieldNow + * console.log("After yield") + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const yieldNow: Effect = internal.yieldNow + +/** + * Yields control back to the Effect runtime with a specified priority, allowing other fibers to execute. + * + * **Example** (Yielding with priority) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * console.log("High priority task") + * yield* Effect.yieldNowWith(10) // Higher priority + * console.log("Continued after yield") + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const yieldNowWith: (priority?: number) => Effect = internal.yieldNowWith + +/** + * Provides access to the current fiber within an effect computation. + * + * **Example** (Reading the current fiber) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.withFiber((fiber) => + * Effect.succeed(`Fiber ID: ${fiber.id}`) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: Fiber ID: 1 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const withFiber: ( + evaluate: (fiber: Fiber) => Effect +) => Effect = core.withFiber + +// ----------------------------------------------------------------------------- +// Conversions +// ----------------------------------------------------------------------------- + +/** + * Converts a `Result` to an `Effect`. + * + * **Example** (Converting a Result into an Effect) + * + * ```ts + * import { Effect, Result } from "effect" + * + * const success = Result.succeed(42) + * const failure = Result.fail("Something went wrong") + * + * const effect1 = Effect.fromResult(success) + * const effect2 = Effect.fromResult(failure) + * + * Effect.runPromise(effect1).then(console.log) // 42 + * Effect.runPromiseExit(effect2).then(console.log) + * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' } } + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const fromResult: (result: Result.Result) => Effect = internal.fromResult + +/** + * Converts an `Option` into an `Effect`. + * + * **When to use** + * + * Use when absence should become a typed `NoSuchElementError` in the effect error + * channel. + * + * **Details** + * + * `Option.some` becomes a successful effect with the contained value, while + * `Option.none` becomes a failed effect. By default the failure is a + * `NoSuchElementError`, but you can provide an `onNone` callback to customize + * the error value. + * + * **Example** (Converting an Option into an Effect) + * + * ```ts + * import { Effect, Option } from "effect" + * + * const some = Option.some(42) + * const none = Option.none() + * + * const effect1 = Effect.fromOption(some) + * const effect2 = Effect.fromOption(none) + * const effect3 = Effect.fromOption(none, () => new Error("missing")) + * + * Effect.runPromise(effect1).then(console.log) // 42 + * Effect.runPromiseExit(effect2).then(console.log) + * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _id: 'NoSuchElementError' } } } + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const fromOption: | LazyArg, E = Cause.NoSuchElementError>( + arg: Arg, + ...rest: [Arg] extends [Option] ? [onNone?: LazyArg] : [] +) => [Arg] extends [Option] ? Effect + : [Arg] extends [LazyArg] ? (option: Option) => Effect + : never = internal.fromOption + +/** + * Converts an `Option` of an `Effect` into an `Effect` of an `Option`. + * + * **When to use** + * + * Use when an effect should run only when an optional value is present, while + * preserving absence as a successful `None`. + * + * **Details** + * + * - `None` becomes an effect that succeeds with `None` + * - `Some(effect)` runs the inner effect and wraps its success value in `Some` + * - Inner failures are preserved in the resulting effect + * + * **Example** (Transposing an Option of an Effect) + * + * ```ts + * import { Effect, Option } from "effect" + * + * const some = Option.some(Effect.succeed(42)) + * + * // ┌─── Effect, never, never> + * // ▼ + * const program = Effect.transposeOption(some) + * + * Effect.runPromise(program).then(console.log) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @category converting + * @since 3.13.0 + */ +export const transposeOption: ( + self: Option> +) => Effect, E, R> = internal.transposeOption + +/** + * Converts a nullable value to an `Effect`, failing with a `NoSuchElementError` + * when the value is `null` or `undefined`. + * + * **Example** (Failing on nullish values) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.fn(function*(input: string | null) { + * const value = yield* Effect.fromNullishOr(input) + * yield* Console.log(value) + * }, + * Effect.catch(() => Console.log("missing")) + * ) + * + * Effect.runPromise(program(null)) + * // Output: missing + * Effect.runPromise(program("hello")) + * // Output: hello + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const fromNullishOr: (value: A) => Effect, Cause.NoSuchElementError> = internal.fromNullishOr + +// ----------------------------------------------------------------------------- +// Mapping +// ----------------------------------------------------------------------------- + +/** + * Chains effects to produce new `Effect` instances, useful for combining + * operations that depend on previous results. + * + * **When to use** + * + * Use when you need to chain multiple effects, ensuring that each + * step produces a new `Effect` while flattening any nested effects that may + * occur. + * + * **Details** + * + * `flatMap` lets you sequence effects so that the result of one effect can be + * used in the next step. It is similar to `flatMap` used with arrays but works + * specifically with `Effect` instances, allowing you to avoid deeply nested + * effect structures. + * + * Since effects are immutable, `flatMap` always returns a new effect instead of + * changing the original one. + * + * **Example** (Choosing flatMap syntax variants) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const myEffect = Effect.succeed(1) + * const transformation = (n: number) => Effect.succeed(n + 1) + * + * const flatMappedWithPipe = pipe(myEffect, Effect.flatMap(transformation)) + * const flatMappedWithDataFirst = Effect.flatMap(myEffect, transformation) + * const flatMappedWithMethod = myEffect.pipe(Effect.flatMap(transformation)) + * ``` + * + * **Example** (Sequencing dependent effects) + * + * ```ts + * import { Data, Effect, pipe } from "effect" + * + * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new DiscountRateError()) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * // Chaining the fetch and discount application using `flatMap` + * const finalAmount = pipe( + * fetchTransactionAmount, + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: 95 + * ``` + * + * @see {@link tap} for a version that ignores the result of the effect. + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (a: A) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (a: A) => Effect + ): Effect +} = internal.flatMap + +/** + * Flattens an `Effect` that produces another `Effect` into a single effect. + * + * **Example** (Flattening nested effects) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const nested = Effect.succeed(Effect.succeed("hello")) + * + * const program = Effect.gen(function*() { + * const value = yield* Effect.flatten(nested) + * yield* Console.log(value) + * // Output: hello + * }) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten: (self: Effect, E2, R2>) => Effect = + internal.flatten + +/** + * Runs this effect and then runs another effect, optionally using the first + * effect's success value to choose the next effect. + * + * **When to use** + * + * Use when you need one effect to run after another and the second effect may + * depend on the first effect's success value. + * + * **Details** + * + * When the second argument is an `Effect`, the first success value is discarded + * and the returned effect produces the second effect's value. When the second + * argument is a function, it receives the first success value and must return + * the next `Effect`. + * + * Failures or requirements from either effect are preserved in the returned + * effect. + * + * **Example** (Choosing andThen syntax variants) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const myEffect = Effect.succeed(1) + * const anotherEffect = Effect.succeed("done") + * + * const transformedWithPipe = pipe(myEffect, Effect.andThen(anotherEffect)) + * const transformedWithDataFirst = Effect.andThen(myEffect, anotherEffect) + * const transformedWithMethod = myEffect.pipe(Effect.andThen(anotherEffect)) + * ``` + * + * **Example** (Sequencing a discount calculation after fetching a total) + * + * ```ts + * import { Data, Effect, pipe } from "effect" + * + * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new DiscountRateError()) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * // Using Effect.map and Effect.flatMap + * const result1 = pipe( + * fetchTransactionAmount, + * Effect.map((amount) => amount * 2), + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(result1).then(console.log) + * // Output: 190 + * + * // Using Effect.andThen + * const result2 = pipe( + * fetchTransactionAmount, + * Effect.andThen((amount) => Effect.succeed(amount * 2)), + * Effect.andThen((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(result2).then(console.log) + * // Output: 190 + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const andThen: { + ( + f: (a: A) => Effect + ): (self: Effect) => Effect + ( + f: Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (a: A) => Effect + ): Effect + ( + self: Effect, + f: Effect + ): Effect +} = internal.andThen + +/** + * Runs a side effect with the result of an effect without changing the original + * value. + * + * **When to use** + * + * Use when you need to run an effectful observation, such as logging or + * tracking, while passing the original success value to the next step. + * + * **Details** + * + * `tap` works similarly to `flatMap`, but it ignores the result of the function + * passed to it. The value from the previous effect remains available for the + * next part of the chain. Note that if the side effect fails, the entire chain + * will fail too. + * + * **Example** (Logging a step in a pipeline) + * + * ```ts + * import { Console, Data, Effect, pipe } from "effect" + * + * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} + * + * // Function to apply a discount safely to a transaction amount + * const applyDiscount = ( + * total: number, + * discountRate: number + * ): Effect.Effect => + * discountRate === 0 + * ? Effect.fail(new DiscountRateError()) + * : Effect.succeed(total - (total * discountRate) / 100) + * + * // Simulated asynchronous task to fetch a transaction amount from database + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const finalAmount = pipe( + * fetchTransactionAmount, + * // Log the fetched transaction amount + * Effect.tap((amount) => Console.log(`Apply a discount to: ${amount}`)), + * // `amount` is still available! + * Effect.flatMap((amount) => applyDiscount(amount, 5)) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: + * // Apply a discount to: 100 + * // 95 + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tap: { + ( + f: (a: NoInfer) => Effect + ): (self: Effect) => Effect + ( + f: Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (a: NoInfer) => Effect + ): Effect + ( + self: Effect, + f: Effect + ): Effect +} = internal.tap + +/** + * Converts both success and failure of an `Effect` into a `Result` type. + * + * **When to use** + * + * Use when you want an `Effect`'s typed failures to be handled as `Result` + * data while preserving the original error value. + * + * **Details** + * + * This function converts an effect that may fail into an effect that always + * succeeds, wrapping the outcome in a `Result` type. The result will be + * `Result.Failure` if the effect fails, containing the recoverable error, or + * `Result.Success` if it succeeds, containing the result. + * + * Using this function, you can handle recoverable errors explicitly without + * causing the effect to fail. This is particularly useful in scenarios where + * you want to chain effects and manage both success and failure in the same + * logical flow. + * + * The resulting effect cannot fail directly because all recoverable failures + * are represented inside the `Result` type. + * + * **Gotchas** + * + * `result` only captures typed, recoverable failures. Defects and + * interruptions are not captured inside the `Result` and still fail the + * effect. + * + * **Example** (Capturing success or failure as Result) + * + * ```ts + * import { Effect } from "effect" + * + * const success = Effect.succeed(42) + * const failure = Effect.fail("Something went wrong") + * + * const program1 = Effect.result(success) + * const program2 = Effect.result(failure) + * + * Effect.runPromise(program1).then(console.log) + * // { _id: 'Result', _tag: 'Success', value: 42 } + * + * Effect.runPromise(program2).then(console.log) + * // { _id: 'Result', _tag: 'Failure', error: 'Something went wrong' } + * ``` + * + * @see {@link option} for a version that uses `Option` instead. + * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`. + * + * @category outcome encapsulation + * @since 4.0.0 + */ +export const result: (self: Effect) => Effect, never, R> = internal.result + +/** + * Converts success to `Option.some` and failure to `Option.none`. + * + * **When to use** + * + * Use when you only care whether an effect succeeds and want recoverable + * failures represented as `Option.none`. + * + * **Details** + * + * Success values become `Option.some`, recoverable failures become + * `Option.none`, and defects still fail the effect. + * + * **Gotchas** + * + * `option` only captures typed, recoverable failures as `Option.none`. + * Defects and interruptions are not captured inside the `Option` and still + * fail the effect. + * + * `option` also discards typed failure values. Use `result` if the failure + * value matters. + * + * **Example** (Capturing success or failure as Option) + * + * ```ts + * import { Console, Effect, Option } from "effect" + * + * const program = Effect.gen(function*() { + * const someValue = yield* Effect.option(Effect.succeed(1)) + * const noneValue = yield* Effect.option(Effect.fail("missing")) + * + * yield* Console.log(Option.isSome(someValue)) + * yield* Console.log(Option.isNone(noneValue)) + * }) + * + * Effect.runPromise(program) + * // true + * // true + * ``` + * + * @see {@link result} for a version that uses `Result` instead. + * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`. + * + * @category outcome encapsulation + * @since 2.0.0 + */ +export const option: (self: Effect) => Effect, never, R> = internal.option + +/** + * Transforms an effect to encapsulate both failure and success using the `Exit` + * data type. + * + * **When to use** + * + * Use when you need to inspect the full outcome, including typed failures, defects, + * and interruptions. + * + * **Details** + * + * `exit` wraps an effect's success or failure inside an `Exit` type, allowing + * you to handle both cases explicitly. + * + * The resulting effect cannot fail because the failure is encapsulated within + * the `Exit.Failure` type. The error type is set to `never`, indicating that + * the effect is structured to never fail directly. + * + * **Example** (Capturing completion as Exit) + * + * ```ts + * import { Effect } from "effect" + * + * const success = Effect.succeed(42) + * const failure = Effect.fail("Something went wrong") + * + * const program1 = Effect.exit(success) + * const program2 = Effect.exit(failure) + * + * Effect.runPromise(program1).then(console.log) + * // { _id: 'Exit', _tag: 'Success', value: 42 } + * + * Effect.runPromise(program2).then(console.log) + * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' } } + * ``` + * + * @see {@link option} for a version that uses `Option` instead. + * @see {@link result} for a version that uses `Result` instead. + * + * @category outcome encapsulation + * @since 2.0.0 + */ +export const exit: ( + self: Effect +) => Effect, never, R> = internal.exit + +/** + * Transforms the value inside an effect by applying a function to it. + * + * **When to use** + * + * Use to transform an effect's success value with a function that returns a + * plain value, producing a new effect without changing the original effect's + * typed error or context requirements. + * + * **Details** + * + * `map` takes a function and applies it to the value contained within an + * effect, creating a new effect with the transformed value. + * + * It's important to note that effects are immutable, meaning that the original + * effect is not modified. Instead, a new effect is returned with the updated + * value. + * + * **Example** (Choosing map syntax variants) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const myEffect = Effect.succeed(1) + * const transformation = (n: number) => n + 1 + * + * const mappedWithPipe = pipe(myEffect, Effect.map(transformation)) + * const mappedWithDataFirst = Effect.map(myEffect, transformation) + * const mappedWithMethod = myEffect.pipe(Effect.map(transformation)) + * ``` + * + * **Example** (Adding a service charge) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * const addServiceCharge = (amount: number) => amount + 1 + * + * const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) + * + * const finalAmount = pipe( + * fetchTransactionAmount, + * Effect.map(addServiceCharge) + * ) + * + * Effect.runPromise(finalAmount).then(console.log) + * // Output: 101 + * ``` + * + * @see {@link mapError} for a version that operates on the error channel. + * @see {@link mapBoth} for a version that operates on both channels. + * @see {@link flatMap} or {@link andThen} for a version that can return a new effect. + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => B): (self: Effect) => Effect + (self: Effect, f: (a: A) => B): Effect +} = internal.map + +/** + * Replaces the value inside an effect with a constant value. + * + * **When to use** + * + * Use to replace a successful value with a constant while preserving failures + * and requirements. + * + * **Details** + * + * `as` allows you to ignore the original value inside an effect and + * replace it with a new constant value. + * + * **Example** (Replacing a success value) + * + * ```ts + * import { Effect, pipe } from "effect" + * + * // Replaces the value 5 with the constant "new value" + * const program = pipe(Effect.succeed(5), Effect.as("new value")) + * + * Effect.runPromise(program).then(console.log) + * // Output: "new value" + * ``` + * + * @see {@link map} for deriving the replacement value from the success value + * @see {@link asVoid} for replacing the success value with `void` + * + * @category mapping + * @since 2.0.0 + */ +export const as: { + (value: B): (self: Effect) => Effect + (self: Effect, value: B): Effect +} = internal.as + +/** + * Maps the success value of an `Effect` to `Some`, preserving failures. + * + * **Example** (Wrapping success in Option.some) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.asSome(Effect.succeed(42)) + * + * Effect.runPromise(program).then(console.log) + * // { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const asSome: (self: Effect) => Effect, E, R> = internal.asSome + +/** + * Maps the success value of an `Effect` to `void`, preserving failures. + * + * **Example** (Discarding success values) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.asVoid(Effect.succeed(42)) + * + * Effect.runPromise(program).then(console.log) + * // undefined (void) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const asVoid: (self: Effect) => Effect = internal.asVoid + +/** + * Swaps an effect's success and failure channels. + * + * **When to use** + * + * Use to swap an `Effect`'s success and failure channels. + * + * **Details** + * + * For an `Effect`, the returned effect has type `Effect`. + * + * **Example** (Swapping success and failure channels) + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) + * + * // ┌─── Effect + * // ▼ + * const flipped = Effect.flip(program) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const flip: (self: Effect) => Effect = internal.flip + +// ----------------------------------------------------------------------------- +// Zipping +// ----------------------------------------------------------------------------- + +/** + * Combines two effects into a single effect, producing a tuple with the results of both effects. + * + * **When to use** + * + * Use to combine exactly two effects into a tuple. + * + * **Details** + * + * The `zip` function executes the first effect (left) and then the second effect (right). + * Once both effects succeed, their results are combined into a tuple. + * + * Concurrency: + * + * By default, `zip` processes the effects sequentially. To execute the effects concurrently, + * use the `{ concurrent: true }` option. + * + * **Example** (Combining two effects sequentially) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * // Combine the two effects together + * // + * // ┌─── Effect<[number, string], never, never> + * // ▼ + * const program = Effect.zip(task1, task2) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // [ 1, 'hello' ] + * ``` + * + * **Example** (Combining two effects concurrently) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * // Run both effects concurrently using the concurrent option + * const program = Effect.zip(task1, task2, { concurrent: true }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="task2 done" + * // timestamp=... level=INFO fiber=#0 message="task1 done" + * // [ 1, 'hello' ] + * ``` + * + * @see {@link zipWith} for a version that combines the results with a custom function. + * @see {@link all} for collecting a larger structure of effects. + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + ( + that: Effect, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): (self: Effect) => Effect<[A, A2], E2 | E, R2 | R> + ( + self: Effect, + that: Effect, + options?: { readonly concurrent?: boolean | undefined } + ): Effect<[A, A2], E | E2, R | R2> +} = internal.zip + +/** + * Combines two effects sequentially and applies a function to their results to + * produce a single value. + * + * **When to use** + * + * Use when you need to run two effects sequentially and combine their results + * with a function instead of keeping the results as a tuple. + * + * **Details** + * + * Concurrency: + * + * By default, the effects are run sequentially. To execute them concurrently, + * use the `{ concurrent: true }` option. + * + * **Example** (Combining two success values with a function) + * + * ```ts + * import { Effect } from "effect" + * + * const task1 = Effect.succeed(1).pipe( + * Effect.delay("200 millis"), + * Effect.tap(Effect.log("task1 done")) + * ) + * const task2 = Effect.succeed("hello").pipe( + * Effect.delay("100 millis"), + * Effect.tap(Effect.log("task2 done")) + * ) + * + * const task3 = Effect.zipWith( + * task1, + * task2, + * // Combines results into a single value + * (number, string) => number + string.length + * ) + * + * Effect.runPromise(task3).then(console.log) + * // Output: + * // timestamp=... level=INFO fiber=#3 message="task1 done" + * // timestamp=... level=INFO fiber=#2 message="task2 done" + * // 6 + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + ( + that: Effect, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): Effect +} = internal.zipWith + +// ----------------------------------------------------------------------------- +// Error handling +// ----------------------------------------------------------------------------- + +const catch_: { + ( + f: (e: E) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (e: E) => Effect + ): Effect +} = internal.catch_ + +export { + /** + * Handles all errors in an effect by providing a fallback effect. + * + * **When to use** + * + * Use when every recoverable error from an effect should be handled by the same + * fallback function while unrecoverable defects remain defects. + * + * **Details** + * + * The `catch` function catches any errors that may occur during the + * execution of an effect and allows you to handle them by specifying a fallback + * effect. This ensures that the program continues without failing by recovering + * from errors using the provided fallback logic. + * + * **Gotchas** + * + * `catch` only handles recoverable errors. It will not recover from + * unrecoverable defects. + * + * @see {@link catchCause} for a version that can recover from both recoverable and unrecoverable errors. + * + * @category error handling + * @since 4.0.0 + */ + catch_ as catch +} + +/** + * Catches and handles specific errors by their `_tag` field, which is used as a + * discriminator. + * + * **When to use** + * + * Use when you need to recover from one specific tagged error in an effect + * error channel. + * + * **Details** + * + * The error type must have a readonly `_tag` field. `catchTag` matches that + * field and only handles errors with the requested tag. + * + * **Example** (Handling a tagged error) + * + * ```ts + * import { Effect } from "effect" + * + * class NetworkError { + * readonly _tag = "NetworkError" + * constructor(readonly message: string) {} + * } + * + * class ValidationError { + * readonly _tag = "ValidationError" + * constructor(readonly message: string) {} + * } + * + * declare const task: Effect.Effect + * + * const program = Effect.catchTag( + * task, + * "NetworkError", + * (error) => Effect.succeed(`Recovered from network error: ${error.message}`) + * ) + * ``` + * + * @see {@link catchTags} for handling multiple tagged errors in one call + * @see {@link catchIf} for recovering from errors that match a predicate + * + * @category error handling + * @since 2.0.0 + */ +export const catchTag: { + < + const K extends Tags | Arr.NonEmptyReadonlyArray>, + E, + A1, + E1, + R1, + A2 = unassigned, + E2 = never, + R2 = never + >( + k: K, + f: (e: ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K>) => Effect, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Effect) + | undefined + ): ( + self: Effect + ) => Effect< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Effect) + | undefined + ): Effect< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > +} = internal.catchTag + +/** + * Handles multiple errors in a single block of code using their `_tag` field. + * + * **When to use** + * + * Use when one recovery step should handle several tagged error types by + * matching their readonly `_tag` fields. + * + * **Details** + * + * Pass a handler table whose keys are tags, plus an optional fallback for + * unmatched errors. + * + * The error type must have a readonly `_tag` field to use `catchTags`. This + * field is used to identify and match errors. + * + * **Example** (Handling multiple tagged errors) + * + * ```ts + * import { Data, Effect } from "effect" + * + * // Define tagged error types + * class ValidationError extends Data.TaggedError("ValidationError")<{ + * message: string + * }> {} + * + * class NetworkError extends Data.TaggedError("NetworkError")<{ + * statusCode: number + * }> {} + * + * // An effect that might fail with multiple error types + * declare const program: Effect.Effect + * + * // Handle multiple error types at once + * const handled = Effect.catchTags(program, { + * ValidationError: (error) => + * Effect.succeed(`Validation failed: ${error.message}`), + * NetworkError: (error) => Effect.succeed(`Network error: ${error.statusCode}`) + * }) + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchTags: { + < + E, + Cases extends + & { [K in Extract["_tag"]]+?: ((error: Extract) => Effect) } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }), + A2 = unassigned, + E2 = never, + R2 = never + >( + cases: Cases, + orElse?: ((e: Exclude) => Effect) | undefined + ): ( + self: Effect + ) => Effect< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > + < + R, + E, + A, + Cases extends + & { [K in Extract["_tag"]]+?: ((error: Extract) => Effect) } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }), + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect, + cases: Cases, + orElse?: ((e: Exclude) => Effect) | undefined + ): Effect< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > +} = internal.catchTags + +/** + * Catches a specific reason within a tagged error. + * + * **When to use** + * + * Use to handle one nested reason inside an `Effect`'s tagged error while + * preserving the parent error shape for unmatched reasons. + * + * **Details** + * + * Use this to handle nested error causes without removing the parent error + * from the error channel. The handler receives the unwrapped reason. + * + * **Example** (Handling an error reason) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * declare const program: Effect.Effect + * + * // Handle rate limits specifically + * const handled = program.pipe( + * Effect.catchReason("AiError", "RateLimitError", (reason) => + * Effect.succeed(`Retry after ${reason.retryAfter}s`) + * ) + * ) + * ``` + * + * @see {@link catchReasons} for handling several nested reason tags + * + * @category error handling + * @since 4.0.0 + */ +export const catchReason: { + < + K extends Tags, + E, + RK extends ReasonTags, K>>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + errorTag: K, + reasonTag: RK, + f: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Effect, + orElse?: + | (( + reasons: ExcludeReason, K>, RK>, + error: OmitReason, K>, RK> + ) => Effect) + | undefined + ): ( + self: Effect + ) => Effect< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > + < + A, + E, + R, + K extends Tags, + RK extends ReasonTags>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + self: Effect, + errorTag: K, + reasonTag: RK, + f: (reason: ExtractReason, RK>, error: NarrowReason, RK>) => Effect, + orElse?: + | ((reasons: ExcludeReason, RK>, error: OmitReason, RK>) => Effect) + | undefined + ): Effect< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > +} = internal.catchReason + +/** + * Catches multiple reasons within a tagged error using an object of handlers. + * + * **Example** (Handling multiple error reasons) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * declare const program: Effect.Effect + * + * const handled = program.pipe( + * Effect.catchReasons("AiError", { + * RateLimitError: (reason) => + * Effect.succeed(`Retry after ${reason.retryAfter}s`), + * QuotaExceededError: (reason) => + * Effect.succeed(`Quota exceeded: ${reason.limit}`) + * }) + * ) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchReasons: { + < + K extends Tags, + E, + Cases extends { + [RK in ReasonTags, K>>]+?: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Effect + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Effect) + | undefined + ): ( + self: Effect + ) => Effect< + | A + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | R2 + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > + < + A, + E, + R, + K extends Tags, + Cases extends { + [RK in ReasonTags>]+?: ( + reason: ExtractReason, RK>, + error: NarrowReason, RK> + ) => Effect + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect, + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Effect) + | undefined + ): Effect< + | A + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? A : never + }[keyof Cases], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? E : never + }[keyof Cases], + | R + | R2 + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect ? R : never + }[keyof Cases] + > +} = internal.catchReasons + +/** + * Type helper that keeps only error tags whose tagged error contains a tagged `reason` field. + * + * **When to use** + * + * Use to constrain custom helpers or overloads to parent error tags whose error + * contains a tagged reason. + * + * **Details** + * + * The mapped type keeps each parent error tag whose extracted tagged error has + * at least one reason tag, and removes tags that do not carry tagged reasons. + * + * @see {@link unwrapReason} for promoting nested reason errors into the error channel + * @see {@link catchReason} for handling one nested reason tag + * @see {@link catchReasons} for handling several nested reason tags + * + * @category error handling + * @since 4.0.0 + */ +export type TagsWithReason = { + [T in Tags]: ReasonTags> extends never ? never : T +}[Tags] + +/** + * Promotes nested reason errors into the Effect error channel, replacing + * the parent error. + * + * **Example** (Extracting the reason from a tagged error) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * declare const program: Effect.Effect + * + * // Before: Effect + * // After: Effect + * const unwrapped = program.pipe(Effect.unwrapReason("AiError")) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const unwrapReason: { + < + K extends TagsWithReason, + E + >( + errorTag: K + ): (self: Effect) => Effect | ReasonOf>, R> + < + A, + E, + R, + K extends TagsWithReason + >( + self: Effect, + errorTag: K + ): Effect | ReasonOf>, R> +} = internal.unwrapReason + +/** + * Handles both recoverable and unrecoverable errors by providing a recovery + * effect. + * + * **When to use** + * + * Use when you need to recover from an `Effect` by inspecting the full `Cause`, + * including recoverable failures, defects, and interruptions, instead of only + * the typed error value. + * + * **Details** + * + * When to Recover from Defects: + * + * Defects are unexpected errors that typically shouldn't be recovered from, as + * they often indicate serious issues. However, in some cases, such as + * dynamically loaded plugins, controlled recovery might be needed. + * + * **Example** (Recovering from full failure causes) + * + * ```ts + * import { Cause, Console, Effect } from "effect" + * + * // An effect that might fail in different ways + * const program = Effect.die("Something went wrong") + * + * // Recover from any cause (including defects) + * const recovered = Effect.catchCause(program, (cause) => { + * if (Cause.hasDies(cause)) { + * return Console.log("Caught defect").pipe( + * Effect.as("Recovered from defect") + * ) + * } + * return Effect.succeed("Unknown error") + * }) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchCause: { + ( + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause) => Effect + ): Effect +} = internal.catchCause + +/** + * Recovers from defects using a provided recovery function. + * + * **When to use** + * + * Use when you need to report or translate defects at integration boundaries. + * + * **Details** + * + * `catchDefect` handles unexpected defects, such as thrown exceptions or + * values passed to `die`, without catching typed failures or interruptions. + * + * When to Recover from Defects: + * + * Defects are unexpected errors that typically should not be recovered from, as + * they often indicate serious issues. In some cases, such as dynamically loaded + * plugins, controlled recovery may be needed. + * + * **Example** (Recovering from defects) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // An effect that might throw an unexpected error (defect) + * const program = Effect.sync(() => { + * throw new Error("Unexpected error") + * }) + * + * // Recover from defects only + * const recovered = Effect.catchDefect(program, (defect) => { + * return Console.log(`Caught defect: ${defect}`).pipe( + * Effect.as("Recovered from defect") + * ) + * }) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchDefect: { + ( + f: (defect: unknown) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (defect: unknown) => Effect + ): Effect +} = internal.catchDefect + +/** + * Recovers from specific errors using a `Predicate` or `Refinement`. + * + * **When to use** + * + * Use when you need to recover from errors that match a condition. + * + * **Details** + * + * Use a `Refinement` for type narrowing or a `Predicate` for simple boolean + * matching. Non-matching errors re-fail with the original cause. Defects and + * interrupts are not caught. + * + * **Example** (Recovering when a predicate matches) + * + * ```ts + * import { Data, Effect, Filter } from "effect" + * + * class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {} + * + * const program = Effect.fail(new NotFound({ id: "user-1" })) + * + * // With a refinement + * const recovered = program.pipe( + * Effect.catchIf( + * (error): error is NotFound => error._tag === "NotFound", + * (error) => Effect.succeed(`missing:${error.id}`) + * ) + * ) + * + * // With a Filter + * const recovered2 = program.pipe( + * Effect.catchFilter( + * Filter.tagged("NotFound"), + * (error) => Effect.succeed(`missing:${error.id}`) + * ) + * ) + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchIf: { + ( + refinement: Predicate.Refinement, EB>, + f: (e: EB) => Effect, + orElse?: ((e: Exclude) => Effect) | undefined + ): ( + self: Effect + ) => Effect, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> + ( + predicate: Predicate.Predicate>, + f: (e: NoInfer) => Effect, + orElse?: ((e: NoInfer) => Effect) | undefined + ): ( + self: Effect + ) => Effect, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> + ( + self: Effect, + refinement: Predicate.Refinement, + f: (e: EB) => Effect, + orElse?: ((e: Exclude) => Effect) | undefined + ): Effect, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> + ( + self: Effect, + predicate: Predicate.Predicate, + f: (e: E) => Effect, + orElse?: ((e: E) => Effect) | undefined + ): Effect, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> +} = internal.catchIf + +/** + * Recovers from specific errors using a `Filter`. + * + * **When to use** + * + * Use to recover from typed `Effect` errors with a reusable `Filter` when + * matching can also narrow or transform the error before choosing the recovery + * effect. + * + * **Details** + * + * The filter runs on typed failures extracted from the `Cause`. Successful + * filter results are passed to `f`; failed filter results are passed to + * `orElse` when provided. Without `orElse`, the original failure cause is + * preserved. + * + * @see {@link catchIf} for predicate-based recovery from typed errors + * @see {@link catchTag} for recovering from a single tagged error + * @see {@link catchTags} for recovering from several tagged errors + * @see {@link catchCauseFilter} for filtering full causes instead of typed errors + * + * @category error handling + * @since 4.0.0 + */ +export const catchFilter: { + ( + filter: Filter.Filter, EB, X>, + f: (e: EB) => Effect, + orElse?: ((e: X) => Effect) | undefined + ): ( + self: Effect + ) => Effect, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> + ( + self: Effect, + filter: Filter.Filter, EB, X>, + f: (e: EB) => Effect, + orElse?: ((e: X) => Effect) | undefined + ): Effect, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> +} = internal.catchFilter + +/** + * Catches `NoSuchElementError` failures and converts them to `Option.none`. + * + * **When to use** + * + * Use when you expect missing-value failures and want them to become an + * optional success while all other failures keep failing. + * + * **Details** + * + * Success values become `Option.some`, `NoSuchElementError` becomes + * `Option.none`, and all other errors are preserved. + * + * **Example** (Recovering from missing Option values) + * + * ```ts + * import { Effect, Option } from "effect" + * + * const some = Effect.fromNullishOr(1).pipe(Effect.catchNoSuchElement) + * const none = Effect.fromNullishOr(null).pipe(Effect.catchNoSuchElement) + * + * Effect.runPromise(some).then(console.log) // { _id: 'Option', _tag: 'Some', value: 1 } + * Effect.runPromise(none).then(console.log) // { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link fromOption} for converting `Option.none` into `NoSuchElementError` + * @see {@link fromNullishOr} for converting nullish values into `NoSuchElementError` + * @see {@link option} for converting any failure into `Option.none` + * + * @category error handling + * @since 4.0.0 + */ +export const catchNoSuchElement: ( + self: Effect +) => Effect, Exclude, R> = internal.catchNoSuchElement + +/** + * Recovers from specific failures based on a predicate. + * + * **When to use** + * + * Use to recover an `Effect` from full causes selected by a predicate. + * + * **Details** + * + * This function allows you to conditionally catch and recover from failures + * that match a specific predicate. This is useful when you want to handle + * only certain types of errors while letting others propagate. + * + * **Example** (Recovering from selected causes) + * + * ```ts + * import { Cause, Console, Effect } from "effect" + * + * const httpRequest = Effect.fail("Network Error") + * + * // Only catch network-related failures + * const program = Effect.catchCauseIf( + * httpRequest, + * Cause.hasFails, + * (cause) => + * Effect.gen(function*() { + * yield* Console.log(`Caught network error: ${Cause.squash(cause)}`) + * return "Fallback response" + * }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Caught network error: Network Error" + * // Then: "Fallback response" + * ``` + * + * @see {@link catchCause} for recovering from every cause + * @see {@link catchCauseFilter} for selecting full causes with a `Filter` + * @see {@link catchIf} for predicate-based recovery from typed errors + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): Effect +} = internal.catchCauseIf + +/** + * Recovers from specific failures based on a `Filter`. + * + * **When to use** + * + * Use when you need to recover an `Effect` only from causes selected by a + * `Filter`, while giving the recovery both the selected value and the original + * `Cause`. + * + * **Details** + * + * The filter is applied to the full `Cause`. When it succeeds, the handler + * receives the selected value and the original cause. When it fails, the effect + * re-fails with the residual cause returned by the filter. + * + * @see {@link catchCauseIf} for predicate-based cause selection + * @see {@link catchFilter} for filtering typed error values instead of full causes + * @see {@link catchCause} for recovering from every cause without filtering + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseFilter: { + >( + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect + ): (self: Effect) => Effect | E2, R | R2> + >( + self: Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect + ): Effect | E2, R | R2> +} = internal.catchCauseFilter + +/** + * Transforms the failure value of an effect without changing its success value. + * + * **When to use** + * + * Use to translate an `Effect`'s typed failures while leaving successful values + * unchanged. + * + * **Details** + * + * Only the failure channel is transformed. The success channel and requirements + * are preserved. + * + * **Example** (Transforming the error channel) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} + * + * // ┌─── Effect + * // ▼ + * const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) + * + * // ┌─── Effect + * // ▼ + * const mapped = Effect.mapError( + * simulatedTask, + * (message) => new TaskError({ message }) + * ) + * ``` + * + * @see {@link map} for a version that operates on the success channel. + * @see {@link mapBoth} for a version that operates on both channels. + * + * @category error handling + * @since 2.0.0 + */ +export const mapError: { + (f: (e: E) => E2): (self: Effect) => Effect + (self: Effect, f: (e: E) => E2): Effect +} = internal.mapError + +/** + * Applies transformations to both the success and error channels of an effect. + * + * **When to use** + * + * Use to transform both success and failure channels of an `Effect` without + * changing whether it succeeds or fails. + * + * **Details** + * + * This function takes two map functions as arguments: one for the error channel + * and one for the success channel. You can use it when you want to modify both + * the error and the success values without altering the overall success or + * failure status of the effect. + * + * **Example** (Transforming success and failure channels) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} + * + * // ┌─── Effect + * // ▼ + * const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) + * + * // ┌─── Effect + * // ▼ + * const modified = Effect.mapBoth(simulatedTask, { + * onFailure: (message) => new TaskError({ message }), + * onSuccess: (n) => n > 0 + * }) + * ``` + * + * @see {@link map} for a version that operates on the success channel. + * @see {@link mapError} for a version that operates on the error channel. + * + * @category mapping + * @since 2.0.0 + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect +} = internal.mapBoth + +/** + * Converts typed failures from the error channel into defects, removing the + * error type from the returned effect. + * + * **When to use** + * + * Use when you need to turn an `Effect` typed failure that represents an + * unrecoverable bug or invalid state into a defect. + * + * **Example** (Converting typed failures into defects) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class DivideByZeroError extends Data.TaggedError("DivideByZeroError")<{}> {} + * + * const divide = (a: number, b: number) => + * b === 0 + * ? Effect.fail(new DivideByZeroError()) + * : Effect.succeed(a / b) + * + * // ┌─── Effect + * // ▼ + * const program = Effect.orDie(divide(1, 0)) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // (FiberFailure) DivideByZeroError + * // ...stack trace... + * ``` + * + * @category converting failures to defects + * @since 2.0.0 + */ +export const orDie: (self: Effect) => Effect = internal.orDie + +/** + * Runs an effectful operation when the source effect fails, while preserving + * the original failure when the operation succeeds. + * + * **Details** + * + * Use this for logging, metrics, or other failure-side observations. If the + * operation passed to `tapError` fails, that error is also represented in the + * returned effect's error channel. + * + * **Example** (Running effects on failure) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Simulate a task that fails with an error + * const task: Effect.Effect = Effect.fail("NetworkError") + * + * // Use tapError to log the error message when the task fails + * const tapping = Effect.tapError( + * task, + * (error) => Console.log(`expected error: ${error}`) + * ) + * + * Effect.runFork(tapping) + * // Output: + * // expected error: NetworkError + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapError: { + ( + f: (e: NoInfer) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (e: E) => Effect + ): Effect +} = internal.tapError + +/** + * Runs an effectful handler when a failure's `_tag` matches. + * + * **Details** + * + * Use this with tagged-union errors to perform side effects for one tag or a + * list of tags. When the handler succeeds, the original failure is preserved; + * if the handler fails, its error is also included in the returned effect. + * + * **Example** (Running effects for tagged failures) + * + * ```ts + * import { Console, Data, Effect } from "effect" + * + * class NetworkError extends Data.TaggedError("NetworkError")<{ + * statusCode: number + * }> {} + * + * class ValidationError extends Data.TaggedError("ValidationError")<{ + * field: string + * }> {} + * + * const task: Effect.Effect = + * Effect.fail(new NetworkError({ statusCode: 504 })) + * + * const program = Effect.tapErrorTag(task, "NetworkError", (error) => + * Console.log(`expected error: ${error.statusCode}`) + * ) + * + * Effect.runPromiseExit(program) + * // Output: + * // expected error: 504 + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapErrorTag: { + | Arr.NonEmptyReadonlyArray>, E, A1, E1, R1>( + k: K, + f: (e: ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K>) => Effect + ): (self: Effect) => Effect + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1 + >( + self: Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect + ): Effect +} = internal.tapErrorTag + +/** + * Runs an effectful operation with the full `Cause` when the source effect + * fails. + * + * **When to use** + * + * Use when failure observation needs typed failures, defects, and interruptions + * rather than only the typed error value. + * + * **Details** + * + * Use this to log or inspect typed failures, defects, and interruptions. When + * the operation succeeds, the original cause is preserved. If the operation + * fails, its error is also represented in the returned effect. + * + * **Example** (Observing full failure causes) + * + * ```ts + * import { Cause, Console, Effect } from "effect" + * + * const task = Effect.fail("Something went wrong") + * + * const program = Effect.tapCause( + * task, + * (cause) => Console.log(`Logging cause: ${Cause.squash(cause)}`) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: "Logging cause: Error: Something went wrong" + * // Then: { _id: 'Exit', _tag: 'Failure', cause: ... } + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const tapCause: { + ( + f: (cause: Cause.Cause>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (cause: Cause.Cause) => Effect + ): Effect +} = internal.tapCause + +/** + * Executes a side effect conditionally when a failed effect's cause matches a predicate. + * + * **Details** + * + * This function allows you to tap into the cause of an effect's failure only when + * the cause matches a specific predicate. This is useful for conditional logging, + * monitoring, or other side effects based on the type of failure. + * + * **Example** (Observing selected failure causes) + * + * ```ts + * import { Cause, Console, Effect } from "effect" + * + * const task = Effect.fail("Network timeout") + * + * // Only log causes that contain failures (not interrupts or defects) + * const program = Effect.tapCauseIf( + * task, + * Cause.hasFails, + * (cause) => Console.log(`Logging failure cause: ${Cause.squash(cause)}`) + * ) + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: "Logging failure cause: Network timeout" + * // Then: { _id: 'Exit', _tag: 'Failure', cause: ... } + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const tapCauseIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): Effect +} = internal.tapCauseIf + +/** + * Executes a side effect conditionally when a failed effect's cause passes a filter. + * + * **When to use** + * + * Use when you need to observe only failure causes selected by a `Filter`, + * while giving the side effect both the selected value and the original + * `Cause`. + * + * **Details** + * + * A successful filter result runs the side effect with the selected value and + * original cause. A failed filter result skips the side effect and preserves the + * original cause. + * + * @see {@link tapCauseIf} for selecting causes with a boolean predicate + * @see {@link tapCause} for observing every failure cause + * @see {@link catchCauseFilter} for recovering from selected causes instead of only observing them + * + * @category sequencing + * @since 4.0.0 + */ +export const tapCauseFilter: { + >( + filter: Filter.Filter, EB, X>, + f: (a: EB, cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + >( + self: Effect, + filter: Filter.Filter, EB, X>, + f: (a: EB, cause: Cause.Cause) => Effect + ): Effect +} = internal.tapCauseFilter + +/** + * Runs an effectful operation when the source effect dies with a defect. + * + * **Details** + * + * Use this for diagnostics such as logging unexpected thrown exceptions or + * values passed to `die`. Recoverable failures are not handled. When the + * operation succeeds, the original defect is preserved; if the operation fails, + * its error is also represented in the returned effect. + * + * **Example** (Observing defects) + * + * ```ts + * import { Console, Effect } from "effect" + * + * // Simulate a task that fails with a recoverable error + * const task1: Effect.Effect = Effect.fail("NetworkError") + * + * // tapDefect won't log anything because NetworkError is not a defect + * const tapping1 = Effect.tapDefect( + * task1, + * (cause) => Console.log(`defect: ${cause}`) + * ) + * + * Effect.runFork(tapping1) + * // No Output + * + * // Simulate a severe failure in the system + * const task2: Effect.Effect = Effect.die( + * "Something went wrong" + * ) + * + * // Log the defect using tapDefect + * const tapping2 = Effect.tapDefect( + * task2, + * (cause) => Console.log(`defect: ${cause}`) + * ) + * + * Effect.runFork(tapping2) + * // Output: + * // defect: RuntimeException: Something went wrong + * // ... stack trace ... + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapDefect: { + (f: (defect: unknown) => Effect): (self: Effect) => Effect + (self: Effect, f: (defect: unknown) => Effect): Effect +} = internal.tapDefect + +/** + * Retries an effect until it succeeds, discarding failures. + * + * **Details** + * + * Yields between attempts so other fibers can run. + * + * **Example** (Retrying until success) + * + * ```ts + * import { Console, Effect } from "effect" + * + * let attempts = 0 + * + * const flaky = Effect.gen(function*() { + * attempts++ + * yield* Console.log(`Attempt ${attempts}`) + * if (attempts < 3) { + * return yield* Effect.fail("Not ready") + * } + * return "Ready" + * }) + * + * const program = Effect.eventually(flaky) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Attempt 1 + * // Attempt 2 + * // Attempt 3 + * // Ready + * ``` + * + * @category repetition + * @since 2.0.0 + */ +export const eventually: (self: Effect) => Effect = internal.eventually + +// ----------------------------------------------------------------------------- +// Error Handling +// ----------------------------------------------------------------------------- + +/** + * Type helpers for retrying effects. + * + * @since 2.0.0 + */ +export declare namespace Retry { + /** + * Computes the result type of `Effect.retry` from the original effect and retry options. + * + * @category error handling + * @since 2.0.0 + */ + export type Return> = Effect< + A, + | (O extends { schedule: Schedule } ? E + : O extends { times: number } ? E + : O extends { until: Predicate.Refinement } ? E2 + : O extends { while: Predicate.Refinement } ? Exclude + : E) + | (O extends { schedule: Schedule } ? E + : never) + | (O extends { while: (...args: Array) => Effect } ? E + : never) + | (O extends { until: (...args: Array) => Effect } ? E + : never), + | R + | (O extends { schedule: Schedule } ? R + : never) + | (O extends { while: (...args: Array) => Effect } ? R + : never) + | (O extends { until: (...args: Array) => Effect } ? R + : never) + > extends infer Z ? Z + : never + + /** + * Options that control whether and how a failing effect is retried. + * + * @category error handling + * @since 2.0.0 + */ + export interface Options { + while?: ((error: E) => boolean | Effect) | undefined + until?: ((error: E) => boolean | Effect) | undefined + times?: number | undefined + schedule?: Schedule | undefined + } +} + +/** + * Retries typed failures from an effect according to a retry policy. + * + * **When to use** + * + * Use when you need to rerun an effect after transient typed failures, such as + * network issues or temporary resource unavailability. + * + * **Details** + * + * The policy can be a `Schedule`, a schedule builder, or a `Retry.Options` + * object using `schedule`, `times`, `while`, or `until`. If a retry eventually + * succeeds, the returned effect succeeds with that value. If the policy stops + * while the effect is still failing, the last failure is propagated. + * + * **Gotchas** + * + * The source effect is always evaluated once before any retry policy is + * applied. For example, `Schedule.recurs(3)` allows up to three retries after + * the initial attempt. + * + * Defects and interruptions are not retried. + * + * **Example** (Retrying with a schedule) + * + * ```ts + * import { Data, Effect, Schedule } from "effect" + * + * class AttemptError extends Data.TaggedError("AttemptError")<{ readonly attempt: number }> {} + * + * let attempt = 0 + * const task = Effect.callback((resume) => { + * attempt++ + * if (attempt <= 2) { + * resume(Effect.fail(new AttemptError({ attempt }))) + * } else { + * resume(Effect.succeed("Success!")) + * } + * }) + * + * const policy = Schedule.addDelay(Schedule.recurs(5), () => Effect.succeed("100 millis")) + * const program = Effect.retry(task, policy) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Success!" (after 2 retries) + * ``` + * + * @see {@link retryOrElse} for a version that allows you to run a fallback. + * @see {@link repeat} if your retry condition is based on successful outcomes rather than errors. + * @category error handling + * @since 2.0.0 + */ +export const retry: { + >(options: O): (self: Effect) => Retry.Return + ( + policy: Schedule, Error, Env> + ): (self: Effect) => Effect + ( + builder: ( + $: (_: Schedule, SE, R>) => Schedule + ) => Schedule, Error, Env> + ): (self: Effect) => Effect + >(self: Effect, options: O): Retry.Return + ( + self: Effect, + policy: Schedule, Error, Env> + ): Effect + ( + self: Effect, + builder: ( + $: (_: Schedule, SE, R>) => Schedule + ) => Schedule, Error, Env> + ): Effect +} = internalSchedule.retry + +/** + * Retries a failing effect and runs a fallback effect if retries are exhausted. + * + * **When to use** + * + * Use when you want to handle failures gracefully by specifying an alternative + * action after repeated failures. + * + * **Details** + * + * The `Effect.retryOrElse` function attempts to retry a failing effect multiple + * times according to a defined {@link Schedule} policy. + * + * If the retries are exhausted and the effect still fails, it runs a fallback + * effect instead. + * + * **Example** (Falling back after retries are exhausted) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class NetworkTimeoutError extends Data.TaggedError("NetworkTimeoutError")<{}> {} + * + * let attempt = 0 + * const networkRequest = Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Network attempt ${attempt}`) + * if (attempt < 3) { + * return yield* Effect.fail(new NetworkTimeoutError()) + * } + * return "Network data" + * }) + * + * // Retry up to 2 times, then fall back to cached data + * const program = Effect.retryOrElse( + * networkRequest, + * Schedule.recurs(2), + * (error, retryCount) => + * Effect.gen(function*() { + * yield* Console.log(`All ${retryCount} retries failed, using cache`) + * return "Cached data" + * }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Network attempt 1 + * // Network attempt 2 + * // Network attempt 3 + * // Network data + * ``` + * + * @see {@link retry} for a version that does not run a fallback effect. + * @category error handling + * @since 2.0.0 + */ +export const retryOrElse: { + ( + policy: Schedule, E1, R1>, + orElse: (e: NoInfer, out: A1) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + policy: Schedule, E1, R1>, + orElse: (e: NoInfer, out: A1) => Effect + ): Effect +} = internalSchedule.retryOrElse + +/** + * Exposes an effect's full failure cause in the error channel as `Cause`. + * + * **Details** + * + * Use `sandbox` when downstream error handling needs to distinguish typed + * failures, defects, and interruptions. Use `unsandbox` to restore the original + * typed error channel after cause-level handling. + * + * **Example** (Exposing failures as causes) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const task = Effect.fail("Something went wrong") + * + * // Sandbox exposes the full cause as the error type + * const program = Effect.gen(function*() { + * const result = yield* Effect.flip(Effect.sandbox(task)) + * return `Caught cause: ${Cause.squash(result)}` + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Caught cause: Something went wrong" + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const sandbox: ( + self: Effect +) => Effect, R> = internal.sandbox + +/** + * Discards both the success and failure values of an effect. + * + * **When to use** + * + * Use when an effect should run for its side effects while both success and + * failure values are discarded. + * + * **Details** + * + * Use the `log` option to emit the full {@link Cause} when the effect fails, + * and `message` to prepend a custom log message. + * + * **Example** (Discarding success and failure values) + * + * ```ts + * import { Effect } from "effect" + * + * // ┌─── Effect + * // ▼ + * const task = Effect.fail("Uh oh!").pipe(Effect.as(5)) + * + * // ┌─── Effect + * // ▼ + * const program = task.pipe(Effect.ignore) + * ``` + * + * **Example** (Logging failures while ignoring results) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.fail("Uh oh!") + * + * const program = task.pipe(Effect.ignore({ log: true })) + * const programWarn = task.pipe(Effect.ignore({ log: "Warn", message: "Ignoring task failure" })) + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const ignore: < + Arg extends Effect | { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } | undefined = { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect + : (self: Effect) => Effect = internal.ignore + +/** + * Ignores the effect's failure cause, including defects and interruptions. + * + * **When to use** + * + * Use when a best-effort effect should never fail, even from defects or + * interruption, and optional cause logging is enough. + * + * **Details** + * + * Use the `log` option to emit the full {@link Cause} when the effect fails, + * and `message` to prepend a custom log message. + * + * **Example** (Ignoring failures and logging causes) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.fail("boom") + * + * const program = task.pipe(Effect.ignoreCause) + * const programLog = task.pipe(Effect.ignoreCause({ log: true, message: "Ignoring failure cause" })) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const ignoreCause: < + Arg extends Effect | { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } | undefined = { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly log?: boolean | Severity | undefined + readonly message?: string | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect + : (self: Effect) => Effect = internal.ignoreCause + +/** + * Applies an `ExecutionPlan` to an effect, retrying with step-provided resources + * until it succeeds or the plan is exhausted. + * + * **Details** + * + * Each attempt updates `ExecutionPlan.CurrentMetadata` (attempt and step index), + * and retry timing is derived per step (the first attempt uses the remaining + * attempts schedule; later retries apply the step schedule at least once). + * + * **Example** (Retrying with an execution plan) + * + * ```ts + * import { Context, Effect, ExecutionPlan, Layer } from "effect" + * + * const Endpoint = Context.Service<{ url: string }>("Endpoint") + * + * const fetchUrl = Effect.gen(function*() { + * const endpoint = yield* Effect.service(Endpoint) + * if (endpoint.url === "bad") { + * return yield* Effect.fail("Unavailable") + * } + * return endpoint.url + * }) + * + * const plan = ExecutionPlan.make( + * { provide: Layer.succeed(Endpoint, { url: "bad" }), attempts: 2 }, + * { provide: Layer.succeed(Endpoint, { url: "good" }) } + * ) + * + * const program = Effect.withExecutionPlan(fetchUrl, plan) + * ``` + * + * @category fallback + * @since 3.16.0 + */ +export const withExecutionPlan: { + ( + plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> + ): ( + effect: Effect + ) => Effect | PlanR> + ( + effect: Effect, + plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> + ): Effect | PlanR> +} = internalExecutionPlan.withExecutionPlan + +/** + * Runs an effect and reports any errors to the configured `ErrorReporter`s. + * + * **Details** + * + * If the `defectsOnly` option is set to `true`, only defects (unrecoverable + * errors) will be reported, while regular failures will be ignored. + * + * @category error handling + * @since 4.0.0 + */ +export const withErrorReporting: < + Arg extends Effect | { readonly defectsOnly?: boolean | undefined } | undefined = { + readonly defectsOnly?: boolean | undefined + } +>( + effectOrOptions: Arg, + options?: { readonly defectsOnly?: boolean | undefined } | undefined +) => [Arg] extends [Effect] ? Arg : (self: Effect) => Effect = + internal.withErrorReporting + +// ----------------------------------------------------------------------------- +// Fallback +// ----------------------------------------------------------------------------- + +/** + * Recovers from a typed failure by producing a fallback success value. + * + * **Details** + * + * If the source effect succeeds, its value is preserved. If it fails in the + * error channel, `orElseSucceed` evaluates the fallback and succeeds with that + * value, removing the typed error from the returned effect. + * + * Defects and interruptions are not recovered by this operator. + * + * **Example** (Replacing failures with a value) + * + * ```ts + * import { Effect } from "effect" + * + * const validate = (age: number): Effect.Effect => { + * if (age < 0) { + * return Effect.fail("NegativeAgeError") + * } else if (age < 18) { + * return Effect.fail("IllegalAgeError") + * } else { + * return Effect.succeed(age) + * } + * } + * + * const program = Effect.orElseSucceed(validate(-1), () => 18) + * + * console.log(Effect.runSyncExit(program)) + * // Output: + * // { _id: 'Exit', _tag: 'Success', value: 18 } + * ``` + * + * @category fallback + * @since 2.0.0 + */ +export const orElseSucceed: { + ( + evaluate: LazyArg + ): (self: Effect) => Effect + ( + self: Effect, + evaluate: LazyArg + ): Effect +} = internal.orElseSucceed + +/** + * Runs a sequence of effects and returns the result of the first successful + * one. + * + * **When to use** + * + * Use when you have prioritized fallback `Effect`s, such as attempting + * multiple APIs, reading configuration from several sources, or trying + * alternative resource locations in order. + * + * **Details** + * + * This function executes the provided effects in sequence, stopping at the + * first success. If an effect succeeds, its result is returned immediately and + * no further effects in the sequence are executed. + * + * If all effects fail, the returned effect fails with the error from the last + * effect. If the collection is empty, the returned effect defects with an + * `Error` whose message is `"Received an empty collection of effects"`. + * + * **Example** (Trying alternatives until one succeeds) + * + * ```ts + * import { Effect } from "effect" + * + * const primary = Effect.fail("primary unavailable") + * const secondary = Effect.succeed("secondary result") + * const tertiary = Effect.sync(() => { + * throw new Error("not evaluated") + * }) + * + * const program = Effect.firstSuccessOf([ + * primary, + * secondary, + * tertiary + * ]) + * + * console.log(Effect.runSync(program)) + * // Output: "secondary result" + * ``` + * + * @category fallback + * @since 2.0.0 + */ +export const firstSuccessOf: >( + effects: Iterable +) => Effect, Error, Services> = internal.firstSuccessOf + +// ----------------------------------------------------------------------------- +// Delays & timeouts +// ----------------------------------------------------------------------------- + +/** + * Adds a time limit to an effect, triggering a timeout if the effect exceeds + * the duration. + * + * **When to use** + * + * Use when you need a timeout of an `Effect` to be represented as a typed + * failure. + * + * **Details** + * + * The `timeout` function allows you to specify a time limit for an + * effect's execution. If the effect does not complete within the given time, a + * `TimeoutException` is raised. This can be useful for controlling how long + * your program waits for a task to finish, ensuring that it doesn't hang + * indefinitely if the task takes too long. + * + * **Gotchas** + * + * If the timeout wins, the source effect is interrupted. + * + * **Example** (Failing when work takes too long) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function*() { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * // Output will show a TimeoutException as the task takes longer + * // than the specified timeout duration + * const timedEffect = task.pipe(Effect.timeout("1 second")) + * + * Effect.runPromiseExit(timedEffect).then(console.log) + * // Output: + * // Start processing... + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Fail', + * // failure: { _tag: 'TimeoutException' } + * // } + * // } + * ``` + * + * @see {@link timeoutOption} for returning `Option.none` on timeout. + * @see {@link timeoutOrElse} for a version that allows specifying both success and timeout handlers. + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const timeout: { + ( + duration: Duration.Input + ): (self: Effect) => Effect + ( + self: Effect, + duration: Duration.Input + ): Effect +} = internal.timeout + +/** + * Runs an effect with a time limit and represents only the timeout case as + * `Option.none`. + * + * **When to use** + * + * Use when a timeout of an `Effect` should be handled as `Option.none`. + * + * **Details** + * + * If the source effect succeeds before the timeout, the returned effect + * succeeds with `Option.some(value)`. If the timeout wins, the source effect is + * interrupted and the returned effect succeeds with `Option.none`. If the + * source effect fails before the timeout, that failure is preserved. + * + * **Example** (Returning None on timeout) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function*() { + * console.log("Start processing...") + * yield* Effect.sleep("2 seconds") // Simulates a delay in processing + * console.log("Processing complete.") + * return "Result" + * }) + * + * const timedOutEffect = Effect.all([ + * task.pipe(Effect.timeoutOption("3 seconds")), + * task.pipe(Effect.timeoutOption("1 second")) + * ]) + * + * Effect.runPromise(timedOutEffect).then(console.log) + * // Output: + * // Start processing... + * // Processing complete. + * // Start processing... + * // [ + * // { _id: 'Option', _tag: 'Some', value: 'Result' }, + * // { _id: 'Option', _tag: 'None' } + * // ] + * ``` + * + * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeoutOrElse} for a version that allows specifying both success and timeout handlers. + * + * @category delays & timeouts + * @since 3.1.0 + */ +export const timeoutOption: { + ( + duration: Duration.Input + ): (self: Effect) => Effect, E, R> + ( + self: Effect, + duration: Duration.Input + ): Effect, E, R> +} = internal.timeoutOption + +/** + * Applies a timeout to an effect, with a fallback effect executed if the timeout is reached. + * + * **When to use** + * + * Use when a timeout of an `Effect` should switch to a fallback effect. + * + * **Details** + * + * The fallback effect is created lazily by `orElse` and may introduce its own + * success, failure, and requirement types. + * + * **Gotchas** + * + * If the timeout wins, the source effect is interrupted before the fallback is + * run. + * + * **Example** (Falling back on timeout) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const slowQuery = Effect.gen(function*() { + * yield* Console.log("Starting database query...") + * yield* Effect.sleep("5 seconds") + * return "Database result" + * }) + * + * // Use cached data as fallback when timeout is reached + * const program = Effect.timeoutOrElse(slowQuery, { + * duration: "2 seconds", + * orElse: () => + * Effect.gen(function*() { + * yield* Console.log("Query timed out, using cached data") + * return "Cached result" + * }) + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Starting database query... + * // Query timed out, using cached data + * // Cached result + * ``` + * + * @see {@link timeout} for failing with a `TimeoutException`. + * @see {@link timeoutOption} for returning `Option.none` on timeout. + * + * @category delays & timeouts + * @since 4.0.0 + */ +export const timeoutOrElse: { + (options: { + readonly duration: Duration.Input + readonly orElse: LazyArg> + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly duration: Duration.Input + readonly orElse: LazyArg> + } + ): Effect +} = internal.timeoutOrElse + +/** + * Returns an effect that is delayed from this effect by the specified + * `Duration`. + * + * **Example** (Delaying an effect) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.delay( + * Console.log("Delayed message"), + * "1 second" + * ) + * + * Effect.runFork(program) + * // Waits 1 second, then prints: "Delayed message" + * ``` + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const delay: { + ( + duration: Duration.Input + ): (self: Effect) => Effect + ( + self: Effect, + duration: Duration.Input + ): Effect +} = internal.delay + +/** + * Returns an effect that suspends the current fiber for the specified duration + * without blocking a JavaScript thread. + * + * **Example** (Pausing without blocking) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.log("Start") + * yield* Effect.sleep("2 seconds") + * yield* Console.log("End") + * }) + * + * Effect.runFork(program) + * // Output: "Start" (immediately) + * // Output: "End" (after 2 seconds) + * ``` + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const sleep: (duration: Duration.Input) => Effect = internal.sleep + +/** + * Returns the runtime duration of an effect together with its result. + * + * **Details** + * + * The original success, failure, or interruption is preserved; only the success + * value is paired with the duration. + * + * **Example** (Measuring execution time) + * + * ```ts + * import { Console, Duration, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const [duration, value] = yield* Effect.timed(Effect.succeed("ok")) + * yield* Console.log(`took ${Duration.toMillis(duration)}ms: ${value}`) + * }) + * ``` + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const timed: (self: Effect) => Effect<[duration: Duration.Duration, result: A], E, R> = + internal.timed + +// ----------------------------------------------------------------------------- +// Racing +// ----------------------------------------------------------------------------- + +/** + * Runs multiple effects concurrently and returns the first successful result. + * + * **When to use** + * + * Use when early failures should be ignored until a success occurs + * or all effects fail. + * + * **Details** + * + * Early failures do not finish the race; `raceAll` keeps waiting until one + * effect succeeds or every effect has failed. When one effect succeeds, the + * remaining effects are interrupted. If every effect fails, the returned effect + * fails with a cause containing the collected failure reasons. + * + * **Example** (Racing many effects) + * + * ```ts + * import { Duration, Effect } from "effect" + * + * // Multiple effects with different delays + * const effect1 = Effect.delay(Effect.succeed("Fast"), Duration.millis(100)) + * const effect2 = Effect.delay(Effect.succeed("Slow"), Duration.millis(500)) + * const effect3 = Effect.delay(Effect.succeed("Very Slow"), Duration.millis(1000)) + * + * // Race all effects - the first to succeed wins + * const raced = Effect.raceAll([effect1, effect2, effect3]) + * + * // Result: "Fast" (after ~100ms) + * ``` + * + * @see {@link race} for a version that handles only two effects. + * @category racing + * @since 2.0.0 + */ +export const raceAll: >( + all: Iterable, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber + readonly index: number + readonly parentFiber: Fiber + }) => void + } +) => Effect, Error, Services> = internal.raceAll + +/** + * Runs multiple effects concurrently and completes with the first effect to + * finish, whether it succeeds or fails. + * + * **Details** + * + * After the first effect completes, all remaining effects are interrupted. Use + * `raceAll` when early failures should be ignored until a success occurs or + * all effects fail. + * + * **Example** (Taking the first settled result) + * + * ```ts + * import { Duration, Effect } from "effect" + * + * // Multiple effects with different delays and potential failures + * const effect1 = Effect.delay(Effect.succeed("First"), Duration.millis(200)) + * const effect2 = Effect.delay(Effect.fail("Second failed"), Duration.millis(100)) + * const effect3 = Effect.delay(Effect.succeed("Third"), Duration.millis(300)) + * + * // Race all effects - the first to succeed wins + * const raced = Effect.raceAllFirst([effect1, effect2, effect3]) + * + * // Result: "First" (after ~200ms, even though effect2 completes first but fails) + * ``` + * + * @category racing + * @since 4.0.0 + */ +export const raceAllFirst: >( + all: Iterable, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber + readonly index: number + readonly parentFiber: Fiber + }) => void + } +) => Effect, Error, Services> = internal.raceAllFirst + +/** + * Races two effects and returns the first successful result. + * + * **Details** + * + * If one effect succeeds, the other is interrupted and `onWinner` can observe the + * winning fiber. If both fail, the race fails. + * + * **Example** (Racing two effects) + * + * ```ts + * import { Console, Duration, Effect } from "effect" + * + * const fastFail = Effect.delay(Effect.fail("fast-fail"), Duration.millis(10)) + * const slowSuccess = Effect.delay(Effect.succeed("slow-success"), Duration.millis(50)) + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.race(fastFail, slowSuccess) + * yield* Console.log(`winner: ${result}`) + * }) + * + * Effect.runPromise(program) + * // Output: winner: slow-success + * ``` + * + * @category racing + * @since 2.0.0 + */ +export const race: { + ( + that: Effect, + options?: { + readonly onWinner?: ( + options: { readonly fiber: Fiber; readonly index: number; readonly parentFiber: Fiber } + ) => void + } + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + options?: { + readonly onWinner?: ( + options: { readonly fiber: Fiber; readonly index: number; readonly parentFiber: Fiber } + ) => void + } + ): Effect +} = internal.race + +/** + * Races two effects and returns the result of the first one to complete, whether + * it succeeds or fails. + * + * **When to use** + * + * Use when any completion, including failure, should decide the race and + * interrupt the losing effect. + * + * **Details** + * + * The losing effect is interrupted, and `onWinner` can observe the winning fiber. + * + * **Example** (Observing the winning fiber) + * + * ```ts + * import { Console, Duration, Effect } from "effect" + * + * const fastFail = Effect.delay(Effect.fail("fast-fail"), Duration.millis(10)) + * const slowSuccess = Effect.delay(Effect.succeed("slow-success"), Duration.millis(50)) + * + * const program = Effect.gen(function*() { + * const message = yield* Effect.match(Effect.raceFirst(fastFail, slowSuccess), { + * onFailure: (error) => `failed: ${error}`, + * onSuccess: (value) => `succeeded: ${value}` + * }) + * yield* Console.log(message) + * }) + * + * Effect.runPromise(program) + * // Output: failed: fast-fail + * ``` + * + * @category racing + * @since 2.0.0 + */ +export const raceFirst: { + ( + that: Effect, + options?: { + readonly onWinner?: ( + options: { readonly fiber: Fiber; readonly index: number; readonly parentFiber: Fiber } + ) => void + } + ): (self: Effect) => Effect + ( + self: Effect, + that: Effect, + options?: { + readonly onWinner?: ( + options: { readonly fiber: Fiber; readonly index: number; readonly parentFiber: Fiber } + ) => void + } + ): Effect +} = internal.raceFirst + +// ----------------------------------------------------------------------------- +// Filtering +// ----------------------------------------------------------------------------- + +/** + * Filters elements of an iterable using a predicate, refinement, or effectful + * predicate. + * + * **Example** (Filtering success values) + * + * ```ts + * import { Effect } from "effect" + * + * // Sync predicate + * const evens = Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0) + * + * // Effectful predicate + * const checked = Effect.filter([1, 2, 3], (n) => Effect.succeed(n > 1)) + * + * // Use Effect.filterMapEffect for effectful Filter.Filter callbacks + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + ( + refinement: Predicate.Refinement, B> + ): (elements: Iterable) => Effect> + ( + predicate: Predicate.Predicate> + ): (elements: Iterable) => Effect> + ( + predicate: (a: NoInfer, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): (iterable: Iterable) => Effect, E, R> + ( + elements: Iterable, + refinement: Predicate.Refinement + ): Effect> + ( + elements: Iterable, + predicate: Predicate.Predicate + ): Effect> + ( + iterable: Iterable, + predicate: (a: NoInfer, i: number) => Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect, E, R> +} = internal.filter + +/** + * Filters and maps elements of an iterable with a `Filter`. + * + * **When to use** + * + * Use when you need to filter an iterable with a `Filter` inside an `Effect`, + * collecting each filter success value. + * + * **Details** + * + * `Result.succeed` values are collected in the returned array, and + * `Result.fail` values are skipped. + * + * @see {@link filter} for keeping original elements with a boolean predicate, refinement, or effectful predicate + * @see {@link filterMapEffect} for using an effectful `Filter` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + ( + filter: Filter.Filter, B, X> + ): (elements: Iterable) => Effect> + ( + elements: Iterable, + filter: Filter.Filter, B, X> + ): Effect> +} = internal.filterMap + +/** + * Filters and maps elements of an iterable effectfully with a `FilterEffect`. + * + * **When to use** + * + * Use when you need to filter each iterable element effectfully and transform + * accepted elements into successful output values. + * + * **Details** + * + * `Result.succeed` values are collected in the returned array, and + * `Result.fail` values are skipped. + * + * **Gotchas** + * + * With concurrent execution, successful values are collected in completion + * order, not input order. + * + * @see {@link filterMap} for using a synchronous `Filter` + * @see {@link filter} for keeping original elements with a predicate + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapEffect: { + ( + filter: Filter.FilterEffect, B, X, E, R>, + options?: { readonly concurrency?: Concurrency | undefined } + ): (elements: Iterable) => Effect, E, R> + ( + elements: Iterable, + filter: Filter.FilterEffect, B, X, E, R>, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect, E, R> +} = internal.filterMapEffect + +/** + * Filters an effect, providing an alternative effect if the predicate fails. + * + * **When to use** + * + * Use when a successful value that fails a predicate should continue with an + * effectful fallback instead of failing the effect. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, it executes the `orElse` effect instead. The + * `orElse` effect can produce an alternative value or perform additional + * computations. + * + * **Example** (Filtering with a fallback effect) + * + * ```ts + * import { Effect } from "effect" + * + * // An effect that produces a number + * const program = Effect.succeed(5) + * + * // Filter for even numbers, provide alternative for odd numbers + * const filtered = Effect.filterOrElse( + * program, + * (n) => n % 2 === 0, + * (n) => Effect.succeed(`Number ${n} is odd`) + * ) + * + * // Result: "Number 5 is odd" (since 5 is not even) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterOrElse: { + ( + refinement: Predicate.Refinement, B>, + orElse: (a: EqualsWith, Exclude, B>>) => Effect + ): (self: Effect) => Effect + ( + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Predicate.Refinement, + orElse: (a: EqualsWith>) => Effect + ): Effect + ( + self: Effect, + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect + ): Effect +} = internal.filterOrElse + +/** + * Filters an effect with a `Filter`, providing an alternative effect on failure. + * + * **When to use** + * + * Use when a successful effect value should be accepted and transformed by a + * `Filter`, while rejected values should continue with an alternative effect + * built from the filter failure. + * + * **Details** + * + * `Result.succeed` becomes the returned success value, and `Result.fail` is + * passed to `orElse`. + * + * @see {@link filterOrElse} for using a predicate and fallback effect + * @see {@link filterMapOrFail} for failing the effect when the filter fails + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapOrElse: { + ( + filter: Filter.Filter, B, X>, + orElse: (x: X) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + filter: Filter.Filter, B, X>, + orElse: (x: X) => Effect + ): Effect +} = internal.filterMapOrElse + +/** + * Filters an effect, failing with a custom error if the predicate fails. + * + * **Details** + * + * This function applies a predicate to the result of an effect. If the + * predicate evaluates to `false`, the effect fails with either a custom + * error (if `orFailWith` is provided) or a `NoSuchElementError`. + * + * **Example** (Filtering with a custom failure) + * + * ```ts + * import { Effect } from "effect" + * + * // An effect that produces a number + * const program = Effect.succeed(5) + * + * // Filter for even numbers, fail for odd numbers + * const filtered = Effect.filterOrFail( + * program, + * (n) => n % 2 === 0, + * (n) => `Expected even number, got ${n}` + * ) + * + * // Result: Effect.fail("Expected even number, got 5") + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterOrFail: { + ( + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): (self: Effect) => Effect + ( + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: Effect) => Effect + ( + refinement: Predicate.Refinement, B> + ): (self: Effect) => Effect + ( + predicate: Predicate.Predicate> + ): (self: Effect) => Effect + ( + self: Effect, + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): Effect + ( + self: Effect, + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 + ): Effect + ( + self: Effect, + refinement: Predicate.Refinement, B> + ): Effect + ( + self: Effect, + predicate: Predicate.Predicate> + ): Effect +} = internal.filterOrFail + +/** + * Filters and maps an effect with a `Filter`, failing when the filter fails. + * + * **When to use** + * + * Use when validating and transforming one effect success with a synchronous + * `Filter`, while rejected values should fail the effect. + * + * **Details** + * + * `Result.succeed` becomes the returned success value. `Result.fail` is mapped + * with `orFailWith` when provided, or fails with `NoSuchElementError`. + * + * @see {@link filterMapOrElse} for continuing with a fallback effect when the filter fails + * @see {@link filterOrFail} for validating with a predicate instead of a `Filter` + * @see {@link filterMap} for filtering and mapping iterable elements + * + * @category filtering + * @since 4.0.0 + */ +export const filterMapOrFail: { + ( + filter: Filter.Filter, B, X>, + orFailWith: (x: X) => E2 + ): (self: Effect) => Effect + ( + filter: Filter.Filter, B, X> + ): (self: Effect) => Effect + ( + self: Effect, + filter: Filter.Filter, + orFailWith: (x: X) => E2 + ): Effect + ( + self: Effect, + filter: Filter.Filter + ): Effect +} = internal.filterMapOrFail + +// ----------------------------------------------------------------------------- +// Conditional Operators +// ----------------------------------------------------------------------------- + +/** + * Runs an effect conditionally based on the result of an effectful boolean + * condition. + * + * **When to use** + * + * Use when you need an effectful check to decide whether another effect should + * run while representing the skipped case explicitly. + * + * **Details** + * + * The condition effect is evaluated first. If it succeeds with `true`, the + * source effect is run and its success value is wrapped in `Option.some`. If it + * succeeds with `false`, the source effect is skipped and the result is + * `Option.none`. If the condition effect fails, that failure is preserved. + * + * **Example** (Conditionally running an effect) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const shouldLog = true + * + * const program = Effect.when( + * Console.log("Condition is true!"), + * Effect.succeed(shouldLog) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Condition is true!" + * // { _id: 'Option', _tag: 'Some', value: undefined } + * ``` + * + * @category conditional operators + * @since 2.0.0 + */ +export const when: { + ( + condition: Effect + ): (self: Effect) => Effect, E | E2, R | R2> + ( + self: Effect, + condition: Effect + ): Effect, E | E2, R | R2> +} = internal.when + +// ----------------------------------------------------------------------------- +// Pattern matching +// ----------------------------------------------------------------------------- + +/** + * Handles both success and failure cases of an effect without performing side + * effects. + * + * **When to use** + * + * Use when you need to fold an `Effect` into a value by handling success and + * failure differently without triggering side effects. + * + * **Details** + * + * `match` lets you define custom handlers for both success and failure + * scenarios. You provide separate functions to handle each case, allowing you + * to process the result if the effect succeeds, or handle the error if the + * effect fails. + * + * **Example** (Matching success and failure values) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class ExampleError extends Data.TaggedError("ExampleError")<{ readonly message: string }> {} + * + * const success: Effect.Effect = Effect.succeed(42) + * + * const program1 = Effect.match(success, { + * onFailure: (error) => `failure: ${error.message}`, + * onSuccess: (value) => `success: ${value}` + * }) + * + * // Run and log the result of the successful effect + * Effect.runPromise(program1).then(console.log) + * // Output: "success: 42" + * + * const failure: Effect.Effect = Effect.fail( + * new ExampleError({ message: "Uh oh!" }) + * ) + * + * const program2 = Effect.match(failure, { + * onFailure: (error) => `failure: ${error.message}`, + * onSuccess: (value) => `success: ${value}` + * }) + * + * // Run and log the result of the failed effect + * Effect.runPromise(program2).then(console.log) + * // Output: "failure: Uh oh!" + * ``` + * + * @see {@link matchEffect} if you need to perform side effects in the handlers. + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect +} = internal.match + +/** + * Handles both success and failure cases of an effect without performing side + * effects, with eager evaluation for resolved effects. + * + * **When to use** + * + * Use when you need to handle both success and failure cases of an + * already-resolved `Effect` with optimized handling. + * + * **Details** + * + * `matchEager` works like `match` but provides better performance for resolved + * effects (Success or Failure). When the effect is already resolved, it applies + * the handlers immediately without fiber scheduling. For unresolved effects, + * it falls back to the regular `match` behavior. + * + * **Example** (Pattern matching eagerly when possible) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Effect.matchEager(Effect.succeed(42), { + * onFailure: (error) => `Failed: ${error}`, + * onSuccess: (value) => `Success: ${value}` + * }) + * console.log(result) // "Success: 42" + * }) + * ``` + * + * @see {@link match} for the non-eager version. + * @see {@link matchEffect} if you need to perform side effects in the handlers. + * @category pattern matching + * @since 4.0.0 + */ +export const matchEager: { + (options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect +} = internal.matchEager + +/** + * Handles failures by matching the cause of failure. + * + * **When to use** + * + * Use when you need to fold an `Effect` while the failure handler inspects the + * full `Cause`. + * + * **Details** + * + * The `matchCause` function allows you to handle failures with access to the + * full cause of the failure within a fiber. + * + * **Example** (Matching on success or failure causes) + * + * ```ts + * import { Cause, Effect } from "effect" + * + * const task = Effect.fail("Something went wrong") + * + * const program = Effect.matchCause(task, { + * onFailure: (cause) => `Failed: ${Cause.squash(cause)}`, + * onSuccess: (value) => `Success: ${value}` + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: "Failed: Error: Something went wrong" + * ``` + * + * @see {@link matchCauseEffect} if you need to perform side effects in the + * handlers. + * @see {@link match} if you don't need to handle the cause of the failure. + * @category pattern matching + * @since 2.0.0 + */ +export const matchCause: { + (options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Effect +} = internal.matchCause + +/** + * Handles failures by matching the cause of failure with eager evaluation. + * + * **When to use** + * + * Use when you expect an `Effect` to already be resolved and want to match the + * `Cause` without regular effect pipeline overhead. + * + * **Details** + * + * `matchCauseEager` works like `matchCause` but provides better performance for resolved + * effects by immediately applying the matching function instead of deferring it + * through the effect pipeline. + * + * **Example** (Eagerly matching already completed effects) + * + * ```ts + * import { Effect } from "effect" + * + * const handleResult = Effect.matchCauseEager(Effect.succeed(42), { + * onSuccess: (value) => `Success: ${value}`, + * onFailure: (cause) => `Failed: ${cause}` + * }) + * ``` + * + * @category pattern matching + * @since 4.0.0 + */ +export const matchCauseEager: { + (options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect +} = internal.matchCauseEager + +/** + * Handles success or failure eagerly with effectful handlers when the effect is already resolved. + * + * **When to use** + * + * Use when you need effectful success and cause-aware failure handlers for + * `Effect` inputs that may already be resolved. + * + * **Details** + * + * If the effect is an `Exit`, the matching handler runs immediately; otherwise it behaves like + * {@link matchCauseEffect}. + * + * @see {@link matchCauseEffect} for the non-eager effectful variant + * @see {@link matchCauseEager} for eager cause matching with pure handlers + * @see {@link matchEffect} for effectful matching on typed failures instead of full causes + * + * @category pattern matching + * @since 4.0.0 + */ +export const matchCauseEffectEager: { + ( + options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + } + ): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = internal.matchCauseEffectEager + +/** + * Handles failures with access to the cause and allows performing side effects. + * + * **When to use** + * + * Use when you need to fold an `Effect` with effectful success handlers and + * `Cause`-aware failure handlers. + * + * **Details** + * + * The `matchCauseEffect` function works similarly to {@link matchCause}, but it + * also allows you to perform additional side effects based on the failure + * cause. This function provides access to the complete cause of the failure, + * making it possible to differentiate between various failure types, and allows + * you to respond accordingly while performing side effects (like logging or + * other operations). + * + * **Example** (Effectfully matching on causes) + * + * ```ts + * import { Cause, Console, Data, Effect, Result } from "effect" + * + * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} + * + * const task = Effect.fail(new TaskError({ message: "Task failed" })) + * + * const program = Effect.matchCauseEffect(task, { + * onFailure: (cause) => + * Effect.gen(function*() { + * if (Cause.hasFails(cause)) { + * const error = Cause.findError(cause) + * if (Result.isSuccess(error)) { + * yield* Console.log(`Handling error: ${error.success.message}`) + * } + * return "recovered from error" + * } else { + * yield* Console.log("Handling interruption or defect") + * return "recovered from interruption/defect" + * } + * }), + * onSuccess: (value) => + * Effect.gen(function*() { + * yield* Console.log(`Success: ${value}`) + * return `processed ${value}` + * }) + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Handling error: Task failed + * // recovered from error + * ``` + * + * @see {@link matchCause} if you don't need side effects and only want to handle the result or failure. + * @see {@link matchEffect} if you don't need to handle the cause of the failure. + * + * @category pattern matching + * @since 2.0.0 + */ +export const matchCauseEffect: { + (options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = internal.matchCauseEffect + +/** + * Handles both success and failure by running effectful handlers. + * + * **When to use** + * + * Use when you need to handle an `Effect`'s failure or success with handlers + * that return effects. + * + * **Details** + * + * Use `matchEffect` when either branch needs to return an `Effect`, such as + * performing logging, recovery, notification, or other effectful work. The + * returned effect succeeds or fails according to the handler that is run. + * + * **Example** (Matching success and failure with effectful handlers) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class ExampleError extends Data.TaggedError("ExampleError")<{ readonly message: string }> {} + * + * const success: Effect.Effect = Effect.succeed(42) + * const failure: Effect.Effect = Effect.fail( + * new ExampleError({ message: "Uh oh!" }) + * ) + * + * const program1 = Effect.matchEffect(success, { + * onFailure: (error) => + * Effect.succeed(`failure: ${error.message}`).pipe( + * Effect.tap(Effect.log) + * ), + * onSuccess: (value) => + * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * }) + * + * console.log(Effect.runSync(program1)) + * // Output: + * // timestamp=... level=INFO fiber=#0 message="success: 42" + * // success: 42 + * + * const program2 = Effect.matchEffect(failure, { + * onFailure: (error) => + * Effect.succeed(`failure: ${error.message}`).pipe( + * Effect.tap(Effect.log) + * ), + * onSuccess: (value) => + * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * }) + * + * console.log(Effect.runSync(program2)) + * // Output: + * // timestamp=... level=INFO fiber=#1 message="failure: Uh oh!" + * // failure: Uh oh! + * ``` + * + * @see {@link match} if you don't need side effects and only want to handle the + * result or failure. + * @category pattern matching + * @since 2.0.0 + */ +export const matchEffect: { + (options: { + readonly onFailure: (e: E) => Effect + readonly onSuccess: (a: A) => Effect + }): (self: Effect) => Effect + ( + self: Effect, + options: { + readonly onFailure: (e: E) => Effect + readonly onSuccess: (a: A) => Effect + } + ): Effect +} = internal.matchEffect + +// ----------------------------------------------------------------------------- +// Condition checking +// ----------------------------------------------------------------------------- + +/** + * Determines whether an effect fails. + * + * **Details** + * + * Defects are not converted; if the effect dies, the resulting effect dies too. + * + * **Example** (Checking whether an effect fails) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const failed = yield* Effect.isFailure(Effect.fail("Uh oh!")) + * yield* Console.log(failed) + * }) + * + * Effect.runPromise(program) + * // Output: true + * ``` + * + * @category condition checking + * @since 2.0.0 + */ +export const isFailure: (self: Effect) => Effect = internal.isFailure + +/** + * Returns whether an effect completes successfully. + * + * **Details** + * + * Returns `false` for failures in the error channel, but defects still fail the + * effect. + * + * **Example** (Checking whether an effect succeeds) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const ok = yield* Effect.isSuccess(Effect.succeed("done")) + * const failed = yield* Effect.isSuccess(Effect.fail("Uh oh")) + * yield* Console.log(`ok: ${ok}`) + * yield* Console.log(`failed: ${failed}`) + * }) + * + * Effect.runPromise(program) + * // Output: + * // ok: true + * // failed: false + * ``` + * + * @category condition checking + * @since 2.0.0 + */ +export const isSuccess: (self: Effect) => Effect = internal.isSuccess + +// ----------------------------------------------------------------------------- +// Environment +// ----------------------------------------------------------------------------- + +/** + * Returns the complete context. + * + * **When to use** + * + * Use to read the complete `Context` available to the current effect. + * + * **Details** + * + * This function allows you to access all services that are currently available + * in the effect's environment. This can be useful for debugging, introspection, + * or when you need to pass the entire context to another function. + * + * **Example** (Reading the full context) + * + * ```ts + * import { Console, Context, Effect, Option } from "effect" + * + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * const Database = Context.Service<{ + * query: (sql: string) => string + * }>("Database") + * + * const program = Effect.gen(function*() { + * const allServices = yield* Effect.context() + * + * // Check if specific services are available + * const loggerOption = Context.getOption(allServices, Logger) + * const databaseOption = Context.getOption(allServices, Database) + * + * yield* Console.log(`Logger available: ${Option.isSome(loggerOption)}`) + * yield* Console.log(`Database available: ${Option.isSome(databaseOption)}`) + * }) + * + * const context = Context.make(Logger, { log: console.log }) + * .pipe(Context.add(Database, { query: () => "result" })) + * + * const provided = Effect.provideContext(program, context) + * ``` + * + * @see {@link contextWith} for deriving an effect from the complete context + * @see {@link service} for reading one service from the context + * + * @category environment + * @since 2.0.0 + */ +export const context: () => Effect, never, R> = internal.context + +/** + * Transforms the current context using the provided function. + * + * **When to use** + * + * Use to derive an effect from the complete `Context`. + * + * **Details** + * + * This function allows you to access the complete context and perform + * computations based on all available services. This is useful when you need + * to conditionally execute logic based on what services are available. + * + * **Example** (Deriving values from the context) + * + * ```ts + * import { Console, Context, Effect, Option } from "effect" + * + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * const Cache = Context.Service<{ + * get: (key: string) => string | null + * }>("Cache") + * + * const program = Effect.contextWith((services) => { + * const cacheOption = Context.getOption(services, Cache) + * const hasCache = Option.isSome(cacheOption) + * + * if (hasCache) { + * return Effect.gen(function*() { + * const cache = yield* Effect.service(Cache) + * yield* Console.log("Using cached data") + * return cache.get("user:123") || "default" + * }) + * } else { + * return Effect.gen(function*() { + * yield* Console.log("No cache available, using fallback") + * return "fallback data" + * }) + * } + * }) + * + * const withCache = Effect.provideService(program, Cache, { + * get: () => "cached_value" + * }) + * ``` + * + * @see {@link context} for reading the complete context as a value + * @see {@link service} for reading one service from the context + * + * @category environment + * @since 2.0.0 + */ +export const contextWith: ( + f: (context: Context.Context) => Effect +) => Effect = internal.contextWith + +/** + * Provides dependencies to an effect using layers or a context. Use `options.local` + * to build the layer every time; by default, layers are shared between provide + * calls. + * + * **Example** (Providing dependencies with a layer) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * interface Database { + * readonly query: (sql: string) => Effect.Effect + * } + * + * const Database = Context.Service("Database") + * + * const DatabaseLive = Layer.succeed(Database)({ + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`)) + * }) + * + * const program = Effect.gen(function*() { + * const db = yield* Database + * return yield* db.query("SELECT * FROM users") + * }) + * + * const provided = Effect.provide(program, DatabaseLive) + * + * Effect.runPromise(provided).then(console.log) + * // Output: "Result for: SELECT * FROM users" + * ``` + * + * @category environment + * @since 2.0.0 + */ +export const provide: { + ]>( + layers: Layers, + options?: { + readonly local?: boolean | undefined + } | undefined + ): ( + self: Effect + ) => Effect< + A, + E | Layer.Error, + Layer.Services | Exclude> + > + ( + layer: Layer.Layer, + options?: { + readonly local?: boolean | undefined + } | undefined + ): ( + self: Effect + ) => Effect> + ( + context: Context.Context + ): (self: Effect) => Effect> + ]>( + self: Effect, + layers: Layers, + options?: { + readonly local?: boolean | undefined + } | undefined + ): Effect< + A, + E | Layer.Error, + Layer.Services | Exclude> + > + ( + self: Effect, + layer: Layer.Layer, + options?: { + readonly local?: boolean | undefined + } | undefined + ): Effect> + ( + self: Effect, + context: Context.Context + ): Effect> +} = internalLayer.provide + +/** + * Provides a context to an effect, fulfilling its service requirements. + * + * **Details** + * + * This function provides multiple services at once by supplying a context + * that contains all the required services. It removes the provided services + * from the effect's requirements, making them available to the effect. + * + * **Example** (Providing a complete context) + * + * ```ts + * import { Context, Effect } from "effect" + * + * // Define service keys + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * const Database = Context.Service<{ + * query: (sql: string) => string + * }>("Database") + * + * // Create a context with multiple services + * const context = Context.make(Logger, { log: console.log }) + * .pipe(Context.add(Database, { query: () => "result" })) + * + * // An effect that requires both services + * const program = Effect.gen(function*() { + * const logger = yield* Effect.service(Logger) + * const db = yield* Effect.service(Database) + * logger.log("Querying database") + * return db.query("SELECT * FROM users") + * }) + * + * const provided = Effect.provideContext(program, context) + * ``` + * + * @category environment + * @since 4.0.0 + */ +export const provideContext: { + ( + context: Context.Context + ): (self: Effect) => Effect> + ( + self: Effect, + context: Context.Context + ): Effect> +} = internal.provideContext + +/** + * Runs an effect with the provided context as its complete environment. + * + * **When to use** + * + * Use when you already have a `Context` containing every service required by + * the effect and want the wrapped effect to run with exactly that context. + * + * **Gotchas** + * + * `setContext` replaces the current context for the wrapped effect. Services + * from an outer context are not inherited unless they are also present in the + * context passed to `setContext`. + * + * **Example** (Running with a complete context) + * + * ```ts + * import { Context, Effect } from "effect" + * + * class Config extends Context.Service()("Config") {} + * + * const program = Effect.gen(function*() { + * const config = yield* Effect.service(Config) + * return `${config.greeting}, World!` + * }) + * + * const context = Context.make(Config, { greeting: "Hello" }) + * + * const runnable = Effect.setContext(program, context) + * + * Effect.runPromise(runnable).then(console.log) + * // Output: "Hello, World!" + * ``` + * + * @see {@link provideContext} for partially satisfying an effect's context requirements. + * @see {@link updateContext} for deriving the required context from the current one. + * + * @category environment + * @since 4.0.0 + */ +export const setContext: { + (context: Context.Context): (self: Effect) => Effect + (self: Effect, context: Context.Context): Effect +} = internal.setContext + +/** + * Accesses a service from the context. + * + * **Example** (Accessing a required service) + * + * ```ts + * import { Context, Effect } from "effect" + * + * interface Database { + * readonly query: (sql: string) => Effect.Effect + * } + * + * const Database = Context.Service("Database") + * + * const program = Effect.gen(function*() { + * const db = yield* Effect.service(Database) + * return yield* db.query("SELECT * FROM users") + * }) + * ``` + * + * @category context + * @since 4.0.0 + */ +export const service: (service: Context.Key) => Effect = internal.service + +/** + * Optionally accesses a service from the environment. + * + * **When to use** + * + * Use to read an optional dependency from the current context without making + * that dependency part of the effect's required environment. + * + * **Details** + * + * This function attempts to access a service from the environment. If the + * service is available, it returns `Some(service)`. If the service is not + * available, it returns `None`. Unlike `service`, this function does not + * require the service to be present in the environment. + * + * **Example** (Accessing an optional service) + * + * ```ts + * import { Context, Effect, Option } from "effect" + * + * // Define a service key + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * + * // Use serviceOption to optionally access the logger + * const program = Effect.gen(function*() { + * const maybeLogger = yield* Effect.serviceOption(Logger) + * + * if (Option.isSome(maybeLogger)) { + * maybeLogger.value.log("Service is available") + * } else { + * console.log("Service not available") + * } + * }) + * ``` + * + * @category context + * @since 2.0.0 + */ +export const serviceOption: (key: Context.Key) => Effect> = internal.serviceOption + +/** + * Provides part of the required context while leaving the rest unchanged. + * + * **Details** + * + * This function allows you to transform the context required by an effect, + * providing part of the context and leaving the rest to be fulfilled later. + * + * **Example** (Updating the context before running) + * + * ```ts + * import { Context, Effect } from "effect" + * + * // Define services + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * const Config = Context.Service<{ + * name: string + * }>("Config") + * + * const program = Effect.service(Config).pipe( + * Effect.map((config) => `Hello ${config.name}!`) + * ) + * + * // Transform services by providing Config while keeping Logger requirement + * const configured = program.pipe( + * Effect.updateContext((context: Context.Context) => + * Context.add(context, Config, { name: "World" }) + * ) + * ) + * + * // The effect now requires only Logger service + * const result = Effect.provideService(configured, Logger, { + * log: (msg) => console.log(msg) + * }) + * ``` + * + * @category context + * @since 4.0.0 + */ +export const updateContext: { + ( + f: (context: Context.Context) => Context.Context> + ): (self: Effect) => Effect + ( + self: Effect, + f: (context: Context.Context) => Context.Context> + ): Effect +} = internal.updateContext + +/** + * Runs an effect with a service implementation transformed by the provided + * function. + * + * **Details** + * + * The service must be available in the effect's context; `updateService` + * replaces it for the wrapped effect with the value returned by the updater. + * + * **Example** (Replacing a service for one effect) + * + * ```ts + * import { Console, Context, Effect } from "effect" + * + * // Define a counter service + * const Counter = Context.Service<{ count: number }>("Counter") + * + * const program = Effect.gen(function*() { + * const updatedCounter = yield* Effect.service(Counter) + * yield* Console.log(`Updated count: ${updatedCounter.count}`) + * return updatedCounter.count + * }).pipe( + * Effect.updateService(Counter, (counter) => ({ count: counter.count + 1 })) + * ) + * + * // Provide initial service and run + * const result = Effect.provideService(program, Counter, { count: 0 }) + * Effect.runPromise(result).then(console.log) + * // Output: Updated count: 1 + * // 1 + * ``` + * + * @category context + * @since 2.0.0 + */ +export const updateService: { + ( + service: Context.Key, + f: (value: A) => A + ): (self: Effect) => Effect + ( + self: Effect, + service: Context.Key, + f: (value: A) => A + ): Effect +} = internal.updateService + +/** + * Provides one concrete service implementation to an effect. + * + * **When to use** + * + * Use to satisfy one service requirement with an already-built implementation. + * + * **Details** + * + * The service requirement identified by the `Context.Key` is removed from the + * effect requirements after the implementation is provided. + * + * **Example** (Providing a service value) + * + * ```ts + * import { Console, Context, Effect } from "effect" + * + * // Define a service for configuration + * const Config = Context.Service<{ + * apiUrl: string + * timeout: number + * }>("Config") + * + * const fetchData = Effect.gen(function*() { + * const config = yield* Effect.service(Config) + * yield* Console.log(`Fetching from: ${config.apiUrl}`) + * yield* Console.log(`Timeout: ${config.timeout}ms`) + * return "data" + * }) + * + * // Provide the service implementation + * const program = Effect.provideService(fetchData, Config, { + * apiUrl: "https://api.example.com", + * timeout: 5000 + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Fetching from: https://api.example.com + * // Timeout: 5000ms + * // data + * ``` + * + * @see {@link provide} for providing multiple layers to an effect. + * @see {@link provideServiceEffect} for acquiring the service implementation effectfully. + * @see {@link provideContext} for providing a complete context. + * @category context + * @since 2.0.0 + */ +export const provideService: { + ( + service: Context.Key + ): { + (implementation: S): (self: Effect) => Effect> + (self: Effect, implementation: S): Effect> + } + ( + service: Context.Key, + implementation: S + ): (self: Effect) => Effect> + ( + self: Effect, + service: Context.Key, + implementation: S + ): Effect> +} = internal.provideService + +/** + * Provides one service to an effect using an effectful acquisition. + * + * **When to use** + * + * Use when the service implementation must be created by an effect and its + * acquisition failure should remain in the returned effect. + * + * **Details** + * + * `provideServiceEffect` runs the acquisition effect to produce the service + * implementation, removes that service from the wrapped effect's requirements, + * and leaves any other requirements to be provided later. Acquisition failures + * are included in the returned effect's error channel. + * + * **Example** (Providing a service with an effect) + * + * ```ts + * import { Console, Context, Effect } from "effect" + * + * // Define a database connection service + * interface DatabaseConnection { + * readonly query: (sql: string) => Effect.Effect + * } + * const Database = Context.Service("Database") + * + * // Effect that creates a database connection + * const createConnection = Effect.gen(function*() { + * yield* Console.log("Establishing database connection...") + * yield* Effect.sleep("100 millis") // Simulate connection time + * yield* Console.log("Database connected!") + * return { + * query: (sql: string) => Effect.succeed(`Result for: ${sql}`) + * } + * }) + * + * const program = Effect.gen(function*() { + * const db = yield* Effect.service(Database) + * return yield* db.query("SELECT * FROM users") + * }) + * + * // Provide the service through an effect + * const withDatabase = Effect.provideServiceEffect( + * program, + * Database, + * createConnection + * ) + * + * Effect.runPromise(withDatabase).then(console.log) + * // Output: + * // Establishing database connection... + * // Database connected! + * // Result for: SELECT * FROM users + * ``` + * + * @category context + * @since 2.0.0 + */ +export const provideServiceEffect: { + ( + service: Context.Key, + acquire: Effect + ): (self: Effect) => Effect | R2> + ( + self: Effect, + service: Context.Key, + acquire: Effect + ): Effect | R2> +} = internal.provideServiceEffect + +// ----------------------------------------------------------------------------- +// References +// ----------------------------------------------------------------------------- + +/** + * Sets the concurrency level for parallel operations within an effect. + * + * **Example** (Setting local concurrency) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const task = (id: number) => + * Effect.gen(function*() { + * yield* Console.log(`Task ${id} starting`) + * yield* Effect.sleep("100 millis") + * yield* Console.log(`Task ${id} completed`) + * return id + * }) + * + * // Run tasks with limited concurrency (max 2 at a time) + * const program = Effect.gen(function*() { + * const tasks = [1, 2, 3, 4, 5].map(task) + * return yield* Effect.all(tasks, { concurrency: 2 }) + * }).pipe( + * Effect.withConcurrency(2) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Tasks will run with max 2 concurrent operations + * // [1, 2, 3, 4, 5] + * ``` + * + * @category references + * @since 2.0.0 + */ +export const withConcurrency: { + ( + concurrency: number | "unbounded" + ): (self: Effect) => Effect + ( + self: Effect, + concurrency: number | "unbounded" + ): Effect +} = internal.withConcurrency + +// ----------------------------------------------------------------------------- +// Resource management & finalization +// ----------------------------------------------------------------------------- + +/** + * Returns the current scope for resource management. + * + * **Example** (Accessing the current scope) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const currentScope = yield* Effect.scope + * yield* Console.log("Got scope for resource management") + * + * // Use the scope to manually manage resources if needed + * const resource = yield* Effect.acquireRelease( + * Console.log("Acquiring resource").pipe(Effect.as("resource")), + * () => Console.log("Releasing resource") + * ) + * + * return resource + * }) + * + * Effect.runPromise(Effect.scoped(program)).then(console.log) + * // Output: + * // Got scope for resource management + * // Acquiring resource + * // resource + * // Releasing resource + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const scope: Effect = internal.scope + +/** + * Runs an effect with a scope that closes when the effect completes. + * + * **When to use** + * + * Use to acquire scoped resources for the duration of a single workflow. + * + * **Details** + * + * Finalizers for resources acquired inside the workflow run as soon as the + * workflow completes, whether by success, failure, or interruption. + * + * **Example** (Running a scoped acquisition) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const resource = Effect.acquireRelease( + * Console.log("Acquiring resource").pipe(Effect.as("resource")), + * () => Console.log("Releasing resource") + * ) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const res = yield* resource + * yield* Console.log(`Using ${res}`) + * return res + * }) + * ) + * + * Effect.runFork(program) + * // Output: "Acquiring resource" + * // Output: "Using resource" + * // Output: "Releasing resource" + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const scoped: ( + self: Effect +) => Effect> = internal.scoped + +/** + * Creates a scoped effect by providing access to the scope. + * + * **When to use** + * + * Use when resource acquisition needs direct access to the scope being created, + * for example to register finalizers manually. + * + * **Example** (Working with an explicit scope) + * + * ```ts + * import { Console, Effect, Scope } from "effect" + * + * const program = Effect.scopedWith((scope) => + * Effect.gen(function*() { + * yield* Console.log("Inside scoped context") + * + * // Manually add a finalizer to the scope + * yield* Scope.addFinalizer(scope, Console.log("Manual finalizer")) + * + * // Create a scoped resource + * const resource = yield* Effect.scoped( + * Effect.acquireRelease( + * Console.log("Acquiring resource").pipe(Effect.as("resource")), + * () => Console.log("Releasing resource") + * ) + * ) + * + * return resource + * }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Inside scoped context + * // Acquiring resource + * // resource + * // Releasing resource + * // Manual finalizer + * ``` + * + * @category resource management + * @since 3.11.0 + */ +export const scopedWith: ( + f: (scope: Scope) => Effect +) => Effect = internal.scopedWith + +/** + * Constructs a scoped resource from an acquisition effect and a release + * finalizer. + * + * **When to use** + * + * Use to acquire a scoped resource with an explicit release finalizer. + * + * **Details** + * + * If acquisition succeeds, the release finalizer is added to the current scope + * and is guaranteed to run when that scope closes. The finalizer receives the + * `Exit` value used to close the scope. + * + * By default, acquisition is protected by an uninterruptible region. Pass + * `{ interruptible: true }` to allow the acquisition effect to be interrupted. + * + * **Example** (Acquiring and releasing a resource) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * // Simulate a resource that needs cleanup + * interface FileHandle { + * readonly path: string + * readonly content: string + * } + * + * // Acquire a file handle + * const acquire = Effect.gen(function*() { + * yield* Console.log("Opening file") + * return { path: "/tmp/file.txt", content: "file content" } + * }) + * + * // Release the file handle + * const release = (handle: FileHandle, exit: Exit.Exit) => + * Console.log( + * `Closing file ${handle.path} with exit: ${ + * Exit.isSuccess(exit) ? "success" : "failure" + * }` + * ) + * + * // Create a scoped resource + * const resource = Effect.acquireRelease(acquire, release) + * + * // Use the resource within a scope + * const program = Effect.scoped( + * Effect.gen(function*() { + * const handle = yield* resource + * yield* Console.log(`Using file: ${handle.path}`) + * return handle.content + * }) + * ) + * ``` + * + * @see {@link acquireDisposable} for resources that implement JavaScript disposal protocols + * @see {@link acquireUseRelease} for bracketing acquire, use, and release in one effect + * + * @category resource management + * @since 2.0.0 + */ +export const acquireRelease: ( + acquire: Effect, + release: (a: A, exit: Exit.Exit) => Effect, + options?: { readonly interruptible?: boolean } +) => Effect = internal.acquireRelease + +/** + * Acquires a scoped resource that implements JavaScript disposal protocols. + * + * **When to use** + * + * Use when you work with JavaScript `Disposable` or `AsyncDisposable` resources + * that should be closed with the surrounding scope. + * + * **Details** + * + * The resource is automatically disposed when the surrounding + * {@link Scope} is closed, using {@link Symbol.dispose} for + * synchronous disposables or {@link Symbol.asyncDispose} for asynchronous + * disposables. + * + * This is similar to {@link acquireRelease}, but uses the standard + * JavaScript disposal protocol instead of requiring an explicit release + * function. It works with JavaScript `Disposable` and `AsyncDisposable` + * resources. + * + * **Example** (Acquiring a disposable resource) + * + * ```ts + * import sqlite from "node:sqlite"; + * import { Effect } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function* () { + * // acquire database connection + * // database will be closed when the scope is closed + * const db = yield* Effect.acquireDisposable( + * Effect.sync(() => new sqlite.DatabaseSync(":memory:")) + * ) + * + * const row = db.prepare("SELECT 1 AS value").get() + * yield* Effect.log(row) // { value: 1 } + * }) + * ) + * ``` + * + * @see {@link acquireRelease} for resources that need an explicit finalizer + * + * @category resource management + * @since 4.0.0 + */ +export const acquireDisposable: ( + acquire: Effect +) => Effect = internal.acquireDisposable + +/** + * Runs resource acquisition, usage, and release as one bracketed effect. + * + * **When to use** + * + * Use to bracket acquire, use, and release logic in one effect. + * + * **Details** + * + * `acquireUseRelease` does the following: + * + * 1. Ensures that the `Effect` value that acquires the resource will not be + * interrupted. Note that acquisition may still fail due to internal + * reasons (such as an uncaught exception). + * 2. Ensures that the `release` `Effect` value will not be interrupted, + * and will be executed as long as the acquisition `Effect` value + * successfully acquires the resource. + * + * During the time period between the acquisition and release of the resource, + * the `use` `Effect` value will be executed. + * + * If the `release` `Effect` value fails, then the entire `Effect` value will + * fail, even if the `use` `Effect` value succeeds. If this fail-fast behavior + * is not desired, errors produced by the `release` `Effect` value can be caught + * and ignored. + * + * **Example** (Acquiring resources with cleanup) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * interface Database { + * readonly connection: string + * readonly query: (sql: string) => Effect.Effect + * } + * + * const program = Effect.acquireUseRelease( + * // Acquire - connect to database + * Effect.gen(function*() { + * yield* Console.log("Connecting to database...") + * return { + * connection: "db://localhost:5432", + * query: (sql: string) => Effect.succeed(`Result for: ${sql}`) + * } + * }), + * // Use - perform database operations + * (db) => + * Effect.gen(function*() { + * yield* Console.log(`Connected to ${db.connection}`) + * const result = yield* db.query("SELECT * FROM users") + * yield* Console.log(`Query result: ${result}`) + * return result + * }), + * // Release - close database connection + * (db, exit) => + * Effect.gen(function*() { + * if (Exit.isSuccess(exit)) { + * yield* Console.log(`Closing connection to ${db.connection} (success)`) + * } else { + * yield* Console.log(`Closing connection to ${db.connection} (failure)`) + * } + * }) + * ) + * + * Effect.runPromise(program) + * // Output: + * // Connecting to database... + * // Connected to db://localhost:5432 + * // Query result: Result for: SELECT * FROM users + * // Closing connection to db://localhost:5432 (success) + * ``` + * + * @see {@link acquireRelease} for scoped resources whose use happens later + * + * @category resource management + * @since 2.0.0 + */ +export const acquireUseRelease: ( + acquire: Effect, + use: (a: Resource) => Effect, + release: (a: Resource, exit: Exit.Exit) => Effect +) => Effect = internal.acquireUseRelease + +/** + * Adds a finalizer to the current scope. + * + * **When to use** + * + * Use to register low-level cleanup in the current scope. + * + * **Details** + * + * The finalizer runs when the surrounding scope is closed and receives the + * `Exit` value used to close the scope. + * + * **Example** (Registering scope finalizers) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * // Add a finalizer that runs when the scope closes + * yield* Effect.addFinalizer((exit) => + * Console.log( + * Exit.isSuccess(exit) + * ? "Cleanup: Operation completed successfully" + * : "Cleanup: Operation failed, cleaning up resources" + * ) + * ) + * + * yield* Console.log("Performing main operation...") + * + * // This could succeed or fail + * return "operation result" + * }) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Performing main operation... + * // Cleanup: Operation completed successfully + * // operation result + * ``` + * + * @see {@link acquireRelease} for resource acquisition with a release finalizer + * @see {@link ensuring} for attaching a finalizer to one effect + * + * @category resource management + * @since 2.0.0 + */ +export const addFinalizer: ( + finalizer: (exit: Exit.Exit) => Effect +) => Effect = internal.addFinalizer + +/** + * Returns an effect that, if this effect _starts_ execution, then the + * specified `finalizer` is guaranteed to be executed, whether this effect + * succeeds, fails, or is interrupted. + * + * **Details** + * + * For use cases that need access to the effect's result, see `onExit`. + * + * Finalizers offer very powerful guarantees, but they are low-level, and + * should generally not be used for releasing resources. For higher-level + * logic built on `ensuring`, see the `acquireRelease` family of methods. + * + * **Example** (Always running cleanup) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Console.log("Task started") + * yield* Effect.sleep("1 second") + * yield* Console.log("Task completed") + * return 42 + * }) + * + * // Ensure cleanup always runs, regardless of success or failure + * const program = Effect.ensuring( + * task, + * Console.log("Cleanup: This always runs!") + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Task started + * // Task completed + * // Cleanup: This always runs! + * // 42 + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const ensuring: { + ( + finalizer: Effect + ): (self: Effect) => Effect + ( + self: Effect, + finalizer: Effect + ): Effect +} = internal.ensuring + +/** + * Runs the specified effect if this effect fails, providing the error to the + * effect if it exists. The provided effect will not be interrupted. + * + * **Example** (Running cleanup on failure) + * + * ```ts + * import { Cause, Console, Data, Effect } from "effect" + * + * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} + * + * const task = Effect.fail(new TaskError({ message: "Something went wrong" })) + * + * const program = Effect.onError( + * task, + * (cause) => Console.log(`Cleanup on error: ${Cause.squash(cause)}`) + * ) + * + * Effect.runPromise(program).catch(console.error) + * // Output: + * // Cleanup on error: TaskError: Something went wrong + * // TaskError: Something went wrong + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const onError: { + ( + cleanup: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + cleanup: (cause: Cause.Cause) => Effect + ): Effect +} = internal.onError + +/** + * Runs the finalizer only when this effect fails and the `Cause` matches the + * provided predicate. + * + * **Example** (Running cleanup for selected failures) + * + * ```ts + * import { Cause, Console, Effect } from "effect" + * + * const task = Effect.fail("boom") + * + * const program = Effect.onErrorIf( + * task, + * Cause.hasFails, + * (cause) => + * Effect.gen(function*() { + * yield* Console.log(`Cause: ${Cause.pretty(cause)}`) + * }) + * ) + * ``` + * + * @category resource management + * @since 4.0.0 + */ +export const onErrorIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect + ): Effect +} = internal.onErrorIf + +/** + * Runs the finalizer only when this effect fails and the cause matches the provided `Filter`. + * + * **When to use** + * + * Use when cleanup or diagnostics should run only for failures whose full + * `Cause` is accepted or transformed by a `Filter`, and the finalizer needs the + * filter's pass value plus the original cause. + * + * @see {@link onError} for cleanup on every failure + * @see {@link onErrorIf} for selecting failures with a boolean predicate + * @see {@link onExitFilter} for selecting from every exit instead of only failures + * + * @category resource management + * @since 4.0.0 + */ +export const onErrorFilter: { + ( + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect + ): Effect +} = internal.onErrorFilter + +/** + * Runs an optional finalizer with the effect's `Exit` value when the effect + * completes. + * + * **When to use** + * + * Use when you are building a low-level `Effect` operator that must inspect the + * source effect's `Exit`, may skip finalization by returning `undefined`, or + * must choose whether finalization is forced into an uninterruptible region. + * + * **Details** + * + * This low-level operator preserves the source effect's result unless the + * finalizer fails. Prefer `onExit` for normal cleanup logic. + * + * @see {@link onExit} for ordinary exit-aware cleanup whose finalizer always returns an effect + * + * @category resource management + * @since 4.0.0 + */ +export const onExitPrimitive: ( + self: Effect, + f: (exit: Exit.Exit) => Effect | undefined, + interruptible?: boolean +) => Effect = internal.onExitPrimitive + +/** + * Ensures that a cleanup function runs whether this effect succeeds, fails, or + * is interrupted. + * + * **Example** (Observing every exit) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * const task = Effect.succeed(42) + * + * const program = Effect.onExit(task, (exit) => + * Console.log( + * Exit.isSuccess(exit) + * ? `Task succeeded with: ${exit.value}` + * : `Task failed: ${Exit.isFailure(exit) ? exit.cause : "interrupted"}` + * )) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Task succeeded with: 42 + * // 42 + * ``` + * + * @category resource management + * @since 2.0.0 + */ +export const onExit: { + ( + f: (exit: Exit.Exit) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (exit: Exit.Exit) => Effect + ): Effect +} = internal.onExit + +/** + * Runs the cleanup effect only when the `Exit` satisfies the provided + * predicate. + * + * **Example** (Observing selected exits) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * const program = Effect.onExitIf( + * Effect.succeed(42), + * Exit.isSuccess, + * (exit) => + * Exit.isSuccess(exit) + * ? Console.log(`Succeeded with: ${exit.value}`) + * : Effect.void + * ) + * ``` + * + * @category resource management + * @since 4.0.0 + */ +export const onExitIf: { + ( + predicate: Predicate.Predicate, NoInfer>>, + f: (exit: Exit.Exit, NoInfer>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + predicate: Predicate.Predicate, NoInfer>>, + f: (exit: Exit.Exit, NoInfer>) => Effect + ): Effect +} = internal.onExitIf + +/** + * Runs the cleanup effect only when the `Exit` matches the provided `Filter`. + * + * **When to use** + * + * Use when cleanup should run only for `Exit` values selected by a `Filter`, + * and the cleanup needs the extracted pass value together with the original + * `Exit`. + * + * **Details** + * + * `Result.fail` skips cleanup, and `Result.succeed` runs cleanup with the + * selected value and the original `Exit`. + * + * @see {@link onExit} for cleanup on every exit + * @see {@link onExitIf} for selecting exits with a boolean predicate + * @see {@link onErrorFilter} for selecting only failure causes + * + * @category resource management + * @since 4.0.0 + */ +export const onExitFilter: { + ( + filter: Filter.Filter, NoInfer>, B, X>, + f: (b: B, exit: Exit.Exit, NoInfer>) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + filter: Filter.Filter, NoInfer>, B, X>, + f: (b: B, exit: Exit.Exit, NoInfer>) => Effect + ): Effect +} = internal.onExitFilter + +// ----------------------------------------------------------------------------- +// Caching +// ----------------------------------------------------------------------------- + +/** + * Returns an effect that lazily computes a result and caches it for subsequent + * evaluations. + * + * **When to use** + * + * Use when you need an expensive or time-consuming operation to be evaluated + * once and reused by later callers. + * + * **Details** + * + * This function wraps an effect and ensures that its result is computed only + * once. Once the result is computed, it is cached, meaning that subsequent + * evaluations of the same effect will return the cached result without + * re-executing the logic. + * + * **Example** (Memoizing an effect until invalidated) + * + * ```ts + * import { Console, Effect } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function*() { + * console.log("non-cached version:") + * yield* expensiveTask.pipe(Effect.andThen(Console.log)) + * yield* expensiveTask.pipe(Effect.andThen(Console.log)) + * console.log("cached version:") + * const cached = yield* Effect.cached(expensiveTask) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // non-cached version: + * // expensive task... + * // result 1 + * // expensive task... + * // result 2 + * // cached version: + * // expensive task... + * // result 3 + * // result 3 + * ``` + * + * @see {@link cachedWithTTL} for a similar function that includes a + * time-to-live duration for the cached value. + * @see {@link cachedInvalidateWithTTL} for a similar function that includes an + * additional effect for manually invalidating the cached value. + * @category caching + * @since 2.0.0 + */ +export const cached: (self: Effect) => Effect> = internal.cached + +/** + * Returns an effect that caches its result for a specified `Duration`, + * known as "timeToLive" (TTL). + * + * **When to use** + * + * Use when you need a costly effect result to be reused for a bounded duration + * before being recomputed. + * + * **Details** + * + * This function is used to cache the result of an effect for a specified amount + * of time. This means that the first time the effect is evaluated, its result + * is computed and stored. + * + * If the effect is evaluated again within the specified `timeToLive`, the + * cached result will be used, avoiding recomputation. + * + * After the specified duration has passed, the cache expires, and the effect + * will be recomputed upon the next evaluation. + * + * **Example** (Memoizing an effect with TTL) + * + * ```ts + * import { Console, Effect } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function*() { + * const cached = yield* Effect.cachedWithTTL(expensiveTask, "150 millis") + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* Effect.sleep("100 millis") + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // expensive task... + * // result 1 + * // result 1 + * // expensive task... + * // result 2 + * ``` + * + * @see {@link cached} for a similar function that caches the result + * indefinitely. + * @see {@link cachedInvalidateWithTTL} for a similar function that includes an + * additional effect for manually invalidating the cached value. + * @category caching + * @since 2.0.0 + */ +export const cachedWithTTL: { + (timeToLive: Duration.Input): (self: Effect) => Effect> + (self: Effect, timeToLive: Duration.Input): Effect> +} = internal.cachedWithTTL + +/** + * Creates a cached effect result for a specified duration and allows manual + * invalidation before expiration. + * + * **When to use** + * + * Use when an effect result should be cached for a bounded time and callers + * also need a manual invalidation effect to force recomputation before + * expiration. + * + * **Details** + * + * This function behaves similarly to {@link cachedWithTTL} by caching the + * result of an effect for a specified period of time. However, it introduces an + * additional feature: it provides an effect that allows you to manually + * invalidate the cached result before it naturally expires. + * + * This gives you more control over the cache, allowing you to refresh the + * result when needed, even if the original cache has not yet expired. + * + * Once the cache is invalidated, the next time the effect is evaluated, the + * result will be recomputed, and the cache will be refreshed. + * + * **Example** (Memoizing with TTL and invalidation) + * + * ```ts + * import { Console, Effect } from "effect" + * + * let i = 1 + * const expensiveTask = Effect.promise(() => { + * console.log("expensive task...") + * return new Promise((resolve) => { + * setTimeout(() => { + * resolve(`result ${i++}`) + * }, 100) + * }) + * }) + * + * const program = Effect.gen(function*() { + * const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL( + * expensiveTask, + * "1 hour" + * ) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* invalidate + * yield* cached.pipe(Effect.andThen(Console.log)) + * }) + * + * Effect.runFork(program) + * // Output: + * // expensive task... + * // result 1 + * // result 1 + * // expensive task... + * // result 2 + * ``` + * + * @see {@link cached} for a similar function that caches the result + * indefinitely. + * @see {@link cachedWithTTL} for a similar function that caches the result for + * a specified duration but does not include an effect for manual invalidation. + * @category caching + * @since 2.0.0 + */ +export const cachedInvalidateWithTTL: { + (timeToLive: Duration.Input): (self: Effect) => Effect<[Effect, Effect]> + (self: Effect, timeToLive: Duration.Input): Effect<[Effect, Effect]> +} = internal.cachedInvalidateWithTTL + +// ----------------------------------------------------------------------------- +// Interruption +// ----------------------------------------------------------------------------- + +/** + * Returns an effect that is immediately interrupted. + * + * **Example** (Creating an interrupted effect) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * return yield* Effect.interrupt + * yield* Effect.succeed("This won't execute and is unreachable") + * }) + * + * Effect.runPromise(program).catch(console.error) + * // Throws: InterruptedException + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const interrupt: Effect = internal.interrupt + +/** + * Returns a new effect that allows the effect to be interruptible. + * + * **Example** (Allowing interruption) + * + * ```ts + * import { Effect } from "effect" + * + * const longRunning = Effect.forever(Effect.succeed("working...")) + * + * const program = Effect.interruptible(longRunning) + * + * // This effect can now be interrupted + * const fiber = Effect.runFork(program) + * // Later: fiber.interrupt() + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const interruptible: ( + self: Effect +) => Effect = internal.interruptible + +/** + * Runs the specified finalizer effect if this effect is interrupted. + * + * **Example** (Running cleanup on interruption) + * + * ```ts + * import { Console, Effect, Fiber } from "effect" + * + * const task = Effect.forever(Effect.succeed("working...")) + * + * const program = Effect.onInterrupt( + * task, + * () => Console.log("Task was interrupted, cleaning up...") + * ) + * + * const fiber = Effect.runFork(program) + * // Later interrupt the task + * Effect.runFork(Fiber.interrupt(fiber)) + * // Output: Task was interrupted, cleaning up... + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const onInterrupt: { + ( + finalizer: (interruptors: ReadonlySet) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + finalizer: (interruptors: ReadonlySet) => Effect + ): Effect +} = internal.onInterrupt + +/** + * Returns a new effect that disables interruption for the given effect. + * + * **Example** (Preventing interruption) + * + * ```ts + * import { Console, Effect, Fiber } from "effect" + * + * const criticalTask = Effect.gen(function*() { + * yield* Console.log("Starting critical section...") + * yield* Effect.sleep("2 seconds") + * yield* Console.log("Critical section completed") + * }) + * + * const program = Effect.uninterruptible(criticalTask) + * + * const fiber = Effect.runFork(program) + * // Even if interrupted, the critical task will complete + * Effect.runPromise(Fiber.interrupt(fiber)) + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const uninterruptible: ( + self: Effect +) => Effect = internal.uninterruptible + +/** + * Disables interruption and provides a restore function to restore the + * interruptible state within the effect. + * + * **Example** (Restoring interruption in protected regions) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.uninterruptibleMask((restore) => + * Effect.gen(function*() { + * yield* Console.log("Uninterruptible phase...") + * yield* Effect.sleep("1 second") + * + * // Restore interruptibility for this part + * yield* restore( + * Effect.gen(function*() { + * yield* Console.log("Interruptible phase...") + * yield* Effect.sleep("2 seconds") + * }) + * ) + * + * yield* Console.log("Back to uninterruptible") + * }) + * ) + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const uninterruptibleMask: ( + f: ( + restore: (effect: Effect) => Effect + ) => Effect +) => Effect = internal.uninterruptibleMask + +/** + * Runs an effect in an interruptible region while providing `restore` for + * locally restoring the previous interruptibility. + * + * **Example** (Controlling interruptibility locally) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.interruptibleMask((restore) => + * Effect.gen(function*() { + * yield* Console.log("Interruptible phase...") + * yield* Effect.sleep("1 second") + * + * // Make this part uninterruptible + * yield* restore( + * Effect.gen(function*() { + * yield* Console.log("Uninterruptible phase...") + * yield* Effect.sleep("2 seconds") + * }) + * ) + * + * yield* Console.log("Back to interruptible") + * }) + * ) + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const interruptibleMask: ( + f: ( + restore: (effect: Effect) => Effect + ) => Effect +) => Effect = internal.interruptibleMask + +/** + * Creates an AbortSignal that is managed by the provided scope. + * + * **When to use** + * + * Use to obtain a scope-managed `AbortSignal` for APIs that accept cancellation + * through a signal. + * + * **Details** + * + * Each acquisition creates a fresh `AbortController`. Closing the owning scope + * runs a finalizer that aborts the controller and the effect succeeds with the + * controller's signal. + * + * **Gotchas** + * + * The signal is aborted when its owning scope closes, so avoid keeping it for + * work that outlives that scope. + * + * @see {@link scoped} for binding resource lifetime to a scope + * + * @category interruption + * @since 4.0.0 + */ +export const abortSignal: Effect = internal.abortSignal + +// ----------------------------------------------------------------------------- +// Repetition & Recursion +// ----------------------------------------------------------------------------- + +/** + * Type helpers for repeating effects. + * + * @since 2.0.0 + */ +export declare namespace Repeat { + /** + * Computes the result type of `Effect.repeat` from the original effect and repeat options. + * + * @category repetition + * @since 2.0.0 + */ + export type Return> = Effect< + O extends { until: Predicate.Refinement } ? B + : O extends { while: Predicate.Refinement } ? Exclude + : A, + | E + | (O extends { schedule: Schedule } ? E + : never) + | (O extends { while: (...args: Array) => Effect } ? E + : never) + | (O extends { until: (...args: Array) => Effect } ? E + : never), + | R + | (O extends { schedule: Schedule } ? R + : never) + | (O extends { + while: (...args: Array) => Effect + } ? R + : never) + | (O extends { + until: (...args: Array) => Effect + } ? R + : never) + > extends infer Z ? Z + : never + + /** + * Options that control whether and how an effect is repeated. + * + * @category repetition + * @since 2.0.0 + */ + export interface Options { + while?: ((_: A) => boolean | Effect) | undefined + until?: ((_: A) => boolean | Effect) | undefined + times?: number | undefined + schedule?: Schedule | undefined + } +} + +/** + * Repeats this effect forever (until the first error). + * + * **Example** (Repeating forever) + * + * ```ts + * import { Console, Effect, Fiber } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Console.log("Task running...") + * yield* Effect.sleep("1 second") + * }) + * + * // This will run forever, printing every second + * const program = task.pipe(Effect.forever) + * + * // This will run forever, without yielding every iteration + * const programNoYield = task.pipe(Effect.forever({ disableYield: true })) + * + * // Run for 5 seconds then interrupt + * const timedProgram = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild(program) + * yield* Effect.sleep("5 seconds") + * yield* Fiber.interrupt(fiber) + * }) + * ``` + * + * @category repetition + * @since 2.0.0 + */ +export const forever: < + Arg extends Effect | { + readonly disableYield?: boolean | undefined + } | undefined = { + readonly disableYield?: boolean | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly disableYield?: boolean | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect + : (self: Effect) => Effect = internal.forever + +/** + * Repeats an effect based on a specified schedule or until the first failure. + * + * **When to use** + * + * Use to rerun an effect after successful executions. + * + * **Details** + * + * This function executes an effect repeatedly according to the given schedule. + * Each repetition occurs after the initial execution of the effect, meaning + * that the schedule determines the number of additional repetitions. For + * example, using `Schedule.once` will result in the effect being executed twice + * (once initially and once as part of the repetition). + * + * If the effect succeeds, it is repeated according to the schedule. If it + * fails, the repetition stops immediately, and the failure is returned. + * + * The schedule can also specify delays between repetitions, making it useful + * for tasks like retrying operations with backoff, periodic execution, or + * performing a series of dependent actions. + * + * You can combine schedules for more advanced repetition logic, such as adding + * delays, limiting recursions, or dynamically adjusting based on the outcome of + * each execution. + * + * **Gotchas** + * + * The source effect is always evaluated once before the schedule is stepped. + * The schedule controls additional repetitions, not the initial execution. + * + * **Example** (Repeating successful effects with a schedule) + * + * ```ts + * // Success Example + * import { Console, Effect, Schedule } from "effect" + * + * const action = Console.log("success") + * const policy = Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("100 millis")) + * const program = Effect.repeat(action, policy) + * + * // Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) + * ``` + * + * **Example** (Stopping repetition on failure) + * + * ```ts + * // Failure Example + * import { Effect, Schedule } from "effect" + * + * let count = 0 + * + * // Define a callback effect that simulates an action with possible failures + * const action = Effect.callback((resume) => { + * if (count > 1) { + * console.log("failure") + * resume(Effect.fail("Uh oh!")) + * } else { + * count++ + * console.log("success") + * resume(Effect.succeed("yay!")) + * } + * }) + * + * const policy = Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("100 millis")) + * const program = Effect.repeat(action, policy) + * + * // Effect.runPromiseExit(program).then(console.log) + * ``` + * + * @see {@link retry} for failure-based repetition + * @see {@link repeatOrElse} for fallback handling when repetition fails + * + * @category repetition + * @since 2.0.0 + */ +export const repeat: { + , A>(options: O): (self: Effect) => Repeat.Return + ( + schedule: Schedule, Error, Env> + ): (self: Effect) => Effect + ( + builder: ( + $: (_: Schedule, E, R>) => Schedule + ) => Schedule, Error, Env> + ): (self: Effect) => Effect + >(self: Effect, options: O): Repeat.Return + ( + self: Effect, + schedule: Schedule, Error, Env> + ): Effect + ( + self: Effect, + builder: ( + $: (_: Schedule, E, R>) => Schedule + ) => Schedule, Error, Env> + ): Effect +} = internalSchedule.repeat + +/** + * Repeats an effect according to a schedule and runs a fallback effect if + * repetition fails before the schedule completes. + * + * **When to use** + * + * Use when successful repetitions should follow a schedule, but failures from + * the repeated effect or schedule need an effectful fallback. + * + * **Details** + * + * If the repeated effect or schedule step fails, `orElse` receives the failure + * and the latest schedule metadata when at least one schedule step has run; + * otherwise it receives `None`. If the schedule completes normally, the + * returned effect succeeds with the schedule's output. + * + * **Example** (Recovering after repetition stops) + * + * ```ts + * import { Console, Effect, Option, Schedule } from "effect" + * + * let attempt = 0 + * const task = Effect.gen(function*() { + * attempt++ + * if (attempt <= 2) { + * yield* Console.log(`Attempt ${attempt} failed`) + * return yield* Effect.fail(`Error ${attempt}`) + * } + * yield* Console.log(`Attempt ${attempt} succeeded`) + * return "success" + * }) + * + * const program = Effect.repeatOrElse( + * task, + * Schedule.recurs(3), + * (error, attempts) => + * Console.log( + * `Final failure: ${error}, after ${ + * Option.getOrElse(attempts, () => 0) + * } attempts` + * ).pipe(Effect.map(() => 0)) + * ) + * ``` + * + * @category repetition + * @since 2.0.0 + */ +export const repeatOrElse: { + ( + schedule: Schedule, + orElse: (error: E | E2, option: Option) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + schedule: Schedule, + orElse: (error: E | E2, option: Option) => Effect + ): Effect +} = internalSchedule.repeatOrElse + +/** + * Returns an array of `n` identical effects. + * + * **When to use** + * + * Use when you need an array of identical effect values without running them + * yet. + * + * **Details** + * + * This only creates the array of effects. It does not run or collect them. + * + * @see {@link all} for running the returned effects and collecting results + * @see {@link replicateEffect} for repeating an effect and collecting results in one step with concurrency and discard options + * + * @category collecting + * @since 2.0.0 + */ +export const replicate: { + (n: number): (self: Effect) => Array> + (self: Effect, n: number): Array> +} = internal.replicate + +/** + * Performs this effect `n` times and collects results with `Effect.all` semantics. + * + * **When to use** + * + * Use when you want to run the repeated effects immediately, with optional + * concurrency control or result discarding. + * + * **Details** + * + * Use `concurrency` to control parallelism and `discard: true` to ignore results. + * + * **Example** (Replicating an effect) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const results = yield* Effect.replicateEffect(3)(Effect.succeed(1)) + * yield* Console.log(results) + * }) + * ``` + * + * @category collecting + * @since 2.0.0 + */ +export const replicateEffect: { + ( + n: number, + options?: { readonly concurrency?: Concurrency | undefined; readonly discard?: false | undefined } + ): (self: Effect) => Effect, E, R> + ( + n: number, + options: { readonly concurrency?: Concurrency | undefined; readonly discard: true } + ): (self: Effect) => Effect + ( + self: Effect, + n: number, + options?: { readonly concurrency?: Concurrency | undefined; readonly discard?: false | undefined } + ): Effect, E, R> + ( + self: Effect, + n: number, + options: { readonly concurrency?: Concurrency | undefined; readonly discard: true } + ): Effect +} = internal.replicateEffect + +/** + * Runs an effect repeatedly according to a schedule and returns the schedule's + * final output. + * + * **When to use** + * + * Use to rerun a successful effect according to a `Schedule` when the schedule + * does not need a custom initial input. + * + * **Details** + * + * The schedule is first stepped with `undefined`. After each successful + * execution, the effect's success value is fed to the schedule to decide + * whether to run again. The returned effect fails if the effect or schedule + * fails, and otherwise succeeds with the schedule output when the schedule + * completes. + * + * **Example** (Scheduling repeated execution) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Console.log("Task executing...") + * return Math.random() + * }) + * + * // Repeat 3 times with 1 second delay between executions + * const program = Effect.schedule( + * task, + * Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("1 second")) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // Task executing... (immediate) + * // Task executing... (after 1 second) + * // Task executing... (after 1 second) + * // Returns the count from Schedule.recurs + * ``` + * + * @see {@link scheduleFrom} for a variant that allows the schedule's decision + * to depend on the result of this effect. + * + * @category repetition + * @since 2.0.0 + */ +export const schedule: { + ( + schedule: Schedule + ): (self: Effect) => Effect + ( + self: Effect, + schedule: Schedule + ): Effect +} = dual(2, ( + self: Effect, + schedule: Schedule +): Effect => scheduleFrom(self, undefined, schedule)) + +/** + * Runs an effect repeatedly according to a schedule that is initialized with a + * specific schedule input. + * + * **Details** + * + * `initial` is passed to the schedule before the first execution, not to the + * effect itself. After each successful execution, the effect's success value is + * fed back into the schedule to decide whether to continue. The returned effect + * succeeds with the schedule output when the schedule completes and fails if + * the effect or schedule fails. + * + * **Example** (Scheduling from an initial value) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * const task = (input: number) => + * Effect.gen(function*() { + * yield* Console.log(`Processing: ${input}`) + * return input + 1 + * }) + * + * // Start with 0, repeat 3 times + * const program = Effect.scheduleFrom( + * task(0), + * 0, + * Schedule.recurs(2) + * ) + * + * Effect.runPromise(program).then(console.log) + * // Returns the schedule count + * ``` + * + * @category repetition + * @since 2.0.0 + */ +export const scheduleFrom: { + ( + initial: Input, + schedule: Schedule + ): (self: Effect) => Effect + ( + self: Effect, + initial: Input, + schedule: Schedule + ): Effect +} = internalSchedule.scheduleFrom + +// ----------------------------------------------------------------------------- +// Tracing +// ----------------------------------------------------------------------------- + +/** + * Returns the current tracer from the context. + * + * **Example** (Accessing the current tracer) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const currentTracer = yield* Effect.tracer + * yield* Effect.log(`Using tracer: ${currentTracer}`) + * return "operation completed" + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const tracer: Effect = internal.tracer + +/** + * Provides a tracer to an effect. + * + * **Example** (Providing a tracer) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Using tracer") + * return "completed" + * }) + * + * // withTracer provides a tracer to the effect context + * // const traced = Effect.withTracer(program, customTracer) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withTracer: { + (value: Tracer): (effect: Effect) => Effect + (effect: Effect, value: Tracer): Effect +} = internal.withTracer + +/** + * Enables or disables tracing for spans created by the given effect. + * + * **Details** + * + * When `enabled` is `false`, spans created inside the effect are not registered + * with the current tracer and do not propagate as normal trace parents. + * + * **Example** (Enabling or disabling tracing) + * + * ```ts + * import { Effect } from "effect" + * + * Effect.succeed(42).pipe( + * Effect.withSpan("my-span"), + * // the span will not be registered with the tracer + * Effect.withTracerEnabled(false) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withTracerEnabled: { + (enabled: boolean): (effect: Effect) => Effect + (effect: Effect, enabled: boolean): Effect +} = internal.withTracerEnabled + +/** + * Enables or disables tracer timing for the given Effect. + * + * **Example** (Enabling or disabling tracing timing) + * + * ```ts + * import { Effect } from "effect" + * + * Effect.succeed(42).pipe( + * Effect.withSpan("my-span"), + * // the span will not have timing information + * Effect.withTracerTiming(false) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withTracerTiming: { + (enabled: boolean): (effect: Effect) => Effect + (effect: Effect, enabled: boolean): Effect +} = internal.withTracerTiming + +/** + * Adds an annotation to each span in this effect. + * + * **Example** (Annotating all spans) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Doing some work...") + * return "result" + * }) + * + * // Add single annotation + * const annotated1 = Effect.annotateSpans(program, "user", "john") + * + * // Add multiple annotations + * const annotated2 = Effect.annotateSpans(program, { + * operation: "data-processing", + * version: "1.0.0", + * environment: "production" + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const annotateSpans: { + ( + key: string, + value: unknown + ): (effect: Effect) => Effect + ( + values: Record + ): (effect: Effect) => Effect + ( + effect: Effect, + key: string, + value: unknown + ): Effect + ( + effect: Effect, + values: Record + ): Effect +} = internal.annotateSpans + +/** + * Adds an annotation to the current span if available. + * + * **Example** (Annotating the current span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.annotateCurrentSpan("userId", "123") + * yield* Effect.annotateCurrentSpan({ + * operation: "user-lookup", + * timestamp: Date.now() + * }) + * yield* Effect.log("User lookup completed") + * return "success" + * }) + * + * const traced = Effect.withSpan(program, "user-operation") + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const annotateCurrentSpan: { + (key: string, value: unknown): Effect + (values: Record): Effect +} = internal.annotateCurrentSpan + +/** + * Returns the currently active local tracing span. + * + * **Details** + * + * The effect fails with `NoSuchElementError` when there is no active local + * `Span`. + * + * **Example** (Reading the current span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const span = yield* Effect.currentSpan + * yield* Effect.log(`Current span: ${span}`) + * return "done" + * }) + * + * const traced = Effect.withSpan(program, "my-span") + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const currentSpan: Effect = internal.currentSpan + +/** + * Returns the current parent span from the effect context. + * + * **Details** + * + * The effect succeeds with either a local span or external span when one is + * present, and fails with `NoSuchElementError` when no parent span is + * available. + * + * **Example** (Reading the parent span) + * + * ```ts + * import { Effect } from "effect" + * + * const childOperation = Effect.gen(function*() { + * const parentSpan = yield* Effect.currentParentSpan + * yield* Effect.log(`Parent span: ${parentSpan}`) + * return "child completed" + * }) + * + * const program = Effect.gen(function*() { + * yield* Effect.withSpan(childOperation, "child-span") + * return "parent completed" + * }) + * + * const traced = Effect.withSpan(program, "parent-span") + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const currentParentSpan: Effect = internal.currentParentSpan + +/** + * Returns the tracing span annotations currently carried in the effect context. + * + * **Details** + * + * These annotations are applied to spans created inside the context, such as + * spans created by `withSpan`, `useSpan`, or `makeSpan`. + * + * **Example** (Providing span annotations) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * // Add some annotations to the current span + * yield* Effect.annotateCurrentSpan("userId", "123") + * yield* Effect.annotateCurrentSpan("operation", "data-processing") + * + * // Retrieve all annotations + * const annotations = yield* Effect.spanAnnotations + * + * console.log("Current span annotations:", annotations) + * return annotations + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: Current span annotations: { userId: "123", operation: "data-processing" } + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const spanAnnotations: Effect>> = internal.spanAnnotations + +/** + * Returns the tracing span links currently carried in the effect context. + * + * **Details** + * + * These links are attached to spans created inside the context. Span links + * connect related spans without making one span the parent of another. + * + * **Example** (Providing span links) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * // Get the current span links + * const links = yield* Effect.spanLinks + * console.log(`Current span has ${links.length} links`) + * return links + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const spanLinks: Effect> = internal.spanLinks + +/** + * Adds a link with the provided span to all spans in this effect. + * + * **Details** + * + * This is useful for connecting spans that are related but not in a direct + * parent-child relationship. For example, you might want to link spans from + * parallel operations or connect spans across different traces. + * + * **Example** (Linking one span to another span) + * + * ```ts + * import { Effect } from "effect" + * + * const parentEffect = Effect.withSpan("parent-operation")( + * Effect.succeed("parent result") + * ) + * + * const childEffect = Effect.withSpan("child-operation")( + * Effect.succeed("child result") + * ) + * + * // Link the child span to the parent span + * const program = Effect.gen(function*() { + * const parentSpan = yield* Effect.currentSpan + * const result = yield* childEffect.pipe( + * Effect.linkSpans(parentSpan, { relationship: "follows" }) + * ) + * return result + * }) + * ``` + * + * **Example** (Linking multiple spans at once) + * + * ```ts + * import { Effect } from "effect" + * + * // Link multiple spans + * const program = Effect.gen(function*() { + * const span1 = yield* Effect.currentSpan + * const span2 = yield* Effect.currentSpan + * + * return yield* Effect.succeed("result").pipe( + * Effect.linkSpans([span1, span2], { + * type: "dependency", + * source: "multiple-operations" + * }) + * ) + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const linkSpans: { + ( + span: AnySpan | ReadonlyArray, + attributes?: Record + ): (self: Effect) => Effect + ( + self: Effect, + span: AnySpan | ReadonlyArray, + attributes?: Record + ): Effect +} = internal.linkSpans + +/** + * Creates a new tracing span and returns it without managing its lifetime. + * + * **Details** + * + * The span is not added to the current span stack and is not ended + * automatically. Use `withSpan`, `useSpan`, or `makeSpanScoped` when the span + * should be installed as context or closed automatically. + * + * **Example** (Creating a span manually) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const span = yield* Effect.makeSpan("my-operation") + * yield* Effect.log("Operation in progress") + * return "completed" + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const makeSpan: (name: string, options?: SpanOptionsNoTrace) => Effect = internal.makeSpan + +/** + * Create a new span for tracing, and automatically close it when the Scope + * finalizes. + * + * **Details** + * + * The span is not added to the current span stack, so no child spans will be + * created for it. + * + * **Example** (Creating a scoped standalone span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const span = yield* Effect.makeSpanScoped("scoped-operation") + * yield* Effect.log("Working...") + * return "done" + * // Span automatically closes when scope ends + * }) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const makeSpanScoped: ( + name: string, + options?: SpanOptionsNoTrace | undefined +) => Effect = internal.makeSpanScoped + +/** + * Create a new span for tracing, and automatically close it when the effect + * completes. + * + * **Details** + * + * The span is not added to the current span stack, so no child spans will be + * created for it. + * + * **Example** (Running an effect with a standalone span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.useSpan( + * "user-operation", + * (span) => + * Effect.gen(function*() { + * yield* Effect.log("Processing user data") + * return "success" + * }) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const useSpan: { + (name: string, evaluate: (span: Span) => Effect): Effect + (name: string, options: SpanOptionsNoTrace, evaluate: (span: Span) => Effect): Effect +} = internal.useSpan + +/** + * Wraps the effect with a child span for tracing. + * + * **Example** (Wrapping an effect in a child span) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Effect.log("Executing task") + * return "result" + * }) + * + * const traced = Effect.withSpan(task, "my-task", { + * attributes: { version: "1.0" } + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withSpan: { + >( + name: string, + options?: + | SpanOptionsNoTrace + | ((...args: NoInfer) => SpanOptionsNoTrace) + | undefined, + traceOptions?: TraceOptions | undefined + ): (self: Effect, ...args: Args) => Effect> + ( + self: Effect, + name: string, + options?: SpanOptions | undefined + ): Effect> +} = internal.withSpan + +/** + * Wraps the effect with a scoped child span for tracing. + * + * **Details** + * + * The span is ended when the Scope is finalized. + * + * **Example** (Creating a scoped child span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const task = Effect.log("Working...") + * yield* Effect.withSpanScoped(task, "scoped-task") + * return "completed" + * }) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withSpanScoped: { + ( + name: string, + options?: SpanOptions + ): ( + self: Effect + ) => Effect | Scope> + ( + self: Effect, + name: string, + options?: SpanOptions + ): Effect | Scope> +} = internal.withSpanScoped + +/** + * Adds the provided span to the current span stack. + * + * **Example** (Setting a parent span) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const span = yield* Effect.makeSpan("parent-span") + * const childTask = Effect.log("Child operation") + * yield* Effect.withParentSpan(childTask, span) + * return "completed" + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withParentSpan: { + (value: AnySpan, options?: TraceOptions): (self: Effect) => Effect> + (self: Effect, value: AnySpan, options?: TraceOptions): Effect> +} = internal.withParentSpan + +// ----------------------------------------------------------------------------- +// Batching +// ----------------------------------------------------------------------------- + +/** + * Executes a request using the provided resolver. + * + * **When to use** + * + * Use when you need resolver-driven batching for a typed `Request`. + * + * **Example** (Executing a request through a resolver) + * + * ```ts + * import { Console, Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * const GetUser = Request.tagged("GetUser") + * + * const resolver = RequestResolver.make( + * Effect.fnUntraced(function*(entries) { + * for (const entry of entries) { + * yield* Request.complete(entry, Exit.succeed(`user-${entry.request.id}`)) + * } + * }) + * ) + * + * const program = Effect.gen(function*() { + * const name = yield* Effect.request(GetUser({ id: 1 }), resolver) + * yield* Console.log(name) + * }) + * ``` + * + * @see {@link requestUnsafe} for the low-level entry point when you already have a `Context` and need to enqueue outside an `Effect` + * + * @category requests & batching + * @since 2.0.0 + */ +export const request: { + ( + resolver: RequestResolver | Effect, EX, RX> + ): (self: A) => Effect, Request.Error | EX, Request.Services | RX> + ( + self: A, + resolver: RequestResolver | Effect, EX, RX> + ): Effect, Request.Error | EX, Request.Services | RX> +} = internalRequest.request + +/** + * Registers a request with a resolver and delivers the exit value via `onExit`. + * + * **When to use** + * + * Use when you already have a `Context` and need to enqueue a request outside + * an `Effect` while receiving completion through `onExit`. + * + * **Details** + * + * It returns a canceler that removes the pending request entry. + * + * @see {@link request} for the `Effect`-returning API used for normal request execution + * + * @category requests & batching + * @since 4.0.0 + */ +export const requestUnsafe: ( + self: A, + options: { + readonly resolver: RequestResolver + readonly onExit: (exit: Exit.Exit, Request.Error>) => void + readonly context: Context.Context + } +) => () => void = internalRequest.requestUnsafe + +// ----------------------------------------------------------------------------- +// Supervision & Fiber's +// ----------------------------------------------------------------------------- + +/** + * Returns an effect that forks this effect into its own separate fiber, + * returning the fiber immediately, without waiting for it to begin executing + * the effect. + * + * **Details** + * + * You can use the `forkChild` method whenever you want to execute an effect in a + * new fiber, concurrently and without "blocking" the fiber executing other + * effects. Using fibers can be tricky, so instead of using this method + * directly, consider other higher-level methods, such as `raceWith`, + * `zipPar`, and so forth. + * + * The fiber returned by this method has methods to interrupt the fiber and to + * wait for it to finish executing the effect. See `Fiber` for more + * information. + * + * Whenever you use this method to launch a new fiber, the new fiber is + * attached to the parent fiber's scope. This means when the parent fiber + * terminates, the child fiber will be terminated as well, ensuring that no + * fibers leak. This behavior is called "auto supervision", and if this + * behavior is not desired, you may use the `forkDetach` or `forkIn` methods. + * + * **Example** (Forking a child fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const longRunningTask = Effect.gen(function*() { + * yield* Effect.sleep("2 seconds") + * yield* Effect.log("Task completed") + * return "result" + * }) + * + * const program = Effect.gen(function*() { + * const fiber = yield* longRunningTask.pipe(Effect.forkChild) + * + * // or fork a fiber that starts immediately: + * yield* longRunningTask.pipe(Effect.forkChild({ startImmediately: true })) + * + * yield* Effect.log("Task forked, continuing...") + * const result = yield* Fiber.join(fiber) + * return result + * }) + * ``` + * + * @category supervision & fibers + * @since 4.0.0 + */ +export const forkChild: < + Arg extends Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect, never, _R> + : (self: Effect) => Effect, never, R> = internal.forkChild + +/** + * Forks the effect in the specified scope. The fiber will be interrupted + * when the scope is closed. + * + * **Example** (Forking into a supplied scope) + * + * ```ts + * import { Effect } from "effect" + * + * const task = Effect.gen(function*() { + * yield* Effect.sleep("10 seconds") + * return "completed" + * }) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const scope = yield* Effect.scope + * const fiber = yield* Effect.forkIn(task, scope) + * yield* Effect.sleep("1 second") + * // Fiber will be interrupted when scope closes + * return "done" + * }) + * ) + * ``` + * + * @category supervision & fibers + * @since 2.0.0 + */ +export const forkIn: { + ( + scope: Scope, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + ): (self: Effect) => Effect, never, R> + ( + self: Effect, + scope: Scope, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + ): Effect, never, R> +} = internal.forkIn + +/** + * Forks the fiber in a `Scope`, interrupting it when the scope is closed. + * + * **Example** (Forking into the current scope) + * + * ```ts + * import { Effect } from "effect" + * + * const backgroundTask = Effect.gen(function*() { + * yield* Effect.sleep("5 seconds") + * yield* Effect.log("Background task completed") + * return "result" + * }) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const fiber = yield* backgroundTask.pipe(Effect.forkScoped) + * + * // or fork a fiber that starts immediately: + * yield* backgroundTask.pipe(Effect.forkScoped({ startImmediately: true })) + * + * yield* Effect.log("Task forked in scope") + * yield* Effect.sleep("1 second") + * + * // Fiber will be interrupted when scope closes + * return "scope completed" + * }) + * ) + * ``` + * + * @category supervision & fibers + * @since 2.0.0 + */ +export const forkScoped: < + Arg extends Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect, never, _R | Scope> + : (self: Effect) => Effect, never, R | Scope> = internal.forkScoped + +/** + * Forks the effect into a new fiber attached to the global scope. Because the + * new fiber is attached to the global scope, when the fiber executing the + * returned effect terminates, the forked fiber will continue running. + * + * **Example** (Forking a detached fiber) + * + * ```ts + * import { Effect } from "effect" + * + * const daemonTask = Effect.gen(function*() { + * while (true) { + * yield* Effect.sleep("1 second") + * yield* Effect.log("Daemon running...") + * } + * }) + * + * const program = Effect.gen(function*() { + * const fiber = yield* daemonTask.pipe(Effect.forkDetach) + * + * // or fork a fiber that starts immediately: + * yield* daemonTask.pipe(Effect.forkDetach({ startImmediately: true })) + * + * yield* Effect.log("Daemon started") + * yield* Effect.sleep("3 seconds") + * // Daemon continues running after this effect completes + * return "main completed" + * }) + * ``` + * + * @category supervision & fibers + * @since 4.0.0 + */ +export const forkDetach: < + Arg extends Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } +>( + effectOrOptions?: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined +) => [Arg] extends [Effect] ? Effect, never, _R> + : (self: Effect) => Effect, never, R> = internal.forkDetach + +/** + * Waits for all child fibers forked by this effect to complete before this + * effect completes. + * + * **When to use** + * + * Use to let an effect start child work concurrently while still delaying its + * own completion until that child work is done. + * + * **Gotchas** + * + * Child fibers that already exist before the wrapped effect starts are not + * awaited. + * + * @see {@link forkChild} for forking child fibers that are awaited by this operator + * @see {@link forkDetach} for forking fibers outside the child scope + * @see {@link forkIn} for forking into an explicit scope + * @see {@link forkScoped} for forking fibers tied to the current scope + * + * @category supervision & fibers + * @since 2.0.0 + */ +export const awaitAllChildren: (self: Effect) => Effect = internal.awaitAllChildren + +/** + * Accesses the fiber currently executing the effect. + * + * **Example** (Accessing the current fiber) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.fiber + * yield* Console.log(`Fiber id: ${fiber.id}`) + * }) + * ``` + * + * @category supervision & fibers + * @since 4.0.0 + */ +export const fiber: Effect> = internal.fiber + +/** + * Accesses the current fiber id executing the effect. + * + * **Example** (Accessing the current fiber id) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.log("event").pipe( + * // Read the current span with the fiber id for tagging. + * Effect.andThen(Effect.all([Effect.currentSpan, Effect.fiberId])), + * Effect.withSpan("A"), + * Effect.map(([span, fiberId]) => ({ + * spanName: span.name, + * fiberId + * })) + * ) + * ``` + * + * @category supervision & fibers + * @since 2.0.0 + */ +export const fiberId: Effect = internal.fiberId + +// ----------------------------------------------------------------------------- +// Running Effects +// ----------------------------------------------------------------------------- + +/** + * Configuration options for running Effect programs, providing control over + * interruption and scheduling behavior. + * + * **When to use** + * + * Use to pass cancellation, scheduler, interruptibility, and fiber-start hooks + * when running an `Effect` at a program boundary. + * + * **Details** + * + * `signal` interrupts the fiber, `scheduler` provides the scheduler service, + * `uninterruptible` starts the fiber uninterruptibly, and `onFiberStart` + * receives the created fiber. + * + * @see {@link runFork} for starting a fiber with these options + * @see {@link runCallback} for callback-based running with these options + * @see {@link runPromise} for promise-based running with these options + * @see {@link runPromiseExit} for promise-based running that returns an `Exit` + * + * @category running + * @since 4.0.0 + */ +export interface RunOptions { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly uninterruptible?: boolean | undefined + readonly onFiberStart?: ((fiber: Fiber) => void) | undefined +} + +/** + * Runs an effect in the background, returning a fiber that can + * be observed or interrupted. + * + * **When to use** + * + * Use when you need to start an effect in the background and receive a fiber. + * + * **Example** (Running an effect in the background) + * + * ```ts + * import { Console, Effect, Fiber, Schedule } from "effect" + * + * // ┌─── Effect + * // ▼ + * const program = Effect.repeat( + * Console.log("running..."), + * Schedule.spaced("200 millis") + * ) + * + * // ┌─── RuntimeFiber + * // ▼ + * const fiber = Effect.runFork(program) + * + * setTimeout(() => { + * Effect.runFork(Fiber.interrupt(fiber)) + * }, 500) + * ``` + * + * @category running + * @since 2.0.0 + */ +export const runFork: (effect: Effect, options?: RunOptions | undefined) => Fiber = + internal.runFork + +/** + * Runs an effect in the background with the provided services. + * + * **When to use** + * + * Use when an effect still requires services, you already have a `Context`, and + * you want a background fiber. + * + * **Example** (Running with services in the background) + * + * ```ts + * import { Context, Effect } from "effect" + * + * interface Logger { + * log: (message: string) => void + * } + * + * const Logger = Context.Service("Logger") + * + * const services = Context.make(Logger, { + * log: (message) => console.log(message) + * }) + * + * const program = Effect.gen(function*() { + * const logger = yield* Logger + * logger.log("Hello from service!") + * return "done" + * }) + * + * const fiber = Effect.runForkWith(services)(program) + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runForkWith: ( + context: Context.Context +) => (effect: Effect, options?: RunOptions | undefined) => Fiber = internal.runForkWith + +/** + * Forks an effect with the provided services, registers `onExit` as a fiber observer, and returns an interruptor. + * + * **When to use** + * + * Use when embedding an effect into callback-style code with explicit services + * and a synchronous interruptor. + * + * **Details** + * + * The returned interruptor calls `fiber.interruptUnsafe`, optionally with an interruptor id. + * + * **Example** (Running with services and a callback) + * + * ```ts + * import { Console, Context, Effect, Exit } from "effect" + * + * interface Logger { + * log: (message: string) => Effect.Effect + * } + * + * const Logger = Context.Service("Logger") + * + * const services = Context.make(Logger, { + * log: (message) => Console.log(message) + * }) + * + * const program = Effect.gen(function*() { + * const logger = yield* Logger + * yield* logger.log("Started") + * return "done" + * }) + * + * const interrupt = Effect.runCallbackWith(services)(program, { + * onExit: (exit) => { + * if (Exit.isFailure(exit)) { + * // handle failure or interruption + * } + * } + * }) + * + * // Use the interruptor if you need to cancel the fiber later. + * interrupt() + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runCallbackWith: ( + context: Context.Context +) => ( + effect: Effect, + options?: (RunOptions & { readonly onExit: (exit: Exit.Exit) => void }) | undefined +) => (interruptor?: number | undefined) => void = internal.runCallbackWith + +/** + * Runs an effect asynchronously, registering `onExit` as a fiber observer and + * returning an interruptor. + * + * **Details** + * + * The interruptor calls `fiber.interruptUnsafe` with the optional interruptor + * id. + * + * **Example** (Running with a callback) + * + * ```ts + * import { Console, Effect, Exit } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.log("working") + * return "done" + * }) + * + * const interrupt = Effect.runCallback(program, { + * onExit: (exit) => { + * Effect.runSync( + * Exit.match(exit, { + * onFailure: () => Console.log("failed"), + * onSuccess: (value) => Console.log(`success: ${value}`) + * }) + * ) + * } + * }) + * + * // Output: + * // working + * // success: done + * + * // interrupt() to cancel the fiber if needed + * ``` + * + * @category running + * @since 2.0.0 + */ +export const runCallback: ( + effect: Effect, + options?: (RunOptions & { readonly onExit: (exit: Exit.Exit) => void }) | undefined +) => (interruptor?: number | undefined) => void = internal.runCallback + +/** + * Executes an effect and returns the result as a `Promise`. + * + * **When to use** + * + * Use when you need to execute an effect and work with the + * result using `Promise` syntax, typically for compatibility with other + * promise-based code. + * + * If the effect succeeds, the promise will resolve with the result. If the + * effect fails, the promise will reject with an error. + * + * **Example** (Running a successful effect as a Promise) + * + * ```ts + * import { Effect } from "effect" + * + * Effect.runPromise(Effect.succeed(1)).then(console.log) + * // Output: 1 + * ``` + * + * **Example** (Running effects as promises) + * + * ```ts + * //Example: Handling a Failing Effect as a Rejected Promise + * import { Effect } from "effect" + * + * Effect.runPromise(Effect.fail("my error")).catch(console.error) + * // Output: + * // (FiberFailure) Error: my error + * ``` + * + * @see {@link runPromiseExit} for a version that returns an `Exit` type instead of rejecting. + * @category running + * @since 2.0.0 + */ +export const runPromise: ( + effect: Effect, + options?: RunOptions | undefined +) => Promise = internal.runPromise + +/** + * Executes an effect as a Promise with the provided services. + * + * **When to use** + * + * Use when you already have a `Context` and need Promise interop that rejects on + * effect failure. + * + * **Example** (Running with services as a promise) + * + * ```ts + * import { Context, Effect } from "effect" + * + * interface Config { + * apiUrl: string + * } + * + * const Config = Context.Service("Config") + * + * const context = Context.make(Config, { + * apiUrl: "https://api.example.com" + * }) + * + * const program = Effect.gen(function*() { + * const config = yield* Config + * return `Connecting to ${config.apiUrl}` + * }) + * + * Effect.runPromiseWith(context)(program).then(console.log) + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runPromiseWith: ( + context: Context.Context +) => (effect: Effect, options?: RunOptions | undefined) => Promise = internal.runPromiseWith + +/** + * Runs an effect and returns a `Promise` that resolves to an `Exit`, which + * represents the outcome (success or failure) of the effect. + * + * **When to use** + * + * Use when you need to determine if an effect succeeded + * or failed, including any defects, and you want to work with a `Promise`. + * + * **Details** + * + * The `Exit` type represents the result of the effect. Successful effects are + * wrapped in `Success`, and failed effects are wrapped in `Failure` with a + * `Cause`. + * + * **Example** (Observing promise results as Exit) + * + * ```ts + * import { Effect } from "effect" + * + * // Execute a successful effect and get the Exit result as a Promise + * Effect.runPromiseExit(Effect.succeed(1)).then(console.log) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: 1 + * // } + * + * // Execute a failing effect and get the Exit result as a Promise + * Effect.runPromiseExit(Effect.fail("my error")).then(console.log) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Failure", + * // cause: { + * // _id: "Cause", + * // _tag: "Fail", + * // failure: "my error" + * // } + * // } + * ``` + * + * @see {@link runPromise} for a version that rejects on failure. + * + * @category running + * @since 2.0.0 + */ +export const runPromiseExit: ( + effect: Effect, + options?: RunOptions | undefined +) => Promise> = internal.runPromiseExit + +/** + * Runs an effect and returns a Promise of Exit with provided services. + * + * **When to use** + * + * Use when you already have a `Context` and need Promise interop that preserves + * success and failure as an `Exit`. + * + * **Example** (Running with services as an Exit promise) + * + * ```ts + * import { Context, Effect, Exit } from "effect" + * + * interface Database { + * query: (sql: string) => string + * } + * + * const Database = Context.Service("Database") + * + * const services = Context.make(Database, { + * query: (sql) => `Result for: ${sql}` + * }) + * + * const program = Effect.gen(function*() { + * const db = yield* Database + * return db.query("SELECT * FROM users") + * }) + * + * Effect.runPromiseExitWith(services)(program).then((exit) => { + * if (Exit.isSuccess(exit)) { + * console.log("Success:", exit.value) + * } + * }) + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runPromiseExitWith: ( + context: Context.Context +) => (effect: Effect, options?: RunOptions | undefined) => Promise> = + internal.runPromiseExitWith + +/** + * Executes an effect synchronously and returns its success value. + * + * **When to use** + * + * Use when you need to execute an effect that is guaranteed to complete + * synchronously. + * + * **Details** + * + * If the effect fails, dies, is interrupted, or performs asynchronous work, + * `runSync` throws a `FiberFailure` instead of returning a value. Use + * `runSyncExit` when you want the failure captured as an `Exit`. + * + * **Example** (Running a synchronous effect) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.sync(() => { + * console.log("Hello, World!") + * return 1 + * }) + * + * const result = Effect.runSync(program) + * // Output: Hello, World! + * + * console.log(result) + * // Output: 1 + * ``` + * + * **Example** (Throwing for failed or async effects) + * + * ```ts + * import { Effect } from "effect" + * + * try { + * // Attempt to run an effect that fails + * Effect.runSync(Effect.fail("my error")) + * } catch (e) { + * console.error(e) + * } + * // Output: + * // (FiberFailure) Error: my error + * + * try { + * // Attempt to run an effect that involves async work + * Effect.runSync(Effect.promise(() => Promise.resolve(1))) + * } catch (e) { + * console.error(e) + * } + * // Output: + * // (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work + * ``` + * + * @see {@link runSyncExit} for a version that returns an `Exit` type instead of + * throwing an error. + * @category running + * @since 2.0.0 + */ +export const runSync: (effect: Effect) => A = internal.runSync + +/** + * Executes an effect synchronously with provided services. + * + * **When to use** + * + * Use when you already have a `Context`, the effect is known to complete + * synchronously, and failures should throw. + * + * **Example** (Running synchronously with services) + * + * ```ts + * import { Context, Effect } from "effect" + * + * interface MathService { + * add: (a: number, b: number) => number + * } + * + * const MathService = Context.Service("MathService") + * + * const context = Context.make(MathService, { + * add: (a, b) => a + b + * }) + * + * const program = Effect.gen(function*() { + * const math = yield* MathService + * return math.add(2, 3) + * }) + * + * const result = Effect.runSyncWith(context)(program) + * console.log(result) // 5 + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runSyncWith: ( + context: Context.Context +) => (effect: Effect) => A = internal.runSyncWith + +/** + * Runs an effect synchronously and captures the outcome safely as an `Exit` type, which + * represents the outcome (success or failure) of the effect. + * + * **When to use** + * + * Use to find out whether an effect succeeded or failed, + * including any defects, without dealing with asynchronous operations. + * + * **Details** + * + * The `Exit` type represents the result of the effect. Successful effects are + * wrapped in `Success`, and failed effects are wrapped in `Failure` with a + * `Cause`. + * + * If the effect contains asynchronous operations, `runSyncExit` will + * return an `Failure` with a `Die` cause, indicating that the effect cannot be + * resolved synchronously. + * + * **Example** (Observing synchronous results as Exit) + * + * ```ts + * import { Effect } from "effect" + * + * console.log(Effect.runSyncExit(Effect.succeed(1))) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Success", + * // value: 1 + * // } + * + * console.log(Effect.runSyncExit(Effect.fail("my error"))) + * // Output: + * // { + * // _id: "Exit", + * // _tag: "Failure", + * // cause: { + * // _id: "Cause", + * // _tag: "Fail", + * // failure: "my error" + * // } + * // } + * ``` + * + * **Example** (Capturing async work as a Die cause) + * + * ```ts + * import { Effect } from "effect" + * + * console.log(Effect.runSyncExit(Effect.promise(() => Promise.resolve(1)))) + * // Output: + * // { + * // _id: 'Exit', + * // _tag: 'Failure', + * // cause: { + * // _id: 'Cause', + * // _tag: 'Die', + * // defect: [Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work] { + * // fiber: [FiberRuntime], + * // _tag: 'AsyncFiberException', + * // name: 'AsyncFiberException' + * // } + * // } + * // } + * ``` + * + * @see {@link runSync} for a version that throws on failure. + * + * @category running + * @since 2.0.0 + */ +export const runSyncExit: (effect: Effect) => Exit.Exit = internal.runSyncExit + +/** + * Runs an effect synchronously with provided services, returning an Exit result safely. + * + * **When to use** + * + * Use when you already have a `Context` and need a synchronous `Exit` instead of + * throwing on failure. + * + * **Example** (Running synchronously with services as Exit) + * + * ```ts + * import { Context, Effect, Exit } from "effect" + * + * // Define a logger service + * const Logger = Context.Service<{ + * log: (msg: string) => void + * }>("Logger") + * + * const program = Effect.gen(function*() { + * const logger = yield* Effect.service(Logger) + * logger.log("Computing result...") + * return 42 + * }) + * + * // Prepare context + * const context = Context.make(Logger, { + * log: (msg) => console.log(`[LOG] ${msg}`) + * }) + * + * const exit = Effect.runSyncExitWith(context)(program) + * + * if (Exit.isSuccess(exit)) { + * console.log(`Success: ${exit.value}`) + * } else { + * console.log(`Failure: ${exit.cause}`) + * } + * // Output: + * // [LOG] Computing result... + * // Success: 42 + * ``` + * + * @category running + * @since 4.0.0 + */ +export const runSyncExitWith: ( + context: Context.Context +) => (effect: Effect) => Exit.Exit = internal.runSyncExitWith + +// ----------------------------------------------------------------------------- +// Function +// ----------------------------------------------------------------------------- + +/** + * Type helpers for functions built with `Effect.fn` and `Effect.fnUntraced`. + * + * **Details** + * + * Use these to describe generator-based signatures and traced or untraced variants. + * + * @since 3.11.0 + */ +export declare namespace fn { + /** + * Generator return type accepted by {@link fn} and {@link fnUntraced}. + * + * **When to use** + * + * Use when you need to annotate the return type of a generator body while + * keeping the produced function's `Effect` return type inferred. + * + * **Example** (Annotating an Effect function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced(function*( + * value: string + * ): Effect.fn.Return { + * return yield* Effect.succeed(value.length) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Annotating a parametric Effect function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced(function*( + * value: A + * ): Effect.fn.Return { + * return yield* Effect.succeed(value) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * @category utility types + * @since 3.19.0 + */ + export type Return = Generator, A, any> + + /** + * Type of the untraced function builder used by `Effect.fnUntraced`. + * + * @category utility types + * @since 3.11.0 + */ + export type Untraced = { + , AEff, Args extends Array>( + body: (this: unassigned, ...args: Args) => Generator + ): (...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + , AEff, Args extends Array>( + body: (this: Self, ...args: Args) => Generator + ): (this: Self, ...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + + , AEff, Args extends Array, A>( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A + ): (...args: Args) => A + , AEff, Args extends Array, A>( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A + ): (this: Self, ...args: Args) => A + , AEff, Args extends Array, A, B>( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B + ): (...args: Args) => B + , AEff, Args extends Array, A, B>( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B + ): (this: Self, ...args: Args) => B + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C + ): (...args: Args) => C + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C + ): (this: Self, ...args: Args) => C + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D + >( + body: (...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D + ): (...args: Args) => D + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D + ): (this: Self, ...args: Args) => D + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E + ): (...args: Args) => E + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E + ): (this: Self, ...args: Args) => E + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F + ): (...args: Args) => F + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F + ): (this: Self, ...args: Args) => F + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G + ): (...args: Args) => G + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G + ): (this: Self, ...args: Args) => G + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H + ): (...args: Args) => H + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H + ): (this: Self, ...args: Args) => H + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I + ): (...args: Args) => I + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I + ): (this: Self, ...args: Args) => I + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J + ): (...args: Args) => J + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J + ): (this: Self, ...args: Args) => J + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K + ): (...args: Args) => K + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K + ): (this: Self, ...args: Args) => K + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L + ): (...args: Args) => L + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L + ): (this: Self, ...args: Args) => L + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M + ): (...args: Args) => M + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M + ): (this: Self, ...args: Args) => M + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N + ): (...args: Args) => N + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N + ): (this: Self, ...args: Args) => N + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O + ): (...args: Args) => O + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O + ): (this: Self, ...args: Args) => O + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P + ): (...args: Args) => P + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P + ): (this: Self, ...args: Args) => P + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q + ): (...args: Args) => Q + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q + ): (this: Self, ...args: Args) => Q + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R + ): (...args: Args) => R + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R + ): (this: Self, ...args: Args) => R + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S + ): (...args: Args) => S + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S + ): (this: Self, ...args: Args) => S + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T + >( + body: (this: unassigned, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S, + t: (_: S, ...args: Args) => T + ): (...args: Args) => T + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T + >( + body: (this: Self, ...args: Args) => Generator, + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S, + t: (_: S, ...args: Args) => T + ): (this: Self, ...args: Args) => T + } + + /** + * Type of the traced function builder used by `Effect.fn`. + * + * @category utility types + * @since 4.0.0 + */ + export type Traced = { + , AEff, Args extends Array>( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect) + ): (...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + , AEff, Args extends Array>( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect) + ): (this: Self, ...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + , AEff, Args extends Array>( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect) + ): (...args: Args) => Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + > + + , AEff, Args extends Array, A>( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A + ): (...args: Args) => A + , AEff, Args extends Array, A>( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A + ): (this: Self, ...args: Args) => A + , AEff, Args extends Array, A>( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A + ): (...args: Args) => A + + , AEff, Args extends Array, A, B>( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B + ): (...args: Args) => B + , AEff, Args extends Array, A, B>( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B + ): (this: Self, ...args: Args) => B + , AEff, Args extends Array, A, B>( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B + ): (...args: Args) => B + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C + ): (...args: Args) => C + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C + ): (this: Self, ...args: Args) => C + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C + ): (...args: Args) => C + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D + ): (...args: Args) => D + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D + ): (this: Self, ...args: Args) => D + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D + ): (...args: Args) => D + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E + ): (...args: Args) => E + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E + ): (this: Self, ...args: Args) => E + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E + ): (...args: Args) => E + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F + ): (...args: Args) => F + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F + ): (this: Self, ...args: Args) => F + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F + ): (...args: Args) => F + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G + ): (...args: Args) => G + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G + ): (this: Self, ...args: Args) => G + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G + ): (...args: Args) => G + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H + ): (...args: Args) => H + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H + ): (this: Self, ...args: Args) => H + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H + ): (...args: Args) => H + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I + ): (...args: Args) => I + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I + ): (this: Self, ...args: Args) => I + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I + ): (...args: Args) => I + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J + ): (...args: Args) => J + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J + ): (this: Self, ...args: Args) => J + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J + ): (...args: Args) => J + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K + ): (...args: Args) => K + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K + ): (this: Self, ...args: Args) => K + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K + ): (...args: Args) => K + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L + ): (...args: Args) => L + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L + ): (this: Self, ...args: Args) => L + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L + ): (...args: Args) => L + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M + ): (...args: Args) => M + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M + ): (this: Self, ...args: Args) => M + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M + ): (...args: Args) => M + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N + ): (...args: Args) => N + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N + ): (this: Self, ...args: Args) => N + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N + ): (...args: Args) => N + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O + ): (...args: Args) => O + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O + ): (this: Self, ...args: Args) => O + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O + ): (...args: Args) => O + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P + ): (...args: Args) => P + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P + ): (this: Self, ...args: Args) => P + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P + ): (...args: Args) => P + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q + ): (...args: Args) => Q + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q + ): (this: Self, ...args: Args) => Q + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q + ): (...args: Args) => Q + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R + ): (...args: Args) => R + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R + ): (this: Self, ...args: Args) => R + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R + ): (...args: Args) => R + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S + ): (...args: Args) => S + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S + ): (this: Self, ...args: Args) => S + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S + ): (...args: Args) => S + + < + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T + >( + body: (this: unassigned, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S, + t: (_: S, ...args: Args) => T + ): (...args: Args) => T + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T + >( + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S, + t: (_: S, ...args: Args) => T + ): (this: Self, ...args: Args) => T + + < + Self, + Eff extends Effect, + AEff, + Args extends Array, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T + >( + options: { readonly self: Self }, + body: (this: Self, ...args: Args) => Generator | (Eff & Effect), + a: ( + _: Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect] ? R + : never + >, + ...args: Args + ) => A, + b: (_: A, ...args: Args) => B, + c: (_: B, ...args: Args) => C, + d: (_: C, ...args: Args) => D, + e: (_: D, ...args: Args) => E, + f: (_: E, ...args: Args) => F, + g: (_: F, ...args: Args) => G, + h: (_: G, ...args: Args) => H, + i: (_: H, ...args: Args) => I, + j: (_: I, ...args: Args) => J, + k: (_: J, ...args: Args) => K, + l: (_: K, ...args: Args) => L, + m: (_: L, ...args: Args) => M, + n: (_: M, ...args: Args) => N, + o: (_: N, ...args: Args) => O, + p: (_: O, ...args: Args) => P, + q: (_: P, ...args: Args) => Q, + r: (_: Q, ...args: Args) => R, + s: (_: R, ...args: Args) => S, + t: (_: S, ...args: Args) => T + ): (...args: Args) => T + } +} + +/** + * Creates an Effect-returning function without tracing. + * + * **When to use** + * + * Use when you are defining a reusable Effect function whose implementation + * would otherwise be a normal function returning {@link gen}, especially when + * tracing spans or stack-frame capture are not needed. + * + * **Details** + * + * Compared to a plain function that returns {@link gen}, `Effect.fnUntraced` + * reuses the generator body instead of allocating a fresh generator closure + * around the arguments on every call. It does not record an Effect stack-frame + * boundary and does not create tracing spans. Use {@link fn} when you need + * those stack frames or spans. Additional arguments after the generator body + * act like `pipe` transforms: each transform receives the previous result and + * the original function arguments. Annotate the generator return type with + * `Effect.fn.Return` when the produced `Effect` type needs to be + * stated explicitly. + * + * **Example** (Defining untraced effect functions) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced(function*( + * value: string + * ) { + * return yield* Effect.succeed(value.length) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Transforming the returned Effect) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced( + * function*(value: string) { + * return yield* Effect.succeed(value.length) + * }, + * (effect, value) => + * effect.pipe(Effect.map((length) => `${value}: ${length}`)) + * ) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Annotating an untraced non-parametric function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced(function*( + * value: string + * ): Effect.fn.Return { + * return yield* Effect.succeed(value.length) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Annotating an untraced parametric function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fnUntraced(function*( + * value: A + * ): Effect.fn.Return { + * return yield* Effect.succeed(value) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * @category functions + * @since 3.12.0 + */ +export const fnUntraced: fn.Untraced = internal.fnUntraced + +/** + * Creates a reusable traced function from an Effect body. + * + * **When to use** + * + * Use when you are defining a reusable Effect function whose implementation + * would otherwise be a normal function returning {@link gen}, and you want + * tracing spans or stack-frame capture. + * + * **Details** + * + * Compared to a plain function that returns {@link gen}, `Effect.fn` reuses the + * generator body instead of allocating a fresh generator closure around the + * arguments on every call. Call `Effect.fn(body, ...)` for a generic + * stack-frame boundary without creating a span. Call + * `Effect.fn("operationName", options?)(body, ...)` when that boundary should + * have a readable operation name and the returned `Effect` should create a + * tracing span when run. {@link SpanOptionsNoTrace} configures span metadata + * such as attributes, links, parent or root selection, kind, sampling, and log + * level. Additional arguments after the generator body act like `pipe` + * transforms: each transform receives the previous result and the original + * function arguments. When those transforms return an `Effect`, the returned + * effect includes stack-frame metadata and, for the named form, a tracing span. + * Generator bodies may declare a `this` parameter; pass `{ self }` before the + * body to bind `this` when the function is created. + * + * **Example** (Defining traced effect functions) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fn("calculateLength")(function*(value: string) { + * return yield* Effect.succeed(value.length) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Transforming the returned Effect) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fn("formatLength")( + * function*(value: string) { + * return yield* Effect.succeed(value.length) + * }, + * (effect, value) => + * effect.pipe(Effect.map((length) => `${value}: ${length}`)) + * ) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Binding this) + * + * ```ts + * import { Effect } from "effect" + * + * class Counter { + * count = 0 + * + * increment = Effect.fn("Counter.increment")( + * { self: this }, + * function*(this: Counter, by: number) { + * this.count += by + * return yield* Effect.succeed(this.count) + * } + * ) + * } + * + * const counter = new Counter() + * + * // ┌─── Effect.Effect + * // ▼ + * const program = counter.increment(1) + * ``` + * + * **Example** (Annotating a traced non-parametric function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fn("calculateLength")(function*( + * value: string + * ): Effect.fn.Return { + * return yield* Effect.succeed(value.length) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * **Example** (Annotating a traced parametric function) + * + * ```ts + * import { Effect } from "effect" + * + * const f = Effect.fn("succeed")(function*( + * value: A + * ): Effect.fn.Return { + * return yield* Effect.succeed(value) + * }) + * + * // ┌─── Effect.Effect + * // ▼ + * const program = f("hello") + * ``` + * + * @category functions + * @since 3.11.0 + */ +export const fn: fn.Traced & { + (name: string, options?: SpanOptionsNoTrace): fn.Traced +} = internal.fn + +// ======================================================================== +// Clock +// ======================================================================== + +/** + * Retrieves the `Clock` service from the context and provides it to the + * specified effectful function. + * + * **Example** (Accessing the Clock service) + * + * ```ts + * import { Console, Effect } from "effect" + * + * const program = Effect.clockWith((clock) => + * clock.currentTimeMillis.pipe( + * Effect.map((currentTime) => `Current time is: ${currentTime}`), + * Effect.tap(Console.log) + * ) + * ) + * + * Effect.runFork(program) + * // Example Output: + * // Current time is: 1735484929744 + * ``` + * + * @category clock + * @since 2.0.0 + */ +export const clockWith: ( + f: (clock: Clock) => Effect +) => Effect = internal.clockWith + +// ======================================================================== +// Logging +// ======================================================================== + +/** + * Creates a logger function that logs at the specified level. + * + * **Details** + * + * If no level is provided, the logger uses the fiber's current log level and + * extracts any `Cause` values from the message list. + * + * **Example** (Logging at a dynamic level) + * + * ```ts + * import { Effect } from "effect" + * + * const logWarn = Effect.logWithLevel("Warn") + * + * const program = Effect.gen(function*() { + * yield* logWarn("Cache miss", { key: "user:1" }) + * }) + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logWithLevel: (level?: Severity) => (...message: ReadonlyArray) => Effect = + internal.logWithLevel + +/** + * Logs one or more messages using the default log level. + * + * **Example** (Logging at the default level) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Starting computation") + * const result = 2 + 2 + * yield* Effect.log("Result:", result) + * yield* Effect.log("Multiple", "values", "can", "be", "logged") + * return result + * }) + * + * Effect.runPromise(program).then(console.log) + * // Output: + * // timestamp=2023-... level=INFO message="Starting computation" + * // timestamp=2023-... level=INFO message="Result: 4" + * // timestamp=2023-... level=INFO message="Multiple values can be logged" + * // 4 + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const log: (...message: ReadonlyArray) => Effect = internal.logWithLevel() + +/** + * Logs one or more messages at the FATAL level. + * + * **Example** (Logging fatal messages) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * try { + * // Simulate a critical system failure + * throw new Error("System memory exhausted") + * } catch (error) { + * const errorMessage = error instanceof Error ? error.message : String(error) + * yield* Effect.logFatal("Critical system failure:", errorMessage) + * yield* Effect.logFatal("System shutting down") + * } + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=FATAL message="Critical system failure: System memory exhausted" + * // timestamp=2023-... level=FATAL message="System shutting down" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logFatal: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Fatal") + +/** + * Logs one or more messages at the WARNING level. + * + * **Example** (Logging warnings) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logWarning("API rate limit approaching") + * yield* Effect.logWarning("Retries remaining:", 2, "Operation:", "fetchData") + * + * // Useful for non-critical issues + * const deprecated = true + * if (deprecated) { + * yield* Effect.logWarning("Using deprecated API endpoint") + * } + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=WARN message="API rate limit approaching" + * // timestamp=2023-... level=WARN message="Retries remaining: 2 Operation: fetchData" + * // timestamp=2023-... level=WARN message="Using deprecated API endpoint" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logWarning: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Warn") + +/** + * Logs one or more messages at the ERROR level. + * + * **Example** (Logging errors) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logError("Database connection failed") + * yield* Effect.logError( + * "Error code:", + * 500, + * "Message:", + * "Internal server error" + * ) + * + * // Can be used with error objects + * const error = new Error("Something went wrong") + * yield* Effect.logError("Caught error:", error.message) + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=ERROR message="Database connection failed" + * // timestamp=2023-... level=ERROR message="Error code: 500 Message: Internal server error" + * // timestamp=2023-... level=ERROR message="Caught error: Something went wrong" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logError: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Error") + +/** + * Logs one or more messages at the INFO level. + * + * **Example** (Logging information) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logInfo("Application starting up") + * yield* Effect.logInfo("Config loaded:", "production", "Port:", 3000) + * + * // Useful for general information + * const version = "1.2.3" + * yield* Effect.logInfo("Application version:", version) + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=INFO message="Application starting up" + * // timestamp=2023-... level=INFO message="Config loaded: production Port: 3000" + * // timestamp=2023-... level=INFO message="Application version: 1.2.3" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logInfo: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Info") + +/** + * Logs one or more messages at the DEBUG level. + * + * **Example** (Logging debug messages) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logDebug("Debug mode enabled") + * + * const userInput = { name: "Alice", age: 30 } + * yield* Effect.logDebug("Processing user input:", userInput) + * + * // Useful for detailed diagnostic information + * yield* Effect.logDebug("Variable state:", "x=10", "y=20", "z=30") + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=DEBUG message="Debug mode enabled" + * // timestamp=2023-... level=DEBUG message="Processing user input: [object Object]" + * // timestamp=2023-... level=DEBUG message="Variable state: x=10 y=20 z=30" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logDebug: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Debug") + +/** + * Logs one or more messages at the TRACE level. + * + * **Example** (Logging trace messages) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.logTrace("Entering function processData") + * + * // Trace detailed execution flow + * for (let i = 0; i < 3; i++) { + * yield* Effect.logTrace("Loop iteration:", i, "Processing item") + * } + * + * yield* Effect.logTrace("Exiting function processData") + * }) + * + * Effect.runPromise(program) + * // Output: + * // timestamp=2023-... level=TRACE message="Entering function processData" + * // timestamp=2023-... level=TRACE message="Loop iteration: 0 Processing item" + * // timestamp=2023-... level=TRACE message="Loop iteration: 1 Processing item" + * // timestamp=2023-... level=TRACE message="Loop iteration: 2 Processing item" + * // timestamp=2023-... level=TRACE message="Exiting function processData" + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const logTrace: (...message: ReadonlyArray) => Effect = internal.logWithLevel("Trace") + +/** + * Adds a logger to the set of loggers which will output logs for this effect. + * + * **Example** (Adding a logger to an effect) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Create a custom logger that logs to the console + * const customLogger = Logger.make(({ message }) => + * Effect.sync(() => console.log(`[CUSTOM]: ${message}`)) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.log("This will go to both default and custom logger") + * return "completed" + * }) + * + * // Add the custom logger to the effect + * const programWithLogger = Effect.withLogger(program, customLogger) + * + * Effect.runPromise(programWithLogger) + * // Output includes both default and custom log outputs + * ``` + * + * @category logging + * @since 4.0.0 + */ +export const withLogger = dual< + ( + logger: Logger + ) => (effect: Effect) => Effect, + ( + effect: Effect, + logger: Logger + ) => Effect +>(2, (effect, logger) => + internal.updateService( + effect, + internal.CurrentLoggers, + (loggers) => new Set([...loggers, logger]) + )) + +/** + * Adds an annotation to each log line in this effect. + * + * **Example** (Adding log annotations) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Starting operation") + * yield* Effect.log("Processing data") + * yield* Effect.log("Operation completed") + * }) + * + * // Add annotations to all log messages + * const annotatedProgram = Effect.annotateLogs(program, { + * userId: "user123", + * operation: "data-processing" + * }) + * + * // Also supports single key-value annotations + * const singleAnnotated = Effect.annotateLogs(program, "requestId", "req-456") + * + * Effect.runPromise(annotatedProgram) + * // All log messages will include the userId and operation annotations + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const annotateLogs = dual< + { + ( + key: string, + value: unknown + ): (effect: Effect) => Effect + ( + values: Record + ): (effect: Effect) => Effect + }, + { + ( + effect: Effect, + key: string, + value: unknown + ): Effect + ( + effect: Effect, + values: Record + ): Effect + } +>( + (args) => isEffect(args[0]), + ( + effect: Effect, + ...args: [Record] | [key: string, value: unknown] + ): Effect => + internal.updateService(effect, CurrentLogAnnotations, (annotations) => { + const newAnnotations = { ...annotations } + if (args.length === 1) { + Object.assign(newAnnotations, args[0]) + } else { + newAnnotations[args[0]] = args[1] + } + return newAnnotations + }) +) + +/** + * Adds log annotations to the current scope. + * + * **When to use** + * + * Use to attach log annotations that last until the current scope closes. + * + * **Details** + * + * This differs from `annotateLogs`, which only annotates a specific effect. + * `annotateLogsScoped` updates annotations for the entire current `Scope` and + * restores the previous annotations when the scope closes. + * + * **Example** (Adding scoped log annotations) + * + * ```ts + * import { Effect } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * yield* Effect.log("before") + * yield* Effect.annotateLogsScoped({ requestId: "req-123" }) + * yield* Effect.log("inside scope") + * }) + * ) + * + * Effect.runPromise(program) + * ``` + * + * @see {@link annotateLogs} for annotating one effect + * + * @category logging + * @since 3.1.0 + */ +export const annotateLogsScoped: { + (key: string, value: unknown): Effect + (values: Record): Effect +} = internal.annotateLogsScoped + +/** + * Adds a span to each log line in this effect. + * + * **Example** (Adding a log span) + * + * ```ts + * import { Effect } from "effect" + * + * const databaseOperation = Effect.gen(function*() { + * yield* Effect.log("Connecting to database") + * yield* Effect.log("Executing query") + * yield* Effect.log("Processing results") + * return "data" + * }) + * + * const httpRequest = Effect.gen(function*() { + * yield* Effect.log("Making HTTP request") + * const data = yield* Effect.withLogSpan(databaseOperation, "db-operation") + * yield* Effect.log("Sending response") + * return data + * }) + * + * const program = Effect.withLogSpan(httpRequest, "http-handler") + * + * Effect.runPromise(program) + * // All log messages will include span information showing the nested operation context + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const withLogSpan = dual< + (label: string) => (effect: Effect) => Effect, + (effect: Effect, label: string) => Effect +>( + 2, + (effect, label) => + internal.flatMap(internal.currentTimeMillis, (now) => + internal.updateService(effect, CurrentLogSpans, (spans) => { + const span: [label: string, timestamp: number] = [label, now] + return [span, ...spans] + })) +) + +// ----------------------------------------------------------------------------- +// Metrics +// ----------------------------------------------------------------------------- + +/** + * Updates the `Metric` every time the `Effect` is executed. + * + * **Details** + * + * Also accepts an optional function which can be used to map the `Exit` value + * of the `Effect` into a valid `Input` for the `Metric`. + * + * **Example** (Incrementing a metric for each execution) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const counter = Metric.counter("effect_executions", { + * description: "Counts effect executions" + * }).pipe(Metric.withConstantInput(1)) + * + * const program = Effect.succeed("Hello").pipe( + * Effect.track(counter) + * ) + * + * // This will increment the counter by 1 when executed + * Effect.runPromise(program).then(() => + * Effect.runPromise(Metric.value(counter)).then(console.log) + * // Output: { count: 1, incremental: false } + * ) + * ``` + * + * **Example** (Mapping exits before updating a metric) + * + * ```ts + * import { Effect, Exit, Metric } from "effect" + * + * // Track different exit types with custom mapping + * const exitTracker = Metric.frequency("exit_types", { + * description: "Tracks success/failure/defect counts" + * }) + * + * const mapExitToString = (exit: Exit.Exit) => { + * if (Exit.isSuccess(exit)) return "success" + * if (Exit.isFailure(exit)) return "failure" + * return "defect" + * } + * + * const effect = Effect.succeed("result").pipe( + * Effect.track(exitTracker, mapExitToString) + * ) + * ``` + * + * @category tracking + * @since 4.0.0 + */ +export const track: { + ( + metric: Metric.Metric, + f: (exit: Exit.Exit) => Input + ): (self: Effect) => Effect + ( + metric: Metric.Metric, NoInfer>, State> + ): (self: Effect) => Effect + ( + self: Effect, + metric: Metric.Metric, + f: (exit: Exit.Exit) => Input + ): Effect + ( + self: Effect, + metric: Metric.Metric, NoInfer>, State> + ): Effect +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect, + metric: Metric.Metric, + f: (exit: Exit.Exit) => Input + ): Effect => + onExit(self, (exit) => { + const input = f === undefined ? exit : internalCall(() => f(exit)) + return Metric.update(metric, input as any) + }) +) + +/** + * Updates the provided `Metric` every time the wrapped `Effect` succeeds with + * a value. + * + * **Details** + * + * Also accepts an optional function which can be used to map the success value + * of the `Effect` into a valid `Input` for the `Metric`. + * + * **Example** (Counting successful results) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const successCounter = Metric.counter("successes").pipe( + * Metric.withConstantInput(1) + * ) + * + * const program = Effect.succeed(42).pipe( + * Effect.trackSuccesses(successCounter) + * ) + * + * Effect.runPromise(program).then(() => + * Effect.runPromise(Metric.value(successCounter)).then(console.log) + * // Output: { count: 1, incremental: false } + * ) + * ``` + * + * **Example** (Mapping successes before tracking) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * // Track successful request sizes + * const requestSizeGauge = Metric.gauge("request_size_bytes") + * + * const program = Effect.succeed("Hello World!").pipe( + * Effect.trackSuccesses(requestSizeGauge, (value: string) => value.length) + * ) + * + * Effect.runPromise(program).then(() => + * Effect.runPromise(Metric.value(requestSizeGauge)).then(console.log) + * // Output: { value: 12 } + * ) + * ``` + * + * @category tracking + * @since 4.0.0 + */ +export const trackSuccesses: { + ( + metric: Metric.Metric, + f: (value: A) => Input + ): (self: Effect) => Effect + ( + metric: Metric.Metric, State> + ): (self: Effect) => Effect + ( + self: Effect, + metric: Metric.Metric, + f: (value: A) => Input + ): Effect + ( + self: Effect, + metric: Metric.Metric, State> + ): Effect +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect, + metric: Metric.Metric, + f: ((value: A) => Input) | undefined + ): Effect => + tap(self, (value) => { + const input = f === undefined ? value : f(value) + return Metric.update(metric, input as any) + }) +) + +/** + * Updates the provided `Metric` every time the wrapped `Effect` fails with an + * **expected** error. + * + * **Details** + * + * Also accepts an optional function which can be used to map the error value + * of the `Effect` into a valid `Input` for the `Metric`. + * + * **Example** (Counting expected failures) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const errorCounter = Metric.counter("errors").pipe( + * Metric.withConstantInput(1) + * ) + * + * const program = Effect.fail("Network timeout").pipe( + * Effect.trackErrors(errorCounter) + * ) + * + * Effect.runPromiseExit(program).then(() => + * Effect.runPromise(Metric.value(errorCounter)).then(console.log) + * // Output: { count: 1, incremental: false } + * ) + * ``` + * + * **Example** (Mapping errors before tracking) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class ConnectionFailedError extends Data.TaggedError("ConnectionFailedError")<{}> {} + * + * // Track error types using frequency metric + * const errorTypeFrequency = Metric.frequency("error_types") + * + * const program = Effect.fail(new ConnectionFailedError()).pipe( + * Effect.trackErrors(errorTypeFrequency, (error: ConnectionFailedError) => error._tag) + * ) + * + * Effect.runPromiseExit(program).then(() => + * Effect.runPromise(Metric.value(errorTypeFrequency)).then(console.log) + * // Output: { occurrences: Map(1) { "ConnectionFailedError" => 1 } } + * ) + * ``` + * + * @category tracking + * @since 4.0.0 + */ +export const trackErrors: { + ( + metric: Metric.Metric, + f: (error: E) => Input + ): (self: Effect) => Effect + ( + metric: Metric.Metric, State> + ): (self: Effect) => Effect + ( + self: Effect, + metric: Metric.Metric, + f: (error: E) => Input + ): Effect + ( + self: Effect, + metric: Metric.Metric, State> + ): Effect +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect, + metric: Metric.Metric, + f: ((error: E) => Input) | undefined + ): Effect => + tapError(self, (error) => { + const input = f === undefined ? error : internalCall(() => f(error)) + return Metric.update(metric, input as any) + }) +) + +/** + * Updates the provided `Metric` every time the wrapped `Effect` fails with an + * **unexpected** error (i.e. a defect). + * + * **Details** + * + * Also accepts an optional function which can be used to map the defect value + * of the `Effect` into a valid `Input` for the `Metric`. + * + * **Example** (Counting defects) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const defectCounter = Metric.counter("defects").pipe( + * Metric.withConstantInput(1) + * ) + * + * const program = Effect.die("Critical system failure").pipe( + * Effect.trackDefects(defectCounter) + * ) + * + * Effect.runPromiseExit(program).then(() => + * Effect.runPromise(Metric.value(defectCounter)).then(console.log) + * // Output: { count: 1, incremental: false } + * ) + * ``` + * + * **Example** (Mapping defects before tracking) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * // Track defect types using frequency metric + * const defectTypeFrequency = Metric.frequency("defect_types") + * + * const program = Effect.die(new Error("Null pointer exception")).pipe( + * Effect.trackDefects(defectTypeFrequency, (defect: unknown) => { + * if (defect instanceof Error) return defect.constructor.name + * return typeof defect + * }) + * ) + * + * Effect.runPromiseExit(program).then(() => + * Effect.runPromise(Metric.value(defectTypeFrequency)).then(console.log) + * // Output: { occurrences: Map(1) { "Error" => 1 } } + * ) + * ``` + * + * @category tracking + * @since 4.0.0 + */ +export const trackDefects: { + ( + metric: Metric.Metric, + f: (defect: unknown) => Input + ): (self: Effect) => Effect + ( + metric: Metric.Metric + ): (self: Effect) => Effect + ( + self: Effect, + metric: Metric.Metric, + f: (defect: unknown) => Input + ): Effect + ( + self: Effect, + metric: Metric.Metric + ): Effect +} = dual( + (args) => isEffect(args[0]), + (self, metric, f) => + tapDefect(self, (defect) => { + const input = f === undefined ? defect : internalCall(() => f(defect)) + return Metric.update(metric, input) + }) +) + +/** + * Updates the provided `Metric` with the `Duration` of time (in nanoseconds) + * that the wrapped `Effect` took to complete. + * + * **Details** + * + * Also accepts an optional function which can be used to map the `Duration` + * that the wrapped `Effect` took to complete into a valid `Input` for the + * `Metric`. + * + * **Example** (Recording execution duration) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const executionTimer = Metric.timer("execution_time") + * + * const program = Effect.sleep("100 millis").pipe( + * Effect.trackDuration(executionTimer) + * ) + * + * Effect.runPromise(program).then(() => + * Effect.runPromise(Metric.value(executionTimer)).then(console.log) + * // Output: { count: 1, min: 100000000, max: 100000000, sum: 100000000 } + * ) + * ``` + * + * **Example** (Mapping duration before tracking) + * + * ```ts + * import { Duration, Effect, Metric } from "effect" + * + * // Track execution time in milliseconds using custom mapping + * const durationGauge = Metric.gauge("execution_millis") + * + * const program = Effect.sleep("200 millis").pipe( + * Effect.trackDuration(durationGauge, (duration) => Duration.toMillis(duration)) + * ) + * + * Effect.runPromise(program).then(() => + * Effect.runPromise(Metric.value(durationGauge)).then(console.log) + * // Output: { value: 200 } + * ) + * ``` + * + * @category tracking + * @since 4.0.0 + */ +export const trackDuration: { + ( + metric: Metric.Metric, + f: (duration: Duration.Duration) => Input + ): (self: Effect) => Effect + ( + metric: Metric.Metric + ): (self: Effect) => Effect + ( + self: Effect, + metric: Metric.Metric, + f: (duration: Duration.Duration) => Input + ): Effect + ( + self: Effect, + metric: Metric.Metric + ): Effect +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect, + metric: Metric.Metric, + f: ((duration: Duration.Duration) => Input) | undefined + ): Effect => + clockWith((clock) => { + const startTime = clock.currentTimeNanosUnsafe() + return onExit(self, () => { + const endTime = clock.currentTimeNanosUnsafe() + const duration = Duration.subtract( + Duration.fromInputUnsafe(endTime), + Duration.fromInputUnsafe(startTime) + ) + const input = f === undefined ? duration : internalCall(() => f(duration)) + return Metric.update(metric, input as any) + }) + }) +) + +// ----------------------------------------------------------------------------- +// Transactions +// ----------------------------------------------------------------------------- + +/** + * Service that holds the current transaction state. + * + * **Details** + * + * It includes a journal that stores non-committed changes to `TxRef` values and + * a retry flag that records whether the transaction should be retried. + * + * **Example** (Building transactions) + * + * ```ts + * import { Effect } from "effect" + * + * // Transaction class for software transactional memory operations + * const txEffect = Effect.gen(function*() { + * const tx = yield* Effect.Transaction + * // Use transaction for coordinated state changes + * return "Transaction complete" + * }) + * ``` + * + * @category transactions + * @since 4.0.0 + */ +export class Transaction extends Context.Service< + Transaction, + { + retry: boolean + readonly journal: Map< + TxRef, + { + readonly version: number + value: any + } + > + } +>()("effect/Effect/Transaction") {} + +/** + * Defines a transaction boundary. Transactions are "all or nothing" with respect to changes + * made to transactional values (i.e. TxRef) that occur within the transaction body. + * + * **Details** + * + * If called inside an active transaction, `tx` composes with the current transaction and reuses + * its journal and retry state instead of creating a nested boundary. + * + * Effect transactions are optimistic with retry. A transaction is retried when + * its body explicitly calls `Effect.txRetry` and any accessed transactional + * value changes, or when any accessed transactional value changes because a + * different transaction commits before the current one. + * + * The outermost `tx` call creates the transaction boundary and commits or rolls back the full + * composed transaction. + * + * **Example** (Running a transaction) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref1 = yield* TxRef.make(0) + * const ref2 = yield* TxRef.make(0) + * + * // Nested tx calls compose into the same transaction + * yield* Effect.tx(Effect.gen(function*() { + * yield* TxRef.set(ref1, 10) + * yield* Effect.tx(TxRef.set(ref2, 20)) + * const sum = (yield* TxRef.get(ref1)) + (yield* TxRef.get(ref2)) + * console.log(`Transaction sum: ${sum}`) + * })) + * + * console.log(`Final ref1: ${yield* TxRef.get(ref1)}`) // 10 + * console.log(`Final ref2: ${yield* TxRef.get(ref2)}`) // 20 + * }) + * ``` + * + * @category transactions + * @since 4.0.0 + */ +export const tx = ( + effect: Effect +): Effect> => + withFiber((fiber) => { + if (fiber.context.mapUnsafe.has(Transaction.key)) { + return effect as Effect> + } + // Create transaction state only at the outermost boundary + const state: Transaction["Service"] = { journal: new Map(), retry: false } + let result: Exit.Exit | undefined + return uninterruptibleMask((restore) => + flatMap( + whileLoop({ + while: () => !result, + body: constant( + restore(effect).pipe( + provideService(Transaction, state), + tapCause(() => { + if (!state.retry) return void_ + return restore(awaitPendingTransaction(state)) + }), + exit + ) + ), + step(exit: Exit.Exit) { + if (state.retry || !isTransactionConsistent(state)) { + return clearTransaction(state) + } + if (Exit.isSuccess(exit)) { + commitTransaction(fiber, state) + } else { + clearTransaction(state) + } + result = exit + } + }), + () => result! + ) + ) + }) + +const isTransactionConsistent = (state: Transaction["Service"]) => { + for (const [ref, { version }] of state.journal) { + if (ref.version !== version) { + return false + } + } + return true +} + +const awaitPendingTransaction = (state: Transaction["Service"]) => + suspend(() => { + const key = {} + const refs = Array.from(state.journal.keys()) + const clearPending = () => { + for (const clear of refs) { + clear.pending.delete(key) + } + } + return callback((resume) => { + const onCall = () => { + clearPending() + resume(void_) + } + for (const ref of refs) { + ref.pending.set(key, onCall) + } + return sync(clearPending) + }) + }) + +function commitTransaction(fiber: Fiber, state: Transaction["Service"]) { + for (const [ref, { value }] of state.journal) { + if (value !== ref.value) { + ref.version = ref.version + 1 + ref.value = value + } + for (const pending of ref.pending.values()) { + fiber.currentDispatcher.scheduleTask(pending, 0) + } + ref.pending.clear() + } +} + +function clearTransaction(state: Transaction["Service"]) { + state.retry = false + state.journal.clear() +} + +/** + * Retries the current transaction by signaling that it must be retried. + * + * **Details** + * + * NOTE: the transaction retries on any change to transactional values (i.e. TxRef) accessed in its body. + * + * **Example** (Retrying transactions) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * // create a transactional reference + * const ref = yield* TxRef.make(0) + * + * // forks a fiber that increases the value of `ref` every 100 millis + * yield* Effect.forkChild(Effect.forever( + * // update to transactional value + * Effect.tx(TxRef.update(ref, (n) => n + 1)).pipe(Effect.delay("100 millis")) + * )) + * + * // the following will retry 10 times until the `ref` value is 10 + * yield* Effect.tx(Effect.gen(function*() { + * const value = yield* TxRef.get(ref) + * if (value < 10) { + * yield* Effect.log(`retry due to value: ${value}`) + * return yield* Effect.txRetry + * } + * yield* Effect.log(`transaction done with value: ${value}`) + * })) + * }) + * + * Effect.runPromise(program).catch(console.error) + * ``` + * + * @category transactions + * @since 4.0.0 + */ +export const txRetry: Effect = flatMap( + Transaction, + (state) => { + state.retry = true + return interrupt + } +) +/** + * Type helpers for converting callback-based functions into `Effect` functions. + * + * @since 4.0.0 + */ +export declare namespace Effectify { + interface Callback { + (err: E, a?: A): void + } + + type ArgsWithCallback, E, A> = [...args: Args, cb: Callback] + + type WithoutNull = unknown extends A ? void : Exclude + + /** + * Converts a callback-based function type into an `Effect`-returning function type. + * + * @category effectify + * @since 4.0.0 + */ + export type Effectify = T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + (...args: ArgsWithCallback): infer _R10 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + (...args: Args6): Effect, E> + (...args: Args7): Effect, E> + (...args: Args8): Effect, E> + (...args: Args9): Effect, E> + (...args: Args10): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + (...args: Args6): Effect, E> + (...args: Args7): Effect, E> + (...args: Args8): Effect, E> + (...args: Args9): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + (...args: Args6): Effect, E> + (...args: Args7): Effect, E> + (...args: Args8): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + (...args: Args6): Effect, E> + (...args: Args7): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + (...args: Args6): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + (...args: Args5): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + (...args: Args4): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + (...args: Args3): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + } ? { + (...args: Args1): Effect, E> + (...args: Args2): Effect, E> + } + : T extends { + (...args: ArgsWithCallback): infer _R1 + } ? { + (...args: Args1): Effect, E> + } + : never + + /** + * Extracts the callback error type from a callback-based function type. + * + * @category effectify + * @since 4.0.0 + */ + export type EffectifyError = T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + (...args: ArgsWithCallback): infer _R10 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + (...args: ArgsWithCallback): infer _R9 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + (...args: ArgsWithCallback): infer _R8 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + (...args: ArgsWithCallback): infer _R7 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + (...args: ArgsWithCallback): infer _R6 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + (...args: ArgsWithCallback): infer _R5 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + (...args: ArgsWithCallback): infer _R4 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + (...args: ArgsWithCallback): infer _R3 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + (...args: ArgsWithCallback): infer _R2 + } ? NonNullable + : T extends { + (...args: ArgsWithCallback): infer _R1 + } ? NonNullable + : never +} + +/** + * Converts an error-first callback API into a function that returns an + * `Effect`. + * + * **Details** + * + * The original function is called with the supplied arguments plus a final + * callback. A non-null callback error fails the returned effect, while a + * successful callback value becomes the effect success. Use `onError` to map + * callback errors and `onSyncError` to turn synchronous throws into typed + * failures; otherwise synchronous throws become defects. + * + * **Example** (Converting callbacks to effects) + * + * ```ts + * import { Effect } from "effect" + * import * as fs from "fs" + * + * // Convert Node.js readFile to an Effect + * const readFile = Effect.effectify(fs.readFile) + * + * // Use the effectified function + * const program = readFile("package.json", "utf8") + * + * Effect.runPromise(program).then(console.log) + * // Output: contents of package.json + * ``` + * + * **Example** (Mapping callback errors to typed failures) + * + * ```ts + * import { Effect } from "effect" + * import * as fs from "fs" + * + * const readFile = Effect.effectify( + * fs.readFile, + * (error, args) => new Error(`Failed to read file ${args[0]}: ${error.message}`) + * ) + * + * const program = readFile("nonexistent.txt", "utf8") + * + * Effect.runPromiseExit(program).then(console.log) + * // Output: Exit.failure with custom error message + * ``` + * + * @category effectify + * @since 4.0.0 + */ +export const effectify: { + ) => any>(fn: F): Effectify.Effectify> + ) => any, E>( + fn: F, + onError: (error: Effectify.EffectifyError, args: Parameters) => E + ): Effectify.Effectify + ) => any, E, E2>( + fn: F, + onError: (error: Effectify.EffectifyError, args: Parameters) => E, + onSyncError: (error: unknown, args: Parameters) => E2 + ): Effectify.Effectify +} = + ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => + (...args: Array) => + callback((resume) => { + try { + fn(...args, (err: globalThis.Error | null, result: A) => { + if (err) { + resume(fail(onError ? onError(err, args) : err)) + } else { + resume(succeed(result)) + } + }) + } catch (err) { + resume(onSyncError ? fail(onSyncError(err, args)) : die(err)) + } + })) as any + +// ----------------------------------------------------------------------------- +// Type constraints +// ----------------------------------------------------------------------------- + +/** + * Ensures that an effect's success type extends a given type `A`. + * + * **Details** + * + * This helper is checked at compile time and does not change the effect's + * runtime behavior. + * + * **Example** (Constraining the success type) + * + * ```ts + * import { Effect } from "effect" + * + * // Define a constraint that the success type must be a number + * const satisfiesNumber = Effect.satisfiesSuccessType() + * + * // This works - Effect<42, never, never> extends Effect + * const validEffect = satisfiesNumber(Effect.succeed(42)) + * + * // This would cause a TypeScript compilation error: + * // const invalidEffect = satisfiesNumber(Effect.succeed("string")) + * // ^^^^^^^^^^^^^^^^^^^^^^ + * // Type 'string' is not assignable to type 'number' + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesSuccessType = () => (effect: Effect): Effect => effect + +/** + * Ensures that an effect's error type extends a given type `E`. + * + * **Details** + * + * This helper is checked at compile time and does not change the effect's + * runtime behavior. + * + * **Example** (Constraining the error type) + * + * ```ts + * import { Data, Effect } from "effect" + * + * class ValidationError extends Data.TaggedError("ValidationError")<{}> {} + * + * // Define a constraint that the error type must be a ValidationError + * const satisfiesError = Effect.satisfiesErrorType() + * + * // This works - Effect extends the constrained type + * const validEffect = satisfiesError(Effect.fail(new ValidationError())) + * + * // This would cause a TypeScript compilation error: + * // const invalidEffect = satisfiesError(Effect.fail("string error")) + * // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * // Type 'string' is not assignable to type 'ValidationError' + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesErrorType = () => (effect: Effect): Effect => effect + +/** + * Ensures that an effect's requirements type extends a given type `R`. + * + * **Details** + * + * This helper is checked at compile time and does not change the effect's + * runtime behavior. + * + * **Example** (Constraining the services type) + * + * ```ts + * import { Effect } from "effect" + * + * // Define a constraint that requires a string as the requirements type + * const satisfiesStringServices = Effect.satisfiesServicesType() + * + * // This works - effect requires string + * const validEffect: Effect.Effect = Effect.succeed(42) + * const constrainedEffect = satisfiesStringServices(validEffect) + * + * // This would cause a TypeScript compilation error if uncommented: + * // const invalidEffect: Effect.Effect = Effect.succeed(42) + * // const constrainedInvalid = satisfiesStringServices(invalidEffect) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesServicesType = () => (effect: Effect): Effect => effect + +/** + * Applies `map` eagerly when an effect is already resolved. + * + * **When to use** + * + * Use when an already-resolved effect should apply a success transformation + * immediately while pending effects still use regular mapping. + * + * **Details** + * + * Success effects apply the mapping function immediately. Failure effects pass + * through unchanged, and pending effects fall back to regular `map` behavior. + * + * **Example** (Mapping already completed effects) + * + * ```ts + * import { Effect } from "effect" + * + * // For resolved effects, the mapping is applied immediately + * const resolved = Effect.succeed(5) + * const mapped = Effect.mapEager(resolved, (n) => n * 2) // Applied eagerly + * + * // For pending effects, behaves like regular map + * const pending = Effect.delay(Effect.succeed(5), "100 millis") + * const mappedPending = Effect.mapEager(pending, (n) => n * 2) // Uses regular map + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const mapEager: { + (f: (a: A) => B): (self: Effect) => Effect + (self: Effect, f: (a: A) => B): Effect +} = internal.mapEager + +/** + * Applies `mapError` eagerly when an effect is already resolved. + * + * **When to use** + * + * Use when an already-resolved failed effect should apply an error + * transformation immediately while pending effects still use regular error + * mapping. + * + * **Details** + * + * Success effects pass through unchanged because there is no error to + * transform. Failure effects apply the mapping function immediately, and + * pending effects fall back to regular `mapError` behavior. + * + * **Example** (Mapping errors eagerly when possible) + * + * ```ts + * import { Effect } from "effect" + * + * // For resolved failure effects, the error mapping is applied immediately + * const failed = Effect.fail("original error") + * const mapped = Effect.mapErrorEager(failed, (err: string) => `mapped: ${err}`) // Applied eagerly + * + * // For pending effects, behaves like regular mapError + * const pending = Effect.delay(Effect.fail("error"), "100 millis") + * const mappedPending = Effect.mapErrorEager( + * pending, + * (err: string) => `mapped: ${err}` + * ) // Uses regular mapError + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const mapErrorEager: { + (f: (e: E) => E2): (self: Effect) => Effect + (self: Effect, f: (e: E) => E2): Effect +} = internal.mapErrorEager + +/** + * Applies `mapBoth` eagerly when an effect is already resolved. + * + * **When to use** + * + * Use when an already-resolved effect should transform either success or + * failure immediately while pending effects still use regular channel mapping. + * + * **Details** + * + * Success effects apply `onSuccess` immediately, and failure effects apply + * `onFailure` immediately. Pending effects fall back to regular `mapBoth` + * behavior. + * + * **Example** (Mapping both channels eagerly when possible) + * + * ```ts + * import { Effect } from "effect" + * + * // For resolved effects, the appropriate mapping is applied immediately + * const success = Effect.succeed(5) + * const mapped = Effect.mapBothEager(success, { + * onFailure: (err: string) => `Failed: ${err}`, + * onSuccess: (n: number) => n * 2 + * }) // onSuccess applied eagerly + * + * const failure = Effect.fail("error") + * const mappedError = Effect.mapBothEager(failure, { + * onFailure: (err: string) => `Failed: ${err}`, + * onSuccess: (n: number) => n * 2 + * }) // onFailure applied eagerly + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const mapBothEager: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect) => Effect + ( + self: Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect +} = internal.mapBothEager + +/** + * Applies `flatMap` eagerly when an effect is already resolved. + * + * **When to use** + * + * Use when an already-resolved successful effect should bind immediately to the + * next effect while pending effects still use regular flat mapping. + * + * **Details** + * + * Success effects apply the flatMap function immediately. Failure effects pass + * through unchanged, and pending effects fall back to regular `flatMap` + * behavior. + * + * **Example** (Flat mapping eagerly when possible) + * + * ```ts + * import { Effect } from "effect" + * + * // For resolved effects, the flatMap is applied immediately + * const resolved = Effect.succeed(5) + * const flatMapped = Effect.flatMapEager(resolved, (n) => Effect.succeed(n * 2)) // Applied eagerly + * + * // For pending effects, behaves like regular flatMap + * const pending = Effect.delay(Effect.succeed(5), "100 millis") + * const flatMappedPending = Effect.flatMapEager( + * pending, + * (n) => Effect.succeed(n * 2) + * ) // Uses regular flatMap + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const flatMapEager: { + (f: (a: A) => Effect): (self: Effect) => Effect + (self: Effect, f: (a: A) => Effect): Effect +} = internal.flatMapEager + +/** + * Applies `catch` eagerly when an effect is already resolved. + * + * **When to use** + * + * Use when an already-resolved failed effect should recover immediately while + * pending effects still use regular error recovery. + * + * **Details** + * + * Success effects pass through unchanged because there is no error to catch. + * Failure effects apply the catch function immediately, and pending effects + * fall back to regular `catch` behavior. + * + * **Example** (Catching failures eagerly when possible) + * + * ```ts + * import { Effect } from "effect" + * + * // For resolved failure effects, the catch function is applied immediately + * const failed = Effect.fail("original error") + * const recovered = Effect.catchEager( + * failed, + * (err: string) => Effect.succeed(`recovered from: ${err}`) + * ) // Applied eagerly + * + * // For success effects, returns success as-is + * const success = Effect.succeed(42) + * const unchanged = Effect.catchEager( + * success, + * (err: string) => Effect.succeed(`recovered from: ${err}`) + * ) // Returns success as-is + * + * // For pending effects, behaves like regular catch + * const pending = Effect.delay(Effect.fail("error"), "100 millis") + * const recoveredPending = Effect.catchEager( + * pending, + * (err: string) => Effect.succeed(`recovered from: ${err}`) + * ) // Uses regular catch + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const catchEager: { + ( + f: (e: NoInfer) => Effect + ): (self: Effect) => Effect + ( + self: Effect, + f: (e: NoInfer) => Effect + ): Effect +} = internal.catchEager + +/** + * Creates untraced function effects with eager evaluation optimization. + * + * **Details** + * + * Executes generator functions eagerly when all yielded effects are synchronous, + * stopping at the first async effect and deferring to normal execution. + * + * **Example** (Defining eager untraced effect functions) + * + * ```ts + * import { Effect } from "effect" + * + * const computation = Effect.fnUntracedEager(function*() { + * yield* Effect.succeed(1) + * yield* Effect.succeed(2) + * return "computed eagerly" + * }) + * + * const effect = computation() // Executed immediately if all effects are sync + * ``` + * + * @category eager + * @since 4.0.0 + */ +export const fnUntracedEager: fn.Untraced = internal.fnUntracedEager diff --git a/repos/effect/packages/effect/src/Effectable.ts b/repos/effect/packages/effect/src/Effectable.ts new file mode 100644 index 0000000..f4a114a --- /dev/null +++ b/repos/effect/packages/effect/src/Effectable.ts @@ -0,0 +1,68 @@ +/** + * Low-level helpers for making custom values behave like Effects. The module + * exposes a prototype builder and an abstract base class that let + * domain-specific values, such as service keys or configuration descriptions, + * be evaluated by Effect and yielded inside `Effect.gen`. + * + * @since 4.0.0 + */ +import type * as Effect from "./Effect.ts" +import type * as Fiber from "./Fiber.ts" +import { evaluate, makePrimitiveProto } from "./internal/core.ts" + +/** + * Create a low-level `Effect` prototype. + * + * **When to use** + * + * Use when you need to create a custom Effect-like value without extending a + * class, by providing a label and an evaluate function that receives the + * current fiber. + * + * **Details** + * + * When the effect is evaluated, it calls `evaluate` with the current fiber. + * + * @see {@link Class} for a class-based approach to defining custom Effect values + * + * @category prototypes + * @since 4.0.0 + */ +export const Prototype = >(options: { + readonly label: string + readonly evaluate: ( + this: A, + fiber: Fiber.Fiber + ) => Effect.Effect, Effect.Error, Effect.Services> +}): Effect.Effect, Effect.Error, Effect.Services> => + makePrimitiveProto({ + op: options.label, + [evaluate]: options.evaluate + }) as any + +const Base: new() => Effect.Effect = (() => { + const Base = function() {} + Base.prototype = Prototype({ + label: "Effectable", + evaluate(_) { + return this + } + }) + return Base as any +})() + +/** + * Provides an abstract class that can be extended to create an `Effect`. + * + * **When to use** + * + * Use as an abstract base class to define custom classes whose instances behave + * as `Effect` values. + * + * @see {@link Prototype} for a lower-level primitive approach to creating custom Effect-like values without a class + * @category constructors + * @since 2.0.0 + */ +export abstract class Class extends Base { + abstract override: Effect.Effect +} diff --git a/repos/effect/packages/effect/src/Encoding.ts b/repos/effect/packages/effect/src/Encoding.ts new file mode 100644 index 0000000..11885fb --- /dev/null +++ b/repos/effect/packages/effect/src/Encoding.ts @@ -0,0 +1,1046 @@ +/** + * Encoding and decoding helpers for Base64, Base64Url, and hexadecimal text. + * The functions convert between strings, UTF-8 text, and `Uint8Array` bytes. + * Encode functions return strings directly, while decode functions return + * `Result.Result` so invalid input is reported as an `EncodingError` instead of + * being thrown. + * + * @since 4.0.0 + */ +import * as Data from "./Data.ts" +import { hasProperty } from "./Predicate.ts" +import * as Result from "./Result.ts" + +// ------------------------------------------------------------------------------------- +// EncodingError +// ------------------------------------------------------------------------------------- + +/** + * Type identifier stored on `EncodingError` values and used by + * `isEncodingError`. + * + * **When to use** + * + * Use when implementing low-level `EncodingError`-compatible values that need + * to carry the runtime marker. + * + * **Details** + * + * This marker is part of the runtime representation of `EncodingError`. Prefer + * `isEncodingError` when narrowing unknown values. + * + * @see {@link isEncodingError} for the public guard that checks this marker + * + * @category type IDs + * @since 4.0.0 + */ +export const EncodingErrorTypeId = "~effect/encoding/EncodingError" as const + +/** + * Literal type of the `EncodingErrorTypeId` marker. + * + * **When to use** + * + * Use to type the marker carried by `EncodingError` values. + * + * @category type IDs + * @since 4.0.0 + */ +export type EncodingErrorTypeId = typeof EncodingErrorTypeId + +/** + * Error returned when an encoding or decoding operation cannot process its + * input. + * + * **When to use** + * + * Use when you need to handle or inspect failures from encoding or decoding + * operations. + * + * **Details** + * + * The error records whether the failure happened during encoding or decoding, + * which encoding module reported it, the original input, and a human-readable + * message. + * + * @see {@link isEncodingError} for checking whether a value is an EncodingError + * @category constructors + * @since 4.0.0 + */ +export class EncodingError extends Data.TaggedError("EncodingError")<{ + kind: "Decode" | "Encode" + module: string + input: unknown + message: string +}> { + /** + * Marks this value as an encoding or decoding error for runtime guards. + * + * **When to use** + * + * Use to identify `EncodingError` instances through `isEncodingError`. + * + * @since 4.0.0 + */ + readonly [EncodingErrorTypeId]: EncodingErrorTypeId = EncodingErrorTypeId +} + +/** + * Checks whether a value is an `EncodingError`. + * + * **When to use** + * + * Use to narrow an unknown value before handling it as an `EncodingError` from + * encoding or decoding code. + * + * **Details** + * + * Returns `true` when the value carries the `EncodingErrorTypeId` marker and + * narrows the value to `EncodingError`. + * + * @see {@link EncodingError} for the structured error produced by failed + * encoding and decoding operations + * + * @category guards + * @since 4.0.0 + */ +export const isEncodingError = (u: unknown): u is EncodingError => hasProperty(u, EncodingErrorTypeId) + +// ------------------------------------------------------------------------------------- +// Base64 +// ------------------------------------------------------------------------------------- + +/** + * Encodes the given value into a base64 (RFC4648) `string`. + * + * **When to use** + * + * Use to encode text or bytes as a standard padded Base64 string for storage or + * transport. + * + * **Details** + * + * String inputs are encoded as UTF-8 bytes before Base64 encoding. + * `Uint8Array` inputs are encoded directly. The output uses the standard + * RFC4648 alphabet with `=` padding. + * + * **Example** (Encoding Base64 strings and bytes) + * + * ```ts + * import { Encoding } from "effect" + * + * // Encode a string + * console.log(Encoding.encodeBase64("hello")) // "aGVsbG8=" + * + * // Encode binary data + * const bytes = new Uint8Array([72, 101, 108, 108, 111]) + * console.log(Encoding.encodeBase64(bytes)) // "SGVsbG8=" + * ``` + * + * @see {@link decodeBase64} for decoding standard Base64 to bytes + * @see {@link decodeBase64String} for decoding standard Base64 to UTF-8 text + * @see {@link encodeBase64Url} for URL-safe unpadded Base64 output + * + * @category encoding + * @since 2.0.0 + */ +export const encodeBase64: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? base64EncodeUint8Array(encoder.encode(input)) : base64EncodeUint8Array(input) + +/** + * Decodes a base64 (RFC4648) string into bytes safely. + * + * **When to use** + * + * Use to decode a standard padded Base64 string into bytes without throwing on + * invalid input. + * + * **Details** + * + * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input is not valid base64. + * + * **Example** (Decoding Base64 bytes) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeBase64("SGVsbG8=") + * if (Result.isSuccess(result)) { + * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111] + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64 = (str: string): Result.Result => { + const stripped = stripCrlf(str) + const length = stripped.length + if (length % 4 !== 0) { + return Result.fail( + new EncodingError({ + kind: "Decode", + module: "Base64", + input: stripped, + message: `Length must be a multiple of 4, but is ${length}` + }) + ) + } + + const index = stripped.indexOf("=") + if (index !== -1 && ((index < length - 2) || (index === length - 2 && stripped[length - 1] !== "="))) { + return Result.fail( + new EncodingError({ + kind: "Decode", + module: "Base64", + input: stripped, + message: `Found a '=' character, but it is not at the end` + }) + ) + } + + try { + const missingOctets = stripped.endsWith("==") ? 2 : stripped.endsWith("=") ? 1 : 0 + const result = new Uint8Array(3 * (length / 4) - missingOctets) + for (let i = 0, j = 0; i < length; i += 4, j += 3) { + const buffer = getBase64Code(stripped.charCodeAt(i)) << 18 | + getBase64Code(stripped.charCodeAt(i + 1)) << 12 | + getBase64Code(stripped.charCodeAt(i + 2)) << 6 | + getBase64Code(stripped.charCodeAt(i + 3)) + + result[j] = buffer >> 16 + result[j + 1] = (buffer >> 8) & 0xff + result[j + 2] = buffer & 0xff + } + + return Result.succeed(result) + } catch (e) { + return Result.fail( + new EncodingError({ + kind: "Decode", + module: "Base64", + input: stripped, + message: e instanceof Error ? e.message : "Invalid input" + }) + ) + } +} + +/** + * Decodes a base64 (RFC4648) string into a UTF-8 string safely. + * + * **When to use** + * + * Use to decode a standard padded Base64 string into UTF-8 text without + * throwing on invalid input. + * + * **Details** + * + * Returns `Result.succeed` with the decoded text when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input is not valid base64. + * + * **Example** (Decoding Base64 strings) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeBase64String("aGVsbG8=") + * if (Result.isSuccess(result)) { + * console.log(result.success) // "hello" + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64String = (str: string) => Result.map(decodeBase64(str), (_) => decoder.decode(_)) + +// ------------------------------------------------------------------------------------- +// Base64Url +// ------------------------------------------------------------------------------------- + +/** + * Encodes the given value into a base64 (URL) `string`. + * + * **When to use** + * + * Use to encode text or bytes as an unpadded Base64Url string for contexts that + * require the URL-safe alphabet. + * + * **Details** + * + * String inputs are encoded as UTF-8 bytes before Base64Url encoding. + * `Uint8Array` inputs are encoded directly. The output removes `=` padding and + * replaces `+` with `-` and `/` with `_`. + * + * **Example** (Encoding URL-safe Base64) + * + * ```ts + * import { Encoding } from "effect" + * + * // URL-safe base64 encoding (uses - and _ instead of + and /) + * console.log(Encoding.encodeBase64Url("hello?")) // "aGVsbG8_" + * + * const bytes = new Uint8Array([72, 101, 108, 108, 111, 63]) + * console.log(Encoding.encodeBase64Url(bytes)) // "SGVsbG8_" + * ``` + * + * @see {@link decodeBase64Url} for decoding URL-safe Base64 to bytes + * @see {@link decodeBase64UrlString} for decoding URL-safe Base64 to UTF-8 text + * @see {@link encodeBase64} for standard padded Base64 output + * + * @category encoding + * @since 2.0.0 + */ +export const encodeBase64Url: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? base64UrlEncodeUint8Array(encoder.encode(input)) : base64UrlEncodeUint8Array(input) + +/** + * Decodes a URL-safe base64 string into bytes safely. + * + * **When to use** + * + * Use to decode padded or unpadded Base64Url text into bytes without throwing + * on invalid input. + * + * **Details** + * + * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input is not valid URL-safe + * base64. Both padded and unpadded URL-safe base64 forms are accepted when + * otherwise valid. + * + * **Example** (Decoding URL-safe Base64 bytes) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeBase64Url("SGVsbG8_") + * if (Result.isSuccess(result)) { + * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111, 63] + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64Url = (str: string): Result.Result => { + const stripped = stripCrlf(str) + const length = stripped.length + if (length % 4 === 1) { + return Result.fail( + new EncodingError({ + module: "Base64Url", + kind: "Decode", + input: stripped, + message: `Length should be a multiple of 4, but is ${length}` + }) + ) + } + + if (!/^[-_A-Z0-9]*?={0,2}$/i.test(stripped)) { + return Result.fail( + new EncodingError({ + module: "Base64Url", + kind: "Decode", + input: stripped, + message: "Invalid input" + }) + ) + } + + // Some variants allow or require omitting the padding '=' signs + let sanitized = length % 4 === 2 ? `${stripped}==` : length % 4 === 3 ? `${stripped}=` : stripped + sanitized = sanitized.replace(/-/g, "+").replace(/_/g, "/") + + return decodeBase64(sanitized) +} + +/** + * Decodes a URL-safe base64 string into a UTF-8 string safely. + * + * **When to use** + * + * Use to decode padded or unpadded Base64Url text into UTF-8 text without + * throwing on invalid input. + * + * **Details** + * + * Returns `Result.succeed` with the decoded text when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input is not valid URL-safe + * base64. + * + * **Example** (Decoding URL-safe Base64 strings) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeBase64UrlString("aGVsbG8_") + * if (Result.isSuccess(result)) { + * console.log(result.success) // "hello?" + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeBase64UrlString = (str: string) => Result.map(decodeBase64Url(str), (_) => decoder.decode(_)) + +// ------------------------------------------------------------------------------------- +// Hex +// ------------------------------------------------------------------------------------- + +/** + * Encodes the given value into a hex `string`. + * + * **When to use** + * + * Use to encode text or bytes as lowercase hexadecimal text. + * + * **Example** (Encoding hex strings and bytes) + * + * ```ts + * import { Encoding } from "effect" + * + * // Encode a string to hex + * console.log(Encoding.encodeHex("hello")) // "68656c6c6f" + * + * // Encode binary data to hex + * const bytes = new Uint8Array([72, 101, 108, 108, 111]) + * console.log(Encoding.encodeHex(bytes)) // "48656c6c6f" + * ``` + * + * @category encoding + * @since 2.0.0 + */ +export const encodeHex: (input: Uint8Array | string) => string = (input) => + typeof input === "string" ? hexEncodeUint8Array(encoder.encode(input)) : hexEncodeUint8Array(input) + +/** + * Decodes a hexadecimal string into bytes safely. + * + * **When to use** + * + * Use to decode hexadecimal text into bytes without throwing on invalid input. + * + * **Details** + * + * Returns `Result.succeed` with a `Uint8Array` when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input has an odd length or + * contains invalid hex characters. + * + * **Example** (Decoding hex bytes) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeHex("48656c6c6f") + * if (Result.isSuccess(result)) { + * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111] + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeHex = (str: string): Result.Result => { + const bytes = new TextEncoder().encode(str) + if (bytes.length % 2 !== 0) { + return Result.fail( + new EncodingError({ + module: "Hex", + kind: "Decode", + input: str, + message: `Length must be a multiple of 2, but is ${bytes.length}` + }) + ) + } + + try { + const length = bytes.length / 2 + const result = new Uint8Array(length) + for (let i = 0; i < length; i++) { + const a = fromHexChar(bytes[i * 2]) + const b = fromHexChar(bytes[i * 2 + 1]) + result[i] = (a << 4) | b + } + + return Result.succeed(result) + } catch (e) { + return Result.fail( + new EncodingError({ + module: "Hex", + kind: "Decode", + input: str, + message: e instanceof Error ? e.message : "Invalid input" + }) + ) + } +} + +/** + * Decodes a hexadecimal string into a UTF-8 string safely. + * + * **When to use** + * + * Use to decode hexadecimal text into UTF-8 text without throwing on invalid + * input. + * + * **Details** + * + * Returns `Result.succeed` with the decoded text when decoding succeeds, or + * `Result.fail` with an `EncodingError` when the input is not valid hex. + * + * **Example** (Decoding hex strings) + * + * ```ts + * import { Encoding, Result } from "effect" + * + * const result = Encoding.decodeHexString("68656c6c6f") + * if (Result.isSuccess(result)) { + * console.log(result.success) // "hello" + * } + * ``` + * + * @category decoding + * @since 2.0.0 + */ +export const decodeHexString = (str: string) => Result.map(decodeHex(str), (_) => decoder.decode(_)) + +// ------------------------------------------------------------------------------------- +// internals +// ------------------------------------------------------------------------------------- + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +const stripCrlf = (str: string) => str.replace(/[\n\r]/g, "") + +// Base64 internals + +const base64EncodeUint8Array = (bytes: Uint8Array) => { + const length = bytes.length + + let result = "" + let i: number + + for (i = 2; i < length; i += 3) { + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)] + result += base64abc[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)] + result += base64abc[bytes[i] & 0x3f] + } + + if (i === length + 1) { + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[(bytes[i - 2] & 0x03) << 4] + result += "==" + } + + if (i === length) { + result += base64abc[bytes[i - 2] >> 2] + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)] + result += base64abc[(bytes[i - 1] & 0x0f) << 2] + result += "=" + } + + return result +} + +function getBase64Code(charCode: number) { + if (charCode >= base64codes.length) { + throw new TypeError(`Invalid character ${String.fromCharCode(charCode)}`) + } + + const code = base64codes[charCode] + if (code === 255) { + throw new TypeError(`Invalid character ${String.fromCharCode(charCode)}`) + } + + return code +} + +const base64abc = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "/" +] + +const base64codes = [ + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 62, + 255, + 255, + 255, + 63, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 255, + 255, + 255, + 0, + 255, + 255, + 255, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 255, + 255, + 255, + 255, + 255, + 255, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51 +] + +// Base64Url internals + +const base64UrlEncodeUint8Array = (data: Uint8Array) => + base64EncodeUint8Array(data).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") + +// Hex internals + +const hexEncodeUint8Array = (bytes: Uint8Array) => { + let result = "" + for (let i = 0; i < bytes.length; ++i) { + result += bytesToHex[bytes[i]] + } + + return result +} + +const fromHexChar = (byte: number) => { + if (48 <= byte && byte <= 57) { + return byte - 48 + } + + if (97 <= byte && byte <= 102) { + return byte - 97 + 10 + } + + if (65 <= byte && byte <= 70) { + return byte - 65 + 10 + } + + throw new TypeError("Invalid input") +} + +const bytesToHex = [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0a", + "0b", + "0c", + "0d", + "0e", + "0f", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1a", + "1b", + "1c", + "1d", + "1e", + "1f", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2a", + "2b", + "2c", + "2d", + "2e", + "2f", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3a", + "3b", + "3c", + "3d", + "3e", + "3f", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4a", + "4b", + "4c", + "4d", + "4e", + "4f", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5a", + "5b", + "5c", + "5d", + "5e", + "5f", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7a", + "7b", + "7c", + "7d", + "7e", + "7f", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8a", + "8b", + "8c", + "8d", + "8e", + "8f", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9a", + "9b", + "9c", + "9d", + "9e", + "9f", + "a0", + "a1", + "a2", + "a3", + "a4", + "a5", + "a6", + "a7", + "a8", + "a9", + "aa", + "ab", + "ac", + "ad", + "ae", + "af", + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "b8", + "b9", + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "c0", + "c1", + "c2", + "c3", + "c4", + "c5", + "c6", + "c7", + "c8", + "c9", + "ca", + "cb", + "cc", + "cd", + "ce", + "cf", + "d0", + "d1", + "d2", + "d3", + "d4", + "d5", + "d6", + "d7", + "d8", + "d9", + "da", + "db", + "dc", + "dd", + "de", + "df", + "e0", + "e1", + "e2", + "e3", + "e4", + "e5", + "e6", + "e7", + "e8", + "e9", + "ea", + "eb", + "ec", + "ed", + "ee", + "ef", + "f0", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "fa", + "fb", + "fc", + "fd", + "fe", + "ff" +] diff --git a/repos/effect/packages/effect/src/Equal.ts b/repos/effect/packages/effect/src/Equal.ts new file mode 100644 index 0000000..d02b474 --- /dev/null +++ b/repos/effect/packages/effect/src/Equal.ts @@ -0,0 +1,550 @@ +/** + * Compares values with Effect's structural equality rules. + * + * `equals` compares primitives, arrays, plain objects, maps, sets, dates, + * regular expressions, and values that implement the `Equal` interface. This + * module also defines the equality symbol, guards, adapters, map and set + * comparison builders, and helpers for marking objects that should compare only + * by reference. + * + * @since 2.0.0 + */ +import type { Equivalence } from "./Equivalence.ts" +import * as Hash from "./Hash.ts" +import { byReferenceInstances, getAllObjectKeys } from "./internal/equal.ts" +import { hasProperty } from "./Predicate.ts" + +/** + * Defines the unique string identifier for the `Equal` interface. + * + * **When to use** + * + * Use when you implement custom equality and need the computed property key for + * the equality method. + * + * **Details** + * + * This is a pure constant with no allocation or side effects. + * + * **Example** (Implementing Equal on a class) + * + * ```ts + * import { Equal, Hash } from "effect" + * + * class UserId implements Equal.Equal { + * constructor(readonly id: string) {} + * + * [Equal.symbol](that: Equal.Equal): boolean { + * return that instanceof UserId && this.id === that.id + * } + * + * [Hash.symbol](): number { + * return Hash.string(this.id) + * } + * } + * ``` + * + * @see {@link Equal} — the interface that uses this symbol + * @see {@link isEqual} — type guard for `Equal` implementors + * @category symbols + * @since 2.0.0 + */ +export const symbol = "~effect/interfaces/Equal" + +/** + * The interface for types that define their own equality logic. + * + * **When to use** + * + * Use when you need value-based equality for a class (e.g. domain IDs, + * coordinates, money values). + * - When your type will be stored in `HashMap` or `HashSet`. + * - When the default structural comparison is too broad or too narrow for + * your type. + * + * **Details** + * + * Any object that implements both `[Equal.symbol]` (equality) and + * `[Hash.symbol]` (hashing) is recognized by {@link equals} and by hash-based + * collections such as `HashMap` and `HashSet`. + * + * - Extends `Hash.Hash`, so implementors **must** also provide `[Hash.symbol]`. + * - The hash contract: if `a[Equal.symbol](b)` returns `true`, then + * `Hash.hash(a)` must equal `Hash.hash(b)`. + * - {@link equals} delegates to this method when both operands implement it. + * If only one operand implements `Equal`, they are considered unequal. + * + * **Example** (Comparing coordinates by value) + * + * ```ts + * import { Equal, Hash } from "effect" + * + * class Coordinate implements Equal.Equal { + * constructor(readonly x: number, readonly y: number) {} + * + * [Equal.symbol](that: Equal.Equal): boolean { + * return that instanceof Coordinate && + * this.x === that.x && + * this.y === that.y + * } + * + * [Hash.symbol](): number { + * return Hash.string(`${this.x},${this.y}`) + * } + * } + * + * console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(1, 2))) // true + * console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(3, 4))) // false + * ``` + * + * @see {@link symbol} — the property key used by the equality method + * @see {@link equals} — the main comparison function + * @see {@link isEqual} — type guard for `Equal` implementors + * @category models + * @since 2.0.0 + */ +export interface Equal extends Hash.Hash { + [symbol](that: Equal): boolean +} + +/** + * Checks whether two values are deeply structurally equal. + * + * **When to use** + * + * Use when you need Effect's default structural equality check. + * + * **Details** + * + * Returns a `boolean` and never throws. Primitives are compared by value, and + * `NaN` equals `NaN`. Objects implementing `Equal` delegate to their + * `[Equal.symbol]` method; if only one operand implements `Equal`, the result + * is `false`. + * + * Dates compare by ISO string, RegExps compare by string representation, + * arrays compare element-by-element, Maps and Sets compare entries + * order-independently, and plain objects compare enumerable keys recursively. + * Functions without an `Equal` implementation compare by reference. Circular + * references are handled when both structures are circular at the same depth. + * + * Hash values are checked first as a fast-path rejection. The function also + * supports dual data-last usage: call it with one argument to get a curried + * predicate. + * + * **Gotchas** + * + * - Results are cached per object pair in a WeakMap. **Objects must not be + * mutated after their first comparison.** + * - Map and Set comparisons are O(n²) in size. + * + * **Example** (Comparing values) + * + * ```ts + * import { Equal } from "effect" + * + * // Primitives + * console.log(Equal.equals(1, 1)) // true + * console.log(Equal.equals(NaN, NaN)) // true + * console.log(Equal.equals("a", "b")) // false + * + * // Objects and arrays + * console.log(Equal.equals({ a: 1, b: 2 }, { a: 1, b: 2 })) // true + * console.log(Equal.equals([1, [2, 3]], [1, [2, 3]])) // true + * + * // Dates + * console.log(Equal.equals(new Date("2024-01-01"), new Date("2024-01-01"))) // true + * + * // Maps (order-independent) + * const m1 = new Map([["a", 1], ["b", 2]]) + * const m2 = new Map([["b", 2], ["a", 1]]) + * console.log(Equal.equals(m1, m2)) // true + * + * // Curried form + * const is5 = Equal.equals(5) + * console.log(is5(5)) // true + * console.log(is5(3)) // false + * ``` + * + * @see {@link Equal} — the interface for custom equality + * @see {@link isEqual} — check whether a value implements `Equal` + * @see {@link asEquivalence} — wrap `equals` as an `Equivalence` + * @category equality + * @since 2.0.0 + */ +export function equals(that: B): (self: A) => boolean +export function equals(self: A, that: B): boolean +export function equals(): any { + if (arguments.length === 1) { + return (self: unknown) => compareBoth(self, arguments[0]) + } + return compareBoth(arguments[0], arguments[1]) +} + +function compareBoth(self: unknown, that: unknown): boolean { + if (self === that) return true + if (self == null || that == null) return false + const selfType = typeof self + if (selfType !== typeof that) { + return false + } + // Special case for NaN: NaN should be considered equal to NaN + if (selfType === "number" && self !== self && that !== that) { + return true + } + if (selfType !== "object" && selfType !== "function") { + return false + } + + if (byReferenceInstances.has(self) || byReferenceInstances.has(that)) { + return false + } + + // For objects and functions, use cached comparison + return withCache(self, that, compareObjects) +} + +/** Helper to run comparison with proper visited tracking */ +function withVisitedTracking( + self: object, + that: object, + fn: () => boolean +): boolean { + const hasLeft = visitedLeft.has(self) + const hasRight = visitedRight.has(that) + // Check for circular references before adding + if (hasLeft && hasRight) { + return true // Both are circular at the same level + } + if (hasLeft || hasRight) { + return false // Only one is circular + } + visitedLeft.add(self) + visitedRight.add(that) + const result = fn() + visitedLeft.delete(self) + visitedRight.delete(that) + return result +} + +const visitedLeft = new WeakSet() +const visitedRight = new WeakSet() + +/** Helper to perform cached object comparison */ +function compareObjects(self: object, that: object): boolean { + if (Hash.hash(self) !== Hash.hash(that)) { + return false + } else if (self instanceof Date) { + if (!(that instanceof Date)) return false + return self.toISOString() === that.toISOString() + } else if (self instanceof RegExp) { + if (!(that instanceof RegExp)) return false + return self.toString() === that.toString() + } + const selfIsEqual = isEqual(self) + const thatIsEqual = isEqual(that) + if (selfIsEqual !== thatIsEqual) return false + const bothEquals = selfIsEqual && thatIsEqual + if (typeof self === "function" && !bothEquals) { + return false + } + return withVisitedTracking(self, that, () => { + if (bothEquals) { + return (self as any)[symbol](that) + } else if (Array.isArray(self)) { + if (!Array.isArray(that) || self.length !== that.length) { + return false + } + return compareArrays(self, that) + } else if (ArrayBuffer.isView(self)) { + if (!ArrayBuffer.isView(that) || self.byteLength !== that.byteLength) { + return false + } + return compareTypedArrays(self as Uint8Array, that as Uint8Array) + } else if (self instanceof Map) { + if (!(that instanceof Map) || self.size !== that.size) { + return false + } + return compareMaps(self, that) + } else if (self instanceof Set) { + if (!(that instanceof Set) || self.size !== that.size) { + return false + } + return compareSets(self, that) + } + return compareRecords(self as any, that as any) + }) +} + +function withCache(self: object, that: object, f: (a: any, b: any) => boolean): boolean { + // Check cache first + let selfMap = equalityCache.get(self) + if (!selfMap) { + selfMap = new WeakMap() + equalityCache.set(self, selfMap) + } else if (selfMap.has(that)) { + return selfMap.get(that)! + } + + // Perform the comparison + const result = f(self, that) + + // Cache the result bidirectionally + selfMap.set(that, result) + + let thatMap = equalityCache.get(that) + if (!thatMap) { + thatMap = new WeakMap() + equalityCache.set(that, thatMap) + } + thatMap.set(self, result) + + return result +} + +const equalityCache = new WeakMap>() + +function compareArrays(self: Array, that: Array): boolean { + for (let i = 0; i < self.length; i++) { + if (!compareBoth(self[i], that[i])) { + return false + } + } + + return true +} + +function compareTypedArrays(self: Uint8Array, that: Uint8Array): boolean { + if (self.length !== that.length) { + return false + } + for (let i = 0; i < self.length; i++) { + if (self[i] !== that[i]) { + return false + } + } + return true +} + +function compareRecords( + self: Record, + that: Record +): boolean { + const selfKeys = getAllObjectKeys(self) + const thatKeys = getAllObjectKeys(that) + + if (selfKeys.size !== thatKeys.size) { + return false + } + + for (const key of selfKeys) { + if (!(thatKeys.has(key)) || !compareBoth(self[key], that[key])) { + return false + } + } + + return true +} + +/** @internal */ +export function makeCompareMap(keyEquivalence: Equivalence, valueEquivalence: Equivalence) { + return function compareMaps(self: Iterable<[K, V]>, that: Iterable<[K, V]>): boolean { + for (const [selfKey, selfValue] of self) { + let found = false + for (const [thatKey, thatValue] of that) { + if (keyEquivalence(selfKey, thatKey) && valueEquivalence(selfValue, thatValue)) { + found = true + break + } + } + if (!found) { + return false + } + } + + return true + } +} + +const compareMaps = makeCompareMap(compareBoth, compareBoth) + +/** @internal */ +export function makeCompareSet(equivalence: Equivalence) { + return function compareSets(self: Iterable, that: Iterable): boolean { + for (const selfValue of self) { + let found = false + for (const thatValue of that) { + if (equivalence(selfValue, thatValue)) { + found = true + break + } + } + if (!found) { + return false + } + } + + return true + } +} + +const compareSets = makeCompareSet(compareBoth) + +/** + * Checks whether a value implements the {@link Equal} interface. + * + * **When to use** + * + * Use when you need generic utility code to distinguish `Equal` implementors + * from plain values before calling `[Equal.symbol]` directly. + * + * **Details** + * + * - Pure function, no side effects. + * - Returns `true` if and only if `u` has a property keyed by + * {@link symbol}. + * - Acts as a TypeScript type guard, narrowing the input to {@link Equal}. + * + * **Example** (Checking Equal values) + * + * ```ts + * import { Equal, Hash } from "effect" + * + * class Token implements Equal.Equal { + * constructor(readonly value: string) {} + * [Equal.symbol](that: Equal.Equal): boolean { + * return that instanceof Token && this.value === that.value + * } + * [Hash.symbol](): number { + * return Hash.string(this.value) + * } + * } + * + * console.log(Equal.isEqual(new Token("abc"))) // true + * console.log(Equal.isEqual({ x: 1 })) // false + * console.log(Equal.isEqual(42)) // false + * ``` + * + * @see {@link Equal} — the interface being checked + * @see {@link symbol} — the property key that signals `Equal` support + * @category guards + * @since 2.0.0 + */ +export const isEqual = (u: unknown): u is Equal => hasProperty(u, symbol) + +/** + * Wraps {@link equals} as an `Equivalence`. + * + * **When to use** + * + * Use when you want to pass `Equal.equals` to APIs that require an + * `Equivalence`. + * + * **Details** + * + * - Returns a function `(a: A, b: A) => boolean` that delegates to + * {@link equals}. + * - Pure; allocates a thin wrapper on each call. + * + * **Example** (Deduplicating with Equal semantics) + * + * ```ts + * import { Array, Equal } from "effect" + * + * const eq = Equal.asEquivalence() + * const result = Array.dedupeWith([1, 2, 2, 3, 1], eq) + * console.log(result) // [1, 2, 3] + * ``` + * + * @see {@link equals} — the underlying comparison function + * @category instances + * @since 4.0.0 + */ +export const asEquivalence: () => Equivalence = () => equals + +/** + * Creates a proxy that uses reference equality instead of structural equality. + * + * **When to use** + * + * Use when you need to compare a plain object or array by identity without + * mutating the original value. + * + * **Details** + * + * - Returns a `Proxy` wrapping `obj`. The proxy reads through to the + * original, so property access is unchanged. + * - The proxy is registered in an internal WeakSet; {@link equals} returns + * `false` for any pair where at least one operand is in that set (unless + * they are the same reference). + * - Each call creates a **new** proxy, so `byReference(x) !== byReference(x)`. + * - Does **not** mutate the original object (unlike {@link byReferenceUnsafe}). + * + * **Example** (Opting out of structural equality) + * + * ```ts + * import { Equal } from "effect" + * + * const a = { x: 1 } + * const b = { x: 1 } + * + * console.log(Equal.equals(a, b)) // true (structural) + * + * const aRef = Equal.byReference(a) + * console.log(Equal.equals(aRef, b)) // false (reference) + * console.log(Equal.equals(aRef, aRef)) // true (same reference) + * console.log(aRef.x) // 1 (proxy reads through) + * ``` + * + * @see {@link byReferenceUnsafe} — same effect without a proxy (mutates the + * original) + * @see {@link equals} — the comparison function affected by this opt-out + * @category equality + * @since 4.0.0 + */ +export const byReference = (obj: T): T => byReferenceUnsafe(new Proxy(obj, {})) + +/** + * Marks an object permanently to use reference equality, without creating a proxy. + * + * **When to use** + * + * Use when you need reference equality without proxy allocation and accept + * permanently marking the original object for reference-only equality. + * + * **Details** + * + * - Adds `obj` to an internal WeakSet. From that point on, {@link equals} + * treats it as reference-only. + * - Returns the **same** object (not a copy or proxy), so + * `byReferenceUnsafe(x) === x`. + * - Does **not** affect the object's prototype, properties, or behavior + * beyond equality checks. + * + * **Gotchas** + * + * The marking is irreversible for the lifetime of the object. + * + * **Example** (Marking an object for reference equality) + * + * ```ts + * import { Equal } from "effect" + * + * const obj1 = { a: 1, b: 2 } + * const obj2 = { a: 1, b: 2 } + * + * Equal.byReferenceUnsafe(obj1) + * + * console.log(Equal.equals(obj1, obj2)) // false (reference) + * console.log(Equal.equals(obj1, obj1)) // true (same reference) + * console.log(obj1 === Equal.byReferenceUnsafe(obj1)) // true (same object) + * ``` + * + * @see {@link byReference} — safer alternative that creates a proxy + * @see {@link equals} — the comparison function affected by this opt-out + * @category unsafe + * @since 4.0.0 + */ +export const byReferenceUnsafe = (obj: T): T => { + byReferenceInstances.add(obj) + return obj +} diff --git a/repos/effect/packages/effect/src/Equivalence.ts b/repos/effect/packages/effect/src/Equivalence.ts new file mode 100644 index 0000000..9a5a20f --- /dev/null +++ b/repos/effect/packages/effect/src/Equivalence.ts @@ -0,0 +1,870 @@ +/** + * Defines reusable equality functions for values of the same type. + * + * An `Equivalence` returns `true` when two `A` values should be treated as + * the same for a particular purpose. This module includes strict equality + * instances for primitive types, constructors for custom comparisons, and + * helpers for tuples, arrays, structs, records, dates, and values compared + * through a derived field. + * + * @since 2.0.0 + */ +import { dual } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Represents an equivalence relation over type `A`. + * + * **When to use** + * + * Use as a type annotation when you accept or return an equivalence function. + * + * **Details** + * + * - Returns `boolean`: `true` if values are equivalent, `false` otherwise + * - Must satisfy reflexive, symmetric, and transitive properties + * + * **Example** (Defining simple number equivalence) + * + * ```ts + * import type { Equivalence } from "effect" + * + * const numberEq: Equivalence.Equivalence = (a, b) => a === b + * + * console.log(numberEq(1, 1)) // true + * console.log(numberEq(1, 2)) // false + * ``` + * + * **Example** (Defining custom object equivalence) + * + * ```ts + * import type { Equivalence } from "effect" + * + * interface Point { + * x: number + * y: number + * } + * + * const pointEq: Equivalence.Equivalence = (a, b) => + * a.x === b.x && a.y === b.y + * + * console.log(pointEq({ x: 1, y: 2 }, { x: 1, y: 2 })) // true + * ``` + * + * @see {@link make} + * @see {@link strictEqual} + * @category type class + * @since 2.0.0 + */ +export type Equivalence = (self: A, that: A) => boolean + +/** + * Type lambda for `Equivalence`, used for higher-kinded type operations. + * + * **When to use** + * + * Use when you need to abstract over `Equivalence` in higher-kinded type code. + * + * **Details** + * + * - Enables `Equivalence` to work with the Effect type system's HKT infrastructure + * - Used internally for type-level computations and generic abstractions + * + * **Example** (Type-level usage) + * + * ```ts + * import type { Equivalence, HKT } from "effect" + * + * // Used internally for type-level computations + * type NumberEquivalence = HKT.Kind< + * Equivalence.EquivalenceTypeLambda, + * never, + * never, + * never, + * number + * > + * // Equivalent to: Equivalence.Equivalence + * ``` + * + * @see {@link Equivalence} + * @see {@link TypeLambda} + * @category type lambdas + * @since 2.0.0 + */ +export interface EquivalenceTypeLambda extends TypeLambda { + readonly type: Equivalence +} + +/** + * Creates a custom equivalence relation with an optimized reference equality check. + * + * **When to use** + * + * Use when you need an equality rule that the built-in instances and input + * mapping helpers cannot express, and you can provide a law-abiding comparison. + * + * **Details** + * + * The returned equivalence first checks reference equality (`===`) for + * performance. If the values are not the same reference, it falls back to the + * provided equivalence function, which must satisfy reflexive, symmetric, and + * transitive properties. + * + * **Example** (Case-insensitive string equivalence) + * + * ```ts + * import { Equivalence } from "effect" + * + * const caseInsensitive = Equivalence.make((a, b) => + * a.toLowerCase() === b.toLowerCase() + * ) + * + * console.log(caseInsensitive("Hello", "HELLO")) // true + * console.log(caseInsensitive("foo", "bar")) // false + * + * // Same reference optimization + * const str = "test" + * console.log(caseInsensitive(str, str)) // true (fast path) + * ``` + * + * **Example** (Comparing numbers with tolerance) + * + * ```ts + * import { Equivalence } from "effect" + * + * const tolerance = Equivalence.make((a, b) => Math.abs(a - b) < 0.0001) + * + * console.log(tolerance(1.0, 1.001)) // false + * console.log(tolerance(1.0, 1.00001)) // true + * ``` + * + * @see {@link strictEqual} + * @see {@link mapInput} + * @category constructors + * @since 2.0.0 + */ +export const make = (isEquivalent: (self: A, that: A) => boolean): Equivalence => (self: A, that: A): boolean => + self === that || isEquivalent(self, that) + +const isStrictEquivalent = (x: unknown, y: unknown) => x === y + +/** + * Creates an equivalence relation that uses strict equality (`===`) to compare values. + * + * **When to use** + * + * Use when you need strict equality (`===`) as the comparison. + * + * **Details** + * + * Uses JavaScript's strict equality operator (`===`). Primitives compare by + * value. Objects compare by reference, so only the same object instance is + * equivalent. Use this as a building block for more complex equivalences via + * `mapInput` or `combine`. + * + * **Gotchas** + * + * `NaN !== NaN`, so `NaN` values are never considered equivalent. + * + * **Example** (Comparing primitive types) + * + * ```ts + * import { Equivalence } from "effect" + * + * const strictEq = Equivalence.strictEqual() + * + * console.log(strictEq(1, 1)) // true + * console.log(strictEq(1, 2)) // false + * console.log(strictEq(NaN, NaN)) // false (NaN !== NaN) + * ``` + * + * **Example** (Comparing objects by reference) + * + * ```ts + * import { Equivalence } from "effect" + * + * const obj = { value: 42 } + * const strictObjEq = Equivalence.strictEqual() + * + * console.log(strictObjEq(obj, obj)) // true + * console.log(strictObjEq(obj, { value: 42 })) // false (different references) + * ``` + * + * @see {@link make} + * @see `Equal` for structural equality + * @category constructors + * @since 4.0.0 + */ +export const strictEqual: () => Equivalence = () => isStrictEquivalent + +/** + * Equivalence instance for strings using strict equality (`===`). + * + * **When to use** + * + * Use when you need to supply case-sensitive string equality. + * + * **Example** (Comparing strings) + * + * ```ts + * import { Equivalence } from "effect" + * + * console.log(Equivalence.String("hello", "hello")) // true + * console.log(Equivalence.String("hello", "world")) // false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const String: Equivalence = isStrictEquivalent + +/** + * Equivalence instance for numbers. + * + * **When to use** + * + * Use when you need numeric equality that treats NaN as equal to itself. + * + * **Example** (Comparing numbers) + * + * ```ts + * import { Equivalence } from "effect" + * + * console.log(Equivalence.Number(1, 1)) // true + * console.log(Equivalence.Number(1, 2)) // false + * console.log(Equivalence.Number(NaN, NaN)) // true + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const Number: Equivalence = make((self, that) => + globalThis.Number.isNaN(self) && globalThis.Number.isNaN(that) +) + +/** + * Equivalence instance for booleans using strict equality (`===`). + * + * **When to use** + * + * Use when you need to supply boolean equality. + * + * **Example** (Comparing booleans) + * + * ```ts + * import { Equivalence } from "effect" + * + * console.log(Equivalence.Boolean(true, true)) // true + * console.log(Equivalence.Boolean(true, false)) // false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const Boolean: Equivalence = isStrictEquivalent + +/** + * Equivalence instance for bigints using strict equality (`===`). + * + * **When to use** + * + * Use when you need to supply bigint equality. + * + * **Example** (Comparing bigints) + * + * ```ts + * import { Equivalence } from "effect" + * + * console.log(Equivalence.BigInt(1n, 1n)) // true + * console.log(Equivalence.BigInt(1n, 2n)) // false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const BigInt: Equivalence = isStrictEquivalent + +/** + * Combines two equivalence relations using logical AND. + * + * **When to use** + * + * Use when you need to combine exactly two equivalences with AND semantics. + * + * **Details** + * + * Returns `true` only if both equivalences return `true`. The comparison + * short-circuits when the first equivalence returns `false`. The result is also + * an equivalence that satisfies reflexive, symmetric, and transitive + * properties. + * + * **Example** (Combining name and age equivalences) + * + * ```ts + * import { Equivalence } from "effect" + * + * interface Person { + * name: string + * age: number + * } + * + * const nameEquivalence = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (p: Person) => p.name + * ) + * + * const ageEquivalence = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (p: Person) => p.age + * ) + * + * const personEquivalence = Equivalence.combine(nameEquivalence, ageEquivalence) + * + * const person1 = { name: "Alice", age: 30 } + * const person2 = { name: "Alice", age: 30 } + * const person3 = { name: "Alice", age: 31 } + * + * console.log(personEquivalence(person1, person2)) // true + * console.log(personEquivalence(person1, person3)) // false (different age) + * ``` + * + * @see {@link combineAll} + * @see {@link mapInput} + * @category combining + * @since 2.0.0 + */ +export const combine: { + (that: Equivalence): (self: Equivalence) => Equivalence + (self: Equivalence, that: Equivalence): Equivalence +} = dual(2, (self: Equivalence, that: Equivalence): Equivalence => make((x, y) => self(x, y) && that(x, y))) + +/** + * Combines multiple equivalence relations into a single equivalence using logical AND. + * + * **When to use** + * + * Use when you need to combine many `Equivalence` instances from an iterable. + * + * **Details** + * + * Returns `true` only if all equivalences in the collection return `true`. The + * comparison stops at the first equivalence that returns `false`. Empty + * collections return an equivalence that always returns `true`. The result is + * also an equivalence that satisfies reflexive, symmetric, and transitive + * properties. + * + * **Example** (Combining multiple field equivalences) + * + * ```ts + * import { Equivalence } from "effect" + * + * interface Point3D { + * x: number + * y: number + * z: number + * } + * + * const xEq = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (p: Point3D) => p.x + * ) + * const yEq = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (p: Point3D) => p.y + * ) + * const zEq = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (p: Point3D) => p.z + * ) + * + * const point3DEq = Equivalence.combineAll([xEq, yEq, zEq]) + * + * const point1 = { x: 1, y: 2, z: 3 } + * const point2 = { x: 1, y: 2, z: 3 } + * const point3 = { x: 1, y: 2, z: 4 } + * + * console.log(point3DEq(point1, point2)) // true + * console.log(point3DEq(point1, point3)) // false (different z) + * ``` + * + * **Example** (Handling empty collections) + * + * ```ts + * import { Equivalence } from "effect" + * + * // Empty collection always returns true + * const alwaysEq = Equivalence.combineAll([]) + * console.log(alwaysEq("anything", "else")) // true + * ``` + * + * @see {@link combine} + * @see {@link mapInput} + * @category combining + * @since 2.0.0 + */ +export const combineAll = (collection: Iterable>): Equivalence => + make((x, y) => { + for (const equivalence of collection) { + if (!equivalence(x, y)) { + return false + } + } + return true + }) + +/** + * Transforms an equivalence relation by mapping the input values before comparison. + * + * **When to use** + * + * Use when you need an equivalence for one type by comparing a derived value. + * + * **Details** + * + * - Applies the transformation function to both values before comparing + * - The transformation function should be pure and have no side effects + * - The resulting equivalence compares the transformed values using the provided equivalence + * - The result is also an equivalence that satisfies reflexive, symmetric, and transitive properties + * - Useful for comparing by one property or normalizing values before + * comparison, such as case-insensitive strings + * + * **Example** (Deriving equivalence from an object property) + * + * ```ts + * import { Equivalence } from "effect" + * + * interface User { + * id: number + * name: string + * email: string + * } + * + * // Create equivalence based on user ID only + * const userByIdEq = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (user: User) => user.id + * ) + * + * const user1 = { id: 1, name: "Alice", email: "alice@example.com" } + * const user2 = { id: 1, name: "Alice Smith", email: "alice.smith@example.com" } + * const user3 = { id: 2, name: "Bob", email: "bob@example.com" } + * + * console.log(userByIdEq(user1, user2)) // true (same ID) + * console.log(userByIdEq(user1, user3)) // false (different ID) + * ``` + * + * **Example** (Case-insensitive string equivalence) + * + * ```ts + * import { Equivalence } from "effect" + * + * const caseInsensitiveEq = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (s: string) => s.toLowerCase() + * ) + * + * console.log(caseInsensitiveEq("Hello", "HELLO")) // true + * console.log(caseInsensitiveEq("Hello", "World")) // false + * ``` + * + * @see {@link combine} + * @see {@link Struct} + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Equivalence) => Equivalence + (self: Equivalence, f: (b: B) => A): Equivalence +} = dual( + 2, + (self: Equivalence, f: (b: B) => A): Equivalence => make((x, y) => self(f(x), f(y))) +) + +/** + * Creates an equivalence for tuples with heterogeneous element types. + * + * **When to use** + * + * Use when you need an `Equivalence` for fixed-length tuples with per-position + * equivalences. + * + * **Details** + * + * Tuples must have the same length; different lengths are never equivalent. + * Each equivalence is applied to the corresponding element position. The result + * returns `true` only if all elements are equivalent according to their + * respective equivalences, and it also satisfies reflexive, symmetric, and + * transitive properties. + * + * **Example** (Comparing homogeneous tuples) + * + * ```ts + * import { Equivalence } from "effect" + * + * const stringTupleEq = Equivalence.Tuple([ + * Equivalence.strictEqual(), + * Equivalence.strictEqual(), + * Equivalence.strictEqual() + * ]) + * + * const tuple1 = ["hello", "world", "test"] as const + * const tuple2 = ["hello", "world", "test"] as const + * const tuple3 = ["hello", "world", "different"] as const + * + * console.log(stringTupleEq(tuple1, tuple2)) // true + * console.log(stringTupleEq(tuple1, tuple3)) // false (different third element) + * ``` + * + * **Example** (Comparing tuples with custom equivalences) + * + * ```ts + * import { Equivalence } from "effect" + * + * const caseInsensitive = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (s: string) => s.toLowerCase() + * ) + * + * const customTupleEq = Equivalence.Tuple([ + * caseInsensitive, + * caseInsensitive, + * caseInsensitive + * ]) + * + * console.log( + * customTupleEq(["Hello", "World", "Test"], ["HELLO", "WORLD", "TEST"]) + * ) // true + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export function Tuple>>( + elements: Elements +): Equivalence<{ readonly [I in keyof Elements]: [Elements[I]] extends [Equivalence] ? A : never }> { + return make((self, that) => { + if (self.length !== that.length) { + return false + } + for (let i = 0; i < self.length; i++) { + if (!elements[i](self[i], that[i])) { + return false + } + } + return true + }) +} + +/** + * @since 4.0.0 + */ +function Array_(item: Equivalence): Equivalence> { + return make((self, that) => { + if (self.length !== that.length) return false + + for (let i = 0; i < self.length; i++) { + if (!item(self[i], that[i])) return false + } + + return true + }) +} +export { + /** + * Creates an equivalence for arrays where all elements are compared using the same equivalence. + * + * **When to use** + * + * Use when you need to compare arrays with one equivalence for every element. + * + * **Details** + * + * - Requires arrays to have the same length; different lengths are never equivalent + * - Compares elements positionally, such as index `0` with index `0` + * - Returns `true` only if all corresponding elements are equivalent + * - Empty arrays are considered equivalent + * - The result is also an equivalence that satisfies reflexive, symmetric, and transitive properties + * + * **Example** (Comparing number arrays) + * + * ```ts + * import { Equivalence } from "effect" + * + * const numberArrayEq = Equivalence.Array(Equivalence.strictEqual()) + * + * console.log(numberArrayEq([1, 2, 3], [1, 2, 3])) // true + * console.log(numberArrayEq([1, 2, 3], [1, 2, 4])) // false + * console.log(numberArrayEq([1, 2], [1, 2, 3])) // false (different length) + * ``` + * + * **Example** (Case-insensitive string array) + * + * ```ts + * import { Equivalence } from "effect" + * + * const caseInsensitive = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (s: string) => s.toLowerCase() + * ) + * const stringArrayEq = Equivalence.Array(caseInsensitive) + * + * console.log(stringArrayEq(["Hello", "World"], ["HELLO", "WORLD"])) // true + * console.log(stringArrayEq(["Hello"], ["Hi"])) // false + * console.log(stringArrayEq([], [])) // true (empty arrays) + * ``` + * + * @see {@link Tuple} + * @see {@link Record} + * @category combinators + * @since 4.0.0 + */ + Array_ as Array +} + +/** + * Creates an equivalence for objects by comparing their properties using provided equivalences. + * + * **When to use** + * + * Use when you need an `Equivalence` for objects with known, fixed property + * names. + * + * **Details** + * + * Compares only the properties specified in the struct definition; other + * properties are ignored. String and symbol keys are supported via + * `Reflect.ownKeys`. The result returns `true` only if all specified properties + * are equivalent according to their equivalences, and it also satisfies + * reflexive, symmetric, and transitive properties. + * + * **Example** (Comparing structs with different equivalences per field) + * + * ```ts + * import { Equivalence } from "effect" + * + * interface Person { + * name: string + * age: number + * email: string + * } + * + * const caseInsensitive = Equivalence.mapInput( + * Equivalence.strictEqual(), + * (s: string) => s.toLowerCase() + * ) + * + * const personEq = Equivalence.Struct({ + * name: caseInsensitive, + * age: Equivalence.strictEqual(), + * email: caseInsensitive + * }) + * + * const person1 = { name: "Alice", age: 30, email: "alice@example.com" } + * const person2 = { name: "ALICE", age: 30, email: "ALICE@EXAMPLE.COM" } + * const person3 = { name: "Alice", age: 31, email: "alice@example.com" } + * + * console.log(personEq(person1, person2)) // true (case-insensitive match) + * console.log(personEq(person1, person3)) // false (different age) + * ``` + * + * **Example** (Comparing specific fields) + * + * ```ts + * import { Equivalence } from "effect" + * + * const nameAgeEq = Equivalence.Struct({ + * name: Equivalence.strictEqual(), + * age: Equivalence.strictEqual() + * }) + * + * // Only compares name and age, ignores other properties + * const obj1 = { name: "Alice", age: 30, extra: "ignored" } + * const obj2 = { name: "Alice", age: 30, extra: "different" } + * console.log(nameAgeEq(obj1, obj2)) // true + * ``` + * + * @see {@link Record} + * @see {@link mapInput} + * @see {@link combine} + * @category combinators + * @since 4.0.0 + */ +export function Struct>>( + fields: R +): Equivalence<{ readonly [K in keyof R]: [R[K]] extends [Equivalence] ? A : never }> { + const keys: Array = Reflect.ownKeys(fields) + return make((self, that) => { + for (const key of keys) { + if (!fields[key](self[key], that[key])) return false + } + return true + }) +} + +/** + * Creates an equivalence for objects by comparing all properties using the same equivalence. + * + * **When to use** + * + * Use when you need to compare records with the same equivalence for every + * property value. + * + * **Details** + * + * - Compares all properties present in both objects + * - Requires both objects to have the same set of keys; different keys result in `false` + * - All property values must be equivalent according to the provided equivalence + * - Supports both string and symbol keys via `Reflect.ownKeys` + * - Empty objects are considered equivalent + * - The result is also an equivalence that satisfies reflexive, symmetric, and transitive properties + * + * **Example** (Defining records with string values) + * + * ```ts + * import { Equivalence } from "effect" + * + * const stringRecordEq = Equivalence.Record(Equivalence.strictEqual()) + * + * const record1 = { a: "hello", b: "world" } + * const record2 = { a: "hello", b: "world" } + * const record3 = { a: "hello", b: "different" } + * const record4 = { a: "hello" } // missing key 'b' + * + * console.log(stringRecordEq(record1, record2)) // true + * console.log(stringRecordEq(record1, record3)) // false + * console.log(stringRecordEq(record1, record4)) // false (different keys) + * ``` + * + * **Example** (Defining records with number values) + * + * ```ts + * import { Equivalence } from "effect" + * + * const numberRecordEq = Equivalence.Record(Equivalence.strictEqual()) + * + * const scores1 = { alice: 100, bob: 85 } + * const scores2 = { alice: 100, bob: 85 } + * const scores3 = { alice: 100, bob: 90 } + * + * console.log(numberRecordEq(scores1, scores2)) // true + * console.log(numberRecordEq(scores1, scores3)) // false + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export function Record(value: Equivalence): Equivalence> { + return make((self, that) => { + const selfKeys = Reflect.ownKeys(self) + const thatKeys = Reflect.ownKeys(that) + + if (selfKeys.length !== thatKeys.length) return false + + for (const key of selfKeys) { + if (!Object.hasOwn(that, key) || !value(self[key], that[key])) { + return false + } + } + + return true + }) +} + +/** + * Creates a `Reducer` for combining `Equivalence` instances, useful for aggregating equivalences in collections. + * + * **When to use** + * + * Use when you need a reducer that combines equivalences. + * + * **Details** + * + * Returns a reducer that combines equivalences using `combine`. The identity + * element for empty collections is an equivalence that always returns `true`. + * The reducer uses `combineAll` for collections of equivalences and can be used + * with fold operations. + * + * **Example** (Creating a Reducer) + * + * ```ts + * import { Equivalence } from "effect" + * + * const reducer = Equivalence.makeReducer() + * const equivalences = [ + * Equivalence.strictEqual(), + * Equivalence.make((a, b) => Math.abs(a - b) < 1) + * ] + * + * const combined = reducer.combineAll(equivalences) + * // Combined equivalence requires both conditions to be true + * console.log(combined(1, 1)) // true (strict equal) + * console.log(combined(1, 1.5)) // false (strict equal fails) + * ``` + * + * @see {@link combine} Combine two equivalences + * @see {@link combineAll} Combine multiple equivalences + * @see {@link Reducer} Reducer type for collection operations + * @category constructors + * @since 4.0.0 + */ +export function makeReducer() { + return Reducer.make>( + combine, + () => true, + combineAll + ) +} + +/** + * Equivalence instance for `Date` objects that compares their `getTime()` values using `Equivalence.Number`. + * + * **When to use** + * + * Use when you need an `Equivalence` for JavaScript date objects by their + * millisecond timestamp. + * + * **Details** + * + * Different `Date` instances that represent the same millisecond timestamp are equivalent. Because `Equivalence.Number` + * treats `NaN` as equal to `NaN`, two invalid `Date` values are also considered equivalent. + * + * **Example** (Comparing Date values) + * + * ```ts + * import { Equivalence } from "effect" + * + * const d1 = new Date("2020-01-01T00:00:00.000Z") + * const d2 = new Date("2020-01-01T00:00:00.000Z") + * const d3 = new Date("2021-01-01T00:00:00.000Z") + * const invalidDate1 = new Date("foo") + * const invalidDate2 = new Date("bar") + * + * console.log(Equivalence.Date(d1, d2)) // true + * console.log(Equivalence.Date(d1, d3)) // false + * console.log(Equivalence.Date(invalidDate1, invalidDate2)) // true + * console.log(Equivalence.Date(invalidDate1, d1)) // false + * ``` + * + * **Example** (Comparing reference and value equality) + * + * ```ts + * import { Equivalence } from "effect" + * + * const d1 = new Date(0) + * const d2 = new Date(0) + * + * console.log(d1 === d2) // false (different references) + * console.log(Equivalence.Date(d1, d2)) // true (same time value) + * ``` + * + * @see {@link Number} for the numeric equivalence applied to each `Date#getTime()` result + * @see {@link mapInput} for deriving an equivalence by mapping inputs before comparison + * @see {@link strictEqual} for reference equality when two values must be the same object + * @category instances + * @since 2.0.0 + */ +export const Date: Equivalence = mapInput( + Number, + (d: Date) => d.getTime() +) diff --git a/repos/effect/packages/effect/src/ErrorReporter.ts b/repos/effect/packages/effect/src/ErrorReporter.ts new file mode 100644 index 0000000..1fbc6cc --- /dev/null +++ b/repos/effect/packages/effect/src/ErrorReporter.ts @@ -0,0 +1,543 @@ +/** + * Reports Effect failures to external code. + * + * An `ErrorReporter` receives `Cause` values from `Effect.withErrorReporting`, + * manual `report` calls, or built-in reporting boundaries. It forwards each + * non-interruption error to a callback, so applications can send failures to + * logging, monitoring, or error-tracking systems. This module also includes + * layers for installing reporters and symbols for marking errors as ignored or + * attaching severity and attributes. + * + * @since 4.0.0 + */ +import type * as Cause from "./Cause.ts" +import type * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import type * as Fiber from "./Fiber.ts" +import * as effect from "./internal/effect.ts" +import * as references from "./internal/references.ts" +import * as Layer from "./Layer.ts" +import * as LogLevel from "./LogLevel.ts" +import type { Severity } from "./LogLevel.ts" +import type { ReadonlyRecord } from "./Record.ts" +import type * as Scope from "./Scope.ts" + +/** + * String literal type used as the runtime type identifier for + * `ErrorReporter` values. + * + * **When to use** + * + * Use to refer to the runtime type identifier type in low-level integrations. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/ErrorReporter" + +/** + * Runtime type identifier attached to `ErrorReporter` values. + * + * **Details** + * + * This marker is part of the runtime representation of `ErrorReporter` + * implementations. Most code should create reporters with `make` and register + * them with `layer`. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~effect/ErrorReporter" + +/** + * An `ErrorReporter` receives reported failures and forwards them to an + * external system such as a logging service or error tracker. + * + * **When to use** + * + * Use as the interface for custom reporters that forward reported Effect + * failures to logging, monitoring, or error-tracking systems. + * + * **Details** + * + * Reporting is triggered by `Effect.withErrorReporting`, + * `ErrorReporter.report`, or built-in boundaries in the HTTP and RPC server + * modules. Use {@link make} to create a reporter; it handles deduplication + * and per-error annotation extraction automatically. + * + * @see {@link make} for creating an `ErrorReporter` from a callback + * @see {@link layer} for registering reporters in the environment + * @see {@link report} for manually reporting a `Cause` + * @see {@link Effect.withErrorReporting} for reporting failures from an effect + * + * @category models + * @since 4.0.0 + */ +export interface ErrorReporter { + readonly [TypeId]: TypeId + report(options: { + readonly cause: Cause.Cause + readonly fiber: Fiber.Fiber + readonly timestamp: bigint + }): void +} + +/** + * Creates an `ErrorReporter` from a callback. + * + * **When to use** + * + * Use to define how reported failures are forwarded to a logging, monitoring, + * or error-tracking backend. + * + * **Details** + * + * The returned reporter automatically deduplicates causes and individual + * errors (the same object is never reported twice), skips interruptions, + * and resolves the `ignore`, `severity`, and `attributes` annotations on + * each error before invoking your callback. + * + * **Example** (Forwarding errors to the console) + * + * ```ts + * import { ErrorReporter } from "effect" + * + * // Forward every failure to the console + * const consoleReporter = ErrorReporter.make( + * ({ error, severity, attributes }) => { + * console.error(`[${severity}]`, error.message, attributes) + * } + * ) + * ``` + * + * @see {@link layer} for registering reporters in the environment + * @see {@link report} for manually reporting a `Cause` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + report: (options: { + readonly cause: Cause.Cause + readonly error: Error + readonly attributes: ReadonlyRecord + readonly severity: Severity + readonly fiber: Fiber.Fiber + readonly timestamp: bigint + }) => void +): ErrorReporter => { + const reported = new WeakSet | object>() + return { + [TypeId]: TypeId, + report(options) { + if (reported.has(options.cause)) return + reported.add(options.cause) + for (let i = 0; i < options.cause.reasons.length; i++) { + const reason = options.cause.reasons[i] + if (reason._tag === "Interrupt") continue + const original = reason._tag === "Fail" ? reason.error : reason.defect + const isObject = typeof original === "object" && original !== null + if (isObject) { + if (reported.has(original)) continue + reported.add(original) + } + if (isIgnored(original)) continue + const pretty = effect.causePrettyError(original as any, reason.annotations) + report({ + ...options, + error: pretty, + severity: isObject ? getSeverity(original) : "Info", + attributes: isObject ? getAttributes(original) : emptyAttributes + }) + } + } + } +} + +/** + * Context reference that holds the set of active error reporters for the + * current fiber. Defaults to an empty set (no reporting). + * + * **When to use** + * + * Use when you need to read or replace the current set of error reporters + * directly. + * + * @category references + * @since 4.0.0 + */ +export const CurrentErrorReporters: Context.Reference> = references.CurrentErrorReporters + +/** + * Creates a `Layer` that registers one or more `ErrorReporter`s. + * + * **When to use** + * + * Use to provide one or more error reporters to effects that perform error + * reporting. + * + * **Details** + * + * Reporters can be plain `ErrorReporter` values or effectful + * `Effect` values that are resolved when the layer is built. By + * default the provided reporters **replace** any previously registered + * reporters. Set `mergeWithExisting: true` to add them alongside existing ones. + * + * **Example** (Providing error reporters) + * + * ```ts + * import { Effect, ErrorReporter } from "effect" + * + * const consoleReporter = ErrorReporter.make(({ error, severity }) => { + * console.error(`[${severity}]`, error.message) + * }) + * + * const metricsReporter = ErrorReporter.make(({ severity }) => { + * // increment an error counter by severity + * }) + * + * // Replace all existing reporters + * const ReporterLive = ErrorReporter.layer([ + * consoleReporter, + * metricsReporter + * ]) + * + * // Add to existing reporters instead of replacing + * const ReporterMerged = ErrorReporter.layer( + * [metricsReporter], + * { mergeWithExisting: true } + * ) + * + * const program = Effect.fail("boom").pipe( + * Effect.withErrorReporting, + * Effect.provide(ReporterLive) + * ) + * ``` + * + * @see {@link make} for creating an `ErrorReporter` from a callback + * @see {@link CurrentErrorReporters} for low-level access to the current reporters + * + * @category layers + * @since 4.0.0 + */ +export const layer = < + const Reporters extends ReadonlyArray> +>( + reporters: Reporters, + options?: { readonly mergeWithExisting?: boolean | undefined } | undefined +): Layer.Layer< + never, + Reporters extends readonly [] ? never : Effect.Error, + Exclude< + Reporters extends readonly [] ? never : Effect.Services, + Scope.Scope + > +> => + Layer.effect( + CurrentErrorReporters, + Effect.withFiber(Effect.fnUntraced(function*(fiber) { + const currentReporters = new Set( + options?.mergeWithExisting === true ? fiber.getRef(references.CurrentErrorReporters) : [] + ) + for (const reporter of reporters) { + currentReporters.add(Effect.isEffect(reporter) ? yield* reporter : reporter) + } + return currentReporters + })) + ) + +/** + * Runs all registered error reporters on the current fiber for a `Cause`. + * + * **When to use** + * + * Use to report a failure for observability without failing the current fiber. + * + * **Example** (Reporting a cause manually) + * + * ```ts + * import { Cause, Effect, ErrorReporter } from "effect" + * + * // Log the cause for monitoring, then continue with a fallback + * const program = Effect.gen(function*() { + * const cause = Cause.fail("something went wrong") + * yield* ErrorReporter.report(cause) + * return "fallback value" + * }) + * ``` + * + * @category Reporting + * @since 4.0.0 + */ +export const report = (cause: Cause.Cause): Effect.Effect => + Effect.withFiber((fiber) => { + effect.reportCauseUnsafe(fiber, cause) + return Effect.void + }) + +/** + * Interface that object errors can implement to control reporting behavior. + * + * **When to use** + * + * Use as the annotation contract for object errors that customize how error + * reporting handles them. + * + * **Details** + * + * All three annotation properties are optional: `[ErrorReporter.ignore]` + * prevents reporting when set to `true`, `[ErrorReporter.severity]` overrides + * the default `"Info"` severity, and `[ErrorReporter.attributes]` adds extra + * key/value pairs forwarded to reporters. The global `Error` interface is + * augmented with `Reportable`, so these properties are available on `Error` + * instances at the type level. + * + * @see {@link ignore} for the runtime annotation key that suppresses reports + * @see {@link severity} for the runtime annotation key that overrides severity + * @see {@link attributes} for the runtime annotation key that attaches reporter + * metadata + * + * @category annotations + * @since 4.0.0 + */ +export interface Reportable { + readonly [ignore]?: boolean + readonly [severity]?: Severity + readonly [attributes]?: ReadonlyRecord +} + +declare global { + interface Error extends Reportable {} +} + +/** + * Defines the string property key used to mark an object error as ignored by error + * reporting. + * + * **When to use** + * + * Use to type the property key that suppresses reporting for expected object + * errors. + * + * **Details** + * + * Set this property to `true` on an error class or object error to prevent it + * from being forwarded to reporters. This is useful for expected failures such + * as HTTP 404 responses. + * + * @category annotations + * @since 4.0.0 + */ +export type ignore = "~effect/ErrorReporter/ignore" + +/** + * Defines the runtime property key used to mark an object error as ignored by error + * reporting. + * + * **When to use** + * + * Use to suppress reporting for expected object errors, such as HTTP 404 + * responses. + * + * **Details** + * + * Set `error[ErrorReporter.ignore]` to `true` to prevent the error from being + * forwarded to reporters. This is useful for expected failures such as HTTP 404 + * responses. + * + * **Example** (Marking errors as ignored) + * + * ```ts + * import { Data, ErrorReporter } from "effect" + * + * class NotFoundError extends Data.TaggedError("NotFoundError")<{}> { + * readonly [ErrorReporter.ignore] = true + * } + * ``` + * + * @see {@link isIgnored} for checking whether a value carries this annotation + * @see {@link Reportable} for the annotation contract recognized on object + * errors + * + * @category annotations + * @since 4.0.0 + */ +export const ignore: ignore = "~effect/ErrorReporter/ignore" + +/** + * Returns `true` if the given value has the `ErrorReporter.ignore` annotation + * set to `true`. + * + * **When to use** + * + * Use to check whether an error value is annotated to be skipped before + * forwarding it to error reporting code. + * + * @see {@link ignore} for the annotation key this predicate reads + * + * @category annotations + * @since 4.0.0 + */ +export const isIgnored = (u: unknown): boolean => + typeof u === "object" && u !== null && ignore in u && u[ignore] === true + +/** + * Defines the string property key used to override the severity level of an object error. + * + * **When to use** + * + * Use to type the property key that overrides the reporting severity for object + * errors. + * + * **Details** + * + * When set to a valid `LogLevel.Severity`, the reporter callback receives this + * value as `severity`. Missing or invalid values fall back to `"Info"`. + * + * @category annotations + * @since 4.0.0 + */ +export type severity = "~effect/ErrorReporter/severity" + +/** + * Defines the runtime property key used to override the severity level of an object error. + * + * **When to use** + * + * Use to annotate object errors with the severity reporter callbacks should + * receive. + * + * **Details** + * + * Set `error[ErrorReporter.severity]` to a valid `LogLevel.Severity` value. + * Missing or invalid values fall back to `"Info"`. + * + * **Example** (Setting error severity annotations) + * + * ```ts + * import { Data, ErrorReporter } from "effect" + * + * class DeprecationWarning extends Data.TaggedError("DeprecationWarning")<{}> { + * readonly [ErrorReporter.severity] = "Warn" as const + * } + * ``` + * + * @see {@link getSeverity} for reading the severity stored under this key + * @see {@link Reportable} for the annotation contract recognized on object + * errors + * + * @category annotations + * @since 4.0.0 + */ +export const severity: severity = "~effect/ErrorReporter/severity" + +/** + * Reads the `ErrorReporter.severity` annotation from an error object, + * falling back to `"Info"` when the annotation is unset or invalid. + * + * **When to use** + * + * Use to inspect the severity that reporter callbacks will receive for an + * object error. + * + * @see {@link severity} for the annotation key used to override severity + * @see {@link Reportable} for the annotation properties recognized on object errors + * + * @category annotations + * @since 4.0.0 + */ +export const getSeverity = (error: object): Severity => { + if (severity in error && LogLevel.values.includes(error[severity] as Severity)) { + return error[severity] as Severity + } + return "Info" +} + +/** + * Defines the string property key used to attach extra key/value metadata to an object + * error report. + * + * **When to use** + * + * Use to type the property key that attaches metadata to object error reports. + * + * **Details** + * + * Reporters receive these attributes alongside the error, making it easy to + * include contextual information such as user IDs, request IDs, or other + * domain-specific debugging data. + * + * @category annotations + * @since 4.0.0 + */ +export type attributes = "~effect/ErrorReporter/attributes" + +/** + * Defines the runtime property key used to attach extra key/value metadata to an object + * error report. + * + * **When to use** + * + * Use to attach domain metadata to object errors so reporter callbacks receive + * it with the reported failure. + * + * **Details** + * + * Set `error[ErrorReporter.attributes]` to a record of metadata that should be + * forwarded to reporters alongside the error. + * + * **Example** (Setting error attributes) + * + * ```ts + * import { Data, ErrorReporter } from "effect" + * + * class PaymentError extends Data.TaggedError("PaymentError")<{ + * readonly orderId: string + * }> { + * readonly [ErrorReporter.attributes] = { + * orderId: this.orderId + * } + * } + * ``` + * + * @see {@link ignore} for suppressing reports for expected object errors + * @see {@link severity} for overriding reporter severity + * @see {@link getAttributes} for reading the metadata stored under this key + * @see {@link Reportable} for the annotation contract recognized on object + * errors + * + * @category annotations + * @since 4.0.0 + */ +export const attributes: attributes = "~effect/ErrorReporter/attributes" + +/** + * Reads the `ErrorReporter.attributes` annotation from an error object, + * returning an empty record when unset. + * + * **When to use** + * + * Use to inspect the attributes that reporter callbacks will receive for an + * object error. + * + * **Details** + * + * Returns the value stored under `ErrorReporter.attributes`, or the module's + * shared empty record when the annotation is absent. + * + * **Gotchas** + * + * The annotation value is returned as-is; this helper does not validate or + * clone it. + * + * @see {@link attributes} for the annotation key used to attach metadata + * @see {@link Reportable} for the annotation properties recognized on object errors + * + * @category annotations + * @since 4.0.0 + */ +export const getAttributes = (error: object): ReadonlyRecord => { + return attributes in error ? error[attributes] as any : emptyAttributes +} + +const emptyAttributes: ReadonlyRecord = {} diff --git a/repos/effect/packages/effect/src/ExecutionPlan.ts b/repos/effect/packages/effect/src/ExecutionPlan.ts new file mode 100644 index 0000000..fd00503 --- /dev/null +++ b/repos/effect/packages/effect/src/ExecutionPlan.ts @@ -0,0 +1,416 @@ +/** + * Describes ordered fallback steps for running effects or streams. + * + * An `ExecutionPlan` contains one or more steps. Each step provides a `Context` + * or `Layer`, and may also define attempt limits, retry schedules, or + * predicates that decide whether to keep trying. The runtime tries steps in + * order until the workflow succeeds or the plan is exhausted. This module also + * supports merging plans and reading metadata for the active step and attempt. + * + * @since 3.16.0 + */ +import type { NonEmptyReadonlyArray } from "./Array.ts" +import * as Context from "./Context.ts" +import type * as Effect from "./Effect.ts" +import { constant } from "./Function.ts" +import * as effect from "./internal/effect.ts" +import * as Layer from "./Layer.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type * as Schedule from "./Schedule.ts" + +/** + * String literal type used as the runtime type identifier for `ExecutionPlan` + * values. + * + * @category type IDs + * @since 3.16.0 + */ +export type TypeId = "~effect/ExecutionPlan" + +/** + * Runtime type identifier attached to `ExecutionPlan` values and used by + * `isExecutionPlan`. + * + * @category type IDs + * @since 3.16.0 + */ +export const TypeId: TypeId = "~effect/ExecutionPlan" + +/** + * Returns `true` if a value is an `ExecutionPlan` by checking for the + * `ExecutionPlan.TypeId` marker. + * + * **When to use** + * + * Use when accepting an unknown value and you need to narrow it to an + * `ExecutionPlan` before reading plan fields or passing it to plan-consuming + * APIs. + * + * **Gotchas** + * + * This is a structural marker check; it does not validate the marker value or + * the shape of the plan steps. + * + * @see {@link make} for constructing execution plans that satisfy this guard + * @see {@link TypeId} for the runtime marker checked by this guard + * + * @category guards + * @since 3.16.0 + */ +export const isExecutionPlan = (u: unknown): u is ExecutionPlan => Predicate.hasProperty(u, TypeId) + +/** + * A `ExecutionPlan` can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted. + * + * **Example** (Defining fallback execution steps) + * + * ```ts + * import { Effect, ExecutionPlan, Schedule } from "effect" + * import type { Layer } from "effect" + * import type { LanguageModel } from "effect/unstable/ai" + * + * declare const layerBad: Layer.Layer + * declare const layerGood: Layer.Layer + * + * const ThePlan = ExecutionPlan.make( + * { + * // First try with the bad layer 2 times with a 3 second delay between attempts + * provide: layerBad, + * attempts: 2, + * schedule: Schedule.spaced(3000) + * }, + * // Then try with the bad layer 3 times with a 1 second delay between attempts + * { + * provide: layerBad, + * attempts: 3, + * schedule: Schedule.spaced(1000) + * }, + * // Finally try with the good layer. + * // + * // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. + * { + * provide: layerGood + * } + * ) + * + * declare const effect: Effect.Effect< + * void, + * never, + * LanguageModel.LanguageModel + * > + * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ``` + * + * @category models + * @since 3.16.0 + */ +export interface ExecutionPlan< + Config extends { + provides: any + input: any + error: any + requirements: any + } +> extends Pipeable { + readonly [TypeId]: TypeId + readonly steps: NonEmptyReadonlyArray<{ + readonly provide: + | Context.Context + | Layer.Layer + readonly attempts?: number | undefined + readonly while?: + | ((input: Config["input"]) => Effect.Effect) + | undefined + readonly schedule?: Schedule.Schedule | undefined + }> + + /** + * Returns an equivalent `ExecutionPlan` with the requirements satisfied, using the current context. + */ + readonly captureRequirements: Effect.Effect< + ExecutionPlan<{ + provides: Config["provides"] + input: Config["input"] + error: Config["error"] + requirements: never + }>, + never, + Config["requirements"] + > +} + +/** + * Base type-level configuration carried by an `ExecutionPlan`. + * + * **Details** + * + * `provides` tracks services supplied by plan steps, `input` tracks the error + * input consumed by schedules and `while` predicates, `error` tracks failures + * from plan layers or predicates, and `requirements` tracks services needed to + * build or run the plan. + * + * @category models + * @since 4.0.0 + */ +export type ConfigBase = { + provides: any + input: any + error: any + requirements: any +} + +/** + * Create an `ExecutionPlan`, which can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted. + * + * **Example** (Creating an execution plan) + * + * ```ts + * import { Effect, ExecutionPlan, Schedule } from "effect" + * import type { Layer } from "effect" + * import type { LanguageModel } from "effect/unstable/ai" + * + * declare const layerBad: Layer.Layer + * declare const layerGood: Layer.Layer + * + * const ThePlan = ExecutionPlan.make( + * { + * // First try with the bad layer 2 times with a 3 second delay between attempts + * provide: layerBad, + * attempts: 2, + * schedule: Schedule.spaced(3000) + * }, + * // Then try with the bad layer 3 times with a 1 second delay between attempts + * { + * provide: layerBad, + * attempts: 3, + * schedule: Schedule.spaced(1000) + * }, + * // Finally try with the good layer. + * // + * // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. + * { + * provide: layerGood + * } + * ) + * + * declare const effect: Effect.Effect< + * void, + * never, + * LanguageModel.LanguageModel + * > + * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ``` + * + * @category constructors + * @since 3.16.0 + */ +export const make = >( + ...steps: Steps & { [K in keyof Steps]: make.Step } +): ExecutionPlan<{ + provides: make.StepProvides + input: make.StepInput + error: + | (Steps[number]["provide"] extends Context.Context | Layer.Layer ? E + : never) + | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect ? _E : never) + requirements: + | (Steps[number]["provide"] extends Layer.Layer ? R : never) + | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect ? R : never) + | (Steps[number]["schedule"] extends Schedule.Schedule ? R : never) +}> => + makeProto(steps.map((options, i) => { + if (options.attempts && options.attempts < 1) { + throw new Error(`ExecutionPlan.make: step[${i}].attempts must be greater than 0`) + } + return { + schedule: options.schedule, + attempts: options.attempts, + while: options.while + ? (input: any) => + effect.suspend(() => { + const result = options.while!(input) + return typeof result === "boolean" ? effect.succeed(result) : result + }) + : undefined, + provide: options.provide + } + }) as any) + +/** + * Namespace containing type helpers used by `ExecutionPlan.make`. + * + * @since 3.16.0 + */ +export declare namespace make { + /** + * Input shape for a single execution-plan step. + * + * **Details** + * + * Each step provides a `Context` or `Layer` and may limit attempts, add a + * `while` predicate for retry decisions, or attach a `Schedule` for retry + * timing. + * + * @category models + * @since 3.16.0 + */ + export type Step = { + readonly provide: Context.Context | Context.Context | Layer.Any + readonly attempts?: number | undefined + readonly while?: ((input: any) => boolean | Effect.Effect) | undefined + readonly schedule?: Schedule.Schedule | undefined + } + + /** + * Computes the intersection of services provided by a list of execution-plan + * steps. + * + * @category utility types + * @since 3.16.1 + */ + export type StepProvides, Out = unknown> = Steps extends + readonly [infer Step, ...infer Rest] ? StepProvides< + Rest, + & Out + & ( + (Step extends { readonly provide: Context.Context | Layer.Layer } ? P + : unknown) + ) + > : + Out + + /** + * Computes the intersection of services provided by a list of execution plans. + * + * @category utility types + * @since 3.16.1 + */ + export type PlanProvides, Out = unknown> = Plans extends + readonly [infer Plan, ...infer Rest] ? + PlanProvides ? T["provides"] : unknown)> : + Out + + /** + * Computes the input type consumed by the `while` predicates and schedules in + * a list of execution-plan steps. + * + * @category utility types + * @since 3.16.0 + */ + export type StepInput, Out = unknown> = Steps extends + readonly [infer Step, ...infer Rest] ? StepInput< + Rest, + & Out + & ( + & (Step extends { readonly while: (input: infer I) => infer _ } ? I : unknown) + & (Step extends { readonly schedule: Schedule.Schedule } ? I : unknown) + ) + > : + Out + + /** + * Computes the combined input type consumed by a list of execution plans. + * + * @category utility types + * @since 3.16.0 + */ + export type PlanInput, Out = unknown> = Plans extends + readonly [infer Plan, ...infer Rest] ? + PlanInput ? T["input"] : unknown)> : + Out +} + +const Proto: Omit, "steps"> = { + [TypeId]: TypeId, + get captureRequirements() { + const self = this as any as ExecutionPlan + return effect.contextWith((context: Context.Context) => + effect.succeed(makeProto(self.steps.map((step) => ({ + ...step, + provide: Layer.isLayer(step.provide) + ? Layer.provide(step.provide, Layer.succeedContext(context)) + : step.provide + })) as any)) + ) + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeProto = ( + steps: ExecutionPlan<{ + provides: Provides + input: In + error: PlanE + requirements: PlanR + }>["steps"] +) => { + const self = Object.create(Proto) + self.steps = steps + return self +} + +/** + * Combines multiple execution plans by concatenating their steps in order. + * + * **When to use** + * + * Use to combine separately defined fallback plans into one ordered plan before + * applying it to an effect or stream. + * + * **Details** + * + * The resulting plan tries every step from the first plan, then every step from + * the next plan, and so on. + * + * @see {@link make} for building a plan from individual steps instead of combining existing plans + * + * @category combining + * @since 3.16.0 + */ +export const merge = >>( + ...plans: Plans +): ExecutionPlan<{ + provides: make.PlanProvides + input: make.PlanInput + error: Plans[number] extends ExecutionPlan ? T["error"] : never + requirements: Plans[number] extends ExecutionPlan ? T["requirements"] : never +}> => makeProto(plans.flatMap((plan) => plan.steps) as any) + +/** + * Metadata describing the currently running execution-plan attempt. + * + * **Details** + * + * `attempt` is the current 1-based attempt number, and `stepIndex` is the + * 0-based index of the plan step currently being evaluated. + * + * @category metadata + * @since 4.0.0 + */ +export interface Metadata { + readonly attempt: number + readonly stepIndex: number +} + +/** + * Context reference containing metadata for the currently running + * execution-plan attempt. + * + * **When to use** + * + * Use to read the active plan step and attempt while code is running under an + * execution plan. + * + * @category metadata + * @since 4.0.0 + */ +export const CurrentMetadata = Context.Reference("effect/ExecutionPlan/CurrentMetadata", { + defaultValue: constant({ + attempt: 0, + stepIndex: 0 + }) +}) diff --git a/repos/effect/packages/effect/src/Exit.ts b/repos/effect/packages/effect/src/Exit.ts new file mode 100644 index 0000000..d3e45bd --- /dev/null +++ b/repos/effect/packages/effect/src/Exit.ts @@ -0,0 +1,1085 @@ +/** + * Represents the result of an Effect computation as a plain value. + * + * An `Exit` is either a success with an `A` or a failure with a + * `Cause`. The failure cause preserves typed errors, defects, and + * interruptions after a workflow has finished. Use this module when completed + * Effect results need to be inspected, transformed, filtered, or matched + * synchronously as data. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.ts" +import type * as Effect from "./Effect.ts" +import * as core from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import type { Option } from "./Option.ts" +import type * as Result from "./Result.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = core.ExitTypeId + +/** + * Represents the result of an Effect computation. + * + * **When to use** + * + * Use when you need to synchronously inspect whether an Effect computation + * succeeded or failed. + * + * **Details** + * + * An `Exit` is either `Success` containing a value of type `A`, or + * `Failure` containing a `Cause` describing why the computation + * failed. + * + * Since `Exit` is also an `Effect`, you can yield it inside `Effect.gen`. + * + * **Example** (Pattern matching on an Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const success: Exit.Exit = Exit.succeed(42) + * const failure: Exit.Exit = Exit.fail("error") + * + * const result = Exit.match(success, { + * onSuccess: (value) => `Got value: ${value}`, + * onFailure: (cause) => `Got error: ${cause}` + * }) + * ``` + * + * @see {@link Success} for the success case + * @see {@link Failure} for the failure case + * @see {@link match} for pattern matching + * + * @category models + * @since 2.0.0 + */ +export type Exit = Success | Failure + +/** + * Namespace containing helper types shared by `Exit` values. + * + * **When to use** + * + * Use to reference helper types that describe the shared structure of `Exit` + * values. + * + * @since 2.0.0 + */ +export declare namespace Exit { + /** + * Base interface shared by both Success and Failure. + * + * **When to use** + * + * Use to describe the common protocol implemented by every `Exit` value. + * + * **Details** + * + * Every Exit is also an Effect, so you can yield it in `Effect.gen`. + * + * @category models + * @since 4.0.0 + */ + export interface Proto extends Effect.Effect { + readonly [TypeId]: typeof TypeId + } +} + +/** + * A successful Exit containing a value. + * + * **When to use** + * + * Use when working with the successful branch of an `Exit` after narrowing + * with {@link isSuccess}. Access the value via the `value` property after + * narrowing. + * + * **Example** (Accessing the success value) + * + * ```ts + * import { Exit } from "effect" + * + * const success = Exit.succeed(42) + * + * if (Exit.isSuccess(success)) { + * console.log(success._tag) // "Success" + * console.log(success.value) // 42 + * } + * ``` + * + * @see {@link isSuccess} to narrow an Exit to Success + * @see {@link Failure} for the failure counterpart + * + * @category models + * @since 2.0.0 + */ +export interface Success extends Exit.Proto { + readonly _tag: "Success" + readonly value: A +} + +/** + * A failed Exit containing a Cause. + * + * **When to use** + * + * Use when working with the failed branch of an `Exit` after narrowing with + * {@link isFailure}. Access the cause via the `cause` property after + * narrowing. + * + * **Details** + * + * The `Cause` may contain typed errors, defects, or interruptions. + * + * **Example** (Accessing the failure cause) + * + * ```ts + * import { Exit } from "effect" + * + * const failure = Exit.fail("something went wrong") + * + * if (Exit.isFailure(failure)) { + * console.log(failure._tag) // "Failure" + * console.log(failure.cause) // Cause representing the error + * } + * ``` + * + * @see {@link isFailure} to narrow an Exit to Failure + * @see {@link Success} for the success counterpart + * + * @category models + * @since 2.0.0 + */ +export interface Failure extends Exit.Proto { + readonly _tag: "Failure" + readonly cause: Cause.Cause +} + +/** + * Checks whether an unknown value is an Exit. + * + * **When to use** + * + * Use to validate unknown values at system boundaries and narrow them to + * `Exit`. + * + * **Details** + * + * Does not inspect the contents of the Exit. Returns `true` for both Success + * and Failure exits. + * + * **Example** (Checking if a value is an Exit) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.isExit(Exit.succeed(42))) // true + * console.log(Exit.isExit(Exit.fail("err"))) // true + * console.log(Exit.isExit("not an exit")) // false + * ``` + * + * @see {@link isSuccess} to check for a successful Exit + * @see {@link isFailure} to check for a failed Exit + * + * @category guards + * @since 2.0.0 + */ +export const isExit: (u: unknown) => u is Exit = core.isExit + +/** + * Creates a successful Exit containing the given value. + * + * **When to use** + * + * Use when you need an Exit that contains a known success value. + * + * **Details** + * + * Returns a `Success` with the provided value. Does not perform any + * computation. + * + * **Example** (Creating a successful Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.succeed(42) + * console.log(Exit.isSuccess(exit)) // true + * ``` + * + * @see {@link fail} to create a failed Exit + * @see {@link void_ void} for a pre-allocated success with no value + * + * @category constructors + * @since 2.0.0 + */ +export const succeed: (a: A) => Exit = core.exitSucceed + +/** + * Creates a failed Exit from a Cause. + * + * **When to use** + * + * Use when you already have a `Cause` and want to wrap it in an Exit + * for advanced error handling where you need full control over the Cause + * structure. + * + * **Details** + * + * Returns a `Failure`. If you only have an error value, use + * {@link fail} instead. + * + * **Example** (Creating a failed Exit from a Cause) + * + * ```ts + * import { Cause, Exit } from "effect" + * + * const cause = Cause.fail("Something went wrong") + * const exit = Exit.failCause(cause) + * console.log(Exit.isFailure(exit)) // true + * ``` + * + * @see {@link fail} to create a Failure from a plain error value + * @see {@link die} to create a Failure from a defect + * + * @category constructors + * @since 2.0.0 + */ +export const failCause: (cause: Cause.Cause) => Exit = core.exitFailCause + +/** + * Creates a failed Exit from a typed error value. + * + * **When to use** + * + * Use when you need to represent an expected typed failure as an `Exit`. + * + * **Details** + * + * The error is wrapped in a `Cause.Fail` internally. + * + * Returns a `Failure`. + * + * **Example** (Creating a failed Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.fail("Something went wrong") + * console.log(Exit.isFailure(exit)) // true + * ``` + * + * @see {@link succeed} to create a successful Exit + * @see {@link die} to create a Failure from an unexpected defect + * @see {@link failCause} to create a Failure from a full Cause + * + * @category constructors + * @since 2.0.0 + */ +export const fail: (e: E) => Exit = core.exitFail + +/** + * Creates a failed Exit from a defect (unexpected error). + * + * **When to use** + * + * Use when you need unexpected, unrecoverable errors that should not appear in + * the typed error channel. + * + * **Details** + * + * The defect is wrapped in a `Cause.Die` internally. + * + * Returns a `Failure` with `E = never`, since defects do not appear in + * the typed error channel. + * + * **Example** (Creating a defect Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.die(new Error("Unexpected error")) + * console.log(Exit.isFailure(exit)) // true + * ``` + * + * @see {@link fail} to create a Failure from a typed error + * @see {@link hasDies} to check whether an Exit contains defects + * + * @category constructors + * @since 2.0.0 + */ +export const die: (defect: unknown) => Exit = core.exitDie + +/** + * Creates a failed Exit representing fiber interruption. + * + * **When to use** + * + * Use to signal that a fiber was interrupted. + * + * **Details** + * + * Optionally pass a fiber ID to identify which fiber was interrupted. Returns + * a `Failure` with an `Interrupt` cause. + * + * **Example** (Creating an interruption Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.interrupt(123) + * console.log(Exit.isFailure(exit)) // true + * console.log(Exit.hasInterrupts(exit)) // true + * ``` + * + * @see {@link hasInterrupts} to check whether an Exit contains interruptions + * + * @category constructors + * @since 2.0.0 + */ +export const interrupt: (fiberId?: number | undefined) => Exit = effect.exitInterrupt + +const void_: Exit = effect.exitVoid +export { + /** + * Provides a pre-allocated successful Exit with a `void` value. + * + * **When to use** + * + * Use when you need a shared successful `Exit` with no meaningful value. + * + * **Details** + * + * Equivalent to `Exit.succeed(undefined)` but shared as a single instance, + * avoiding allocation for a common case. + * + * **Example** (Referencing the void Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.void + * console.log(Exit.isSuccess(exit)) // true + * ``` + * + * @see {@link succeed} to create a success with a specific value + * @see {@link asVoid} to discard the value of an existing Exit + * + * @category constructors + * @since 2.0.0 + */ + void_ as void +} + +/** + * Checks whether an Exit is a Success. + * + * **When to use** + * + * Use as a type guard to narrow `Exit` to `Success` and access the + * `value` property. + * + * **Example** (Narrowing to success) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.succeed(42) + * + * if (Exit.isSuccess(exit)) { + * console.log(exit.value) // 42 + * } + * ``` + * + * @see {@link isFailure} for the opposite check + * @see {@link match} for exhaustive pattern matching + * + * @category guards + * @since 2.0.0 + */ +export const isSuccess: (self: Exit) => self is Success = effect.exitIsSuccess + +/** + * Checks whether an Exit is a Failure. + * + * **When to use** + * + * Use as a type guard to narrow `Exit` to `Failure` and access the + * `cause` property. + * + * **Example** (Narrowing to failure) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.fail("error") + * + * if (Exit.isFailure(exit)) { + * console.log(exit.cause) + * } + * ``` + * + * @see {@link isSuccess} for the opposite check + * @see {@link match} for exhaustive pattern matching + * + * @category guards + * @since 2.0.0 + */ +export const isFailure: (self: Exit) => self is Failure = effect.exitIsFailure + +/** + * Checks whether a failed Exit contains typed errors (Fail reasons). + * + * **When to use** + * + * Use to distinguish typed failures from defects or interruptions. + * + * **Details** + * + * Returns `false` for successful exits. Only checks for `Fail` reasons in the + * Cause. A Cause with only `Die` or `Interrupt` reasons returns `false`. + * + * **Example** (Checking for typed errors) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.hasFails(Exit.fail("err"))) // true + * console.log(Exit.hasFails(Exit.die(new Error("bug")))) // false + * console.log(Exit.hasFails(Exit.succeed(42))) // false + * ``` + * + * @see {@link hasDies} to check for defects + * @see {@link hasInterrupts} to check for interruptions + * + * @category guards + * @since 4.0.0 + */ +export const hasFails: (self: Exit) => self is Failure = effect.exitHasFails + +/** + * Checks whether a failed Exit contains defects (Die reasons). + * + * **When to use** + * + * Use to check whether an `Exit` failure cause contains unexpected errors. + * + * **Details** + * + * Returns `false` for successful exits. Only checks for `Die` reasons in the + * Cause. A Cause with only `Fail` or `Interrupt` reasons returns `false`. + * + * **Example** (Checking for defects) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.hasDies(Exit.die(new Error("bug")))) // true + * console.log(Exit.hasDies(Exit.fail("err"))) // false + * console.log(Exit.hasDies(Exit.succeed(42))) // false + * ``` + * + * @see {@link hasFails} to check for typed errors + * @see {@link hasInterrupts} to check for interruptions + * + * @category guards + * @since 4.0.0 + */ +export const hasDies: (self: Exit) => self is Failure = effect.exitHasDies + +/** + * Checks whether a failed Exit contains interruptions (Interrupt reasons). + * + * **When to use** + * + * Use to check whether an `Exit` contains fiber interruption. + * + * **Details** + * + * Returns `false` for successful exits. Only checks for `Interrupt` reasons in + * the Cause. A Cause with only `Fail` or `Die` reasons returns `false`. + * + * **Example** (Checking for interruptions) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.hasInterrupts(Exit.interrupt(1))) // true + * console.log(Exit.hasInterrupts(Exit.fail("err"))) // false + * console.log(Exit.hasInterrupts(Exit.succeed(42))) // false + * ``` + * + * @see {@link hasFails} to check for typed errors + * @see {@link hasDies} to check for defects + * + * @category guards + * @since 4.0.0 + */ +export const hasInterrupts: (self: Exit) => self is Failure = effect.exitHasInterrupts + +/** + * Extracts the Success variant from an Exit as a Result. + * + * **When to use** + * + * Use when composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you want the full Success wrapper. + * + * **Details** + * + * Returns `Result.succeed(success)` when the Exit is a Success, or + * `Result.fail(failure)` with the original Failure otherwise. + * + * **Gotchas** + * + * This is not an `Option` accessor or an Effect failure. A failed extraction is + * represented as data in the `Result` failure channel. + * + * **Example** (Filtering for success) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.succeed(42) + * const result = Exit.filterSuccess(exit) + * + * console.log(Result.isSuccess(result)) // true + * ``` + * + * @see {@link filterFailure} for the inverse + * @see {@link filterValue} to extract the raw value instead of the Success object + * + * @category filtering + * @since 4.0.0 + */ +export const filterSuccess: ( + self: Exit +) => Result.Result, Failure> = effect.exitFilterSuccess + +/** + * Extracts the success value from an Exit as a Result. + * + * **When to use** + * + * Use when composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you want the raw success value rather than the Success + * wrapper. + * + * **Details** + * + * Returns `Result.succeed(value)` when the Exit is a Success, or + * `Result.fail(failure)` with the original Failure otherwise. + * + * **Gotchas** + * + * This is not an `Option` accessor or an Effect failure. A failed extraction is + * represented as data in the `Result` failure channel. + * + * **Example** (Filtering for the value) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.succeed(42) + * const result = Exit.filterValue(exit) + * + * console.log(Result.isSuccess(result) && result.success) // 42 + * ``` + * + * @see {@link filterSuccess} to get the full Success object + * @see {@link getSuccess} to get the value as an Option instead + * + * @category filtering + * @since 4.0.0 + */ +export const filterValue: (self: Exit) => Result.Result> = effect.exitFilterValue + +/** + * Extracts the Failure variant from an Exit as a Result. + * + * **When to use** + * + * Use when composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you want the full Failure wrapper. + * + * **Details** + * + * Returns `Result.succeed(failure)` when the Exit is a Failure, or + * `Result.fail(success)` with the original Success otherwise. + * + * **Gotchas** + * + * This is not an `Option` accessor or an Effect failure. A failed extraction is + * represented as data in the `Result` failure channel. + * + * **Example** (Filtering for failure) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.fail("err") + * const result = Exit.filterFailure(exit) + * + * console.log(Result.isSuccess(result)) // true + * ``` + * + * @see {@link filterSuccess} for the inverse + * @see {@link filterCause} to extract the Cause directly + * + * @category filtering + * @since 4.0.0 + */ +export const filterFailure: (self: Exit) => Result.Result, Success> = + effect.exitFilterFailure + +/** + * Extracts the Cause from a failed Exit as a Result. + * + * **When to use** + * + * Use when composing Exit checks with `Filter` or other `Result`-based + * filtering APIs and you want the raw Cause rather than the Failure wrapper. + * + * **Details** + * + * Returns `Result.succeed(cause)` when the Exit is a Failure, or + * `Result.fail(success)` with the original Success otherwise. + * + * **Gotchas** + * + * This is not an `Option` accessor or an Effect failure. A failed extraction is + * represented as data in the `Result` failure channel. + * + * **Example** (Filtering for the cause) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.fail("err") + * const result = Exit.filterCause(exit) + * + * console.log(Result.isSuccess(result)) // true + * ``` + * + * @see {@link filterFailure} to get the full Failure object + * @see {@link getCause} to get the Cause as an Option instead + * + * @category filtering + * @since 4.0.0 + */ +export const filterCause: (self: Exit) => Result.Result, Success> = effect.exitFilterCause + +/** + * Extracts the first typed error value from a failed Exit as a Result. + * + * **When to use** + * + * Use when you need the first typed error from an `Exit` as a `Result` for + * `Filter` or other `Result`-based filtering APIs. + * + * **Details** + * + * Returns `Result.succeed(error)` when the Cause contains a Fail reason, or + * `Result.fail(exit)` with the original Exit otherwise. + * + * **Gotchas** + * + * Only finds the first Fail reason. If the Cause has multiple errors, the rest + * are ignored. + * + * **Example** (Finding the first typed error) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.fail("not found") + * const result = Exit.findError(exit) + * console.log(Result.isSuccess(result) && result.success) // "not found" + * + * const defect = Exit.die(new Error("bug")) + * const noError = Exit.findError(defect) + * console.log(Result.isFailure(noError)) // true + * ``` + * + * @see {@link findErrorOption} to get the error as an Option instead + * @see {@link findDefect} to find defects instead + * + * @category filtering + * @since 4.0.0 + */ +export const findError: (input: Exit) => Result.Result> = effect.exitFindError + +/** + * Extracts the first defect from a failed Exit as a Result. + * + * **When to use** + * + * Use when you need the first defect from an `Exit` as a `Result` for + * `Filter` or other `Result`-based filtering APIs. + * + * **Details** + * + * Returns `Result.succeed(defect)` when the Cause contains a Die reason, or + * `Result.fail(exit)` with the original Exit otherwise. + * + * **Gotchas** + * + * Only finds the first Die reason. If the Cause has multiple defects, the rest + * are ignored. + * + * **Example** (Finding the first defect) + * + * ```ts + * import { Exit, Result } from "effect" + * + * const exit = Exit.die("boom") + * const result = Exit.findDefect(exit) + * console.log(Result.isSuccess(result) && result.success) // "boom" + * + * const typed = Exit.fail("err") + * const noDefect = Exit.findDefect(typed) + * console.log(Result.isFailure(noDefect)) // true + * ``` + * + * @see {@link findError} to find typed errors instead + * @see {@link hasDies} to check for defects without extracting them + * + * @category filtering + * @since 4.0.0 + */ +export const findDefect: (input: Exit) => Result.Result> = effect.exitFindDefect + +/** + * Pattern matches on an Exit, handling both success and failure cases. + * + * **When to use** + * + * Use when you need exhaustive handling of both `Exit` success and failure + * outcomes. + * + * **Details** + * + * Calls `onSuccess` with the value if the Exit is a Success, and calls + * `onFailure` with the Cause if the Exit is a Failure. + * + * **Example** (Matching on an Exit) + * + * ```ts + * import { Exit } from "effect" + * + * const success = Exit.succeed(42) + * + * const result = Exit.match(success, { + * onSuccess: (value) => `Got: ${value}`, + * onFailure: () => "Failed" + * }) + * console.log(result) // "Got: 42" + * ``` + * + * @see {@link isSuccess} and {@link isFailure} for simple boolean checks + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onSuccess: (a: NoInfer) => X1 + readonly onFailure: (cause: Cause.Cause>) => X2 + }): (self: Exit) => X1 | X2 + ( + self: Exit, + options: { + readonly onSuccess: (a: A) => X1 + readonly onFailure: (cause: Cause.Cause) => X2 + } + ): X1 | X2 +} = effect.exitMatch + +/** + * Transforms the success value of an Exit using the given function. + * + * **When to use** + * + * Use to apply a transformation to the value inside a successful Exit + * + * **Details** + * + * Failures pass through unchanged. + * + * Allocates a new Exit if successful. + * + * **Example** (Mapping over a success) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.succeed(21) + * const doubled = Exit.map(exit, (x) => x * 2) + * console.log(Exit.isSuccess(doubled) && doubled.value) // 42 + * ``` + * + * @see {@link mapError} to transform the error + * @see {@link mapBoth} to transform both success and error + * + * @category combinators + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => B): (self: Exit) => Exit + (self: Exit, f: (a: A) => B): Exit +} = effect.exitMap + +/** + * Transforms the typed error of a failed Exit using the given function. + * + * **When to use** + * + * Use to remap typed errors while preserving the Exit structure + * + * **Details** + * + * Successes pass through unchanged. + * + * Allocates a new Exit if the error is transformed. + * + * **Gotchas** + * + * Only transforms typed errors (Fail reasons). If the Cause contains only + * defects or interruptions, the failure passes through unchanged. + * + * **Example** (Mapping over an error) + * + * ```ts + * import { Data, Exit } from "effect" + * + * class ExitError extends Data.TaggedError("ExitError")<{ readonly input: string }> {} + * + * const exit = Exit.fail("bad input") + * const mapped = Exit.mapError(exit, (e) => new ExitError({ input: e })) + * console.log(Exit.isFailure(mapped)) // true + * ``` + * + * @see {@link map} to transform the success value + * @see {@link mapBoth} to transform both success and error + * + * @category combinators + * @since 2.0.0 + */ +export const mapError: { + (f: (a: NoInfer) => E2): (self: Exit) => Exit + (self: Exit, f: (a: NoInfer) => E2): Exit +} = effect.exitMapError + +/** + * Transforms both the success value and typed error of an Exit. + * + * **When to use** + * + * Use when you need to remap both channels in one step. + * + * **Details** + * + * `onSuccess` transforms the value if the Exit is a Success. `onFailure` + * transforms the typed error if the Exit is a Failure with a Fail reason. + * Allocates a new Exit. + * + * **Gotchas** + * + * If the Cause contains only defects or interruptions, the failure passes + * through unchanged. + * + * **Example** (Mapping both channels) + * + * ```ts + * import { Data, Exit } from "effect" + * + * class ExitError extends Data.TaggedError("ExitError")<{ readonly input: string }> {} + * + * const exit = Exit.succeed(42) + * const mapped = Exit.mapBoth(exit, { + * onSuccess: (x) => String(x), + * onFailure: (e: string) => new ExitError({ input: e }) + * }) + * console.log(Exit.isSuccess(mapped) && mapped.value) // "42" + * ``` + * + * @see {@link map} to transform only the success value + * @see {@link mapError} to transform only the error + * + * @category combinators + * @since 2.0.0 + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Exit) => Exit + ( + self: Exit, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Exit +} = effect.exitMapBoth + +/** + * Discards the success value of an Exit, replacing it with `void`. + * + * **When to use** + * + * Use when you need to discard a successful `Exit` value while preserving + * whether the `Exit` succeeded or failed. + * + * **Details** + * + * Failures pass through unchanged. + * + * Allocates a new Exit if successful. + * + * **Example** (Discarding the success value) + * + * ```ts + * import { Exit } from "effect" + * + * const exit = Exit.succeed(42) + * const voided = Exit.asVoid(exit) + * console.log(Exit.isSuccess(voided)) // true + * ``` + * + * @see {@link void_ void} for a pre-allocated void success + * @see {@link asVoidAll} to combine multiple exits into a single void Exit + * + * @category combinators + * @since 2.0.0 + */ +export const asVoid: (self: Exit) => Exit = effect.exitAsVoid + +/** + * Combines multiple Exit values into a single `Exit`. + * + * **When to use** + * + * Use to validate that all exits in a collection succeeded + * + * **Details** + * + * If all exits are successful, this returns a void success. If any exit is a + * failure, this returns a single failure with all error causes combined. + * + * Iterates over the entire collection. Collects all failure causes, not just + * the first. + * + * **Example** (Combining exits) + * + * ```ts + * import { Exit } from "effect" + * + * const exits = [Exit.succeed(1), Exit.succeed(2), Exit.succeed(3)] + * console.log(Exit.isSuccess(Exit.asVoidAll(exits))) // true + * + * const mixed = [Exit.succeed(1), Exit.fail("err"), Exit.succeed(3)] + * console.log(Exit.isFailure(Exit.asVoidAll(mixed))) // true + * ``` + * + * @see {@link asVoid} to discard the value of a single Exit + * + * @category combinators + * @since 4.0.0 + */ +export const asVoidAll: >>( + exits: I +) => Exit> ? _E : never> = effect.exitAsVoidAll + +/** + * Returns the success value of an Exit as an Option. + * + * **When to use** + * + * Use when you need the success value from an `Exit` as an `Option` instead of + * pattern matching. + * + * **Details** + * + * Returns `Option.some(value)` for a Success and `Option.none()` for a Failure. + * + * **Example** (Getting the success value) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.getSuccess(Exit.succeed(42))) // { _tag: "Some", value: 42 } + * console.log(Exit.getSuccess(Exit.fail("err"))) // { _tag: "None" } + * ``` + * + * @see {@link getCause} to extract the Cause of a failure + * @see {@link filterValue} for filter-pipeline usage + * + * @category accessors + * @since 4.0.0 + */ +export const getSuccess: (self: Exit) => Option = effect.exitGetSuccess + +/** + * Returns the Cause of a failed Exit as an Option. + * + * **When to use** + * + * Use when you need the failure `Cause` from an `Exit` as an `Option` instead + * of pattern matching. + * + * **Details** + * + * Returns `Option.some(cause)` for a Failure and `Option.none()` for a Success. + * + * **Example** (Getting the failure cause) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.getCause(Exit.fail("err"))) // { _tag: "Some", value: ... } + * console.log(Exit.getCause(Exit.succeed(42))) // { _tag: "None" } + * ``` + * + * @see {@link getSuccess} to extract the success value + * @see {@link filterCause} for filter-pipeline usage + * + * @category accessors + * @since 4.0.0 + */ +export const getCause: (self: Exit) => Option> = effect.exitGetCause + +/** + * Returns the first typed error from a failed Exit as an Option. + * + * **When to use** + * + * Use when you need the first typed error from an `Exit` as an `Option`, + * ignoring successes and non-typed failures. + * + * **Details** + * + * Returns `Option.some(error)` if the Cause contains a Fail reason. Successes, + * defect-only failures, and interrupt-only failures return `Option.none()`. + * + * **Gotchas** + * + * Only finds the first Fail reason. If the Cause has multiple typed errors, + * the rest are ignored. + * + * **Example** (Getting the first error) + * + * ```ts + * import { Exit } from "effect" + * + * console.log(Exit.findErrorOption(Exit.fail("err"))) // { _tag: "Some", value: "err" } + * console.log(Exit.findErrorOption(Exit.die(new Error("bug")))) // { _tag: "None" } + * console.log(Exit.findErrorOption(Exit.succeed(42))) // { _tag: "None" } + * ``` + * + * @see {@link findError} for filter-pipeline usage + * @see {@link getCause} to get the full Cause as an Option + * + * @category accessors + * @since 4.0.0 + */ +export const findErrorOption: (self: Exit) => Option = effect.exitFindErrorOption diff --git a/repos/effect/packages/effect/src/Fiber.ts b/repos/effect/packages/effect/src/Fiber.ts new file mode 100644 index 0000000..ed60b91 --- /dev/null +++ b/repos/effect/packages/effect/src/Fiber.ts @@ -0,0 +1,632 @@ +/** + * Operations for handles returned by forking effects. A `Fiber` is a + * lightweight execution of an `Effect` that may still be running or may already + * have completed. This module lets callers await or join fiber results, + * interrupt one or many fibers, check unknown values, access the current fiber, + * and attach manually managed fibers to a `Scope` for cleanup. + * + * @since 2.0.0 + */ +import type * as Arr from "./Array.ts" +import type * as Context from "./Context.ts" +import type { Effect } from "./Effect.ts" +import type { Exit } from "./Exit.ts" +import * as effect from "./internal/effect.ts" +import { version } from "./internal/version.ts" +import type { LogLevel } from "./LogLevel.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type { StackFrame } from "./References.ts" +import type { Scheduler, SchedulerDispatcher } from "./Scheduler.ts" +import type { Scope } from "./Scope.ts" +import type { AnySpan } from "./Tracer.ts" +import type { Covariant } from "./Types.ts" + +const TypeId = `~effect/Fiber/${version}` + +/** + * A runtime fiber is a lightweight thread that executes Effects. Fibers are + * the unit of concurrency in Effect. They provide a way to run multiple + * Effects concurrently while maintaining structured concurrency and + * cancellation safety. + * + * **When to use** + * + * Use to observe, join, interrupt, or coordinate work that has already been + * forked. + * + * **Details** + * + * A fiber exposes both safe Effect-based operations, such as {@link await_ await}, + * {@link join}, and {@link interrupt}, and low-level runtime fields used by + * the scheduler and runtime internals. + * + * **Gotchas** + * + * Prefer the exported functions in this module over calling `interruptUnsafe` + * or `pollUnsafe` directly. The unsafe methods are immediate runtime hooks and + * do not provide the same Effect-based sequencing guarantees. + * + * **Example** (Awaiting a forked fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * // Fork an effect to run in a new fiber + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * + * // Wait for the fiber to complete and get its result + * const result = yield* Fiber.await(fiber) + * console.log(result) // Exit.succeed(42) + * + * return result + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Fiber extends Pipeable { + readonly [TypeId]: Fiber.Variance + + readonly id: number + readonly currentOpCount: number + readonly getRef: (ref: Context.Reference) => A + readonly context: Context.Context + setContext(context: Context.Context): void + readonly currentScheduler: Scheduler + readonly currentDispatcher: SchedulerDispatcher + readonly currentSpan?: AnySpan | undefined + readonly currentLogLevel: LogLevel + readonly minimumLogLevel: LogLevel + readonly currentStackFrame?: StackFrame | undefined + readonly maxOpsBeforeYield: number + readonly currentPreventYield: boolean + readonly addObserver: (cb: (exit: Exit) => void) => () => void + readonly interruptUnsafe: ( + fiberId?: number | undefined, + annotations?: Context.Context | undefined + ) => void + readonly pollUnsafe: () => Exit | undefined +} + +/** + * The Fiber namespace contains utility types and functions for working with fibers. + * It provides type-level utilities for fiber operations and variance encoding. + * + * **When to use** + * + * Use to reference type-level helpers associated with `Fiber`. + * + * **Details** + * + * The namespace currently exposes type-level support used by the `Fiber` + * interface. Runtime operations are exported as module-level functions. + * + * **Example** (Working with fiber types) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a fiber + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * + * // Use namespace types for variance + * const typedFiber: Fiber.Fiber = fiber + * + * // Access fiber properties + * console.log(`Fiber ID: ${fiber.id}`) + * + * // Join the fiber + * const result = yield* Fiber.join(fiber) + * return result // 42 + * }) + * ``` + * + * @since 2.0.0 + */ +export declare namespace Fiber { + /** + * Variance encoding for the Fiber type, specifying covariance in both the + * success type `A` and the error type `E`. + * + * **When to use** + * + * Use to carry the success and error type parameters for `Fiber` in Effect's + * type machinery. + * + * **Example** (Upcasting fibers safely) + * + * ```ts + * import type { Fiber } from "effect" + * + * // Variance allows safe subtyping + * declare const fiber: Fiber.Fiber + * const upcast: Fiber.Fiber = fiber + * ``` + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly _A: Covariant + readonly _E: Covariant + } +} + +const await_: (self: Fiber) => Effect> = effect.fiberAwait +export { + /** + * Waits for a fiber to complete and returns its exit value. + * + * **When to use** + * + * Use when you need to inspect whether the fiber succeeded, + * failed, died, or was interrupted without propagating the failure. + * + * **Details** + * + * The returned Effect always succeeds with an `Exit` describing the fiber's + * outcome. + * + * **Gotchas** + * + * This does not flatten the fiber result into the current Effect. Use + * {@link join} when you want fiber failures to fail the current Effect. + * + * **Example** (Awaiting a fiber exit) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * const exit = yield* Fiber.await(fiber) + * console.log(exit) // Exit.succeed(42) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ + await_ as await +} +/** + * Waits for all fibers in the provided iterable to complete and returns + * an array of their exit values. + * + * **When to use** + * + * Use when you need every fiber outcome as data, including failures and + * interruptions. + * + * **Details** + * + * The returned array is ordered like the input iterable. + * + * **Gotchas** + * + * Failures are captured as `Exit.Failure` values. Use {@link joinAll} when you + * want the first failed fiber to fail the returned Effect. + * + * **Example** (Awaiting multiple fiber exits) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber1 = yield* Effect.forkChild(Effect.succeed(1)) + * const fiber2 = yield* Effect.forkChild(Effect.succeed(2)) + * const exits = yield* Fiber.awaitAll([fiber1, fiber2]) + * console.log(exits) // [Exit.succeed(1), Exit.succeed(2)] + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const awaitAll: >( + self: Iterable +) => Effect< + Array< + Exit< + A extends Fiber ? _A : never, + A extends Fiber ? _E : never + > + > +> = effect.fiberAwaitAll + +/** + * Joins a fiber, blocking until it completes. If the fiber succeeds, + * returns its value. If it fails, the error is propagated. + * + * **When to use** + * + * Use when you need a forked fiber's failure to fail the current Effect because + * that fiber is part of the current workflow. + * + * **Gotchas** + * + * Joining a failed fiber propagates the fiber's Cause. Use {@link await_ await} when + * you need to inspect the `Exit` instead of failing. + * + * **Example** (Joining a fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * const result = yield* Fiber.join(fiber) + * console.log(result) // 42 + * }) + * ``` + * + * @see {@link await_ await} for inspecting the fiber outcome as an Exit + * + * @category combinators + * @since 2.0.0 + */ +export const join: (self: Fiber) => Effect = effect.fiberJoin + +/** + * Waits for all fibers to succeed and returns their values in input order. + * + * **When to use** + * + * Use when you need every fiber to succeed and want the successful values + * rather than the `Exit` values. + * + * **Details** + * + * If any fiber fails, the returned `Effect` fails with that fiber's cause and + * stops waiting for additional results. This does not interrupt the remaining + * fibers. + * + * **Gotchas** + * + * A failure stops waiting, but it does not interrupt any other fibers. Use + * {@link interruptAll} separately when remaining fibers should be stopped. + * + * @see {@link awaitAll} for collecting every fiber outcome as an Exit + * + * @category combinators + * @since 2.0.0 + */ +export const joinAll: >>( + self: A +) => Effect< + Arr.ReadonlyArray.With< + A, + A extends Iterable> ? _A : never + >, + A extends Fiber ? _E : never +> = effect.fiberJoinAll + +/** + * Interrupts a fiber, causing it to stop executing and clean up any + * acquired resources. + * + * **When to use** + * + * Use when you need to cancel a forked fiber and wait for its cleanup to + * complete. + * + * **Details** + * + * The returned Effect completes only after the interrupted fiber has completed. + * + * **Gotchas** + * + * Interruption is cooperative. A fiber can continue running while it is inside + * uninterruptible work or finalizers. + * + * **Example** (Interrupting a fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild( + * Effect.delay("1 second")(Effect.succeed(42)) + * ) + * yield* Fiber.interrupt(fiber) + * console.log("Fiber interrupted") + * }) + * ``` + * + * @see {@link interruptAs} for specifying the interrupting fiber ID + * @see {@link await_ await} for observing the interrupted fiber's Exit + * + * @category interruption + * @since 2.0.0 + */ +export const interrupt: (self: Fiber) => Effect = effect.fiberInterrupt + +/** + * Interrupts a fiber with a specific fiber ID as the interruptor. This allows + * tracking which fiber initiated the interruption. + * + * **When to use** + * + * Use when runtime diagnostics or tracing should attribute the interruption to + * a specific fiber ID. + * + * **Details** + * + * The returned Effect completes only after the interrupted fiber has completed. + * + * **Gotchas** + * + * The supplied ID affects the recorded interruptor. It does not make + * interruption synchronous or force uninterruptible regions to stop early. + * + * **Example** (Interrupting a fiber as another fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const targetFiber = yield* Effect.forkChild( + * Effect.delay("5 seconds")(Effect.succeed("task completed")) + * ) + * + * // Interrupt the fiber, specifying fiber ID 123 as the interruptor + * yield* Fiber.interruptAs(targetFiber, 123) + * console.log("Fiber interrupted by fiber #123") + * }) + * ``` + * + * @see {@link interrupt} for using the current fiber as the interruptor + * + * @category interruption + * @since 2.0.0 + */ +export const interruptAs: { + ( + fiberId: number | undefined, + annotations?: Context.Context | undefined + ): (self: Fiber) => Effect + ( + self: Fiber, + fiberId: number | undefined, + annotations?: Context.Context | undefined + ): Effect +} = effect.fiberInterruptAs + +/** + * Interrupts all fibers in the provided iterable, causing them to stop executing + * and clean up any acquired resources. + * + * **When to use** + * + * Use when you need to cancel several forked fibers and wait for their cleanup + * to complete. + * + * **Details** + * + * The current fiber is recorded as the interruptor. The returned Effect + * completes only after all interrupted fibers have completed. + * + * **Gotchas** + * + * Interruption is cooperative for each fiber. The returned Effect can wait for + * uninterruptible work and finalizers in any interrupted fiber. + * + * **Example** (Interrupting multiple fibers) + * + * ```ts + * import { Console, Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * // Create multiple long-running fibers + * const fiber1 = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("5 seconds") + * yield* Console.log("Task 1 completed") + * return "result1" + * }) + * ) + * + * const fiber2 = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("3 seconds") + * yield* Console.log("Task 2 completed") + * return "result2" + * }) + * ) + * + * const fiber3 = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("4 seconds") + * yield* Console.log("Task 3 completed") + * return "result3" + * }) + * ) + * + * // Wait a bit, then interrupt all fibers + * yield* Effect.sleep("1 second") + * yield* Console.log("Interrupting all fibers...") + * yield* Fiber.interruptAll([fiber1, fiber2, fiber3]) + * yield* Console.log("All fibers have been interrupted") + * }) + * ``` + * + * @see {@link interruptAllAs} for specifying the interrupting fiber ID + * + * @category interruption + * @since 2.0.0 + */ +export const interruptAll: >>( + fibers: A +) => Effect = effect.fiberInterruptAll + +/** + * Interrupts all fibers in the provided iterable using the specified fiber ID as the + * interrupting fiber. This allows you to control which fiber is considered the source + * of the interruption, which can be useful for debugging and tracing. + * + * **When to use** + * + * Use to interrupt several fibers while recording a specific fiber ID as the + * interruptor. + * + * **Details** + * + * The returned Effect completes only after all interrupted fibers have + * completed. + * + * **Gotchas** + * + * The supplied ID affects the recorded interruptor. It does not make + * interruption synchronous or force uninterruptible regions to stop early. + * + * **Example** (Interrupting multiple fibers as another fiber) + * + * ```ts + * import { Console, Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a controlling fiber + * const controllerFiber = yield* Effect.forkChild(Effect.succeed("controller")) + * + * // Create multiple worker fibers + * const worker1 = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("5 seconds") + * yield* Console.log("Worker 1 completed") + * return "worker1" + * }) + * ) + * + * const worker2 = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* Effect.sleep("3 seconds") + * yield* Console.log("Worker 2 completed") + * return "worker2" + * }) + * ) + * + * // Interrupt all workers using the controller fiber's ID + * yield* Effect.sleep("1 second") + * yield* Console.log("Interrupting workers from controller...") + * yield* Fiber.interruptAllAs([worker1, worker2], controllerFiber.id) + * yield* Console.log("All workers interrupted by controller") + * }) + * ``` + * + * @see {@link interruptAll} for using the current fiber as the interruptor + * + * @category interruption + * @since 2.0.0 + */ +export const interruptAllAs: { + (fiberId: number): >>(fibers: A) => Effect + >>(fibers: A, fiberId: number): Effect +} = effect.fiberInterruptAllAs + +/** + * Checks whether a value is a Fiber. This is a type guard that can be used to + * determine if an unknown value is a Fiber instance. + * + * **When to use** + * + * Use when checking values at boundaries where an unknown value may be a + * runtime fiber. + * + * **Details** + * + * The check looks for the internal Fiber type ID marker and does not inspect + * the fiber's current state. + * + * **Example** (Checking for fibers) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a fiber + * const fiber = yield* Effect.forkChild(Effect.succeed(42)) + * + * // Test if values are fibers + * console.log(Fiber.isFiber(fiber)) // true + * console.log(Fiber.isFiber("hello")) // false + * console.log(Fiber.isFiber(42)) // false + * console.log(Fiber.isFiber(null)) // false + * + * // Use as a type guard + * const maybeValue: unknown = fiber + * if (Fiber.isFiber(maybeValue)) { + * // TypeScript knows maybeValue is a Fiber here + * console.log(`Fiber ID: ${maybeValue.id}`) + * } + * }) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isFiber = ( + u: unknown +): u is Fiber => hasProperty(u, effect.FiberTypeId) + +/** + * Returns the current fiber if called from within a fiber context, + * otherwise returns `undefined`. + * + * **When to use** + * + * Use when you need low-level runtime integrations that need access to the currently + * executing fiber. + * + * **Gotchas** + * + * This is a synchronous accessor, not an Effect. It returns `undefined` outside + * an active fiber runtime context. + * + * **Example** (Getting the current fiber) + * + * ```ts + * import { Effect, Fiber } from "effect" + * + * const program = Effect.gen(function*() { + * const current = Fiber.getCurrent() + * if (current) { + * console.log(`Current fiber ID: ${current.id}`) + * } + * }) + * ``` + * + * @category accessors + * @since 4.0.0 + */ +export const getCurrent: () => Fiber | undefined = effect.getCurrentFiber + +/** + * Adds a fiber to a `Scope` and returns the same fiber. + * + * **When to use** + * + * Use when a manually managed fiber should be interrupted when a Scope closes. + * + * **Details** + * + * When the scope is closed, the fiber is interrupted. If the scope is already + * closed, the fiber is interrupted immediately. + * + * **Gotchas** + * + * This does not wait for the fiber to complete. It only registers the + * interruption finalizer and returns the same fiber. + * + * @see {@link interrupt} for interrupting and waiting for completion + * + * @category resource management + * @since 4.0.0 + */ +export const runIn: { + (scope: Scope): (self: Fiber) => Fiber + (self: Fiber, scope: Scope): Fiber +} = effect.fiberRunIn diff --git a/repos/effect/packages/effect/src/FiberHandle.ts b/repos/effect/packages/effect/src/FiberHandle.ts new file mode 100644 index 0000000..fe1b3f8 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberHandle.ts @@ -0,0 +1,815 @@ +/** + * Manages at most one fiber inside a scope. + * + * A `FiberHandle` can hold one `Fiber`. Installing a new fiber + * interrupts the previous one unless the operation is configured with + * `onlyIfMissing`, and closing the owning scope interrupts the current fiber. + * This module includes constructors for handles and scoped runtimes, helpers + * for setting, reading, clearing, and running fibers, and operations for joining + * the current fiber or waiting until the handle is empty. + * + * @since 2.0.0 + */ +import * as Cause from "./Cause.ts" +import type { Context } from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import * as Filter from "./Filter.ts" +import { dual } from "./Function.ts" +import type * as Inspectable from "./Inspectable.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type { Scheduler } from "./Scheduler.ts" +import type * as Scope from "./Scope.ts" + +const TypeId = "~effect/FiberHandle" + +/** + * Scoped handle that manages at most one fiber, interrupts the current fiber + * when the handle's scope closes, and removes managed fibers from the handle + * when they complete. + * + * **Example** (Managing a single fiber) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * // Create a FiberHandle that can hold fibers producing strings + * const handle = yield* FiberHandle.make() + * + * // The handle can store and manage a single fiber + * const fiber = yield* FiberHandle.run(handle, Effect.succeed("hello")) + * const result = yield* Fiber.await(fiber) + * console.log(result) // "hello" + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface FiberHandle extends Pipeable, Inspectable.Inspectable { + readonly [TypeId]: typeof TypeId + readonly deferred: Deferred.Deferred + state: { + readonly _tag: "Open" + fiber: Fiber.Fiber | undefined + } | { + readonly _tag: "Closed" + } +} + +/** + * Returns `true` if a value is a `FiberHandle` by checking for the + * `FiberHandle` runtime marker. + * + * **Example** (Checking fiber handles) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * console.log(FiberHandle.isFiberHandle(handle)) // true + * console.log(FiberHandle.isFiberHandle("not a handle")) // false + * }) + * ``` + * + * @category refinements + * @since 2.0.0 + */ +export const isFiberHandle = (u: unknown): u is FiberHandle => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + ...PipeInspectableProto, + toJSON(this: FiberHandle) { + return { + _id: "FiberHandle", + state: this.state + } + } +} + +const makeUnsafe = (): FiberHandle => { + const self = Object.create(Proto) + self.state = { _tag: "Open", fiber: undefined } + self.deferred = Deferred.makeUnsafe() + return self +} + +/** + * Creates a scoped `FiberHandle` that can store a single fiber. + * + * **Details** + * + * When the associated `Scope` is closed, the contained fiber will be + * interrupted. You can add a fiber to the handle using `FiberHandle.run`, and + * the fiber will be automatically removed from the `FiberHandle` when it + * completes. + * + * **Example** (Creating a scoped fiber handle) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // run some effects + * yield* FiberHandle.run(handle, Effect.never) + * // this will interrupt the previous fiber + * yield* FiberHandle.run(handle, Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fiber will be interrupted when the scope is closed + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.sync(() => makeUnsafe()), + (handle) => { + const state = handle.state + if (state._tag === "Closed") return Effect.void + handle.state = { _tag: "Closed" } + return state.fiber ? + Deferred.into( + Effect.asVoid(Fiber.interruptAs(state.fiber, internalFiberId)), + handle.deferred + ) : + Deferred.done(handle.deferred, Exit.void) + } + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberHandle`. + * + * **Details** + * + * Each call returns the forked fiber, stores it in the handle, and interrupts + * the previous fiber unless `onlyIfMissing` is set. The managed fiber is + * interrupted when the handle's scope closes. + * + * **Example** (Running effects with a fiber handle) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const run = yield* FiberHandle.makeRuntime() + * + * // Run effects and get fibers back + * const fiberA = run(Effect.succeed("first")) + * const fiberB = run(Effect.succeed("second")) + * + * // The second fiber will interrupt the first + * const resultA = yield* Fiber.await(fiberA) + * const resultB = yield* Fiber.await(fiberB) + * }).pipe(Effect.scoped) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeRuntime = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: + | { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Fiber.Fiber, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberHandle` + * and returns a `Promise` for each effect result. + * + * **When to use** + * + * Use when integrating a scoped `FiberHandle` runner with Promise-based APIs + * and Promise rejection from squashed failures is the desired boundary. + * + * **Details** + * + * Each call stores the fiber in the handle and interrupts the previous fiber + * unless `onlyIfMissing` is set. The returned Promise resolves with the + * effect's success value or rejects with the squashed failure cause. + * + * **Example** (Running effects as promises) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const run = yield* FiberHandle.makeRuntimePromise() + * + * // Run effects and get promises back + * const promise = run(Effect.succeed("hello")) + * const result = yield* Effect.promise(() => promise) + * console.log(result) // "hello" + * }).pipe(Effect.scoped) + * ``` + * + * @category constructors + * @since 3.13.0 + */ +export const makeRuntimePromise = (): Effect.Effect< + ( + effect: Effect.Effect, + options?: { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ) => Promise, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberId = -1 +const isInternalInterruption = Filter.toPredicate(Filter.compose( + Cause.filterInterruptors, + Filter.has(internalFiberId) +)) + +/** + * Sets the fiber in a FiberHandle. When the fiber completes, it will be removed from the FiberHandle. + * If a fiber is already running, it will be interrupted unless `options.onlyIfMissing` is set. + * + * **When to use** + * + * Use when an existing forked fiber must be installed synchronously into a + * handle and immediate interruption of replaced or closed fibers is acceptable. + * + * **Example** (Setting a fiber unsafely) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * const fiber = Effect.runFork(Effect.succeed("hello")) + * + * // Set the fiber directly (unsafe) + * FiberHandle.setUnsafe(handle, fiber) + * + * // The fiber is now managed by the handle + * const result = yield* Fiber.await(fiber) + * console.log(result) // "hello" + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const setUnsafe: { + ( + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + ): (self: FiberHandle) => void + ( + self: FiberHandle, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + ): void +} = dual((args) => isFiberHandle(args[0]), ( + self: FiberHandle, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } +): void => { + if (self.state._tag === "Closed") { + fiber.interruptUnsafe(internalFiberId) + return + } else if (self.state.fiber !== undefined) { + if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) + return + } else if (self.state.fiber === fiber) { + return + } + self.state.fiber.interruptUnsafe(internalFiberId) + self.state.fiber = undefined + } + + self.state.fiber = fiber + fiber.addObserver((exit) => { + if (self.state._tag === "Open" && fiber === self.state.fiber) { + self.state.fiber = undefined + } + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.hasInterruptsOnly(exit.cause) + ) + ) { + Deferred.doneUnsafe(self.deferred, exit as any) + } + }) +}) + +/** + * Sets the fiber in the `FiberHandle`. + * + * **Details** + * + * When the fiber completes, it will be removed from the `FiberHandle`. If a + * fiber already exists in the `FiberHandle`, it will be interrupted unless + * `options.onlyIfMissing` is set. + * + * **Example** (Setting a fiber safely) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * const fiber = Effect.runFork(Effect.succeed("hello")) + * + * // Set the fiber safely + * yield* FiberHandle.set(handle, fiber) + * + * // The fiber is now managed by the handle + * const result = yield* Fiber.await(fiber) + * console.log(result) // "hello" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const set: { + ( + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): (self: FiberHandle) => Effect.Effect + ( + self: FiberHandle, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } + ): Effect.Effect +} = dual((args) => isFiberHandle(args[0]), ( + self: FiberHandle, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } +): Effect.Effect => + Effect.sync(() => + setUnsafe(self, fiber, { + onlyIfMissing: options?.onlyIfMissing, + propagateInterruption: options?.propagateInterruption + }) + )) + +/** + * Retrieves the fiber from the FiberHandle synchronously. + * + * **When to use** + * + * Use when synchronous inspection of the current fiber is needed and an + * `Option` result is enough outside the Effect workflow. + * + * **Example** (Reading the current fiber unsafely) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // No fiber initially + * const emptyFiber = FiberHandle.getUnsafe(handle) + * console.log(emptyFiber._tag === "None") // true + * + * // Add a fiber + * yield* FiberHandle.run(handle, Effect.succeed("hello")) + * const fiber = FiberHandle.getUnsafe(handle) + * console.log(fiber._tag === "Some") // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export function getUnsafe(self: FiberHandle): Option.Option> { + return self.state._tag === "Closed" ? Option.none() : Option.fromUndefinedOr(self.state.fiber) +} + +/** + * Retrieves the fiber from the FiberHandle effectfully. + * + * **Example** (Reading the current fiber) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // Add a fiber + * yield* FiberHandle.run(handle, Effect.succeed("hello")) + * + * // Get the current fiber if present + * const fiber = yield* FiberHandle.get(handle) + * if (fiber._tag === "Some") { + * const result = yield* Fiber.await(fiber.value) + * console.log(result) // "hello" + * } + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export function get(self: FiberHandle): Effect.Effect>> { + return Effect.suspend(() => Effect.succeed(getUnsafe(self))) +} + +/** + * Interrupts the fiber currently stored in the `FiberHandle`, if any, and + * leaves the handle empty. + * + * **Example** (Clearing a fiber handle) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // Add a fiber + * yield* FiberHandle.run(handle, Effect.never) + * + * // Clear the handle, interrupting the fiber + * yield* FiberHandle.clear(handle) + * + * // The handle is now empty + * const fiber = FiberHandle.getUnsafe(handle) + * console.log(fiber) // Option.none() + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const clear = (self: FiberHandle): Effect.Effect => + Effect.uninterruptibleMask((restore) => { + if (self.state._tag === "Closed" || self.state.fiber === undefined) { + return Effect.void + } + return Effect.andThen( + restore(Fiber.interruptAs(self.state.fiber, internalFiberId)), + Effect.sync(() => { + if (self.state._tag === "Open") { + self.state.fiber = undefined + } + }) + ) + }) + +const constInterruptedFiber = (function() { + let fiber: Fiber.Fiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Forks an Effect and stores the resulting fiber in the `FiberHandle`. + * + * **Details** + * + * The handle manages only one fiber: running a new effect interrupts the + * previous fiber unless `onlyIfMissing` is set. When the managed fiber + * completes, it is removed from the handle. + * + * **Example** (Running an effect in a fiber handle) + * + * ```ts + * import { Effect, Fiber, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // Run an effect and get the fiber + * const fiber = yield* FiberHandle.run(handle, Effect.succeed("hello")) + * const result = yield* Fiber.await(fiber) + * console.log(result) // "hello" + * + * // Running another effect will interrupt the previous one + * const fiber2 = yield* FiberHandle.run(handle, Effect.succeed("world")) + * const result2 = yield* Fiber.await(fiber2) + * console.log(result2) // "world" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const run: { + ( + self: FiberHandle, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberHandle, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] as FiberHandle + if (Effect.isEffect(arguments[1])) { + return runImpl(self, arguments[1], arguments[2]) as any + } + const options = arguments[1] + return (effect: Effect.Effect) => runImpl(self, effect, options) +} + +const runImpl = ( + self: FiberHandle, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean | undefined + } +): Effect.Effect, never, R> => + Effect.withFiber((parent) => { + if (self.state._tag === "Closed") { + return Effect.interrupt + } else if (self.state.fiber !== undefined && options?.onlyIfMissing === true) { + return Effect.sync(constInterruptedFiber) + } + const fiber = Effect.runForkWith(parent.context as Context)(effect) + setUnsafe(self, fiber, options) + return Effect.succeed(fiber) + }) + +/** + * Captures the current runtime and returns a function for forking effects into + * an existing `FiberHandle`. + * + * **Details** + * + * Each call returns the forked fiber, stores it in the handle, and interrupts + * the previous fiber unless `onlyIfMissing` is set. + * + * **Example** (Capturing a runtime for fiber handles) + * + * ```ts + * import { Context, Effect, FiberHandle } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.Service> + * }>("Users") + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * const run = yield* FiberHandle.runtime(handle)() + * + * // run an effect and set the fiber in the handle + * run(Effect.andThen(Users, (_) => _.getAll)) + * + * // this will interrupt the previous fiber + * run(Effect.andThen(Users, (_) => _.getAll)) + * }).pipe( + * Effect.scoped // The fiber will be interrupted when the scope is closed + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const runtime: ( + self: FiberHandle +) => () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Fiber.Fiber, + never, + R +> = (self: FiberHandle) => () => + Effect.map( + Effect.context(), + (services) => { + const runFork = Effect.runForkWith(services) + return ( + effect: Effect.Effect, + options?: + | { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } else if (self.state.fiber !== undefined && options?.onlyIfMissing === true) { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + setUnsafe(self, fiber, options) + return fiber + } + } + ) + +/** + * Captures the current runtime and returns a function for running effects in + * an existing `FiberHandle` as Promises. + * + * **Details** + * + * Each call stores the forked fiber in the handle and interrupts the previous + * fiber unless `onlyIfMissing` is set. The Promise resolves with the effect's + * success value or rejects with the squashed failure cause. + * + * **Example** (Capturing a runtime for promises) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * const runPromise = yield* FiberHandle.runtimePromise(handle)() + * + * // Run an effect and get a promise + * const promise = runPromise(Effect.succeed("hello")) + * const result = yield* Effect.promise(() => promise) + * console.log(result) // "hello" + * }) + * ``` + * + * @category combinators + * @since 3.13.0 + */ +export const runtimePromise = (self: FiberHandle): () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + effect: Effect.Effect, + options?: + | { + readonly signal?: AbortSignal | undefined + readonly scheduler?: Scheduler | undefined + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * Waits for the `FiberHandle` to fail or close. + * + * **Details** + * + * The returned Effect fails with the first managed fiber failure that is not + * ignored by the handle's interruption rules. Normal successful completion of + * a managed fiber only removes it from the handle; use `awaitEmpty` to wait + * for the current fiber to finish. + * + * **Example** (Propagating fiber failures) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * yield* FiberHandle.set(handle, Effect.runFork(Effect.fail("error"))) + * + * // parent fiber will fail with "error" + * yield* FiberHandle.join(handle) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const join = (self: FiberHandle): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Waits for the fiber in the FiberHandle to complete. + * + * **Example** (Waiting for a fiber to complete) + * + * ```ts + * import { Effect, FiberHandle } from "effect" + * + * Effect.gen(function*() { + * const handle = yield* FiberHandle.make() + * + * // Start a long-running effect + * yield* FiberHandle.run(handle, Effect.sleep(1000)) + * + * // Wait for the fiber to complete + * yield* FiberHandle.awaitEmpty(handle) + * + * console.log("Fiber completed") + * }) + * ``` + * + * @category combinators + * @since 3.13.0 + */ +export const awaitEmpty = (self: FiberHandle): Effect.Effect => + Effect.suspend(() => { + if (self.state._tag === "Closed" || self.state.fiber === undefined) { + return Effect.void + } + return Fiber.await(self.state.fiber) + }) diff --git a/repos/effect/packages/effect/src/FiberMap.ts b/repos/effect/packages/effect/src/FiberMap.ts new file mode 100644 index 0000000..9684132 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberMap.ts @@ -0,0 +1,1030 @@ +/** + * Manages fibers by key inside a scope. + * + * A `FiberMap` owns a map of running fibers, interrupts them when its + * scope closes, and automatically removes each entry when the corresponding + * fiber completes. Use it when a program needs to start, replace, join, or + * interrupt background work by a stable key while keeping all fibers tied to + * one scope. + * + * @since 2.0.0 + */ +import * as Cause from "./Cause.ts" +import type { Context } from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import * as Filter from "./Filter.ts" +import { constVoid, dual } from "./Function.ts" +import type * as Inspectable from "./Inspectable.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Iterable from "./Iterable.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type * as Scope from "./Scope.ts" + +const TypeId = "~effect/FiberMap" + +/** + * A FiberMap is a collection of fibers, indexed by a key. When the associated + * Scope is closed, all fibers in the map will be interrupted. Fibers are + * automatically removed from the map when they complete. + * + * **Example** (Managing fibers in a map) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * // Create a FiberMap with string keys + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add some fibers to the map + * yield* FiberMap.run(map, "task1", Effect.never) + * yield* FiberMap.run(map, "task2", Effect.never) + * + * // Get the size of the map + * const size = yield* FiberMap.size(map) + * console.log(size) // 2 + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface FiberMap + extends Pipeable, Inspectable.Inspectable, Iterable<[K, Fiber.Fiber]> +{ + readonly [TypeId]: typeof TypeId + readonly deferred: Deferred.Deferred + state: { + readonly _tag: "Open" + readonly backing: MutableHashMap.MutableHashMap> + } | { + readonly _tag: "Closed" + } +} + +/** + * Returns `true` if a value is a `FiberMap`. + * + * **Details** + * + * This is a type guard that checks for the `FiberMap` runtime marker. + * + * **Example** (Checking if a value is a FiberMap) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * console.log(FiberMap.isFiberMap(map)) // true + * console.log(FiberMap.isFiberMap({})) // false + * console.log(FiberMap.isFiberMap(null)) // false + * }) + * ``` + * + * @category refinements + * @since 2.0.0 + */ +export const isFiberMap = (u: unknown): u is FiberMap => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + [Symbol.iterator](this: FiberMap) { + if (this.state._tag === "Closed") { + return Iterable.empty() + } + return this.state.backing[Symbol.iterator]() + }, + ...PipeInspectableProto, + toJSON(this: FiberMap) { + return { + _id: "FiberMap", + state: this.state + } + } +} + +const makeUnsafe = ( + backing: MutableHashMap.MutableHashMap>, + deferred: Deferred.Deferred +): FiberMap => { + const self = Object.create(Proto) + self.state = { _tag: "Open", backing } + self.deferred = deferred + return self +} + +/** + * Creates a scoped `FiberMap` for storing fibers by key. + * + * **Details** + * + * When the associated Scope is closed, all fibers in the map will be + * interrupted. You can add fibers to the map using `FiberMap.set` or + * `FiberMap.run`, and the fibers will be automatically removed from the + * `FiberMap` when they complete. + * + * **Example** (Creating a scoped FiberMap) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // run some effects and add the fibers to the map + * yield* FiberMap.run(map, "fiber a", Effect.never) + * yield* FiberMap.run(map, "fiber b", Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.sync(() => + makeUnsafe( + MutableHashMap.empty(), + Deferred.makeUnsafe() + ) + ), + (map) => + Effect.suspend(() => { + const state = map.state + if (state._tag === "Closed") return Effect.void + map.state = { _tag: "Closed" } + return Fiber.interruptAll(MutableHashMap.values(state.backing)).pipe( + Deferred.into(map.deferred) + ) + }) + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberMap`. + * + * **Details** + * + * Each call stores the forked fiber under the supplied key and returns that + * fiber. If the key already has a fiber, the previous fiber is interrupted + * unless `onlyIfMissing` is set. All managed fibers are interrupted when the + * map's scope closes. + * + * **Example** (Creating a scoped runtime) + * + * ```ts + * import { Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const run = yield* FiberMap.makeRuntime() + * + * // Run effects and get back fibers + * const fiber1 = run("task1", Effect.succeed("Hello")) + * const fiber2 = run("task2", Effect.succeed("World")) + * + * // Join the fibers to get their successful values + * const result1 = yield* Fiber.join(fiber1) + * const result2 = yield* Fiber.join(fiber2) + * + * console.log(result1, result2) // "Hello", "World" + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeRuntime = (): Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onlyIfMissing?: boolean | undefined + } + | undefined + ) => Fiber.Fiber, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberMap` and + * returns a `Promise` for each effect result. + * + * **When to use** + * + * Use when keyed fibers must be managed in a scoped map while exposing their + * results through Promise-based APIs. + * + * **Details** + * + * Each call stores the fiber under the supplied key, interrupting any previous + * fiber for that key unless `onlyIfMissing` is set. The returned Promise + * resolves with the effect's success value or rejects with the squashed failure + * cause. + * + * **Example** (Creating a promise runtime) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const run = yield* FiberMap.makeRuntimePromise() + * + * // Run effects and get back promises + * const promise1 = run("task1", Effect.succeed("Hello")) + * const promise2 = run("task2", Effect.succeed("World")) + * + * // Convert to Effect and await + * const result1 = yield* Effect.promise(() => promise1) + * const result2 = yield* Effect.promise(() => promise2) + * + * console.log(result1, result2) // "Hello", "World" + * }) + * ``` + * + * @category constructors + * @since 3.13.0 + */ +export const makeRuntimePromise = (): Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onlyIfMissing?: boolean | undefined + } + | undefined + ) => Promise, + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberId = -1 +const isInternalInterruption = Filter.toPredicate(Filter.compose( + Cause.filterInterruptors, + Filter.has(internalFiberId) +)) + +/** + * Adds a fiber to the `FiberMap` under a key using a synchronous, unsafe + * mutation. + * + * **When to use** + * + * Use when an existing forked fiber must be installed under a key immediately + * and synchronous interruption of the replaced fiber is acceptable. + * + * **Details** + * + * When the fiber completes, it is removed from the map. If the key already has + * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; + * in that case the new fiber is interrupted and the existing entry is kept. + * + * **Example** (Adding a fiber unsafely) + * + * ```ts + * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const deferred = yield* Deferred.make() + * + * // Create a fiber and add it to the map + * const fiber = yield* Effect.forkChild(Deferred.await(deferred)) + * FiberMap.setUnsafe(map, "greeting", fiber) + * + * yield* Deferred.succeed(deferred, "Hello") + * + * // Join the fiber to get its successful value + * const result = yield* Fiber.join(fiber) + * console.log(result) // "Hello" + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const setUnsafe: { + ( + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberMap) => void + ( + self: FiberMap, + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): void +} = dual((args) => isFiberMap(args[0]), ( + self: FiberMap, + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined +): void => { + if (self.state._tag === "Closed") { + fiber.interruptUnsafe(internalFiberId) + return + } + + const previous = MutableHashMap.get(self.state.backing, key) + if (previous._tag === "Some") { + if (options?.onlyIfMissing === true) { + fiber.interruptUnsafe(internalFiberId) + return + } else if (previous.value === fiber) { + return + } + previous.value.interruptUnsafe(internalFiberId) + } + + MutableHashMap.set(self.state.backing, key, fiber) + fiber.addObserver((exit) => { + if (self.state._tag === "Closed") { + return + } + const current = MutableHashMap.get(self.state.backing, key) + if (Option.isSome(current) && fiber === current.value) { + MutableHashMap.remove(self.state.backing, key) + } + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.hasInterruptsOnly(exit.cause) + ) + ) { + Deferred.doneUnsafe(self.deferred, exit as any) + } + }) +}) + +/** + * Adds a fiber to the `FiberMap` under a key. + * + * **Details** + * + * When the fiber completes, it is removed from the map. If the key already has + * a fiber, that previous fiber is interrupted unless `onlyIfMissing` is set; + * in that case the new fiber is interrupted and the existing entry is kept. + * + * This is the Effect-wrapped version of `setUnsafe`. + * + * **Example** (Adding a fiber) + * + * ```ts + * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const deferred = yield* Deferred.make() + * + * // Create a fiber and add it to the map using Effect + * const fiber = yield* Effect.forkChild(Deferred.await(deferred)) + * yield* FiberMap.set(map, "greeting", fiber) + * + * yield* Deferred.succeed(deferred, "Hello") + * + * // Join the fiber to get its successful value + * const result = yield* Fiber.join(fiber) + * console.log(result) // "Hello" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const set: { + ( + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberMap) => Effect.Effect + ( + self: FiberMap, + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual((args) => isFiberMap(args[0]), ( + self: FiberMap, + key: K, + fiber: Fiber.Fiber, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } | undefined +): Effect.Effect => Effect.sync(() => setUnsafe(self, key, fiber, options))) + +/** + * Retrieves a fiber from the FiberMap synchronously. + * + * **When to use** + * + * Use when synchronous keyed lookup of a fiber in a `FiberMap` is needed and an + * `Option` result is enough outside the Effect workflow. + * + * **Example** (Retrieving a fiber unsafely) + * + * ```ts + * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const deferred = yield* Deferred.make() + * + * // Add a fiber to the map + * const fiber = yield* Effect.forkChild(Deferred.await(deferred)) + * FiberMap.setUnsafe(map, "greeting", fiber) + * + * // Retrieve the fiber + * const retrieved = FiberMap.getUnsafe(map, "greeting") + * if (retrieved._tag === "Some") { + * yield* Deferred.succeed(deferred, "Hello") + * + * const result = yield* Fiber.join(retrieved.value) + * console.log(result) // "Hello" + * } + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const getUnsafe: { + (key: K): (self: FiberMap) => Option.Option> + (self: FiberMap, key: K): Option.Option> +} = dual( + 2, + (self: FiberMap, key: K): Option.Option> => { + return self.state._tag === "Closed" ? Option.none() : MutableHashMap.get(self.state.backing, key) + } +) + +/** + * Retrieves a fiber from the FiberMap effectfully. + * + * **Details** + * + * Returns an `Option` wrapped in `Effect`. + * + * **Example** (Retrieving a fiber) + * + * ```ts + * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const deferred = yield* Deferred.make() + * + * // Add a fiber to the map + * const fiber = yield* Effect.forkChild(Deferred.await(deferred)) + * yield* FiberMap.set(map, "greeting", fiber) + * + * // Retrieve the fiber with error handling + * const retrieved = yield* FiberMap.get(map, "greeting") + * if (retrieved._tag === "Some") { + * yield* Deferred.succeed(deferred, "Hello") + * + * const result = yield* Fiber.join(retrieved.value) + * console.log(result) // "Hello" + * } + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const get: { + (key: K): (self: FiberMap) => Effect.Effect>> + (self: FiberMap, key: K): Effect.Effect>> +} = dual( + 2, + (self: FiberMap, key: K): Effect.Effect>> => + Effect.suspend(() => Effect.succeed(getUnsafe(self, key))) +) + +/** + * Checks whether a key exists in the FiberMap. + * + * **Example** (Checking if a key exists unsafely) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add a fiber to the map + * yield* FiberMap.run(map, "task1", Effect.never) + * + * // Check if keys exist + * console.log(FiberMap.hasUnsafe(map, "task1")) // true + * console.log(FiberMap.hasUnsafe(map, "task2")) // false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const hasUnsafe: { + (key: K): (self: FiberMap) => boolean + (self: FiberMap, key: K): boolean +} = dual( + 2, + (self: FiberMap, key: K): boolean => + self.state._tag === "Closed" ? false : MutableHashMap.has(self.state.backing, key) +) + +/** + * Checks whether a key exists in the FiberMap. + * This is the Effect-wrapped version of `hasUnsafe`. + * + * **Example** (Checking if a key exists) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add a fiber to the map + * yield* FiberMap.run(map, "task1", Effect.never) + * + * // Check if keys exist using Effect + * const exists1 = yield* FiberMap.has(map, "task1") + * const exists2 = yield* FiberMap.has(map, "task2") + * + * console.log(exists1) // true + * console.log(exists2) // false + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const has: { + (key: K): (self: FiberMap) => Effect.Effect + (self: FiberMap, key: K): Effect.Effect +} = dual( + 2, + (self: FiberMap, key: K): Effect.Effect => Effect.sync(() => hasUnsafe(self, key)) +) + +/** + * Removes a fiber from the FiberMap, interrupting it if it exists. + * + * **Example** (Removing a fiber) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add some fibers to the map + * yield* FiberMap.run(map, "task1", Effect.never) + * yield* FiberMap.run(map, "task2", Effect.never) + * + * console.log(yield* FiberMap.size(map)) // 2 + * + * // Remove a specific fiber (this will interrupt it) + * yield* FiberMap.remove(map, "task1") + * + * console.log(yield* FiberMap.size(map)) // 1 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const remove: { + (key: K): (self: FiberMap) => Effect.Effect + (self: FiberMap, key: K): Effect.Effect +} = dual< + ( + key: K + ) => (self: FiberMap) => Effect.Effect, + ( + self: FiberMap, + key: K + ) => Effect.Effect +>(2, (self, key) => + Effect.suspend(() => { + if (self.state._tag === "Closed") { + return Effect.void + } + const fiber = MutableHashMap.get(self.state.backing, key) + if (fiber._tag === "None") { + return Effect.void + } + return Fiber.interruptAs(fiber.value, internalFiberId) + })) + +/** + * Removes all fibers from the FiberMap, interrupting them. + * + * **Example** (Clearing all fibers) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add some fibers to the map + * yield* FiberMap.run(map, "task1", Effect.never) + * yield* FiberMap.run(map, "task2", Effect.never) + * yield* FiberMap.run(map, "task3", Effect.never) + * + * console.log(yield* FiberMap.size(map)) // 3 + * + * // Clear all fibers (this will interrupt all of them) + * yield* FiberMap.clear(map) + * + * console.log(yield* FiberMap.size(map)) // 0 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const clear = (self: FiberMap): Effect.Effect => + Effect.suspend(() => { + if (self.state._tag === "Closed") { + return Effect.void + } + return Fiber.interruptAllAs(MutableHashMap.values(self.state.backing), internalFiberId) + }) + +const constInterruptedFiber = (function() { + let fiber: Fiber.Fiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Forks an Effect and stores the resulting fiber in the `FiberMap` under a key. + * + * **Details** + * + * When the fiber completes, it is removed from the map. If the key already has + * a fiber, the previous fiber is interrupted unless `onlyIfMissing` is set. + * + * **Example** (Forking effects into a map) + * + * ```ts + * import { Effect, Fiber, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Run effects and add the fibers to the map + * const fiber1 = yield* FiberMap.run(map, "task1", Effect.succeed("Hello")) + * const fiber2 = yield* FiberMap.run(map, "task2", Effect.succeed("World")) + * + * // Join the fibers to get their successful values + * const result1 = yield* Fiber.join(fiber1) + * const result2 = yield* Fiber.join(fiber2) + * + * console.log(result1, result2) // "Hello", "World" + * console.log(yield* FiberMap.size(map)) // 0 (fibers are removed after completion) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const run: { + ( + self: FiberMap, + key: K, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } | undefined + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberMap, + key: K, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } | undefined + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] + if (Effect.isEffect(arguments[2])) { + return runImpl(self, arguments[1], arguments[2], arguments[3]) as any + } + const key = arguments[1] + const options = arguments[2] + return (effect: Effect.Effect) => runImpl(self, key, effect, options) +} + +const runImpl = ( + self: FiberMap, + key: K, + effect: Effect.Effect, + options?: { + readonly onlyIfMissing?: boolean + readonly propagateInterruption?: boolean | undefined + } +) => + Effect.withFiber((parent) => { + if (self.state._tag === "Closed") { + return Effect.interrupt + } else if (options?.onlyIfMissing === true && hasUnsafe(self, key)) { + return Effect.sync(constInterruptedFiber) + } + const fiber = Effect.runForkWith(parent.context as Context)(effect) + setUnsafe(self, key, fiber, options) + return Effect.succeed(fiber) + }) + +/** + * Captures the current runtime and returns a function for forking effects into + * an existing `FiberMap`. + * + * **Details** + * + * Each call stores the forked fiber under the supplied key. If that key already + * has a fiber, the previous fiber is interrupted unless `onlyIfMissing` is set. + * + * **Example** (Capturing a runtime) + * + * ```ts + * import { Context, Effect, FiberMap } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.Service> + * }>("Users") + * + * Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const run = yield* FiberMap.runtime(map)() + * + * // run some effects and add the fibers to the map + * run("effect-a", Effect.andThen(Users, (_) => _.getAll)) + * run("effect-b", Effect.andThen(Users, (_) => _.getAll)) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const runtime: ( + self: FiberMap +) => () => Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Fiber.Fiber, + never, + R +> = (self: FiberMap) => () => + Effect.map( + Effect.context(), + (services) => { + const runFork = Effect.runForkWith(services) + return ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } else if (options?.onlyIfMissing === true && hasUnsafe(self, key)) { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + setUnsafe(self, key, fiber, options) + return fiber + } + } + ) + +/** + * Captures the current runtime and returns a function for running effects in + * an existing `FiberMap` as Promises. + * + * **Details** + * + * Each call stores the forked fiber under the supplied key, interrupting any + * previous fiber for that key unless `onlyIfMissing` is set. The Promise + * resolves with the effect's success value or rejects with the squashed failure + * cause. + * + * **Example** (Running effects as promises) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * const runPromise = yield* FiberMap.runtimePromise(map)() + * + * // Create promises that will be backed by fibers in the map + * const promise1 = runPromise("task1", Effect.succeed("Hello")) + * const promise2 = runPromise("task2", Effect.succeed("World")) + * + * // Convert promises back to Effects and await + * const result1 = yield* Effect.promise(() => promise1) + * const result2 = yield* Effect.promise(() => promise2) + * + * console.log(result1, result2) // "Hello", "World" + * }) + * ``` + * + * @category combinators + * @since 3.13.0 + */ +export const runtimePromise = (self: FiberMap): () => Effect.Effect< + ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onlyIfMissing?: boolean | undefined + readonly propagateInterruption?: boolean | undefined + } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + key: K, + effect: Effect.Effect, + options?: + | Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(key, effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * Gets the number of fibers currently in the FiberMap. + * + * **Example** (Checking the map size) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * console.log(yield* FiberMap.size(map)) // 0 + * + * // Add some fibers + * yield* FiberMap.run(map, "task1", Effect.never) + * yield* FiberMap.run(map, "task2", Effect.never) + * + * console.log(yield* FiberMap.size(map)) // 2 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const size = (self: FiberMap): Effect.Effect => + Effect.sync(() => self.state._tag === "Closed" ? 0 : MutableHashMap.size(self.state.backing)) + +/** + * Waits for the `FiberMap` to fail or close. + * + * **Details** + * + * The returned Effect fails with the first managed fiber failure that is not + * ignored by the map's interruption rules. Normal successful completion + * removes fibers from the map; use `awaitEmpty` to wait until the map has no + * fibers. + * + * **Example** (Joining failing fibers) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* FiberMap.make() + * yield* FiberMap.set(map, "a", Effect.runFork(Effect.fail("error"))) + * + * // parent fiber will fail with "error" + * yield* FiberMap.join(map) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const join = (self: FiberMap): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Waits for the FiberMap to be empty. + * This will wait for all currently running fibers to complete. + * + * **Example** (Waiting for an empty map) + * + * ```ts + * import { Effect, FiberMap } from "effect" + * + * const program = Effect.gen(function*() { + * const map = yield* FiberMap.make() + * + * // Add some fibers that will complete after a delay + * yield* FiberMap.run(map, "task1", Effect.sleep(1000)) + * yield* FiberMap.run(map, "task2", Effect.sleep(2000)) + * + * console.log("Waiting for all fibers to complete...") + * + * // Wait for the map to be empty + * yield* FiberMap.awaitEmpty(map) + * + * console.log("All fibers completed!") + * console.log(yield* FiberMap.size(map)) // 0 + * }) + * ``` + * + * @category combinators + * @since 3.13.0 + */ +export const awaitEmpty = (self: FiberMap): Effect.Effect => + Effect.whileLoop({ + while: () => self.state._tag === "Open" && MutableHashMap.size(self.state.backing) > 0, + body: () => Fiber.await(Iterable.headUnsafe(self)[1]), + step: constVoid + }) diff --git a/repos/effect/packages/effect/src/FiberSet.ts b/repos/effect/packages/effect/src/FiberSet.ts new file mode 100644 index 0000000..7b7b366 --- /dev/null +++ b/repos/effect/packages/effect/src/FiberSet.ts @@ -0,0 +1,713 @@ +/** + * Manages many fibers together inside one scope. + * + * A `FiberSet` tracks running fibers, removes each fiber when it + * completes, and interrupts all still-running fibers when the owning scope + * closes. This module includes scoped runtime constructors plus helpers for + * adding, clearing, running, counting, joining, and waiting for managed fibers. + * + * @since 2.0.0 + */ +import * as Cause from "./Cause.ts" +import type { Context } from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import * as Filter from "./Filter.ts" +import { constVoid, dual } from "./Function.ts" +import type * as Inspectable from "./Inspectable.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Iterable from "./Iterable.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type * as Scope from "./Scope.ts" + +const TypeId = "~effect/FiberSet" + +/** + * A FiberSet is a collection of fibers that can be managed together. + * When the associated Scope is closed, all fibers in the set will be interrupted. + * + * **Example** (Managing fibers in a set) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // Add fibers to the set + * yield* FiberSet.run(set, Effect.succeed("hello")) + * yield* FiberSet.run(set, Effect.succeed("world")) + * + * // Wait for all fibers to complete + * yield* FiberSet.awaitEmpty(set) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface FiberSet + extends Pipeable, Inspectable.Inspectable, Iterable> +{ + readonly [TypeId]: typeof TypeId + readonly deferred: Deferred.Deferred + state: { + readonly _tag: "Open" + readonly backing: Set> + } | { + readonly _tag: "Closed" + } +} + +/** + * Checks whether a value is a FiberSet. + * + * **Example** (Checking if a value is a FiberSet) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * console.log(FiberSet.isFiberSet(set)) // true + * console.log(FiberSet.isFiberSet({})) // false + * }) + * ``` + * + * @category refinements + * @since 2.0.0 + */ +export const isFiberSet = (u: unknown): u is FiberSet => Predicate.hasProperty(u, TypeId) + +const Proto = { + [TypeId]: TypeId, + [Symbol.iterator](this: FiberSet) { + if (this.state._tag === "Closed") { + return Iterable.empty() + } + return this.state.backing[Symbol.iterator]() + }, + ...PipeInspectableProto, + toJSON(this: FiberSet) { + return { + _id: "FiberMap", + state: this.state + } + } +} + +const makeUnsafe = ( + backing: Set>, + deferred: Deferred.Deferred +): FiberSet => { + const self = Object.create(Proto) + self.state = { _tag: "Open", backing } + self.deferred = deferred + return self +} + +/** + * Creates a scoped `FiberSet` for storing fibers. + * + * **Details** + * + * When the associated Scope is closed, all fibers in the set will be + * interrupted. You can add fibers to the set using `FiberSet.add` or + * `FiberSet.run`, and the fibers will be automatically removed from the + * FiberSet when they complete. + * + * **Example** (Creating a scoped FiberSet) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // run some effects and add the fibers to the set + * yield* FiberSet.run(set, Effect.never) + * yield* FiberSet.run(set, Effect.never) + * + * yield* Effect.sleep(1000) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.sync(() => makeUnsafe(new Set(), Deferred.makeUnsafe())), + (set) => + Effect.suspend(() => { + const state = set.state + if (state._tag === "Closed") return Effect.void + set.state = { _tag: "Closed" } + const fibers = state.backing + return Fiber.interruptAll(fibers).pipe( + Deferred.into(set.deferred) + ) + }) + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberSet`. + * + * **Details** + * + * Each call returns the forked fiber and adds it to the set. Managed fibers are + * removed when they complete and are interrupted when the set's scope closes. + * + * **Example** (Creating a scoped runtime) + * + * ```ts + * import { Effect, Fiber, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const runFork = yield* FiberSet.makeRuntime() + * + * // Fork effects using the runtime + * const fiber1 = runFork(Effect.succeed("hello")) + * const fiber2 = runFork(Effect.succeed("world")) + * + * const result1 = yield* Fiber.await(fiber1) + * const result2 = yield* Fiber.await(fiber2) + * + * console.log(result1, result2) // "hello" "world" + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeRuntime = (): Effect.Effect< + (( + effect: Effect.Effect, + options?: (Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined }) | undefined + ) => Fiber.Fiber), + never, + Scope.Scope | R +> => + Effect.flatMap( + make(), + (self) => runtime(self)() + ) + +/** + * Creates a scoped run function that forks effects into a new `FiberSet` and + * returns a `Promise` for each effect result. + * + * **When to use** + * + * Use when many scoped fibers should be tracked as a set while exposing each + * result through Promise-based APIs. + * + * **Details** + * + * Managed fibers are removed when they complete and are interrupted when the + * set's scope closes. Each Promise resolves with the effect's success value or + * rejects with the squashed failure cause. + * + * **Example** (Creating a promise runtime) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const runPromise = yield* FiberSet.makeRuntimePromise() + * + * // Run effects as promises + * const promise1 = runPromise(Effect.succeed("hello")) + * const promise2 = runPromise(Effect.succeed("world")) + * + * const result1 = yield* Effect.promise(() => promise1) + * const result2 = yield* Effect.promise(() => promise2) + * + * console.log(result1, result2) // "hello" "world" + * }) + * ``` + * + * @category constructors + * @since 3.13.0 + */ +export const makeRuntimePromise = (): Effect.Effect< + (( + effect: Effect.Effect, + options?: (Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined }) | undefined + ) => Promise), + never, + R | Scope.Scope +> => + Effect.flatMap( + make(), + (self) => runtimePromise(self)() + ) + +const internalFiberId = -1 +const isInternalInterruption = Filter.toPredicate(Filter.compose( + Cause.filterInterruptors, + Filter.has(internalFiberId) +)) + +/** + * Adds an existing fiber to the `FiberSet` using a synchronous, unsafe + * mutation. + * + * **When to use** + * + * Use when an already forked fiber must be registered immediately and + * synchronous interruption on a closed set is acceptable. + * + * **Details** + * + * When the fiber completes, it is removed from the set. If the set is already + * closed, the supplied fiber is interrupted immediately. Non-interruption + * failures are recorded for `FiberSet.join`. + * + * **Example** (Adding a fiber unsafely) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * const fiber = yield* Effect.forkChild(Effect.succeed("hello")) + * + * // Unsafe add - doesn't return an Effect + * FiberSet.addUnsafe(set, fiber) + * + * // The fiber is now managed by the set + * console.log(yield* FiberSet.size(set)) // 1 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const addUnsafe: { + ( + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberSet) => void + ( + self: FiberSet, + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): void +} = dual((args) => isFiberSet(args[0]), ( + self: FiberSet, + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined +): void => { + if (self.state._tag === "Closed") { + fiber.interruptUnsafe(internalFiberId) + return + } else if (self.state.backing.has(fiber)) { + return + } + self.state.backing.add(fiber) + fiber.addObserver((exit) => { + if (self.state._tag === "Closed") { + return + } + self.state.backing.delete(fiber) + if ( + Exit.isFailure(exit) && + ( + options?.propagateInterruption === true ? + !isInternalInterruption(exit.cause) : + !Cause.hasInterruptsOnly(exit.cause) + ) + ) { + Deferred.doneUnsafe(self.deferred, exit as any) + } + }) +}) + +/** + * Adds a fiber to the FiberSet. When the fiber completes, it will be removed. + * + * **Example** (Adding a fiber) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * const fiber = yield* Effect.forkChild(Effect.succeed("hello")) + * + * // Add the fiber to the set + * yield* FiberSet.add(set, fiber) + * + * // The fiber is now managed by the set + * console.log(yield* FiberSet.size(set)) // 1 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const add: { + ( + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): (self: FiberSet) => Effect.Effect + ( + self: FiberSet, + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual( + (args) => isFiberSet(args[0]), + ( + self: FiberSet, + fiber: Fiber.Fiber, + options?: { + readonly propagateInterruption?: boolean | undefined + } | undefined + ): Effect.Effect => Effect.sync(() => addUnsafe(self, fiber, options)) +) + +/** + * Interrupts all fibers in the `FiberSet` and clears the set. + * + * **Example** (Clearing all fibers) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // Add some fibers + * yield* FiberSet.run(set, Effect.never) + * yield* FiberSet.run(set, Effect.never) + * + * console.log(yield* FiberSet.size(set)) // 2 + * + * // Clear all fibers + * yield* FiberSet.clear(set) + * + * console.log(yield* FiberSet.size(set)) // 0 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const clear = (self: FiberSet): Effect.Effect => + Effect.suspend(() => { + if (self.state._tag === "Closed") { + return Effect.void + } + return Fiber.interruptAllAs(self.state.backing, internalFiberId) + }) + +const constInterruptedFiber = (function() { + let fiber: Fiber.Fiber | undefined = undefined + return () => { + if (fiber === undefined) { + fiber = Effect.runFork(Effect.interrupt) + } + return fiber + } +})() + +/** + * Forks an Effect and add the forked fiber to the FiberSet. + * When the fiber completes, it will be removed from the FiberSet. + * + * **Example** (Forking effects into a set) + * + * ```ts + * import { Effect, Fiber, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // Fork and add to set + * const fiber1 = yield* FiberSet.run(set, Effect.succeed("hello")) + * const fiber2 = yield* FiberSet.run(set, Effect.succeed("world")) + * + * // Get results + * const result1 = yield* Fiber.await(fiber1) + * const result2 = yield* Fiber.await(fiber2) + * + * console.log(result1, result2) // "hello" "world" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const run: { + ( + self: FiberSet, + options?: { + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } | undefined + ): ( + effect: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: FiberSet, + effect: Effect.Effect, + options?: { + readonly propagateInterruption?: boolean | undefined + readonly startImmediately?: boolean | undefined + } | undefined + ): Effect.Effect, never, R> +} = function() { + const self = arguments[0] as FiberSet + if (!Effect.isEffect(arguments[1])) { + const options = arguments[1] + return (effect: Effect.Effect) => runImpl(self, effect, options) + } + return runImpl(self, arguments[1], arguments[2]) as any +} + +const runImpl = ( + self: FiberSet, + effect: Effect.Effect, + options?: { + readonly propagateInterruption?: boolean | undefined + } +): Effect.Effect, never, R> => + Effect.withFiber((parent) => { + if (self.state._tag === "Closed") { + return Effect.sync(constInterruptedFiber) + } + const fiber = Effect.runForkWith(parent.context as Context)(effect) + addUnsafe(self, fiber, options) + return Effect.succeed(fiber) + }) + +/** + * Captures a `Runtime` and uses it to fork effects into the `FiberSet`. + * + * **Example** (Capturing a runtime) + * + * ```ts + * import { Context, Effect, FiberSet } from "effect" + * + * interface Users { + * readonly _: unique symbol + * } + * const Users = Context.Service> + * }>("Users") + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * const run = yield* FiberSet.runtime(set)() + * + * // run some effects and add the fibers to the set + * run(Effect.andThen(Users, (_) => _.getAll)) + * }).pipe( + * Effect.scoped // The fibers will be interrupted when the scope is closed + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const runtime: ( + self: FiberSet +) => () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => Fiber.Fiber, + never, + R +> = (self: FiberSet) => () => + Effect.map( + Effect.context(), + (services) => { + const runFork = Effect.runForkWith(services) + return ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => { + if (self.state._tag === "Closed") { + return constInterruptedFiber() + } + const fiber = runFork(effect, options) + addUnsafe(self, fiber) + return fiber + } + } + ) + +/** + * Captures a `Runtime` and returns a Promise-based runner that forks effects + * into the `FiberSet`. + * + * **When to use** + * + * Use when you need to bridge effects to `Promise` values while still tracking + * their fibers in a `FiberSet`. + * + * **Details** + * + * The returned run function returns a `Promise` for each effect result. + * + * **Example** (Running effects as promises) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * const runPromise = yield* FiberSet.runtimePromise(set)() + * + * // Run effects as promises + * const promise1 = runPromise(Effect.succeed("hello")) + * const promise2 = runPromise(Effect.succeed("world")) + * + * const result1 = yield* Effect.promise(() => promise1) + * const result2 = yield* Effect.promise(() => promise2) + * + * console.log(result1, result2) // "hello" "world" + * }) + * ``` + * + * @see {@link runtime} for a runner that returns the forked `Fiber` + * + * @category combinators + * @since 3.13.0 + */ +export const runtimePromise = (self: FiberSet): () => Effect.Effect< + ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ) => Promise, + never, + R +> => +() => + Effect.map( + runtime(self)(), + (runFork) => + ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { readonly propagateInterruption?: boolean | undefined } + | undefined + ): Promise => + new Promise((resolve, reject) => + runFork(effect, options).addObserver((exit) => { + if (Exit.isSuccess(exit)) { + resolve(exit.value) + } else { + reject(Cause.squash(exit.cause)) + } + }) + ) + ) + +/** + * Gets the number of fibers currently in the FiberSet. + * + * **Example** (Checking the set size) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * console.log(yield* FiberSet.size(set)) // 0 + * + * // Add some fibers + * yield* FiberSet.run(set, Effect.never) + * yield* FiberSet.run(set, Effect.never) + * + * console.log(yield* FiberSet.size(set)) // 2 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const size = (self: FiberSet): Effect.Effect => + Effect.sync(() => self.state._tag === "Closed" ? 0 : self.state.backing.size) + +/** + * Joins all fibers in the FiberSet. If any fiber in the set terminates with a failure, + * the returned Effect will terminate with the first failure that occurred. + * + * **Example** (Joining failing fibers) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * Effect.gen(function*() { + * const set = yield* FiberSet.make() + * yield* FiberSet.add(set, Effect.runFork(Effect.fail("error"))) + * + * // parent fiber will fail with "error" + * yield* FiberSet.join(set) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const join = (self: FiberSet): Effect.Effect => + Deferred.await(self.deferred as Deferred.Deferred) + +/** + * Waits until the fiber set is empty. + * + * **Example** (Waiting for an empty set) + * + * ```ts + * import { Effect, FiberSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set = yield* FiberSet.make() + * + * // Add some fibers that will complete + * yield* FiberSet.run(set, Effect.sleep(100)) + * yield* FiberSet.run(set, Effect.sleep(200)) + * + * // Wait for all fibers to complete + * yield* FiberSet.awaitEmpty(set) + * + * console.log(yield* FiberSet.size(set)) // 0 + * }) + * ``` + * + * @category combinators + * @since 3.13.0 + */ +export const awaitEmpty = (self: FiberSet): Effect.Effect => + Effect.whileLoop({ + while: () => self.state._tag === "Open" && self.state.backing.size > 0, + body: () => Fiber.await(Iterable.headUnsafe(self)), + step: constVoid + }) diff --git a/repos/effect/packages/effect/src/FileSystem.ts b/repos/effect/packages/effect/src/FileSystem.ts new file mode 100644 index 0000000..beadaa7 --- /dev/null +++ b/repos/effect/packages/effect/src/FileSystem.ts @@ -0,0 +1,1407 @@ +/** + * Defines the portable file system service for Effect programs. + * + * `FileSystem` is the boundary between Effect code and the host file system. + * Platform packages provide concrete layers, while this module defines the + * operations for reading, writing, inspecting, streaming, and watching files. + * Operations return `Effect`, `Stream`, or `Sink` values and fail with + * `PlatformError`. The module also includes file handles, size helpers, open + * flags, watch events, and the watch backend service. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" +import * as Brand from "./Brand.ts" +import * as Cause from "./Cause.ts" +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import { pipe } from "./Function.ts" +import * as Layer from "./Layer.ts" +import * as Option from "./Option.ts" +import { badArgument, type PlatformError, systemError } from "./PlatformError.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Pull from "./Pull.ts" +import type { Scope } from "./Scope.ts" +import * as Sink from "./Sink.ts" +import * as Stream from "./Stream.ts" + +const TypeId = "~effect/platform/FileSystem" + +/** + * Core interface for file system operations in Effect. + * + * **Details** + * + * The FileSystem interface provides a comprehensive set of file and directory operations + * that work cross-platform. All operations return Effect values that can be composed, + * transformed, and executed safely with proper error handling. + * + * **Example** (Accessing file system operations) + * + * ```ts + * import { Console, Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Basic file operations + * const exists = yield* fs.exists("./config.json") + * if (!exists) { + * yield* fs.writeFileString("./config.json", "{\"env\": \"development\"}") + * } + * + * // Directory operations + * yield* fs.makeDirectory("./logs", { recursive: true }) + * + * // File information + * const stats = yield* fs.stat("./config.json") + * yield* Console.log(`File size: ${stats.size} bytes`) + * + * // Streaming operations + * const content = yield* fs.readFileString("./config.json") + * yield* Console.log("Config:", content) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface FileSystem { + readonly [TypeId]: typeof TypeId + + /** + * Checks whether a file can be accessed. + * You can optionally specify the level of access to check for. + */ + readonly access: ( + path: string, + options?: { + readonly ok?: boolean | undefined + readonly readable?: boolean | undefined + readonly writable?: boolean | undefined + } + ) => Effect.Effect + /** + * Copy a file or directory from `fromPath` to `toPath`. + * + * **Details** + * + * Equivalent to `cp -r`. + */ + readonly copy: ( + fromPath: string, + toPath: string, + options?: { + readonly overwrite?: boolean | undefined + readonly preserveTimestamps?: boolean | undefined + } + ) => Effect.Effect + /** + * Copy a file from `fromPath` to `toPath`. + */ + readonly copyFile: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Change the permissions of a file. + */ + readonly chmod: ( + path: string, + mode: number + ) => Effect.Effect + /** + * Change the owner and group of a file. + */ + readonly chown: ( + path: string, + uid: number, + gid: number + ) => Effect.Effect + /** + * Glob a directory. + */ + readonly glob: ( + pattern: string, + options?: { + readonly root?: string | undefined + readonly exclude?: ReadonlyArray | undefined + } + ) => Effect.Effect, PlatformError> + /** + * Checks whether a path exists. + */ + readonly exists: ( + path: string + ) => Effect.Effect + /** + * Create a hard link from `fromPath` to `toPath`. + */ + readonly link: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Create a directory at `path`. You can optionally specify the mode and + * whether to recursively create nested directories. + */ + readonly makeDirectory: ( + path: string, + options?: { + readonly recursive?: boolean | undefined + readonly mode?: number | undefined + } + ) => Effect.Effect + /** + * Create a temporary directory. + * + * **Details** + * + * By default the directory will be created inside the system's default + * temporary directory, but you can specify a different location by setting + * the `directory` option. + * + * You can also specify a prefix for the directory name by setting the + * `prefix` option. + */ + readonly makeTempDirectory: (options?: { + readonly directory?: string | undefined + readonly prefix?: string | undefined + }) => Effect.Effect + /** + * Create a temporary directory inside a scope. + * + * **Details** + * + * Functionally equivalent to `makeTempDirectory`, but the directory will be + * automatically deleted when the scope is closed. + */ + readonly makeTempDirectoryScoped: (options?: { + readonly directory?: string | undefined + readonly prefix?: string | undefined + }) => Effect.Effect + /** + * Create a temporary file. + * The directory creation is functionally equivalent to `makeTempDirectory`. + * The file name will be a randomly generated string. + */ + readonly makeTempFile: (options?: { + readonly directory?: string | undefined + readonly prefix?: string | undefined + readonly suffix?: string | undefined + }) => Effect.Effect + /** + * Create a temporary file inside a scope. + * + * **Details** + * + * Functionally equivalent to `makeTempFile`, but the file will be + * automatically deleted when the scope is closed. + */ + readonly makeTempFileScoped: (options?: { + readonly directory?: string | undefined + readonly prefix?: string | undefined + readonly suffix?: string | undefined + }) => Effect.Effect + /** + * Open a file at `path` with the specified `options`. + * + * **Details** + * + * The file handle will be automatically closed when the scope is closed. + */ + readonly open: ( + path: string, + options?: { + readonly flag?: OpenFlag | undefined + readonly mode?: number | undefined + } + ) => Effect.Effect + /** + * List the contents of a directory. + * + * **Details** + * + * You can recursively list the contents of nested directories by setting the + * `recursive` option. + */ + readonly readDirectory: ( + path: string, + options?: { + readonly recursive?: boolean | undefined + } + ) => Effect.Effect, PlatformError> + /** + * Read the contents of a file. + */ + readonly readFile: ( + path: string + ) => Effect.Effect + /** + * Read the contents of a file. + */ + readonly readFileString: ( + path: string, + encoding?: string + ) => Effect.Effect + /** + * Read the destination of a symbolic link. + */ + readonly readLink: ( + path: string + ) => Effect.Effect + /** + * Resolve a path to its canonicalized absolute pathname. + */ + readonly realPath: ( + path: string + ) => Effect.Effect + /** + * Remove a file or directory. + */ + readonly remove: ( + path: string, + options?: { + /** + * When `true`, you can recursively remove nested directories. + */ + readonly recursive?: boolean | undefined + /** + * When `true`, exceptions will be ignored if `path` does not exist. + */ + readonly force?: boolean | undefined + } + ) => Effect.Effect + /** + * Rename a file or directory. + */ + readonly rename: ( + oldPath: string, + newPath: string + ) => Effect.Effect + /** + * Create a writable `Sink` for the specified `path`. + */ + readonly sink: ( + path: string, + options?: { + readonly flag?: OpenFlag | undefined + readonly mode?: number | undefined + } + ) => Sink.Sink + /** + * Get information about a file at `path`. + */ + readonly stat: ( + path: string + ) => Effect.Effect + /** + * Create a readable `Stream` for the specified `path`. + * + * **Details** + * + * Changing the `bufferSize` option will change the internal buffer size of + * the stream. It defaults to `4`. + * + * The `chunkSize` option will change the size of the chunks emitted by the + * stream. It defaults to 64kb. + * + * Changing `offset` and `bytesToRead` will change the offset and the number + * of bytes to read from the file. + */ + readonly stream: ( + path: string, + options?: { + readonly bytesToRead?: SizeInput | undefined + readonly chunkSize?: SizeInput | undefined + readonly offset?: SizeInput | undefined + } + ) => Stream.Stream + /** + * Create a symbolic link from `fromPath` to `toPath`. + */ + readonly symlink: ( + fromPath: string, + toPath: string + ) => Effect.Effect + /** + * Truncate a file to a specified length. If the `length` is not specified, + * the file will be truncated to length `0`. + */ + readonly truncate: ( + path: string, + length?: SizeInput + ) => Effect.Effect + /** + * Change the file system timestamps of the file at `path`. + */ + readonly utimes: ( + path: string, + atime: Date | number, + mtime: Date | number + ) => Effect.Effect + /** + * Watch a directory or file for changes + */ + readonly watch: (path: string) => Stream.Stream + /** + * Write data to a file at `path`. + */ + readonly writeFile: ( + path: string, + data: Uint8Array, + options?: { + readonly flag?: OpenFlag | undefined + readonly mode?: number | undefined + } + ) => Effect.Effect + /** + * Write a string to a file at `path`. + */ + readonly writeFileString: ( + path: string, + data: string, + options?: { + readonly flag?: OpenFlag | undefined + readonly mode?: number | undefined + } + ) => Effect.Effect +} + +/** + * Represents a file size in bytes using a branded bigint. + * + * **Details** + * + * This type ensures type safety when working with file sizes, preventing + * accidental mixing of regular numbers with size values. The underlying + * bigint allows for handling very large file sizes beyond JavaScript's + * number precision limits. + * + * **Example** (Creating branded file sizes) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * // Create sizes using the Size constructor + * const smallFile = FileSystem.Size(1024) // 1 KB + * const largeFile = FileSystem.Size(BigInt("9007199254740992")) // Very large + * + * // Use with file operations + * const truncateToSize = Effect.fnUntraced(function*(path: string, size: FileSystem.Size) { + * const fs = yield* FileSystem.FileSystem + * return yield* fs.truncate(path, size) + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export type Size = Brand.Branded + +/** + * Input type for size parameters that accepts multiple numeric types. + * + * **Details** + * + * This union type allows file system operations to accept size values in + * different formats for convenience, which are then normalized to the + * branded `Size` type internally. + * + * **Example** (Using size inputs) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // All of these are valid SizeInput values + * yield* fs.truncate("file1.txt", 1024) // number + * yield* fs.truncate("file2.txt", BigInt(2048)) // bigint + * yield* fs.truncate("file3.txt", FileSystem.Size(4096)) // Size + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export type SizeInput = bigint | number | Size + +/** + * Creates a `Size` from various numeric input types. + * + * **Details** + * + * Converts numbers, bigints, or existing Size values into a properly + * branded Size type. This function handles the conversion and ensures + * type safety for file size operations. + * + * **Example** (Converting size inputs) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * // From number + * const size1 = FileSystem.Size(1024) + * console.log(typeof size1) // "bigint" + * + * // From bigint + * const size2 = FileSystem.Size(BigInt(2048)) + * + * // From existing Size (identity) + * const size3 = FileSystem.Size(size1) + * + * // Use in file operations + * const readChunk = (path: string, chunkSize: number) => + * Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * return fs.stream(path, { + * chunkSize: FileSystem.Size(chunkSize) + * }) + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const Size = (bytes: SizeInput): Size => typeof bytes === "bigint" ? bytes as Size : BigInt(bytes) as Size + +/** + * Creates a `Size` representing kilobytes (1024 bytes). + * + * **Details** + * + * Converts a number of kilobytes to the equivalent size in bytes. + * Uses binary kilobytes (1024 bytes) rather than decimal (1000 bytes). + * + * **Example** (Creating kibibyte sizes) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Create a 64 KiB buffer size for streaming + * const bufferSize = FileSystem.KiB(64) + * + * const stream = fs.stream("large-file.txt", { + * chunkSize: bufferSize + * }) + * + * // Truncate file to 100 KiB + * yield* fs.truncate("data.txt", FileSystem.KiB(100)) + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const KiB = (n: number): Size => Size(n * 1024) + +/** + * Creates a `Size` representing mebibytes (1024² bytes). + * + * **Details** + * + * Converts a number of mebibytes to the equivalent size in bytes. + * Uses binary mebibytes (1,048,576 bytes) rather than decimal megabytes. + * + * **Example** (Creating mebibyte sizes) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Set a 10 MiB chunk size for large file operations + * const largeChunkSize = FileSystem.MiB(10) + * + * const stream = fs.stream("video.mp4", { + * chunkSize: largeChunkSize + * }) + * + * // Check if file is larger than 100 MiB + * const stats = yield* fs.stat("archive.zip") + * const maxSize = FileSystem.MiB(100) + * if (stats.size > maxSize) { + * yield* Effect.log("File is very large!") + * } + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const MiB = (n: number): Size => Size(n * 1024 * 1024) + +/** + * Creates a `Size` representing gibibytes (1024³ bytes). + * + * **Details** + * + * Converts a number of gibibytes to the equivalent size in bytes. + * Uses binary gibibytes (1,073,741,824 bytes) rather than decimal gigabytes. + * + * **Example** (Creating gibibyte sizes) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Use GiB values as size thresholds + * const maxArchiveSize = FileSystem.GiB(1) + * console.log(maxArchiveSize.toString()) // "1073741824" + * + * const tempFile = yield* fs.makeTempFile({ prefix: "archive-" }) + * yield* fs.writeFileString(tempFile, "backup data") + * + * const info = yield* fs.stat(tempFile) + * console.log(info.size < maxArchiveSize) // true + * + * yield* fs.remove(tempFile) + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const GiB = (n: number): Size => Size(n * 1024 * 1024 * 1024) + +/** + * Creates a `Size` representing tebibytes (1024⁴ bytes). + * + * **Details** + * + * Converts a number of tebibytes to the equivalent size in bytes. + * Uses binary tebibytes (1,099,511,627,776 bytes) rather than decimal terabytes. + * + * **Example** (Creating tebibyte sizes) + * + * ```ts + * import { Console, Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Check if we're dealing with very large files + * const stats = yield* fs.stat("database-backup.sql") + * const oneTiB = FileSystem.TiB(1) + * + * if (stats.size > oneTiB) { + * yield* Console.log("This is a very large database backup!") + * + * // Use larger chunk sizes for such files + * const stream = fs.stream("database-backup.sql", { + * chunkSize: FileSystem.MiB(100) // 100 MiB chunks + * }) + * } + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const TiB = (n: number): Size => Size(n * 1024 * 1024 * 1024 * 1024) + +const bigint1024 = BigInt(1024) +const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024 + +/** + * Creates a `Size` representing pebibytes (1024⁵ bytes). + * + * **Details** + * + * Converts a number of pebibytes to the equivalent size in bytes. + * Uses binary pebibytes (1,125,899,906,842,624 bytes) rather than decimal petabytes. + * This function uses BigInt arithmetic to handle the very large numbers involved. + * + * **Example** (Creating pebibyte sizes) + * + * ```ts + * import { Console, Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // For extremely large data processing scenarios + * const massiveDataset = FileSystem.PiB(2) // 2 PiB + * + * // This would typically be used in enterprise/cloud scenarios + * yield* Console.log(`Processing ${massiveDataset} bytes of data`) + * + * // Such large files would require specialized streaming + * const stream = fs.stream("massive-dataset.bin", { + * chunkSize: FileSystem.GiB(1), // 1 GiB chunks + * offset: FileSystem.TiB(100) // Start from 100 TiB offset + * }) + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const PiB = (n: number): Size => Size(BigInt(n) * bigintPiB) + +/** + * File open flags that determine how a file is opened and what operations are allowed. + * + * **Details** + * + * These flags correspond to standard POSIX file open modes and control the file access + * permissions and behavior when opening files. + * + * - `"r"` - Read-only. File must exist. + * - `"r+"` - Read/write. File must exist. + * - `"w"` - Write-only. Truncates file to zero length or creates new file. + * - `"wx"` - Like 'w' but fails if file exists. + * - `"w+"` - Read/write. Truncates file to zero length or creates new file. + * - `"wx+"` - Like 'w+' but fails if file exists. + * - `"a"` - Write-only. Appends to file or creates new file. + * - `"ax"` - Like 'a' but fails if file exists. + * - `"a+"` - Read/write. Appends to file or creates new file. + * - `"ax+"` - Like 'a+' but fails if file exists. + * + * **Example** (Opening files with flags) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Open for reading only + * const readFile = yield* fs.open("data.txt", { flag: "r" }) + * + * // Open for writing, truncating existing content + * const writeFile = yield* fs.open("output.txt", { flag: "w" }) + * + * // Open for appending + * const appendFile = yield* fs.open("log.txt", { flag: "a" }) + * + * // Open for read/write, but fail if file doesn't exist + * const editFile = yield* fs.open("config.json", { flag: "r+" }) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export type OpenFlag = + | "r" + | "r+" + | "w" + | "wx" + | "w+" + | "wx+" + | "a" + | "ax" + | "a+" + | "ax+" + +/** + * Service tag for platform file-system operations. + * + * **When to use** + * + * Use to access or provide operations for files, directories, permissions, + * streams, and sinks through the Effect context. + * + * **Details** + * + * This key is used to provide and access the FileSystem service in the Effect context. + * + * **Example** (Accessing and providing FileSystem) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * // Access the FileSystem service + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * const exists = yield* fs.exists("./data.txt") + * if (exists) { + * const content = yield* fs.readFileString("./data.txt") + * yield* Effect.log("File content:", content) + * } + * }) + * + * // Provide a custom FileSystem implementation + * declare const platformImpl: Omit< + * FileSystem.FileSystem, + * "exists" | "readFileString" | "stream" | "sink" | "writeFileString" + * > + * const customFs = FileSystem.make(platformImpl) + * + * const withCustomFs = Effect.provideService( + * program, + * FileSystem.FileSystem, + * customFs + * ) + * ``` + * + * @category services + * @since 4.0.0 + */ +export const FileSystem: Context.Service = Context.Service("effect/platform/FileSystem") + +/** + * Creates a FileSystem implementation from a partial implementation. + * + * **When to use** + * + * Use to build a concrete `FileSystem` service from platform-specific core + * operations while deriving the convenience methods that can be implemented + * from them. + * + * **Details** + * + * This function takes a partial FileSystem implementation and automatically provides + * default implementations for `exists`, `readFileString`, `stream`, `sink`, and + * `writeFileString` methods based on the provided core methods. + * + * @see {@link makeNoop} for a testing stub that accepts method overrides without requiring a complete implementation + * @see {@link layerNoop} for providing a no-op `FileSystem` as a `Layer` in tests + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + impl: Omit +): FileSystem => + FileSystem.of({ + ...impl, + [TypeId]: TypeId, + exists: (path) => + pipe( + impl.access(path), + Effect.as(true), + Effect.catchTag( + "PlatformError", + (e) => e.reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(e) + ) + ), + readFileString: (path, encoding) => + Effect.flatMap(impl.readFile(path), (_) => + Effect.try({ + try: () => new TextDecoder(encoding).decode(_), + catch: (cause) => + badArgument({ + module: "FileSystem", + method: "readFileString", + description: "invalid encoding", + cause + }) + })), + stream: Effect.fnUntraced(function*(path, options) { + const file = yield* impl.open(path, { flag: "r" }) + if (options?.offset) { + yield* file.seek(options.offset, "start") + } + const bytesToRead = options?.bytesToRead !== undefined ? Size(options.bytesToRead) : undefined + let totalBytesRead = BigInt(0) + const chunkSize = Size(options?.chunkSize ?? 64 * 1024) + const readChunk = file.readAlloc(chunkSize) + return Stream.fromPull(Effect.succeed( + Effect.flatMap( + Effect.suspend((): Pull.Pull, PlatformError> => { + if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) { + return Cause.done() + } + return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize + ? file.readAlloc(bytesToRead - totalBytesRead) + : readChunk + }), + Option.match({ + onNone: () => Cause.done(), + onSome: (buf) => { + totalBytesRead += BigInt(buf.length) + return Effect.succeed(Arr.of(buf)) + } + }) + ) + )) + }, Stream.unwrap), + sink: (path, options) => + pipe( + impl.open(path, { flag: "w", ...options }), + Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))), + Sink.unwrap + ), + writeFileString: (path, data, options) => + Effect.flatMap( + Effect.try({ + try: () => new TextEncoder().encode(data), + catch: (cause) => + badArgument({ + module: "FileSystem", + method: "writeFileString", + description: "could not encode string", + cause + }) + }), + (_) => impl.writeFile(path, _, options) + ) + }) + +const notFound = (method: string, path: string) => + systemError({ + module: "FileSystem", + method, + _tag: "NotFound", + description: "No such file or directory", + pathOrDescriptor: path + }) + +/** + * Creates a stub `FileSystem` implementation for tests. + * + * **Details** + * + * By default, `exists` returns `false`, `remove` succeeds, many file operations + * fail with `PlatformError` `NotFound`, and temporary-directory/file operations + * die as not implemented. Pass method overrides to provide the behavior needed + * by a specific test without touching the real file system. + * + * **Example** (Creating a no-op FileSystem) + * + * ```ts + * import { Effect, FileSystem, PlatformError } from "effect" + * + * // Create a test filesystem that only allows reading specific files + * const testFs = FileSystem.makeNoop({ + * readFileString: (path) => { + * if (path === "test-config.json") { + * return Effect.succeed("{\"test\": true}") + * } + * return Effect.fail( + * PlatformError.systemError({ + * _tag: "NotFound", + * module: "FileSystem", + * method: "readFileString", + * description: "File not found", + * pathOrDescriptor: path + * }) + * ) + * }, + * exists: (path) => Effect.succeed(path === "test-config.json") + * }) + * + * // Use in tests + * const program = Effect.gen(function*() { + * const content = yield* testFs.readFileString("test-config.json") + * // Will succeed with mocked content + * }) + * + * // Test with the no-op filesystem + * const testProgram = Effect.provideService( + * program, + * FileSystem.FileSystem, + * testFs + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeNoop = (fileSystem: Partial): FileSystem => + FileSystem.of({ + [TypeId]: TypeId, + access(path) { + return Effect.fail(notFound("access", path)) + }, + chmod(path) { + return Effect.fail(notFound("chmod", path)) + }, + chown(path) { + return Effect.fail(notFound("chown", path)) + }, + copy(path) { + return Effect.fail(notFound("copy", path)) + }, + copyFile(path) { + return Effect.fail(notFound("copyFile", path)) + }, + glob(pattern) { + return Effect.fail(notFound("glob", pattern)) + }, + exists() { + return Effect.succeed(false) + }, + link(path) { + return Effect.fail(notFound("link", path)) + }, + makeDirectory() { + return Effect.die("not implemented") + }, + makeTempDirectory() { + return Effect.die("not implemented") + }, + makeTempDirectoryScoped() { + return Effect.die("not implemented") + }, + makeTempFile() { + return Effect.die("not implemented") + }, + makeTempFileScoped() { + return Effect.die("not implemented") + }, + open(path) { + return Effect.fail(notFound("open", path)) + }, + readDirectory(path) { + return Effect.fail(notFound("readDirectory", path)) + }, + readFile(path) { + return Effect.fail(notFound("readFile", path)) + }, + readFileString(path) { + return Effect.fail(notFound("readFileString", path)) + }, + readLink(path) { + return Effect.fail(notFound("readLink", path)) + }, + realPath(path) { + return Effect.fail(notFound("realPath", path)) + }, + remove() { + return Effect.void + }, + rename(oldPath) { + return Effect.fail(notFound("rename", oldPath)) + }, + sink(path) { + return Sink.fail(notFound("sink", path)) + }, + stat(path) { + return Effect.fail(notFound("stat", path)) + }, + stream(path) { + return Stream.fail(notFound("stream", path)) + }, + symlink(fromPath) { + return Effect.fail(notFound("symlink", fromPath)) + }, + truncate(path) { + return Effect.fail(notFound("truncate", path)) + }, + utimes(path) { + return Effect.fail(notFound("utimes", path)) + }, + watch(path) { + return Stream.fail(notFound("watch", path)) + }, + writeFile(path) { + return Effect.fail(notFound("writeFile", path)) + }, + writeFileString(path) { + return Effect.fail(notFound("writeFileString", path)) + }, + ...fileSystem + }) + +/** + * Creates a Layer that provides a no-op FileSystem implementation for testing. + * + * **Details** + * + * This is a convenience function that wraps `makeNoop` in a Layer, making it easy + * to provide the test filesystem to your Effect programs. + * + * **Example** (Providing a no-op FileSystem layer) + * + * ```ts + * import { Effect, FileSystem } from "effect" + * + * // Create a test layer with specific behaviors + * const testLayer = FileSystem.layerNoop({ + * readFileString: (path) => Effect.succeed("mocked content"), + * exists: () => Effect.succeed(true) + * }) + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * const content = yield* fs.readFileString("any-file.txt") + * return content + * }) + * + * // Provide the test layer + * const testProgram = Effect.provide(program, testLayer) + * ``` + * + * @category layers + * @since 4.0.0 + */ +export const layerNoop = (fileSystem: Partial): Layer.Layer => + Layer.succeed(FileSystem)(makeNoop(fileSystem)) + +/** + * Runtime type identifier attached to `FileSystem.File` handles and used by + * `isFile` to recognize them. + * + * **Details** + * + * This marker is part of the runtime representation of file handles. Prefer + * `isFile` when narrowing unknown values. + * + * @see {@link File} for the open file handle shape that carries this marker + * @see {@link isFile} for the public guard that checks this marker + * + * @category type IDs + * @since 4.0.0 + */ +export const FileTypeId = "~effect/platform/FileSystem/File" + +/** + * Returns `true` if a value is a `File` handle by checking for the + * `FileTypeId` marker. + * + * **When to use** + * + * Use when accepting an unknown value and you need to narrow it to a `File` + * before calling file-handle operations. + * + * **Details** + * + * This is a structural marker check. It does not validate the marker value or + * the shape of the file handle. + * + * @see {@link File} for the file-handle interface narrowed by this guard + * @see {@link FileTypeId} for the runtime marker checked by this guard + * + * @category file + * @since 4.0.0 + */ +export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) + +/** + * Interface representing an open file handle. + * + * **Details** + * + * Provides low-level file operations including reading, writing, seeking, + * and retrieving file information. File handles are automatically managed + * within scoped operations to ensure proper cleanup. + * + * **Example** (Working with file handles) + * + * ```ts + * import { Console, Effect, FileSystem } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // Open a file and work with the handle + * yield* Effect.scoped( + * Effect.gen(function*() { + * const file = yield* fs.open("./data.txt", { flag: "r+" }) + * + * // Get file information + * const stats = yield* file.stat + * yield* Console.log(`File size: ${stats.size} bytes`) + * + * // Read from specific position + * yield* file.seek(10, "start") + * const buffer = new Uint8Array(5) + * const bytesRead = yield* file.read(buffer) + * yield* Console.log(`Read ${bytesRead} bytes:`, buffer) + * + * // Write data + * const data = new TextEncoder().encode("Hello") + * yield* file.write(data) + * yield* file.sync // Flush to disk + * }) + * ) + * }) + * ``` + * + * @category file + * @since 4.0.0 + */ +export interface File { + readonly [FileTypeId]: typeof FileTypeId + readonly fd: File.Descriptor + readonly stat: Effect.Effect + readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect + readonly sync: Effect.Effect + readonly read: (buffer: Uint8Array) => Effect.Effect + readonly readAlloc: (size: SizeInput) => Effect.Effect, PlatformError> + readonly truncate: (length?: SizeInput) => Effect.Effect + readonly write: (buffer: Uint8Array) => Effect.Effect + readonly writeAll: (buffer: Uint8Array) => Effect.Effect +} + +/** + * Namespace containing types associated with open file handles, including file + * descriptors, entry kinds, and stat information. + * + * @since 4.0.0 + */ +export declare namespace File { + /** + * Branded type for file descriptors. + * + * **Details** + * + * File descriptors are numeric handles used by the operating system + * to identify open files. The branded type ensures type safety. + * + * @category file + * @since 4.0.0 + */ + export type Descriptor = Brand.Branded + + /** + * Enumeration of possible file system entry types. + * + * **Details** + * + * Represents the different types of entries that can exist in a file system, + * from regular files to special device files and symbolic links. + * + * @category file + * @since 4.0.0 + */ + export type Type = + | "File" + | "Directory" + | "SymbolicLink" + | "BlockDevice" + | "CharacterDevice" + | "FIFO" + | "Socket" + | "Unknown" + + /** + * Comprehensive file information structure. + * + * **Details** + * + * Contains metadata about a file or directory including type, timestamps, + * permissions, and size information. This structure is returned by file + * stat operations. + * + * **Example** (Inspecting file information) + * + * ```ts + * import { Effect, FileSystem, Option } from "effect" + * + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * const path = yield* fs.makeTempFile({ prefix: "info-" }) + * yield* fs.writeFileString(path, "hello") + * + * const info: FileSystem.File.Info = yield* fs.stat(path) + * + * console.log(`File type: ${info.type}`) // "File type: File" + * console.log(`File size: ${info.size} bytes`) // "File size: 5 bytes" + * console.log(`Mode: ${info.mode.toString(8)}`) // Octal permissions + * + * // Handle optional timestamps without inventing a fallback date + * const modified = Option.match(info.mtime, { + * onNone: () => "unavailable", + * onSome: (mtime) => mtime.toISOString() + * }) + * console.log(`Modified: ${modified}`) + * + * // Check if it's a regular file + * if (info.type === "File") { + * console.log("Processing regular file...") // "Processing regular file..." + * } + * + * yield* fs.remove(path) + * }) + * ``` + * + * @category file + * @since 4.0.0 + */ + export interface Info { + readonly type: Type + readonly mtime: Option.Option + readonly atime: Option.Option + readonly birthtime: Option.Option + readonly dev: number + readonly ino: Option.Option + readonly mode: number + readonly nlink: Option.Option + readonly uid: Option.Option + readonly gid: Option.Option + readonly rdev: Option.Option + readonly size: Size + readonly blksize: Option.Option + readonly blocks: Option.Option + } +} + +/** + * Creates a `File.Descriptor` from a number. + * + * **When to use** + * + * Use to brand an operating-system file descriptor number when implementing a + * `FileSystem` that returns custom `File` handles. + * + * **Details** + * + * `File.Descriptor` is a branded integer handle used by operating systems to + * identify open files. + * + * **Gotchas** + * + * This constructor is nominal and does not check that the number is an integer + * or that it refers to an open file descriptor. + * + * @see {@link File.Descriptor} for the branded descriptor type produced by this constructor + * @see {@link File} for file handles that expose a descriptor through `fd` + * + * @category constructors + * @since 4.0.0 + */ +export const FileDescriptor = Brand.nominal() + +/** + * Specifies the reference point for seeking within an open file. + * + * **When to use** + * + * Use with `File` handles when positioning the cursor before a read or write + * and the offset must be interpreted from either the start of the file or the + * current cursor. + * + * **Details** + * + * - `"start"` seeks from the beginning of the file. + * - `"current"` seeks from the current cursor position. + * + * @see {@link File} for the open file handle API whose `seek` method consumes this mode + * + * @category models + * @since 4.0.0 + */ +export type SeekMode = "start" | "current" + +/** + * Represents file system events emitted when watching files or directories. + * + * **When to use** + * + * Use when consuming file system watch streams and pattern matching on `_tag` + * to handle created, updated, or removed paths. + * + * **Details** + * + * The union covers create, update, and remove events. Each event carries the + * reported `path`. + * + * @see {@link FileSystem} for the service interface whose `watch` operation emits these events + * + * @category models + * @since 4.0.0 + */ +export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove + +/** + * Namespace containing the concrete event shapes emitted by `FileSystem.watch`. + * + * @since 4.0.0 + */ +export declare namespace WatchEvent { + /** + * Event representing the creation of a new file or directory. + * + * **Details** + * + * This event is triggered when a new file or directory is created + * in the watched location. + * + * @category models + * @since 4.0.0 + */ + export interface Create { + readonly _tag: "Create" + readonly path: string + } + + /** + * Event representing the modification of an existing file or directory. + * + * **Details** + * + * This event is triggered when an existing file or directory is + * modified in the watched location. + * + * @category models + * @since 4.0.0 + */ + export interface Update { + readonly _tag: "Update" + readonly path: string + } + + /** + * Event representing the deletion of a file or directory. + * + * **Details** + * + * This event is triggered when a file or directory is deleted + * from the watched location. + * + * @category models + * @since 4.0.0 + */ + export interface Remove { + readonly _tag: "Remove" + readonly path: string + } +} + +/** + * Service key for file system watch backend implementations. + * + * **Details** + * + * This service provides the low-level file watching capabilities that can be + * implemented differently on various platforms (e.g., inotify on Linux, + * FSEvents on macOS, etc.). + * + * **Example** (Providing a custom watch backend) + * + * ```ts + * import { Effect, FileSystem, Option, Stream } from "effect" + * + * // Custom watch backend implementation + * const customWatchBackend = { + * register: (path: string, stat: FileSystem.File.Info) => { + * // Implementation would depend on platform + * return Option.some(Stream.empty) // Placeholder implementation + * } + * } + * + * // Provide custom watch backend + * const program = Effect.gen(function*() { + * const fs = yield* FileSystem.FileSystem + * + * // File watching will use the custom backend + * const watcher = fs.watch("./directory") + * }) + * + * const withCustomBackend = Effect.provideService( + * program, + * FileSystem.WatchBackend, + * customWatchBackend + * ) + * ``` + * + * @category file watcher + * @since 4.0.0 + */ +export class WatchBackend extends Context.Service Option.Option> +}>()("effect/platform/FileSystem/WatchBackend") {} diff --git a/repos/effect/packages/effect/src/Filter.ts b/repos/effect/packages/effect/src/Filter.ts new file mode 100644 index 0000000..c492eb2 --- /dev/null +++ b/repos/effect/packages/effect/src/Filter.ts @@ -0,0 +1,827 @@ +/** + * Defines composable checks that can also transform values. + * + * A `Filter` receives an input and returns a `Result`. + * Success means the value passed the filter, and failure means the value was + * filtered out. Filters may also narrow or transform the passing value. This + * module includes constructors from predicates, options, and effects, built-in + * filters for common JavaScript values and tags, helpers for combining filters, + * and conversions to predicates, options, and results. + * + * @since 4.0.0 + */ +import type { Effect } from "./Effect.ts" +import * as Equal from "./Equal.ts" +import { dual } from "./Function.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Result from "./Result.ts" +import type { EqualsWith, ExcludeTag, ExtractReason, ExtractTag, ReasonTags, Tags } from "./Types.ts" + +/** + * Represents a filter function that can transform inputs to outputs or filter them out. + * + * **Details** + * + * A filter takes an input value and either returns a boxed pass value or the + * special `fail` type to indicate the value should be filtered out. + * + * **Example** (Defining a positive number filter) + * + * ```ts + * import { Filter, Result } from "effect" + * + * // A filter that only passes positive numbers + * const positiveFilter: Filter.Filter = (n) => n > 0 ? Result.succeed(n) : Result.fail(n) + * + * console.log(positiveFilter(5)) // Result.succeed(5) + * console.log(positiveFilter(-3)) // Result.fail(-3) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Filter { + (input: Input): Result.Result +} + +/** + * Represents an effectful filter function that can produce Effects. + * + * **Details** + * + * Similar to a regular `Filter`, but the filtering operation itself can be + * effectful, allowing for asynchronous operations, error handling, and + * dependency injection. + * + * **Example** (Defining an effectful user filter) + * + * ```ts + * import { Effect, Filter, Result } from "effect" + * + * // An effectful filter that validates user data + * type User = { id: string; isActive: boolean } + * type ValidationError = { message: string } + * + * const validateUser: Filter.FilterEffect< + * string, + * User, + * User, + * ValidationError, + * never + * > = (id) => + * Effect.gen(function*() { + * const user: User = { id, isActive: id.length > 0 } + * return user.isActive ? Result.succeed(user) : Result.fail(user) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface FilterEffect< + in Input, + out Pass, + out Fail, + out E = never, + out R = never +> { + (input: Input): Effect, E, R> +} + +// ------------------------------------------------------------------------------------- +// Constructors +// ------------------------------------------------------------------------------------- + +/** + * Creates a Filter from a function that returns either a `pass` or `fail` value. + * + * **Details** + * + * This is the primary constructor for creating custom filters. The function + * should return either `Result.succeed(value)` or `Result.fail(value)`. + * + * **Example** (Creating custom filters) + * + * ```ts + * import { Filter, Result } from "effect" + * + * // Create a filter for positive numbers + * const positiveFilter = Filter.make((n: number) => n > 0 ? Result.succeed(n) : Result.fail(n)) + * + * // Create a filter that transforms strings to uppercase + * const uppercaseFilter = Filter.make((s: string) => + * s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + f: (input: Input) => Result.Result +): Filter => f as any + +/** + * Creates an effectful Filter from a function that returns an Effect. + * + * **Details** + * + * This constructor is used when the filtering operation needs to perform + * effectful computations, such as async operations, error handling, or accessing + * services from the environment. + * + * **Example** (Creating effectful filters) + * + * ```ts + * import { Effect, Filter, Result } from "effect" + * + * // Create an effectful filter that validates async + * const asyncValidate = Filter.makeEffect((id: string) => + * Effect.gen(function*() { + * const isValid = yield* Effect.succeed(id.length > 0) + * return isValid ? Result.succeed(id) : Result.fail(id) + * }) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeEffect = ( + f: (input: Input) => Effect, E, R> +): FilterEffect => f as any + +/** + * Transforms the failure value produced by a `Filter`, leaving successful + * results unchanged. + * + * @category mapping + * @since 4.0.0 + */ +export const mapFail: { + (f: (fail: Fail) => Fail2): (self: Filter) => Filter + ( + self: Filter, + f: (fail: Fail) => Fail2 + ): Filter +} = dual(2, ( + self: Filter, + f: (value: Fail) => Fail2 +): Filter => +(input: Input): Result.Result => Result.mapError(self(input), f)) + +const try_ = (f: (input: Input) => Output): Filter => (input) => { + try { + return Result.succeed(f(input)) + } catch { + return Result.fail(input) + } +} + +export { + /** + * Creates a Filter that tries to apply a function and returns `fail` on + * error. + * + * @category constructors + * @since 4.0.0 + */ + try_ as try +} + +/** + * Creates a Filter from a predicate or refinement function. + * + * **Details** + * + * This is a convenient way to create filters from boolean-returning functions. + * When the predicate returns true, the input value is passed through unchanged. + * When it returns false, the `fail` type is returned. + * + * **Example** (Creating filters from predicates) + * + * ```ts + * import { Filter, Result } from "effect" + * + * // Create filter from predicate + * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) + * const nonEmptyStrings = Filter.fromPredicate((s: string) => s.length > 0) + * + * // Type refinement + * const isString = Filter.fromPredicate((x: unknown): x is string => + * typeof x === "string" + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromPredicate: { + (refinement: Predicate.Refinement): Filter>> + (predicate: Predicate.Predicate): Filter +} = (predicate: Predicate.Predicate | Predicate.Refinement): Filter => (input: A) => + predicate(input) ? Result.succeed(input as B) : Result.fail(input) + +/** + * Creates a `Filter` from a function that returns an `Option`; `Some(value)` + * passes with `value`, and `None` fails with the original input. + * + * @category constructors + * @since 4.0.0 + */ +export const fromPredicateOption = (predicate: (a: A) => Option.Option): Filter => (input) => { + const o = predicate(input) + return o._tag === "None" ? Result.fail(input) : Result.succeed(o.value) +} + +/** + * Converts a Filter into a predicate function. + * + * **When to use** + * + * Use to reuse a `Filter` with APIs that accept only boolean predicates when + * the pass and fail payloads are not needed. + * + * @see {@link toOption} for keeping passed values and discarding failure values + * @see {@link toResult} for preserving both pass and failure values + * + * @category converting + * @since 4.0.0 + */ +export const toPredicate = ( + self: Filter +): Predicate.Predicate => +(input: A) => !Result.isFailure(self(input)) + +/** + * A predefined filter that only passes through string values. + * + * **Example** (Filtering strings) + * + * ```ts + * import { Filter, Result } from "effect" + * + * console.log(Filter.string("hello")) // Result.succeed("hello") + * console.log(Filter.string(42)) // fail + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const string: Filter = fromPredicate(Predicate.isString) + +/** + * Creates a `Filter` that passes only values strictly equal to the specified + * value using JavaScript `===` comparison. + * + * **When to use** + * + * Use when you need a `Filter` that accepts only the exact primitive value or + * object reference using JavaScript strict equality in a `Filter` / `Result` + * pipeline. + * + * **Gotchas** + * + * `NaN` never passes, even when the expected value is `NaN`, and objects pass + * only when they are the same reference. + * + * @see {@link equals} for structural equality when distinct values with equal + * contents should pass + * + * @category constructors + * @since 4.0.0 + */ +export const equalsStrict = + (value: A): Filter>> => (u) => + (u as unknown) === value ? Result.succeed(value) : Result.fail(u as any) + +/** + * Creates a `Filter` that passes inputs whose `has(key)` method returns + * `true` for the specified key. + * + * **When to use** + * + * Use to keep inputs that expose a `has` method, such as `Set` or `Map`, when + * they contain a required key. + * + * @see {@link fromPredicate} for custom predicate filters or inputs without a + * `has` method + * @see {@link Predicate.hasProperty} for guarding property presence instead of + * calling an input's `has` method + * + * @category constructors + * @since 4.0.0 + */ +export const has = + (key: K) => boolean }>(input: Input): Result.Result => + input.has(key) ? Result.succeed(input) : Result.fail(input) + +/** + * Creates a filter that only passes instances of the given constructor. + * + * **When to use** + * + * Use to narrow unknown input to values created by a specific JavaScript + * constructor while keeping the result in the `Filter` / `Result` pipeline. + * + * **Details** + * + * The filter succeeds when the input satisfies `instanceof constructor`. + * Otherwise it fails with the original input. + * + * **Gotchas** + * + * This uses JavaScript `instanceof` semantics, including prototype-chain and + * realm behavior. + * + * @see {@link fromPredicate} for custom predicate-based narrowing + * + * @category constructors + * @since 4.0.0 + */ +export const instanceOf = + any>(constructor: K) => + (u: Input): Result.Result, Exclude>> => + u instanceof constructor ? Result.succeed(u as InstanceType) : Result.fail(u) as any + +/** + * A predefined filter that only passes through number values. + * + * **Example** (Filtering numbers) + * + * ```ts + * import { Filter, Result } from "effect" + * + * console.log(Filter.number(42)) // Result.succeed(42) + * console.log(Filter.number("42")) // fail + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const number: Filter = fromPredicate(Predicate.isNumber) + +/** + * A predefined filter that only passes through boolean values. + * + * **When to use** + * + * Use when accepting an unknown input only if it is already a boolean and you + * want a `Filter` result rather than a plain predicate result. + * + * **Details** + * + * Implemented with `fromPredicate(Predicate.isBoolean)`, so `true` and `false` + * succeed and non-booleans fail with the original input. + * + * @see {@link Predicate.isBoolean} for the underlying guard + * @see {@link fromPredicate} for custom predicate-based filters + * + * @category constructors + * @since 4.0.0 + */ +export const boolean: Filter = fromPredicate(Predicate.isBoolean) + +/** + * A predefined filter that only passes through `bigint` primitive values. + * + * **When to use** + * + * Use to keep primitive big integer values from unknown input while staying in + * the composable `Filter` / `Result` pipeline. + * + * **Details** + * + * Implemented with `fromPredicate(Predicate.isBigInt)`, so values where + * `typeof input === "bigint"` succeed and all other inputs fail with the + * original input. + * + * **Gotchas** + * + * This filter does not coerce numbers or strings; `1n` passes while `1` fails. + * + * @see {@link number} for JavaScript `number` values + * @see {@link Predicate.isBigInt} for the underlying guard + * + * @category constructors + * @since 4.0.0 + */ +export const bigint: Filter = fromPredicate(Predicate.isBigInt) + +/** + * A predefined filter that only passes through Symbol values. + * + * @category constructors + * @since 4.0.0 + */ +export const symbol: Filter = fromPredicate(Predicate.isSymbol) + +/** + * A predefined filter that only passes through Date objects. + * + * **When to use** + * + * Use when you need to narrow unknown input to JavaScript `Date` instances with + * a reusable `Filter`. + * + * **Details** + * + * Implemented with `fromPredicate(Predicate.isDate)`, so passing values return + * `Result.succeed(input)` and failing values return `Result.fail(input)`. + * + * **Gotchas** + * + * The check uses `instanceof Date`, so invalid `Date` objects still pass; the + * filter does not validate the timestamp. + * + * @see {@link Predicate.isDate} for the underlying guard + * @see {@link instanceOf} for constructor-based filtering + * @see {@link fromPredicate} for custom date checks + * + * @category constructors + * @since 4.0.0 + */ +export const date: Filter = fromPredicate(Predicate.isDate) + +/** + * Creates a filter that checks if an input is tagged with a specific tag. + * + * **When to use** + * + * Use to keep only the matching member of a `_tag`-discriminated union while + * staying in a composable `Filter` / `Result` pipeline. + * + * **Details** + * + * The filter succeeds when `Predicate.isTagged(input, tag)` returns `true`. + * Otherwise it fails with the original input. + * + * **Gotchas** + * + * This only checks `_tag`; it does not validate the rest of the variant fields. + * + * @see {@link Predicate.isTagged} for the underlying boolean guard when a + * `Filter` result is not needed + * @see {@link reason} for extracting a nested reason variant from tagged errors + * + * @category constructors + * @since 4.0.0 + */ +export const tagged: { + (): >(tag: Tag) => Filter, ExcludeTag> + >( + tag: Tag + ): Filter, ExcludeTag> + ( + tag: Tag + ): (input: Input) => Result.Result, ExcludeTag> +} = function() { + return arguments.length === 0 ? taggedImpl : taggedImpl(arguments[0] as any) +} as any + +const taggedImpl = + (tag: Tag) => + (input: Input): Result.Result, ExcludeTag> => + Predicate.isTagged(input, tag) ? Result.succeed(input as any) : Result.fail(input as ExcludeTag) + +/** + * Creates a filter that extracts a reason from a tagged error. + * + * @category constructors + * @since 4.0.0 + */ +export const reason: { + (): , const ReasonTag extends ReasonTags>>( + tag: Tag, + reasonTag: ReasonTag + ) => Filter, ReasonTag>, Input> + , const ReasonTag extends ReasonTags>>( + tag: Tag, + reasonTag: ReasonTag + ): Filter, ReasonTag>, Input> + ( + tag: Tag, + reasonTag: ReasonTag + ): (input: Input) => Result.Result, ReasonTag>, Input> +} = function() { + return arguments.length === 0 ? reasonImpl : reasonImpl(arguments[0] as any, arguments[1] as any) +} as any + +const reasonImpl = + (tag: Tag, reasonTag: ReasonTag) => + (input: Input): Result.Result, ExcludeTag> => { + if ( + Predicate.isTagged(input, tag) && Predicate.hasProperty(input, "reason") && + Predicate.isTagged(input.reason, reasonTag) + ) { + return Result.succeed(input.reason as any) + } + return Result.fail(input as any) + } + +/** + * Creates a filter that only passes values equal to the specified value using structural equality. + * + * **When to use** + * + * Use to accept inputs that are structurally equal to a known expected value + * while staying in a composable `Filter` / `Result` pipeline. + * + * **Details** + * + * Delegates to `Equal.equals`. On success it returns `Result.succeed(value)`; + * on failure it returns `Result.fail(input)`. + * + * @see {@link equalsStrict} for JavaScript `===` matching instead of structural + * equality + * @see {@link Equal.equals} for the underlying structural equality semantics + * + * @category constructors + * @since 4.0.0 + */ +export const equals = + (value: A): Filter>> => (u) => + Equal.equals(u, value) ? Result.succeed(value) : Result.fail(u as any) + +/** + * Combines two filters with logical OR semantics. + * + * @category combinators + * @since 4.0.0 + */ +export const or: { + ( + that: Filter + ): (self: Filter) => Filter + ( + self: Filter, + that: Filter + ): Filter +} = dual(2, ( + self: Filter, + that: Filter +): Filter => +(input) => { + const selfResult = self(input) + return Result.isSuccess(selfResult) ? selfResult as Result.Result : that(input) +}) + +/** + * Combines two filters and applies a function to their results. + * + * **When to use** + * + * Use to combine two filters with a custom function to merge their outputs. + * + * **Details** + * + * Both filters must succeed (not return `fail`) for the combination to succeed. + * If both filters pass, their outputs are combined using the provided function. + * + * @see {@link zip} for combining two filters into a tuple + * + * @category combinators + * @since 4.0.0 + */ +export const zipWith: { + ( + right: Filter, + f: (left: PassL, right: PassR) => A + ): (left: Filter) => Filter + ( + left: Filter, + right: Filter, + f: (left: PassL, right: PassR) => A + ): Filter +} = dual(3, ( + left: Filter, + right: Filter, + f: (left: PassL, right: PassR) => A +): Filter => +(input) => { + const leftResult = left(input) + if (Result.isFailure(leftResult)) return leftResult as Result.Result + const rightResult = right(input) + if (Result.isFailure(rightResult)) return rightResult as Result.Result + return Result.succeed(f(leftResult.success, rightResult.success)) +}) + +/** + * Combines two filters into a tuple of their results. + * + * **Details** + * + * Both filters must succeed for the combination to succeed. If both pass, their + * outputs are combined into a tuple. + * + * **Example** (Zipping filters) + * + * ```ts + * import { Filter } from "effect" + * + * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) + * const evenNumbers = Filter.fromPredicate((n: number) => n % 2 === 0) + * + * const positiveAndEven = Filter.zip(positiveNumbers, evenNumbers) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const zip: { + ( + right: Filter + ): ( + left: Filter + ) => Filter + ( + left: Filter, + right: Filter + ): Filter +} = dual(2, ( + left: Filter, + right: Filter +): Filter => + zipWith(left, right, (leftResult, rightResult) => [leftResult, rightResult])) + +/** + * Combines two filters but only returns the result of the left filter. + * + * **Example** (Keeping the left filter result) + * + * ```ts + * import { Filter } from "effect" + * + * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) + * const evenNumbers = Filter.fromPredicate((n: number) => n % 2 === 0) + * + * const positiveEven = Filter.andLeft(positiveNumbers, evenNumbers) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const andLeft: { + ( + right: Filter + ): ( + left: Filter + ) => Filter + ( + left: Filter, + right: Filter + ): Filter +} = dual(2, ( + left: Filter, + right: Filter +): Filter => zipWith(left, right, (leftResult) => leftResult)) + +/** + * Combines two filters but only returns the result of the right filter. + * + * **Example** (Keeping the right filter result) + * + * ```ts + * import { Filter, Result } from "effect" + * + * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) + * const doubleNumbers = Filter.make((n: number) => + * n > 0 ? Result.succeed(n * 2) : Result.fail(n) + * ) + * + * const positiveDoubled = Filter.andRight(positiveNumbers, doubleNumbers) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const andRight: { + ( + right: Filter + ): ( + left: Filter + ) => Filter + ( + left: Filter, + right: Filter + ): Filter +} = dual(2, ( + left: Filter, + right: Filter +): Filter => zipWith(left, right, (_, rightResult) => rightResult)) + +/** + * Composes two filters sequentially, feeding the output of the first into the second. + * + * **Example** (Composing filters) + * + * ```ts + * import { Filter, Result } from "effect" + * + * const stringFilter = Filter.string + * const nonEmptyUpper = Filter.make((s: string) => + * s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s) + * ) + * + * const stringToUpper = Filter.compose(stringFilter, nonEmptyUpper) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const compose: { + ( + right: Filter + ): (left: Filter) => Filter + ( + left: Filter, + right: Filter + ): Filter +} = dual(2, ( + left: Filter, + right: Filter +): Filter => +(input) => { + const leftOut = left(input) + if (Result.isFailure(leftOut)) return leftOut as Result.Result + return right(leftOut.success) +}) + +/** + * Composes two filters sequentially, passing the successful output of the first + * filter to the second. + * + * **Details** + * + * If either filter fails, the returned filter fails with the original input + * instead of the intermediate failure value. + * + * @category combinators + * @since 4.0.0 + */ +export const composePassthrough: { + ( + right: Filter + ): (left: Filter) => Filter + ( + left: Filter, + right: Filter + ): Filter +} = dual(2, ( + left: Filter, + right: Filter +): Filter => +(input) => { + const leftOut = left(input) + if (Result.isFailure(leftOut)) return Result.fail(input) + const rightOut = right(leftOut.success) + if (Result.isFailure(rightOut)) return Result.fail(input) + return rightOut as Result.Result +}) + +/** + * Converts a `Filter` into a function that returns `Some` for passed values + * and `None` for filtered-out values. + * + * **When to use** + * + * Use when adapting a `Filter` to `Option`-based code where passed values + * become `Some` and filtered-out inputs become `None`. + * + * @see {@link toResult} for keeping the filter failure value + * @see {@link toPredicate} for plain boolean pass/fail checks + * + * @category converting + * @since 4.0.0 + */ +export const toOption = ( + self: Filter +): (input: A) => Option.Option => +(input: A) => { + const result = self(input) + return Result.isFailure(result) ? Option.none() : Option.some(result.success) +} + +/** + * Converts a `Filter` into a function that returns the underlying + * `Result.Result` for each input. + * + * **When to use** + * + * Use to adapt a `Filter` to APIs that expect a plain function returning + * `Result`, while preserving both the pass value and the failure value. + * + * @see {@link toOption} for keeping only passed values + * @see {@link toPredicate} for plain boolean pass/fail checks + * + * @category converting + * @since 4.0.0 + */ +export const toResult = ( + self: Filter +): (input: A) => Result.Result => +(input: A) => { + const result = self(input) + return Result.isFailure(result) ? Result.fail(result.failure) : Result.succeed(result.success) +} diff --git a/repos/effect/packages/effect/src/Formatter.ts b/repos/effect/packages/effect/src/Formatter.ts new file mode 100644 index 0000000..426131f --- /dev/null +++ b/repos/effect/packages/effect/src/Formatter.ts @@ -0,0 +1,319 @@ +/** + * Formats JavaScript values into readable strings. + * + * `format` is intended for logs, diagnostics, and error messages. It handles + * primitives, objects, arrays, dates, regular expressions, maps, sets, class + * instances, errors, circular references, and redactable values. `formatJson` + * wraps JSON formatting with redaction and circular-reference handling, and the + * module also includes helpers for property keys, paths, and dates. + * + * @since 4.0.0 + */ +import * as Predicate from "./Predicate.ts" +import { getRedacted, redact, symbolRedactable } from "./Redactable.ts" + +/** + * A callable interface representing a function that converts a `Value` into a `Format`, which defaults to `string`. + * + * **When to use** + * + * Use when you want to type a formatting or rendering function generically, or when you are building a pipeline that accepts pluggable formatters. + * + * **Details** + * + * This is a pure callable type and carries no runtime implementation. It is contravariant in `Value` and covariant in `Format`. + * + * **Example** (Defining a custom formatter) + * + * ```ts + * import type { Formatter } from "effect" + * + * const upper: Formatter.Formatter = (s) => s.toUpperCase() + * + * console.log(upper("hello")) + * // HELLO + * ``` + * + * @see {@link format} + * @see {@link formatJson} + * @category models + * @since 4.0.0 + */ +export interface Formatter { + (value: Value): Format +} + +/** + * Converts any JavaScript value into a human-readable string. + * + * **When to use** + * + * Use when you need to format arbitrary JavaScript values for debugging, + * logging, or error messages. + * + * **Details** + * + * - Output is **not** valid JSON; use {@link formatJson} when you need + * parseable JSON. + * - Handles `BigInt`, `Symbol`, `Set`, `Map`, `Date`, `RegExp`, and class + * instances that `JSON.stringify` cannot represent. + * - Circular references are shown as `"[Circular]"` instead of throwing. + * - Primitives: stringified naturally (`null`, `undefined`, `123`, `true`). + * Strings are JSON-quoted. + * - Objects with a custom `toString` (not `Object.prototype.toString`): + * `toString()` is called unless `ignoreToString` is `true`. + * - Errors with a `cause`: formatted as `" (cause: )"`. + * - Iterables (`Set`, `Map`, etc.): formatted as + * `ClassName([...elements])`. + * - Class instances: wrapped as `ClassName({...})`. + * - `Redactable` values are automatically redacted. + * - Arrays/objects with 0–1 entries are inline; larger ones are + * pretty-printed when `space` is set. + * - `space` — indentation unit (number of spaces, or a string like + * `"\t"`). Defaults to `0` (compact). + * - `ignoreToString` — skip calling `toString()`. Defaults to `false`. + * + * **Example** (Formatting compact output) + * + * ```ts + * import { Formatter } from "effect" + * + * console.log(Formatter.format({ a: 1, b: [2, 3] })) + * // {"a":1,"b":[2,3]} + * ``` + * + * **Example** (Pretty-printed output) + * + * ```ts + * import { Formatter } from "effect" + * + * console.log(Formatter.format({ a: 1, b: [2, 3] }, { space: 2 })) + * // { + * // "a": 1, + * // "b": [ + * // 2, + * // 3 + * // ] + * // } + * ``` + * + * **Example** (Handling circular references) + * + * ```ts + * import { Formatter } from "effect" + * + * const obj: any = { name: "loop" } + * obj.self = obj + * console.log(Formatter.format(obj)) + * // {"name":"loop","self":[Circular]} + * ``` + * + * @see {@link formatJson} + * @see {@link Formatter} + * @category formatting + * @since 2.0.0 + */ +export function format(input: unknown, options?: { + readonly space?: number | string | undefined + readonly ignoreToString?: boolean | undefined +}): string { + const space = options?.space ?? 0 + const seen = new WeakSet() + const gap = !space ? "" : (typeof space === "number" ? " ".repeat(space) : space) + const ind = (d: number) => gap.repeat(d) + + const wrap = (v: unknown, body: string): string => { + const ctor = (v as any)?.constructor + return ctor && ctor !== Object.prototype.constructor && ctor.name ? `${ctor.name}(${body})` : body + } + + const ownKeys = (o: object): Array => { + try { + return Reflect.ownKeys(o) + } catch { + return ["[ownKeys threw]"] + } + } + + function recur(v: unknown, d = 0): string { + if (Array.isArray(v)) { + if (seen.has(v)) return CIRCULAR + seen.add(v) + if (!gap || v.length <= 1) return `[${v.map((x) => recur(x, d)).join(",")}]` + const inner = v.map((x) => recur(x, d + 1)).join(",\n" + ind(d + 1)) + return `[\n${ind(d + 1)}${inner}\n${ind(d)}]` + } + + if (v instanceof Date) return formatDate(v) + + if ( + !options?.ignoreToString && + Predicate.hasProperty(v, "toString") && + typeof v["toString"] === "function" && + v["toString"] !== Object.prototype.toString && + v["toString"] !== Array.prototype.toString + ) { + const s = safeToString(v) + if (v instanceof Error && v.cause) { + return `${s} (cause: ${recur(v.cause, d)})` + } + return s + } + + if (typeof v === "string") return JSON.stringify(v) + + if ( + typeof v === "number" || + v == null || + typeof v === "boolean" || + typeof v === "symbol" + ) return String(v) + + if (typeof v === "bigint") return String(v) + "n" + + if (typeof v === "object" || typeof v === "function") { + if (seen.has(v)) return CIRCULAR + seen.add(v) + + if (symbolRedactable in v) return format(getRedacted(v as any)) + + if (Symbol.iterator in v) { + return `${v.constructor.name}(${recur(Array.from(v as any), d)})` + } + + const keys = ownKeys(v) + if (!gap || keys.length <= 1) { + const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${recur((v as any)[k], d)}`).join(",")}}` + return wrap(v, body) + } + const body = `{\n${ + keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${recur((v as any)[k], d + 1)}`).join(",\n") + }\n${ind(d)}}` + return wrap(v, body) + } + + return String(v) + } + + return recur(input, 0) +} + +const CIRCULAR = "[Circular]" + +/** + * @internal + */ +export function formatPropertyKey(name: PropertyKey): string { + return typeof name === "string" ? JSON.stringify(name) : String(name) +} + +/** + * Formats an array of property keys as a bracket-notation path string. + * + * @internal + */ +export function formatPath(path: ReadonlyArray): string { + return path.map((key) => `[${formatPropertyKey(key)}]`).join("") +} + +/** + * Formats a `Date` as an ISO 8601 string, returning `"Invalid Date"` for + * invalid dates instead of throwing. + * + * @internal + */ +export function formatDate(date: Date): string { + try { + return date.toISOString() + } catch { + return "Invalid Date" + } +} + +function safeToString(input: any): string { + try { + const s = input.toString() + return typeof s === "string" ? s : String(s) + } catch { + return "[toString threw]" + } +} + +/** + * Stringifies a value to JSON safely, silently dropping circular references. + * + * **When to use** + * + * Use when you need valid JSON output, unlike `format`, and the input may + * contain circular references that should be silently omitted rather than + * throwing a `TypeError`. + * + * **Details** + * + * Uses `JSON.stringify` internally with a replacer that tracks the current + * object ancestry. Circular references are replaced with `undefined`, which + * omits them from object output. `Redactable` values are automatically redacted + * before serialization. Values not supported by JSON, such as `BigInt`, + * `Symbol`, `undefined`, and functions, follow standard `JSON.stringify` + * behavior. The `space` parameter controls indentation and defaults to `0`. + * + * **Example** (Formatting compact JSON) + * + * ```ts + * import { Formatter } from "effect" + * + * console.log(Formatter.formatJson({ name: "Alice", age: 30 })) + * // {"name":"Alice","age":30} + * ``` + * + * **Example** (Handling circular references) + * + * ```ts + * import { Formatter } from "effect" + * + * const obj: any = { name: "test" } + * obj.self = obj + * console.log(Formatter.formatJson(obj)) + * // {"name":"test"} + * ``` + * + * **Example** (Pretty-printed JSON) + * + * ```ts + * import { Formatter } from "effect" + * + * console.log(Formatter.formatJson({ name: "Alice", age: 30 }, { space: 2 })) + * // { + * // "name": "Alice", + * // "age": 30 + * // } + * ``` + * + * @see {@link format} + * @see {@link Formatter} + * @category serialization + * @since 4.0.0 + */ +export function formatJson(input: unknown, options?: { + readonly space?: number | string | undefined +}): string { + const ancestors: Array = [] + return JSON.stringify( + input, + function(this: unknown, _key: string, value: unknown) { + const redacted = redact(value) + if (typeof redacted !== "object" || redacted === null) { + return redacted + } + while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { + ancestors.pop() + } + if (ancestors.includes(redacted)) { + return undefined // circular reference + } + ancestors.push(redacted) + return redacted + }, + options?.space + ) +} diff --git a/repos/effect/packages/effect/src/Function.ts b/repos/effect/packages/effect/src/Function.ts new file mode 100644 index 0000000..02506c8 --- /dev/null +++ b/repos/effect/packages/effect/src/Function.ts @@ -0,0 +1,1415 @@ +/** + * Provides small helpers for defining and reusing TypeScript functions. + * + * The main helpers are `pipe` and `flow` for left-to-right composition and + * `dual` for APIs that support both direct and pipe-friendly call styles. The + * module also contains small identity, constant, tuple, type-level, and + * memoization helpers used across the library. + * + * @since 2.0.0 + */ +import type { TypeLambda } from "./HKT.ts" +import { pipeArguments } from "./Pipeable.ts" + +/** + * Type lambda for function types, used for higher-kinded type operations. + * + * **When to use** + * + * Use when defining higher-kinded abstractions that must accept function types + * as one of their type-lambda inputs. + * + * **Example** (Creating a function type with a type lambda) + * + * ```ts + * import type { Function, HKT } from "effect" + * + * // Create a function type using the type lambda + * type StringToNumber = HKT.Kind + * // Equivalent to: (a: string) => number + * ``` + * + * @category type lambdas + * @since 2.0.0 + */ +export interface FunctionTypeLambda extends TypeLambda { + readonly type: (a: this["In"]) => this["Target"] +} + +/** + * Creates a function that can be called in data-first style or data-last + * (`pipe`-friendly) style. + * + * **When to use** + * + * Use to expose one implementation through both direct and `pipe`-friendly + * call styles. + * + * **Details** + * + * Pass either the arity of the uncurried function or a predicate that decides + * whether the current call is data-first. Arity is the common case. Use a + * predicate when optional arguments make arity ambiguous. + * + * **Example** (Selecting data-first or data-last style by arity) + * + * ```ts + * import { Function, pipe } from "effect" + * + * const sum = Function.dual< + * (that: number) => (self: number) => number, + * (self: number, that: number) => number + * >(2, (self, that) => self + that) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * **Example** (Defining overloads with call signatures) + * + * ```ts + * import { Function, pipe } from "effect" + * + * const sum: { + * (that: number): (self: number) => number + * (self: number, that: number): number + * } = Function.dual(2, (self: number, that: number): number => self + that) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * **Example** (Selecting data-first or data-last style with a predicate) + * + * ```ts + * import { Function, pipe } from "effect" + * + * const sum = Function.dual< + * (that: number) => (self: number) => number, + * (self: number, that: number) => number + * >( + * (args) => args.length === 2, + * (self, that) => self + that + * ) + * + * console.log(sum(2, 3)) // 5 + * console.log(pipe(2, sum(3))) // 5 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const dual: { + ) => any, DataFirst extends (...args: Array) => any>( + arity: Parameters["length"], + body: DataFirst + ): DataLast & DataFirst + ) => any, DataFirst extends (...args: Array) => any>( + isDataFirst: (args: IArguments) => boolean, + body: DataFirst + ): DataLast & DataFirst +} = function(arity, body) { + if (typeof arity === "function") { + return function(this: any) { + return arity(arguments) + ? body.apply(this, arguments as any) + : ((self: any) => body(self, ...arguments)) as any + } + } + + switch (arity) { + case 0: + case 1: + throw new RangeError(`Invalid arity ${arity}`) + + case 2: + return function(a, b) { + if (arguments.length >= 2) { + return body(a, b) + } + return function(self: any) { + return body(self, a) + } + } + + case 3: + return function(a, b, c) { + if (arguments.length >= 3) { + return body(a, b, c) + } + return function(self: any) { + return body(self, a, b) + } + } + + default: + return function() { + if (arguments.length >= arity) { + // @ts-expect-error + return body.apply(this, arguments) + } + const args = arguments + return function(self: any) { + return body(self, ...args) + } + } + } +} +/** + * Applies a function to a given value. + * + * **When to use** + * + * Use to pass a fixed value into a unary function, especially when the function + * is the value flowing through `pipe`. + * + * **Details** + * + * `apply(a)(f)` is equivalent to `f(a)`. + * + * **Example** (Applying an argument to a function) + * + * ```ts + * import { Function, pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe(String.length, Function.apply("hello")), 5) + * ``` + * + * @see {@link pipe} for building left-to-right pipelines + * + * @category combinators + * @since 2.0.0 + */ +export const apply = (a: A) => (self: (a: A) => B): B => self(a) + +/** + * A zero-argument function that produces a value when invoked. + * + * **When to use** + * + * Use to type a lazy value provider that should not run until called. + * + * **Example** (Creating a lazy argument) + * + * ```ts + * import { Function } from "effect" + * + * const constNull: Function.LazyArg = Function.constant(null) + * ``` + * + * @category models + * @since 2.0.0 + */ +export type LazyArg = () => A + +/** + * Represents a function with multiple arguments. + * + * **When to use** + * + * Use to describe a function whose argument list is represented as a tuple + * type. + * + * **Example** (Typing a variadic function) + * + * ```ts + * import type { Function } from "effect" + * import * as assert from "node:assert" + * + * const sum: Function.FunctionN<[number, number], number> = (a, b) => a + b + * assert.deepStrictEqual(sum(2, 3), 5) + * ``` + * + * @category models + * @since 2.0.0 + */ +export type FunctionN, B> = (...args: A) => B + +/** + * Returns its input argument unchanged. + * + * **When to use** + * + * Use to return a value unchanged where a function is required. + * + * **Example** (Returning the same value) + * + * ```ts + * import { identity } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(identity(5), 5) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const identity = (a: A): A => a + +/** + * Ensures that the type of an expression matches some type, + * without changing the resulting type of that expression. + * + * **When to use** + * + * Use to check assignability while preserving the expression's precise inferred + * type. + * + * **Example** (Checking an expression against a type) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const test1 = Function.satisfies()(5 as const) + * // ^? const test: 5 + * // @ts-expect-error + * const test2 = Function.satisfies()(5) + * // ^? Argument of type 'number' is not assignable to parameter of type 'string' + * + * assert.deepStrictEqual(Function.satisfies()(5), 5) + * ``` + * + * @see {@link cast} for changing only the static TypeScript type + * + * @category utility types + * @since 2.0.0 + */ +export const satisfies = () => (b: B) => b + +/** + * Returns the input value with a different static type. + * + * **When to use** + * + * Use when you need an explicit type-level cast and accept that the value is + * returned unchanged at runtime. + * + * **Gotchas** + * + * This is a type-level cast only; it performs no runtime validation or + * conversion. + * + * @see {@link satisfies} for checking assignability without changing the resulting type + * + * @category utility types + * @since 4.0.0 + */ +export const cast: (a: A) => B = identity as any + +/** + * Creates a zero-argument function that always returns the provided value. + * + * **When to use** + * + * Use when you need a thunk or callback that returns the same value on every + * invocation. + * + * **Example** (Creating a constant thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const constNull = Function.constant(null) + * + * assert.deepStrictEqual(constNull(), null) + * assert.deepStrictEqual(constNull(), null) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const constant = (value: A): LazyArg => () => value + +/** + * Returns `true` when called. + * + * **When to use** + * + * Use when you need a thunk that returns `true` on every invocation. + * + * **Example** (Returning true from a thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.constTrue(), true) + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const constTrue: LazyArg = constant(true) + +/** + * Returns `false` when called. + * + * **When to use** + * + * Use when you need a thunk that returns `false` on every invocation. + * + * **Example** (Returning false from a thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.constFalse(), false) + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const constFalse: LazyArg = constant(false) + +/** + * Returns `null` when called. + * + * **When to use** + * + * Use when you need a thunk that returns `null` on every invocation. + * + * **Example** (Returning null from a thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.constNull(), null) + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const constNull: LazyArg = constant(null) + +/** + * Returns `undefined` when called. + * + * **When to use** + * + * Use when you need a thunk that returns `undefined` on every invocation. + * + * **Example** (Returning undefined from a thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.constUndefined(), undefined) + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const constUndefined: LazyArg = constant(undefined) + +/** + * Returns no meaningful value when called. + * + * **When to use** + * + * Use when you need a thunk that is called only for its effect and has no + * meaningful return value. + * + * **Example** (Returning void from a thunk) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.constVoid(), undefined) + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const constVoid: LazyArg = constUndefined + +/** + * Reverses the order of arguments for a curried function. + * + * **When to use** + * + * Use to adapt a curried function when its argument groups need to be supplied + * in the opposite order. + * + * **Example** (Flipping curried arguments) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const f = (a: number) => (b: string) => a - b.length + * + * assert.deepStrictEqual(Function.flip(f)("aaa")(2), -1) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const flip = , B extends Array, C>( + f: (...a: A) => (...b: B) => C +): (...b: B) => (...a: A) => C => +(...b) => +(...a) => f(...a)(...b) + +/** + * Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`. + * The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`. + * + * **When to use** + * + * Use to compose exactly two unary functions into a reusable unary function. + * + * **Example** (Composing two functions) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const increment = (n: number) => n + 1 + * const square = (n: number) => n * n + * + * assert.strictEqual(Function.compose(increment, square)(2), 9) + * ``` + * + * @see {@link flow} for composing a left-to-right sequence of functions + * @see {@link pipe} for applying a value through a left-to-right sequence immediately + * + * @category combinators + * @since 2.0.0 + */ +export const compose: { + (bc: (b: B) => C): (self: (a: A) => B) => (a: A) => C + (self: (a: A) => B, bc: (b: B) => C): (a: A) => C +} = dual(2, (ab: (a: A) => B, bc: (b: B) => C): (a: A) => C => (a) => bc(ab(a))) + +/** + * Marks an impossible branch by accepting a `never` value and returning any + * type. + * + * **When to use** + * + * Use when you need a return value in a branch that exhaustive checks prove + * cannot be reached. + * + * **Gotchas** + * + * Calling `absurd` throws, because a value of type `never` should be + * impossible at runtime. + * + * **Example** (Handling impossible values) + * + * ```ts + * import { absurd } from "effect" + * + * const handleNever = (value: never) => { + * return absurd(value) // This will throw an error if called + * } + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export const absurd = (_: never): A => { + throw new Error("Called `absurd` function which should be uncallable") +} + +/** + * Creates a tupled version of this function: instead of `n` arguments, it accepts a single tuple argument. + * + * **When to use** + * + * Use to adapt a multi-argument function so it accepts one tuple argument. + * + * **Example** (Converting arguments to a tuple) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const sumTupled = Function.tupled((x: number, y: number): number => x + y) + * + * assert.deepStrictEqual(sumTupled([1, 2]), 3) + * ``` + * + * @see {@link untupled} for adapting a tuple-argument function back to multiple arguments + * + * @category combinators + * @since 2.0.0 + */ +export const tupled = , B>(f: (...a: A) => B): (a: A) => B => (a) => f(...a) + +/** + * Converts a tupled function back to an uncurried function. + * + * **When to use** + * + * Use to adapt a tuple-argument function so it accepts multiple arguments. + * + * **Example** (Converting a tuple to arguments) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * const getFirst = Function.untupled((tuple: [A, B]): A => tuple[0]) + * + * assert.deepStrictEqual(getFirst(1, 2), 1) + * ``` + * + * @see {@link tupled} for adapting a multi-argument function to one tuple argument + * + * @category combinators + * @since 2.0.0 + */ +export const untupled = , B>(f: (a: A) => B): (...a: A) => B => (...a) => f(a) + +/** + * Pipes the value of an expression through a left-to-right sequence of + * functions. + * + * **When to use** + * + * Use when you need to compose data-last functions into readable + * transformation pipelines instead of method-style chains. + * + * **Details** + * + * Takes an initial value, passes it to the first function, then passes each + * result to the next function in order. The final function result is returned. + * + * **Gotchas** + * + * Each function passed after the initial value must accept a single argument, + * because `pipe` calls each step with only the previous result. + * + * **Example** (Piping values through functions) + * + * In this example, `1` is passed to the first function, and each result becomes + * the input for the next function. + * + * ```ts + * import { pipe } from "effect" + * + * const result = pipe( + * 1, + * (n) => n + 1, + * (n) => n * 2, + * (n) => `result: ${n}` + * ) + * + * console.log(result) // "result: 4" + * ``` + * + * **Example** (Chaining methods before conversion) + * + * ```ts + * const numbers = [1, 2, 3, 4] + * const double = (n: number) => n * 2 + * const greaterThanFour = (n: number) => n > 4 + * + * const result = numbers.map(double).filter(greaterThanFour) + * + * console.log(result) // [6, 8] + * ``` + * + * **Example** (Rewriting method chains with pipe) + * + * The same transformation can be written with data-last functions. + * + * ```ts + * import { Array, pipe } from "effect" + * + * const numbers = [1, 2, 3, 4] + * const double = (n: number) => n * 2 + * const greaterThanFour = (n: number) => n > 4 + * + * const result = pipe( + * numbers, + * Array.map(double), + * Array.filter(greaterThanFour) + * ) + * + * console.log(result) // [6, 8] + * ``` + * + * **Example** (Chaining arithmetic operations) + * + * ```ts + * import { pipe } from "effect" + * + * // Define simple arithmetic operations + * const increment = (x: number) => x + 1 + * const double = (x: number) => x * 2 + * const subtractTen = (x: number) => x - 10 + * + * // Sequentially apply these operations using `pipe` + * const result = pipe(5, increment, double, subtractTen) + * + * console.log(result) + * // Output: 2 + * ``` + * + * **Example** (Building a simple transformation pipeline) + * + * ```ts + * import { pipe } from "effect" + * + * // Simple transformation pipeline + * const result = pipe( + * 5, + * (x) => x * 2, // 10 + * (x) => x + 1, // 11 + * (x) => x.toString() // "11" + * ) + * + * console.log(result) // "11" + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export function pipe(a: A): A +export function pipe(a: A, ab: (a: A) => B): B +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C +): C +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D +): D +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): E +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): F +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): G +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): H +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): I +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): J +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K +): K +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L +): L +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M +): M +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N +): N +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O +): O +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P +): P +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q +): Q +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R +): R +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S +): S +export function pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never +>( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T +): T +export function pipe(a: unknown, ...args: Array): unknown { + return pipeArguments(a, args as any) +} + +/** + * Performs left-to-right function composition. + * + * **When to use** + * + * Use to build a reusable function from a left-to-right sequence of + * transformations. + * + * **Details** + * + * The first function may have any arity. Every following function must be + * unary. + * + * **Example** (Composing functions left to right) + * + * ```ts + * import { flow } from "effect" + * import * as assert from "node:assert" + * + * const len = (s: string): number => s.length + * const double = (n: number): number => n * 2 + * + * const f = flow(len, double) + * + * assert.strictEqual(f("aaa"), 6) + * ``` + * + * @see {@link pipe} for applying a value through a left-to-right sequence immediately + * @see {@link compose} for composing exactly two functions + * + * @category combinators + * @since 2.0.0 + */ +export function flow, B = never>( + ab: (...a: A) => B +): (...a: A) => B +export function flow, B = never, C = never>( + ab: (...a: A) => B, + bc: (b: B) => C +): (...a: A) => C +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never +>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D): (...a: A) => D +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): (...a: A) => E +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): (...a: A) => F +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): (...a: A) => G +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): (...a: A) => H +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): (...a: A) => I +export function flow< + A extends ReadonlyArray, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never +>( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): (...a: A) => J +export function flow( + ab: Function, + bc?: Function, + cd?: Function, + de?: Function, + ef?: Function, + fg?: Function, + gh?: Function, + hi?: Function, + ij?: Function +): unknown { + switch (arguments.length) { + case 1: + return ab + case 2: + return function(this: unknown) { + return bc!(ab.apply(this, arguments)) + } + case 3: + return function(this: unknown) { + return cd!(bc!(ab.apply(this, arguments))) + } + case 4: + return function(this: unknown) { + return de!(cd!(bc!(ab.apply(this, arguments)))) + } + case 5: + return function(this: unknown) { + return ef!(de!(cd!(bc!(ab.apply(this, arguments))))) + } + case 6: + return function(this: unknown) { + return fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments)))))) + } + case 7: + return function(this: unknown) { + return gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments))))))) + } + case 8: + return function(this: unknown) { + return hi!(gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments)))))))) + } + case 9: + return function(this: unknown) { + return ij!(hi!(gh!(fg!(ef!(de!(cd!(bc!(ab.apply(this, arguments))))))))) + } + } + return +} + +/** + * Creates a compile-time placeholder for a value of any type. + * + * **When to use** + * + * Use as a temporary typed placeholder while developing incomplete code. + * + * **Gotchas** + * + * `hole` is intended for temporary development use. If the placeholder is + * evaluated at runtime, it throws. + * + * **Example** (Creating a development placeholder) + * + * ```ts + * import { hole } from "effect" + * + * // Intentionally not called: `hole` throws if the placeholder is evaluated. + * const buildUser = (id: number): { readonly id: number; readonly name: string } => ({ + * id, + * name: hole() + * }) + * + * console.log(typeof buildUser) // "function" + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export const hole: () => T = cast(absurd) + +/** + * Returns the second argument and discards the first. The SK combinator is + * a fundamental combinator in the lambda calculus and the SKI combinator + * calculus. + * + * **When to use** + * + * Use to discard the first argument and return the second argument. + * + * **Example** (Discarding the first argument) + * + * ```ts + * import { Function } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Function.SK(0, "hello"), "hello") + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const SK = (_: A, b: B): B => b + +/** + * Creates a memoized function whose input is an object, caching results by + * object identity. + * + * **When to use** + * + * Use to reuse the result of a synchronous computation whose output is stable + * for a given object reference. + * + * **Details** + * + * Each memoized wrapper owns a private `WeakMap` keyed by object identity. + * Cached `undefined` results are still returned because the cache is checked + * with `WeakMap.has`. + * + * **Gotchas** + * + * Structurally equal objects do not share cache entries. If the same object is + * mutated after its first call, later calls still return the cached result for + * that reference. + * + * @category caching + * @since 4.0.0 + */ +export function memoize(f: (a: A) => O): (ast: A) => O { + const cache = new WeakMap() + return (a) => { + if (cache.has(a)) { + return cache.get(a)! + } + const result = f(a) + cache.set(a, result) + return result + } +} diff --git a/repos/effect/packages/effect/src/Graph.ts b/repos/effect/packages/effect/src/Graph.ts new file mode 100644 index 0000000..dd44727 --- /dev/null +++ b/repos/effect/packages/effect/src/Graph.ts @@ -0,0 +1,5964 @@ +/** + * Models relationships between indexed nodes and edges. + * + * This module provides immutable and scoped-mutable graph data structures. A + * graph can be directed or undirected, and it can store user-defined data on + * both nodes and edges. The module includes traversal, analysis, path-finding, + * transformation, and diagram export utilities. + * + * @since 4.0.0 + */ + +import * as Data from "./Data.ts" +import * as Equal from "./Equal.ts" +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol } from "./Inspectable.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type { Covariant, Invariant, Mutable } from "./Types.ts" + +const TypeId = "~effect/collections/Graph" + +/** + * Node index for node identification using plain numbers. + * + * **When to use** + * + * Use when storing or passing the stable identifier of a graph node between + * `Graph` operations. + * + * **Details** + * + * `addNode` allocates node identifiers from the graph's next node index. + * + * **Gotchas** + * + * A `NodeIndex` is an identifier, not an array offset. Removed node identifiers + * are not reused. + * + * @see {@link EdgeIndex} for edge identifiers instead of node identifiers + * @see {@link addNode} for creating node identifiers + * + * @category models + * @since 3.18.0 + */ +export type NodeIndex = number + +/** + * Edge index for edge identification using plain numbers. + * + * **When to use** + * + * Use when you need to keep the identifier for a graph edge so you can later + * read, update, remove, or compare that edge. + * + * **Gotchas** + * + * An `EdgeIndex` is an identifier, not an array offset. Removed edge + * identifiers are not reused. + * + * @see {@link NodeIndex} for node identifiers instead of edge identifiers + * @see {@link Edge} for the edge value addressed by this identifier + * @see {@link addEdge} for creating edge identifiers + * @see {@link getEdge} for reading edges by identifier + * + * @category models + * @since 3.18.0 + */ +export type EdgeIndex = number + +/** + * Represents edge data containing source, target, and user data. + * + * **When to use** + * + * Use as the graph edge value that carries source node, target node, and stored + * edge data together. + * + * @see {@link getEdge} for reading a single edge by identifier + * @see {@link addEdge} for adding edges to a graph + * @see {@link edges} for iterating graph edges + * + * @category models + * @since 3.18.0 + */ +export class Edge extends Data.Class<{ + readonly source: NodeIndex + readonly target: NodeIndex + readonly data: E +}> {} + +/** + * Graph type for distinguishing directed and undirected graphs. + * + * **When to use** + * + * Use when writing graph-polymorphic types or helpers that need to preserve + * whether a graph is directed or undirected. + * + * @see {@link Graph} for immutable graphs parameterized by kind + * @see {@link MutableGraph} for mutable graphs parameterized by kind + * + * @category models + * @since 3.18.0 + */ +export type Kind = "directed" | "undirected" + +/** + * Common public protocol for graph values. + * + * **Details** + * + * Contains only the runtime marker and shared protocols. Graph storage is kept + * internal; use module functions such as `nodes`, `edges`, `getNode`, and + * `getEdge` to inspect graph contents. + * + * @category models + * @since 3.18.0 + */ +export interface Proto extends Iterable, Equal.Equal, Pipeable, Inspectable { + readonly [TypeId]: Graph.Variance +} + +/** + * Immutable graph interface. + * + * **When to use** + * + * Use as the immutable graph model for code that queries, traverses, + * transforms, or analyzes graph structure without mutating it. + * + * @see {@link MutableGraph} for the mutable counterpart used inside mutation scopes + * @see {@link DirectedGraph} for a `Graph` fixed to directed edges + * @see {@link UndirectedGraph} for a `Graph` fixed to undirected edges + * + * @category models + * @since 3.18.0 + */ +export interface Graph extends Proto { + readonly type: T + readonly mutable: false +} + +/** + * Companion namespace containing type-level metadata for immutable graphs. + * + * @category models + * @since 4.0.0 + */ +export declare namespace Graph { + /** + * Type-level variance marker for immutable graphs. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + readonly _N: Covariant + readonly _E: Covariant + } +} + +/** + * Mutable graph interface. + * + * **When to use** + * + * Use when adding, removing, or updating nodes and edges inside a graph + * mutation scope. + * + * @see {@link Graph} for the immutable graph interface + * @see {@link mutate} for scoped mutation of an immutable graph + * @see {@link beginMutation} for opening a mutable graph manually + * @see {@link endMutation} for returning to an immutable graph + * + * @category models + * @since 3.18.0 + */ +export interface MutableGraph + extends Iterable, Equal.Equal, Pipeable, Inspectable +{ + readonly [TypeId]: MutableGraph.Variance + readonly type: T + readonly mutable: true +} + +/** + * Companion namespace containing type-level metadata for scoped mutable graphs. + * + * @category models + * @since 4.0.0 + */ +export declare namespace MutableGraph { + /** + * Type-level variance marker for scoped mutable graphs. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + readonly _N: Invariant + readonly _E: Invariant + } +} + +/** @internal */ +interface GraphImpl + extends Iterable, Equal.Equal, Pipeable, Inspectable +{ + readonly [TypeId]: unknown + readonly type: T + mutable: boolean + nodes: Map + edges: Map> + adjacency: Map> + reverseAdjacency: Map> + nextNodeIndex: NodeIndex + nextEdgeIndex: EdgeIndex + acyclic: Option.Option +} + +/** @internal */ +const graphImpl = ( + graph: Graph | MutableGraph +): GraphImpl => graph as unknown as GraphImpl + +/** @internal */ +const cloneAdjacency = (adjacency: Map>): Map> => { + const cloned = new Map>() + for (const [nodeIndex, edges] of adjacency) { + cloned.set(nodeIndex, [...edges]) + } + return cloned +} + +/** + * Immutable graph type for source-to-target relationships. + * + * **When to use** + * + * Use as the immutable graph type when edge direction is part of the model and + * traversal or neighbor queries should follow source-to-target edges. + * + * **Details** + * + * `DirectedGraph` is a `Graph` with node data of type + * `N` and edge data of type `E`. + * + * @see {@link directed} for constructing directed graphs + * @see {@link Graph} for the generic immutable graph type + * @see {@link UndirectedGraph} for graphs whose edges connect both endpoints + * @see {@link MutableDirectedGraph} for the mutable directed graph type + * + * @category models + * @since 3.18.0 + */ +export type DirectedGraph = Graph + +/** + * Immutable graph type for relationships without source-to-target direction. + * + * **When to use** + * + * Use when modeling relationships where each edge connects both endpoints + * without a source-to-target direction. + * + * **Details** + * + * `UndirectedGraph` is a `Graph`. + * + * @see {@link undirected} for constructing undirected graphs + * @see {@link DirectedGraph} for graphs whose edges have source-to-target direction + * @see {@link MutableUndirectedGraph} for the mutable undirected graph type + * + * @category models + * @since 3.18.0 + */ +export type UndirectedGraph = Graph + +/** + * Mutable directed graph type alias. + * + * **When to use** + * + * Use when annotating a temporary graph value that can be changed in place and + * whose edges have source-to-target direction. + * + * @see {@link MutableGraph} for the generic mutable graph type + * @see {@link DirectedGraph} for the immutable directed graph type + * @see {@link MutableUndirectedGraph} for mutable graphs without edge direction + * + * @category models + * @since 3.18.0 + */ +export type MutableDirectedGraph = MutableGraph + +/** + * Mutable undirected graph type alias. + * + * **When to use** + * + * Use when annotating a temporary graph value that can be changed in place and + * whose edges connect both endpoints without direction. + * + * @see {@link MutableDirectedGraph} for mutable graphs with directed edges + * @see {@link UndirectedGraph} for the immutable undirected graph type + * @see {@link MutableGraph} for the generic mutable graph type + * + * @category models + * @since 3.18.0 + */ +export type MutableUndirectedGraph = MutableGraph + +// ============================================================================= +// Proto Objects +// ============================================================================= + +/** @internal */ +const edgeEquals = (type: Kind, self: Edge, that: Edge): boolean => + (type === "directed" + ? self.source === that.source && self.target === that.target + : (self.source === that.source && self.target === that.target) || + (self.source === that.target && self.target === that.source)) && + Equal.equals(self.data, that.data) + +/** @internal */ +const edgeHash = (type: Kind, edge: Edge): number => + type === "directed" + ? Hash.hash(edge) + : Hash.optimize(Hash.hash(edge.data) ^ (Hash.hash(edge.source) + Hash.hash(edge.target))) + +/** @internal */ +const ProtoGraph = { + [TypeId]: { + _N: (_: never) => _, + _E: (_: never) => _ + }, + [Symbol.iterator](this: GraphImpl) { + return this.nodes[Symbol.iterator]() + }, + [NodeInspectSymbol](this: GraphImpl) { + return this.toJSON() + }, + [Equal.symbol](this: GraphImpl, that: Equal.Equal): boolean { + if (isGraph(that)) { + const thatImpl = graphImpl(that) + if ( + this.nodes.size !== thatImpl.nodes.size || + this.edges.size !== thatImpl.edges.size || + this.nextNodeIndex !== thatImpl.nextNodeIndex || + this.nextEdgeIndex !== thatImpl.nextEdgeIndex || + this.type !== thatImpl.type + ) { + return false + } + // Compare nodes + for (const [nodeIndex, nodeData] of this.nodes) { + if (!thatImpl.nodes.has(nodeIndex)) { + return false + } + const otherNodeData = thatImpl.nodes.get(nodeIndex)! + if (!Equal.equals(nodeData, otherNodeData)) { + return false + } + } + // Compare edges + for (const [edgeIndex, edgeData] of this.edges) { + if (!thatImpl.edges.has(edgeIndex)) { + return false + } + const otherEdge = thatImpl.edges.get(edgeIndex)! + if (!edgeEquals(this.type, edgeData, otherEdge)) { + return false + } + } + return true + } + return false + }, + [Hash.symbol](this: GraphImpl): number { + let hash = Hash.string("Graph") + hash = hash ^ Hash.string(this.type) + hash = hash ^ Hash.number(this.nodes.size) + hash = hash ^ Hash.number(this.edges.size) + hash = hash ^ Hash.number(this.nextNodeIndex) + hash = hash ^ Hash.number(this.nextEdgeIndex) + for (const [nodeIndex, nodeData] of this.nodes) { + hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData)) + } + for (const [edgeIndex, edgeData] of this.edges) { + hash = hash ^ (Hash.hash(edgeIndex) + edgeHash(this.type, edgeData)) + } + return hash + }, + toJSON(this: GraphImpl) { + return { + _id: "Graph", + nodeCount: this.nodes.size, + edgeCount: this.edges.size, + type: this.type + } + }, + toString(this: GraphImpl) { + return `Graph(${this.type}, ${this.nodes.size}, ${this.edges.size})` + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Errors +// ============================================================================= + +// TODO: Do we need safe variants for these? + +/** + * Error thrown by graph operations when the requested graph structure is + * invalid, such as referencing a missing node or using unsupported edge + * weights. + * + * **When to use** + * + * Use when handling failures thrown by graph operations that reject invalid + * graph structure or unsupported algorithm inputs. + * + * @category errors + * @since 3.18.0 + */ +export class GraphError extends Data.TaggedError("GraphError")<{ + readonly message: string +}> {} + +/** @internal */ +const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` }) + +/** @internal */ +function assertMutable( + graph: Graph | MutableGraph +): asserts graph is MutableGraph { + if (!graph.mutable) { + throw new GraphError({ message: "Graph is not mutable" }) + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Returns `true` if a value has the graph runtime type identifier, narrowing + * it to an immutable or mutable graph. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a graph value. + * + * **Gotchas** + * + * This guard checks the shared graph runtime type identifier and does not + * distinguish immutable graphs from mutable graphs or directed graphs from + * undirected graphs. + * + * @category guards + * @since 4.0.0 + */ +export const isGraph = ( + u: U | Graph | MutableGraph +): u is Graph | MutableGraph => hasProperty(u, TypeId) + +/** + * Creates a graph constructor for the specified graph kind. + * + * **When to use** + * + * Use when the graph kind is selected dynamically. Prefer `directed` or + * `undirected` when the kind is known statically. + * + * **Example** (Constructing by kind) + * + * ```ts + * import { Graph } from "effect" + * + * const makeGraph = Graph.make("directed") + * const graph = makeGraph((mutable) => { + * Graph.addNode(mutable, "A") + * }) + * + * console.log(graph.type) // "directed" + * ``` + * + * @see {@link directed} for constructing a directed graph directly + * @see {@link undirected} for constructing an undirected graph directly + * @category constructors + * @since 4.0.0 + */ +export const make = + (type: T) => (mutate?: (mutable: MutableGraph) => undefined): Graph => { + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = type + graph.nodes = new Map() + graph.edges = new Map() + graph.adjacency = new Map() + graph.reverseAdjacency = new Map() + graph.nextNodeIndex = 0 + graph.nextEdgeIndex = 0 + graph.acyclic = Option.some(true) + + if (mutate === undefined) { + graph.mutable = false + return graph as unknown as Graph + } + + graph.mutable = true + const mutable = Equal.byReferenceUnsafe(graph as unknown as MutableGraph) + return mutateScoped(mutable, mutate) + } + +/** + * Creates a directed graph, optionally with initial mutations. + * + * **Example** (Creating a directed graph) + * + * ```ts + * import { Graph } from "effect" + * + * // Directed graph with initial nodes and edges + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * }) + * ``` + * + * @category constructors + * @since 3.18.0 + */ +export const directed: ( + mutate?: (mutable: MutableDirectedGraph) => undefined +) => DirectedGraph = make("directed") + +/** + * Creates an undirected graph, optionally with initial mutations. + * + * **Example** (Creating an undirected graph) + * + * ```ts + * import { Graph } from "effect" + * + * // Undirected graph with initial nodes and edges + * const graph = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * ``` + * + * @category constructors + * @since 3.18.0 + */ +export const undirected: ( + mutate?: (mutable: MutableUndirectedGraph) => undefined +) => UndirectedGraph = make("undirected") + +// ============================================================================= +// Scoped Mutable API +// ============================================================================= + +/** + * Creates a mutable scope for safe graph mutations by copying the data structure. + * + * **Example** (Beginning a mutation scope) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const mutable = Graph.beginMutation(graph) + * // Now mutable can be safely modified without affecting original graph + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const beginMutation = ( + graph: Graph +): MutableGraph => { + const source = graphImpl(graph) + const adjacency = cloneAdjacency(source.adjacency) + const reverseAdjacency = cloneAdjacency(source.reverseAdjacency) + + const mutable: Mutable> = Object.create(ProtoGraph) + mutable.type = source.type + mutable.nodes = new Map(source.nodes) + mutable.edges = new Map(source.edges) + mutable.adjacency = adjacency + mutable.reverseAdjacency = reverseAdjacency + mutable.nextNodeIndex = source.nextNodeIndex + mutable.nextEdgeIndex = source.nextEdgeIndex + mutable.acyclic = source.acyclic + mutable.mutable = true + + return Equal.byReferenceUnsafe(mutable as unknown as MutableGraph) +} + +/** + * Converts a mutable graph back to an immutable graph, ending the mutation scope. + * + * **Details** + * + * Finalizes the mutable handle. Later public mutation operations on that handle + * fail with a `GraphError`. + * + * **Example** (Ending a mutation scope) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const mutable = Graph.beginMutation(graph) + * // ... perform mutations on mutable ... + * const newGraph = Graph.endMutation(mutable) + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const endMutation = ( + mutable: MutableGraph +): Graph => { + assertMutable(mutable) + const source = graphImpl(mutable) + + const graph: Mutable> = Object.create(ProtoGraph) + graph.type = source.type + graph.nodes = new Map(source.nodes) + graph.edges = new Map(source.edges) + graph.adjacency = cloneAdjacency(source.adjacency) + graph.reverseAdjacency = cloneAdjacency(source.reverseAdjacency) + graph.nextNodeIndex = source.nextNodeIndex + graph.nextEdgeIndex = source.nextEdgeIndex + graph.acyclic = source.acyclic + graph.mutable = false + source.mutable = false + + return graph as unknown as Graph +} + +/** @internal */ +const mutateScoped = ( + mutable: MutableGraph, + f: (mutable: MutableGraph) => undefined +): Graph => { + let graph: Graph + try { + f(mutable) + } finally { + graph = endMutation(mutable) + } + return graph +} + +/** + * Performs scoped mutations on a graph, automatically managing the mutation lifecycle. + * + * **Example** (Applying scoped mutations) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed() + * const newGraph = Graph.mutate(graph, (mutable) => { + * const nodeA = Graph.addNode(mutable, "A") + * const nodeB = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * }) + * + * console.log(Graph.nodeCount(newGraph)) // 2 + * console.log(Graph.edgeCount(newGraph)) // 1 + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const mutate: { + ( + f: (mutable: MutableGraph) => undefined + ): (graph: Graph) => Graph + ( + graph: Graph, + f: (mutable: MutableGraph) => undefined + ): Graph +} = dual(2, ( + graph: Graph, + f: (mutable: MutableGraph) => undefined +): Graph => { + const mutable = beginMutation(graph) + return mutateScoped(mutable, f) +}) + +// ============================================================================= +// Set Operations +// ============================================================================= + +/** @internal */ +type NodeMaps = { + readonly byIdentity: MutableHashMap.MutableHashMap + readonly byIndex: Map +} + +/** @internal */ +class EdgeIdentity implements Equal.Equal { + readonly type: Kind + readonly source: NI + readonly target: NI + readonly identity: EI + + constructor( + type: Kind, + source: NI, + target: NI, + identity: EI + ) { + this.type = type + this.source = source + this.target = target + this.identity = identity + } + + [Equal.symbol](that: Equal.Equal): boolean { + if (!(that instanceof EdgeIdentity) || this.type !== that.type || !Equal.equals(this.identity, that.identity)) { + return false + } + + if (this.type === "directed") { + return Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target) + } + + return ( + (Equal.equals(this.source, that.source) && Equal.equals(this.target, that.target)) || + (Equal.equals(this.source, that.target) && Equal.equals(this.target, that.source)) + ) + } + + [Hash.symbol](): number { + const hash = Hash.hash(this.identity) + return this.type === "directed" + ? Hash.combine(Hash.hash(this.target))(Hash.combine(Hash.hash(this.source))(hash)) + : Hash.optimize(hash ^ (Hash.hash(this.source) + Hash.hash(this.target))) + } +} + +/** + * Configures node and edge identity for graph set operations. + * + * **Details** + * + * Both functions default to using the complete node or edge data. Edge identity + * also includes the identities of its endpoint nodes and the graph kind. + * + * **Gotchas** + * + * Edge identity defines set membership, not edge multiplicity. Parallel edges + * with the same endpoint identities and projected edge identity are treated as + * the same member by graph set operations. + * + * @category models + * @since 4.0.0 + */ +export interface IdentityOptions { + readonly nodeIdentity?: (node: N) => NI + readonly edgeIdentity?: (edge: E) => EI +} + +/** @internal */ +const buildNodeMaps = ( + graph: Graph, + identity: (node: N) => I +): NodeMaps => { + const impl = graphImpl(graph) + const byIdentity = MutableHashMap.empty() + const byIndex = new Map() + + for (const [index, data] of impl.nodes) { + const nodeIdentity = identity(data) + MutableHashMap.set(byIdentity, nodeIdentity, data) + byIndex.set(index, nodeIdentity) + } + + return { byIdentity, byIndex } +} + +/** @internal */ +const nodeIdentityAt = (maps: NodeMaps, index: NodeIndex): I => maps.byIndex.get(index) as I + +/** @internal */ +const buildEdgeMap = ( + graph: Graph, + nodeMaps: NodeMaps, + identity: (edge: E) => EI +): MutableHashMap.MutableHashMap, E> => { + const impl = graphImpl(graph) + const edges = MutableHashMap.empty, E>() + for (const edge of impl.edges.values()) { + const sourceIdentity = nodeIdentityAt(nodeMaps, edge.source) + const targetIdentity = nodeIdentityAt(nodeMaps, edge.target) + MutableHashMap.set( + edges, + new EdgeIdentity(graph.type, sourceIdentity, targetIdentity, identity(edge.data)), + edge.data + ) + } + return edges +} + +/** @internal */ +const addNodesByIdentity = ( + mutable: MutableGraph, + nodes: Iterable +): MutableHashMap.MutableHashMap => { + const indexByIdentity = MutableHashMap.empty() + for (const [identity, data] of nodes) { + MutableHashMap.set(indexByIdentity, identity, addNode(mutable, data)) + } + return indexByIdentity +} + +/** @internal */ +const addEdgeByIdentity = ( + mutable: MutableGraph, + indexByIdentity: MutableHashMap.MutableHashMap, + identity: EdgeIdentity, + data: E +): void => { + const sourceIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.source)) + const targetIndex = Option.getOrUndefined(MutableHashMap.get(indexByIdentity, identity.target)) + if (sourceIndex !== undefined && targetIndex !== undefined) { + addEdge(mutable, sourceIndex, targetIndex, data) + } +} + +/** @internal */ +const assertSameKind = (self: Graph, that: Graph): void => { + if (self.type !== that.type) { + throw new GraphError({ message: `Cannot combine ${self.type} and ${that.type} graphs` }) + } +} + +/** + * Composes two graphs, merging nodes by identity. + * + * **Details** + * + * Nodes and edges present in both graphs use data from `that`. The result has + * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do + * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and + * edge data. Edge identity also includes the endpoint identities. + * + * `G1 ∪ G2 = {V1 ∪ V2, E1 ∪ E2}` + * + * **Gotchas** + * + * Nodes with equal identities in one input graph are coalesced. The last node + * supplies the data, and redirected edges can collapse or become self-loops. + * Parallel edges with equal identities are also coalesced, with the last edge + * supplying the data. + * + * **Example** (Combining graphs) + * + * ```ts + * import { Graph } from "effect" + * + * const left = Graph.directed<{ id: string }, string>((mutable) => { + * const a = Graph.addNode(mutable, { id: "A" }) + * const b = Graph.addNode(mutable, { id: "B" }) + * Graph.addEdge(mutable, a, b, "A-B") + * }) + * + * const right = Graph.directed<{ id: string }, string>((mutable) => { + * const b = Graph.addNode(mutable, { id: "B" }) + * const c = Graph.addNode(mutable, { id: "C" }) + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * + * const result = Graph.compose(left, right, { + * nodeIdentity: (node) => node.id + * }) + * + * console.log(Graph.nodeCount(result)) // 3 + * console.log(Graph.edgeCount(result)) // 2 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const compose: { + ( + that: Graph, + options?: IdentityOptions + ): (self: Graph>) => Graph + ( + self: Graph, + that: Graph>, + options?: IdentityOptions + ): Graph +} = dual( + (args) => isGraph(args[0]) && isGraph(args[1]), + ( + self: Graph, + that: Graph, + options?: IdentityOptions + ): Graph => { + assertSameKind(self, that) + const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI) + const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI) + const selfMaps = buildNodeMaps(self, getNodeIdentity) + const thatMaps = buildNodeMaps(that, getNodeIdentity) + const nodes = MutableHashMap.empty() + const edges = buildEdgeMap(self, selfMaps, getEdgeIdentity) + + for (const [identity, data] of selfMaps.byIdentity) { + MutableHashMap.set(nodes, identity, data) + } + for (const [identity, data] of thatMaps.byIdentity) { + MutableHashMap.set(nodes, identity, data) + } + + for (const [identity, data] of buildEdgeMap(that, thatMaps, getEdgeIdentity)) { + MutableHashMap.set(edges, identity, data) + } + + return make(self.type)((mutable) => { + const indexByIdentity = addNodesByIdentity(mutable, nodes) + + for (const [identity, data] of edges) { + addEdgeByIdentity(mutable, indexByIdentity, identity, data) + } + }) + } +) + +/** + * Returns the intersection of two graphs, matching nodes by identity. + * + * **Details** + * + * Node data comes from `self`, and edge data comes from `that`. The result has + * the same graph kind as `self`. Throws a `GraphError` when the graph kinds do + * not match. `nodeIdentity` and `edgeIdentity` default to the complete node and + * edge data. Edge identity also includes the endpoint identities. + * + * `G1 ∩ G2 = {V1 ∩ V2, E1 ∩ E2}` + * + * **Gotchas** + * + * Nodes with equal identities in one input graph are coalesced. The last node + * supplies the data, and redirected edges can collapse or become self-loops. + * The result contains at most one edge for each shared edge identity. + * + * **Example** (Finding shared structure) + * + * ```ts + * import { Graph } from "effect" + * + * const left = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "shared") + * }) + * + * const right = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "shared") + * }) + * + * const result = Graph.intersection(left, right) + * + * console.log(Graph.nodeCount(result)) // 2 + * console.log(Graph.edgeCount(result)) // 1 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const intersection: { + ( + that: Graph, + options?: IdentityOptions + ): (self: Graph>) => Graph + ( + self: Graph, + that: Graph>, + options?: IdentityOptions + ): Graph +} = dual((args) => isGraph(args[0]) && isGraph(args[1]), ( + self: Graph, + that: Graph, + options?: IdentityOptions +): Graph => { + assertSameKind(self, that) + const thatImpl = graphImpl(that) + const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI) + const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI) + const selfMaps = buildNodeMaps(self, getNodeIdentity) + const thatMaps = buildNodeMaps(that, getNodeIdentity) + const nodes = MutableHashMap.empty() + const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity) + const thatEdges = MutableHashMap.empty, E>() + + for (const [identity, data] of selfMaps.byIdentity) { + if (MutableHashMap.has(thatMaps.byIdentity, identity)) { + MutableHashMap.set(nodes, identity, data) + } + } + + for (const edge of thatImpl.edges.values()) { + const sourceIdentity = nodeIdentityAt(thatMaps, edge.source) + const targetIdentity = nodeIdentityAt(thatMaps, edge.target) + if (MutableHashMap.has(nodes, sourceIdentity) && MutableHashMap.has(nodes, targetIdentity)) { + const edgeIdentity = new EdgeIdentity(that.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data)) + MutableHashMap.set(thatEdges, edgeIdentity, edge.data) + } + } + + return make(self.type)((mutable) => { + const indexByIdentity = addNodesByIdentity(mutable, nodes) + + for (const [identity, data] of thatEdges) { + if (MutableHashMap.has(selfEdges, identity)) { + addEdgeByIdentity(mutable, indexByIdentity, identity, data) + } + } + }) +}) + +/** + * Returns `self` without edges also present in `that`. + * + * **Details** + * + * All nodes from `self` are preserved. Edges are matched by endpoint and edge + * identities. The result has the same graph kind as `self`. Throws a + * `GraphError` when the graph kinds do not match. `nodeIdentity` and + * `edgeIdentity` default to the complete node and edge data. + * + * `G1 \ G2 = {V1, E1 \ E2}` + * + * **Gotchas** + * + * Nodes with equal identities in one input graph are coalesced. The last node + * supplies the data, and redirected edges can collapse or become self-loops. + * If `that` contains an edge identity, every parallel edge with that identity + * is removed from `self`. + * + * **Example** (Removing shared edges) + * + * ```ts + * import { Graph } from "effect" + * + * const left = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * + * const right = Graph.directed((mutable) => { + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * + * const result = Graph.difference(left, right) + * + * console.log(Graph.nodeCount(result)) // 3 + * console.log(Graph.edgeCount(result)) // 1 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const difference: { + ( + that: Graph, + options?: IdentityOptions + ): (self: Graph>) => Graph + ( + self: Graph, + that: Graph>, + options?: IdentityOptions + ): Graph +} = dual((args) => isGraph(args[0]) && isGraph(args[1]), ( + self: Graph, + that: Graph, + options?: IdentityOptions +): Graph => { + assertSameKind(self, that) + const selfImpl = graphImpl(self) + const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI) + const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI) + const selfMaps = buildNodeMaps(self, getNodeIdentity) + const thatMaps = buildNodeMaps(that, getNodeIdentity) + const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity) + + return make(self.type)((mutable) => { + const indexByIdentity = addNodesByIdentity(mutable, selfMaps.byIdentity) + + for (const edge of selfImpl.edges.values()) { + const sourceIdentity = nodeIdentityAt(selfMaps, edge.source) + const targetIdentity = nodeIdentityAt(selfMaps, edge.target) + const edgeIdentity = new EdgeIdentity(self.type, sourceIdentity, targetIdentity, getEdgeIdentity(edge.data)) + if (!MutableHashMap.has(thatEdges, edgeIdentity)) { + addEdgeByIdentity(mutable, indexByIdentity, edgeIdentity, edge.data) + } + } + }) +}) + +/** + * Returns edges present in exactly one of two graphs. + * + * **Details** + * + * Keeps nodes from both graphs. Overlapping nodes use data from `that`. The + * result has the same graph kind as `self`. Throws a `GraphError` when the + * graph kinds do not match. `nodeIdentity` and `edgeIdentity` default to the + * complete node and edge data. Edge identity also includes the endpoint + * identities. + * + * `G1 Δ G2 = {V1 ∪ V2, (E1 ∪ E2) \ (E1 ∩ E2)}` + * + * **Gotchas** + * + * Edges with different projected identities are distinct. + * Nodes with equal identities in one input graph are coalesced. The last node + * supplies the data, and redirected edges can collapse or become self-loops. + * Parallel edges with equal identities are coalesced before the graphs are + * compared. + * + * **Example** (Finding differing edges) + * + * ```ts + * import { Graph } from "effect" + * + * const left = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * + * const right = Graph.directed((mutable) => { + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * const d = Graph.addNode(mutable, "D") + * Graph.addEdge(mutable, b, c, "B-C") + * Graph.addEdge(mutable, c, d, "C-D") + * }) + * + * const result = Graph.symmetricDifference(left, right) + * + * console.log(Graph.nodeCount(result)) // 4 + * console.log(Graph.edgeCount(result)) // 2 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const symmetricDifference: { + ( + that: Graph, + options?: IdentityOptions + ): (self: Graph>) => Graph + ( + self: Graph, + that: Graph>, + options?: IdentityOptions + ): Graph +} = dual((args) => isGraph(args[0]) && isGraph(args[1]), ( + self: Graph, + that: Graph, + options?: IdentityOptions +): Graph => { + assertSameKind(self, that) + const getNodeIdentity = options?.nodeIdentity ?? ((node: N) => node as unknown as NI) + const getEdgeIdentity = options?.edgeIdentity ?? ((edge: E) => edge as unknown as EI) + const selfMaps = buildNodeMaps(self, getNodeIdentity) + const thatMaps = buildNodeMaps(that, getNodeIdentity) + const nodes = MutableHashMap.empty() + const selfEdges = buildEdgeMap(self, selfMaps, getEdgeIdentity) + const thatEdges = buildEdgeMap(that, thatMaps, getEdgeIdentity) + + for (const [identity, data] of selfMaps.byIdentity) { + MutableHashMap.set(nodes, identity, data) + } + + for (const [identity, data] of thatMaps.byIdentity) { + MutableHashMap.set(nodes, identity, data) + } + + return make(self.type)((mutable) => { + const indexByIdentity = addNodesByIdentity(mutable, nodes) + + for (const [identity, data] of selfEdges) { + if (!MutableHashMap.has(thatEdges, identity)) { + addEdgeByIdentity(mutable, indexByIdentity, identity, data) + } + } + + for (const [identity, data] of thatEdges) { + if (!MutableHashMap.has(selfEdges, identity)) { + addEdgeByIdentity(mutable, indexByIdentity, identity, data) + } + } + }) +}) + +/** + * Returns the complement over the existing node set. + * + * **Details** + * + * Adds every missing edge between distinct nodes. The `createEdge` function + * receives the source and target node data for each added edge. The result has + * the same graph kind as `self`. + * + * `G' = {V, (V x V) \ E}` + * + * **Example** (Finding missing relationships) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A-B") + * }) + * + * const result = Graph.complement(graph, (source, target) => `${source}-${target}`) + * + * console.log(Graph.edgeCount(result)) // 1 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const complement: { + ( + createEdge: (source: N, target: N) => E + ): (self: Graph) => Graph + ( + self: Graph, + createEdge: (source: N, target: N) => E + ): Graph +} = dual(2, ( + self: Graph, + createEdge: (source: N, target: N) => E +): Graph => { + const selfImpl = graphImpl(self) + const nodeEntries = Array.from(selfImpl.nodes) + + return make(self.type)((mutable) => { + const newIndexMap = new Map() + + for (const [oldIndex, data] of nodeEntries) { + newIndexMap.set(oldIndex, addNode(mutable, data)) + } + + for (let i = 0; i < nodeEntries.length; i++) { + const [sourceOldIndex, sourceData] = nodeEntries[i] + const start = self.type === "undirected" ? i + 1 : 0 + + for (let j = start; j < nodeEntries.length; j++) { + const [targetOldIndex, targetData] = nodeEntries[j] + if (sourceOldIndex === targetOldIndex || hasEdge(self, sourceOldIndex, targetOldIndex)) { + continue + } + + const sourceIndex = newIndexMap.get(sourceOldIndex) + const targetIndex = newIndexMap.get(targetOldIndex) + if (sourceIndex !== undefined && targetIndex !== undefined) { + addEdge(mutable, sourceIndex, targetIndex, createEdge(sourceData, targetData)) + } + } + } + }) +}) + +/** + * Configuration for selecting a graph neighborhood. + * + * **Details** + * + * `radius` limits the edge distance from the center node and defaults to `1`. + * `direction` controls how directed edges are traversed and defaults to + * `"outgoing"`. + * + * @category models + * @since 4.0.0 + */ +export interface NeighborhoodConfig { + readonly radius?: number + readonly direction?: TraversalDirection +} + +/** + * Returns the induced subgraph containing nodes within a radius of a node. + * + * **Details** + * + * The `radius` option is the maximum edge distance from `nodeIndex` and + * defaults to `1`. The `direction` option controls directed graph traversal and + * defaults to `"outgoing"`. The result has the same graph kind as `self` and + * keeps all original edges whose endpoints are both reached. `"undirected"` + * ignores edge direction while finding reachable nodes. + * + * **Example** (Getting a local neighborhood) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, b, c, "B-C") + * }) + * + * const result = Graph.neighborhood(graph, 1, { radius: 1 }) + * + * console.log(Graph.nodeCount(result)) // 2 + * ``` + * + * @category set operations + * @since 4.0.0 + */ +export const neighborhood: { + ( + nodeIndex: NodeIndex, + options?: NeighborhoodConfig + ): (self: Graph) => Graph + ( + self: Graph, + nodeIndex: NodeIndex, + options?: NeighborhoodConfig + ): Graph +} = dual((args) => isGraph(args[0]), ( + self: Graph, + nodeIndex: NodeIndex, + options?: NeighborhoodConfig +): Graph => { + const selfImpl = graphImpl(self) + const radius = options?.radius ?? 1 + const direction = options?.direction ?? "outgoing" + const reached = new Set() + + for (const index of indices(bfs(self, { start: [nodeIndex], direction, radius }))) { + reached.add(index) + } + + return make(self.type)((mutable) => { + const newIndexMap = new Map() + + for (const oldIndex of reached) { + newIndexMap.set(oldIndex, addNode(mutable, Option.getOrThrow(getNode(self, oldIndex)))) + } + + for (const edge of selfImpl.edges.values()) { + if (reached.has(edge.source) && reached.has(edge.target)) { + const sourceIndex = newIndexMap.get(edge.source) + const targetIndex = newIndexMap.get(edge.target) + if (sourceIndex !== undefined && targetIndex !== undefined) { + addEdge(mutable, sourceIndex, targetIndex, edge.data) + } + } + } + }) +}) + +/** + * Returns the disjoint union of two graphs. + * + * **Details** + * + * Copies all nodes and edges from both graphs without merging equal node data. + * The result has the same graph kind as `self`. Throws a `GraphError` when the + * graph kinds do not match. + * + * `G1 + G2 = {disjoint V1 + V2, disjoint E1 + E2}` + * + * @category set operations + * @since 4.0.0 + */ +export const sum: { + (that: Graph): (self: Graph>) => Graph + (self: Graph, that: Graph>): Graph +} = dual(2, (self: Graph, that: Graph): Graph => { + assertSameKind(self, that) + return make(self.type)((mutable) => { + const copyInto = (graph: Graph) => { + const impl = graphImpl(graph) + const indexMap = new Map() + + for (const [oldIndex, data] of impl.nodes) { + indexMap.set(oldIndex, addNode(mutable, data)) + } + + for (const edge of impl.edges.values()) { + const sourceIndex = indexMap.get(edge.source) + const targetIndex = indexMap.get(edge.target) + if (sourceIndex !== undefined && targetIndex !== undefined) { + addEdge(mutable, sourceIndex, targetIndex, edge.data) + } + } + } + + copyInto(self) + copyInto(that) + }) +}) + +// ============================================================================= +// Basic Node Operations +// ============================================================================= + +/** + * Adds a new node to a mutable graph and returns its index. + * + * **When to use** + * + * Use to allocate a new node in a mutable graph before storing edges or + * querying it by index. + * + * **Details** + * + * The returned index is allocated from the graph's next node index. The mutable + * graph stores the node data and initializes empty incoming and outgoing edge + * indexes for the new node. + * + * **Gotchas** + * + * `NodeIndex` values are identifiers and are not reused after removals. + * + * **Example** (Adding nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * console.log(nodeA) // NodeIndex with value 0 + * console.log(nodeB) // NodeIndex with value 1 + * }) + * ``` + * + * @see {@link mutate} for obtaining a mutable graph from an immutable graph + * @see {@link addEdge} for connecting existing nodes + * @see {@link removeNode} for removing nodes from a mutable graph + * + * @category mutations + * @since 3.18.0 + */ +export const addNode = ( + mutable: MutableGraph, + data: N +): NodeIndex => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const nodeIndex = impl.nextNodeIndex + + // Add node data + impl.nodes.set(nodeIndex, data) + + // Initialize empty adjacency lists + impl.adjacency.set(nodeIndex, []) + impl.reverseAdjacency.set(nodeIndex, []) + + // Update graph allocators + impl.nextNodeIndex = impl.nextNodeIndex + 1 + + return nodeIndex +} + +/** + * Gets the data associated with a node index safely, if it exists. + * + * **Example** (Getting node data) + * + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * }) + * + * const nodeIndex = 0 + * const nodeData = Graph.getNode(graph, nodeIndex) + * + * if (Option.isSome(nodeData)) { + * console.log(nodeData.value) // "Node A" + * } + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const getNode: { + (nodeIndex: NodeIndex): ( + graph: Graph | MutableGraph + ) => Option.Option + ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex + ): Option.Option +} = dual(2, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Option.Option => { + const impl = graphImpl(graph) + return impl.nodes.has(nodeIndex) ? Option.some(impl.nodes.get(nodeIndex)!) : Option.none() +}) + +/** + * Checks whether a node with the given index exists in the graph. + * + * **Example** (Checking node existence) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * }) + * + * const nodeIndex = 0 + * const exists = Graph.hasNode(graph, nodeIndex) + * console.log(exists) // true + * + * const nonExistentIndex = 999 + * const notExists = Graph.hasNode(graph, nonExistentIndex) + * console.log(notExists) // false + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const hasNode: { + (nodeIndex: NodeIndex): (graph: Graph | MutableGraph) => boolean + (graph: Graph | MutableGraph, nodeIndex: NodeIndex): boolean +} = dual(2, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): boolean => graphImpl(graph).nodes.has(nodeIndex)) + +/** + * Returns the number of nodes in the graph. + * + * **Example** (Counting nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const emptyGraph = Graph.directed() + * console.log(Graph.nodeCount(emptyGraph)) // 0 + * + * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Node C") + * }) + * + * console.log(Graph.nodeCount(graphWithNodes)) // 3 + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const nodeCount = ( + graph: Graph | MutableGraph +): number => graphImpl(graph).nodes.size + +/** + * Finds the first node that matches the given predicate. + * + * **Example** (Finding the first matching node) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Node C") + * }) + * + * const result = Graph.findNode(graph, (data) => data.startsWith("Node B")) + * console.log(result) // Option.some(1) + * + * const notFound = Graph.findNode(graph, (data) => data === "Node D") + * console.log(notFound) // Option.none() + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const findNode: { + ( + predicate: (data: N) => boolean + ): (graph: Graph | MutableGraph) => Option.Option + ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean + ): Option.Option +} = dual(2, ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean +): Option.Option => { + const impl = graphImpl(graph) + for (const [index, data] of impl.nodes) { + if (predicate(data)) { + return Option.some(index) + } + } + return Option.none() +}) + +/** + * Finds all nodes that match the given predicate. + * + * **Example** (Finding matching nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Start A") + * Graph.addNode(mutable, "Node B") + * Graph.addNode(mutable, "Start C") + * }) + * + * const result = Graph.findNodes(graph, (data) => data.startsWith("Start")) + * console.log(result) // [0, 2] + * + * const empty = Graph.findNodes(graph, (data) => data === "Not Found") + * console.log(empty) // [] + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const findNodes: { + ( + predicate: (data: N) => boolean + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean + ): Array +} = dual(2, ( + graph: Graph | MutableGraph, + predicate: (data: N) => boolean +): Array => { + const impl = graphImpl(graph) + const results: Array = [] + for (const [index, data] of impl.nodes) { + if (predicate(data)) { + results.push(index) + } + } + return results +}) + +/** + * Finds the first edge that matches the given predicate. + * + * **Example** (Finding the first matching edge) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.addEdge(mutable, nodeB, nodeC, 20) + * }) + * + * const result = Graph.findEdge(graph, (data) => data > 15) + * console.log(result) // Option.some(1) + * + * const notFound = Graph.findEdge(graph, (data) => data > 100) + * console.log(notFound) // Option.none() + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const findEdge: { + ( + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean + ): (graph: Graph | MutableGraph) => Option.Option + ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean + ): Option.Option +} = dual(2, ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean +): Option.Option => { + const impl = graphImpl(graph) + for (const [edgeIndex, edgeData] of impl.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + return Option.some(edgeIndex) + } + } + return Option.none() +}) + +/** + * Finds all edges that match the given predicate. + * + * **Example** (Finding matching edges) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.addEdge(mutable, nodeB, nodeC, 20) + * Graph.addEdge(mutable, nodeC, nodeA, 30) + * }) + * + * const result = Graph.findEdges(graph, (data) => data >= 20) + * console.log(result) // [1, 2] + * + * const empty = Graph.findEdges(graph, (data) => data > 100) + * console.log(empty) // [] + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const findEdges: { + ( + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean + ): Array +} = dual(2, ( + graph: Graph | MutableGraph, + predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean +): Array => { + const impl = graphImpl(graph) + const results: Array = [] + for (const [edgeIndex, edgeData] of impl.edges) { + if (predicate(edgeData.data, edgeData.source, edgeData.target)) { + results.push(edgeIndex) + } + } + return results +}) + +/** + * Updates a single node's data by applying a transformation function. + * + * **Example** (Updating node data) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "Node A") + * Graph.addNode(mutable, "Node B") + * Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) + * }) + * + * const nodeData = Graph.getNode(graph, 0) + * console.log(nodeData) // Option.some("NODE A") + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const updateNode = ( + mutable: MutableGraph, + index: NodeIndex, + f: (data: N) => N +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + if (!impl.nodes.has(index)) { + return + } + + const currentData = impl.nodes.get(index)! + const newData = f(currentData) + impl.nodes.set(index, newData) +} + +/** + * Updates a single edge's data by applying a transformation function. + * + * **Example** (Updating edge data) + * + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10) + * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) + * }) + * + * const edgeData = Graph.getEdge(result, 0) + * console.log(edgeData) // Option.some(new Graph.Edge({ source: 0, target: 1, data: 20 })) + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const updateEdge = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex, + f: (data: E) => E +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + if (!impl.edges.has(edgeIndex)) { + return + } + + const currentEdge = impl.edges.get(edgeIndex)! + const newData = f(currentEdge.data) + impl.edges.set(edgeIndex, new Edge({ ...currentEdge, data: newData })) +} + +/** + * Transforms every node's data in a mutable graph in place using the provided + * mapping function. + * + * **Details** + * + * Node indices and edges are preserved; only the stored node data is replaced. + * + * **Example** (Mapping node data) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "node a") + * Graph.addNode(mutable, "node b") + * Graph.addNode(mutable, "node c") + * Graph.mapNodes(mutable, (data) => data.toUpperCase()) + * }) + * + * const nodeData = Graph.getNode(graph, 0) + * console.log(nodeData) // Option.some("NODE A") + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const mapNodes = ( + mutable: MutableGraph, + f: (data: N) => N +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + // Transform existing node data in place + for (const [index, data] of impl.nodes) { + const newData = f(data) + impl.nodes.set(index, newData) + } +} + +/** + * Transforms all edge data in a mutable graph using the provided mapping function. + * + * **Example** (Mapping edge data) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 10) + * Graph.addEdge(mutable, b, c, 20) + * Graph.mapEdges(mutable, (data) => data * 2) + * }) + * + * const edgeData = Graph.getEdge(graph, 0) + * console.log(edgeData) // Option.some(new Graph.Edge({ source: 0, target: 1, data: 20 })) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const mapEdges = ( + mutable: MutableGraph, + f: (data: E) => E +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + // Transform existing edge data in place + for (const [index, edgeData] of impl.edges) { + const newData = f(edgeData.data) + impl.edges.set( + index, + new Edge({ + ...edgeData, + data: newData + }) + ) + } +} + +/** + * @internal + */ +const rebuildAdjacency = ( + mutable: GraphImpl +): void => { + mutable.adjacency.clear() + mutable.reverseAdjacency.clear() + + for (const nodeIndex of mutable.nodes.keys()) { + mutable.adjacency.set(nodeIndex, []) + mutable.reverseAdjacency.set(nodeIndex, []) + } + + for (const [edgeIndex, edgeData] of mutable.edges) { + mutable.adjacency.get(edgeData.source)!.push(edgeIndex) + mutable.reverseAdjacency.get(edgeData.target)!.push(edgeIndex) + + if (mutable.type === "undirected") { + mutable.adjacency.get(edgeData.target)!.push(edgeIndex) + mutable.reverseAdjacency.get(edgeData.source)!.push(edgeIndex) + } + } +} + +/** + * Swaps source and target nodes for every edge in a mutable graph. + * + * **Example** (Reversing edge directions) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) // A -> B + * Graph.addEdge(mutable, b, c, 2) // B -> C + * Graph.reverse(mutable) // Now B -> A, C -> B + * }) + * + * const edge0 = Graph.getEdge(graph, 0) + * console.log(edge0) // Option.some(new Graph.Edge({ source: 1, target: 0, data: 1 })) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const reverse = ( + mutable: MutableGraph +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + if (impl.type === "undirected") { + return + } + + // Reverse all edges by swapping source and target + for (const [index, edgeData] of impl.edges) { + impl.edges.set( + index, + new Edge({ + source: edgeData.target, + target: edgeData.source, + data: edgeData.data + }) + ) + } + + rebuildAdjacency(impl) + + // Invalidate cycle flag since edge directions changed + impl.acyclic = Option.none() +} + +/** + * Filters and optionally transforms nodes in a mutable graph using a predicate function. + * Nodes that return Option.none are removed along with all their connected edges. + * + * **Example** (Filtering and mapping nodes) + * + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "active") + * const b = Graph.addNode(mutable, "inactive") + * const c = Graph.addNode(mutable, "active") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 2) + * + * // Keep only "active" nodes and transform to uppercase + * Graph.filterMapNodes( + * mutable, + * (data) => + * data === "active" ? Option.some(data.toUpperCase()) : Option.none() + * ) + * }) + * + * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const filterMapNodes = ( + mutable: MutableGraph, + f: (data: N) => Option.Option +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const nodesToRemove: Array = [] + + // First pass: identify nodes to remove and transform data for nodes to keep + for (const [index, data] of impl.nodes) { + const result = f(data) + if (Option.isSome(result)) { + // Transform node data + impl.nodes.set(index, result.value) + } else { + // Mark for removal + nodesToRemove.push(index) + } + } + + // Second pass: remove filtered out nodes and their edges + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters and optionally transforms edges in a mutable graph using a predicate function. + * Edges that return Option.none are removed from the graph. + * + * **Example** (Filtering and mapping edges) + * + * ```ts + * import { Graph, Option } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, b, c, 15) + * Graph.addEdge(mutable, c, a, 25) + * + * // Keep only edges with weight >= 10 and double their weight + * Graph.filterMapEdges( + * mutable, + * (data) => data >= 10 ? Option.some(data * 2) : Option.none() + * ) + * }) + * + * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const filterMapEdges = ( + mutable: MutableGraph, + f: (data: E) => Option.Option +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const edgesToRemove: Array = [] + + // First pass: identify edges to remove and transform data for edges to keep + for (const [index, edgeData] of impl.edges) { + const result = f(edgeData.data) + if (Option.isSome(result)) { + // Transform edge data + impl.edges.set( + index, + new Edge({ + ...edgeData, + data: result.value + }) + ) + } else { + // Mark for removal + edgesToRemove.push(index) + } + } + + // Second pass: remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +/** + * Filters nodes by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * **Example** (Filtering nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * Graph.addNode(mutable, "active") + * Graph.addNode(mutable, "inactive") + * Graph.addNode(mutable, "pending") + * Graph.addNode(mutable, "active") + * + * // Keep only "active" nodes + * Graph.filterNodes(mutable, (data) => data === "active") + * }) + * + * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const filterNodes = ( + mutable: MutableGraph, + predicate: (data: N) => boolean +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const nodesToRemove: Array = [] + + // Identify nodes to remove + for (const [index, data] of impl.nodes) { + if (!predicate(data)) { + nodesToRemove.push(index) + } + } + + // Remove filtered out nodes (this also removes connected edges) + for (const nodeIndex of nodesToRemove) { + removeNode(mutable, nodeIndex) + } +} + +/** + * Filters edges by removing those that don't match the predicate. + * This function modifies the mutable graph in place. + * + * **Example** (Filtering edges) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, b, c, 15) + * Graph.addEdge(mutable, c, a, 25) + * + * // Keep only edges with weight >= 10 + * Graph.filterEdges(mutable, (data) => data >= 10) + * }) + * + * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed) + * ``` + * + * @category transforming + * @since 3.18.0 + */ +export const filterEdges = ( + mutable: MutableGraph, + predicate: (data: E) => boolean +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const edgesToRemove: Array = [] + + // Identify edges to remove + for (const [index, edgeData] of impl.edges) { + if (!predicate(edgeData.data)) { + edgesToRemove.push(index) + } + } + + // Remove filtered out edges + for (const edgeIndex of edgesToRemove) { + removeEdge(mutable, edgeIndex) + } +} + +// ============================================================================= +// Cycle Flag Management (Internal) +// ============================================================================= + +/** @internal */ +const invalidateCycleFlagOnRemoval = ( + mutable: GraphImpl +): void => { + // Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs). + if (mutable.acyclic._tag === "Some" && mutable.acyclic.value === false) { + mutable.acyclic = Option.none() + } +} + +/** @internal */ +const invalidateCycleFlagOnAddition = ( + mutable: GraphImpl +): void => { + // Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs). + if (mutable.acyclic._tag === "Some" && mutable.acyclic.value === true) { + mutable.acyclic = Option.none() + } +} + +// ============================================================================= +// Edge Operations +// ============================================================================= + +/** + * Adds a new edge to a mutable graph and returns its index. + * + * **When to use** + * + * Use to connect two existing nodes in a mutable graph while storing edge data + * and receiving the new edge identifier. + * + * **Details** + * + * Creates an `Edge` with the source, target, and data at the next edge index, + * updates adjacency indexes, and increments the graph's next edge index. + * Undirected graphs register the same edge for both endpoints. + * + * **Gotchas** + * + * The source and target nodes must already exist in the mutable graph; missing + * endpoints throw a `GraphError`. + * + * **Example** (Adding edges) + * + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) + * console.log(edge) // EdgeIndex with value 0 + * }) + * ``` + * + * @see {@link mutate} for obtaining a mutable graph from an immutable graph + * @see {@link addNode} for creating node indexes before connecting them + * @see {@link getEdge} for reading the returned edge + * @see {@link removeEdge} for removing an edge from a mutable graph + * + * @category mutations + * @since 3.18.0 + */ +export const addEdge = ( + mutable: MutableGraph, + source: NodeIndex, + target: NodeIndex, + data: E +): EdgeIndex => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + // Validate that both nodes exist + if (!impl.nodes.has(source)) { + throw missingNode(source) + } + if (!impl.nodes.has(target)) { + throw missingNode(target) + } + + const edgeIndex = impl.nextEdgeIndex + + // Create edge data + const edgeData = new Edge({ source, target, data }) + impl.edges.set(edgeIndex, edgeData) + + // Update adjacency lists + const sourceAdjacency = impl.adjacency.get(source) + if (sourceAdjacency !== undefined) { + sourceAdjacency.push(edgeIndex) + } + + const targetReverseAdjacency = impl.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + targetReverseAdjacency.push(edgeIndex) + } + + // For undirected graphs, add reverse connections + if (impl.type === "undirected") { + const targetAdjacency = impl.adjacency.get(target) + if (targetAdjacency !== undefined) { + targetAdjacency.push(edgeIndex) + } + + const sourceReverseAdjacency = impl.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + sourceReverseAdjacency.push(edgeIndex) + } + } + + // Update allocators + impl.nextEdgeIndex = impl.nextEdgeIndex + 1 + + // Only invalidate cycle flag if the graph was acyclic + // Adding edges cannot remove cycles from cyclic graphs + invalidateCycleFlagOnAddition(impl) + + return edgeIndex +} + +/** + * Removes a node and all its incident edges from a mutable graph. + * + * **Example** (Removing a node) + * + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * + * // Remove nodeA and all edges connected to it + * Graph.removeNode(mutable, nodeA) + * }) + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const removeNode = ( + mutable: MutableGraph, + nodeIndex: NodeIndex +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + // Check if node exists + if (!impl.nodes.has(nodeIndex)) { + return // Node doesn't exist, nothing to remove + } + + // Collect all incident edges for removal + const edgesToRemove: Array = [] + + // Get outgoing edges + const outgoingEdges = impl.adjacency.get(nodeIndex) + if (outgoingEdges !== undefined) { + for (const edge of outgoingEdges) { + edgesToRemove.push(edge) + } + } + + // Get incoming edges + const incomingEdges = impl.reverseAdjacency.get(nodeIndex) + if (incomingEdges !== undefined) { + for (const edge of incomingEdges) { + edgesToRemove.push(edge) + } + } + + // Remove all incident edges + for (const edgeIndex of edgesToRemove) { + removeEdgeInternal(impl, edgeIndex) + } + + // Remove the node itself + impl.nodes.delete(nodeIndex) + impl.adjacency.delete(nodeIndex) + impl.reverseAdjacency.delete(nodeIndex) + + // Only invalidate cycle flag if the graph wasn't already known to be acyclic + // Removing nodes cannot introduce cycles in an acyclic graph + invalidateCycleFlagOnRemoval(impl) +} + +/** + * Removes an edge from a mutable graph. + * + * **Example** (Removing an edge) + * + * ```ts + * import { Graph } from "effect" + * + * const result = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const edge = Graph.addEdge(mutable, nodeA, nodeB, 42) + * + * // Remove the edge + * Graph.removeEdge(mutable, edge) + * }) + * ``` + * + * @category mutations + * @since 3.18.0 + */ +export const removeEdge = ( + mutable: MutableGraph, + edgeIndex: EdgeIndex +): void => { + assertMutable(mutable) + const impl = graphImpl(mutable) + + const wasRemoved = removeEdgeInternal(impl, edgeIndex) + + // Only invalidate cycle flag if an edge was actually removed + // and only if the graph wasn't already known to be acyclic + if (wasRemoved) { + invalidateCycleFlagOnRemoval(impl) + } +} + +/** @internal */ +const removeEdgeInternal = ( + mutable: GraphImpl, + edgeIndex: EdgeIndex +): boolean => { + // Get edge data + const edge = mutable.edges.get(edgeIndex) + if (edge === undefined) { + return false // Edge doesn't exist, no mutation occurred + } + + const { source, target } = edge + + // Remove from adjacency lists + const sourceAdjacency = mutable.adjacency.get(source) + if (sourceAdjacency !== undefined) { + const index = sourceAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceAdjacency.splice(index, 1) + } + } + + const targetReverseAdjacency = mutable.reverseAdjacency.get(target) + if (targetReverseAdjacency !== undefined) { + const index = targetReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetReverseAdjacency.splice(index, 1) + } + } + + // For undirected graphs, remove reverse connections + if (mutable.type === "undirected") { + const targetAdjacency = mutable.adjacency.get(target) + if (targetAdjacency !== undefined) { + const index = targetAdjacency.indexOf(edgeIndex) + if (index !== -1) { + targetAdjacency.splice(index, 1) + } + } + + const sourceReverseAdjacency = mutable.reverseAdjacency.get(source) + if (sourceReverseAdjacency !== undefined) { + const index = sourceReverseAdjacency.indexOf(edgeIndex) + if (index !== -1) { + sourceReverseAdjacency.splice(index, 1) + } + } + } + + // Remove edge data + mutable.edges.delete(edgeIndex) + + return true // Edge was successfully removed +} + +// ============================================================================= +// Edge Query Operations +// ============================================================================= + +/** + * Gets the edge data associated with an edge index safely, if it exists. + * + * **Example** (Getting edge data) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * }) + * + * const edgeIndex = 0 + * const edgeData = Graph.getEdge(graph, edgeIndex) + * + * if (edgeData._tag === "Some") { + * console.log(edgeData.value.data) // 42 + * console.log(edgeData.value.source) // 0 + * console.log(edgeData.value.target) // 1 + * } + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const getEdge: { + (edgeIndex: EdgeIndex): ( + graph: Graph | MutableGraph + ) => Option.Option> + ( + graph: Graph | MutableGraph, + edgeIndex: EdgeIndex + ): Option.Option> +} = dual(2, ( + graph: Graph | MutableGraph, + edgeIndex: EdgeIndex +): Option.Option> => Option.fromUndefinedOr(graphImpl(graph).edges.get(edgeIndex))) + +/** + * Checks whether an edge exists between two nodes in the graph. + * + * **Example** (Checking edge existence) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 42) + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * const nodeC = 2 + * + * const hasAB = Graph.hasEdge(graph, nodeA, nodeB) + * console.log(hasAB) // true + * + * const hasAC = Graph.hasEdge(graph, nodeA, nodeC) + * console.log(hasAC) // false + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const hasEdge: { + ( + source: NodeIndex, + target: NodeIndex + ): (graph: Graph | MutableGraph) => boolean + ( + graph: Graph | MutableGraph, + source: NodeIndex, + target: NodeIndex + ): boolean +} = dual(3, ( + graph: Graph | MutableGraph, + source: NodeIndex, + target: NodeIndex +): boolean => { + const impl = graphImpl(graph) + const adjacencyList = impl.adjacency.get(source) + if (adjacencyList === undefined) { + return false + } + + // Check if any edge in the adjacency list connects to the target + for (const edgeIndex of adjacencyList) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target + if (neighbor === target) { + return true + } + } + } + + return false +}) + +/** + * Returns the number of edges in the graph. + * + * **Example** (Counting edges) + * + * ```ts + * import { Graph } from "effect" + * + * const emptyGraph = Graph.directed() + * console.log(Graph.edgeCount(emptyGraph)) // 0 + * + * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeB, nodeC, 2) + * Graph.addEdge(mutable, nodeC, nodeA, 3) + * }) + * + * console.log(Graph.edgeCount(graphWithEdges)) // 3 + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const edgeCount = ( + graph: Graph | MutableGraph +): number => graphImpl(graph).edges.size + +const getDirectedNeighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex, + direction: Direction +): Array => { + const impl = graphImpl(graph) + const adjacencyMap = direction === "incoming" + ? impl.reverseAdjacency + : impl.adjacency + + const adjacencyList = adjacencyMap.get(nodeIndex) + if (adjacencyList === undefined) { + return [] + } + + const result: Array = [] + for (const edgeIndex of adjacencyList) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + result.push(direction === "incoming" ? edge.source : edge.target) + } + } + + return result +} + +/** + * Returns the neighboring node indices for a node. + * + * **Details** + * + * For directed graphs, neighbors are the targets of outgoing edges. For + * undirected graphs, neighbors are the other endpoints of incident edges. + * + * **Example** (Getting outgoing neighbors) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeA, nodeC, 2) + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * const nodeC = 2 + * + * const neighborsA = Graph.neighbors(graph, nodeA) + * console.log(neighborsA) // [1, 2] + * + * const neighborsB = Graph.neighbors(graph, nodeB) + * console.log(neighborsB) // [] + * ``` + * + * @category getters + * @since 3.18.0 + */ +export const neighbors: { + ( + nodeIndex: NodeIndex + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex + ): Array +} = dual(2, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + // For undirected graphs, use the specialized helper that returns the other endpoint + if (graph.type === "undirected") { + return getUndirectedNeighbors(graph as any, nodeIndex) + } + + return getDirectedNeighbors(graph as Graph | MutableGraph, nodeIndex, "outgoing") +}) + +/** + * Returns the outgoing neighbor node indices for a node in a directed graph. + * + * **When to use** + * + * Use when you need the nodes reached by following outgoing edges from a node in + * a directed graph. + * + * **Gotchas** + * + * Throws a `GraphError` when used with an undirected graph. + * + * @see {@link predecessors} for incoming neighbors in a directed graph + * @see {@link neighbors} for generic neighbor lookup across graph kinds + * + * @category queries + * @since 4.0.0 + */ +export const successors: { + ( + nodeIndex: NodeIndex + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex + ): Array +} = dual(2, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + if (graph.type === "undirected") { + throw new GraphError({ message: "Cannot get successors of undirected graph" }) + } + return getDirectedNeighbors(graph as Graph | MutableGraph, nodeIndex, "outgoing") +}) + +/** + * Returns the incoming neighbor node indices for a node in a directed graph. + * + * **When to use** + * + * Use when you need the nodes that reach a node by following incoming edges in a + * directed graph. + * + * **Gotchas** + * + * Throws a `GraphError` when used with an undirected graph. + * + * @see {@link successors} for outgoing neighbors in a directed graph + * @see {@link neighbors} for generic neighbor lookup across graph kinds + * + * @category queries + * @since 4.0.0 + */ +export const predecessors: { + ( + nodeIndex: NodeIndex + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex + ): Array +} = dual(2, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + if (graph.type === "undirected") { + throw new GraphError({ message: "Cannot get predecessors of undirected graph" }) + } + return getDirectedNeighbors(graph as Graph | MutableGraph, nodeIndex, "incoming") +}) + +/** + * Gets directed neighbors of a node in a specific direction. + * + * **When to use** + * + * Use when maintaining existing code that already passes an explicit traversal + * direction. New code should prefer `successors` or `predecessors`. + * + * **Gotchas** + * + * Throws a `GraphError` when used with an undirected graph. + * + * **Example** (Traversing directed neighbors) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A->B") + * }) + * + * const nodeA = 0 + * const nodeB = 1 + * + * // Get outgoing neighbors (nodes that nodeA points to) + * const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing") + * + * // Get incoming neighbors (nodes that point to nodeB) + * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming") + * ``` + * + * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors. + * @see {@link successors} for outgoing neighbors in a directed graph + * @see {@link predecessors} for incoming neighbors in a directed graph + * @category queries + * @since 3.18.0 + */ +export const neighborsDirected: { + ( + nodeIndex: NodeIndex, + direction: Direction + ): (graph: Graph | MutableGraph) => Array + ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex, + direction: Direction + ): Array +} = dual(3, ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex, + direction: Direction +): Array => { + if (graph.type === "undirected") { + throw new GraphError({ message: "Cannot get directed neighbors of undirected graph" }) + } + return getDirectedNeighbors(graph as Graph | MutableGraph, nodeIndex, direction) +}) + +// ============================================================================= +// GraphViz Export +// ============================================================================= + +/** + * Configuration options for GraphViz DOT format generation from graphs. + * + * **Details** + * + * These options customize node labels, edge labels, and graph naming in DOT + * format compatible with GraphViz tools. + * + * **Example** (Configuring GraphViz labels) + * + * ```ts + * import type { Graph } from "effect" + * + * // Basic options with custom labels + * const basicOptions: Graph.GraphVizOptions = { + * nodeLabel: (data) => `Node: ${data}`, + * edgeLabel: (data) => `Weight: ${data}` + * } + * + * // Complete options with graph naming + * const namedOptions: Graph.GraphVizOptions = { + * nodeLabel: (data) => data.toUpperCase(), + * edgeLabel: (data) => data, + * graphName: "MyDependencyGraph" + * } + * ``` + * + * @category options + * @since 3.18.0 + */ +export interface GraphVizOptions { + /** + * Function to generate custom labels for nodes. + * Defaults to String(data) if not provided. + */ + readonly nodeLabel?: (data: N) => string + + /** + * Function to generate custom labels for edges. + * Defaults to String(data) if not provided. + */ + readonly edgeLabel?: (data: E) => string + + /** + * Name for the DOT graph. + * Defaults to "G" if not provided. + */ + readonly graphName?: string +} + +const escapeGraphVizString = (value: string): string => + value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r\n|\r|\n/g, "\\n") + +/** + * Exports a graph to GraphViz DOT format for visualization. + * + * **Example** (Exporting GraphViz DOT) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.mutate(Graph.directed(), (mutable) => { + * const nodeA = Graph.addNode(mutable, "Node A") + * const nodeB = Graph.addNode(mutable, "Node B") + * const nodeC = Graph.addNode(mutable, "Node C") + * Graph.addEdge(mutable, nodeA, nodeB, 1) + * Graph.addEdge(mutable, nodeB, nodeC, 2) + * Graph.addEdge(mutable, nodeC, nodeA, 3) + * }) + * + * const dot = Graph.toGraphViz(graph) + * console.log(dot) + * // digraph "G" { + * // "0" [label="Node A"]; + * // "1" [label="Node B"]; + * // "2" [label="Node C"]; + * // "0" -> "1" [label="1"]; + * // "1" -> "2" [label="2"]; + * // "2" -> "0" [label="3"]; + * // } + * ``` + * + * @category converting + * @since 3.18.0 + */ +export const toGraphViz: { + ( + options?: GraphVizOptions + ): (graph: Graph | MutableGraph) => string + ( + graph: Graph | MutableGraph, + options?: GraphVizOptions + ): string +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + options?: GraphVizOptions +): string => { + const impl = graphImpl(graph) + const { + edgeLabel = (data: E) => String(data), + graphName = "G", + nodeLabel = (data: N) => String(data) + } = options ?? {} + + const isDirected = graph.type === "directed" + const graphType = isDirected ? "digraph" : "graph" + const edgeOperator = isDirected ? "->" : "--" + const graphId = `"${escapeGraphVizString(graphName)}"` + + const lines: Array = [] + lines.push(`${graphType} ${graphId} {`) + + // Add nodes + for (const [nodeIndex, nodeData] of impl.nodes) { + const label = escapeGraphVizString(nodeLabel(nodeData)) + lines.push(` "${nodeIndex}" [label="${label}"];`) + } + + // Add edges + for (const [, edgeData] of impl.edges) { + const label = escapeGraphVizString(edgeLabel(edgeData.data)) + lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`) + } + + lines.push("}") + return lines.join("\n") +}) + +// ============================================================================= +// Mermaid Export +// ============================================================================= + +/** + * Mermaid node shape types for diagram visualization. + * + * **Details** + * + * Each shape produces different visual representations in Mermaid diagrams: + * - `rectangle`: Standard rectangular nodes `A["label"]` + * - `rounded`: Rounded rectangular nodes `A("label")` + * - `circle`: Circular nodes `A(("label"))` + * - `diamond`: Diamond-shaped nodes `A{"label"}` + * - `hexagon`: Hexagonal nodes `A{{"label"}}` + * - `stadium`: Stadium-shaped nodes `A(["label"])` + * - `subroutine`: Subroutine-style nodes `A[["label"]]` + * - `cylindrical`: Cylindrical database-style nodes `A[("label")]` + * + * **Example** (Selecting Mermaid node shapes) + * + * ```ts + * import type { Graph } from "effect" + * + * // Shape selector function for different node types + * const shapeSelector = (nodeData: string): Graph.MermaidNodeShape => { + * if (nodeData.includes("start") || nodeData.includes("end")) return "circle" + * if (nodeData.includes("decision")) return "diamond" + * if (nodeData.includes("process")) return "rectangle" + * if (nodeData.includes("data")) return "cylindrical" + * return "rounded" + * } + * + * const options: Graph.MermaidOptions = { + * nodeShape: shapeSelector + * } + * ``` + * + * @category models + * @since 3.18.0 + */ +export type MermaidNodeShape = + | "rectangle" // A["label"] + | "rounded" // A("label") + | "circle" // A(("label")) + | "diamond" // A{"label"} + | "hexagon" // A{{"label"}} + | "stadium" // A(["label"]) + | "subroutine" // A[["label"]] + | "cylindrical" // A[("label")] + +/** + * Mermaid diagram direction types for controlling layout orientation. + * + * **Details** + * + * Determines the flow direction of nodes and edges in the diagram: + * - `TB`/`TD`: Top to Bottom (vertical layout, default) + * - `BT`: Bottom to Top (reverse vertical) + * - `LR`: Left to Right (horizontal layout) + * - `RL`: Right to Left (reverse horizontal) + * + * **Example** (Configuring Mermaid directions) + * + * ```ts + * import type { Graph } from "effect" + * + * // Horizontal workflow diagram + * const horizontalOptions: Graph.MermaidOptions = { + * direction: "LR" + * } + * + * // Vertical hierarchy (default) + * const verticalOptions: Graph.MermaidOptions = { + * direction: "TB" + * } + * + * // Bottom-up flow + * const bottomUpOptions: Graph.MermaidOptions = { + * direction: "BT" + * } + * ``` + * + * @category models + * @since 3.18.0 + */ +export type MermaidDirection = + | "TB" // Top to Bottom (default) + | "TD" // Top Down (same as TB) + | "BT" // Bottom to Top + | "RL" // Right to Left + | "LR" // Left to Right + +/** + * Mermaid diagram types for different visualization formats. + * + * **Details** + * + * Specifies the Mermaid diagram syntax to use: + * - `flowchart`: For directed graphs with arrows (`A --> B`) + * - `graph`: For undirected graphs with lines (`A --- B`) + * + * When not specified, automatically selects based on graph type: + * directed graphs use "flowchart", undirected graphs use "graph". + * + * **Example** (Selecting Mermaid diagram types) + * + * ```ts + * import type { Graph } from "effect" + * + * // Force flowchart format (even for undirected graphs) + * const flowchartOptions: Graph.MermaidOptions = { + * diagramType: "flowchart" + * } + * + * // Force graph format (shows undirected connections) + * const graphOptions: Graph.MermaidOptions = { + * diagramType: "graph" + * } + * + * // Auto-detection (recommended, default behavior) + * const autoOptions: Graph.MermaidOptions = {} + * ``` + * + * @category models + * @since 3.18.0 + */ +export type MermaidDiagramType = + | "flowchart" // For directed graphs + | "graph" // For undirected graphs + +/** + * Configuration options for Mermaid diagram generation, following GraphViz pattern. + * + * @category models + * @since 4.0.0 + */ +/** + * Configuration options for Mermaid diagram generation from graphs. + * + * **Details** + * + * These options customize node labels, edge labels, diagram type, layout + * direction, node shapes, and graph naming in Mermaid format. + * + * **Example** (Configuring Mermaid output) + * + * ```ts + * import type { Graph } from "effect" + * + * // Basic options with custom labels + * const basicOptions: Graph.MermaidOptions = { + * nodeLabel: (data) => `Node: ${data}`, + * edgeLabel: (data) => `Weight: ${data}` + * } + * + * // Advanced options with all features + * const advancedOptions: Graph.MermaidOptions = { + * nodeLabel: (data) => data.toUpperCase(), + * edgeLabel: (data) => data, + * diagramType: "flowchart", + * direction: "LR", + * nodeShape: (data) => data.includes("start") ? "circle" : "rectangle" + * } + * ``` + * + * @category options + * @since 3.18.0 + */ +export interface MermaidOptions { + /** + * Function to generate custom labels for nodes. + * Defaults to String(data) if not provided. + */ + readonly nodeLabel?: (data: N) => string + + /** + * Function to generate custom labels for edges. + * Defaults to String(data) if not provided. + */ + readonly edgeLabel?: (data: E) => string + + /** + * Diagram type override. If not specified, automatically detects: + * - "flowchart" for directed graphs + * - "graph" for undirected graphs + */ + readonly diagramType?: MermaidDiagramType + + /** + * Direction for diagram layout. + * Defaults to "TD" (Top Down) if not provided. + */ + readonly direction?: MermaidDirection + + /** + * Function to determine node shape for each node. + * Defaults to "rectangle" for all nodes if not provided. + */ + readonly nodeShape?: (data: N) => MermaidNodeShape +} + +/** + * Escapes special characters in labels for Mermaid syntax compatibility. + */ +const escapeMermaidLabel = (label: string): string => { + // Escape special characters for Mermaid using HTML entity codes + // According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax + return label + .replace(/#/g, "#35;") + .replace(/"/g, "#quot;") + .replace(//g, "#gt;") + .replace(/&/g, "#amp;") + .replace(/\[/g, "#91;") + .replace(/\]/g, "#93;") + .replace(/\{/g, "#123;") + .replace(/\}/g, "#125;") + .replace(/\(/g, "#40;") + .replace(/\)/g, "#41;") + .replace(/\|/g, "#124;") + .replace(/\\/g, "#92;") + .replace(/\n/g, "
") +} + +/** + * Formats a Mermaid node with the specified shape and label. + */ +const formatMermaidNode = ( + nodeId: string, + label: string, + shape: MermaidNodeShape +): string => { + switch (shape) { + case "rectangle": + return `${nodeId}["${label}"]` + case "rounded": + return `${nodeId}("${label}")` + case "circle": + return `${nodeId}(("${label}"))` + case "diamond": + return `${nodeId}{"${label}"}` + case "hexagon": + return `${nodeId}{{"${label}"}}` + case "stadium": + return `${nodeId}(["${label}"])` + case "subroutine": + return `${nodeId}[["${label}"]]` + case "cylindrical": + return `${nodeId}[("${label}")]` + default: + return `${nodeId}["${label}"]` // Default rectangle + } +} + +/** + * Exports a graph to Mermaid diagram format for visualization. + * + * **Details** + * + * Mermaid is a popular diagram-as-code tool that generates flowcharts and other + * visualizations from text-based definitions. This function converts Effect Graph + * structures to valid Mermaid syntax for use in documentation, web applications, + * and visualization tools. + * + * **Example** (Exporting a directed Mermaid diagram) + * + * ```ts + * import { Graph } from "effect" + * + * // Basic directed graph export + * const graph = Graph.directed((mutable) => { + * const app = Graph.addNode(mutable, "App") + * const db = Graph.addNode(mutable, "Database") + * const cache = Graph.addNode(mutable, "Cache") + * Graph.addEdge(mutable, app, db, 1) + * Graph.addEdge(mutable, app, cache, 2) + * }) + * + * const mermaid = Graph.toMermaid(graph) + * console.log(mermaid) + * // flowchart TD + * // 0["App"] + * // 1["Database"] + * // 2["Cache"] + * // 0 -->|"1"| 1 + * // 0 -->|"2"| 2 + * ``` + * + * **Example** (Exporting an undirected Mermaid diagram) + * + * ```ts + * import { Graph } from "effect" + * + * // Undirected graph with custom labels and direction + * const socialGraph = Graph.undirected<{ name: string }, string>((mutable) => { + * const alice = Graph.addNode(mutable, { name: "Alice" }) + * const bob = Graph.addNode(mutable, { name: "Bob" }) + * const charlie = Graph.addNode(mutable, { name: "Charlie" }) + * Graph.addEdge(mutable, alice, bob, "friends") + * Graph.addEdge(mutable, bob, charlie, "colleagues") + * }) + * + * const mermaid = Graph.toMermaid(socialGraph, { + * nodeLabel: (person) => person.name, + * edgeLabel: (relationship) => relationship, + * direction: "LR" + * }) + * console.log(mermaid) + * // graph LR + * // 0["Alice"] + * // 1["Bob"] + * // 2["Charlie"] + * // 0 ---|"friends"| 1 + * // 1 ---|"colleagues"| 2 + * ``` + * + * **Example** (Customizing Mermaid node shapes) + * + * ```ts + * import { Graph } from "effect" + * + * // Advanced styling with node shapes for flowchart + * const workflow = Graph.directed<{ type: string; name: string }, string>( + * (mutable) => { + * const start = Graph.addNode(mutable, { type: "start", name: "Begin" }) + * const process = Graph.addNode(mutable, { + * type: "process", + * name: "Process Data" + * }) + * const decision = Graph.addNode(mutable, { + * type: "decision", + * name: "Valid?" + * }) + * const end = Graph.addNode(mutable, { type: "end", name: "Complete" }) + * Graph.addEdge(mutable, start, process, "") + * Graph.addEdge(mutable, process, decision, "") + * Graph.addEdge(mutable, decision, end, "yes") + * } + * ) + * + * const mermaid = Graph.toMermaid(workflow, { + * nodeLabel: (node) => node.name, + * nodeShape: (node) => { + * switch (node.type) { + * case "start": + * return "stadium" + * case "process": + * return "rectangle" + * case "decision": + * return "diamond" + * case "end": + * return "stadium" + * default: + * return "rectangle" + * } + * } + * }) + * console.log(mermaid) + * // flowchart TD + * // 0(["Begin"]) + * // 1["Process Data"] + * // 2{"Valid?"} + * // 3(["Complete"]) + * // 0 --> 1 + * // 1 --> 2 + * // 2 --> 3 + * ``` + * + * **Example** (Visualizing dependency graphs) + * + * ```ts + * import { Graph } from "effect" + * + * // Real-world example: Software dependency graph + * interface Dependency { + * name: string + * version: string + * type: "library" | "framework" | "tool" + * } + * + * const dependencyGraph = Graph.directed((mutable) => { + * const app = Graph.addNode(mutable, { + * name: "MyApp", + * version: "1.0.0", + * type: "library" + * } satisfies Dependency) + * const react = Graph.addNode(mutable, { + * name: "React", + * version: "18.0.0", + * type: "framework" + * } satisfies Dependency) + * const lodash = Graph.addNode(mutable, { + * name: "Lodash", + * version: "4.17.0", + * type: "library" + * } satisfies Dependency) + * const webpack = Graph.addNode(mutable, { + * name: "Webpack", + * version: "5.0.0", + * type: "tool" + * } satisfies Dependency) + * + * Graph.addEdge(mutable, app, react, "depends on") + * Graph.addEdge(mutable, app, lodash, "depends on") + * Graph.addEdge(mutable, app, webpack, "builds with") + * }) + * + * const dependencyDiagram = Graph.toMermaid(dependencyGraph, { + * nodeLabel: (dep) => `${dep.name}\\nv${dep.version}`, + * edgeLabel: (edge) => edge, + * nodeShape: (dep) => + * dep.type === "framework" ? + * "hexagon" : + * dep.type === "tool" + * ? "diamond" + * : "rectangle", + * direction: "TB" + * }) + * + * console.log(dependencyDiagram) + * // flowchart TB + * // 0["MyApp\nv1.0.0"] + * // 1{{"React\nv18.0.0"}} + * // 2["Lodash\nv4.17.0"] + * // 3{"Webpack\nv5.0.0"} + * // 0 -->|"depends on"| 1 + * // 0 -->|"depends on"| 2 + * // 0 -->|"builds with"| 3 + * ``` + * + * @category converting + * @since 3.18.0 + */ +export const toMermaid: { + ( + options?: MermaidOptions + ): (graph: Graph | MutableGraph) => string + ( + graph: Graph | MutableGraph, + options?: MermaidOptions + ): string +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + options?: MermaidOptions +): string => { + const impl = graphImpl(graph) + // Extract and validate options with defaults + const { + diagramType, + direction = "TD", + edgeLabel = (data: E) => String(data), + nodeLabel = (data: N) => String(data), + nodeShape = () => "rectangle" as const + } = options ?? {} + + // Auto-detect diagram type if not specified + const finalDiagramType = diagramType ?? + (graph.type === "directed" ? "flowchart" : "graph") + + // Generate diagram header + const lines: Array = [] + lines.push(`${finalDiagramType} ${direction}`) + + // Add nodes + for (const [nodeIndex, nodeData] of impl.nodes) { + const nodeId = String(nodeIndex) + const label = escapeMermaidLabel(nodeLabel(nodeData)) + const shape = nodeShape(nodeData) + const formattedNode = formatMermaidNode(nodeId, label, shape) + lines.push(` ${formattedNode}`) + } + + // Add edges + const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---" + for (const [, edgeData] of impl.edges) { + const sourceId = String(edgeData.source) + const targetId = String(edgeData.target) + const label = escapeMermaidLabel(edgeLabel(edgeData.data)) + + if (label) { + lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`) + } else { + lines.push(` ${sourceId} ${edgeOperator} ${targetId}`) + } + } + + return lines.join("\n") +}) + +// ============================================================================= +// Edge Direction Types +// ============================================================================= + +/** + * Direction of directed edges relative to a node. + * + * **Details** + * + * `"outgoing"` selects edges whose source is the node, while `"incoming"` + * selects edges whose target is the node. + * + * @category models + * @since 3.18.0 + */ +export type Direction = "outgoing" | "incoming" + +/** + * Controls how traversal follows directed edges. + * + * **Details** + * + * `"outgoing"` follows edges from source to target, `"incoming"` follows them + * from target to source, and `"undirected"` allows traversal in either + * direction. + * + * **Example** (Traversing by direction) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A-B") + * Graph.addEdge(mutable, a, c, "A-C") + * }) + * + * const outgoing = Array.from( + * Graph.indices(Graph.bfs(graph, { start: [0], direction: "outgoing" })) + * ) // [0, 1, 2] + * + * const incoming = Array.from( + * Graph.indices(Graph.bfs(graph, { start: [1], direction: "incoming" })) + * ) // [1, 0] + * + * const undirected = Array.from( + * Graph.indices(Graph.bfs(graph, { start: [1], direction: "undirected" })) + * ) // [1, 0, 2] + * ``` + * + * @category models + * @since 4.0.0 + */ +export type TraversalDirection = Direction | "undirected" + +// ============================================================================= +// Graph Structure Analysis Algorithms +// ============================================================================= + +/** + * Checks whether the graph is acyclic (contains no cycles). + * + * **Details** + * + * Uses depth-first search to detect back edges, which indicate cycles. + * For directed graphs, any back edge creates a cycle. For undirected graphs, + * a back edge that doesn't use the same edge used to enter the current node + * creates a cycle. + * + * **Example** (Checking cycles) + * + * ```ts + * import { Graph } from "effect" + * + * // Acyclic directed graph (DAG) + * const dag = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * }) + * console.log(Graph.isAcyclic(dag)) // true + * + * // Cyclic directed graph + * const cyclic = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle + * }) + * console.log(Graph.isAcyclic(cyclic)) // false + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const isAcyclic = ( + graph: Graph | MutableGraph +): boolean => { + const impl = graphImpl(graph) + // Use existing cycle flag if available + if (Option.isSome(impl.acyclic)) { + return impl.acyclic.value + } + + if (graph.type === "undirected") { + const visited = new Set() + + for (const startNode of impl.nodes.keys()) { + if (visited.has(startNode)) { + continue + } + + visited.add(startNode) + const stack: Array<{ node: NodeIndex; incoming: EdgeIndex | null }> = [{ node: startNode, incoming: null }] + + while (stack.length > 0) { + const { node, incoming } = stack.pop()! + const adjacencyList = impl.adjacency.get(node) + if (adjacencyList === undefined) { + continue + } + + for (const edgeIndex of adjacencyList) { + if (edgeIndex === incoming) { + continue + } + const edge = impl.edges.get(edgeIndex) + if (edge === undefined) { + continue + } + const neighbor = getTraversableNeighbor(graph, node, edge) + if (!visited.has(neighbor)) { + visited.add(neighbor) + stack.push({ node: neighbor, incoming: edgeIndex }) + } else { + impl.acyclic = Option.some(false) + return false + } + } + } + } + + impl.acyclic = Option.some(true) + return true + } + + // Stack-safe DFS cycle detection using iterative approach + const visited = new Set() + const recursionStack = new Set() + + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + // Get all nodes to handle disconnected components + for (const startNode of impl.nodes.keys()) { + if (visited.has(startNode)) { + continue // Already processed this component + } + + // Iterative DFS with explicit stack + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + // First visit to this node + if (isFirstVisit) { + if (recursionStack.has(node)) { + // Back edge found - cycle detected + impl.acyclic = Option.some(false) + return false + } + + if (visited.has(node)) { + stack.pop() + continue + } + + visited.add(node) + recursionStack.add(node) + + // Get neighbors for this node + const nodeNeighbors = getDirectedNeighbors( + graph as Graph | MutableGraph, + node, + "outgoing" + ) + stack[stack.length - 1] = [node, nodeNeighbors, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < neighbors.length) { + const neighbor = neighbors[neighborIndex] + stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false] + + if (recursionStack.has(neighbor)) { + // Back edge found - cycle detected + impl.acyclic = Option.some(false) + return false + } + + if (!visited.has(neighbor)) { + stack.push([neighbor, [], 0, true]) + } + } else { + // Done with this node - backtrack + recursionStack.delete(node) + stack.pop() + } + } + } + + // Cache the result + impl.acyclic = Option.some(true) + return true +} + +/** + * Checks whether an undirected graph is bipartite. + * + * **Details** + * + * A bipartite graph is one whose vertices can be divided into two disjoint sets + * such that no two vertices within the same set are adjacent. Uses BFS coloring + * to determine bipartiteness. + * + * **Example** (Checking bipartite graphs) + * + * ```ts + * import { Graph } from "effect" + * + * // Bipartite graph (alternating coloring possible) + * const bipartite = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * const d = Graph.addNode(mutable, "D") + * Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D} + * Graph.addEdge(mutable, b, c, "edge") + * Graph.addEdge(mutable, c, d, "edge") + * }) + * console.log(Graph.isBipartite(bipartite)) // true + * + * // Non-bipartite graph (odd cycle) + * const triangle = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "edge") + * Graph.addEdge(mutable, b, c, "edge") + * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) + * }) + * console.log(Graph.isBipartite(triangle)) // false + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const isBipartite = ( + graph: Graph | MutableGraph +): boolean => { + const impl = graphImpl(graph) + const coloring = new Map() + const discovered = new Set() + let isBipartiteGraph = true + + // Get all nodes to handle disconnected components + for (const startNode of impl.nodes.keys()) { + if (!discovered.has(startNode)) { + // Start BFS coloring from this component + const queue: Array = [startNode] + coloring.set(startNode, 0) // Color start node with 0 + discovered.add(startNode) + + while (queue.length > 0 && isBipartiteGraph) { + const current = queue.shift()! + const currentColor = coloring.get(current)! + const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0 + + // Get all neighbors for undirected graph + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!discovered.has(neighbor)) { + // Color unvisited neighbor with opposite color + coloring.set(neighbor, neighborColor) + discovered.add(neighbor) + queue.push(neighbor) + } else { + // Check if neighbor has the same color (conflict) + if (coloring.get(neighbor) === currentColor) { + isBipartiteGraph = false + break + } + } + } + } + + // Early exit if not bipartite + if (!isBipartiteGraph) { + break + } + } + } + + return isBipartiteGraph +} + +/** + * Get neighbors for undirected graphs by checking both adjacency and reverse adjacency. + * For undirected graphs, we need to find the other endpoint of each edge incident to the node. + */ +const getUndirectedNeighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex +): Array => { + const impl = graphImpl(graph) + const neighbors = new Set() + + // Check edges where this node is the source + const adjacencyList = impl.adjacency.get(nodeIndex) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + // For undirected graphs, the neighbor is the other endpoint + const otherNode = edge.source === nodeIndex ? edge.target : edge.source + neighbors.add(otherNode) + } + } + } + + return Array.from(neighbors) +} + +const getTraversalNeighbors = ( + graph: Graph | MutableGraph, + nodeIndex: NodeIndex, + direction: TraversalDirection +): Array => { + if (graph.type === "undirected") { + return getUndirectedNeighbors(graph as any, nodeIndex) + } + const directed = graph as Graph | MutableGraph + if (direction !== "undirected") { + return getDirectedNeighbors(directed, nodeIndex, direction) + } + const neighbors = new Set(getDirectedNeighbors(directed, nodeIndex, "outgoing")) + for (const neighbor of getDirectedNeighbors(directed, nodeIndex, "incoming")) { + neighbors.add(neighbor) + } + return Array.from(neighbors) +} + +const getTraversableNeighbor = ( + graph: Graph | MutableGraph, + current: NodeIndex, + edge: Edge +): NodeIndex => graph.type === "undirected" && edge.target === current ? edge.source : edge.target + +/** + * Finds connected components in an undirected graph. + * Each component is represented as an array of node indices. + * + * **Example** (Finding connected components) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.undirected((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * const d = Graph.addNode(mutable, "D") + * Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B + * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D + * }) + * + * const components = Graph.connectedComponents(graph) + * console.log(components) // [[0, 1], [2, 3]] + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const connectedComponents = ( + graph: Graph | MutableGraph +): Array> => { + const impl = graphImpl(graph) + const visited = new Set() + const components: Array> = [] + for (const startNode of impl.nodes.keys()) { + if (!visited.has(startNode)) { + // DFS to find all nodes in this component + const component: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const current = stack.pop()! + if (!visited.has(current)) { + visited.add(current) + component.push(current) + + // Add all unvisited neighbors to stack + const nodeNeighbors = getUndirectedNeighbors(graph, current) + for (const neighbor of nodeNeighbors) { + if (!visited.has(neighbor)) { + stack.push(neighbor) + } + } + } + } + + components.push(component) + } + } + + return components +} + +/** + * Finds strongly connected components in a directed graph using Kosaraju's algorithm. + * Each SCC is represented as an array of node indices. + * + * **Gotchas** + * + * Throws a `GraphError` when used with an undirected graph. + * + * **Example** (Finding strongly connected components) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, "A->B") + * Graph.addEdge(mutable, b, c, "B->C") + * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C + * }) + * + * const sccs = Graph.stronglyConnectedComponents(graph) + * console.log(sccs) // [[0, 1, 2]] + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const stronglyConnectedComponents = ( + graph: Graph | MutableGraph +): Array> => { + if ((graph as Graph | MutableGraph).type === "undirected") { + throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" }) + } + + const impl = graphImpl(graph) + const visited = new Set() + const finishOrder: Array = [] + // Iterate directly over node keys + + // Step 1: Stack-safe DFS on original graph to get finish times + // Stack entry: [node, neighbors, neighborIndex, isFirstVisit] + type DfsStackEntry = [NodeIndex, Array, number, boolean] + + for (const startNode of impl.nodes.keys()) { + if (visited.has(startNode)) { + continue + } + + const stack: Array = [[startNode, [], 0, true]] + + while (stack.length > 0) { + const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1] + + if (isFirstVisit) { + if (visited.has(node)) { + stack.pop() + continue + } + + visited.add(node) + const nodeNeighborsList = getDirectedNeighbors(graph, node, "outgoing") + stack[stack.length - 1] = [node, nodeNeighborsList, 0, false] + continue + } + + // Process next neighbor + if (neighborIndex < nodeNeighbors.length) { + const neighbor = nodeNeighbors[neighborIndex] + stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false] + + if (!visited.has(neighbor)) { + stack.push([neighbor, [], 0, true]) + } + } else { + // Done with this node - add to finish order (post-order) + finishOrder.push(node) + stack.pop() + } + } + } + + // Step 2: Stack-safe DFS on transpose graph in reverse finish order + visited.clear() + const sccs: Array> = [] + + for (let i = finishOrder.length - 1; i >= 0; i--) { + const startNode = finishOrder[i] + if (visited.has(startNode)) { + continue + } + + const scc: Array = [] + const stack: Array = [startNode] + + while (stack.length > 0) { + const node = stack.pop()! + + if (visited.has(node)) { + continue + } + + visited.add(node) + scc.push(node) + + // Use reverse adjacency (transpose graph) + const reverseAdjacency = impl.reverseAdjacency.get(node) + if (reverseAdjacency !== undefined) { + for (const edgeIndex of reverseAdjacency) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + const predecessor = edge.source + if (!visited.has(predecessor)) { + stack.push(predecessor) + } + } + } + } + } + + sccs.push(scc) + } + + return sccs +} + +// ============================================================================= +// Path Finding Algorithms +// ============================================================================= + +/** + * Result of a shortest path computation. + * + * **When to use** + * + * Use to read the successful source-to-target shortest path returned by + * path-finding algorithms, including the ordered node indices, total distance, + * and traversed edge data. + * + * **Details** + * + * Contains the node-index path, the total numeric distance, and the edge data + * encountered along the path. + * + * **Gotchas** + * + * `costs` contains original edge data, not the numeric output of the cost + * function unless the edge data is numeric. + * + * @see {@link dijkstra} for shortest paths with non-negative edge costs + * @see {@link astar} for heuristic shortest-path search + * @see {@link bellmanFord} for shortest paths that may include negative edge weights + * @see {@link AllPairsResult} for the all-pairs shortest-path result shape + * + * @category models + * @since 3.18.0 + */ +export interface PathResult { + readonly path: Array + readonly distance: number + readonly costs: Array +} + +interface MinHeapEntry { + readonly node: NodeIndex + readonly priority: number + readonly sequence: number +} + +const minHeapLessThan = (self: MinHeapEntry, that: MinHeapEntry): boolean => + self.priority < that.priority || (self.priority === that.priority && self.sequence < that.sequence) + +const minHeapPush = (heap: Array, entry: MinHeapEntry): void => { + let index = heap.length + heap.push(entry) + while (index > 0) { + const parent = (index - 1) >>> 1 + if (!minHeapLessThan(entry, heap[parent])) { + break + } + heap[index] = heap[parent] + index = parent + } + heap[index] = entry +} + +const minHeapPop = (heap: Array): MinHeapEntry | undefined => { + const first = heap[0] + const last = heap.pop() + if (last === undefined || heap.length === 0) { + return first + } + let index = 0 + while (true) { + const left = index * 2 + 1 + if (left >= heap.length) { + break + } + const right = left + 1 + const child = right < heap.length && minHeapLessThan(heap[right], heap[left]) ? right : left + if (!minHeapLessThan(heap[child], last)) { + break + } + heap[index] = heap[child] + index = child + } + heap[index] = last + return first +} + +/** + * Configuration for finding a shortest path with Dijkstra's algorithm. + * + * **When to use** + * + * Use when configuring `dijkstra` to find a shortest path between two existing + * node indices with non-negative edge costs. + * + * **Details** + * + * Specifies the source and target node indices, plus a cost function that maps + * each edge's data to a non-negative numeric weight. `Infinity` is allowed and + * behaves like an impassable edge. + * + * **Gotchas** + * + * `dijkstra` throws a `GraphError` when either endpoint does not exist or when + * the cost function returns a negative weight or `NaN`. + * + * @see {@link dijkstra} for the algorithm that consumes this configuration + * @see {@link AstarConfig} for heuristic shortest-path search + * @see {@link BellmanFordConfig} for shortest paths that may include negative edge weights + * + * @category models + * @since 3.18.0 + */ +export interface DijkstraConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number +} + +/** + * Finds the shortest path from the configured source node to the target node + * using Dijkstra's algorithm. + * + * **Details** + * + * Edge costs must be non-negative and not `NaN`. `Infinity` is allowed and + * behaves like an impassable edge. Returns `Option.none()` when the target is + * not reachable, and throws a `GraphError` when either endpoint is missing or an + * edge cost is negative or `NaN`. + * + * **Example** (Finding shortest paths with Dijkstra) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 5) + * Graph.addEdge(mutable, a, c, 10) + * Graph.addEdge(mutable, b, c, 2) + * }) + * + * const result = Graph.dijkstra(graph, { + * source: 0, + * target: 2, + * cost: (edgeData) => edgeData + * }) + * + * if (result._tag === "Some") { + * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C + * console.log(result.value.distance) // 7 - total distance + * } + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const dijkstra: { + ( + config: DijkstraConfig + ): (graph: Graph | MutableGraph) => Option.Option> + ( + graph: Graph | MutableGraph, + config: DijkstraConfig + ): Option.Option> +} = dual(2, ( + graph: Graph | MutableGraph, + config: DijkstraConfig +): Option.Option> => { + const impl = graphImpl(graph) + // Validate that source and target nodes exist + if (!impl.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!impl.nodes.has(config.target)) { + throw missingNode(config.target) + } + + const edgeWeights = new Map() + for (const [edgeIndex, edgeData] of impl.edges) { + const weight = config.cost(edgeData.data) + if (Number.isNaN(weight) || weight < 0) { + throw new GraphError({ message: "Dijkstra's algorithm requires non-negative edge weights" }) + } + edgeWeights.set(edgeIndex, weight) + } + + // Early return if source equals target + if (config.source === config.target) { + return Option.some({ + path: [config.source], + distance: 0, + costs: [] + }) + } + + // Distance tracking and priority queue simulation + const distances = new Map() + const previous = new Map() + const visited = new Set() + + // Initialize distances + // Iterate directly over node keys + for (const node of impl.nodes.keys()) { + distances.set(node, node === config.source ? 0 : Infinity) + previous.set(node, null) + } + + const priorityQueue: Array = [] + let sequence = 0 + minHeapPush(priorityQueue, { node: config.source, priority: 0, sequence: sequence++ }) + + while (priorityQueue.length > 0) { + const current = minHeapPop(priorityQueue)! + const currentNode = current.node + + // Skip if already visited (can happen with duplicate entries) + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === config.target) { + break + } + + // Get current distance + const currentDistance = distances.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = impl.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = getTraversableNeighbor(graph, currentNode, edge) + const cost = edgeWeights.get(edgeIndex)! + + const newDistance = currentDistance + cost + const neighborDistance = distances.get(neighbor)! + + // Relaxation step + if (newDistance < neighborDistance) { + distances.set(neighbor, newDistance) + previous.set(neighbor, { node: currentNode, edgeData: edge.data }) + + // Add to priority queue if not visited + if (!visited.has(neighbor)) { + minHeapPush(priorityQueue, { node: neighbor, priority: newDistance, sequence: sequence++ }) + } + } + } + } + } + } + + // Check if target is reachable + const distance = distances.get(config.target)! + if (distance === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = config.target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance, + costs + }) +}) + +/** + * Result of an all-pairs shortest path computation. + * + * **When to use** + * + * Use when storing or passing around the complete output of `floydWarshall` so + * callers can look up shortest distances, node paths, and edge data for any + * source and target node pair. + * + * **Details** + * + * Contains distance, node-path, and edge-data maps keyed by source and target + * node indices. + * + * @see {@link floydWarshall} for computing an all-pairs shortest path result + * @see {@link PathResult} for the single source-to-target result shape used by path-finding algorithms + * + * @category models + * @since 3.18.0 + */ +export interface AllPairsResult { + readonly distances: Map> + readonly paths: Map | null>> + readonly costs: Map>> +} + +/** + * Finds shortest paths between all pairs of nodes using the Floyd-Warshall + * algorithm. + * + * **Details** + * + * Computes distances, reconstructed node paths, and edge-data paths for every + * source and target pair in O(V^3) time. Negative edge weights are allowed, and + * `Infinity` behaves like an impassable edge. A `GraphError` is thrown if any + * edge weight is `NaN` or `-Infinity`, or if any negative cycle is detected. + * + * **Example** (Finding all-pairs shortest paths) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 3) + * Graph.addEdge(mutable, b, c, 2) + * Graph.addEdge(mutable, a, c, 7) + * }) + * + * const result = Graph.floydWarshall(graph, (edgeData) => edgeData) + * const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C) + * const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2] + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const floydWarshall: { + ( + cost: (edgeData: E) => number + ): (graph: Graph | MutableGraph) => AllPairsResult + ( + graph: Graph | MutableGraph, + cost: (edgeData: E) => number + ): AllPairsResult +} = dual(2, ( + graph: Graph | MutableGraph, + cost: (edgeData: E) => number +): AllPairsResult => { + const impl = graphImpl(graph) + // Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration) + const allNodes = Array.from(impl.nodes.keys()) + + // Initialize distance matrix + const distances = new Map>() + const next = new Map>() + const edgeMatrix = new Map>() + + // Initialize with infinity for all pairs + for (const i of allNodes) { + distances.set(i, new Map()) + next.set(i, new Map()) + edgeMatrix.set(i, new Map()) + + for (const j of allNodes) { + distances.get(i)!.set(j, i === j ? 0 : Infinity) + } + } + + // Set edge weights + for (const [, edgeData] of impl.edges) { + const weight = cost(edgeData.data) + if (Number.isNaN(weight) || weight === -Infinity) { + throw new GraphError({ message: "Floyd-Warshall algorithm does not support NaN or -Infinity edge weights" }) + } + const i = edgeData.source + const j = edgeData.target + + // Use minimum weight if multiple edges exist + const currentWeight = distances.get(i)!.get(j)! + if (weight < currentWeight) { + distances.get(i)!.set(j, weight) + next.get(i)!.set(j, j) + edgeMatrix.get(i)!.set(j, edgeData.data) + } + + if (graph.type === "undirected") { + const reverseWeight = distances.get(j)!.get(i)! + if (weight < reverseWeight) { + distances.get(j)!.set(i, weight) + next.get(j)!.set(i, i) + edgeMatrix.get(j)!.set(i, edgeData.data) + } + } + } + + // Floyd-Warshall main loop + for (const k of allNodes) { + for (const i of allNodes) { + for (const j of allNodes) { + const distIK = distances.get(i)!.get(k)! + const distKJ = distances.get(k)!.get(j)! + const distIJ = distances.get(i)!.get(j)! + + if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) { + const nextIK = next.get(i)!.get(k) + if (nextIK !== undefined) { + distances.get(i)!.set(j, distIK + distKJ) + next.get(i)!.set(j, nextIK) + } + } + } + } + } + + // Check for negative cycles + for (const i of allNodes) { + if (distances.get(i)!.get(i)! < 0) { + throw new GraphError({ message: `Negative cycle detected involving node ${i}` }) + } + } + + // Build result paths and edge weights + const paths = new Map | null>>() + const costs = new Map>>() + + for (const i of allNodes) { + paths.set(i, new Map()) + costs.set(i, new Map()) + + for (const j of allNodes) { + if (i === j) { + paths.get(i)!.set(j, [i]) + costs.get(i)!.set(j, []) + } else if (distances.get(i)!.get(j)! === Infinity) { + paths.get(i)!.set(j, null) + costs.get(i)!.set(j, []) + } else { + // Reconstruct path iteratively + const path: Array = [] + const weights: Array = [] + let current = i + + path.push(current) + while (current !== j) { + const nextNode = next.get(current)!.get(j) + if (nextNode === undefined) break + + const edgeRow = edgeMatrix.get(current)! + if (edgeRow.has(nextNode)) { + weights.push(edgeRow.get(nextNode) as E) + } + + current = nextNode + path.push(current) + } + + paths.get(i)!.set(j, path) + costs.get(i)!.set(j, weights) + } + } + } + + return { + distances, + paths, + costs + } +}) + +/** + * Configuration for finding a shortest path with the A* algorithm. + * + * **When to use** + * + * Use when configuring `astar` for point-to-point shortest-path searches where + * node data can provide a heuristic estimate toward the target. + * + * **Details** + * + * Specifies the source and target node indices, an edge-cost function that maps + * edge data to non-negative weights, and a heuristic that estimates the + * remaining cost from a node to the target. + * + * @see {@link astar} for the algorithm that consumes this configuration + * @see {@link DijkstraConfig} for shortest paths without a heuristic + * @see {@link BellmanFordConfig} for shortest paths that may include negative edge weights + * + * @category models + * @since 3.18.0 + */ +export interface AstarConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number + heuristic: (sourceNodeData: N, targetNodeData: N) => number +} + +/** + * Finds the shortest path from the configured source node to the target node + * using the A* pathfinding algorithm. + * + * **Details** + * + * The edge-cost function must return non-negative weights and not `NaN`. + * `Infinity` is allowed and behaves like an impassable edge. The heuristic + * should be consistent to preserve shortest-path guarantees. Returns + * `Option.none()` when the target is not reachable, and throws a `GraphError` + * when either endpoint is missing or an edge cost is negative or `NaN`. + * + * **Example** (Finding shortest paths with A-star) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { + * const a = Graph.addNode(mutable, { x: 0, y: 0 }) + * const b = Graph.addNode(mutable, { x: 1, y: 0 }) + * const c = Graph.addNode(mutable, { x: 2, y: 0 }) + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Manhattan distance heuristic + * const heuristic = ( + * nodeData: { x: number; y: number }, + * targetData: { x: number; y: number } + * ) => Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y) + * + * const result = Graph.astar(graph, { + * source: 0, + * target: 2, + * cost: (edgeData) => edgeData, + * heuristic + * }) + * + * if (result._tag === "Some") { + * console.log(result.value.path) // [0, 1, 2] - shortest path + * console.log(result.value.distance) // 2 - total distance + * } + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const astar: { + ( + config: AstarConfig + ): (graph: Graph | MutableGraph) => Option.Option> + ( + graph: Graph | MutableGraph, + config: AstarConfig + ): Option.Option> +} = dual(2, ( + graph: Graph | MutableGraph, + config: AstarConfig +): Option.Option> => { + const impl = graphImpl(graph) + // Validate that source and target nodes exist + if (!impl.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!impl.nodes.has(config.target)) { + throw missingNode(config.target) + } + + const edgeWeights = new Map() + for (const [edgeIndex, edgeData] of impl.edges) { + const weight = config.cost(edgeData.data) + if (Number.isNaN(weight) || weight < 0) { + throw new GraphError({ message: "A* algorithm requires non-negative edge weights" }) + } + edgeWeights.set(edgeIndex, weight) + } + + // Early return if source equals target + if (config.source === config.target) { + return Option.some({ + path: [config.source], + distance: 0, + costs: [] + }) + } + + // Get target node data for heuristic calculations + const targetNodeData = getNode(graph, config.target) + if (Option.isNone(targetNodeData)) { + throw new GraphError({ message: `Missing node data for target node ${config.target}` }) + } + + // Distance tracking (g-score) and f-score (g + h) + const gScore = new Map() + const fScore = new Map() + const previous = new Map() + const visited = new Set() + + // Initialize scores + // Iterate directly over node keys + for (const node of impl.nodes.keys()) { + gScore.set(node, node === config.source ? 0 : Infinity) + fScore.set(node, Infinity) + previous.set(node, null) + } + + // Calculate initial f-score for source + const sourceNodeData = getNode(graph, config.source) + if (Option.isSome(sourceNodeData)) { + const h = config.heuristic(sourceNodeData.value, targetNodeData.value) + fScore.set(config.source, h) + } + + const openSet: Array = [] + let sequence = 0 + minHeapPush(openSet, { + node: config.source, + priority: fScore.get(config.source)!, + sequence: sequence++ + }) + + while (openSet.length > 0) { + const current = minHeapPop(openSet)! + const currentNode = current.node + + // Skip if already visited + if (visited.has(currentNode)) { + continue + } + + visited.add(currentNode) + + // Early termination if we reached the target + if (currentNode === config.target) { + break + } + + // Get current g-score + const currentGScore = gScore.get(currentNode)! + + // Examine all outgoing edges + const adjacencyList = impl.adjacency.get(currentNode) + if (adjacencyList !== undefined) { + for (const edgeIndex of adjacencyList) { + const edge = impl.edges.get(edgeIndex) + if (edge !== undefined) { + const neighbor = getTraversableNeighbor(graph, currentNode, edge) + const weight = edgeWeights.get(edgeIndex)! + + const tentativeGScore = currentGScore + weight + const neighborGScore = gScore.get(neighbor)! + + // If this path to neighbor is better than any previous one + if (tentativeGScore < neighborGScore) { + // Update g-score and previous + gScore.set(neighbor, tentativeGScore) + previous.set(neighbor, { node: currentNode, edgeData: edge.data }) + + // Calculate f-score using heuristic + const neighborNodeData = getNode(graph, neighbor) + if (Option.isSome(neighborNodeData)) { + const h = config.heuristic(neighborNodeData.value, targetNodeData.value) + const f = tentativeGScore + h + fScore.set(neighbor, f) + + // Add to open set if not visited + if (!visited.has(neighbor)) { + minHeapPush(openSet, { node: neighbor, priority: f, sequence: sequence++ }) + } + } + } + } + } + } + } + + // Check if target is reachable + const distance = gScore.get(config.target)! + if (distance === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = config.target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode) ?? null + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance, + costs + }) +}) + +/** + * Configuration for finding a shortest path with the Bellman-Ford algorithm. + * + * **When to use** + * + * Use when configuring `bellmanFord` to find a shortest path where edge + * weights may be negative. + * + * **Details** + * + * Specifies the source and target node indices, plus a cost function that maps + * each edge's data to a numeric weight. + * + * @see {@link bellmanFord} for the algorithm that consumes this configuration + * @see {@link DijkstraConfig} for non-negative edge costs + * @see {@link AstarConfig} for heuristic shortest-path search + * + * @category models + * @since 3.18.0 + */ +export interface BellmanFordConfig { + source: NodeIndex + target: NodeIndex + cost: (edgeData: E) => number +} + +/** + * Finds the shortest path from the configured source node to the target node + * using the Bellman-Ford algorithm. + * + * **Details** + * + * Negative edge weights are allowed, and `Infinity` behaves like an impassable + * edge. Returns `Option.none()` when the target is unreachable or when a + * negative cycle affects the path to the target. Throws a `GraphError` when + * either endpoint is missing or an edge weight is `NaN` or `-Infinity`. + * + * **Example** (Finding shortest paths with Bellman-Ford) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, -1) // Negative weight allowed + * Graph.addEdge(mutable, b, c, 3) + * Graph.addEdge(mutable, a, c, 5) + * }) + * + * const result = Graph.bellmanFord(graph, { + * source: 0, + * target: 2, + * cost: (edgeData) => edgeData + * }) + * + * if (result._tag === "Some") { + * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C + * console.log(result.value.distance) // 2 - total distance + * } + * ``` + * + * @category algorithms + * @since 3.18.0 + */ +export const bellmanFord: { + ( + config: BellmanFordConfig + ): (graph: Graph | MutableGraph) => Option.Option> + ( + graph: Graph | MutableGraph, + config: BellmanFordConfig + ): Option.Option> +} = dual(2, ( + graph: Graph | MutableGraph, + config: BellmanFordConfig +): Option.Option> => { + const impl = graphImpl(graph) + // Validate that source and target nodes exist + if (!impl.nodes.has(config.source)) { + throw missingNode(config.source) + } + if (!impl.nodes.has(config.target)) { + throw missingNode(config.target) + } + + // Initialize distances and predecessors + const distances = new Map() + const previous = new Map() + + // Iterate directly over node keys + for (const node of impl.nodes.keys()) { + distances.set(node, node === config.source ? 0 : Infinity) + previous.set(node, null) + } + + // Collect all edges for relaxation + const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = [] + for (const [, edgeData] of impl.edges) { + const weight = config.cost(edgeData.data) + if (Number.isNaN(weight) || weight === -Infinity) { + throw new GraphError({ message: "Bellman-Ford algorithm does not support NaN or -Infinity edge weights" }) + } + edges.push({ + source: edgeData.source, + target: edgeData.target, + weight, + edgeData: edgeData.data + }) + if (graph.type === "undirected" && edgeData.source !== edgeData.target) { + edges.push({ + source: edgeData.target, + target: edgeData.source, + weight, + edgeData: edgeData.data + }) + } + } + + // Relax edges up to V-1 times + const nodeCount = impl.nodes.size + for (let i = 0; i < nodeCount - 1; i++) { + let hasUpdate = false + + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + // Relaxation step + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + distances.set(edge.target, sourceDistance + edge.weight) + previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData }) + hasUpdate = true + } + } + + // Early termination if no updates + if (!hasUpdate) { + break + } + } + + // Check for negative cycles + for (const edge of edges) { + const sourceDistance = distances.get(edge.source)! + const targetDistance = distances.get(edge.target)! + + if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) { + // Negative cycle detected - check if it affects the path to target + const affectedNodes = new Set() + const queue = [edge.target] + + while (queue.length > 0) { + const node = queue.shift()! + if (affectedNodes.has(node)) continue + affectedNodes.add(node) + + // Add all nodes reachable from this node + for (const neighbor of getTraversalNeighbors(graph, node, "outgoing")) { + queue.push(neighbor) + } + } + + // If target is affected by a negative cycle, no shortest path exists. + if (affectedNodes.has(config.target)) { + return Option.none() + } + } + } + + // Check if target is reachable + const distance = distances.get(config.target)! + if (distance === Infinity) { + return Option.none() // No path exists + } + + // Reconstruct path + const path: Array = [] + const costs: Array = [] + let currentNode: NodeIndex | null = config.target + + while (currentNode !== null) { + path.unshift(currentNode) + const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)! + if (prev !== null) { + costs.unshift(prev.edgeData) + currentNode = prev.node + } else { + currentNode = null + } + } + + return Option.some({ + path, + distance, + costs + }) +}) + +/** + * Represents an iterable wrapper used by graph traversal and listing APIs. + * + * **Details** + * + * A `Walker` yields `[index, data]` pairs lazily and can be viewed as just the + * indices, just the values, or mapped entries with `indices`, `values`, + * `entries`, and `visit`. + * + * **Example** (Working with node walkers) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * // Both traversal and element iterators return NodeWalker + * const dfsNodes: Graph.NodeWalker = Graph.dfs(graph, { start: [0] }) + * const allNodes: Graph.NodeWalker = Graph.nodes(graph) + * + * // Common interface for working with node iterables + * function processNodes(nodeIterable: Graph.NodeWalker): Array { + * return Array.from(Graph.indices(nodeIterable)) + * } + * + * // Access node data using values() or entries() + * const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"] + * const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]] + * ``` + * + * @category models + * @since 3.18.0 + */ +export class Walker implements Iterable<[T, N]> { + // @ts-ignore + readonly [Symbol.iterator]: () => Iterator<[T, N]> + + /** + * Visits each element and maps it to a value using the provided function. + * + * **Details** + * + * Takes a function that receives the index and data, + * and returns an iterable of the mapped values. Skips elements that + * no longer exist in the graph. + * + * **Example** (Visiting walker elements) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * + * // Map to just the node data + * const values = Array.from(dfs.visit((index, data) => data)) + * console.log(values) // ["A", "B"] + * + * // Map to custom objects + * const custom = Array.from( + * dfs.visit((index, data) => ({ id: index, name: data })) + * ) + * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] + * ``` + * + * @since 4.0.0 + */ + readonly visit: (f: (index: T, data: N) => U) => Iterable + + constructor( + /** + * Visits each element and maps it to a value using the provided function. + * + * Takes a function that receives the index and data, + * and returns an iterable of the mapped values. Skips elements that + * no longer exist in the graph. + * + * **Example** (Visiting walker elements) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * + * // Map to just the node data + * const values = Array.from(dfs.visit((index, data) => data)) + * console.log(values) // ["A", "B"] + * + * // Map to custom objects + * const custom = Array.from( + * dfs.visit((index, data) => ({ id: index, name: data })) + * ) + * console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }] + * ``` + * + * @category iterators + * @since 4.0.0 + */ + visit: (f: (index: T, data: N) => U) => Iterable + ) { + this.visit = visit + this[Symbol.iterator] = () => visit((index, data) => [index, data] as [T, N])[Symbol.iterator]() + } +} + +/** + * Type alias for node iteration using Walker. + * NodeWalker is represented as Walker. + * + * **When to use** + * + * Use as the shared node walker type returned by graph traversal and node + * listing APIs. + * + * @see {@link Walker} for the generic lazy iterator wrapper + * @see {@link EdgeWalker} for edge iterators + * + * @category models + * @since 3.18.0 + */ +export type NodeWalker = Walker + +/** + * Type alias for edge iteration using Walker. + * EdgeWalker is represented as Walker>. + * + * **When to use** + * + * Use to type helpers or parameters that consume edge iterators returned by + * `Graph` APIs, where each item is keyed by an `EdgeIndex` and carries the + * full `Edge`. + * + * @see {@link Walker} for the generic lazy iterator wrapper + * @see {@link NodeWalker} for node iterators + * @see {@link edges} for creating edge walkers + * + * @category models + * @since 3.18.0 + */ +export type EdgeWalker = Walker> + +/** + * Returns an iterator over the indices in the walker. + * + * **Example** (Iterating walker indices) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const indices = Array.from(Graph.indices(dfs)) + * console.log(indices) // [0, 1] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const indices = (walker: Walker): Iterable => walker.visit((index, _) => index) + +/** + * Returns an iterator over the values (data) in the walker. + * + * **Example** (Iterating walker values) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const values = Array.from(Graph.values(dfs)) + * console.log(values) // ["A", "B"] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const values = (walker: Walker): Iterable => walker.visit((_, data) => data) + +/** + * Returns an iterator over [index, data] entries in the walker. + * + * **Example** (Iterating walker entries) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const dfs = Graph.dfs(graph, { start: [0] }) + * const entries = Array.from(Graph.entries(dfs)) + * console.log(entries) // [[0, "A"], [1, "B"]] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const entries = (walker: Walker): Iterable<[T, N]> => + walker.visit((index, data) => [index, data] as [T, N]) + +/** + * Configuration for DFS, BFS, and postorder graph traversals. + * + * **When to use** + * + * Use to configure the starting node indices and edge-following direction for + * lazy graph traversals. + * + * **Details** + * + * `start` supplies the node indices where traversal begins. If it is omitted, + * the iterator is empty. `direction` chooses whether traversal follows + * outgoing edges, incoming edges, or ignores edge direction. `radius` limits + * traversal by edge distance from the nearest start node. + * + * **Gotchas** + * + * Traversal creation throws a `GraphError` when any configured `start` node + * does not exist. + * + * @see {@link dfs} for depth-first traversal + * @see {@link bfs} for breadth-first traversal + * @see {@link dfsPostOrder} for depth-first postorder traversal + * + * @category models + * @since 3.18.0 + */ +export interface SearchConfig { + readonly start?: Array + readonly direction?: TraversalDirection + readonly radius?: number +} + +/** + * Creates a lazy depth-first traversal iterator from the configured start + * nodes. + * + * **Details** + * + * If no start nodes are supplied, the iterator is empty. The `direction` option + * chooses whether to follow outgoing or incoming edges. The `radius` option + * limits traversal by edge distance from the start nodes. Throws a `GraphError` + * if any configured start node does not exist. + * + * **Example** (Traversing depth-first) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Start from a specific node + * const dfs1 = Graph.dfs(graph, { start: [0] }) + * for (const nodeIndex of Graph.indices(dfs1)) { + * console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2 + * } + * + * // Empty iterator (no starting nodes) + * const dfs2 = Graph.dfs(graph) + * // Can be used programmatically + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const dfs: { + ( + config?: SearchConfig + ): (graph: Graph | MutableGraph) => NodeWalker + ( + graph: Graph | MutableGraph, + config?: SearchConfig + ): NodeWalker +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + const radius = config.radius ?? Infinity + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const depths = radius === Infinity ? undefined : new Map() + const stack: Array = [] + const yielded = new Set() + + if (depths === undefined) { + for (const node of start) { + stack.push([node, 0]) + } + } else { + const starts = new Set() + for (let i = start.length - 1; i >= 0; i--) { + const node = start[i] + if (!starts.has(node)) { + starts.add(node) + stack.push([node, 0]) + depths.set(node, 0) + } + } + stack.reverse() + } + + const nextMapped = () => { + while (stack.length > 0) { + const [current, depth] = stack.pop()! + + if (depths === undefined) { + if (yielded.has(current)) { + continue + } + } else if (depths.get(current) !== depth) { + continue + } + + const nodeDataOption = getNode(graph, current) + if (Option.isNone(nodeDataOption)) { + continue + } + + if (depth < radius) { + const neighbors = getTraversalNeighbors(graph, current, direction) + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + const nextDepth = depth + 1 + if (depths === undefined) { + if (!yielded.has(neighbor)) { + stack.push([neighbor, nextDepth]) + } + continue + } + const neighborDepth = depths.get(neighbor) + if (neighborDepth === undefined || nextDepth < neighborDepth) { + depths.set(neighbor, nextDepth) + stack.push([neighbor, nextDepth]) + } + } + } + + if (yielded.has(current)) { + continue + } + + yielded.add(current) + + return { done: false, value: f(current, nodeDataOption.value) } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Creates a lazy breadth-first traversal iterator from the configured start + * nodes. + * + * **Details** + * + * If no start nodes are supplied, the iterator is empty. The `direction` option + * chooses whether to follow outgoing or incoming edges. The `radius` option + * limits traversal by edge distance from the start nodes. Throws a `GraphError` + * if any configured start node does not exist. + * + * **Example** (Traversing breadth-first) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Start from a specific node + * const bfs1 = Graph.bfs(graph, { start: [0] }) + * for (const nodeIndex of Graph.indices(bfs1)) { + * console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2 + * } + * + * // Empty iterator (no starting nodes) + * const bfs2 = Graph.bfs(graph) + * // Can be used programmatically + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const bfs: { + ( + config?: SearchConfig + ): (graph: Graph | MutableGraph) => NodeWalker + ( + graph: Graph | MutableGraph, + config?: SearchConfig + ): NodeWalker +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + const radius = config.radius ?? Infinity + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const queue: Array = start.map((node) => [node, 0]) + const discovered = new Set() + + const nextMapped = () => { + while (queue.length > 0) { + const [current, depth] = queue.shift()! + + if (!discovered.has(current)) { + discovered.add(current) + + if (depth < radius) { + const neighbors = getTraversalNeighbors(graph, current, direction) + for (const neighbor of neighbors) { + if (!discovered.has(neighbor)) { + queue.push([neighbor, depth + 1]) + } + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + continue + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Configuration for the topological sort iterator. + * + * **When to use** + * + * Use to prioritize specific zero in-degree nodes in a topological sort. + * + * **Details** + * + * `initials` optionally supplies zero in-degree node indices used as + * prioritized initial queue entries. Topological sorting still includes the + * other zero in-degree nodes and produces a complete topological order. + * + * **Gotchas** + * + * Throws a `GraphError` when any initial node has incoming edges. + * + * @see {@link topo} for the iterator that consumes this configuration + * + * @category models + * @since 3.18.0 + */ +export interface TopoConfig { + readonly initials?: Array +} + +/** + * Creates a new topological sort iterator with optional configuration. + * + * **Details** + * + * The iterator uses Kahn's algorithm to lazily produce nodes in topological order. + * Throws an error if the graph contains cycles. + * + * **Example** (Sorting topologically) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 1) + * }) + * + * // Standard topological sort + * const topo1 = Graph.topo(graph) + * for (const nodeIndex of Graph.indices(topo1)) { + * console.log(nodeIndex) // 0, 1, 2 (topological order) + * } + * + * // With initial nodes + * const topo2 = Graph.topo(graph, { initials: [0] }) + * + * // Check before sorting a cyclic graph + * const cyclicGraph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, a, 2) // Creates cycle + * }) + * + * if (!Graph.isAcyclic(cyclicGraph)) { + * console.log("cyclic graph") // cyclic graph + * } + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const topo: { + ( + config?: TopoConfig + ): (graph: Graph | MutableGraph) => NodeWalker + ( + graph: Graph | MutableGraph, + config?: TopoConfig + ): NodeWalker +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + config: TopoConfig = {} +): NodeWalker => { + if (graph.type === "undirected") { + throw new GraphError({ message: "Cannot perform topological sort on undirected graph" }) + } + + // Check if graph is acyclic first + if (!isAcyclic(graph)) { + throw new GraphError({ message: "Cannot perform topological sort on cyclic graph" }) + } + + const initials = config.initials ?? [] + + // Validate that all initial nodes exist + for (const nodeIndex of initials) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const impl = graphImpl(graph) + const inDegree = new Map() + const remaining = new Set() + const initialSet = new Set(initials) + const queue = [...initials] + + // Initialize in-degree counts + for (const [nodeIndex] of impl.nodes) { + inDegree.set(nodeIndex, 0) + remaining.add(nodeIndex) + } + + // Calculate in-degrees + for (const [, edgeData] of impl.edges) { + const currentInDegree = inDegree.get(edgeData.target) || 0 + inDegree.set(edgeData.target, currentInDegree + 1) + } + + for (const nodeIndex of initials) { + if (inDegree.get(nodeIndex)! !== 0) { + throw new GraphError({ message: `Initial node ${nodeIndex} has incoming edges` }) + } + } + + // Add remaining zero in-degree nodes after prioritized initials. + for (const [nodeIndex, degree] of inDegree) { + if (degree === 0 && !initialSet.has(nodeIndex)) { + queue.push(nodeIndex) + } + } + + const nextMapped = () => { + while (queue.length > 0) { + const current = queue.shift()! + + if (remaining.has(current)) { + remaining.delete(current) + + // Process outgoing edges, reducing in-degree of targets + const neighbors = getDirectedNeighbors( + graph as Graph | MutableGraph, + current, + "outgoing" + ) + for (const neighbor of neighbors) { + if (remaining.has(neighbor)) { + const currentInDegree = inDegree.get(neighbor) || 0 + const newInDegree = currentInDegree - 1 + inDegree.set(neighbor, newInDegree) + + // If in-degree becomes 0, add to queue + if (newInDegree === 0) { + queue.push(neighbor) + } + } + } + + const nodeData = getNode(graph, current) + if (Option.isSome(nodeData)) { + return { done: false, value: f(current, nodeData.value) } + } + continue + } + } + + if (remaining.size > 0) { + throw new GraphError({ message: "Cannot perform topological sort on cyclic graph" }) + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Creates a lazy depth-first postorder traversal iterator from the configured + * start nodes. + * + * **Details** + * + * Nodes are emitted after their reachable descendants have been processed. If + * no start nodes are supplied, the iterator is empty. The `direction` option + * chooses whether to follow outgoing or incoming edges. The `radius` option + * limits traversal by edge distance from the start nodes. + * + * **Gotchas** + * + * With a finite `radius`, iteration first performs a bounded breadth-first + * traversal to determine shortest-distance membership before emitting nodes in + * postorder. + * + * **Example** (Traversing in postorder) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const root = Graph.addNode(mutable, "root") + * const child1 = Graph.addNode(mutable, "child1") + * const child2 = Graph.addNode(mutable, "child2") + * Graph.addEdge(mutable, root, child1, 1) + * Graph.addEdge(mutable, root, child2, 1) + * }) + * + * // Postorder: children before parents + * const postOrder = Graph.dfsPostOrder(graph, { start: [0] }) + * for (const node of postOrder) { + * console.log(node) // 1, 2, 0 + * } + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const dfsPostOrder: { + ( + config?: SearchConfig + ): (graph: Graph | MutableGraph) => NodeWalker + ( + graph: Graph | MutableGraph, + config?: SearchConfig + ): NodeWalker +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + config: SearchConfig = {} +): NodeWalker => { + const start = config.start ?? [] + const direction = config.direction ?? "outgoing" + const radius = config.radius ?? Infinity + + // Validate that all start nodes exist + for (const nodeIndex of start) { + if (!hasNode(graph, nodeIndex)) { + throw missingNode(nodeIndex) + } + } + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const reached = radius === Infinity + ? undefined + : new Set(indices(bfs(graph, { start, direction, radius }))) + const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = [] + const discovered = new Set() + const finished = new Set() + + // Initialize stack with start nodes + for (let i = start.length - 1; i >= 0; i--) { + stack.push({ node: start[i], visitedChildren: false }) + } + + const nextMapped = () => { + while (stack.length > 0) { + const current = stack[stack.length - 1] + + if (!discovered.has(current.node)) { + discovered.add(current.node) + current.visitedChildren = false + } + + if (!current.visitedChildren) { + current.visitedChildren = true + const neighbors = getTraversalNeighbors(graph, current.node, direction) + + for (let i = neighbors.length - 1; i >= 0; i--) { + const neighbor = neighbors[i] + if ( + (reached === undefined || reached.has(neighbor)) && + !discovered.has(neighbor) && + !finished.has(neighbor) + ) { + stack.push({ node: neighbor, visitedChildren: false }) + } + } + } else { + const nodeToEmit = stack.pop()!.node + + if (!finished.has(nodeToEmit)) { + finished.add(nodeToEmit) + + const nodeData = getNode(graph, nodeToEmit) + if (Option.isSome(nodeData)) { + return { done: false, value: f(nodeToEmit, nodeData.value) } + } + continue + } + } + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) + +/** + * Creates an iterator over all node indices in the graph. + * + * **Details** + * + * The iterator produces node indices in the order they were added to the graph. + * This provides access to all nodes regardless of connectivity. + * + * **Example** (Iterating all nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * }) + * + * const indices = Array.from(Graph.indices(Graph.nodes(graph))) + * console.log(indices) // [0, 1, 2] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const nodes = ( + graph: Graph | MutableGraph +): NodeWalker => + new Walker((f) => ({ + [Symbol.iterator]() { + const nodeMap = graphImpl(graph).nodes + const iterator = nodeMap.entries() + + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const [nodeIndex, nodeData] = result.value + return { done: false, value: f(nodeIndex, nodeData) } + } + } + } + })) + +/** + * Creates an iterator over all edge indices in the graph. + * + * **Details** + * + * The iterator produces edge indices in the order they were added to the graph. + * This provides access to all edges regardless of connectivity. + * + * **Example** (Iterating all edges) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const a = Graph.addNode(mutable, "A") + * const b = Graph.addNode(mutable, "B") + * const c = Graph.addNode(mutable, "C") + * Graph.addEdge(mutable, a, b, 1) + * Graph.addEdge(mutable, b, c, 2) + * }) + * + * const indices = Array.from(Graph.indices(Graph.edges(graph))) + * console.log(indices) // [0, 1] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const edges = ( + graph: Graph | MutableGraph +): EdgeWalker => + new Walker((f) => ({ + [Symbol.iterator]() { + const edgeMap = graphImpl(graph).edges + const iterator = edgeMap.entries() + + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const [edgeIndex, edgeData] = result.value + return { done: false, value: f(edgeIndex, edgeData) } + } + } + } + })) + +/** + * Configuration for selecting external nodes. + * + * **When to use** + * + * Use to configure how `externals` identifies graph boundary nodes when you + * need sinks with no outgoing edges or sources with no incoming edges. + * + * **Details** + * + * `direction` chooses which missing edge direction makes a node external: + * `"outgoing"` selects nodes with no outgoing edges, and `"incoming"` selects + * nodes with no incoming edges. If omitted, `direction` defaults to + * `"outgoing"`. + * + * @see {@link externals} for the iterator that consumes this configuration + * + * @category models + * @since 3.18.0 + */ +export interface ExternalsConfig { + readonly direction?: Direction +} + +/** + * Creates an iterator over external nodes (nodes without edges in the specified direction). + * + * **Details** + * + * External nodes have no outgoing edges (`direction: "outgoing"`) or no + * incoming edges (`direction: "incoming"`). These are useful for finding + * sources, sinks, or isolated nodes. + * + * **Example** (Iterating external nodes) + * + * ```ts + * import { Graph } from "effect" + * + * const graph = Graph.directed((mutable) => { + * const source = Graph.addNode(mutable, "source") // 0 - no incoming + * const middle = Graph.addNode(mutable, "middle") // 1 - has both + * const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing + * const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges + * + * Graph.addEdge(mutable, source, middle, 1) + * Graph.addEdge(mutable, middle, sink, 2) + * }) + * + * // Nodes with no outgoing edges (sinks + isolated) + * const sinks = Array.from( + * Graph.indices(Graph.externals(graph, { direction: "outgoing" })) + * ) + * console.log(sinks) // [2, 3] + * + * // Nodes with no incoming edges (sources + isolated) + * const sources = Array.from( + * Graph.indices(Graph.externals(graph, { direction: "incoming" })) + * ) + * console.log(sources) // [0, 3] + * ``` + * + * @category iterators + * @since 3.18.0 + */ +export const externals: { + ( + config?: ExternalsConfig + ): (graph: Graph | MutableGraph) => NodeWalker + ( + graph: Graph | MutableGraph, + config?: ExternalsConfig + ): NodeWalker +} = dual((args) => isGraph(args[0]), ( + graph: Graph | MutableGraph, + config: ExternalsConfig = {} +): NodeWalker => { + const direction = config.direction ?? "outgoing" + + return new Walker((f) => ({ + [Symbol.iterator]: () => { + const impl = graphImpl(graph) + const nodeMap = impl.nodes + const adjacencyMap = direction === "incoming" + ? impl.reverseAdjacency + : impl.adjacency + + const nodeIterator = nodeMap.entries() + + const nextMapped = () => { + let current = nodeIterator.next() + while (!current.done) { + const [nodeIndex, nodeData] = current.value + const adjacencyList = adjacencyMap.get(nodeIndex) + + // Node is external if it has no edges in the specified direction + if (adjacencyList === undefined || adjacencyList.length === 0) { + return { done: false, value: f(nodeIndex, nodeData) } + } + current = nodeIterator.next() + } + + return { done: true, value: undefined } as const + } + + return { next: nextMapped } + } + })) +}) diff --git a/repos/effect/packages/effect/src/HKT.ts b/repos/effect/packages/effect/src/HKT.ts new file mode 100644 index 0000000..4cbbfea --- /dev/null +++ b/repos/effect/packages/effect/src/HKT.ts @@ -0,0 +1,213 @@ +/** + * Provides type-level helpers for generic code over container-like types. + * + * TypeScript cannot directly abstract over shapes such as `Option
`, + * `ReadonlyArray`, or `Effect`. This module represents those shapes + * with `TypeLambda` and applies concrete type arguments with `Kind`. It is + * mostly useful when defining generic helpers or type classes that should work + * across several data types. + * + * @since 2.0.0 + */ +import type * as Types from "./Types.ts" + +/** + * Defines the unique symbol used to associate `TypeClass` implementations with their `TypeLambda`. + * + * **When to use** + * + * Use when you need to define a custom type class that exposes the `TypeLambda` + * it operates on. + * + * **Details** + * + * This symbol links a type class shape with its compile-time type lambda. It is + * intended for type-class definitions and has no runtime behavior. + * + * **Example** (Linking a type class to a type lambda) + * + * ```ts + * import type { HKT } from "effect" + * + * interface IdentityTypeLambda extends HKT.TypeLambda { + * readonly type: this["Target"] + * } + * + * interface IdentityTypeClass extends HKT.TypeClass { + * readonly [HKT.URI]?: IdentityTypeLambda + * readonly of: (value: A) => HKT.Kind + * } + * + * const identity: IdentityTypeClass = { + * of: (value) => value + * } + * + * type LinkedTypeLambda = typeof identity[typeof HKT.URI] + * + * const value: HKT.Kind, never, never, never, string> = identity.of("ok") + * console.log(value) // "ok" + * ``` + * + * @category symbols + * @since 2.0.0 + */ +export declare const URI: unique symbol + +/** + * Base interface for type classes that work with Higher-Kinded Types. + * + * **When to use** + * + * Use to define type class interfaces parameterized by a `TypeLambda`. + * + * **Details** + * + * A `TypeClass` defines operations that can be performed on any type constructor + * that matches the given `TypeLambda`. This enables writing generic code that + * works across different container types like Array, Option, Effect, etc. + * + * **Example** (Defining higher-kinded type classes) + * + * ```ts + * import type { HKT } from "effect" + * + * // Define a Functor type class + * interface Functor extends HKT.TypeClass { + * map( + * fa: HKT.Kind, + * f: (a: A) => B + * ): HKT.Kind + * } + * + * // Define a Monad type class + * interface Monad extends Functor { + * flatMap( + * fa: HKT.Kind, + * f: (a: A) => HKT.Kind + * ): HKT.Kind + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface TypeClass { + readonly [URI]?: F +} + +/** + * Base interface for defining Higher-Kinded Type parameters. + * + * **When to use** + * + * Use to encode a type constructor for higher-kinded generic programming. + * + * **Details** + * + * A `TypeLambda` encodes the "shape" of a type constructor, specifying how many + * type parameters it takes and their variance (contravariant, covariant, or + * invariant). The four parameters are `In` for contravariant input, `Out2` for + * covariant output often used for errors, `Out1` for covariant output often used + * for context or environment, and `Target` for the invariant main type. + * + * **Example** (Defining type lambdas) + * + * ```ts + * import type { Effect, HKT } from "effect" + * + * // TypeLambda for Array + * interface ArrayTypeLambda extends HKT.TypeLambda { + * readonly type: Array + * } + * + * // TypeLambda for Effect + * interface EffectTypeLambda extends HKT.TypeLambda { + * readonly type: Effect.Effect + * } + * + * // TypeLambda for function (A) => B + * interface FunctionTypeLambda extends HKT.TypeLambda { + * readonly type: (a: this["In"]) => this["Target"] + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface TypeLambda { + readonly In: unknown + readonly Out2: unknown + readonly Out1: unknown + readonly Target: unknown +} + +/** + * Applies type parameters to a `TypeLambda` to get the concrete type. + * + * **When to use** + * + * Use to apply a `TypeLambda` to type parameters and obtain its concrete type. + * + * **Details** + * + * This type-level function takes a `TypeLambda` and four type parameters, then + * "applies" them to get the actual type. It handles variance correctly, ensuring + * contravariant parameters are used as inputs and covariant parameters as + * outputs. This is the core mechanism that allows HKT to transform abstract type + * constructors into concrete types by applying arguments. + * + * **Example** (Applying type lambdas) + * + * ```ts + * import type { Effect, HKT, Option } from "effect" + * + * // Define TypeLambdas + * interface OptionTypeLambda extends HKT.TypeLambda { + * readonly type: Option.Option + * } + * + * interface EffectTypeLambda extends HKT.TypeLambda { + * readonly type: Effect.Effect + * } + * + * // Apply type parameters to get concrete types + * type OptionString = HKT.Kind + * // Result: Option.Option + * + * type EffectStringNumberBoolean = HKT.Kind< + * EffectTypeLambda, + * never, + * number, + * boolean, + * string + * > + * // Result: Effect.Effect + * + * // TypeLambdas enable generic programming over type constructors + * type StringType = HKT.Kind< + * F, + * never, + * never, + * never, + * string + * > + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export type Kind = F extends { + readonly type: unknown +} ? (F & { + readonly In: In + readonly Out2: Out2 + readonly Out1: Out1 + readonly Target: Target + })["type"] + : { + readonly F: F + readonly In: Types.Contravariant + readonly Out2: Types.Covariant + readonly Out1: Types.Covariant + readonly Target: Types.Invariant + } diff --git a/repos/effect/packages/effect/src/Hash.ts b/repos/effect/packages/effect/src/Hash.ts new file mode 100644 index 0000000..2d925c3 --- /dev/null +++ b/repos/effect/packages/effect/src/Hash.ts @@ -0,0 +1,539 @@ +/** + * Computes Effect hash values and defines the interface for objects that want + * to provide their own hash implementation. Hashes are small numeric + * fingerprints used by Effect data structures to bucket values quickly; they + * are not cryptographic digests and they are not proof that two values are + * equal. The module also includes helpers for primitive, structure, array, and + * reference-based hashes, plus functions for combining and optimizing numeric + * hash values. + * + * @since 2.0.0 + */ +import { dual } from "./Function.ts" +import { byReferenceInstances, getAllObjectKeys } from "./internal/equal.ts" +import { hasProperty } from "./Predicate.ts" + +/** + * Defines the unique identifier used to identify objects that implement the Hash interface. + * + * **When to use** + * + * Use as the computed property key for the method that supplies a custom hash + * value on a `Hash` implementor. + * + * @see {@link Hash} for the interface implemented with this symbol + * @see {@link isHash} for checking whether a value implements `Hash` + * @see {@link hash} for computing hash values + * + * @category symbols + * @since 2.0.0 + */ +export const symbol = "~effect/interfaces/Hash" + +/** + * A type that represents an object that can be hashed. + * + * **When to use** + * + * Use to let a custom type provide its own stable hash value. + * + * **Details** + * + * Objects implementing this interface provide a method to compute their hash value, + * which is used for efficient comparison and storage operations. + * + * **Example** (Implementing Hash) + * + * ```ts + * import { Hash } from "effect" + * + * class MyClass implements Hash.Hash { + * constructor(private value: number) {} + * + * [Hash.symbol](): number { + * return Hash.hash(this.value) + * } + * } + * + * const instance = new MyClass(42) + * console.log(instance[Hash.symbol]()) // hash value of 42 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Hash { + [symbol](): number +} + +/** + * Computes a hash value for any given value. + * + * **When to use** + * + * Use to compute an Effect hash for primitives, collections, and hashable + * objects. + * + * **Details** + * + * This function can hash primitives (numbers, strings, booleans, etc.) as well as + * objects, arrays, and other complex data structures. It automatically handles + * different types and provides a consistent hash value for equivalent inputs. + * + * **Gotchas** + * + * Objects being hashed must be treated as immutable after their first hash + * computation. Hash results are cached, so mutating an object after hashing will + * lead to stale cached values and broken hash-based operations. For mutable + * objects, implement a custom `Hash` interface that hashes the object reference + * rather than its content. + * + * **Example** (Hashing different values) + * + * ```ts + * import { Hash } from "effect" + * + * // Hash primitive values + * console.log(Hash.hash(42)) // numeric hash + * console.log(Hash.hash("hello")) // string hash + * console.log(Hash.hash(true)) // boolean hash + * + * // Hash objects and arrays + * console.log(Hash.hash({ name: "John", age: 30 })) + * console.log(Hash.hash([1, 2, 3])) + * console.log(Hash.hash({ id: "user-1", roles: ["admin", "editor"] })) + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const hash: (self: A) => number = (self: A) => { + switch (typeof self) { + case "number": + return number(self) + case "bigint": + return string(self.toString(10)) + case "boolean": + return string(String(self)) + case "symbol": + return string(String(self)) + case "string": + return string(self) + case "undefined": + return string("undefined") + case "function": + case "object": { + if (self === null) { + return string("null") + } else if (self instanceof Date) { + return string(self.toISOString()) + } else if (self instanceof RegExp) { + return string(self.toString()) + } else { + if (byReferenceInstances.has(self)) { + return random(self) + } + if (hashCache.has(self)) { + return hashCache.get(self)! + } + const h = withVisitedTracking(self, () => { + if (isHash(self)) { + return self[symbol]() + } else if (typeof self === "function") { + return random(self) + } else if (Array.isArray(self) || ArrayBuffer.isView(self)) { + return array(self as any) + } else if (self instanceof Map) { + return hashMap(self) + } else if (self instanceof Set) { + return hashSet(self) + } + return structure(self) + }) + hashCache.set(self, h) + return h + } + } + default: + throw new Error( + `BUG: unhandled typeof ${typeof self} - please report an issue at https://github.com/Effect-TS/effect/issues` + ) + } +} + +/** + * Generates a random hash value for an object and caches it. + * + * **When to use** + * + * Use to hash an object by reference identity instead of structural content. + * + * **Details** + * + * This function creates a random hash value for objects that don't have their own + * hash implementation. The hash value is cached using a WeakMap, so the same object + * will always return the same hash value during its lifetime. + * + * **Example** (Hashing objects by reference) + * + * ```ts + * import { Hash } from "effect" + * + * const obj1 = { a: 1 } + * const obj2 = { a: 1 } + * + * // Same object always returns the same hash + * console.log(Hash.random(obj1) === Hash.random(obj1)) // true + * + * // Different objects get different hashes + * console.log(Hash.random(obj1) === Hash.random(obj2)) // false + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const random: (self: A) => number = (self) => { + if (!randomHashCache.has(self)) { + randomHashCache.set(self, number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))) + } + return randomHashCache.get(self)! +} + +/** + * Combines two hash values into a single hash value. + * + * **When to use** + * + * Use to build a hash for a composite value by folding together hash values for + * its parts. + * + * **Details** + * + * Supports both direct and pipeable usage. The implementation combines two + * hash values with `(self * 53) ^ b`. + * + * **Example** (Combining hash values) + * + * ```ts + * import { Hash, pipe } from "effect" + * + * // Can also be used with pipe + * + * const hash1 = Hash.hash("hello") + * const hash2 = Hash.hash("world") + * + * // Combine two hash values + * const combined = Hash.combine(hash2)(hash1) + * console.log(combined) + * const result = pipe(hash1, Hash.combine(hash2)) + * ``` + * + * @see {@link hash} for computing hash values from arbitrary inputs + * @see {@link structureKeys} for hashing selected object fields without manual combination + * + * @category hashing + * @since 2.0.0 + */ +export const combine: { + (b: number): (self: number) => number + (self: number, b: number): number +} = dual(2, (self: number, b: number): number => (self * 53) ^ b) + +/** + * Applies bit manipulation techniques to optimize a hash value. + * + * **When to use** + * + * Use to improve the bit distribution of a raw numeric hash value. + * + * **Details** + * + * This function takes a hash value and applies bitwise operations to improve + * the distribution of hash values, reducing the likelihood of collisions. + * + * **Example** (Optimizing a hash value) + * + * ```ts + * import { Hash } from "effect" + * + * const rawHash = 1234567890 + * const optimizedHash = Hash.optimize(rawHash) + * console.log(optimizedHash) // optimized hash value + * + * // Often used internally by other hash functions + * const stringHash = Hash.optimize(Hash.string("hello")) + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const optimize = (n: number): number => (n & 0xbfffffff) | ((n >>> 1) & 0x40000000) + +/** + * Checks whether a value implements the Hash interface. + * + * **When to use** + * + * Use to detect whether an unknown value provides a custom hash implementation. + * + * **Details** + * + * This function determines whether a given value has the Hash symbol property, + * indicating that it can provide its own hash value implementation. + * + * **Example** (Checking for Hash support) + * + * ```ts + * import { Hash } from "effect" + * + * class MyHashable implements Hash.Hash { + * [Hash.symbol]() { + * return 42 + * } + * } + * + * const obj = new MyHashable() + * console.log(Hash.isHash(obj)) // true + * console.log(Hash.isHash({})) // false + * console.log(Hash.isHash("string")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isHash = (u: unknown): u is Hash => hasProperty(u, symbol) + +/** + * Computes a hash value for a number. + * + * **When to use** + * + * Use to hash a JavaScript number with Effect's numeric hash semantics. + * + * **Details** + * + * This function creates a hash value for numeric inputs, handling special cases + * like NaN, Infinity, and -Infinity with distinct hash values. It uses bitwise operations to ensure good distribution + * of hash values across different numeric inputs. + * + * **Example** (Hashing numbers) + * + * ```ts + * import { Hash } from "effect" + * + * console.log(Hash.number(42)) // hash of 42 + * console.log(Hash.number(3.14)) // hash of 3.14 + * console.log(Hash.number(NaN)) // hash of "NaN" + * console.log(Hash.number(Infinity)) // 0 (special case) + * + * // Same numbers produce the same hash + * console.log(Hash.number(100) === Hash.number(100)) // true + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const number = (n: number) => { + if (n !== n) { + return string("NaN") + } + if (n === Infinity) { + return string("Infinity") + } + if (n === -Infinity) { + return string("-Infinity") + } + let h = n | 0 + if (h !== n) { + h ^= n * 0xffffffff + } + while (n > 0xffffffff) { + h ^= n /= 0xffffffff + } + return optimize(h) +} + +/** + * Computes a hash value for a string using the djb2 algorithm. + * + * **When to use** + * + * Use when you need a string field to contribute to a custom structural hash + * implementation. + * + * **Details** + * + * This function implements a variation of the djb2 hash algorithm, which is + * known for its good distribution properties and speed. It processes each + * character of the string to produce a consistent hash value. + * + * **Example** (Hashing strings) + * + * ```ts + * import { Hash } from "effect" + * + * console.log(Hash.string("hello")) // hash of "hello" + * console.log(Hash.string("world")) // hash of "world" + * console.log(Hash.string("")) // hash of empty string + * + * // Same strings produce the same hash + * console.log(Hash.string("test") === Hash.string("test")) // true + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const string = (str: string) => { + let h = 5381, i = str.length + while (i) { + h = (h * 33) ^ str.charCodeAt(--i) + } + return optimize(h) +} + +/** + * Computes a hash value for an object using only the specified keys. + * + * **When to use** + * + * Use to hash an object by a selected set of property keys. + * + * **Details** + * + * This function allows you to hash an object by considering only specific keys, + * which is useful when you want to create a hash based on a subset of an object's + * properties. + * + * **Example** (Hashing selected object keys) + * + * ```ts + * import { Hash } from "effect" + * + * const person = { name: "John", age: 30, city: "New York" } + * + * // Hash only specific keys + * const hash1 = Hash.structureKeys(person, ["name", "age"]) + * const hash2 = Hash.structureKeys(person, ["name", "city"]) + * + * console.log(hash1) // hash based on name and age + * console.log(hash2) // hash based on name and city + * + * // Same keys produce the same hash + * const person2 = { name: "John", age: 30, city: "Boston" } + * const hash3 = Hash.structureKeys(person2, ["name", "age"]) + * console.log(hash1 === hash3) // true + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const structureKeys = (o: object, keys: Iterable) => { + let h = 12289 + for (const key of keys) { + h ^= combine(hash(key), hash((o as any)[key])) + } + return optimize(h) +} + +/** + * Computes a structural hash for an object using Effect's object key collection. + * + * **When to use** + * + * Use to hash an object from all structural keys collected by Effect. + * + * **Details** + * + * The hash is based on the object's structural keys and their values, including + * symbol keys and relevant prototype keys for non-plain objects. + * + * **Example** (Hashing object structures) + * + * ```ts + * import { Hash } from "effect" + * + * const obj1 = { name: "John", age: 30 } + * const obj2 = { name: "Jane", age: 25 } + * const obj3 = { name: "John", age: 30 } + * + * console.log(Hash.structure(obj1)) // hash of obj1 + * console.log(Hash.structure(obj2)) // different hash + * console.log(Hash.structure(obj3)) // same as obj1 + * + * // Objects with same properties produce same hash + * console.log(Hash.structure(obj1) === Hash.structure(obj3)) // true + * ``` + * + * @category hashing + * @since 2.0.0 + */ +export const structure = (o: A) => structureKeys(o, getAllObjectKeys(o)) + +const iterableWith = (seed: number, f: (el: any) => number) => (iter: Iterable) => { + let h = seed + for (const element of iter) { + h ^= f(element) + } + return optimize(h) +} + +/** + * Computes a hash value for an iterable by hashing all of its elements. + * + * **When to use** + * + * Use to hash the values yielded by an iterable with Effect hash semantics. + * + * **Details** + * + * The implementation folds element hashes from the seed `6151` with XOR and + * then optimizes the final hash. + * + * **Gotchas** + * + * A hash is not an equality proof. Because this implementation uses XOR, + * reordered inputs can produce the same hash. + * + * **Example** (Hashing arrays) + * + * ```ts + * import { Hash } from "effect" + * + * const arr1 = [1, 2, 3] + * const arr2 = [1, 2, 3] + * const arr3 = [3, 2, 1] + * + * console.log(Hash.array(arr1)) // hash of [1, 2, 3] + * console.log(Hash.array(arr2)) // same hash as arr1 + * console.log(Hash.array(arr3)) // may match reordered inputs + * + * console.log(Hash.array(arr1) === Hash.array(arr2)) // true + * console.log(Hash.array(arr1) === Hash.array(arr3)) // true + * ``` + * + * @see {@link hash} for the general-purpose hash dispatcher + * + * @category hashing + * @since 2.0.0 + */ +export const array: (arr: Iterable) => number = iterableWith(6151, hash) + +const hashMap: (map: Iterable) => number = iterableWith( + string("Map"), + ([k, v]) => combine(hash(k), hash(v)) +) +const hashSet: (set: Iterable) => number = iterableWith(string("Set"), hash) + +const randomHashCache = new WeakMap() +const hashCache = new WeakMap() +const visitedObjects = new WeakSet() + +function withVisitedTracking(obj: object, fn: () => T): T { + if (visitedObjects.has(obj)) { + return string("[Circular]") as T + } + visitedObjects.add(obj) + const result = fn() + visitedObjects.delete(obj) + return result +} diff --git a/repos/effect/packages/effect/src/HashMap.ts b/repos/effect/packages/effect/src/HashMap.ts new file mode 100644 index 0000000..223d781 --- /dev/null +++ b/repos/effect/packages/effect/src/HashMap.ts @@ -0,0 +1,1236 @@ +/** + * Stores key/value entries in an immutable hash map. + * + * A `HashMap` hashes keys and resolves matches with Effect's + * structural equality rules. Lookup, insertion, removal, and transformation + * operations return new maps, while temporary mutation helpers support efficient + * batch updates. This module also includes constructors, iteration, conversion, + * mapping, filtering, and reducing helpers. + * + * @since 2.0.0 + */ + +import type { Equal } from "./Equal.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as internal from "./internal/hashMap.ts" +import type { Option } from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Result } from "./Result.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = internal.HashMapTypeId + +/** + * A HashMap is an immutable key-value data structure that provides efficient lookup, + * insertion, and deletion operations. It uses a Hash Array Mapped Trie (HAMT) internally + * for structural sharing and optimal performance. + * + * **Example** (Using basic HashMap operations) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a HashMap + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * + * // Access values + * const valueA = HashMap.get(map, "a") // Option.some(1) + * const valueD = HashMap.get(map, "d") // Option.none() + * + * // Check if key exists + * console.log(HashMap.has(map, "b")) // true + * + * // Add/update values (returns new HashMap) + * const updated = HashMap.set(map, "d", 4) + * console.log(HashMap.size(updated)) // 4 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface HashMap extends Iterable<[Key, Value]>, Equal, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId +} + +/** + * The HashMap namespace contains type-level utilities and helper types + * for working with HashMap instances. + * + * **Example** (Extracting HashMap types) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a concrete HashMap for type extraction + * const inventory = HashMap.make( + * ["laptop", { quantity: 5, price: 999 }], + * ["mouse", { quantity: 20, price: 29 }] + * ) + * + * // Extract types for reuse + * type ProductId = HashMap.HashMap.Key // string + * type Product = HashMap.HashMap.Value // { quantity: number, price: number } + * type InventoryEntry = HashMap.HashMap.Entry // [string, Product] + * + * // Use extracted types in functions + * const updateInventory = (id: ProductId, product: Product) => + * HashMap.set(inventory, id, product) + * + * const processEntry = ([id, product]: InventoryEntry) => + * `${id}: ${product.quantity} @ $${product.price}` + * + * // Example of extracted types in action + * const newProduct: Product = { quantity: 10, price: 199 } + * const updatedInventory = updateInventory("tablet", newProduct) + * ``` + * + * @since 2.0.0 + */ +export declare namespace HashMap { + /** + * A function that updates a value based on its current state. + * Takes an Option representing the current value and returns an Option + * representing the new value. + * + * **Example** (Updating values from Options) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * + * // Increment existing value or set to 1 if not present + * const updateFn = (option: Option.Option) => + * Option.isSome(option) ? Option.some(option.value + 1) : Option.some(1) + * + * const updated = HashMap.modifyAt(map, "a", updateFn) + * console.log(HashMap.get(updated, "a")) // Option.some(2) + * ``` + * + * @category models + * @since 2.0.0 + */ + export type UpdateFn = (option: Option) => Option + + /** + * This type-level utility extracts the key type `K` from a `HashMap` type. + * + * **Example** (Extracting key types) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a HashMap to extract key type from + * const userMap = HashMap.make( + * ["alice", { name: "Alice", age: 30 }], + * ["bob", { name: "Bob", age: 25 }] + * ) + * + * // Extract the key type (string) + * type UserKey = HashMap.HashMap.Key + * + * // Use the extracted type in functions + * const getUserById = (id: UserKey) => HashMap.get(userMap, id) + * console.log(getUserById("alice")) // Option.some({ name: "Alice", age: 30 }) + * ``` + * + * @category utility types + * @since 2.0.0 + */ + export type Key> = [T] extends [HashMap] ? _K : never + + /** + * This type-level utility extracts the value type `V` from a `HashMap` type. + * + * **Example** (Extracting value types) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a HashMap with user data + * const userMap = HashMap.make( + * ["alice", { name: "Alice", age: 30, active: true }], + * ["bob", { name: "Bob", age: 25, active: false }] + * ) + * + * // Extract the value type (User object) + * type User = HashMap.HashMap.Value + * + * // Use the extracted type for type-safe operations + * const processUser = (user: User) => { + * return user.active ? `${user.name} (active)` : `${user.name} (inactive)` + * } + * + * const alice = HashMap.get(userMap, "alice") + * // alice has type Option thanks to type extraction + * ``` + * + * @category utility types + * @since 2.0.0 + */ + export type Value> = [T] extends [HashMap] ? _V : never + + /** + * This type-level utility extracts the entry type `[K, V]` from a `HashMap` type. + * + * **Example** (Extracting entry types) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a product catalog HashMap + * const catalog = HashMap.make( + * ["laptop", { price: 999, category: "electronics" }], + * ["book", { price: 29, category: "education" }] + * ) + * + * // Extract the entry type [string, Product] + * type CatalogEntry = HashMap.HashMap.Entry + * + * // Use the extracted type for processing entries + * const processEntry = ([productId, product]: CatalogEntry) => { + * return `${productId}: $${product.price} (${product.category})` + * } + * + * // Convert to entries, process, and sort for deterministic output + * const descriptions = HashMap.toEntries(catalog).map(processEntry).sort() + * console.log(descriptions) // ["book: $29 (education)", "laptop: $999 (electronics)"] + * ``` + * + * @category utility types + * @since 3.9.0 + */ + export type Entry> = [Key, Value] +} + +/** + * Checks whether a value is a HashMap. + * + * **Example** (Checking HashMap values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * const notMap = { a: 1 } + * + * console.log(HashMap.isHashMap(map)) // true + * console.log(HashMap.isHashMap(notMap)) // false + * console.log(HashMap.isHashMap(null)) // false + * ``` + * + * @category refinements + * @since 2.0.0 + */ +export const isHashMap: { + (u: Iterable): u is HashMap + (u: unknown): u is HashMap +} = internal.isHashMap + +/** + * Creates a new empty `HashMap`. + * + * **Example** (Creating an empty HashMap) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.empty() + * console.log(HashMap.isEmpty(map)) // true + * console.log(HashMap.size(map)) // 0 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => HashMap = internal.empty + +/** + * Constructs a new `HashMap` from an array of key/value pairs. + * + * **Example** (Creating a HashMap from entries) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * console.log(HashMap.size(map)) // 3 + * console.log(HashMap.get(map, "b")) // Option.some(2) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: >( + ...entries: Entries +) => HashMap< + Entries[number] extends readonly [infer K, any] ? K : never, + Entries[number] extends readonly [any, infer V] ? V : never +> = internal.make + +/** + * Creates a new `HashMap` from an iterable collection of key/value pairs. + * + * **Example** (Creating a HashMap from an iterable) + * + * ```ts + * import { HashMap } from "effect" + * + * const entries = [["a", 1], ["b", 2], ["c", 3]] as const + * const map = HashMap.fromIterable(entries) + * console.log(HashMap.size(map)) // 3 + * console.log(HashMap.get(map, "a")) // Option.some(1) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable: (entries: Iterable) => HashMap = internal.fromIterable + +/** + * Checks whether the `HashMap` contains no entries. + * + * **Example** (Checking for empty HashMaps) + * + * ```ts + * import { HashMap } from "effect" + * + * const emptyMap = HashMap.empty() + * const nonEmptyMap = HashMap.make(["a", 1]) + * + * console.log(HashMap.isEmpty(emptyMap)) // true + * console.log(HashMap.isEmpty(nonEmptyMap)) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const isEmpty: (self: HashMap) => boolean = internal.isEmpty + +/** + * Looks up the value for the specified key in the `HashMap` safely using the + * internal hashing function. + * + * **Example** (Looking up values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * + * console.log(HashMap.get(map, "a")) // Option.some(1) + * console.log(HashMap.get(map, "c")) // Option.none() + * + * // Using pipe syntax + * const value = HashMap.get("b")(map) + * console.log(value) // Option.some(2) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const get: { + (key: K1): (self: HashMap) => Option + (self: HashMap, key: K1): Option +} = internal.get + +/** + * Looks up the value for the specified key in the `HashMap` safely using a custom hash. + * + * **Example** (Looking up values with a hash) + * + * ```ts + * import { Hash, HashMap } from "effect" + * + * // Useful when implementing custom equality for complex keys + * const userMap = HashMap.make( + * ["user123", { name: "Alice", role: "admin" }], + * ["user456", { name: "Bob", role: "user" }] + * ) + * + * // Use precomputed hash for performance in hot paths + * const userId = "user123" + * const precomputedHash = Hash.string(userId) + * + * // Lookup with custom hash (e.g., cached hash value) + * const user = HashMap.getHash(userMap, userId, precomputedHash) + * console.log(user) // Option.some({ name: "Alice", role: "admin" }) + * + * // This avoids recomputing the hash when you already have it + * const notFound = HashMap.getHash(userMap, "user999", Hash.string("user999")) + * console.log(notFound) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const getHash: { + (key: K1, hash: number): (self: HashMap) => Option + (self: HashMap, key: K1, hash: number): Option +} = internal.getHash + +/** + * Looks up the value for the specified key in the `HashMap` unsafely using the + * internal hashing function. + * + * **When to use** + * + * Use when reading from a `HashMap` by a key known to exist, and throwing is an + * acceptable programming error for a missing key. + * + * **Gotchas** + * + * This function throws an error if the key is not found. Use `HashMap.get` for + * safe access that returns `Option`. + * + * **Example** (Unsafely looking up values) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const config = HashMap.make( + * ["api_url", "https://api.example.com"], + * ["timeout", "5000"], + * ["retries", "3"] + * ) + * + * // Safe: use when you're certain the key exists + * const apiUrl = HashMap.getUnsafe(config, "api_url") // "https://api.example.com" + * console.log(`Connecting to: ${apiUrl}`) + * + * // Preferred: use get() for uncertain keys + * const dbUrl = HashMap.get(config, "db_url") // Option.none() + * if (Option.isSome(dbUrl)) { + * console.log(`Database: ${dbUrl.value}`) + * } + * + * // This would throw: HashMap.getUnsafe(config, "db_url") + * // Error: "HashMap.getUnsafe: key not found" + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const getUnsafe: { + (key: K1): (self: HashMap) => V + (self: HashMap, key: K1): V +} = internal.getUnsafe + +/** + * Checks whether the specified key has an entry in the `HashMap`. + * + * **Example** (Checking for keys) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * + * console.log(HashMap.has(map, "a")) // true + * console.log(HashMap.has(map, "c")) // false + * + * // Using pipe syntax + * const hasB = HashMap.has("b")(map) + * console.log(hasB) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (key: K1): (self: HashMap) => boolean + (self: HashMap, key: K1): boolean +} = internal.has + +/** + * Checks whether the specified key has an entry in the `HashMap` using a custom + * hash. + * + * **Example** (Checking keys with a hash) + * + * ```ts + * import { Hash, HashMap } from "effect" + * + * // Create a map with case-sensitive keys + * const userMap = HashMap.make( + * ["Admin", { role: "administrator" }], + * ["User", { role: "standard" }] + * ) + * + * // Check with exact hash + * const exactHash = Hash.string("Admin") + * console.log(HashMap.hasHash(userMap, "Admin", exactHash)) // true + * + * // A matching hash does not override key equality + * console.log(HashMap.hasHash(userMap, "admin", exactHash)) // false + * + * // A different hash also cannot find the existing key + * const lowercaseHash = Hash.string("admin") + * console.log(HashMap.hasHash(userMap, "Admin", lowercaseHash)) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const hasHash: { + (key: K1, hash: number): (self: HashMap) => boolean + (self: HashMap, key: K1, hash: number): boolean +} = internal.hasHash + +/** + * Checks whether an element matching the given predicate exists in the given `HashMap`. + * + * **Example** (Checking entries by predicate) + * + * ```ts + * import { HashMap } from "effect" + * + * const hm = HashMap.make([1, "a"]) + * HashMap.hasBy(hm, (value, key) => value === "a" && key === 1) // -> true + * HashMap.hasBy(hm, (value) => value === "b") // -> false + * ``` + * + * @category elements + * @since 3.16.0 + */ +export const hasBy: { + (predicate: (value: NoInfer, key: NoInfer) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (value: NoInfer, key: NoInfer) => boolean): boolean +} = internal.hasBy + +/** + * Sets the specified key to the specified value using the internal hashing + * function. + * + * **Example** (Setting a value) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1]) + * const map2 = HashMap.set(map1, "b", 2) + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.get(map2, "b")) // Option.some(2) + * + * // Original map is unchanged + * console.log(HashMap.size(map1)) // 1 + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const set: { + (key: K, value: V): (self: HashMap) => HashMap + (self: HashMap, key: K, value: V): HashMap +} = internal.set + +/** + * Returns an `IterableIterator` of the keys within the `HashMap`. + * + * **Example** (Iterating keys) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const keys = Array.from(HashMap.keys(map)) + * console.log(keys.sort()) // ["a", "b", "c"] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const keys: (self: HashMap) => IterableIterator = internal.keys + +/** + * Returns an `IterableIterator` of the values within the `HashMap`. + * + * **Example** (Iterating values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const values = Array.from(HashMap.values(map)) + * console.log(values.sort()) // [1, 2, 3] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const values: (self: HashMap) => IterableIterator = internal.values + +/** + * Returns an `Array` of the values within the `HashMap`. + * + * **Example** (Converting values to an array) + * + * ```ts + * import { HashMap } from "effect" + * + * const employees = HashMap.make( + * ["alice", { department: "engineering", salary: 90000 }], + * ["bob", { department: "marketing", salary: 75000 }], + * ["charlie", { department: "engineering", salary: 95000 }] + * ) + * + * // Extract all employee records + * const allEmployees = HashMap.toValues(employees) + * console.log(allEmployees.length) // 3 + * + * // Calculate total salary + * const totalSalary = allEmployees.reduce((sum, emp) => sum + emp.salary, 0) + * console.log(totalSalary) // 260000 + * + * // Filter by department + * const engineers = allEmployees.filter((emp) => emp.department === "engineering") + * console.log(engineers.length) // 2 + * ``` + * + * @category getters + * @since 3.13.0 + */ +export const toValues = (self: HashMap): Array => Array.from(values(self)) + +/** + * Returns an `IterableIterator` of the entries within the `HashMap`. + * + * **Example** (Iterating entries) + * + * ```ts + * import { HashMap } from "effect" + * + * // Create a configuration map + * const config = HashMap.make( + * ["database.host", "localhost"], + * ["database.port", "5432"], + * ["cache.enabled", "true"] + * ) + * + * // Sort the derived array for deterministic output + * const settings = Array.from(HashMap.entries(config)) + * .sort(([left], [right]) => left.localeCompare(right)) + * .map(([key, value]) => `Setting ${key} = ${value}`) + * + * console.log(settings) + * // ["Setting cache.enabled = true", "Setting database.host = localhost", "Setting database.port = 5432"] + * + * // Convert to array when you need all entries at once + * const allEntries = Array.from(HashMap.entries(config)) + * console.log(allEntries.length) // 3 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const entries: (self: HashMap) => IterableIterator<[K, V]> = internal.entries + +/** + * Returns an `Array<[K, V]>` of the entries within the `HashMap`. + * + * **Example** (Converting entries to an array) + * + * ```ts + * import { HashMap } from "effect" + * + * const gameScores = HashMap.make( + * ["alice", 1250], + * ["bob", 980], + * ["charlie", 1100] + * ) + * + * // Convert to entries for processing + * const scoreEntries = HashMap.toEntries(gameScores) + * + * // Sort by score (descending) + * const leaderboard = scoreEntries + * .sort(([, a], [, b]) => b - a) + * .map(([player, score], rank) => `${rank + 1}. ${player}: ${score}`) + * + * console.log(leaderboard) + * // ["1. alice: 1250", "2. charlie: 1100", "3. bob: 980"] + * + * // Convert back to HashMap if needed + * const sortedMap = HashMap.fromIterable(scoreEntries) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toEntries = (self: HashMap): Array<[K, V]> => Array.from(entries(self)) + +/** + * Returns the number of entries within the `HashMap`. + * + * **Example** (Getting the size) + * + * ```ts + * import { HashMap } from "effect" + * + * const emptyMap = HashMap.empty() + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * + * console.log(HashMap.size(emptyMap)) // 0 + * console.log(HashMap.size(map)) // 3 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size: (self: HashMap) => number = internal.size + +/** + * Creates a transient mutable `HashMap` for efficient batched updates. + * + * **Details** + * + * Apply updates to the returned map, then call `endMutation` to finish the + * mutation window and use the result as an immutable `HashMap`. + * + * **Example** (Beginning batch mutation) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1]) + * + * // Begin mutation for efficient batch operations + * const mutable = HashMap.beginMutation(map) + * + * // Multiple operations are now more efficient + * HashMap.set(mutable, "b", 2) + * HashMap.set(mutable, "c", 3) + * HashMap.remove(mutable, "a") + * + * // End mutation to get final immutable result + * const result = HashMap.endMutation(mutable) + * console.log(HashMap.size(result)) // 2 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const beginMutation: (self: HashMap) => HashMap = internal.beginMutation + +/** + * Marks the `HashMap` as immutable, completing the mutation cycle. + * + * **Example** (Ending batch mutation) + * + * ```ts + * import { HashMap } from "effect" + * + * // Start with an existing map + * const original = HashMap.make(["x", 10], ["y", 20]) + * + * // Begin mutation for batch operations + * const mutable = HashMap.beginMutation(original) + * + * // Perform multiple efficient operations + * HashMap.set(mutable, "z", 30) + * HashMap.remove(mutable, "x") + * HashMap.set(mutable, "w", 40) + * + * // End mutation to get final immutable result + * const final = HashMap.endMutation(mutable) + * + * console.log(HashMap.size(final)) // 3 + * console.log(HashMap.has(final, "x")) // false + * console.log(HashMap.get(final, "z")) // Option.some(30) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const endMutation: (self: HashMap) => HashMap = internal.endMutation + +/** + * Runs a batch of updates against a transient mutable copy of the `HashMap` + * and returns the finalized immutable result. + * + * **Details** + * + * The callback may call mutation-oriented helpers such as `set` and `remove` + * on the transient map. + * + * **Example** (Applying batched mutations) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1]) + * const map2 = HashMap.mutate(map1, (mutable) => { + * HashMap.set(mutable, "b", 2) + * HashMap.set(mutable, "c", 3) + * }) + * // Returns a new HashMap with mutations applied + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const mutate: { + (f: (self: HashMap) => void): (self: HashMap) => HashMap + (self: HashMap, f: (self: HashMap) => void): HashMap +} = internal.mutate + +/** + * Sets or removes the specified key using an update function. + * + * **Details** + * + * The update function receives `Some(value)` when the key exists or `None` + * when it does not. Returning `Some(newValue)` stores the value, and returning + * `None` removes the key or leaves it absent. + * + * **Example** (Updating values with Options) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * + * // Increment existing value or set to 1 if not present + * const updateFn = (option: Option.Option) => + * Option.isSome(option) ? Option.some(option.value + 1) : Option.some(1) + * + * const updated = HashMap.modifyAt(map, "a", updateFn) + * console.log(HashMap.get(updated, "a")) // Option.some(2) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const modifyAt: { + (key: K, f: HashMap.UpdateFn): (self: HashMap) => HashMap + (self: HashMap, key: K, f: HashMap.UpdateFn): HashMap +} = internal.modifyAt + +/** + * Sets or removes the specified key using a precomputed hash and an update + * function. + * + * **Details** + * + * The update function receives `Some(value)` when the key exists or `None` + * when it does not. Returning `Some(newValue)` stores the value, and returning + * `None` removes the key or leaves it absent. + * + * **Example** (Updating values with a hash) + * + * ```ts + * import { Hash, HashMap, Option } from "effect" + * + * // Useful when working with precomputed hashes for performance + * const counters = HashMap.make(["downloads", 100], ["views", 250]) + * + * // Cache hash computation for frequently accessed keys + * const metricKey = "downloads" + * const cachedHash = Hash.string(metricKey) + * + * // Update function that increments counter or initializes to 1 + * const incrementCounter = (current: Option.Option) => + * Option.isSome(current) ? Option.some(current.value + 1) : Option.some(1) + * + * // Use cached hash for efficient updates in loops + * const updated = HashMap.modifyHash( + * counters, + * metricKey, + * cachedHash, + * incrementCounter + * ) + * console.log(HashMap.get(updated, "downloads")) // Option.some(101) + * + * // Add new metric with precomputed hash + * const newMetric = "clicks" + * const clicksHash = Hash.string(newMetric) + * const withClicks = HashMap.modifyHash( + * updated, + * newMetric, + * clicksHash, + * incrementCounter + * ) + * console.log(HashMap.get(withClicks, "clicks")) // Option.some(1) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const modifyHash: { + (key: K, hash: number, f: HashMap.UpdateFn): (self: HashMap) => HashMap + (self: HashMap, key: K, hash: number, f: HashMap.UpdateFn): HashMap +} = internal.modifyHash + +/** + * Updates the value of the specified key within the `HashMap` if it exists. + * + * **Example** (Modifying existing values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2]) + * const map2 = HashMap.modify(map1, "a", (value) => value * 3) + * + * console.log(HashMap.get(map2, "a")) // Option.some(3) + * console.log(HashMap.get(map2, "b")) // Option.some(2) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const modify: { + (key: K, f: (v: V) => V): (self: HashMap) => HashMap + (self: HashMap, key: K, f: (v: V) => V): HashMap +} = internal.modify + +/** + * Combines two `HashMap`s into one. + * + * **Details** + * + * Entries from `that` are inserted into `self`; when both maps contain an + * equal key, the value from `that` replaces the value from `self`. + * + * **Example** (Combining HashMaps) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2]) + * const map2 = HashMap.make(["b", 20], ["c", 3]) + * const union = HashMap.union(map1, map2) + * + * console.log(HashMap.size(union)) // 3 + * console.log(HashMap.get(union, "b")) // Option.some(20) - map2 wins + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const union: { + (that: HashMap): (self: HashMap) => HashMap + (self: HashMap, that: HashMap): HashMap +} = internal.union + +/** + * Removes the entry for the specified key in the `HashMap` using the internal + * hashing function. + * + * **Example** (Removing a key) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const map2 = HashMap.remove(map1, "b") + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.has(map2, "b")) // false + * console.log(HashMap.has(map2, "a")) // true + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const remove: { + (key: K): (self: HashMap) => HashMap + (self: HashMap, key: K): HashMap +} = internal.remove + +/** + * Removes all entries in the `HashMap` which have the specified keys. + * + * **Example** (Removing multiple keys) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) + * const map2 = HashMap.removeMany(map1, ["b", "d"]) + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.has(map2, "a")) // true + * console.log(HashMap.has(map2, "c")) // true + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const removeMany: { + (keys: Iterable): (self: HashMap) => HashMap + (self: HashMap, keys: Iterable): HashMap +} = internal.removeMany + +/** + * Sets multiple key-value pairs in the `HashMap`. + * + * **Example** (Setting multiple entries) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2]) + * const newEntries = [["c", 3], ["d", 4], ["a", 10]] as const // "a" will be overwritten + * const map2 = HashMap.setMany(map1, newEntries) + * + * console.log(HashMap.size(map2)) // 4 + * console.log(HashMap.get(map2, "a")) // Option.some(10) + * console.log(HashMap.get(map2, "c")) // Option.some(3) + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export const setMany: { + (entries: Iterable): (self: HashMap) => HashMap + (self: HashMap, entries: Iterable): HashMap +} = internal.setMany + +/** + * Maps over the entries of the `HashMap` using the specified function. + * + * **Example** (Mapping values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const map2 = HashMap.map(map1, (value, key) => `${key}:${value * 2}`) + * + * console.log(HashMap.get(map2, "a")) // Option.some("a:2") + * console.log(HashMap.get(map2, "b")) // Option.some("b:4") + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (value: V, key: K) => A): (self: HashMap) => HashMap + (self: HashMap, f: (value: V, key: K) => A): HashMap +} = internal.map + +/** + * Maps each entry to a `HashMap` and flattens the results. + * + * **Gotchas** + * + * The hash and equality behavior of both maps have to be the same. + * + * **Example** (Flat mapping values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2]) + * const map2 = HashMap.flatMap( + * map1, + * (value, key) => HashMap.make([key + "1", value], [key + "2", value * 2]) + * ) + * + * console.log(HashMap.size(map2)) // 4 + * console.log(HashMap.get(map2, "a1")) // Option.some(1) + * console.log(HashMap.get(map2, "b2")) // Option.some(4) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + (f: (value: A, key: K) => HashMap): (self: HashMap) => HashMap + (self: HashMap, f: (value: A, key: K) => HashMap): HashMap +} = internal.flatMap + +/** + * Applies the specified function to the entries of the `HashMap`. + * + * **Example** (Iterating with side effects) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2]) + * const collected: Array<[string, number]> = [] + * + * HashMap.forEach(map, (value, key) => { + * collected.push([key, value]) + * }) + * + * console.log(collected.sort()) // [["a", 1], ["b", 2]] + * ``` + * + * @category traversing + * @since 2.0.0 + */ +export const forEach: { + (f: (value: V, key: K) => void): (self: HashMap) => void + (self: HashMap, f: (value: V, key: K) => void): void +} = internal.forEach + +/** + * Reduces the specified state over the entries of the `HashMap`. + * + * **Example** (Reducing values) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const sum = HashMap.reduce(map, 0, (acc, value) => acc + value) + * + * console.log(sum) // 6 + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: V, key: K) => Z): (self: HashMap) => Z + (self: HashMap, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = internal.reduce + +/** + * Filters entries out of a `HashMap` using the specified predicate. + * + * **Example** (Filtering entries) + * + * ```ts + * import { HashMap } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) + * const map2 = HashMap.filter(map1, (value) => value % 2 === 0) + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.has(map2, "b")) // true + * console.log(HashMap.has(map2, "d")) // true + * console.log(HashMap.has(map2, "a")) // false + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (f: (a: NoInfer, k: K) => boolean): (self: HashMap) => HashMap + (self: HashMap, f: (a: A, k: K) => boolean): HashMap +} = internal.filter + +/** + * Filters out `None` values from a `HashMap` of `Options`s. + * + * **Example** (Compacting Option values) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const map1 = HashMap.make( + * ["a", Option.some(1)], + * ["b", Option.none()], + * ["c", Option.some(3)] + * ) + * const map2 = HashMap.compact(map1) + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.get(map2, "a")) // Option.some(1) + * console.log(HashMap.has(map2, "b")) // false + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const compact: (self: HashMap>) => HashMap = internal.compact + +/** + * Maps over the entries of the `HashMap` using the specified filter and keeps + * only successful results. + * + * **Example** (Filtering and mapping Results) + * + * ```ts + * import { HashMap, Result } from "effect" + * + * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) + * const map2 = HashMap.filterMap( + * map1, + * (value) => value % 2 === 0 ? Result.succeed(value * 2) : Result.failVoid + * ) + * + * console.log(HashMap.size(map2)) // 2 + * console.log(HashMap.get(map2, "b")) // Option.some(4) + * console.log(HashMap.get(map2, "d")) // Option.some(8) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (input: A, key: K) => Result): (self: HashMap) => HashMap + (self: HashMap, f: (input: A, key: K) => Result): HashMap +} = internal.filterMap + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * **Example** (Finding the first matching entry) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * const result = HashMap.findFirst(map, (value, key) => key === "b" && value > 1) + * console.log(result) // Option.some(["b", 2]) + * console.log(Option.getOrElse(result, () => ["", 0])) // ["b", 2] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => Option<[K, A]> + (self: HashMap, predicate: (a: A, k: K) => boolean): Option<[K, A]> +} = internal.findFirst + +/** + * Checks whether any entry in a hashmap meets a specific condition. + * + * **Example** (Checking for any matching entry) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * + * console.log(HashMap.some(map, (value) => value > 2)) // true + * console.log(HashMap.some(map, (value) => value > 5)) // false + * ``` + * + * @category elements + * @since 3.13.0 + */ +export const some: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (a: A, k: K) => boolean): boolean +} = internal.some + +/** + * Checks whether all entries in a hashmap meets a specific condition. + * + * **Example** (Checking all entries) + * + * ```ts + * import { HashMap } from "effect" + * + * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) + * + * console.log(HashMap.every(map, (value) => value > 0)) // true + * console.log(HashMap.every(map, (value) => value > 1)) // false + * ``` + * + * @category elements + * @since 3.14.0 + */ +export const every: { + (predicate: (a: NoInfer, k: K) => boolean): (self: HashMap) => boolean + (self: HashMap, predicate: (a: A, k: K) => boolean): boolean +} = internal.every diff --git a/repos/effect/packages/effect/src/HashRing.ts b/repos/effect/packages/effect/src/HashRing.ts new file mode 100644 index 0000000..0b9f9c8 --- /dev/null +++ b/repos/effect/packages/effect/src/HashRing.ts @@ -0,0 +1,426 @@ +/** + * Assigns string inputs to nodes with weighted consistent hashing. + * + * A hash ring minimizes remapping when nodes are added, removed, or reweighted. + * This makes it useful for routing requests, partitioning keys, and + * distributing shards across service instances or storage backends. This module + * can create rings, add or remove nodes by `PrimaryKey`, route an input string + * to a node, and compute shard assignments. + * + * @since 4.0.0 + */ +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Iterable from "./Iterable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as PrimaryKey from "./PrimaryKey.ts" + +const TypeId = "~effect/cluster/HashRing" as const + +/** + * A weighted consistent-hashing ring for assigning inputs to nodes with stable + * remapping as nodes are added or removed. + * + * **When to use** + * + * Use to maintain a mutable weighted hash ring for routing keys or shards to + * nodes identified by `PrimaryKey`. + * + * **Details** + * + * Nodes are identified by their `PrimaryKey` value and can be iterated from the + * ring. + * + * @category models + * @since 3.19.0 + */ +export interface HashRing extends Pipeable, Iterable { + readonly [TypeId]: typeof TypeId + readonly baseWeight: number + totalWeightCache: number + readonly nodes: Map + ring: Array<[hash: number, node: string]> +} + +/** + * Checks whether a value is a `HashRing`. + * + * **When to use** + * + * Use to narrow an `unknown` value before treating it as a `HashRing`, such as + * values crossing an untyped boundary. + * + * **Details** + * + * The guard checks for the module's internal `TypeId` property and narrows to + * `HashRing`. + * + * **Gotchas** + * + * This is a structural type-id check; it does not validate the ring's `nodes`, + * `ring`, or weight state. + * + * @see {@link HashRing} for the type narrowed by this guard + * @see {@link make} for creating an empty `HashRing` + * + * @category guards + * @since 3.19.0 + */ +export const isHashRing = (u: unknown): u is HashRing => hasProperty(u, TypeId) + +/** + * Creates an empty `HashRing`. + * + * **When to use** + * + * Use to create an empty weighted consistent-hashing ring with the default or + * custom virtual-point density. + * + * **Details** + * + * `baseWeight` controls how many virtual points are added for a node with + * weight `1`; it defaults to `128` and is clamped to at least `1`. + * + * @see {@link add} for registering one node after creation + * @see {@link addMany} for registering several nodes after creation + * + * @category constructors + * @since 3.19.0 + */ +export const make = (options?: { + readonly baseWeight?: number | undefined +}): HashRing => { + const self = Object.create(Proto) + self.baseWeight = Math.max(options?.baseWeight ?? 128, 1) + self.totalWeightCache = 0 + self.nodes = new Map() + self.ring = [] + return self +} + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + [Symbol.iterator](this: HashRing): Iterator { + return Iterable.map(this.nodes.values(), ([n]) => n)[Symbol.iterator]() + }, + toJSON(this: HashRing) { + return { + _id: "HashRing", + baseWeight: this.baseWeight, + nodes: this.ring.map(([, n]) => this.nodes.get(n)![0]) + } + } +} + +/** + * Adds new nodes to the ring. If a node already exists in the ring, it + * will be updated. For example, you can use this to update the node's weight. + * + * **When to use** + * + * Use to register or update several nodes in a `HashRing` at the same weight. + * + * @category combinators + * @since 3.19.0 + */ +export const addMany: { + (nodes: Iterable, options?: { + readonly weight?: number | undefined + }): (self: HashRing) => HashRing + (self: HashRing, nodes: Iterable, options?: { + readonly weight?: number | undefined + }): HashRing +} = dual( + (args) => isHashRing(args[0]), + (self: HashRing, nodes: Iterable, options?: { + readonly weight?: number | undefined + }): HashRing => { + const weight = Math.max(options?.weight ?? 1, 0.1) + const keys: Array = [] + let toRemove: Set | undefined + for (const node of nodes) { + const key = PrimaryKey.value(node) + const entry = self.nodes.get(key) + if (entry) { + if (entry[1] === weight) continue + toRemove ??= new Set() + toRemove.add(key) + self.totalWeightCache -= entry[1] + self.totalWeightCache += weight + entry[1] = weight + } else { + self.nodes.set(key, [node, weight]) + self.totalWeightCache += weight + } + keys.push(key) + } + if (toRemove) { + self.ring = self.ring.filter(([, n]) => !toRemove.has(n)) + } + addNodesToRing(self, keys, Math.round(weight * self.baseWeight)) + return self + } +) + +function addNodesToRing(self: HashRing, keys: Array, weight: number) { + for (let i = weight; i > 0; i--) { + for (let j = 0; j < keys.length; j++) { + const key = keys[j] + self.ring.push([ + Hash.string(`${key}:${i}`), + key + ]) + } + } + self.ring.sort((a, b) => a[0] - b[0]) +} + +/** + * Adds a new node to the ring. If the node already exists in the ring, it + * will be updated. For example, you can use this to update the node's weight. + * + * **When to use** + * + * Use to register one node in a `HashRing` so lookups and shard assignments can + * return it, or update that node's weight. + * + * **Details** + * + * Nodes are matched by `PrimaryKey.value`. The weight defaults to `1` and is + * clamped to at least `0.1`. + * + * **Gotchas** + * + * This mutates and returns the same ring instance. + * + * @see {@link addMany} for adding or updating several nodes + * @see {@link remove} for unregistering a node + * @see {@link has} for checking primary-key membership + * + * @category combinators + * @since 3.19.0 + */ +export const add: { + (node: A, options?: { + readonly weight?: number | undefined + }): (self: HashRing) => HashRing + (self: HashRing, node: A, options?: { + readonly weight?: number | undefined + }): HashRing +} = dual((args) => isHashRing(args[0]), (self: HashRing, node: A, options?: { + readonly weight?: number | undefined +}): HashRing => addMany(self, [node], options)) + +/** + * Removes the node from the ring. No-op's if the node does not exist. + * + * **When to use** + * + * Use to remove a node that has left the pool so future lookups and shard + * assignments stop returning it. + * + * **Details** + * + * Removal matches by `PrimaryKey.value`, so any value with the same primary key + * removes the same ring member. + * + * **Gotchas** + * + * This mutates and returns the same ring instance. + * + * @see {@link add} for registering or updating a node + * @see {@link has} for checking membership by primary key + * + * @category combinators + * @since 3.19.0 + */ +export const remove: { + (node: A): (self: HashRing) => HashRing + (self: HashRing, node: A): HashRing +} = dual(2, (self: HashRing, node: A): HashRing => { + const key = PrimaryKey.value(node) + const entry = self.nodes.get(key) + if (entry) { + self.nodes.delete(key) + self.ring = self.ring.filter(([, n]) => n !== key) + self.totalWeightCache -= entry[1] + } + return self +}) + +/** + * Checks whether the ring contains a node with the same `PrimaryKey` value. + * + * **When to use** + * + * Use when you need to know whether registering a node would update an existing + * ring member because another node already has the same primary-key identity. + * + * **Details** + * + * Membership is checked with `self.nodes.has(PrimaryKey.value(node))`, so + * matching is by primary key, not object identity or weight. + * + * @see {@link add} for registering or updating nodes + * @see {@link remove} for removing nodes by the same primary-key identity + * @see {@link get} for routing an input string to a node + * + * @category combinators + * @since 3.19.0 + */ +export const has: { + (node: A): (self: HashRing) => boolean + (self: HashRing, node: A): boolean +} = dual( + 2, + (self: HashRing, node: A): boolean => self.nodes.has(PrimaryKey.value(node)) +) + +/** + * Gets the node which should handle the given input. Returns undefined if + * the hashring has no elements with weight. + * + * **When to use** + * + * Use to route a single string input key to the current ring member responsible + * for that key. + * + * @see {@link getShards} for assigning fixed shard indexes instead of routing + * one input string at a time + * + * @category combinators + * @since 3.19.0 + */ +export const get = (self: HashRing, input: string): A | undefined => { + if (self.ring.length === 0) { + return undefined + } + const index = getIndexForInput(self, Hash.string(input))[0] + const node = self.ring[index][1]! + return self.nodes.get(node)![0] +} + +/** + * Computes a balanced shard distribution across the nodes in the ring. + * + * **When to use** + * + * Use to precompute ownership for a fixed number of shard indexes across the + * current ring members. + * + * @category combinators + * @since 3.19.0 + */ +export const getShards = (self: HashRing, count: number): Array | undefined => { + if (self.ring.length === 0) { + return undefined + } + + const shards = new Array(count) + + // for tracking how many shards have been allocated to each node + const allocations = new Map() + // for tracking which shards still need to be allocated + const remaining = new Set() + // for tracking which nodes have reached the max allocation + const exclude = new Set() + + // First pass - allocate the closest nodes, skipping nodes that have reached + // max + const distances = new Array<[shard: number, node: string, distance: number]>(count) + for (let shard = 0; shard < count; shard++) { + const hash = (shardHashes[shard] ??= Hash.string(`shard-${shard}`)) + const [index, distance] = getIndexForInput(self, hash) + const node = self.ring[index][1]! + distances[shard] = [shard, node, distance] + remaining.add(shard) + } + distances.sort((a, b) => a[2] - b[2]) + for (let i = 0; i < count; i++) { + const [shard, node] = distances[i] + if (exclude.has(node)) continue + const [value, weight] = self.nodes.get(node)! + shards[shard] = value + remaining.delete(shard) + const nodeCount = (allocations.get(node) ?? 0) + 1 + allocations.set(node, nodeCount) + const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache))) + if (nodeCount >= maxPerNode) { + exclude.add(node) + } + } + + // Second pass - allocate any remaining shards, skipping nodes that have + // reached max + let allAtMax = exclude.size === self.nodes.size + remaining.forEach((shard) => { + const index = getIndexForInput(self, shardHashes[shard], allAtMax ? undefined : exclude)[0] + const node = self.ring[index][1] + const [value, weight] = self.nodes.get(node)! + shards[shard] = value + + if (allAtMax) return + const nodeCount = (allocations.get(node) ?? 0) + 1 + allocations.set(node, nodeCount) + const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache))) + if (nodeCount >= maxPerNode) { + exclude.add(node) + if (exclude.size === self.nodes.size) { + allAtMax = true + } + } + }) + + return shards +} + +const shardHashes: Array = [] + +function getIndexForInput( + self: HashRing, + hash: number, + exclude?: ReadonlySet | undefined +): readonly [index: number, distance: number] { + const ring = self.ring + const len = ring.length + + let mid: number + let lo = 0 + let hi = len - 1 + + while (lo <= hi) { + mid = ((lo + hi) / 2) >>> 0 + if (ring[mid][0] >= hash) { + hi = mid - 1 + } else { + lo = mid + 1 + } + } + const a = lo === len ? lo - 1 : lo + const distA = Math.abs(ring[a][0] - hash) + if (exclude === undefined) { + const b = lo - 1 + if (b < 0) { + return [a, distA] + } + const distB = Math.abs(ring[b][0] - hash) + return distA <= distB ? [a, distA] : [b, distB] + } else if (!exclude.has(ring[a][1])) { + return [a, distA] + } + const range = Math.max(lo, len - lo) + for (let i = 1; i < range; i++) { + let index = lo - i + if (index >= 0 && index < len && !exclude.has(ring[index][1])) { + return [index, Math.abs(ring[index][0] - hash)] + } + index = lo + i + if (index >= 0 && index < len && !exclude.has(ring[index][1])) { + return [index, Math.abs(ring[index][0] - hash)] + } + } + return [a, distA] +} diff --git a/repos/effect/packages/effect/src/HashSet.ts b/repos/effect/packages/effect/src/HashSet.ts new file mode 100644 index 0000000..fc6e6dd --- /dev/null +++ b/repos/effect/packages/effect/src/HashSet.ts @@ -0,0 +1,605 @@ +/** + * Stores unique values in an immutable hash set. + * + * A `HashSet` contains at most one value for each equality class according + * to Effect's `Equal` and `Hash` rules. Membership checks, additions, removals, + * and set operations return new sets. This module also includes constructors, + * union, intersection, difference, subset checks, mapping, filtering, and + * reducing helpers. + * + * @since 2.0.0 + */ + +import type { Equal } from "./Equal.ts" +import * as Dual from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as internal from "./internal/hashSet.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Predicate, Refinement } from "./Predicate.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = internal.HashSetTypeId + +/** + * A HashSet is an immutable set data structure that provides efficient storage + * and retrieval of unique values. It uses a HashMap internally for optimal performance. + * + * **Example** (Creating and updating a HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * // Create a HashSet + * const set = HashSet.make("apple", "banana", "cherry") + * + * // Check membership + * console.log(HashSet.has(set, "apple")) // true + * console.log(HashSet.has(set, "grape")) // false + * + * // Add values (returns new HashSet) + * const updated = HashSet.add(set, "grape") + * console.log(HashSet.size(updated)) // 4 + * + * // Remove values (returns new HashSet) + * const smaller = HashSet.remove(set, "banana") + * console.log(HashSet.size(smaller)) // 2 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface HashSet extends Iterable, Equal, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId +} + +/** + * The HashSet namespace contains type-level utilities and helper types + * for working with HashSet instances. + * + * **Example** (Extracting value types from a HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * // Create a concrete HashSet for type extraction + * const fruits = HashSet.make("apple", "banana", "cherry") + * + * // Extract the value type for reuse + * type Fruit = HashSet.HashSet.Value // string + * + * // Use extracted type in functions + * const processFruit = (fruit: Fruit) => { + * return `Processing ${fruit}` + * } + * ``` + * + * @since 2.0.0 + */ +export declare namespace HashSet { + /** + * Extracts the element type from a `HashSet`. + * + * **Details** + * + * For `HashSet.HashSet`, `HashSet.Value<...>` resolves to `A`. + * + * **Example** (Extracting a HashSet value type) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(1, 2, 3, 4, 5) + * + * // Extract the value type + * type NumberType = HashSet.HashSet.Value // number + * + * const processNumber = (n: NumberType) => n * 2 + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type Value = T extends HashSet ? V : never +} + +/** + * Creates an empty HashSet. + * + * **Example** (Creating an empty HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * const set = HashSet.empty() + * + * console.log(HashSet.size(set)) // 0 + * console.log(HashSet.isEmpty(set)) // true + * + * // Add some values + * const withValues = HashSet.add(HashSet.add(set, "hello"), "world") + * console.log(HashSet.size(withValues)) // 2 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => HashSet = internal.empty + +/** + * Creates a HashSet from a variable number of values. + * + * **Example** (Creating a HashSet from values) + * + * ```ts + * import { HashSet } from "effect" + * + * const fruits = HashSet.make("apple", "banana", "cherry") + * console.log(HashSet.size(fruits)) // 3 + * + * const numbers = HashSet.make(1, 2, 3, 2, 1) // Duplicates ignored + * console.log(HashSet.size(numbers)) // 3 + * + * const mixed = HashSet.make("hello", 42, true) + * console.log(HashSet.size(mixed)) // 3 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: >( + ...values: Values +) => HashSet = internal.make + +/** + * Creates a HashSet from an iterable collection of values. + * + * **Example** (Creating a HashSet from an iterable) + * + * ```ts + * import { HashSet } from "effect" + * + * const fromArray = HashSet.fromIterable(["a", "b", "c", "b", "a"]) + * console.log(HashSet.size(fromArray)) // 3 + * + * const fromSet = HashSet.fromIterable(new Set([1, 2, 3])) + * console.log(HashSet.size(fromSet)) // 3 + * + * const fromString = HashSet.fromIterable("hello") + * console.log(Array.from(fromString)) // ["h", "e", "l", "o"] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable: (values: Iterable) => HashSet = internal.fromIterable + +/** + * Checks whether a value is a HashSet. + * + * **Example** (Checking for a HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * const set = HashSet.make(1, 2, 3) + * const array = [1, 2, 3] + * + * console.log(HashSet.isHashSet(set)) // true + * console.log(HashSet.isHashSet(array)) // false + * console.log(HashSet.isHashSet(null)) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isHashSet: { + (u: Iterable): u is HashSet + (u: unknown): u is HashSet +} = internal.isHashSet + +/** + * Adds a value to the HashSet, returning a new HashSet. + * + * **Example** (Adding values to a HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * const set = HashSet.make("a", "b") + * const withC = HashSet.add(set, "c") + * + * console.log(HashSet.size(set)) // 2 (original unchanged) + * console.log(HashSet.size(withC)) // 3 + * console.log(HashSet.has(withC, "c")) // true + * + * // Adding existing value has no effect + * const same = HashSet.add(set, "a") + * console.log(HashSet.size(same)) // 2 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const add: { + (value: V): (self: HashSet) => HashSet + (self: HashSet, value: V): HashSet +} = Dual.dual< + (value: V) => (self: HashSet) => HashSet, + (self: HashSet, value: V) => HashSet +>(2, internal.add) + +/** + * Checks whether the HashSet contains the specified value. + * + * **Example** (Checking HashSet membership) + * + * ```ts + * import { Equal, Hash, HashSet } from "effect" + * + * // Works with any type that implements Equal + * + * const set = HashSet.make("apple", "banana", "cherry") + * + * console.log(HashSet.has(set, "apple")) // true + * console.log(HashSet.has(set, "grape")) // false + * + * class Person implements Equal.Equal { + * constructor(readonly name: string) {} + * + * [Equal.symbol](other: unknown) { + * return other instanceof Person && this.name === other.name + * } + * + * [Hash.symbol](): number { + * return Hash.string(this.name) + * } + * } + * + * const people = HashSet.make(new Person("Alice"), new Person("Bob")) + * console.log(HashSet.has(people, new Person("Alice"))) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (value: V): (self: HashSet) => boolean + (self: HashSet, value: V): boolean +} = Dual.dual< + (value: V) => (self: HashSet) => boolean, + (self: HashSet, value: V) => boolean +>(2, internal.has) + +/** + * Removes a value from the HashSet, returning a new HashSet. + * + * **Example** (Removing values from a HashSet) + * + * ```ts + * import { HashSet } from "effect" + * + * const set = HashSet.make("a", "b", "c") + * const withoutB = HashSet.remove(set, "b") + * + * console.log(HashSet.size(set)) // 3 (original unchanged) + * console.log(HashSet.size(withoutB)) // 2 + * console.log(HashSet.has(withoutB, "b")) // false + * + * // Removing non-existent value has no effect + * const same = HashSet.remove(set, "d") + * console.log(HashSet.size(same)) // 3 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (value: V): (self: HashSet) => HashSet + (self: HashSet, value: V): HashSet +} = Dual.dual< + (value: V) => (self: HashSet) => HashSet, + (self: HashSet, value: V) => HashSet +>(2, internal.remove) + +/** + * Returns the number of values in the HashSet. + * + * **Example** (Getting the HashSet size) + * + * ```ts + * import { HashSet } from "effect" + * + * const empty = HashSet.empty() + * console.log(HashSet.size(empty)) // 0 + * + * const small = HashSet.make("a", "b") + * console.log(HashSet.size(small)) // 2 + * + * const withDuplicates = HashSet.fromIterable(["x", "y", "z", "x", "y"]) + * console.log(HashSet.size(withDuplicates)) // 3 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size: (self: HashSet) => number = internal.size + +/** + * Checks whether the HashSet is empty. + * + * **Example** (Checking whether a HashSet is empty) + * + * ```ts + * import { HashSet } from "effect" + * + * const empty = HashSet.empty() + * console.log(HashSet.isEmpty(empty)) // true + * + * const nonEmpty = HashSet.make("a") + * console.log(HashSet.isEmpty(nonEmpty)) // false + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const isEmpty: (self: HashSet) => boolean = internal.isEmpty + +/** + * Creates the union of two HashSets. + * + * **Example** (Combining HashSets) + * + * ```ts + * import { HashSet } from "effect" + * + * const set1 = HashSet.make("a", "b") + * const set2 = HashSet.make("b", "c") + * const combined = HashSet.union(set1, set2) + * + * console.log(Array.from(combined).sort()) // ["a", "b", "c"] + * console.log(HashSet.size(combined)) // 3 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const union: { + (that: HashSet): (self: HashSet) => HashSet + (self: HashSet, that: HashSet): HashSet +} = Dual.dual< + (that: HashSet) => (self: HashSet) => HashSet, + (self: HashSet, that: HashSet) => HashSet +>(2, internal.union) + +/** + * Creates the intersection of two HashSets. + * + * **Example** (Finding common HashSet values) + * + * ```ts + * import { HashSet } from "effect" + * + * const set1 = HashSet.make("a", "b", "c") + * const set2 = HashSet.make("b", "c", "d") + * const common = HashSet.intersection(set1, set2) + * + * console.log(Array.from(common).sort()) // ["b", "c"] + * console.log(HashSet.size(common)) // 2 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const intersection: { + (that: HashSet): (self: HashSet) => HashSet + (self: HashSet, that: HashSet): HashSet +} = Dual.dual< + (that: HashSet) => (self: HashSet) => HashSet, + (self: HashSet, that: HashSet) => HashSet +>(2, internal.intersection) + +/** + * Creates the difference of two HashSets (elements in the first set that are not in the second). + * + * **Example** (Finding HashSet differences) + * + * ```ts + * import { HashSet } from "effect" + * + * const set1 = HashSet.make("a", "b", "c") + * const set2 = HashSet.make("b", "d") + * const diff = HashSet.difference(set1, set2) + * + * console.log(Array.from(diff).sort()) // ["a", "c"] + * console.log(HashSet.size(diff)) // 2 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const difference: { + (that: HashSet): (self: HashSet) => HashSet + (self: HashSet, that: HashSet): HashSet +} = Dual.dual< + (that: HashSet) => (self: HashSet) => HashSet, + (self: HashSet, that: HashSet) => HashSet +>(2, internal.difference) + +/** + * Checks whether a HashSet is a subset of another HashSet. + * + * **Example** (Checking subset relationships) + * + * ```ts + * import { HashSet } from "effect" + * + * const small = HashSet.make("a", "b") + * const large = HashSet.make("a", "b", "c", "d") + * const other = HashSet.make("x", "y") + * + * console.log(HashSet.isSubset(small, large)) // true + * console.log(HashSet.isSubset(large, small)) // false + * console.log(HashSet.isSubset(small, other)) // false + * console.log(HashSet.isSubset(small, small)) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const isSubset: { + (that: HashSet): (self: HashSet) => boolean + (self: HashSet, that: HashSet): boolean +} = Dual.dual< + (that: HashSet) => (self: HashSet) => boolean, + (self: HashSet, that: HashSet) => boolean +>(2, internal.isSubset) + +/** + * Maps each value in the HashSet using the provided function. + * + * **Example** (Mapping HashSet values) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(1, 2, 3) + * const doubled = HashSet.map(numbers, (n) => n * 2) + * + * console.log(Array.from(doubled).sort()) // [2, 4, 6] + * console.log(HashSet.size(doubled)) // 3 + * + * // Mapping can reduce size if function produces duplicates + * const strings = HashSet.make("apple", "banana", "cherry") + * const lengths = HashSet.map(strings, (s) => s.length) + * console.log(Array.from(lengths).sort()) // [5, 6] (apple=5, banana=6, cherry=6) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (value: V) => U): (self: HashSet) => HashSet + (self: HashSet, f: (value: V) => U): HashSet +} = Dual.dual< + (f: (value: V) => U) => (self: HashSet) => HashSet, + (self: HashSet, f: (value: V) => U) => HashSet +>(2, internal.map) + +/** + * Filters the HashSet keeping only values that satisfy the predicate. + * + * **Example** (Filtering HashSet values) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(1, 2, 3, 4, 5, 6) + * const evens = HashSet.filter(numbers, (n) => n % 2 === 0) + * + * console.log(Array.from(evens).sort()) // [2, 4, 6] + * console.log(HashSet.size(evens)) // 3 + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: Refinement, U>): (self: HashSet) => HashSet + (predicate: Predicate>): (self: HashSet) => HashSet + (self: HashSet, refinement: Refinement): HashSet + (self: HashSet, predicate: Predicate): HashSet +} = Dual.dual< + { + (refinement: Refinement, U>): (self: HashSet) => HashSet + (predicate: Predicate>): (self: HashSet) => HashSet + }, + { + (self: HashSet, refinement: Refinement): HashSet + (self: HashSet, predicate: Predicate): HashSet + } +>(2, internal.filter) + +/** + * Checks whether at least one value in the HashSet satisfies the predicate. + * + * **Example** (Testing whether some values match) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(1, 2, 3, 4, 5) + * + * console.log(HashSet.some(numbers, (n) => n > 3)) // true + * console.log(HashSet.some(numbers, (n) => n > 10)) // false + * + * const empty = HashSet.empty() + * console.log(HashSet.some(empty, (n) => n > 0)) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const some: { + (predicate: Predicate): (self: HashSet) => boolean + (self: HashSet, predicate: Predicate): boolean +} = Dual.dual< + (predicate: Predicate) => (self: HashSet) => boolean, + (self: HashSet, predicate: Predicate) => boolean +>(2, internal.some) + +/** + * Checks whether all values in the HashSet satisfy the predicate. + * + * **Example** (Testing whether every value matches) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(2, 4, 6, 8) + * + * console.log(HashSet.every(numbers, (n) => n % 2 === 0)) // true + * console.log(HashSet.every(numbers, (n) => n > 5)) // false + * + * const empty = HashSet.empty() + * console.log(HashSet.every(empty, (n) => n > 0)) // true (vacuously true) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const every: { + (predicate: Predicate): (self: HashSet) => boolean + (self: HashSet, predicate: Predicate): boolean +} = Dual.dual< + (predicate: Predicate) => (self: HashSet) => boolean, + (self: HashSet, predicate: Predicate) => boolean +>(2, internal.every) + +/** + * Reduces the HashSet to a single value by iterating through the values and applying an accumulator function. + * + * **Example** (Reducing HashSet values) + * + * ```ts + * import { HashSet } from "effect" + * + * const numbers = HashSet.make(1, 2, 3, 4, 5) + * const sum = HashSet.reduce(numbers, 0, (acc, n) => acc + n) + * + * console.log(sum) // 15 + * + * const strings = HashSet.make("a", "b", "c") + * const concatenated = HashSet.reduce(strings, "", (acc, s) => acc + s) + * console.log(concatenated) // Order may vary: "abc", "bac", etc. + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (zero: U, f: (accumulator: U, value: V) => U): (self: HashSet) => U + (self: HashSet, zero: U, f: (accumulator: U, value: V) => U): U +} = Dual.dual< + (zero: U, f: (accumulator: U, value: V) => U) => (self: HashSet) => U, + (self: HashSet, zero: U, f: (accumulator: U, value: V) => U) => U +>(3, internal.reduce) diff --git a/repos/effect/packages/effect/src/Inspectable.ts b/repos/effect/packages/effect/src/Inspectable.ts new file mode 100644 index 0000000..05fed12 --- /dev/null +++ b/repos/effect/packages/effect/src/Inspectable.ts @@ -0,0 +1,329 @@ +/** + * Controls how values appear in logs and debugging output. + * + * Effect data types use `Inspectable` to provide stable string, JSON, and + * Node.js inspection output. This keeps custom values readable in logs, REPLs, + * test failures, and diagnostics. This module defines the Node inspect symbol, + * the `Inspectable` interface, safe conversion helpers, and shared prototype or + * class implementations for custom values. + * + * @since 2.0.0 + */ +import { format, formatJson } from "./Formatter.ts" +import * as Predicate from "./Predicate.ts" +import { redact } from "./Redactable.ts" + +/** + * Defines the symbol used by Node.js for custom object inspection. + * + * **When to use** + * + * Use to implement Node.js custom inspection for a value. + * + * **Details** + * + * This symbol is recognized by Node.js's `util.inspect()` function and the REPL + * for custom object representation. When an object has a method with this symbol, + * it will be called to determine how the object should be displayed. + * + * **Example** (Defining custom Node inspection) + * + * ```ts + * import { Inspectable } from "effect" + * + * class CustomObject { + * constructor(private value: string) {} + * + * [Inspectable.NodeInspectSymbol]() { + * return `CustomObject(${this.value})` + * } + * } + * + * const obj = new CustomObject("hello") + * console.log(obj) // Displays: CustomObject(hello) + * ``` + * + * @category symbols + * @since 2.0.0 + */ +export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom") + +/** + * The type of the Node.js inspection symbol used for custom object inspection. + * This symbol type is used to implement custom inspection behavior in Node.js + * environments. + * + * **When to use** + * + * Use to type methods keyed by the Node.js custom inspection symbol. + * + * **Example** (Typing custom Node inspection) + * + * ```ts + * import { Inspectable } from "effect" + * + * class CustomObject { + * constructor(private value: string) {} + * + * [Inspectable.NodeInspectSymbol]() { + * return `CustomObject(${this.value})` + * } + * } + * + * const obj = new CustomObject("test") + * console.log(obj) // CustomObject(test) + * ``` + * + * @category symbols + * @since 2.0.0 + */ +export type NodeInspectSymbol = typeof NodeInspectSymbol + +/** + * Interface for objects that can be inspected and provide custom string representations. + * + * **When to use** + * + * Use to define values with custom string, JSON, and Node.js inspection output. + * + * **Details** + * + * Objects implementing this interface can control how they appear in debugging contexts, + * JSON serialization, and Node.js inspection. This is particularly useful for creating + * custom data types that display meaningful information during development. + * + * **Example** (Implementing inspectable objects) + * + * ```ts + * import { Formatter, Inspectable } from "effect" + * + * class Result implements Inspectable.Inspectable { + * constructor( + * private readonly tag: "Success" | "Failure", + * private readonly value: unknown + * ) {} + * + * toString(): string { + * return Formatter.format(this.toJSON()) + * } + * + * toJSON() { + * return { _tag: this.tag, value: this.value } + * } + * + * [Inspectable.NodeInspectSymbol]() { + * return this.toJSON() + * } + * } + * + * const success = new Result("Success", 42) + * console.log(success.toString()) // Pretty formatted JSON + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Inspectable { + toString(): string + toJSON(): unknown + [NodeInspectSymbol](): unknown +} + +/** + * Converts a value to a JSON-serializable representation safely. + * + * **When to use** + * + * Use when you need a safe, JSON-serializable representation of a value + * without risking unhandled errors. + * + * **Details** + * + * This function attempts to extract JSON data from objects that implement the + * `toJSON` method, recursively processes arrays, and handles errors gracefully. + * For objects that don't have a `toJSON` method, it applies redaction to + * protect sensitive information. + * + * @see {@link toStringUnknown} for converting unknown values to strings + * + * @category converting + * @since 4.0.0 + */ +export const toJson = (input: unknown): unknown => { + try { + if ( + Predicate.hasProperty(input, "toJSON") && + Predicate.isFunction(input["toJSON"]) && + input["toJSON"].length === 0 + ) { + return input.toJSON() + } else if (Array.isArray(input)) { + return input.map(toJson) + } + } catch { + return "[toJSON threw]" + } + return redact(input) +} + +/** + * Converts an unknown value to a string for diagnostics. + * + * **When to use** + * + * Use to produce a diagnostic string from a value whose runtime type is unknown. + * + * **Details** + * + * Strings are returned unchanged. Objects are formatted as JSON using the + * provided whitespace setting when possible, and values that cannot be + * formatted are converted with `String`. + * + * @category converting + * @since 2.0.0 + */ +export const toStringUnknown = (u: unknown, whitespace: number | string | undefined = 2): string => { + if (typeof u === "string") { + return u + } + try { + return typeof u === "object" ? formatJson(u, { space: whitespace }) : String(u) + } catch { + return String(u) + } +} + +/** + * A base prototype object that implements the {@link Inspectable} interface. + * + * **When to use** + * + * Use as a prototype for plain objects that should share standard inspectable behavior. + * + * **Details** + * + * This object provides default implementations for the {@link Inspectable} methods. + * It can be used as a prototype for objects that want to be inspectable, + * or as a mixin to add inspection capabilities to existing objects. + * + * **Example** (Using the base inspectable prototype) + * + * ```ts + * import { Inspectable } from "effect" + * + * // Use as prototype + * const myObject = Object.create(Inspectable.BaseProto) + * myObject.name = "example" + * myObject.value = 42 + * + * console.log(myObject.toString()) // Pretty printed representation + * + * // Or extend in a constructor + * function MyClass(this: any, name: string) { + * this.name = name + * } + * MyClass.prototype = Object.create(Inspectable.BaseProto) + * MyClass.prototype.constructor = MyClass + * ``` + * + * @category prototypes + * @since 2.0.0 + */ +export const BaseProto: Inspectable = { + toJSON() { + return toJson(this) + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + toString() { + return format(this.toJSON()) + } +} + +/** + * Provides an abstract base class that implements the Inspectable interface. + * + * **When to use** + * + * Use as a base class for inspectable objects that define their own JSON representation. + * + * **Details** + * + * This class provides a convenient way to create inspectable objects by extending it. + * Subclasses only need to implement the `toJSON()` method, and they automatically + * get proper `toString()` and Node.js inspection support. + * + * **Example** (Extending the inspectable base class) + * + * ```ts + * import { Inspectable } from "effect" + * + * class User extends Inspectable.Class { + * constructor( + * public readonly id: number, + * public readonly name: string, + * public readonly email: string + * ) { + * super() + * } + * + * toJSON() { + * return { + * _tag: "User", + * id: this.id, + * name: this.name, + * email: this.email + * } + * } + * } + * + * const user = new User(1, "Alice", "alice@example.com") + * console.log(user.toString()) // Pretty printed JSON with _tag, id, name, email + * console.log(user) // In Node.js, shows the same formatted output + * ``` + * + * @category classes + * @since 2.0.0 + */ +export abstract class Class { + /** + * Returns a JSON representation of this object. + * + * **When to use** + * + * Use to provide the JSON representation consumed by inherited inspection methods. + * + * **Details** + * + * Subclasses must implement this method to define how the object + * should be serialized for debugging and inspection purposes. + * + * @since 2.0.0 + */ + abstract toJSON(): unknown + /** + * Node.js custom inspection method. + * + * **When to use** + * + * Use to expose the class JSON representation to Node.js inspection. + * + * @since 2.0.0 + */ + [NodeInspectSymbol]() { + return this.toJSON() + } + /** + * Returns a formatted string representation of this object. + * + * **When to use** + * + * Use to format the class JSON representation as a string. + * + * @since 2.0.0 + */ + toString() { + return format(this.toJSON()) + } +} diff --git a/repos/effect/packages/effect/src/Iterable.ts b/repos/effect/packages/effect/src/Iterable.ts new file mode 100644 index 0000000..d30a082 --- /dev/null +++ b/repos/effect/packages/effect/src/Iterable.ts @@ -0,0 +1,2513 @@ +/** + * Works with JavaScript values that implement `[Symbol.iterator]`. + * + * Iterables include arrays, strings, generators, sets, and custom lazy + * sequences. The helpers in this module let code transform, search, group, and + * fold iterable values while preserving the input as an iterable instead of + * forcing an array first. + * + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "./Array.ts" +import * as Equal from "./Equal.ts" +import { dual } from "./Function.ts" +import type { Option } from "./Option.ts" +import * as O from "./Option.ts" +import { isBoolean } from "./Predicate.ts" +import type * as Record from "./Record.ts" +import type { Result } from "./Result.ts" +import * as R from "./Result.ts" +import * as Tuple from "./Tuple.ts" +import type { NoInfer } from "./Types.ts" + +/** + * Creates an iterable by applying a function to consecutive integers. + * + * **Details** + * + * The function is called with each index starting from `0`. If no length is + * specified, the iterable is infinite. This is useful for generating + * sequences, patterns, or any indexed data. + * + * **Example** (Generating values by index) + * + * ```ts + * import { Iterable } from "effect" + * + * // Generate first 5 even numbers + * const evens = Iterable.makeBy((n) => n * 2, { length: 5 }) + * console.log(Array.from(evens)) // [0, 2, 4, 6, 8] + * + * // Generate squares + * const squares = Iterable.makeBy((n) => n * n, { length: 4 }) + * console.log(Array.from(squares)) // [0, 1, 4, 9] + * + * // Infinite sequence (be careful when consuming!) + * const naturals = Iterable.makeBy((n) => n) + * const first10 = Iterable.take(naturals, 10) + * console.log(Array.from(first10)) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeBy = (f: (i: number) => A, options?: { + readonly length?: number +}): Iterable => { + const max = options?.length !== undefined ? Math.max(1, Math.floor(options.length)) : Infinity + return { + [Symbol.iterator]() { + let i = 0 + return { + next(): IteratorResult { + if (i < max) { + return { value: f(i++), done: false } + } + return { done: true, value: undefined } + } + } + } + } +} + +/** + * Returns an iterable of integers starting at `start` and increasing by `1`. + * + * **Details** + * + * When `end` is provided and `start <= end`, both endpoints are included. When + * `end` is omitted, the iterable is unbounded. When `start > end`, the + * iterable contains only `start`. + * + * **Example** (Creating a range) + * + * ```ts + * import { Iterable } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Array.from(Iterable.range(1, 3)), [1, 2, 3]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const range = (start: number, end?: number): Iterable => { + if (end === undefined) { + return makeBy((i) => start + i) + } + return makeBy((i) => start + i, { + length: start <= end ? end - start + 1 : 1 + }) +} + +/** + * Returns a `Iterable` containing a value repeated the specified number of times. + * + * **Details** + * + * `n` is normalized to an integer greater than or equal to `1`. + * + * **Example** (Repeating a value) + * + * ```ts + * import { Iterable } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Array.from(Iterable.replicate("a", 3)), ["a", "a", "a"]) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const replicate: { + (n: number): (a: A) => Iterable + (a: A, n: number): Iterable +} = dual(2, (a: A, n: number): Iterable => makeBy(() => a, { length: n })) + +/** + * Repeats an iterable `n` times, yielding the full contents of `self` for each + * repetition. + * + * **When to use** + * + * Use to repeat an iterable's contents a specific number of times. + * + * **Details** + * + * The result is lazy. Each repetition obtains a new iterator from `self`. + * + * @see {@link forever} for repeating without an upper bound + * @see {@link replicate} for repeating a single value + * @category constructors + * @since 4.0.0 + */ +export const repeat: { + (n: number): (self: Iterable) => Iterable + (self: Iterable, n: number): Iterable +} = dual(2, (self: Iterable, n: number): Iterable => flatten(makeBy(() => self, { length: n }))) + +/** + * Repeats an iterable without an upper bound. + * + * **When to use** + * + * Use to cycle a reusable iterable without an upper bound when a downstream + * consumer controls how many values are taken. + * + * **Gotchas** + * + * The returned iterable is lazy and should usually be bounded with `take` or + * another terminating consumer before materializing it. + * + * @see {@link repeat} for repeating an iterable a specific number of times + * @see {@link take} for bounding the unbounded result before materializing it + * + * @category constructors + * @since 4.0.0 + */ +export const forever = (self: Iterable): Iterable => repeat(self, Infinity) + +/** + * Takes a record and returns an Iterable of tuples containing its keys and values. + * + * **Example** (Converting a record to entries) + * + * ```ts + * import { Iterable } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(Array.from(Iterable.fromRecord(x)), [["a", 1], ["b", 2], [ + * "c", + * 3 + * ]]) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const fromRecord = (self: Readonly>): Iterable<[K, A]> => ({ + *[Symbol.iterator]() { + for (const key in self) { + if (Object.hasOwn(self, key)) { + yield [key, self[key]] + } + } + } +}) + +/** + * Prepends an element to the front of an `Iterable`, creating a new `Iterable`. + * + * **Example** (Prepending an element) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [2, 3, 4] + * const withOne = Iterable.prepend(numbers, 1) + * console.log(Array.from(withOne)) // [1, 2, 3, 4] + * + * // Works with any iterable + * const letters = "abc" + * const withZ = Iterable.prepend(letters, "z") + * console.log(Array.from(withZ)) // ["z", "a", "b", "c"] + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const prepend: { + (head: B): (self: Iterable) => Iterable + (self: Iterable, head: B): Iterable +} = dual(2, (self: Iterable, head: B): Iterable => prependAll(self, [head])) + +/** + * Prepends the specified prefix iterable to the beginning of the specified iterable. + * + * **Example** (Prepending another iterable) + * + * ```ts + * import { Iterable } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Array.from(Iterable.prependAll([1, 2], ["a", "b"])), + * ["a", "b", 1, 2] + * ) + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const prependAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => appendAll(that, self) +) + +/** + * Appends an element to the end of an `Iterable`, creating a new `Iterable`. + * + * **When to use** + * + * Use to add one element after all elements of an iterable while keeping the + * result as a lazy `Iterable`. + * + * **Details** + * + * The result yields every element from `self` first, then yields `last` after + * `self` is exhausted. + * + * **Gotchas** + * + * If `self` is infinite or never completes, the appended element is never + * reached. + * + * **Example** (Appending an element) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3] + * const withFour = Iterable.append(numbers, 4) + * console.log(Array.from(withFour)) // [1, 2, 3, 4] + * + * // Chain multiple appends + * const result = Iterable.append( + * Iterable.append([1, 2], 3), + * 4 + * ) + * console.log(Array.from(result)) // [1, 2, 3, 4] + * ``` + * + * @see {@link prepend} for adding one element before the existing elements + * @see {@link appendAll} for appending all elements from another iterable + * + * @category combining + * @since 2.0.0 + */ +export const append: { + (last: B): (self: Iterable) => Iterable + (self: Iterable, last: B): Iterable +} = dual(2, (self: Iterable, last: B): Iterable => appendAll(self, [last])) + +/** + * Concatenates two iterables, combining their elements. + * + * **When to use** + * + * Use to lazily concatenate two iterables while preserving order, yielding all + * elements from `self` before `that`. + * + * **Details** + * + * The result is lazy. The iterator for `that` is not created or read until + * `self` is exhausted. + * + * **Gotchas** + * + * If `self` is infinite or never completes, `that` is never reached. + * + * **Example** (Concatenating iterables) + * + * ```ts + * import { Iterable } from "effect" + * + * const first = [1, 2, 3] + * const second = [4, 5, 6] + * const combined = Iterable.appendAll(first, second) + * console.log(Array.from(combined)) // [1, 2, 3, 4, 5, 6] + * + * // Works with different iterable types + * const numbers = [1, 2] + * const letters = "abc" + * const mixed = Iterable.appendAll(numbers, letters) + * console.log(Array.from(mixed)) // [1, 2, "a", "b", "c"] + * + * // Lazy evaluation - only consumes what's needed + * const infinite = Iterable.range(1) + * const finite = [0, -1, -2] + * const result = Iterable.take(Iterable.appendAll(finite, infinite), 5) + * console.log(Array.from(result)) // [0, -1, -2, 1, 2] + * ``` + * + * @see {@link append} for appending one value instead of another iterable + * @see {@link prependAll} for yielding another iterable before `self` + * + * @category combining + * @since 2.0.0 + */ +export const appendAll: { + (that: Iterable): (self: Iterable) => Iterable + (self: Iterable, that: Iterable): Iterable +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable => ({ + [Symbol.iterator]() { + const iterA = self[Symbol.iterator]() + let doneA = false + let iterB: Iterator + return { + next() { + if (!doneA) { + const r = iterA.next() + if (r.done) { + doneA = true + iterB = that[Symbol.iterator]() + return iterB.next() + } + return r + } + return iterB.next() + } + } + } + }) +) + +/** + * Reduces an `Iterable` from the left, keeping all intermediate results instead of only the final result. + * + * **Example** (Tracking running results) + * + * ```ts + * import { Iterable } from "effect" + * + * // Running sum of numbers + * const numbers = [1, 2, 3, 4, 5] + * const runningSum = Iterable.scan(numbers, 0, (acc, n) => acc + n) + * console.log(Array.from(runningSum)) // [0, 1, 3, 6, 10, 15] + * + * // Build strings progressively + * const letters = ["a", "b", "c"] + * const progressive = Iterable.scan(letters, "", (acc, letter) => acc + letter) + * console.log(Array.from(progressive)) // ["", "a", "ab", "abc"] + * + * // Track maximum values seen so far + * const values = [3, 1, 4, 1, 5, 9, 2] + * const runningMax = Iterable.scan(values, -Infinity, Math.max) + * console.log(Array.from(runningMax)) // [-Infinity, 3, 3, 4, 4, 5, 9, 9] + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const scan: { + (b: B, f: (b: B, a: A) => B): (self: Iterable) => Iterable + (self: Iterable, b: B, f: (b: B, a: A) => B): Iterable +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A) => B): Iterable => ({ + [Symbol.iterator]() { + let acc = b + let iterator: Iterator | undefined + function next() { + if (iterator === undefined) { + iterator = self[Symbol.iterator]() + return { done: false, value: acc } + } + const result = iterator.next() + if (result.done) { + return result + } + acc = f(acc, result.value) + return { done: false, value: acc } + } + return { next } + } +})) + +/** + * Checks whether an `Iterable` is empty. + * + * **Example** (Checking for emptiness) + * + * ```ts + * import { Iterable } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Iterable.isEmpty([]), true) + * assert.deepStrictEqual(Iterable.isEmpty([1, 2, 3]), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmpty = (self: Iterable): self is Iterable => { + const iterator = self[Symbol.iterator]() + return iterator.next().done === true +} + +/** + * Returns the number of elements in a `Iterable`. + * + * **Example** (Counting iterable elements) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * console.log(Iterable.size(numbers)) // 5 + * + * const empty = Iterable.empty() + * console.log(Iterable.size(empty)) // 0 + * + * // Works with any iterable + * const letters = "hello" + * console.log(Iterable.size(letters)) // 5 + * + * // Note: This consumes the entire iterable + * const range = Iterable.range(1, 100) + * console.log(Iterable.size(range)) // 100 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: Iterable): number => { + const iterator = self[Symbol.iterator]() + let count = 0 + while (!iterator.next().done) { + count++ + } + return count +} + +/** + * Gets the first element of a `Iterable` safely, or `None` if the `Iterable` is empty. + * + * **Example** (Getting the first element) + * + * ```ts + * import { Iterable, Option } from "effect" + * + * const numbers = [1, 2, 3] + * console.log(Iterable.head(numbers)) // Option.some(1) + * + * const empty = Iterable.empty() + * console.log(Iterable.head(empty)) // Option.none() + * + * // Safe way to get first element + * const firstEven = Iterable.head( + * Iterable.filter([1, 3, 4, 5], (x) => x % 2 === 0) + * ) + * console.log(firstEven) // Option.some(4) + * + * // Use with Option methods + * const doubled = Option.map(Iterable.head([5, 10, 15]), (x) => x * 2) + * console.log(doubled) // Option.some(10) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const head = (self: Iterable): Option => { + const iterator = self[Symbol.iterator]() + const result = iterator.next() + return result.done ? O.none() : O.some(result.value) +} + +/** + * Gets the first element of an `Iterable` without returning an `Option`. + * + * **When to use** + * + * Use when the `Iterable` is known to be non-empty and direct access to the + * first element is preferred over handling `Option.none`. + * + * **Gotchas** + * + * Throws if the `Iterable` is empty. + * + * **Example** (Getting the first element unsafely) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3] + * console.log(Iterable.headUnsafe(numbers)) // 1 + * + * const letters = "hello" + * console.log(Iterable.headUnsafe(letters)) // "h" + * + * // Iterable.headUnsafe(Iterable.empty()) + * // throws Error: "headUnsafe: empty iterable" + * + * // Use only when you're certain the iterable is non-empty + * const nonEmpty = Iterable.range(1, 10) + * console.log(Iterable.headUnsafe(nonEmpty)) // 1 + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const headUnsafe = (self: Iterable): A => { + const iterator = self[Symbol.iterator]() + const result = iterator.next() + if (result.done) throw new Error("headUnsafe: empty iterable") + return result.value +} + +/** + * Keeps only a max number of elements from the start of an `Iterable`, creating a new `Iterable`. + * + * **Details** + * + * `n` is normalized to a non-negative integer. + * + * **Example** (Taking from the start) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const firstThree = Iterable.take(numbers, 3) + * console.log(Array.from(firstThree)) // [1, 2, 3] + * + * // Taking more than available returns all elements + * const firstTen = Iterable.take(numbers, 10) + * console.log(Array.from(firstTen)) // [1, 2, 3, 4, 5] + * + * // Taking 0 or negative returns empty + * const none = Iterable.take(numbers, 0) + * console.log(Array.from(none)) // [] + * + * // Useful with infinite iterables + * const naturals = Iterable.range(1) + * const firstFive = Iterable.take(naturals, 5) + * console.log(Array.from(firstFive)) // [1, 2, 3, 4, 5] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Iterable) => Iterable + (self: Iterable, n: number): Iterable +} = dual(2, (self: Iterable, n: number): Iterable => ({ + [Symbol.iterator]() { + let i = 0 + const iterator = self[Symbol.iterator]() + return { + next() { + if (i < n) { + i++ + return iterator.next() + } + return { done: true, value: undefined } + } + } + } +})) + +/** + * Takes the longest initial `Iterable` prefix for which all elements satisfy the + * specified predicate. + * + * **Example** (Taking while a predicate holds) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [2, 4, 6, 8, 3, 10, 12] + * const evenPrefix = Iterable.takeWhile(numbers, (x) => x % 2 === 0) + * console.log(Array.from(evenPrefix)) // [2, 4, 6, 8] + * + * // With index + * const letters = ["a", "b", "c", "d", "e"] + * const firstThreeByIndex = Iterable.takeWhile(letters, (_, i) => i < 3) + * console.log(Array.from(firstThreeByIndex)) // ["a", "b", "c"] + * + * // Stops at first non-matching element + * const mixed = [1, 3, 5, 4, 7, 9] + * const oddPrefix = Iterable.takeWhile(mixed, (x) => x % 2 === 1) + * console.log(Array.from(oddPrefix)) // [1, 3, 5] + * + * // Type refinement + * const values: Array = ["a", "b", "c", 1, "d"] + * const stringPrefix = Iterable.takeWhile( + * values, + * (x): x is string => typeof x === "string" + * ) + * console.log(Array.from(stringPrefix)) // ["a", "b", "c"] (typed as string[]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Iterable + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Iterable + (self: Iterable, refinement: (a: A, i: number) => a is B): Iterable + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable +} = dual(2, (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done || !predicate(result.value, i++)) { + return { done: true, value: undefined } + } + return result + } + } + } +})) + +/** + * Drops a max number of elements from the start of an `Iterable` + * + * **Details** + * + * `n` is normalized to a non-negative integer. + * + * **Example** (Dropping from the start) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * const withoutFirstTwo = Iterable.drop(numbers, 2) + * console.log(Array.from(withoutFirstTwo)) // [3, 4, 5] + * + * // Dropping more than available returns empty + * const withoutFirstTen = Iterable.drop(numbers, 10) + * console.log(Array.from(withoutFirstTen)) // [] + * + * // Dropping 0 or negative returns all elements + * const all = Iterable.drop(numbers, 0) + * console.log(Array.from(all)) // [1, 2, 3, 4, 5] + * + * // Combine with take for slicing + * const slice = Iterable.take(Iterable.drop(numbers, 1), 3) + * console.log(Array.from(slice)) // [2, 3, 4] + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Iterable) => Iterable + (self: Iterable, n: number): Iterable +} = dual(2, (self: Iterable, n: number): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + while (i < n) { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + i++ + } + return iterator.next() + } + } + } +})) + +/** + * Returns the first element that satisfies the specified + * predicate, or `None` if no such element exists. + * + * **Example** (Finding the first match) + * + * ```ts + * import { Iterable, Option } from "effect" + * + * const numbers = [1, 3, 4, 6, 8] + * const firstEven = Iterable.findFirst(numbers, (x) => x % 2 === 0) + * console.log(firstEven) // Option.some(4) + * + * const firstGreaterThan10 = Iterable.findFirst(numbers, (x) => x > 10) + * console.log(firstGreaterThan10) // Option.none() + * + * // With index + * const letters = ["a", "b", "c", "d"] + * const atEvenIndex = Iterable.findFirst(letters, (_, i) => i % 2 === 0) + * console.log(atEvenIndex) // Option.some("a") + * + * // Type refinement + * const mixed: Array = [1, "hello", 2, "world"] + * const firstString = Iterable.findFirst( + * mixed, + * (x): x is string => typeof x === "string" + * ) + * console.log(firstString) // Option.some("hello") + * + * // Transform during search + * const findSquareRoot = Iterable.findFirst([1, 4, 9, 16], (x) => { + * const sqrt = Math.sqrt(x) + * return Number.isInteger(sqrt) ? Option.some(sqrt) : Option.none() + * }) + * console.log(findSquareRoot) // Option.some(1) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findFirst: { + (f: (a: NoInfer, i: number) => Option): (self: Iterable) => Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option + (self: Iterable, f: (a: A, i: number) => Option): Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option +} = dual( + 2, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + let i = 0 + for (const a of self) { + const o = f(a, i) + if (isBoolean(o)) { + if (o) { + return O.some(a) + } + } else { + if (O.isSome(o)) { + return o + } + } + i++ + } + return O.none() + } +) + +/** + * Finds the last element for which a predicate holds. + * + * **Example** (Finding the last match) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 3, 4, 6, 8, 2] + * const lastEven = Iterable.findLast(numbers, (x) => x % 2 === 0) + * console.log(lastEven) // Option.some(2) + * + * const lastGreaterThan10 = Iterable.findLast(numbers, (x) => x > 10) + * console.log(lastGreaterThan10) // Option.none() + * + * // With index + * const letters = ["a", "b", "c", "d", "e"] + * const lastAtEvenIndex = Iterable.findLast(letters, (_, i) => i % 2 === 0) + * console.log(lastAtEvenIndex) // Option.some("e") (index 4) + * + * // Type refinement + * const mixed: Array = [1, "hello", 2, "world", 3] + * const lastString = Iterable.findLast( + * mixed, + * (x): x is string => typeof x === "string" + * ) + * console.log(lastString) // Option.some("world") + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const findLast: { + (f: (a: NoInfer, i: number) => Option): (self: Iterable) => Option + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Option + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Option + (self: Iterable, f: (a: A, i: number) => Option): Option + (self: Iterable, refinement: (a: A, i: number) => a is B): Option + (self: Iterable, predicate: (a: A, i: number) => boolean): Option +} = dual( + 2, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + let i = 0 + let last: Option = O.none() + for (const a of self) { + const o = f(a, i) + if (isBoolean(o)) { + if (o) { + last = O.some(a) + } + } else { + if (O.isSome(o)) { + last = o + } + } + i++ + } + return last + } +) + +/** + * Takes two `Iterable`s and returns an `Iterable` of corresponding pairs. + * + * **Example** (Zipping iterables) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3] + * const letters = ["a", "b", "c"] + * const zipped = Iterable.zip(numbers, letters) + * console.log(Array.from(zipped)) // [[1, "a"], [2, "b"], [3, "c"]] + * + * // Different lengths - shorter one determines result length + * const short = [1, 2] + * const long = ["a", "b", "c", "d"] + * const partial = Iterable.zip(short, long) + * console.log(Array.from(partial)) // [[1, "a"], [2, "b"]] + * + * // Works with any iterables + * const range = Iterable.range(1, 3) + * const word = "abc" + * const mixed = Iterable.zip(range, word) + * console.log(Array.from(mixed)) // [[1, "a"], [2, "b"], [3, "c"]] + * + * // Create indexed pairs + * const values = ["apple", "banana", "cherry"] + * const indices = Iterable.range(0, 2) + * const indexed = Iterable.zip(indices, values) + * console.log(Array.from(indexed)) // [[0, "apple"], [1, "banana"], [2, "cherry"]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: Iterable): (self: Iterable) => Iterable<[A, B]> + (self: Iterable, that: Iterable): Iterable<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable<[A, B]> => zipWith(self, that, Tuple.make) +) + +/** + * Applies a function to pairs of elements at the same index in two `Iterable`s, collecting the results. If one + * input `Iterable` is short, excess elements of the longer `Iterable` are discarded. + * + * **Example** (Zipping with a combining function) + * + * ```ts + * import { Iterable } from "effect" + * + * // Add corresponding elements + * const a = [1, 2, 3, 4] + * const b = [10, 20, 30, 40] + * const sums = Iterable.zipWith(a, b, (x, y) => x + y) + * console.log(Array.from(sums)) // [11, 22, 33, 44] + * + * // Combine strings + * const firstNames = ["John", "Jane", "Bob"] + * const lastNames = ["Doe", "Smith", "Johnson"] + * const fullNames = Iterable.zipWith( + * firstNames, + * lastNames, + * (first, last) => `${first} ${last}` + * ) + * console.log(Array.from(fullNames)) // ["John Doe", "Jane Smith", "Bob Johnson"] + * + * // Different lengths - stops at shorter + * const short = [1, 2] + * const long = ["a", "b", "c", "d"] + * const combined = Iterable.zipWith( + * short, + * long, + * (num, letter) => `${num}${letter}` + * ) + * console.log(Array.from(combined)) // ["1a", "2b"] + * + * // Complex transformations + * const prices = [10.99, 25.50, 5.00] + * const quantities = [2, 1, 3] + * const totals = Iterable.zipWith(prices, quantities, (price, qty) => { + * return Math.round(price * qty * 100) / 100 // round to 2 decimal places + * }) + * console.log(Array.from(totals)) // [21.98, 25.5, 15] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Iterable + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable +} = dual(3, (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable => ({ + [Symbol.iterator]() { + const selfIterator = self[Symbol.iterator]() + const thatIterator = that[Symbol.iterator]() + return { + next() { + const selfResult = selfIterator.next() + const thatResult = thatIterator.next() + if (selfResult.done || thatResult.done) { + return { done: true, value: undefined } + } + return { done: false, value: f(selfResult.value, thatResult.value) } + } + } + } +})) + +/** + * Places a separator between members of an `Iterable`. + * + * **When to use** + * + * Use to lazily insert a separator between adjacent values. + * + * **Details** + * + * If the input is a non-empty array, the result is also a non-empty array. + * + * **Example** (Interspersing separators) + * + * ```ts + * import { Iterable } from "effect" + * + * // Join numbers with separator + * const numbers = [1, 2, 3, 4] + * const withCommas = Iterable.intersperse(numbers, ",") + * console.log(Array.from(withCommas)) // [1, ",", 2, ",", 3, ",", 4] + * + * // Join words with spaces + * const words = ["hello", "world", "from", "effect"] + * const sentence = Iterable.intersperse(words, " ") + * console.log(Array.from(sentence).join("")) // "hello world from effect" + * + * // Empty iterable remains empty + * const empty = Iterable.empty() + * const stillEmpty = Iterable.intersperse(empty, "-") + * console.log(Array.from(stillEmpty)) // [] + * + * // Single element has no separators added + * const single = [42] + * const noSeparator = Iterable.intersperse(single, "|") + * console.log(Array.from(noSeparator)) // [42] + * + * // Build CSS-like strings + * const styles = ["color: red", "font-size: 14px", "margin: 10px"] + * const css = Iterable.intersperse(styles, "; ") + * console.log(Array.from(css).join("")) // "color: red; font-size: 14px; margin: 10px" + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const intersperse: { + (middle: B): (self: Iterable) => Iterable + (self: Iterable, middle: B): Iterable +} = dual(2, (self: Iterable, middle: B): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let next = iterator.next() + let emitted = false + return { + next() { + if (next.done) { + return next + } else if (emitted) { + emitted = false + return { done: false, value: middle } + } + emitted = true + const result = next + next = iterator.next() + return result + } + } + } +})) + +/** + * Returns a function that checks if an `Iterable` contains a given value using a provided `isEquivalent` function. + * + * **Example** (Checking membership with custom equivalence) + * + * ```ts + * import { Iterable } from "effect" + * + * // Custom equivalence for objects + * const byId = (a: { id: number }, b: { id: number }) => a.id === b.id + * const containsById = Iterable.containsWith(byId) + * + * const users = [{ id: 1 }, { id: 2 }] + * const hasUser1 = containsById(users, { id: 1 }) + * console.log(hasUser1) // true (same id) + * + * // Case-insensitive string comparison + * const caseInsensitive = (a: string, b: string) => + * a.toLowerCase() === b.toLowerCase() + * const containsCaseInsensitive = Iterable.containsWith(caseInsensitive) + * + * const words = ["Hello", "World"] + * const hasHello = containsCaseInsensitive(words, "hello") + * console.log(hasHello) // true + * + * // Approximate number comparison + * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.1 + * const containsApprox = Iterable.containsWith(approxEqual) + * + * const values = [1.0, 2.0, 3.0] + * const hasAlmostTwo = containsApprox(values, 2.05) + * console.log(hasAlmostTwo) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} => + dual(2, (self: Iterable, a: A): boolean => { + for (const i of self) { + if (isEquivalent(a, i)) { + return true + } + } + return false + }) + +/** + * Checks whether an iterable contains a value using Effect's default `Equal` + * equivalence. + * + * **Details** + * + * Can be called as `contains(self, value)` or curried as + * `contains(value)(self)`. + * + * **Example** (Checking membership) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3, 4, 5] + * console.log(Iterable.contains(numbers, 3)) // true + * console.log(Iterable.contains(numbers, 6)) // false + * + * const letters = "hello" + * console.log(Iterable.contains(letters, "l")) // true + * console.log(Iterable.contains(letters, "x")) // false + * + * // Works with any iterable + * const range = Iterable.range(1, 100) + * console.log(Iterable.contains(range, 50)) // true + * console.log(Iterable.contains(range, 150)) // false + * + * // Curried version + * const containsThree = Iterable.contains(3) + * console.log(containsThree([1, 2, 3])) // true + * console.log(containsThree([4, 5, 6])) // false + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Iterable) => boolean + (self: Iterable, a: A): boolean +} = containsWith(Equal.asEquivalence()) + +/** + * Splits an `Iterable` into length-`n` pieces. The last piece will be shorter if `n` does not evenly divide the length of + * the `Iterable`. + * + * **Example** (Chunking an iterable) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] + * const chunks = Iterable.chunksOf(numbers, 3) + * console.log(Array.from(chunks).map((chunk) => Array.from(chunk))) + * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * + * // Last chunk can be shorter + * const uneven = [1, 2, 3, 4, 5, 6, 7] + * const chunks2 = Iterable.chunksOf(uneven, 3) + * console.log(Array.from(chunks2).map((chunk) => Array.from(chunk))) + * // [[1, 2, 3], [4, 5, 6], [7]] + * + * // Chunk size larger than iterable + * const small = [1, 2] + * const chunks3 = Iterable.chunksOf(small, 5) + * console.log(Array.from(chunks3).map((chunk) => Array.from(chunk))) + * // [[1, 2]] + * + * // Process data in batches + * const data = Iterable.range(1, 100) + * const batches = Iterable.chunksOf(data, 10) + * const batchSums = Iterable.map( + * batches, + * (batch) => Iterable.reduce(batch, 0, (sum, n) => sum + n) + * ) + * console.log(Array.from(Iterable.take(batchSums, 3))) // [55, 155, 255] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const chunksOf: { + (n: number): (self: Iterable) => Iterable> + (self: Iterable, n: number): Iterable> +} = dual(2, (self: Iterable, n: number): Iterable> => { + const safeN = Math.max(1, Math.floor(n)) + return ({ + [Symbol.iterator]() { + let iterator: Iterator | undefined = self[Symbol.iterator]() + return { + next() { + if (iterator === undefined) { + return { done: true, value: undefined } + } + + const chunk: Array = [] + for (let i = 0; i < safeN; i++) { + const result = iterator.next() + if (result.done) { + iterator = undefined + return chunk.length === 0 ? { done: true, value: undefined } : { done: false, value: chunk } + } + chunk.push(result.value) + } + + return { done: false, value: chunk } + } + } + } + }) +}) + +/** + * Groups equal, consecutive elements of an `Iterable` into `NonEmptyArray`s using the provided `isEquivalent` function. + * + * **Example** (Grouping consecutive elements with custom equivalence) + * + * ```ts + * import { Iterable } from "effect" + * + * // Group consecutive equal numbers + * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] + * const grouped = Iterable.groupWith(numbers, (a, b) => a === b) + * console.log(Array.from(grouped)) + * // [[1, 1], [2, 2, 2], [3], [1, 1]] + * + * // Case-insensitive grouping of strings + * const words = ["Apple", "APPLE", "banana", "Banana", "cherry"] + * const caseInsensitive = (a: string, b: string) => + * a.toLowerCase() === b.toLowerCase() + * const groupedWords = Iterable.groupWith(words, caseInsensitive) + * console.log(Array.from(groupedWords)) + * // [["Apple", "APPLE"], ["banana", "Banana"], ["cherry"]] + * + * // Group by approximate equality + * const floats = [1.1, 1.12, 1.9, 2.01, 2.05, 3.5] + * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.2 + * const groupedFloats = Iterable.groupWith(floats, approxEqual) + * console.log(Array.from(groupedFloats)) + * // [[1.1, 1.12], [1.9, 2.01, 2.05], [3.5]] + * + * // Only groups consecutive elements + * const scattered = [1, 2, 1, 2, 1] + * const scatteredGroups = Iterable.groupWith(scattered, (a, b) => a === b) + * console.log(Array.from(scatteredGroups)) + * // [[1], [2], [1], [2], [1]] (no grouping since none are consecutive) + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Iterable> + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable> +} = dual( + 2, + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable> => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let nextResult: IteratorResult | undefined + return { + next() { + let result: IteratorResult + if (nextResult !== undefined) { + if (nextResult.done) { + return { done: true, value: undefined } + } + result = nextResult + nextResult = undefined + } else { + result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + } + const chunk: NonEmptyArray = [result.value] + + while (true) { + const next = iterator.next() + if (next.done || !isEquivalent(result.value, next.value)) { + nextResult = next + return { done: false, value: chunk } + } + chunk.push(next.value) + } + } + } + } + }) +) + +/** + * Groups equal, consecutive elements of an `Iterable` into `NonEmptyArray`s. + * + * **Example** (Grouping consecutive elements) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] + * const grouped = Iterable.group(numbers) + * console.log(Array.from(grouped)) + * // [[1, 1], [2, 2, 2], [3], [1, 1]] + * + * const letters = "aabbccaa" + * const groupedLetters = Iterable.group(letters) + * console.log(Array.from(groupedLetters)) + * // [["a", "a"], ["b", "b"], ["c", "c"], ["a", "a"]] + * + * // Works with objects using deep equality + * const objects = [ + * { type: "A", value: 1 }, + * { type: "A", value: 1 }, + * { type: "B", value: 2 }, + * { type: "A", value: 1 } + * ] + * const groupedObjects = Iterable.group(objects) + * console.log(Array.from(groupedObjects).length) // 3 groups + * // Note: Only consecutive equal objects are grouped together + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const group: (self: Iterable) => Iterable> = groupWith( + Equal.asEquivalence() +) + +/** + * Groups all elements by the string or symbol key returned by `f`. + * + * **Details** + * + * Each property in the returned record contains a non-empty array of elements + * that produced that key. Unlike `group`, matching elements do not need to be + * consecutive. + * + * **Example** (Grouping by a key) + * + * ```ts + * import { Iterable } from "effect" + * + * // Group by string length + * const words = ["a", "bb", "ccc", "dd", "eee", "f"] + * const byLength = Iterable.groupBy(words, (word) => word.length.toString()) + * console.log(byLength) + * // { "1": ["a", "f"], "2": ["bb", "dd"], "3": ["ccc", "eee"] } + * + * // Group by first letter + * const names = ["Alice", "Bob", "Charlie", "David", "Anna", "Betty"] + * const byFirstLetter = Iterable.groupBy(names, (name) => name[0]) + * console.log(byFirstLetter) + * // { "A": ["Alice", "Anna"], "B": ["Bob", "Betty"], "C": ["Charlie"], "D": ["David"] } + * + * // Group by category + * const items = [ + * { name: "apple", category: "fruit" }, + * { name: "carrot", category: "vegetable" }, + * { name: "banana", category: "fruit" }, + * { name: "broccoli", category: "vegetable" } + * ] + * const byCategory = Iterable.groupBy(items, (item) => item.category) + * console.log(byCategory) + * // { + * // "fruit": [{ name: "apple", category: "fruit" }, { name: "banana", category: "fruit" }], + * // "vegetable": [{ name: "carrot", category: "vegetable" }, { name: "broccoli", category: "vegetable" }] + * // } + * + * // Group numbers by even/odd + * const numbers = [1, 2, 3, 4, 5, 6] + * const evenOdd = Iterable.groupBy(numbers, (n) => n % 2 === 0 ? "even" : "odd") + * console.log(evenOdd) + * // { "odd": [1, 3, 5], "even": [2, 4, 6] } + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupBy: { + ( + f: (a: A) => K + ): (self: Iterable) => Record, NonEmptyArray> + ( + self: Iterable, + f: (a: A) => K + ): Record, NonEmptyArray> +} = dual(2, ( + self: Iterable, + f: (a: A) => K +): Record, NonEmptyArray> => { + const out: Record> = {} + for (const a of self) { + const k = f(a) + if (Object.hasOwn(out, k)) { + out[k].push(a) + } else { + out[k] = [a] + } + } + return out +}) + +const constEmpty: Iterable = { + [Symbol.iterator]() { + return constEmptyIterator + } +} +const constEmptyIterator: Iterator = { + next() { + return { done: true, value: undefined } + } +} + +/** + * Creates an empty iterable that yields no elements. + * + * **When to use** + * + * Use when you need an empty iterable as a typed "no data" value or a base + * case for iterable operations. + * + * **Example** (Creating an empty iterable) + * + * ```ts + * import { Iterable } from "effect" + * + * const empty = Iterable.empty() + * console.log(Array.from(empty)) // [] + * console.log(Iterable.isEmpty(empty)) // true + * + * // Useful as base case for reductions + * const hasData = true + * const result = hasData + * ? Iterable.range(1, 5) + * : Iterable.empty() + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Iterable => constEmpty + +/** + * Creates an iterable containing a single element. + * + * **When to use** + * + * Use to wrap a single value in an iterable context so it can be combined + * with other iterable operations. + * + * **Example** (Wrapping a single value) + * + * ```ts + * import { Iterable } from "effect" + * + * const single = Iterable.of(42) + * console.log(Array.from(single)) // [42] + * + * // Useful for creating homogeneous sequences + * const sequences = [ + * Iterable.of("hello"), + * Iterable.range(1, 3), + * Iterable.empty() + * ] + * + * // Can be used with flatMap for conditional inclusion + * const numbers = [1, 2, 3, 4, 5] + * const evensOnly = Iterable.flatMap( + * numbers, + * (n) => n % 2 === 0 ? Iterable.of(n) : Iterable.empty() + * ) + * console.log(Array.from(evensOnly)) // [2, 4] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const of = (a: A): Iterable => [a] + +/** + * Transforms each element of an iterable using a function. + * + * **Details** + * + * This is one of the most fundamental operations for working with iterables. + * It applies a transformation function to each element, creating a new iterable + * with the transformed values. The operation is lazy, so elements are only + * transformed when the iterable is consumed. + * + * **Example** (Mapping elements) + * + * ```ts + * import { Iterable } from "effect" + * + * // Transform numbers to their squares + * const numbers = [1, 2, 3, 4, 5] + * const squares = Iterable.map(numbers, (x) => x * x) + * console.log(Array.from(squares)) // [1, 4, 9, 16, 25] + * + * // Use index in transformation + * const indexed = Iterable.map(["a", "b", "c"], (char, i) => `${i}: ${char}`) + * console.log(Array.from(indexed)) // ["0: a", "1: b", "2: c"] + * + * // Chain transformations + * const result = Iterable.map( + * Iterable.map([1, 2, 3], (x) => x * 2), + * (x) => x + 1 + * ) + * console.log(Array.from(result)) // [3, 5, 7] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + ( + f: (a: NoInfer, i: number) => B + ): (self: Iterable) => Iterable + (self: Iterable, f: (a: NoInfer, i: number) => B): Iterable +} = dual(2, (self: Iterable, f: (a: A, i: number) => B): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + return { done: false, value: f(result.value, i++) } + } + } + } +})) + +/** + * Applies a function to each element in an Iterable and returns a new Iterable containing the concatenated mapped elements. + * + * **Example** (Flat mapping iterables) + * + * ```ts + * import { Iterable } from "effect" + * + * // Expand each number to a range + * const numbers = [1, 2, 3] + * const expanded = Iterable.flatMap(numbers, (n) => Iterable.range(1, n)) + * console.log(Array.from(expanded)) // [1, 1, 2, 1, 2, 3] + * + * // Split strings into characters + * const words = ["hi", "bye"] + * const chars = Iterable.flatMap(words, (word) => word) + * console.log(Array.from(chars)) // ["h", "i", "b", "y", "e"] + * + * // Conditional expansion with empty iterables + * const values = [1, 2, 3, 4, 5] + * const evenMultiples = Iterable.flatMap( + * values, + * (n) => n % 2 === 0 ? [n, n * 2, n * 3] : [] + * ) + * console.log(Array.from(evenMultiples)) // [2, 4, 6, 4, 8, 12] + * + * // Use index in transformation + * const letters = ["a", "b", "c"] + * const indexed = Iterable.flatMap( + * letters, + * (letter, i) => Iterable.replicate(letter, i + 1) + * ) + * console.log(Array.from(indexed)) // ["a", "b", "b", "c", "c", "c"] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (a: NoInfer, i: number) => Iterable + ): (self: Iterable) => Iterable + (self: Iterable, f: (a: NoInfer, i: number) => Iterable): Iterable +} = dual( + 2, + (self: Iterable, f: (a: A, i: number) => Iterable): Iterable => flatten(map(self, f)) +) + +/** + * Flattens an Iterable of Iterables into a single Iterable + * + * **Example** (Flattening nested iterables) + * + * ```ts + * import { Iterable } from "effect" + * + * // Flatten nested arrays + * const nested = [[1, 2], [3, 4], [5, 6]] + * const flat = Iterable.flatten(nested) + * console.log(Array.from(flat)) // [1, 2, 3, 4, 5, 6] + * + * // Flatten different iterable types + * const mixed: Array> = ["ab", "cd"] + * const flatMixed = Iterable.flatten(mixed) + * console.log(Array.from(flatMixed)) // ["a", "b", "c", "d"] + * + * // Flatten deeply nested (only one level) + * const deepNested = [[[1, 2]], [[3, 4]]] + * const oneLevelFlat = Iterable.flatten(deepNested) + * console.log(Array.from(oneLevelFlat).map((arr) => Array.from(arr))) + * // [[1, 2], [3, 4]] (still contains arrays) + * + * // Empty iterables are handled correctly + * const withEmpty = [[1, 2], [], [3, 4], []] + * const flatWithEmpty = Iterable.flatten(withEmpty) + * console.log(Array.from(flatWithEmpty)) // [1, 2, 3, 4] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten = (self: Iterable>): Iterable => ({ + [Symbol.iterator]() { + const outerIterator = self[Symbol.iterator]() + let innerIterator: Iterator | undefined + function next() { + if (innerIterator === undefined) { + const next = outerIterator.next() + if (next.done) { + return next + } + innerIterator = next.value[Symbol.iterator]() + } + const result = innerIterator.next() + if (result.done) { + innerIterator = undefined + return next() + } + return result + } + return { next } + } +}) + +/** + * Transforms elements of an iterable using a function that returns a `Result`, keeping only successful values. + * + * **Details** + * + * This combines mapping and filtering in a single operation. The function is + * applied to each element, and only elements that result in `Result.succeed` + * are included in the result. + * + * **Example** (Filtering and transforming Result values) + * + * ```ts + * import { Iterable, Result } from "effect" + * + * // Parse strings to numbers, keeping only valid ones + * const strings = ["1", "2", "invalid", "4", "not-a-number"] + * const numbers = Iterable.filterMap(strings, (s) => { + * const num = parseInt(s) + * return isNaN(num) ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Array.from(numbers)) // [1, 2, 4] + * + * // Extract specific properties from objects + * const users = [ + * { name: "Alice", age: 25, email: "alice@example.com" }, + * { name: "Bob", age: 17, email: undefined }, + * { name: "Charlie", age: 30, email: "charlie@example.com" }, + * { name: "David", age: 16, email: undefined } + * ] + * const adultEmails = Iterable.filterMap( + * users, + * (user) => + * user.age >= 18 && user.email ? Result.succeed(user.email) : Result.failVoid + * ) + * console.log(Array.from(adultEmails)) // ["alice@example.com", "charlie@example.com"] + * + * // Use index in transformation + * const items = ["a", "b", "c", "d", "e"] + * const evenIndexItems = Iterable.filterMap( + * items, + * (item, i) => i % 2 === 0 ? Result.succeed(`${i}: ${item}`) : Result.failVoid + * ) + * console.log(Array.from(evenIndexItems)) // ["0: a", "2: c", "4: e"] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (input: A, i: number) => Result): (self: Iterable) => Iterable + (self: Iterable, f: (input: A, i: number) => Result): Iterable +} = dual( + 2, + (self: Iterable, f: (input: A, i: number) => Result): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + let result = iterator.next() + while (!result.done) { + const next = f(result.value, i++) + if (R.isSuccess(next)) { + return { done: false, value: next.success } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + }) +) + +/** + * Transforms all elements of the `Iterable` for as long as the specified function succeeds. + * + * **Example** (Filtering and transforming until failure) + * + * ```ts + * import { Iterable, Result } from "effect" + * + * // Parse numbers until we hit an invalid one + * const strings = ["1", "2", "3", "invalid", "4", "5"] + * const numbers = Iterable.filterMapWhile(strings, (s) => { + * const num = parseInt(s) + * return isNaN(num) ? Result.failVoid : Result.succeed(num) + * }) + * console.log(Array.from(numbers)) // [1, 2, 3] (stops at "invalid") + * + * // Take elements while they meet a condition and transform them + * const values = [2, 4, 6, 7, 8, 10] + * const doubledEvens = Iterable.filterMapWhile( + * values, + * (n) => n % 2 === 0 ? Result.succeed(n * 2) : Result.failVoid + * ) + * console.log(Array.from(doubledEvens)) // [4, 8, 12] (stops at 7) + * + * // Process with index until condition fails + * const letters = ["a", "b", "c", "d", "e"] + * const indexedUntilC = Iterable.filterMapWhile( + * letters, + * (letter, i) => letter !== "c" ? Result.succeed(`${i}: ${letter}`) : Result.failVoid + * ) + * console.log(Array.from(indexedUntilC)) // ["0: a", "1: b"] (stops at "c") + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMapWhile: { + (f: (input: A, i: number) => Result): (self: Iterable) => Iterable + (self: Iterable, f: (input: A, i: number) => Result): Iterable +} = dual(2, (self: Iterable, f: (input: A, i: number) => Result) => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + const next = f(result.value, i++) + if (R.isSuccess(next)) { + return { done: false, value: next.success } + } + return { done: true, value: undefined } + } + } + } +})) + +/** + * Retrieves the `Some` values from an `Iterable` of `Option`s. + * + * **Example** (Extracting Some values) + * + * ```ts + * import { Iterable, Option } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Array.from( + * Iterable.getSomes([Option.some(1), Option.none(), Option.some(2)]) + * ), + * [1, 2] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getSomes = (self: Iterable>): Iterable => { + return { + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + return { + next() { + let result = iterator.next() + while (!result.done) { + if (O.isSome(result.value)) { + return { done: false, value: result.value.value } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + } +} + +/** + * Returns a lazy iterable containing the failure values from an iterable of + * `Result`s, skipping successful results. + * + * **Example** (Extracting failures) + * + * ```ts + * import { Iterable, Result } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Array.from( + * Iterable.getFailures([ + * Result.succeed(1), + * Result.fail("err"), + * Result.succeed(2) + * ]) + * ), + * ["err"] + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const getFailures = (self: Iterable>): Iterable => { + return { + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + return { + next() { + let result = iterator.next() + while (!result.done) { + if (R.isFailure(result.value)) { + return { done: false, value: result.value.failure } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + } +} + +/** + * Returns a lazy iterable containing the success values from an iterable of + * `Result`s, skipping failed results. + * + * **Example** (Extracting successes) + * + * ```ts + * import { Iterable, Result } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Array.from( + * Iterable.getSuccesses([ + * Result.succeed(1), + * Result.fail("err"), + * Result.succeed(2) + * ]) + * ), + * [1, 2] + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const getSuccesses = (self: Iterable>): Iterable => { + return { + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + return { + next() { + let result = iterator.next() + while (!result.done) { + if (R.isSuccess(result.value)) { + return { done: false, value: result.value.success } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + } +} + +/** + * Filters an iterable to only include elements that match a predicate. + * + * **Details** + * + * This function creates a new iterable containing only the elements for which + * the predicate function returns true. Like map, this operation is lazy and + * elements are only tested when the iterable is consumed. + * + * **Example** (Filtering elements) + * + * ```ts + * import { Iterable } from "effect" + * + * // Filter even numbers + * const numbers = [1, 2, 3, 4, 5, 6] + * const evens = Iterable.filter(numbers, (x) => x % 2 === 0) + * console.log(Array.from(evens)) // [2, 4, 6] + * + * // Filter with index + * const items = ["a", "b", "c", "d"] + * const oddPositions = Iterable.filter(items, (_, i) => i % 2 === 1) + * console.log(Array.from(oddPositions)) // ["b", "d"] + * + * // Type refinement + * const mixed: Array = ["hello", 42, "world", 100] + * const onlyStrings = Iterable.filter( + * mixed, + * (x): x is string => typeof x === "string" + * ) + * console.log(Array.from(onlyStrings)) // ["hello", "world"] (typed as string[]) + * + * // Combine with map + * const processed = Iterable.map( + * Iterable.filter([1, 2, 3, 4, 5], (x) => x > 2), + * (x) => x * 10 + * ) + * console.log(Array.from(processed)) // [30, 40, 50] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: (a: NoInfer, i: number) => a is B): (self: Iterable) => Iterable + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => Iterable + (self: Iterable, refinement: (a: A, i: number) => a is B): Iterable + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let i = 0 + return { + next() { + let result = iterator.next() + while (!result.done) { + if (predicate(result.value, i++)) { + return { done: false, value: result.value } + } + result = iterator.next() + } + return { done: true, value: undefined } + } + } + } + }) +) + +/** + * Transforms elements using a function that may return null or undefined, filtering out the null/undefined results. + * + * **When to use** + * + * Use when working with APIs or functions that return nullable values, + * providing a clean way to filter out null or undefined while transforming. + * + * **Example** (Flat mapping nullable results) + * + * ```ts + * import { Iterable } from "effect" + * + * // Extract valid elements from nullable function results + * const data = ["1", "2", "invalid", "4"] + * const parsed = Iterable.flatMapNullishOr(data, (s) => { + * const num = parseInt(s) + * return isNaN(num) ? null : num * 2 + * }) + * console.log(Array.from(parsed)) // [2, 4, 8] + * + * // Safe property access + * const objects = [ + * { nested: { value: 10 } }, + * { nested: null }, + * { nested: { value: 20 } }, + * {} + * ] + * const values = Iterable.flatMapNullishOr(objects, (obj) => obj.nested?.value) + * console.log(Array.from(values)) // [10, 20] + * + * // Working with Map.get (returns undefined for missing keys) + * const map = new Map([ + * ["a", 1], + * ["b", 2], + * ["c", 3] + * ]) + * const keys = ["a", "x", "b", "y", "c"] + * const foundValues = Iterable.flatMapNullishOr(keys, (key) => map.get(key)) + * console.log(Array.from(foundValues)) // [1, 2, 3] + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const flatMapNullishOr: { + (f: (a: A) => B): (self: Iterable) => Iterable> + (self: Iterable, f: (a: A) => B): Iterable> +} = dual( + 2, + (self: Iterable, f: (a: A) => B): Iterable> => + filterMap(self, (a) => { + const b = f(a) + return b == null ? R.failVoid : R.succeed(b) + }) +) + +/** + * Checks whether a predicate holds true for some `Iterable` element. + * + * **Example** (Checking whether some element matches) + * + * ```ts + * import { Iterable } from "effect" + * + * const numbers = [1, 3, 5, 7, 8] + * const hasEven = Iterable.some(numbers, (x) => x % 2 === 0) + * console.log(hasEven) // true (because of 8) + * + * const allOdd = [1, 3, 5, 7] + * const hasEvenInAllOdd = Iterable.some(allOdd, (x) => x % 2 === 0) + * console.log(hasEvenInAllOdd) // false + * + * // With index + * const letters = ["a", "b", "c"] + * const hasElementAtIndex2 = Iterable.some(letters, (_, i) => i === 2) + * console.log(hasElementAtIndex2) // true + * + * // Early termination - stops at first match + * const infiniteOdds = Iterable.filter(Iterable.range(1), (x) => x % 2 === 1) + * const hasEvenInInfiniteOdds = Iterable.some( + * Iterable.take(infiniteOdds, 1000), + * (x) => x % 2 === 0 + * ) + * console.log(hasEvenInInfiniteOdds) // false (quickly, doesn't check all 1000) + * + * // Type guard usage + * const mixed: Array = [1, 2, "hello"] + * const hasString = Iterable.some( + * mixed, + * (x): x is string => typeof x === "string" + * ) + * console.log(hasString) // true + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const some: { + (predicate: (a: A, i: number) => boolean): (self: Iterable) => boolean + (self: Iterable, predicate: (a: A, i: number) => boolean): boolean +} = dual( + 2, + (self: Iterable, predicate: (a: A, i: number) => boolean): boolean => { + let i = 0 + for (const a of self) { + if (predicate(a, i++)) { + return true + } + } + return false + } +) + +/** + * Generates an iterable by repeatedly applying a function that produces the + * next element and state. + * + * **Details** + * + * This is useful for creating iterables from a generating function that + * maintains state. The function should return `Option.some([value, nextState])` + * to continue or `Option.none()` to stop. + * + * **Example** (Unfolding state into values) + * + * ```ts + * import { Iterable, Option } from "effect" + * + * // Generate Fibonacci sequence + * const fibonacci = Iterable.unfold([0, 1], ([a, b]) => Option.some([a, [b, a + b]])) + * const first10Fib = Iterable.take(fibonacci, 10) + * console.log(Array.from(first10Fib)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] + * + * // Generate powers of 2 up to a limit + * const powersOf2 = Iterable.unfold(1, (n) => n <= 1000 ? Option.some([n, n * 2]) : Option.none()) + * console.log(Array.from(powersOf2)) // [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + * + * // Generate countdown + * const countdown = Iterable.unfold(5, (n) => n > 0 ? Option.some([n, n - 1]) : Option.none()) + * console.log(Array.from(countdown)) // [5, 4, 3, 2, 1] + * + * // Generate collatz sequence + * const collatz = Iterable.unfold(7, (n) => { + * if (n === 1) return Option.none() + * const next = n % 2 === 0 ? n / 2 : n * 3 + 1 + * return Option.some([n, next]) + * }) + * console.log(Array.from(collatz)) // [7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unfold = (b: B, f: (b: B) => Option): Iterable => ({ + [Symbol.iterator]() { + let next = b + return { + next() { + const ab = f(next) + if (O.isNone(ab)) { + return { done: true, value: undefined } + } + const [a, b] = ab.value + next = b + return { done: false, value: a } + } + } + } +}) + +/** + * Iterates over the `Iterable`, applying `f` to each element. + * + * **Example** (Iterating with side effects) + * + * ```ts + * import { Iterable } from "effect" + * + * // Print each element + * const numbers = [1, 2, 3, 4, 5] + * Iterable.forEach(numbers, (n) => console.log(n)) + * // Prints: 1, 2, 3, 4, 5 + * + * // Use index in the callback + * const letters = ["a", "b", "c"] + * Iterable.forEach(letters, (letter, i) => { + * console.log(`${i}: ${letter}`) + * }) + * // Prints: "0: a", "1: b", "2: c" + * + * // Side effects with any iterable + * const results: Array = [] + * Iterable.forEach(Iterable.range(1, 5), (n) => { + * results.push(n * n) + * }) + * console.log(results) // [1, 4, 9, 16, 25] + * + * // Process in chunks + * const data = Iterable.chunksOf([1, 2, 3, 4, 5, 6], 2) + * Iterable.forEach(data, (chunk) => { + * console.log(`Processing chunk: ${Array.from(chunk)}`) + * }) + * // Prints: "Processing chunk: 1,2", "Processing chunk: 3,4", "Processing chunk: 5,6" + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const forEach: { + (f: (a: A, i: number) => void): (self: Iterable) => void + (self: Iterable, f: (a: A, i: number) => void): void +} = dual(2, (self: Iterable, f: (a: A, i: number) => void): void => { + let i = 0 + for (const a of self) { + f(a, i++) + } +}) + +/** + * Reduces an iterable to a single value by applying a function to each element and accumulating the result. + * + * **Details** + * + * This function applies a reducing function against an accumulator and each element + * of the iterable (from left to right) to reduce it to a single value. + * + * **Example** (Reducing an iterable) + * + * ```ts + * import { Iterable } from "effect" + * + * // Sum all numbers + * const numbers = [1, 2, 3, 4, 5] + * const sum = Iterable.reduce(numbers, 0, (acc, n) => acc + n) + * console.log(sum) // 15 + * + * // Find maximum value + * const values = [3, 1, 4, 1, 5, 9, 2] + * const max = Iterable.reduce(values, -Infinity, Math.max) + * console.log(max) // 9 + * + * // Build an object from key-value pairs + * const pairs = [["a", 1], ["b", 2], ["c", 3]] as const + * const obj = Iterable.reduce( + * pairs, + * {} as Record, + * (acc, [key, value]) => { + * acc[key] = value + * return acc + * } + * ) + * console.log(obj) // { a: 1, b: 2, c: 3 } + * + * // Use index in the reducer + * const letters = ["a", "b", "c"] + * const indexed = Iterable.reduce( + * letters, + * [] as Array, + * (acc, letter, i) => { + * acc.push(`${i}: ${letter}`) + * return acc + * } + * ) + * console.log(indexed) // ["0: a", "1: b", "2: c"] + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (b: B, f: (b: B, a: A, i: number) => B): (self: Iterable) => B + (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B +} = dual(3, (self: Iterable, b: B, f: (b: B, a: A, i: number) => B): B => { + if (Array.isArray(self)) { + return self.reduce(f, b) + } + let i = 0 + let result = b + for (const n of self) { + result = f(result, n, i++) + } + return result +}) + +/** + * Deduplicates adjacent elements that are identical using the provided `isEquivalent` function. + * + * **Example** (Deduplicating adjacent elements with custom equivalence) + * + * ```ts + * import { Iterable } from "effect" + * + * // Remove adjacent duplicates with custom equality + * const numbers = [1, 1, 2, 2, 3, 1, 1] + * const dedupedNumbers = Iterable.dedupeAdjacentWith(numbers, (a, b) => a === b) + * console.log(Array.from(dedupedNumbers)) // [1, 2, 3, 1] + * + * // Case-insensitive deduplication + * const words = ["Hello", "HELLO", "world", "World", "test"] + * const caseInsensitive = (a: string, b: string) => + * a.toLowerCase() === b.toLowerCase() + * const dedupedWords = Iterable.dedupeAdjacentWith(words, caseInsensitive) + * console.log(Array.from(dedupedWords)) // ["Hello", "world", "test"] + * + * // Deduplication by object property + * const users = [ + * { id: 1, name: "Alice" }, + * { id: 1, name: "Alice Updated" }, // different name, same id + * { id: 2, name: "Bob" }, + * { id: 2, name: "Bob" }, + * { id: 3, name: "Charlie" } + * ] + * const byId = (a: typeof users[0], b: typeof users[0]) => a.id === b.id + * const dedupedUsers = Iterable.dedupeAdjacentWith(users, byId) + * console.log(Array.from(dedupedUsers).map((u) => u.id)) // [1, 2, 3] + * + * // Approximate numeric equality + * const floats = [1.0, 1.01, 1.02, 2.0, 2.01, 3.0] + * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.1 + * const dedupedFloats = Iterable.dedupeAdjacentWith(floats, approxEqual) + * console.log(Array.from(dedupedFloats)) // [1.0, 2.0, 3.0] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dedupeAdjacentWith: { + (isEquivalent: (self: A, that: A) => boolean): (self: Iterable) => Iterable + (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable +} = dual(2, (self: Iterable, isEquivalent: (self: A, that: A) => boolean): Iterable => ({ + [Symbol.iterator]() { + const iterator = self[Symbol.iterator]() + let first = true + let last: A + function next(): IteratorResult { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + if (first) { + first = false + last = result.value + return result + } + const current = result.value + if (isEquivalent(last, current)) { + return next() + } + last = current + return result + } + return { next } + } +})) + +/** + * Deduplicates adjacent elements that are identical. + * + * **Example** (Deduplicating adjacent elements) + * + * ```ts + * import { Iterable } from "effect" + * + * // Remove adjacent duplicate numbers + * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] + * const deduped = Iterable.dedupeAdjacent(numbers) + * console.log(Array.from(deduped)) // [1, 2, 3, 1] + * + * // Remove adjacent duplicate characters + * const letters = "aabbccaa" + * const dedupedLetters = Iterable.dedupeAdjacent(letters) + * console.log(Array.from(dedupedLetters)) // ["a", "b", "c", "a"] + * + * // Works with objects using deep equality + * const objects = [ + * { type: "A" }, + * { type: "A" }, + * { type: "B" }, + * { type: "B" }, + * { type: "A" } + * ] + * const dedupedObjects = Iterable.dedupeAdjacent(objects) + * console.log(Array.from(dedupedObjects).map((o) => o.type)) // ["A", "B", "A"] + * + * // Clean up streaming data + * const sensorData = [100, 100, 100, 101, 101, 102, 102, 102, 100] + * const cleanedData = Iterable.dedupeAdjacent(sensorData) + * console.log(Array.from(cleanedData)) // [100, 101, 102, 100] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dedupeAdjacent: (self: Iterable) => Iterable = dedupeAdjacentWith(Equal.asEquivalence()) + +/** + * Zips this Iterable crosswise with the specified Iterable using the specified combiner. + * + * **Example** (Combining cartesian products) + * + * ```ts + * import { Iterable } from "effect" + * + * // Create coordinate pairs + * const xs = [1, 2] + * const ys = ["a", "b", "c"] + * const coordinates = Iterable.cartesianWith(xs, ys, (x, y) => `(${x},${y})`) + * console.log(Array.from(coordinates)) // ["(1,a)", "(1,b)", "(1,c)", "(2,a)", "(2,b)", "(2,c)"] + * + * // Generate all combinations of options + * const sizes = ["S", "M", "L"] + * const colors = ["red", "blue"] + * const products = Iterable.cartesianWith( + * sizes, + * colors, + * (size, color) => ({ size, color }) + * ) + * console.log(Array.from(products)) + * // [ + * // { size: "S", color: "red" }, { size: "S", color: "blue" }, + * // { size: "M", color: "red" }, { size: "M", color: "blue" }, + * // { size: "L", color: "red" }, { size: "L", color: "blue" } + * // ] + * + * // Mathematical operations on all pairs + * const a = [1, 2, 3] + * const b = [10, 20] + * const mathProducts = Iterable.cartesianWith(a, b, (x, y) => x * y) + * console.log(Array.from(mathProducts)) // [10, 20, 20, 40, 30, 60] + * + * // Create test data combinations + * const userTypes = ["admin", "user"] + * const features = ["read", "write", "delete"] + * const testCases = Iterable.cartesianWith( + * userTypes, + * features, + * (user, feature) => `${user}_can_${feature}` + * ) + * console.log(Array.from(testCases)) + * // ["admin_can_read", "admin_can_write", "admin_can_delete", "user_can_read", "user_can_write", "user_can_delete"] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const cartesianWith: { + (that: Iterable, f: (a: A, b: B) => C): (self: Iterable) => Iterable + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable +} = dual( + 3, + (self: Iterable, that: Iterable, f: (a: A, b: B) => C): Iterable => ({ + [Symbol.iterator]() { + const cache: Array = [] + let iterator: Iterator | undefined + let done = false + const replay: Iterable = { + [Symbol.iterator]() { + let index = 0 + return { + next(): IteratorResult { + if (index < cache.length) { + return { done: false, value: cache[index++] } + } + if (done) { + return { done: true, value: undefined } + } + iterator ??= that[Symbol.iterator]() + const result = iterator.next() + if (result.done) { + done = true + return { done: true, value: undefined } + } + cache.push(result.value) + index++ + return result + } + } + } + } + return flatMap(self, (a) => map(replay, (b) => f(a, b)))[Symbol.iterator]() + } + }) +) + +/** + * Zips this Iterable crosswise with the specified Iterable. + * + * **Example** (Generating cartesian pairs) + * + * ```ts + * import { Iterable } from "effect" + * + * // All pairs of numbers and letters + * const numbers = [1, 2, 3] + * const letters = ["a", "b"] + * const pairs = Iterable.cartesian(numbers, letters) + * console.log(Array.from(pairs)) + * // [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]] + * + * // Generate coordinate grid + * const x = [0, 1, 2] + * const y = [0, 1] + * const grid = Iterable.cartesian(x, y) + * console.log(Array.from(grid)) + * // [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]] + * + * // All combinations for testing + * const browsers = ["chrome", "firefox"] + * const devices = ["desktop", "mobile", "tablet"] + * const testMatrix = Iterable.cartesian(browsers, devices) + * console.log(Array.from(testMatrix)) + * // [ + * // ["chrome", "desktop"], ["chrome", "mobile"], ["chrome", "tablet"], + * // ["firefox", "desktop"], ["firefox", "mobile"], ["firefox", "tablet"] + * // ] + * + * // Empty iterable results in empty cartesian product + * const empty = Iterable.empty() + * const withEmpty = Iterable.cartesian([1, 2], empty) + * console.log(Array.from(withEmpty)) // [] + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const cartesian: { + (that: Iterable): (self: Iterable) => Iterable<[A, B]> + (self: Iterable, that: Iterable): Iterable<[A, B]> +} = dual( + 2, + (self: Iterable, that: Iterable): Iterable<[A, B]> => cartesianWith(self, that, (a, b) => [a, b]) +) + +/** + * Computes how many elements of the iterable pass the given predicate. + * + * **Example** (Counting matching elements) + * + * ```ts + * import { Iterable } from "effect" + * + * const result = Iterable.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) + * console.log(result) // 2 + * ``` + * + * @category folding + * @since 3.16.0 + */ +export const countBy: { + (predicate: (a: NoInfer, i: number) => boolean): (self: Iterable) => number + (self: Iterable, predicate: (a: A, i: number) => boolean): number +} = dual( + 2, + ( + self: Iterable, + f: (a: A, i: number) => boolean + ): number => { + let count = 0 + let i = 0 + for (const a of self) { + if (f(a, i)) { + count++ + } + i++ + } + return count + } +) diff --git a/repos/effect/packages/effect/src/JsonPatch.ts b/repos/effect/packages/effect/src/JsonPatch.ts new file mode 100644 index 0000000..5e05cd7 --- /dev/null +++ b/repos/effect/packages/effect/src/JsonPatch.ts @@ -0,0 +1,469 @@ +/** + * The `JsonPatch` module computes and applies deterministic patch documents for + * JSON values. A patch is an ordered list of `add`, `remove`, and `replace` + * operations addressed by JSON Pointer paths. Use it to describe the structural + * difference between two JSON documents, serialize that difference, and replay + * it without mutating the original input. + * + * @since 4.0.0 + */ +import { format } from "./Formatter.ts" +import { escapeToken, unescapeToken } from "./JsonPointer.ts" +import * as Predicate from "./Predicate.ts" +import type * as Schema from "./Schema.ts" + +/** + * A single JSON Patch operation. + * + * **When to use** + * + * Use to manually construct patch operations, accept patch operations from + * callers, or type-check patch operation structures. + * + * **Details** + * + * Represents one transformation step in a JSON Patch document. This is a subset + * of RFC 6902, restricted to operations that can be applied deterministically + * without additional context. All fields are readonly, paths use JSON Pointer + * syntax, and the empty string `""` refers to the root document. Operations are + * discriminated by the `op` field, and the optional `description` field can be + * used for documentation. + * + * **Example** (Defining all operation types) + * + * ```ts + * import { JsonPatch } from "effect" + * + * const addOp: JsonPatch.JsonPatchOperation = { + * op: "add", + * path: "/users/-", + * value: { id: 1, name: "Alice" } + * } + * + * const removeOp: JsonPatch.JsonPatchOperation = { + * op: "remove", + * path: "/users/0" + * } + * + * const replaceOp: JsonPatch.JsonPatchOperation = { + * op: "replace", + * path: "/users/0/name", + * value: "Bob" + * } + * ``` + * + * @see {@link JsonPatch} for the array of operations forming a complete patch + * @see {@link get} to compute operations automatically from value differences + * @see {@link apply} to apply operations to transform documents + * @category models + * @since 4.0.0 + */ +export type JsonPatchOperation = + | { + readonly op: "add" + /** + * JSON Pointer to the target location. For arrays, the last token may be `-` + * to append. + * + * **When to use** + * + * Use to identify where the `add` operation inserts its value. + */ + readonly path: string + readonly value: Schema.Json + readonly description?: string + } + | { + readonly op: "remove" + /** + * JSON Pointer to the target location. + * + * **When to use** + * + * Use to identify which location the `remove` operation deletes. + */ + readonly path: string + readonly description?: string + } + | { + readonly op: "replace" + /** + * JSON Pointer to the target location. Use `""` to replace the root document. + * + * **When to use** + * + * Use to identify which location the `replace` operation overwrites. + */ + readonly path: string + readonly value: Schema.Json + readonly description?: string + } + +/** + * A JSON Patch document (an ordered list of operations). + * + * **When to use** + * + * Use to store, serialize, pass, or validate complete patch documents. + * + * **Details** + * + * Represents a complete transformation as a readonly sequence of immutable + * operations. Operations are applied sequentially from first to last, and later + * operations observe the document state produced by earlier operations. An empty + * array represents a no-op patch and returns the original document. + * + * **Example** (Defining a multi-operation patch) + * + * ```ts + * import { JsonPatch } from "effect" + * + * const patch: JsonPatch.JsonPatch = [ + * { op: "add", path: "/items/-", value: "apple" }, + * { op: "replace", path: "/count", value: 5 }, + * { op: "remove", path: "/oldField" } + * ] + * + * const result = JsonPatch.apply(patch, { count: 3, oldField: "value" }) + * // { count: 5, items: ["apple"] } + * ``` + * + * @see {@link JsonPatchOperation} for individual operation types + * @see {@link get} to generate patches from value differences + * @see {@link apply} to execute patches to transform documents + * @category models + * @since 4.0.0 + */ +export type JsonPatch = ReadonlyArray + +/** + * Computes a structural patch that transforms `oldValue` into `newValue`. + * + * **When to use** + * + * Use to compute a JSON Patch from before and after JSON documents, detect + * structural changes, or create deterministic update operations. + * + * **Details** + * + * Generates a structural diff between two JSON values, producing a patch that + * yields `newValue` when applied to `oldValue`. It returns an empty array when + * values are identical, recursively diffs nested structures, emits root + * `replace` operations for primitive changes, and processes object keys in + * sorted order for stable output. + * + * **Gotchas** + * + * Arrays are compared by index position, with no move or copy detection. Array + * removals are emitted from highest to lowest index to prevent index shifting. + * The output is deterministic but not guaranteed to be minimal. + * + * **Example** (Computing object diff) + * + * ```ts + * import { JsonPatch } from "effect" + * + * const oldValue = { users: [{ id: 1, name: "Alice" }], count: 1 } + * const newValue = { users: [{ id: 1, name: "Bob" }, { id: 2, name: "Charlie" }], count: 2 } + * + * const patch = JsonPatch.get(oldValue, newValue) + * // [ + * // { op: "replace", path: "/users/0/name", value: "Bob" }, + * // { op: "add", path: "/users/1", value: { id: 2, name: "Charlie" } }, + * // { op: "replace", path: "/count", value: 2 } + * // ] + * ``` + * + * @see {@link apply} to apply the generated patch to a document + * @see {@link JsonPatchOperation} for the operation types in the patch + * @category transforming + * @since 4.0.0 + */ +export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { + if (Object.is(oldValue, newValue)) return [] + const patches: Array = [] + + if (Array.isArray(oldValue) && Array.isArray(newValue)) { + const len1 = oldValue.length + const len2 = newValue.length + + // Compare shared prefix by index + const shared = Math.min(len1, len2) + for (let i = 0; i < shared; i++) { + const path = `/${i}` + const patch = get(oldValue[i], newValue[i]) + for (const op of patch) { + prefixPathInPlace(op, path) + patches.push(op) + } + } + + // Remove from end to start so later indices do not shift. + for (let i = len1 - 1; i >= len2; i--) { + patches.push({ op: "remove", path: `/${i}` }) + } + + // Add from beginning to end. + for (let i = len1; i < len2; i++) { + patches.push({ op: "add", path: `/${i}`, value: newValue[i] }) + } + + return patches + } + + if (isJsonObject(oldValue) && isJsonObject(newValue)) { + const keys1 = Object.keys(oldValue) + const keys2 = Object.keys(newValue) + const allKeys = Array.from(new Set([...keys1, ...keys2])).sort() + + for (const key of allKeys) { + const esc = escapeToken(key) + const path = `/${esc}` + const hasKey1 = Object.hasOwn(oldValue, key) + const hasKey2 = Object.hasOwn(newValue, key) + + if (hasKey1 && hasKey2) { + const patch = get(oldValue[key], newValue[key]) + for (const op of patch) { + prefixPathInPlace(op, path) + patches.push(op) + } + } else if (!hasKey1 && hasKey2) { + patches.push({ op: "add", path, value: newValue[key] }) + } else if (hasKey1 && !hasKey2) { + patches.push({ op: "remove", path }) + } + } + + return patches + } + + patches.push({ op: "replace", path: "", value: newValue }) + return patches +} + +/** + * Applies a JSON Patch to a JSON document. + * + * **When to use** + * + * Use to execute patches generated by {@link get}, transform documents + * with manually constructed patches, or process patch operations from external + * sources. + * + * **Details** + * + * Executes patch operations sequentially, so later operations see changes made + * by earlier operations. It never mutates the input document; array and object + * operations copy the affected containers. An empty patch returns the original + * reference, and a root replace (`path: ""`) returns the provided value + * directly. + * + * **Gotchas** + * + * Invalid paths, missing properties, and out-of-bounds array indices throw + * errors. + * + * **Example** (Applying a patch) + * + * ```ts + * import { JsonPatch } from "effect" + * + * const document = { items: [1, 2, 3], total: 6 } + * const patch: JsonPatch.JsonPatch = [ + * { op: "add", path: "/items/-", value: 4 }, + * { op: "replace", path: "/total", value: 10 } + * ] + * + * const result = JsonPatch.apply(patch, document) + * // { items: [1, 2, 3, 4], total: 10 } + * ``` + * + * @see {@link get} to generate patches from value differences + * @see {@link JsonPatchOperation} for the operation types being applied + * @category transforming + * @since 4.0.0 + */ +export function apply(patch: JsonPatch, oldValue: Schema.Json): Schema.Json { + let doc = oldValue + + for (const op of patch) { + switch (op.op) { + case "replace": { + doc = op.path === "" ? op.value : setAt(doc, op.path, op.value, "replace") + break + } + case "add": { + doc = addAt(doc, op.path, op.value) + break + } + case "remove": { + doc = setAt(doc, op.path, undefined, "remove") + break + } + } + } + + return doc +} + +// Mutates op.path in place for perf; safe because child ops are freshly created and not shared. +function prefixPathInPlace(op: JsonPatchOperation, parent: string): void { + ;(op as any).path = op.path === "" ? parent : parent + op.path +} + +function isJsonObject(value: unknown): value is Schema.JsonObject { + return Predicate.isObject(value) +} + +/** + * Tokenize a JSON Pointer into unescaped reference tokens. + * + * - `""` (empty pointer) refers to the root and returns `[]` + * - Non-empty pointers must start with `/` + */ +function tokenize(pointer: string): Array { + if (pointer === "") return [] + if (pointer.charCodeAt(0) !== 47 /* "/" */) { + throw new Error(`Invalid JSON Pointer, it must start with "/": ${format(pointer)}`) + } + return pointer.split("/").slice(1).map(unescapeToken) +} + +/** Convert a reference token to a non-negative array index (rejects `-` and negatives). */ +function toIndex(token: string): number { + if (!/^(0|[1-9]\d*)$/.test(token)) { + throw new Error(`Invalid array index: "${token}"`) + } + return Number(token) +} + +function addAt(doc: Schema.Json, pointer: string, val: Schema.Json): Schema.Json { + if (pointer === "") return val + + const resolved = resolveParent(doc, pointer) + if (resolved === null) { + throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) + } + + const { lastToken, parent, stack } = resolved + + if (Array.isArray(parent)) { + const idx = lastToken === "-" ? parent.length : toIndex(lastToken) + if (idx < 0 || idx > parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) + const updated = parent.slice() + updated.splice(idx, 0, val) + return rebuildFromStack(stack, updated) + } + + if (isJsonObject(parent)) { + const updated = { ...parent } + updated[lastToken] = val + return rebuildFromStack(stack, updated) + } + + throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) +} + +function setAt( + doc: Schema.Json, + pointer: string, + val: Schema.Json | undefined, + mode: "replace" | "remove" +): Schema.Json { + if (pointer === "") { + if (mode === "remove" || val === undefined) throw new Error("Unsupported operation at the root") + return val + } + + const resolved = resolveParent(doc, pointer) + if (resolved === null) { + throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) + } + + const { lastToken, parent, stack } = resolved + + if (Array.isArray(parent)) { + if (lastToken === "-") throw new Error(`"-" is not valid for ${mode} at "${pointer}".`) + const idx = toIndex(lastToken) + if (idx < 0 || idx >= parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) + const updated = parent.slice() + if (mode === "remove") updated.splice(idx, 1) + else updated[idx] = val + return rebuildFromStack(stack, updated) + } + + if (isJsonObject(parent)) { + if (!Object.hasOwn(parent, lastToken)) { + throw new Error(`Property "${lastToken}" does not exist at "${pointer}".`) + } + const updated = { ...parent } + if (mode === "remove") delete updated[lastToken] + else updated[lastToken] = val! + return rebuildFromStack(stack, updated) + } + + throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) +} + +type StackEntry = { readonly container: unknown; readonly token: number | string } + +// Walk to the parent of `pointer`, recording the path. +// Returns null if the parent path cannot be resolved. +function resolveParent( + doc: Schema.Json, + pointer: string +): { readonly stack: ReadonlyArray; readonly parent: unknown; readonly lastToken: string } | null { + const tokens = tokenize(pointer) + if (tokens.length === 0) return null // caller handles root + + const lastToken = tokens[tokens.length - 1] + const stack: Array = [] + let cur: unknown = doc + + for (let i = 0; i < tokens.length - 1; i++) { + const token = tokens[i] + + if (cur == null) return null + + if (Array.isArray(cur)) { + const idx = toIndex(token) + if (idx < 0 || idx >= cur.length) return null + stack.push({ container: cur, token: idx }) + cur = cur[idx] + continue + } + + if (cur && typeof cur === "object") { + if (!Object.hasOwn(cur, token)) return null + stack.push({ container: cur, token }) + cur = (cur as any)[token] + continue + } + + return null + } + + return { stack, parent: cur, lastToken } +} + +// Rebuild the document by writing `newParent` back through `stack`. +function rebuildFromStack(stack: ReadonlyArray, newParent: Schema.Json): Schema.Json { + let acc: Schema.Json = newParent + + for (let i = stack.length - 1; i >= 0; i--) { + const { container, token } = stack[i] + + if (Array.isArray(container)) { + const copy = container.slice() + copy[token as number] = acc + acc = copy + } else { + const copy = { ...(container as Schema.JsonObject) } + copy[token as string] = acc + acc = copy + } + } + + return acc +} diff --git a/repos/effect/packages/effect/src/JsonPointer.ts b/repos/effect/packages/effect/src/JsonPointer.ts new file mode 100644 index 0000000..a3d0733 --- /dev/null +++ b/repos/effect/packages/effect/src/JsonPointer.ts @@ -0,0 +1,80 @@ +/** + * Helpers for escaping and unescaping JSON Pointer path segments. JSON Pointer + * uses `/` to separate path tokens inside a JSON document, so token text must + * encode literal `~` and `/` characters. This module provides the two RFC 6901 + * token conversions used by JSON Patch and related path handling. + * + * @since 4.0.0 + */ + +/** + * Escapes a JSON Pointer reference token according to RFC 6901 by encoding special characters so the token can be safely used as a segment in a JSON Pointer. + * + * **When to use** + * + * Use when you need to escape a single JSON Pointer path segment. + * + * **Details** + * + * - Returns a new escaped string + * - Replaces `~` (tilde) with `~0` and `/` (forward slash) with `~1` + * - Returns the input unchanged if it contains no special characters + * - Empty strings are valid and returned unchanged + * + * **Gotchas** + * + * The replacement order matters: `~` is replaced before `/` to prevent double-escaping. + * + * **Example** (Escaping special characters) + * + * ```ts + * import { JsonPointer } from "effect" + * + * JsonPointer.escapeToken("a/b") // "a~1b" + * JsonPointer.escapeToken("c~d") // "c~0d" + * JsonPointer.escapeToken("path/to~key") // "path~1to~0key" + * ``` + * + * @see {@link unescapeToken} The inverse operation for decoding escaped tokens + * @category encoding + * @since 4.0.0 + */ +export function escapeToken(token: string): string { + return token.replace(/~/g, "~0").replace(/\//g, "~1") +} + +/** + * Decodes a JSON Pointer reference token according to RFC 6901 escaping rules. + * + * **When to use** + * + * Use when you need to decode a single escaped JSON Pointer path segment. + * + * **Details** + * + * - Returns a new unescaped string + * - Replaces `~1` with `/` (forward slash) and `~0` with `~` (tilde) + * - Returns the input unchanged if it contains no escaped sequences + * - Empty strings are valid and returned unchanged + * + * **Gotchas** + * + * The replacement order matters: `~1` is replaced before `~0` to prevent incorrect decoding. + * + * **Example** (Unescaping special characters) + * + * ```ts + * import { JsonPointer } from "effect" + * + * JsonPointer.unescapeToken("a~1b") // "a/b" + * JsonPointer.unescapeToken("c~0d") // "c~d" + * JsonPointer.unescapeToken("path~1to~0key") // "path/to~key" + * ``` + * + * @see {@link escapeToken} The inverse operation for encoding tokens + * @category decoding + * @since 4.0.0 + */ +export function unescapeToken(token: string): string { + return token.replace(/~1/g, "/").replace(/~0/g, "~") +} diff --git a/repos/effect/packages/effect/src/JsonSchema.ts b/repos/effect/packages/effect/src/JsonSchema.ts new file mode 100644 index 0000000..7def9d3 --- /dev/null +++ b/repos/effect/packages/effect/src/JsonSchema.ts @@ -0,0 +1,948 @@ +/** + * Helpers for normalizing and converting JSON Schema and OpenAPI schema + * documents. Supported inputs include JSON Schema Draft-07, Draft 2020-12, + * OpenAPI 3.0, and OpenAPI 3.1; conversions normalize through + * `Document<"draft-2020-12">` before emitting another dialect. The module also + * defines document types, meta-schema constants, OpenAPI component-key helpers, + * and `$ref` resolution utilities. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" +import { unescapeToken } from "./JsonPointer.ts" +import * as Predicate from "./Predicate.ts" +import * as Rec from "./Record.ts" + +/** + * A plain object representing a single JSON Schema node. + * + * **When to use** + * + * Use to represent an arbitrary JSON Schema object regardless of dialect. + * + * **Details** + * + * This is an open record type (`[x: string]: unknown`) so it can hold any JSON + * Schema keyword. Most functions in this module accept or return this type. + * + * @category models + * @since 4.0.0 + */ +export interface JsonSchema { + [x: string]: unknown +} + +/** + * The set of JSON Schema dialects supported by this module. + * + * **When to use** + * + * Use as the dialect marker for `JsonSchema` documents when parsing, + * converting, or emitting schemas across the supported formats. + * + * **Details** + * + * Supported values are `"draft-07"` for JSON Schema Draft-07, + * `"draft-2020-12"` for JSON Schema Draft 2020-12 and the canonical internal + * form, `"openapi-3.1"` for OpenAPI 3.1, and `"openapi-3.0"` for OpenAPI 3.0. + * + * @see {@link Document} for a single root schema tagged with a dialect + * @see {@link MultiDocument} for multiple root schemas tagged with a dialect + * + * @category models + * @since 4.0.0 + */ +export type Dialect = "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0" + +/** + * The JSON Schema primitive type names. + * + * **When to use** + * + * Use to restrict a JSON Schema `type` keyword to the supported primitive names. + * + * @category models + * @since 4.0.0 + */ +export type Type = "string" | "number" | "boolean" | "array" | "object" | "null" | "integer" + +/** + * A record of named JSON Schema definitions, keyed by definition name. + * + * **When to use** + * + * Use as the shared lookup table for named JSON Schema nodes that are + * referenced from JSON Schema documents. + * + * **Details** + * + * The map is dialect-neutral. Conversion APIs emit it as `$defs`, + * `definitions`, or `components.schemas` depending on the target format. + * + * @see {@link Document} for a single root schema with definitions + * @see {@link MultiDocument} for multiple root schemas sharing definitions + * @see {@link resolve$ref} for resolving a `$ref` against definitions + * + * @category models + * @since 4.0.0 + */ +export interface Definitions extends Record {} + +/** + * A structured container for a single JSON Schema and its associated + * definitions. + * + * **When to use** + * + * Use when you need to carry a root schema together with its shared + * definitions, or when converting between dialects with the `from*` and `to*` + * functions. + * + * **Details** + * + * The `schema` field holds the root schema *without* the definitions + * collection. Root definitions are stored separately in `definitions` and + * referenced via `#/$defs/` for Draft-2020-12, `#/definitions/` + * for Draft-07, and `#/components/schemas/` for OpenAPI 3.1 and + * OpenAPI 3.0. + * + * **Example** (Inspecting a parsed document) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "string", + * $defs: { Trimmed: { type: "string", minLength: 1 } } + * } + * + * const doc = JsonSchema.fromSchemaDraft2020_12(raw) + * + * console.log(doc.dialect) // "draft-2020-12" + * console.log(doc.schema) // { type: "string" } + * console.log(doc.definitions) // { Trimmed: { type: "string", minLength: 1 } } + * ``` + * + * @see {@link MultiDocument} + * @see {@link fromSchemaDraft2020_12} + * @category models + * @since 4.0.0 + */ +export interface Document { + readonly dialect: D + readonly schema: JsonSchema + readonly definitions: Definitions +} + +/** + * Like {@link Document}, but carries multiple root schemas that share a + * single definitions pool. + * + * **When to use** + * + * Use when generating several schemas, such as a request body + * and a response body, that reference the same set of definitions. + * + * **Details** + * + * The `schemas` tuple is non-empty and contains at least one element. + * + * @see {@link Document} + * @see {@link toMultiDocumentOpenApi3_1} + * @category models + * @since 4.0.0 + */ +export interface MultiDocument { + readonly dialect: D + readonly schemas: readonly [JsonSchema, ...Array] + readonly definitions: Definitions +} + +/** + * Represents the `$schema` meta-schema URI for JSON Schema Draft-07. + * + * **When to use** + * + * Use when constructing a Draft-07 JSON Schema document and you need a stable + * value for the root `$schema` field. + * + * **Details** + * + * The exported value is the literal string + * `http://json-schema.org/draft-07/schema`. + * + * @see {@link META_SCHEMA_URI_DRAFT_2020_12} for the Draft 2020-12 `$schema` URI + * + * @category constants + * @since 4.0.0 + */ +export const META_SCHEMA_URI_DRAFT_07 = "http://json-schema.org/draft-07/schema" + +/** + * Represents the `$schema` meta-schema URI for JSON Schema Draft 2020-12. + * + * **When to use** + * + * Use when you need to populate the `$schema` field while emitting a JSON + * Schema document that should declare JSON Schema Draft 2020-12. + * + * **Details** + * + * The exported value is the literal string + * `https://json-schema.org/draft/2020-12/schema`. + * + * @see {@link META_SCHEMA_URI_DRAFT_07} for the Draft-07 `$schema` URI + * + * @category constants + * @since 4.0.0 + */ +export const META_SCHEMA_URI_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema" + +const RE_DEFINITIONS = /^#\/definitions(?=\/|$)/ +const RE_DEFS = /^#\/\$defs(?=\/|$)/ +const RE_COMPONENTS_SCHEMAS = /^#\/components\/schemas(?=\/|$)/ + +/** + * Parses a raw Draft-07 JSON Schema into a `Document<"draft-2020-12">`. + * + * **When to use** + * + * Use when you have a raw JSON Schema object that follows Draft-07 conventions + * and need the canonical Draft-2020-12 document representation. + * + * **Details** + * + * This converts Draft-07 tuple syntax (`items` as array plus + * `additionalItems`) to Draft-2020-12 form (`prefixItems` plus `items`), + * rewrites `#/definitions/...` refs to `#/$defs/...`, and extracts root-level + * `definitions` into the `definitions` field. + * + * **Gotchas** + * + * Unsupported keywords, such as `if`/`then`/`else` and `$id`, are dropped. + * + * **Example** (Parsing a Draft-07 schema) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "object", + * properties: { + * tags: { + * type: "array", + * items: { type: "string" } + * } + * } + * } + * + * const doc = JsonSchema.fromSchemaDraft07(raw) + * console.log(doc.dialect) // "draft-2020-12" + * console.log(doc.schema.properties) // { tags: { type: "array", items: { type: "string" } } } + * ``` + * + * @see {@link fromSchemaDraft2020_12} + * @see {@link fromSchemaOpenApi3_0} + * @see {@link toDocumentDraft07} + * @category decoding + * @since 4.0.0 + */ +export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { + let definitions: Definitions | undefined + + const schema = walk(js, true) as JsonSchema + return { + dialect: "draft-2020-12", + schema, + definitions: definitions ?? {} + } + + function walk(node: unknown, isRoot: boolean): unknown { + if (Array.isArray(node)) return node.map((v) => walk(v, false)) + if (!Predicate.isObject(node)) return node + + const out: Record = {} + + let prefixItems: unknown = undefined + let additionalItems: unknown = undefined + + for (const k of Object.keys(node)) { + const v = node[k] + + switch (k) { + case "$ref": + out.$ref = typeof v === "string" ? v.replace(RE_DEFINITIONS, "#/$defs") : v + break + + case "definitions": { + const mapped = walk_object(v, walk) + if (isRoot) { + definitions = mapped as Definitions | undefined + } else { + out.definitions = mapped ?? v + } + break + } + + case "items": + prefixItems = v + break + case "additionalItems": + additionalItems = v + break + + case "properties": + case "patternProperties": { + const mapped = walk_object(v, walk) + out[k] = mapped ?? v + break + } + + case "additionalProperties": + case "propertyNames": + out[k] = walk(v, false) + break + + case "allOf": + case "anyOf": + case "oneOf": + out[k] = Array.isArray(v) ? v.map((x) => walk(x, false)) : v + break + + case "type": + case "required": + case "enum": + case "const": + case "title": + case "description": + case "default": + case "examples": + case "format": + case "readOnly": + case "writeOnly": + case "pattern": + case "minimum": + case "maximum": + case "exclusiveMinimum": + case "exclusiveMaximum": + case "minLength": + case "maxLength": + case "minItems": + case "maxItems": + case "minProperties": + case "maxProperties": + case "multipleOf": + case "uniqueItems": + out[k] = v + break + + default: + break + } + } + + // Draft-07 tuples -> 2020-12 tuples + if (prefixItems !== undefined) { + if (Array.isArray(prefixItems)) { + out.prefixItems = prefixItems.map((x) => walk(x, false)) + if (additionalItems !== undefined) out.items = walk(additionalItems, false) + } else { + out.items = walk(prefixItems, false) + } + } + + return out + } +} + +/** + * Parses a raw Draft-2020-12 JSON Schema into a `Document<"draft-2020-12">`. + * + * **When to use** + * + * Use when you already have a raw JSON Schema object in Draft-2020-12 format. + * + * **Details** + * + * This separates `$defs` from the root schema into the `definitions` field. + * Unlike {@link fromSchemaDraft07}, this performs no keyword rewriting. + * + * **Example** (Parsing a Draft-2020-12 schema) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "number", + * minimum: 0, + * $defs: { PositiveInt: { type: "integer", minimum: 1 } } + * } + * + * const doc = JsonSchema.fromSchemaDraft2020_12(raw) + * console.log(doc.schema) // { type: "number", minimum: 0 } + * console.log(doc.definitions) // { PositiveInt: { type: "integer", minimum: 1 } } + * ``` + * + * @see {@link fromSchemaDraft07} + * @see {@link fromSchemaOpenApi3_1} + * @category decoding + * @since 4.0.0 + */ +export function fromSchemaDraft2020_12(js: JsonSchema): Document<"draft-2020-12"> { + const { $defs, ...schema } = js + return { + dialect: "draft-2020-12", + schema, + definitions: Predicate.isObject($defs) ? ($defs as Definitions) : {} + } +} + +/** + * Parses a raw OpenAPI 3.1 JSON Schema into a `Document<"draft-2020-12">`. + * + * **When to use** + * + * Use when you need to consume raw JSON Schema objects from an OpenAPI 3.1 + * specification. + * + * **Details** + * + * This rewrites `#/components/schemas/...` refs to `#/$defs/...`, then delegates + * to {@link fromSchemaDraft2020_12}. + * + * **Example** (Parsing an OpenAPI 3.1 schema) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "object", + * properties: { + * user: { $ref: "#/components/schemas/User" } + * } + * } + * + * const doc = JsonSchema.fromSchemaOpenApi3_1(raw) + * // $ref is rewritten to Draft-2020-12 form + * console.log(doc.schema.properties) // { user: { $ref: "#/$defs/User" } } + * ``` + * + * @see {@link fromSchemaOpenApi3_0} + * @see {@link toMultiDocumentOpenApi3_1} + * @category decoding + * @since 4.0.0 + */ +export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> { + const schema = rewrite_refs(js, (ref) => ref.replace(RE_COMPONENTS_SCHEMAS, "#/$defs")) as JsonSchema + return fromSchemaDraft2020_12(schema) +} + +/** + * Parses a raw OpenAPI 3.0 JSON Schema into a `Document<"draft-2020-12">`. + * + * **When to use** + * + * Use when you need to consume raw JSON Schema objects from an OpenAPI 3.0 + * specification. + * + * **Details** + * + * This handles OpenAPI 3.0 extensions, including `nullable`, singular + * `example`, and boolean `exclusiveMinimum` or `exclusiveMaximum`. It + * normalizes the schema to Draft-07 first, then converts to Draft-2020-12 via + * {@link fromSchemaDraft07}. + * + * **Example** (Parsing an OpenAPI 3.0 nullable schema) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const raw: JsonSchema.JsonSchema = { + * type: "string", + * nullable: true + * } + * + * const doc = JsonSchema.fromSchemaOpenApi3_0(raw) + * // nullable is expanded into a type array + * console.log(doc.schema.type) // ["string", "null"] + * ``` + * + * @see {@link fromSchemaOpenApi3_1} + * @see {@link fromSchemaDraft07} + * @category decoding + * @since 4.0.0 + */ +export function fromSchemaOpenApi3_0(schema: JsonSchema): Document<"draft-2020-12"> { + const normalized = normalize_OpenApi3_0_to_Draft07(schema) + return fromSchemaDraft07(normalized as JsonSchema) +} + +/** + * Converts a `Document<"draft-2020-12">` to a `Document<"draft-07">`. + * + * **When to use** + * + * Use when you need to output a canonical JSON Schema document in Draft-07 + * format. + * + * **Details** + * + * This rewrites `#/$defs/...` refs to `#/definitions/...`, converts + * Draft-2020-12 tuple syntax (`prefixItems` plus `items`) to Draft-07 form + * (`items` as array plus `additionalItems`), and converts both the root schema + * and all definitions. + * + * **Gotchas** + * + * Unsupported Draft-2020-12 keywords are dropped. + * + * **Example** (Converting to Draft-07) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const doc = JsonSchema.fromSchemaDraft2020_12({ + * type: "array", + * prefixItems: [{ type: "string" }, { type: "number" }], + * items: { type: "boolean" } + * }) + * + * const draft07 = JsonSchema.toDocumentDraft07(doc) + * console.log(draft07.dialect) // "draft-07" + * console.log(draft07.schema.items) // [{ type: "string" }, { type: "number" }] + * console.log(draft07.schema.additionalItems) // { type: "boolean" } + * ``` + * + * @see {@link fromSchemaDraft07} + * @see {@link toMultiDocumentOpenApi3_1} + * @category encoding + * @since 4.0.0 + */ +export function toDocumentDraft07(document: Document<"draft-2020-12">): Document<"draft-07"> { + return { + dialect: "draft-07", + schema: toSchemaDraft07(document.schema), + definitions: Rec.map(document.definitions, toSchemaDraft07) + } +} + +function toSchemaDraft07(schema: JsonSchema): JsonSchema { + return rewrite(schema) + + function rewrite(node: unknown): JsonSchema { + return walk(rewrite_refs(node, (ref) => ref.replace(RE_DEFS, "#/definitions")), true) as JsonSchema + } + + function walk(node: unknown, _isRoot: boolean): unknown { + if (Array.isArray(node)) return node.map((v) => walk(v, false)) + if (!Predicate.isObject(node)) return node + + const src = node as Record + const out: Record = {} + + let prefixItems: unknown = undefined + let items: unknown = undefined + + for (const k of Object.keys(src)) { + const v = src[k] + + switch (k) { + // We already rewrote $ref via rewrite_refs, so just copy it through. + case "$ref": + case "type": + case "required": + case "enum": + case "const": + case "title": + case "description": + case "default": + case "examples": + case "format": + case "pattern": + case "minimum": + case "maximum": + case "exclusiveMinimum": + case "exclusiveMaximum": + case "minLength": + case "maxLength": + case "minItems": + case "maxItems": + case "minProperties": + case "maxProperties": + case "multipleOf": + case "uniqueItems": + out[k] = v + break + + // Schema maps + case "properties": + case "patternProperties": { + const mapped = walk_object(v, walk) + out[k] = mapped ?? v + break + } + + // Single subschemas + case "additionalProperties": + case "propertyNames": + out[k] = walk(v, false) + break + + // Schema arrays + case "allOf": + case "anyOf": + case "oneOf": + out[k] = Array.isArray(v) ? v.map((x) => walk(x, false)) : v + break + + // Tuple handling (2020-12 form) + case "prefixItems": + prefixItems = v + break + case "items": + items = v + break + + default: + // drop everything else (subset) + break + } + } + + // 2020-12 tuples -> Draft-07 tuples + if (prefixItems !== undefined) { + if (Array.isArray(prefixItems)) { + out.items = prefixItems.map((x) => walk(x, false)) + if (items !== undefined) out.additionalItems = walk(items, false) + } else { + // Non-standard, but keep a reasonable behavior + out.items = walk(prefixItems, false) + } + } else if (items !== undefined) { + // Regular items schema stays as items + out.items = walk(items, false) + } + + return out + } +} + +/** + * Converts a `MultiDocument<"draft-2020-12">` to a + * `MultiDocument<"openapi-3.1">`. + * + * **When to use** + * + * Use when you need to emit an OpenAPI 3.1 multi-document from canonical JSON + * Schema documents. + * + * **Details** + * + * This rewrites `#/$defs/...` refs to `#/components/schemas/...`, sanitizes + * definition keys to match the OpenAPI component key pattern + * (`^[a-zA-Z0-9.\-_]+$`) by replacing invalid characters with `_`, updates all + * `$ref` pointers to use the sanitized keys, and converts all schemas and + * definitions in the multi-document. + * + * **Example** (Converting to OpenAPI 3.1) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const multi: JsonSchema.MultiDocument<"draft-2020-12"> = { + * dialect: "draft-2020-12", + * schemas: [{ $ref: "#/$defs/User" }], + * definitions: { + * User: { type: "object", properties: { name: { type: "string" } } } + * } + * } + * + * const openapi = JsonSchema.toMultiDocumentOpenApi3_1(multi) + * console.log(openapi.dialect) // "openapi-3.1" + * console.log(openapi.schemas[0]) // { $ref: "#/components/schemas/User" } + * ``` + * + * @see {@link toDocumentDraft07} + * @see {@link MultiDocument} + * @category encoding + * @since 4.0.0 + */ +export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-2020-12">): MultiDocument<"openapi-3.1"> { + const keyMap = new Map() + for (const key of Object.keys(multiDocument.definitions)) { + const sanitized = sanitizeOpenApiComponentsSchemasKey(key) + if (sanitized !== key) { + keyMap.set(key, sanitized) + } + } + + function rewrite(schema: JsonSchema): JsonSchema { + return rewrite_refs(schema, ($ref) => { + const tokens = $ref.split("/") + if (tokens.length > 0) { + const identifier = unescapeToken(tokens[tokens.length - 1]) + const sanitized = keyMap.get(identifier) + if (sanitized !== undefined) { + $ref = tokens.slice(0, -1).join("/") + "/" + sanitized + } + } + return $ref.replace(RE_DEFS, "#/components/schemas") + }) as JsonSchema + } + + return { + dialect: "openapi-3.1", + schemas: Arr.map(multiDocument.schemas, rewrite), + definitions: Rec.mapEntries( + multiDocument.definitions, + (definition, key) => [keyMap.get(key) ?? key, rewrite(definition)] + ) + } +} + +/** @internal */ +export const VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP = /^[a-zA-Z0-9.\-_]+$/ + +/** + * Returns a sanitized key for an OpenAPI component schema. + * Should match the `^[a-zA-Z0-9.\-_]+$` regular expression. + * + * @internal + */ +export function sanitizeOpenApiComponentsSchemasKey(s: string): string { + if (s.length === 0) return "_" + if (VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(s)) return s + + const out: Array = [] + + for (const ch of s) { + const code = ch.codePointAt(0) + if ( + code !== undefined && + ((code >= 48 && code <= 57) || // 0-9 + (code >= 65 && code <= 90) || // A-Z + (code >= 97 && code <= 122) || // a-z + code === 46 || // . + code === 45 || // - + code === 95) // _ + ) { + out.push(ch) + } else { + out.push("_") + } + } + + return out.join("") +} + +function rewrite_refs(node: unknown, f: ($ref: string) => string): unknown { + if (Array.isArray(node)) return node.map((v) => rewrite_refs(v, f)) + if (!Predicate.isObject(node)) return node + + const out: Record = {} + + for (const k of Object.keys(node)) { + const v = node[k] + + if (k === "$ref") { + out[k] = typeof v === "string" ? f(v) : v + } else if (Array.isArray(v) || Predicate.isObject(v)) { + out[k] = rewrite_refs(v, f) + } else { + out[k] = v + } + } + + return out +} + +function walk_object( + value: unknown, + walk: (node: unknown, isRoot: boolean) => unknown +): Record | undefined { + if (!Predicate.isObject(value)) return undefined + const out: Record = {} + for (const k of Object.keys(value)) out[k] = walk(value[k], false) + return out +} + +function normalize_OpenApi3_0_to_Draft07(node: unknown): unknown { + if (Array.isArray(node)) return node.map(normalize_OpenApi3_0_to_Draft07) + if (!Predicate.isObject(node)) return node + + const src = node as Record + let out: Record = {} + + for (const k of Object.keys(src)) { + const v = src[k] + if (k === "$ref" && typeof v === "string") { + out[k] = v.replace(RE_COMPONENTS_SCHEMAS, "#/definitions") + } else if (k === "example") { + if (src.examples === undefined) { + out.examples = [v] + } + } else if (Array.isArray(v) || Predicate.isObject(v)) { + out[k] = normalize_OpenApi3_0_to_Draft07(v) + } else { + out[k] = v + } + } + + // Draft-04-style numeric exclusivity booleans + out = adjust_exclusivity(out) + + // OpenAPI 3.0 nullable + if (out.nullable === true) { + out = apply_nullable(out) + } + delete out.nullable + + return out +} + +function adjust_exclusivity(node: Record): Record { + let out = node + + if (typeof out.exclusiveMinimum === "boolean") { + if (out.exclusiveMinimum === true && typeof out.minimum === "number") { + out = { ...out, exclusiveMinimum: out.minimum } + delete out.minimum + } else { + out = { ...out } + delete out.exclusiveMinimum + } + } + + if (typeof out.exclusiveMaximum === "boolean") { + if (out.exclusiveMaximum === true && typeof out.maximum === "number") { + out = { ...out, exclusiveMaximum: out.maximum } + delete out.maximum + } else { + out = { ...out } + delete out.exclusiveMaximum + } + } + + return out +} + +function apply_nullable(node: Record): Record { + // enum widening + if (Array.isArray(node.enum)) { + return widen_type({ + ...node, + enum: node.enum.includes(null) ? node.enum : [...node.enum, null] + }) + } + + // type widening + if (node.type !== undefined) return widen_type(node) + + // const === null + if (node.const === null) return node + + // fallback + return { anyOf: [node, { type: "null" }] } +} + +function widen_type(node: Record): Record { + const t = node.type + if (typeof t === "string") return t === "null" ? node : { ...node, type: [t, "null"] } + if (Array.isArray(t)) return t.includes("null") ? node : { ...node, type: [...t, "null"] } + return node +} + +/** + * Resolves a `$ref` string by looking up the last path segment in a + * definitions map. + * + * **When to use** + * + * Use when you need to dereference a `$ref` pointer to get the JSON Schema + * object it points to. + * + * **Details** + * + * This only resolves the final segment of the ref path, such as `"User"` from + * `"#/$defs/User"`. It returns `undefined` if the definition is not found. + * + * **Gotchas** + * + * This function does not follow arbitrary JSON Pointer paths. + * + * **Example** (Resolving a $ref) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const definitions: JsonSchema.Definitions = { + * User: { type: "object", properties: { name: { type: "string" } } } + * } + * + * const result = JsonSchema.resolve$ref("#/$defs/User", definitions) + * console.log(result) // { type: "object", properties: { name: { type: "string" } } } + * + * const missing = JsonSchema.resolve$ref("#/$defs/Unknown", definitions) + * console.log(missing) // undefined + * ``` + * + * @see {@link resolveTopLevel$ref} + * @see {@link Definitions} + * @category getters + * @since 4.0.0 + */ +export function resolve$ref($ref: string, definitions: Definitions): JsonSchema | undefined { + const tokens = $ref.split("/") + if (tokens.length > 0) { + const identifier = unescapeToken(tokens[tokens.length - 1]) + const definition = definitions[identifier] + if (definition !== undefined) { + return definition + } + } +} + +/** + * Resolves a document whose root schema is a top-level `$ref`. + * + * **When to use** + * + * Use when you need to dereference a top-level `$ref` before inspecting the + * root JSON Schema object's properties directly. + * + * **Details** + * + * This returns the same object if no change is needed, or a shallow copy with + * the resolved schema. + * + * **Example** (Resolving a top-level $ref) + * + * ```ts + * import { JsonSchema } from "effect" + * + * const doc: JsonSchema.Document<"draft-2020-12"> = { + * dialect: "draft-2020-12", + * schema: { $ref: "#/$defs/User" }, + * definitions: { + * User: { type: "object", properties: { name: { type: "string" } } } + * } + * } + * + * const resolved = JsonSchema.resolveTopLevel$ref(doc) + * console.log(resolved.schema) // { type: "object", properties: { name: { type: "string" } } } + * ``` + * + * @see {@link resolve$ref} + * @see {@link Document} + * @category transforming + * @since 4.0.0 + */ +export function resolveTopLevel$ref(document: Document<"draft-2020-12">): Document<"draft-2020-12"> { + if (typeof document.schema.$ref === "string") { + const schema = resolve$ref(document.schema.$ref, document.definitions) + if (schema !== undefined) { + return { ...document, schema } + } + } + return document +} diff --git a/repos/effect/packages/effect/src/Latch.ts b/repos/effect/packages/effect/src/Latch.ts new file mode 100644 index 0000000..418c78e --- /dev/null +++ b/repos/effect/packages/effect/src/Latch.ts @@ -0,0 +1,394 @@ +/** + * Reusable synchronization primitives for coordinating fibers. A `Latch` is + * either open or closed: when it is closed, `await` and `whenOpen` suspend + * until the latch opens or the current waiters are released. The module + * includes effectful and synchronous constructors plus helpers to open, release, + * close, wait, and gate effects behind the latch. + * + * @since 4.0.0 + */ +import type * as Effect from "./Effect.ts" +import * as internal from "./internal/effect.ts" + +/** + * A reusable coordination primitive that lets fibers wait until they are + * released by the latch. + * + * **When to use** + * + * Use to coordinate fibers that must wait for an explicit open or release + * signal before continuing. + * + * **Details** + * + * A closed latch causes `await` and `whenOpen` to suspend. `open` opens the + * latch and releases current and future waiters, `release` releases only + * current waiters without opening it, and `close` makes future waiters suspend + * again. + * + * **Example** (Coordinating fibers with a latch) + * + * ```ts + * import { Effect, Latch } from "effect" + * + * // Create and use a latch for coordination between fibers + * const program = Effect.gen(function*() { + * const latch = yield* Latch.make() + * + * // Wait for the latch to be opened + * yield* latch.await + * + * return "Latch was opened!" + * }) + * ``` + * + * @see {@link make} for creating a latch inside Effect code + * @see {@link open} for releasing current and future waiters + * @see {@link release} for releasing only the current waiters + * + * @category models + * @since 4.0.0 + */ +export interface Latch { + /** + * Opens the latch, releasing all fibers waiting on it. + * + * **When to use** + * + * Use to let current and future waiters continue. + */ + readonly open: Effect.Effect + + /** + * Opens the latch synchronously, releasing all fibers waiting on it. + * + * **When to use** + * + * Use when synchronous code must open the latch immediately. + */ + openUnsafe(this: Latch): boolean + + /** + * Releases all fibers currently waiting on the latch without opening it. + * + * **When to use** + * + * Use to let current waiters continue while future waiters still suspend. + */ + readonly release: Effect.Effect + + /** + * Waits for the latch to be opened or released. + * + * **When to use** + * + * Use to suspend until the latch allows the current fiber to continue. + */ + readonly await: Effect.Effect + + /** + * Closes the latch so future waiters suspend again. + * + * **When to use** + * + * Use to re-enable waiting after a latch has been opened. + */ + readonly close: Effect.Effect + + /** + * Closes the latch synchronously so future waiters suspend again. + * + * **When to use** + * + * Use when synchronous code must close the latch immediately. + */ + closeUnsafe(this: Latch): boolean + + /** + * Runs the given effect only after the latch allows waiting fibers to + * continue. + * + * **When to use** + * + * Use to gate an effect behind the latch signal. + */ + whenOpen(self: Effect.Effect): Effect.Effect + + /** + * Checks whether the latch is currently open or closed. + * + * **When to use** + * + * Use to check the state of the latch without suspending or changing its state. + */ + isOpen(this: Latch): boolean +} + +/** + * Creates a `Latch` synchronously, outside of `Effect`. + * + * **When to use** + * + * Use when you need to allocate a `Latch` synchronously outside an Effect + * workflow. + * + * **Details** + * + * The latch starts closed by default; pass `true` to create it open. + * + * **Example** (Creating a latch unsafely) + * + * ```ts + * import { Effect, Latch } from "effect" + * + * const latch = Latch.makeUnsafe(false) + * + * const waiter = Effect.gen(function*() { + * yield* Effect.log("Waiting for latch to open...") + * yield* latch.await + * yield* Effect.log("Latch opened! Continuing...") + * }) + * + * const opener = Effect.gen(function*() { + * yield* Effect.sleep("2 seconds") + * yield* Effect.log("Opening latch...") + * yield* latch.open + * }) + * + * const program = Effect.all([waiter, opener]) + * ``` + * + * @see {@link make} for creating a latch inside Effect code + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe: (open?: boolean | undefined) => Latch = internal.makeLatchUnsafe + +/** + * Creates a `Latch` inside `Effect`. + * + * **When to use** + * + * Use to create a latch for coordinating fibers inside Effect code. + * + * **Details** + * + * The latch starts closed by default; pass `true` to create it open. + * + * **Example** (Creating a latch) + * + * ```ts + * import { Effect, Latch } from "effect" + * + * const program = Effect.gen(function*() { + * const latch = yield* Latch.make(false) + * + * const waiter = Effect.gen(function*() { + * yield* Effect.log("Waiting for latch to open...") + * yield* latch.await + * yield* Effect.log("Latch opened! Continuing...") + * }) + * + * const opener = Effect.gen(function*() { + * yield* Effect.sleep("2 seconds") + * yield* Effect.log("Opening latch...") + * yield* latch.open + * }) + * + * yield* Effect.all([waiter, opener]) + * }) + * ``` + * + * @see {@link makeUnsafe} for synchronous allocation outside Effect code + * + * @category constructors + * @since 4.0.0 + */ +export const make: (open?: boolean | undefined) => Effect.Effect = internal.makeLatch + +/** + * Opens the latch and releases fibers waiting on it. + * + * **When to use** + * + * Use to open a latch and release all fibers that are waiting on it. + * + * **Details** + * + * The returned effect succeeds with `true` when this call changed the latch + * from closed to open, or `false` if it was already open. + * + * @see {@link openUnsafe} for a synchronous variant + * @see {@link release} to release waiting fibers without opening the latch + * + * @category combinators + * @since 4.0.0 + */ +export const open = (self: Latch): Effect.Effect => self.open + +/** + * Opens the latch synchronously and releases fibers waiting on it. + * + * **When to use** + * + * Use when you need synchronous code to open a latch immediately and release + * the fibers waiting on it. + * + * **Details** + * + * Returns `true` when this call changed the latch from closed to open, or + * `false` if it was already open. This unsafe variant performs the state + * change immediately instead of returning an `Effect`. + * + * @see {@link open} for the effectful variant + * @see {@link release} to release waiting fibers without opening the latch + * @see {@link closeUnsafe} for the synchronous inverse operation + * + * @category unsafe + * @since 4.0.0 + */ +export const openUnsafe = (self: Latch): boolean => self.openUnsafe() + +/** + * Releases the fibers currently waiting on a closed latch without opening it. + * + * **When to use** + * + * Use to let the fibers currently waiting on a latch proceed while keeping the + * latch closed for future waiters. + * + * **Details** + * + * The returned effect succeeds with `true` when release was requested while + * the latch was closed, or `false` if the latch was already open. Future + * waiters still suspend until the latch is opened or released again. + * + * @see {@link open} for opening the latch for current and future waiters + * + * @category combinators + * @since 4.0.0 + */ +export const release = (self: Latch): Effect.Effect => self.release + +const _await = (self: Latch): Effect.Effect => self.await + +export { + /** + * Waits for the latch to be opened. + * + * **When to use** + * + * Use to suspend the current fiber until the latch is opened or the current + * set of waiters is released. + * + * **Details** + * + * Awaiting an already open latch completes immediately. Awaiting a closed + * latch suspends until `open` or `release` resumes the waiters. + * + * **Gotchas** + * + * `release` can resume current waiters without opening the latch, so later + * waiters may still suspend. + * + * @see {@link open} for opening the latch for current and future waiters + * @see {@link release} for resuming current waiters without opening the latch + * @see {@link whenOpen} for waiting before running another effect + * + * @category getters + * @since 4.0.0 + */ + _await as await +} + +/** + * Closes the latch so future `await` and `whenOpen` calls suspend. + * + * **When to use** + * + * Use to re-enable waiting on a latch after it was opened, so later `await` + * and `whenOpen` calls suspend again. + * + * **Details** + * + * The returned effect succeeds with `true` when this call changed the latch + * from open to closed, or `false` if it was already closed. + * + * @see {@link closeUnsafe} for a synchronous variant + * @see {@link open} for opening the latch for current and future waiters + * + * @category combinators + * @since 4.0.0 + */ +export const close = (self: Latch): Effect.Effect => self.close + +/** + * Closes the latch synchronously so future `await` and `whenOpen` calls + * suspend. + * + * **When to use** + * + * Use to close a latch synchronously when the state change must happen outside + * an `Effect`. + * + * **Details** + * + * Returns `true` when this call changed the latch from open to closed, or + * `false` if it was already closed. This unsafe variant performs the state + * change immediately instead of returning an `Effect`. + * + * @see {@link close} for the effectful variant + * @see {@link openUnsafe} to synchronously open the latch and release waiting + * fibers + * + * @category unsafe + * @since 4.0.0 + */ +export const closeUnsafe = (self: Latch): boolean => self.closeUnsafe() + +/** + * Waits on the latch, then runs the provided effect. + * + * **When to use** + * + * Use to gate another effect so it starts only after the latch is opened or + * the current waiters are released. + * + * **Details** + * + * If the latch is open, the effect runs immediately. If it is closed, the + * returned effect suspends until the latch is opened or the current waiters are + * released. The provided effect's success, failure, and requirements are + * preserved. + * + * @see `await` for waiting without running another effect + * @see {@link open} for opening the latch for current and future waiters + * @see {@link release} for resuming current waiters without opening the latch + * + * @category combinators + * @since 4.0.0 + */ +export const whenOpen: { + (self: Latch): (effect: Effect.Effect) => Effect.Effect + (self: Latch, effect: Effect.Effect): Effect.Effect +} = ((...args: Array) => { + if (args.length === 1) { + const [self] = args + return (effect: Effect.Effect) => self.whenOpen(effect) + } + const [self, effect] = args + return self.whenOpen(effect) +}) as any + +/** + * Checks whether the latch is currently open or closed. + * + * **When to use** + * + * Use to check the state of the latch without suspending or changing its state. + * + * @category getters + * @since 4.0.0 + */ +export const isOpen = (self: Latch): boolean => self.isOpen() diff --git a/repos/effect/packages/effect/src/Layer.ts b/repos/effect/packages/effect/src/Layer.ts new file mode 100644 index 0000000..37e758e --- /dev/null +++ b/repos/effect/packages/effect/src/Layer.ts @@ -0,0 +1,2773 @@ +/** + * Builds and wires services for Effect applications. + * + * A `Layer` describes how to acquire one or more services, which + * services are required to build them, and which errors can occur during + * acquisition. Layers can manage scoped resources, memoize shared services, + * combine with other layers, provide services to effects or streams, and attach + * error handling, tracing, or lifecycle hooks. + * + * @since 2.0.0 + */ +import type { NonEmptyArray, NonEmptyReadonlyArray } from "./Array.ts" +import type * as Cause from "./Cause.ts" +import type * as Channel from "./Channel.ts" +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import type { Effect } from "./Effect.ts" +import type * as Exit from "./Exit.ts" +import type { LazyArg } from "./Function.ts" +import { constant, constTrue, constUndefined, dual, identity } from "./Function.ts" +import * as core from "./internal/core.ts" +import * as internalEffect from "./internal/effect.ts" +import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" +import * as internalTracer from "./internal/tracer.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import { CurrentStackFrame } from "./References.ts" +import * as Scope from "./Scope.ts" +import type * as Stream from "./Stream.ts" +import * as Tracer from "./Tracer.ts" +import type * as Types from "./Types.ts" +import type * as Unify from "./Unify.ts" + +const TypeId = "~effect/Layer" + +/** + * A `Layer` describes how to build one or more services for dependency injection. + * + * **When to use** + * + * Use to model construction of application services for dependency injection, + * especially when services have dependencies, can fail during construction, or + * need scoped setup and release. + * + * **Details** + * + * A `Layer` represents `ROut` as the services this layer + * provides, `E` as the possible errors during layer construction, and `RIn` as + * the services this layer requires as dependencies. + * + * @category models + * @since 2.0.0 + */ +export interface Layer extends Variance, Pipeable { + /** @internal */ + build(memoMap: MemoMap, scope: Scope.Scope): Effect, E, RIn> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: LayerUnify + [Unify.ignoreSymbol]?: LayerUnifyIgnore +} + +/** + * Type-level hook that allows `Layer` values to participate in `Unify` + * inference. + * + * **Details** + * + * This is used by Effect's pipe and unification machinery to preserve the + * provided services, error, and requirements of a `Layer`. + * + * @category models + * @since 4.0.0 + */ +export interface LayerUnify { + Layer?: () => A[Unify.typeSymbol] extends Layer | infer _ ? Layer< + Success>, + Error>, + Services> + > + : never +} + +/** + * Type-level marker used by `Unify` for `Layer` types that should be ignored + * during unification. + * + * @category models + * @since 4.0.0 + */ +export interface LayerUnifyIgnore {} + +/** + * The variance interface for Layer type parameters. + * + * @category models + * @since 2.0.0 + */ +export interface Variance { + readonly [TypeId]: { + readonly _ROut: Types.Contravariant + readonly _E: Types.Covariant + readonly _RIn: Types.Covariant + } +} +/** + * A type-level constraint for working with any `Layer` type. + * + * **When to use** + * + * Use to constrain generic parameters or layer collections to any `Layer` + * value while preserving its provided, error, and required service types for + * inference. + * + * **Details** + * + * This interface is used to constrain generic types to `Layer` values without + * specifying exact type parameters. + * + * @see {@link Layer} for the concrete layer interface + * @see {@link Services} for extracting required services from a layer type + * @see {@link Error} for extracting construction errors from a layer type + * @see {@link Success} for extracting provided services from a layer type + * + * @category utility types + * @since 3.9.0 + */ +export interface Any { + readonly [TypeId]: { + readonly _ROut: any + readonly _E: any + readonly _RIn: any + } +} +/** + * Extracts the service requirements (`RIn`) from a `Layer` type. + * + * **When to use** + * + * Use to derive the dependency requirements of a generic or inferred `Layer` + * without restating its `RIn` type parameter. + * + * @see {@link Success} for extracting the services provided by the same `Layer` + * @see {@link Error} for extracting the construction failure type from the same `Layer` + * + * @category utility types + * @since 4.0.0 + */ +export type Services = T extends infer L + ? L extends Layer ? _RIn : never + : never +/** + * Extracts the error type (`E`) from a `Layer` type. + * + * **When to use** + * + * Use to derive a layer construction error type for helper types, wrappers, or + * APIs that preserve a layer failure channel. + * + * @see {@link Success} for extracting the services provided by the same `Layer` + * @see {@link Services} for extracting the dependency requirements of the same `Layer` + * + * @category utility types + * @since 2.0.0 + */ +export type Error = T extends Layer ? _E : never +/** + * Extracts the service output type (`ROut`) from a `Layer` type. + * + * **When to use** + * + * Use to derive the services provided by an existing or generic `Layer` without + * restating its `ROut` type parameter. + * + * @see {@link Error} for extracting the layer construction error type instead + * @see {@link Services} for extracting the layer input service requirements instead + * + * @category utility types + * @since 2.0.0 + */ +export type Success = T extends Layer ? _ROut : never + +const MemoMapTypeId = "~effect/Layer/MemoMap" + +/** + * A `MemoMap` is used to memoize layer construction and ensure sharing of + * layers. + * + * **Details** + * + * The `MemoMap` prevents duplicate construction of the same layer instance, + * enabling efficient resource sharing across layer dependencies. + * + * **Example** (Sharing layer construction with a memo map) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Create a custom MemoMap for manual layer building + * const program = Effect.gen(function*() { + * const memoMap = yield* Layer.makeMemoMap + * const scope = yield* Effect.scope + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const context = yield* Layer.buildWithMemoMap(dbLayer, memoMap, scope) + * + * return Context.get(context, Database) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface MemoMap { + readonly [MemoMapTypeId]: typeof MemoMapTypeId + readonly get: ( + layer: Layer, + scope: Scope.Scope + ) => Effect, E, RIn> | undefined + readonly getOrElseMemoize: ( + layer: Layer, + scope: Scope.Scope, + build: (memoMap: MemoMap, scope: Scope.Scope) => Effect, E, RIn> + ) => Effect, E, RIn> +} + +type MemoMapEntry = { + observers: number + effect: Effect, any> + readonly finalizer: (exit: Exit.Exit) => Effect +} + +const memoMapReuse = ( + entry: MemoMapEntry, + scope: Scope.Scope +): Effect, E, RIn> => { + entry.observers++ + return internalEffect.andThen( + internalEffect.scopeAddFinalizerExit(scope, (exit) => entry.finalizer(exit)), + entry.effect + ) +} + +/** + * Returns `true` if the specified value is a `Layer`, `false` otherwise. + * + * **Example** (Checking whether a value is a layer) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const notALayer = { someProperty: "value" } + * + * console.log(Layer.isLayer(dbLayer)) // true + * console.log(Layer.isLayer(notALayer)) // false + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isLayer = (u: unknown): u is Layer => hasProperty(u, TypeId) + +const LayerProto = { + [TypeId]: { + _ROut: identity, + _E: identity, + _RIn: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const fromBuildUnsafe = ( + build: ( + memoMap: MemoMap, + scope: Scope.Scope + ) => Effect, E, RIn> +): Layer => { + const self = Object.create(LayerProto) + self.build = build + return self +} + +/** + * Constructs a `Layer` from a function that uses a `MemoMap` and `Scope` to + * build the layer. + * + * **Details** + * + * The function receives a `MemoMap` for memoization and a `Scope` for resource management. + * A child scope is created, and if the build fails, the child scope is closed. + * + * **Example** (Constructing a layer from a build function) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const databaseLayer = Layer.fromBuild(() => + * Effect.sync(() => + * Context.make(Database, { + * query: (sql: string) => Effect.succeed("result") + * }) + * ) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromBuild = ( + build: ( + memoMap: MemoMap, + scope: Scope.Scope + ) => Effect, E, RIn> +): Layer => + fromBuildUnsafe((memoMap: MemoMap, scope: Scope.Scope) => { + const layerScope = Scope.forkUnsafe(scope) + return internalEffect.onExit( + build(memoMap, layerScope), + (exit) => exit._tag === "Failure" ? Scope.close(layerScope, exit) : internalEffect.void + ) + }) + +/** + * Constructs a `Layer` from a function that uses a `MemoMap` and `Scope` to + * build the layer, with automatic memoization. + * + * **Details** + * + * This is similar to `fromBuild` but provides automatic memoization of the layer construction. + * The layer will be memoized based on the provided `MemoMap`. + * + * **Example** (Memoizing layer construction) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const databaseLayer = Layer.fromBuildMemo(() => + * Effect.sync(() => + * Context.make(Database, { + * query: (sql: string) => Effect.succeed("result") + * }) + * ) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromBuildMemo = ( + build: ( + memoMap: MemoMap, + scope: Scope.Scope + ) => Effect, E, RIn> +): Layer => { + const self: Layer = fromBuild((memoMap, scope) => memoMap.getOrElseMemoize(self, scope, build)) + return self +} + +const memoMapBuild = ( + memoMap: MemoMapImpl, + layer: Layer, + scope: Scope.Scope, + build: (memoMap: MemoMap, scope: Scope.Scope) => Effect, E, RIn> +): Effect, E, RIn> => { + const layerScope = Scope.makeUnsafe() + const deferred = Deferred.makeUnsafe, E>() + const entry: MemoMapEntry = { + observers: 1, + effect: Deferred.await(deferred), + finalizer: (exit: Exit.Exit) => + internalEffect.suspend(() => { + entry.observers-- + if (entry.observers === 0) { + memoMap.map.delete(layer) + return Scope.close(layerScope, exit) + } + return internalEffect.void + }) + } + memoMap.map.set(layer, entry) + return internalEffect.scopeAddFinalizerExit(scope, entry.finalizer).pipe( + internalEffect.flatMap(() => build(memoMap, layerScope)), + internalEffect.onExit((exit) => { + entry.effect = exit + return Deferred.done(deferred, exit) + }) + ) +} + +class MemoMapImpl implements MemoMap { + get [MemoMapTypeId](): typeof MemoMapTypeId { + return MemoMapTypeId + } + + readonly parent: MemoMap | undefined + + constructor(parent?: MemoMap) { + this.parent = parent + } + + readonly map = new Map, MemoMapEntry>() + + get( + layer: Layer, + scope: Scope.Scope + ): Effect, E, RIn> | undefined { + const local = this.map.get(layer) + if (local) { + return memoMapReuse(local, scope) + } + return this.parent?.get(layer, scope) + } + + getOrElseMemoize( + layer: Layer, + scope: Scope.Scope, + build: (memoMap: MemoMap, scope: Scope.Scope) => Effect, E, RIn> + ): Effect, E, RIn> { + const existing = this.get(layer, scope) + if (existing) { + return existing + } + return memoMapBuild(this, layer, scope, build) + } +} + +/** + * Constructs a `MemoMap` synchronously so it can be used to build additional layers. + * + * **Example** (Creating a memo map unsafely) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Create a memo map for manual layer building + * const program = Effect.gen(function*() { + * const memoMap = Layer.makeMemoMapUnsafe() + * const scope = yield* Effect.scope + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const context = yield* Layer.buildWithMemoMap(dbLayer, memoMap, scope) + * + * return Context.get(context, Database) + * }) + * ``` + * + * @category memo map + * @since 4.0.0 + */ +export const makeMemoMapUnsafe = (): MemoMap => new MemoMapImpl() + +/** + * Constructs a child `MemoMap` synchronously, allowing it to reuse layers + * already memoized in the parent while isolating any new layer allocations to + * the child map. + * + * **When to use** + * + * Use to synchronously fork a memo map for manual layer building when child + * builds should see parent memoized layers without writing newly built layers + * back to the parent. + * + * @see {@link forkMemoMap} for allocating the child memo map inside `Effect` + * @see {@link makeMemoMapUnsafe} for creating a root memo map without a parent + * + * @category memo map + * @since 4.0.0 + */ +export const forkMemoMapUnsafe = (parent: MemoMap): MemoMap => new MemoMapImpl(parent) + +/** + * Constructs a `MemoMap` effectfully so it can be used to build additional layers. + * + * **Example** (Creating a memo map in an effect) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Create a memo map safely within an Effect + * const program = Effect.gen(function*() { + * const memoMap = yield* Layer.makeMemoMap + * const scope = yield* Effect.scope + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const context = yield* Layer.buildWithMemoMap(dbLayer, memoMap, scope) + * + * return Context.get(context, Database) + * }) + * ``` + * + * @category memo map + * @since 2.0.0 + */ +export const makeMemoMap: Effect = internalEffect.sync(makeMemoMapUnsafe) + +/** + * Constructs a child `MemoMap` effectfully, allowing it to reuse layers already + * memoized in the parent while isolating any new layer allocations to the child + * map. + * + * **When to use** + * + * Use when a layer build should inherit already memoized layers from an + * existing `MemoMap` while keeping newly memoized layers out of the parent map. + * + * @see {@link makeMemoMap} for creating a root memo map in an `Effect` + * @see {@link forkMemoMapUnsafe} for the synchronous constructor variant + * @see {@link buildWithMemoMap} for building layers with an explicit memo map + * + * @category memo map + * @since 4.0.0 + */ +export const forkMemoMap = (parent: MemoMap): Effect => internalEffect.sync(() => forkMemoMapUnsafe(parent)) + +/** + * Context service for the current `MemoMap` used in layer construction. + * + * **When to use** + * + * Use when building custom layer operations that need to access the current + * memoization map from the fiber context. + * + * **Details** + * + * This service wraps a `MemoMap` as a `Context.Service`, making it available + * for dependency injection during layer construction. + * + * @see {@link MemoMap} the memoization map type wrapped by this service + * + * @category models + * @since 3.13.0 + */ +export class CurrentMemoMap extends Context.Service()("effect/Layer/CurrentMemoMap") { + static forkOrCreate(self: Context.Context): MemoMap { + const current = Context.getOrUndefined(self, CurrentMemoMap) + return current ? forkMemoMapUnsafe(current) : makeMemoMapUnsafe() + } +} + +/** + * Builds a layer into an `Effect` value, using the specified `MemoMap` to memoize + * the layer construction. + * + * **Example** (Building layers with an explicit memo map) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * // Build layers with explicit memoization control + * const program = Effect.gen(function*() { + * const memoMap = yield* Layer.makeMemoMap + * const scope = yield* Effect.scope + * + * // Build database layer with memoization + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const dbContext = yield* Layer.buildWithMemoMap(dbLayer, memoMap, scope) + * + * // Build logger layer with same memoization (reuses memo if same layer) + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * }) + * const loggerContext = yield* Layer.buildWithMemoMap( + * loggerLayer, + * memoMap, + * scope + * ) + * + * return { + * database: Context.get(dbContext, Database), + * logger: Context.get(loggerContext, Logger) + * } + * }) + * ``` + * + * @category memo map + * @since 2.0.0 + */ +export const buildWithMemoMap: { + ( + memoMap: MemoMap, + scope: Scope.Scope + ): (self: Layer) => Effect, E, RIn> + ( + self: Layer, + memoMap: MemoMap, + scope: Scope.Scope + ): Effect, E, RIn> +} = dual(3, ( + self: Layer, + memoMap: MemoMap, + scope: Scope.Scope +): Effect, E, RIn> => + internalEffect.provideService( + internalEffect.map(self.build(memoMap, scope), Context.add(CurrentMemoMap, memoMap)), + CurrentMemoMap, + memoMap + )) + +/** + * Builds a layer into a scoped value. + * + * **Example** (Building a layer into a context) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Build a layer to get its services + * const program = Effect.gen(function*() { + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * + * // Build the layer into Context - automatically manages scope and memoization + * const context = yield* Layer.build(dbLayer) + * + * // Extract the specific service from the built layer + * const database = Context.get(context, Database) + * + * return yield* database.query("SELECT * FROM users") + * }) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const build = ( + self: Layer +): Effect, E, RIn | Scope.Scope> => + core.withFiber((fiber) => + buildWithMemoMap( + self, + CurrentMemoMap.forkOrCreate(fiber.context), + Context.getUnsafe(fiber.context, Scope.Scope) + ) + ) + +/** + * Builds a layer using an explicit scope. + * + * **When to use** + * + * Use to control the lifetime of layer resources with a scope supplied by the + * caller. + * + * **Details** + * + * Resources created by the layer are released when the supplied scope is + * closed, unless a resource extends its own scope. + * + * **Example** (Building a layer with an explicit scope) + * + * ```ts + * import { Context, Effect, Layer, Scope } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Build a layer with explicit scope control + * const program = Effect.gen(function*() { + * const scope = yield* Effect.scope + * + * const dbLayer = Layer.effect(Database, Effect.gen(function*() { + * console.log("Initializing database...") + * yield* Scope.addFinalizer( + * scope, + * Effect.sync(() => console.log("Database closed")) + * ) + * return { query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) } + * })) + * + * // Build with specific scope - resources tied to this scope + * const context = yield* Layer.buildWithScope(dbLayer, scope) + * const database = Context.get(context, Database) + * + * return yield* database.query("SELECT * FROM users") + * // Database will be closed when scope is closed + * }) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const buildWithScope: { + (scope: Scope.Scope): (self: Layer) => Effect, E, RIn> + (self: Layer, scope: Scope.Scope): Effect, E, RIn> +} = dual(2, ( + self: Layer, + scope: Scope.Scope +): Effect, E, RIn> => + core.withFiber((fiber) => + buildWithMemoMap( + self, + CurrentMemoMap.forkOrCreate(fiber.context), + scope + ) + )) + +/** + * Constructs a layer that provides a single service from an already available + * value. + * + * **When to use** + * + * Use when you need a `Layer` that provides a service from an already + * constructed implementation without effectful acquisition. + * + * **Example** (Creating a layer from a service implementation) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const DatabaseLive = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Query result: ${sql}`)) + * }) + * ``` + * + * @see {@link sync} for constructing layers from lazy values + * + * @category constructors + * @since 2.0.0 + */ +export const succeed: { + (service: Context.Key): (resource: S) => Layer + (service: Context.Key, resource: Types.NoInfer): Layer +} = function() { + if (arguments.length === 1) { + return (resource: any) => succeedContext(Context.make(arguments[0], resource)) + } + return succeedContext(Context.make(arguments[0], arguments[1])) +} as any + +/** + * Constructs a layer that provides all services in an already available + * `Context`. + * + * **When to use** + * + * Use when you need a `Layer` built from an existing `Context`, including when + * you need to provide multiple services at once. + * + * **Details** + * + * This is a more general version of `succeed` that allows you to provide + * multiple services at once through a `Context`. + * + * **Example** (Providing multiple services from a context) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * const context = Context.make(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }).pipe( + * Context.add(Logger, { + * log: (msg: string) => Effect.sync(() => console.log(msg)) + * }) + * ) + * + * const layer = Layer.succeedContext(context) + * ``` + * + * @see {@link succeed} for providing a single service from a value + * + * @category constructors + * @since 2.0.0 + */ +export const succeedContext = (context: Context.Context): Layer => + fromBuildUnsafe(constant(internalEffect.succeed(context))) + +/** + * An empty layer that provides no services, cannot fail, has no requirements, + * and performs no construction or finalization work. + * + * **When to use** + * + * Use as the no-op branch when conditionally composing layers. + * + * **Example** (Disabling optional lifecycle work) + * + * ```ts + * import { Console, Layer } from "effect" + * + * declare const flag: boolean + * + * const StartupLogLive = flag + * ? Layer.effectDiscard(Console.log("application starting")) + * : Layer.empty + * ``` + * + * @see {@link effectDiscard} for running an effect while providing no services + * + * @category constructors + * @since 2.0.0 + */ +export const empty: Layer = succeedContext(Context.empty()) + +/** + * Constructs a layer lazily that provides a single service. + * + * **When to use** + * + * Use when you need a `Layer` that provides one service whose value is created + * synchronously, but creation should be deferred until the layer is built. + * + * **Details** + * + * This is a lazy version of `succeed` where the service value is computed + * synchronously only when the layer is built. + * + * **Example** (Lazily providing a service) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const layer = Layer.sync(Database, () => ({ + * query: (sql: string) => Effect.succeed(`Query: ${sql}`) + * })) + * ``` + * + * @see {@link succeed} for constructing layers from static values + * + * @category constructors + * @since 2.0.0 + */ +export const sync: { + (service: Context.Key): (evaluate: LazyArg) => Layer + (service: Context.Key, evaluate: LazyArg>): Layer +} = function() { + if (arguments.length === 1) { + return (evaluate: LazyArg) => syncContext(() => Context.make(arguments[0], evaluate())) + } + return syncContext(() => Context.make(arguments[0], arguments[1]())) +} as any + +/** + * Constructs a layer lazily that provides all services in a `Context`. + * + * **When to use** + * + * Use when you need a `Layer` that creates multiple services synchronously but + * defers that work until the layer is built. + * + * **Details** + * + * This is a lazy version of `succeedContext` where the `Context` is computed + * synchronously only when the layer is built. + * + * **Example** (Lazily providing a context) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const layer = Layer.syncContext(() => + * Context.make(Database, { + * query: (sql: string) => Effect.succeed(`Query: ${sql}`) + * }) + * ) + * ``` + * + * @see {@link sync} for lazily providing a single service + * @see {@link succeedContext} for providing an already available context + * + * @category constructors + * @since 2.0.0 + */ +export const syncContext = (evaluate: LazyArg>): Layer => + fromBuildMemo(constant(internalEffect.sync(evaluate))) + +/** + * Constructs a layer from an effect that produces a single service. + * + * **When to use** + * + * Use when you need to construct a `Layer`-provided service with an `Effect`, + * dependencies, or scoped resource acquisition. + * + * **Details** + * + * This allows you to create a `Layer` from an `Effect` that produces a service. + * The `Effect` is executed in the scope of the layer, allowing for proper + * resource management. + * + * **Example** (Creating a layer from an effect) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const layer = Layer.effect(Database, + * Effect.sync(() => ({ + * query: (sql: string) => Effect.succeed(`Query: ${sql}`) + * })) + * ) + * ``` + * + * @see {@link effectContext} for effectfully providing multiple services + * @see {@link effectDiscard} for running construction work without providing services + * + * @category constructors + * @since 2.0.0 + */ +export const effect: { + (service: Context.Key): ( + effect: Effect + ) => Layer> + ( + service: Context.Key, + effect: Effect, E, R> + ): Layer> +} = function() { + if (arguments.length === 1) { + return (effect: any) => effectImpl(arguments[0], effect) + } + return effectImpl(arguments[0], arguments[1]) +} as any + +const effectImpl = ( + service: Context.Key, + effect: Effect +): Layer> => + effectContext(internalEffect.map(effect, (value) => Context.make(service, value))) + +/** + * Constructs a layer from an effect that produces all services in a `Context`. + * + * **When to use** + * + * Use when you need a `Layer` that effectfully constructs a `Context` with + * multiple services. + * + * **Details** + * + * This allows you to create a `Layer` from an effectful computation that + * returns multiple services. The `Effect` is executed in the scope of the + * layer. + * + * **Example** (Creating a layer from an effectful context) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service< + * Database, + * { readonly query: (sql: string) => Effect.Effect } + * >()("Database") {} + * + * const layer = Layer.effectContext( + * Effect.succeed(Context.make(Database, { + * query: (sql: string) => Effect.succeed(`Query: ${sql}`) + * })) + * ) + * ``` + * + * @see {@link effect} for effectfully providing a single service + * + * @category constructors + * @since 2.0.0 + */ +export const effectContext = ( + effect: Effect, E, R> +): Layer> => fromBuildMemo((_, scope) => Scope.provide(effect, scope)) + +/** + * Constructs a layer from an effect, discarding its value and providing no + * services. + * + * **When to use** + * + * Use when layer construction should run an Effect for its side effects while providing no + * services. + * + * **Example** (Running an effect during layer construction) + * + * ```ts + * import { Effect, Layer } from "effect" + * + * const initLayer = Layer.effectDiscard( + * Effect.sync(() => { + * console.log("Initializing application...") + * }) + * ) + * ``` + * + * @see {@link empty} for a no-op layer that performs no construction work + * + * @category constructors + * @since 2.0.0 + */ +export const effectDiscard = (effect: Effect): Layer> => + effectContext(internalEffect.as(effect, Context.empty())) + +/** + * Constructs a layer lazily using the specified factory. + * + * **Details** + * + * The factory is evaluated only when the suspended layer is first built, and + * the result is memoized with normal layer sharing semantics. + * + * **Example** (Choosing a layer lazily) + * + * ```ts + * import { Context, Layer } from "effect" + * + * class Config extends Context.Service()("Config") {} + * + * const useProd = true + * + * const layer = Layer.suspend(() => + * useProd + * ? Layer.succeed(Config, "https://api.example.com") + * : Layer.succeed(Config, "http://localhost:3000") + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const suspend = (evaluate: LazyArg>): Layer => + fromBuildMemo((memoMap, scope) => internalEffect.suspend(() => evaluate().build(memoMap, scope))) + +/** + * Unwraps a `Layer` from an `Effect`, flattening the nested structure. + * + * **When to use** + * + * Use when you have an `Effect` that produces a `Layer` and you want to + * use that layer directly. + * + * **Details** + * + * The resulting Layer will have the combined error and dependency types from + * both the outer Effect and the inner Layer. + * + * **Example** (Unwrapping an effectful layer) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const layerEffect = Effect.succeed( + * Layer.succeed(Database, { query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) }) + * ) + * + * const unwrappedLayer = Layer.unwrap(layerEffect) + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const unwrap = ( + self: Effect, E, R> +): Layer> => { + const service = Context.Service>("effect/Layer/unwrap") + return flatMap(effect(service)(self), Context.get(service)) +} + +const mergeAllEffect = , ...Array>]>( + layers: Layers, + memoMap: MemoMap, + scope: Scope.Scope +): Effect< + Context.Context<{ [k in keyof Layers]: Success }[number]>, + { [k in keyof Layers]: Error }[number], + { [k in keyof Layers]: Services }[number] +> => { + const parentScope = Scope.forkUnsafe(scope, "parallel") + return internalEffect.forEach(layers, (layer) => layer.build(memoMap, Scope.forkUnsafe(parentScope, "sequential")), { + concurrency: layers.length + }).pipe( + internalEffect.map((context) => Context.mergeAll(...(context as any))) + ) +} + +/** + * Combines all the provided layers concurrently, creating a new layer with + * merged input, error, and output types. + * + * **When to use** + * + * Use when you need to combine multiple independent layers. + * + * **Details** + * + * All layers are built concurrently, and their outputs are merged into a single layer. + * + * If multiple merged layers depend on the same layer value, that dependency is + * shared by default. Reuse a named layer value when you want services to share + * the same resource, such as one database pool. + * + * **Example** (Merging independent layers) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * }) + * + * const mergedLayer = Layer.mergeAll(dbLayer, loggerLayer) + * ``` + * + * @see {@link merge} for merging one layer with another layer or array + * + * @category zipping + * @since 2.0.0 + */ +export const mergeAll = , ...Array>]>( + ...layers: Layers +): Layer< + Success, + Error, + Services +> => fromBuild((memoMap, scope) => mergeAllEffect(layers, memoMap, scope)) + +/** + * Merges this layer with another layer concurrently, producing a new layer with + * combined input, error, and output types. + * + * **When to use** + * + * Use to combine an existing `Layer` with another `Layer` or an array of + * layers while preserving pipeline style. + * + * **Details** + * + * This is a binary version of `mergeAll` that merges exactly two layers or one + * layer with an array of layers. The layers are built concurrently and their + * outputs are combined. + * + * **Example** (Merging two layers) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) + * }) + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * }) + * + * const mergedLayer = Layer.merge(dbLayer, loggerLayer) + * ``` + * + * @see {@link mergeAll} for merging several layers at once + * + * @category zipping + * @since 2.0.0 + */ +export const merge: { + ( + that: Layer + ): (self: Layer) => Layer + ]>( + that: Layers + ): ( + self: Layer + ) => Layer< + A | Success, + E | Error, + | Services + | R + > + ( + self: Layer, + that: Layer + ): Layer + ]>( + self: Layer, + that: Layers + ): Layer< + A | Success, + E | Error, + | Services + | R + > +} = dual(2, ( + self: Layer, + that: Layer | ReadonlyArray> +) => mergeAll(self, ...(Array.isArray(that) ? that : [that]))) + +const provideWith = ( + self: Layer, + that: Layer | ReadonlyArray>, + f: ( + selfContext: Context.Context, + thatContext: Context.Context + ) => Context.Context +) => + fromBuild((memoMap, scope) => + internalEffect.flatMap( + Array.isArray(that) + ? mergeAllEffect(that as NonEmptyArray>, memoMap, scope) + : (that as Layer).build(memoMap, scope), + (context) => + self.build(memoMap, scope).pipe( + internalEffect.provideContext(context), + internalEffect.map((merged) => f(merged, context)) + ) + ) + ) + +/** + * Feeds the output services of the dependency layer into the requirements of + * this layer, returning a layer that only provides the services from this layer. + * + * **When to use** + * + * Use when you need to hide an implementation dependency layer from callers. + * + * **Details** + * + * In `serviceLayer.pipe(Layer.provide(dependencyLayer))`, the dependency layer is + * built first and is used to satisfy the requirements of `serviceLayer`. + * + * **Example** (Providing layer dependencies) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class UserService extends Context.Service Effect.Effect<{ + * id: string + * name: string + * }> + * }>()("UserService") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * // Create dependency layers + * const databaseLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`)) + * }) + * + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`))) + * }) + * + * // UserService depends on Database and Logger + * const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() { + * const database = yield* Database + * const logger = yield* Logger + * + * return { + * getUser: Effect.fn("UserService.getUser")(function*(id: string) { + * yield* logger.log(`Looking up user ${id}`) + * const result = yield* database.query( + * `SELECT * FROM users WHERE id = ${id}` + * ) + * return { id, name: result } + * }) + * } + * })) + * + * // Provide dependencies to UserService layer + * const userServiceWithDependencies = userServiceLayer.pipe( + * Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer)) + * ) + * + * // Now UserService layer has no dependencies + * const program = Effect.gen(function*() { + * const userService = yield* UserService + * return yield* userService.getUser("123") + * }).pipe( + * Effect.provide(userServiceWithDependencies) + * ) + * ``` + * + * @see {@link provideMerge} for retaining the dependency services + * + * @category providing services + * @since 2.0.0 + */ +export const provide: { + ( + that: Layer + ): (self: Layer) => Layer> + ]>( + that: Layers + ): ( + self: Layer + ) => Layer< + A, + E | Error, + | Services + | Exclude> + > + ( + self: Layer, + that: Layer + ): Layer> + ]>( + self: Layer, + that: Layers + ): Layer< + A, + E | Error, + | Services + | Exclude> + > +} = dual(2, ( + self: Layer, + that: Layer | ReadonlyArray> +) => provideWith(self, that, identity)) + +/** + * Feeds the output services of the dependency layer into the requirements of + * this layer, returning a layer that provides both sets of services. + * + * **When to use** + * + * Use when you need to compose `Layer`s while keeping both the constructed + * service and the dependency used to build it available. + * + * **Details** + * + * Prefer {@link provide} when the dependency should stay private. + * + * **Example** (Providing dependencies while retaining services) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * class UserService extends Context.Service Effect.Effect<{ + * id: string + * name: string + * }> + * }>()("UserService") {} + * + * // Create dependency layers + * const databaseLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`)) + * }) + * + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`))) + * }) + * + * // UserService depends on Database and Logger + * const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() { + * const database = yield* Database + * const logger = yield* Logger + * + * return { + * getUser: Effect.fn("UserService.getUser")(function*(id: string) { + * yield* logger.log(`Looking up user ${id}`) + * const result = yield* database.query( + * `SELECT * FROM users WHERE id = ${id}` + * ) + * return { id, name: result } + * }) + * } + * })) + * + * // Provide dependencies and merge all services together + * const allServicesLayer = userServiceLayer.pipe( + * Layer.provideMerge(Layer.mergeAll(databaseLayer, loggerLayer)) + * ) + * + * // Now the resulting layer provides UserService, Database, AND Logger + * const program = Effect.gen(function*() { + * const userService = yield* UserService + * const logger = yield* Logger // Still available! + * const database = yield* Database // Still available! + * + * const user = yield* userService.getUser("123") + * yield* logger.log(`Found user: ${user.name}`) + * + * return user + * }).pipe( + * Effect.provide(allServicesLayer) + * ) + * ``` + * + * @see {@link provide} for keeping dependency services private + * + * @category providing services + * @since 2.0.0 + */ +export const provideMerge: { + ( + that: Layer + ): (self: Layer) => Layer> + ]>( + that: Layers + ): ( + self: Layer + ) => Layer< + A | Success, + E | Error, + | Services + | Exclude> + > + ( + self: Layer, + that: Layer + ): Layer> + ]>( + self: Layer, + that: Layers + ): Layer< + A | Success, + E | Error, + | Services + | Exclude> + > +} = dual(2, ( + self: Layer, + that: Layer | ReadonlyArray> +) => + provideWith( + self, + that, + (self, that) => Context.merge(that, self) + )) + +/** + * Constructs a layer dynamically based on the output of this layer. + * + * **Example** (Creating services from layer output) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Config extends Context.Service()("Config") {} + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * // Base config layer + * const configLayer = Layer.succeed(Config, { + * dbUrl: "postgres://localhost:5432/mydb", + * logLevel: "debug" + * }) + * + * // Dynamically create services based on config + * const dynamicServiceLayer = configLayer.pipe( + * Layer.flatMap((context) => { + * const config = Context.get(context, Config) + * + * // Create database layer based on config + * const dbLayer = Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => + * Effect.succeed( + * `Querying ${config.dbUrl}: ${sql}` + * )) + * }) + * + * // Create logger layer based on config + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => + * config.logLevel === "debug" + * ? Effect.sync(() => console.log(`[DEBUG] ${msg}`)) + * : Effect.sync(() => console.log(msg)) + * ) + * }) + * + * // Return combined layer + * return Layer.mergeAll(dbLayer, loggerLayer) + * }) + * ) + * + * // Use the dynamic services + * const program = Effect.gen(function*() { + * const database = yield* Database + * const logger = yield* Logger + * + * yield* logger.log("Starting database query") + * const result = yield* database.query("SELECT * FROM users") + * + * return result + * }).pipe( + * Effect.provide(dynamicServiceLayer) + * ) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (context: Context.Context) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + f: (context: Context.Context) => Layer + ): Layer +} = dual(2, ( + self: Layer, + f: (context: Context.Context) => Layer +): Layer => + fromBuild((memoMap, scope) => + internalEffect.flatMap( + self.build(memoMap, scope), + (context) => f(context).build(memoMap, scope) + ) + )) + +/** + * Performs the specified effect if this layer succeeds. + * + * **When to use** + * + * Use to run an effectful observation after a layer has been built + * successfully, such as logging or metrics, without changing the services the + * layer provides. + * + * **Details** + * + * The callback receives the services produced by this layer. Its result is + * discarded, and the original layer output is preserved. + * + * @see {@link tapError} for running an effect when layer construction fails with a typed error + * @see {@link tapCause} for running an effect when layer construction fails with any cause + * + * @category sequencing + * @since 2.0.0 + */ +export const tap: { + ( + f: (context: Context.Context) => Effect + ): (self: Layer) => Layer> + ( + self: Layer, + f: (context: Context.Context) => Effect + ): Layer> +} = dual(2, ( + self: Layer, + f: (context: Context.Context) => Effect +): Layer> => + fromBuild((memoMap, scope) => + internalEffect.flatMap( + self.build(memoMap, scope), + (context) => Scope.provide(internalEffect.as(f(context as Context.Context), context), scope) + ) + )) + +/** + * Performs the specified effect if this layer fails. + * + * **When to use** + * + * Use to run logging, metrics, or other effects when layer construction fails + * while preserving the original typed error. + * + * **Details** + * + * The callback receives the typed error. If the callback succeeds, the layer + * still fails with the original error; if the callback fails, that failure is + * added to the layer's error type. + * + * @see {@link tap} for running an effect when layer construction succeeds + * @see {@link tapCause} for inspecting the full failure cause, including defects and interruption + * + * @category sequencing + * @since 2.0.0 + */ +export const tapError: { + ( + f: (e: XE) => Effect + ): (self: Layer) => Layer> + ( + self: Layer, + f: (e: XE) => Effect + ): Layer> +} = dual(2, ( + self: Layer, + f: (e: XE) => Effect +): Layer> => + fromBuild((memoMap, scope) => + internalEffect.catch_( + self.build(memoMap, scope), + (error) => Scope.provide(internalEffect.andThen(f(error as XE), internalEffect.fail(error)), scope) + ) + )) + +/** + * Performs the specified effect when this layer fails with any cause. + * + * **When to use** + * + * Use to run diagnostics or reporting when layer construction fails and the + * full `Cause` is needed. + * + * **Details** + * + * The callback receives the layer's `Cause`, so it can inspect typed errors, + * defects, and interruption information. If the callback succeeds, the layer + * fails again with the original cause; if the callback fails, that failure is + * added to the layer's error type. + * + * @see {@link tapError} for observing only typed layer construction errors + * @see {@link catchCause} for recovering from a layer construction failure by switching to another layer + * + * @category sequencing + * @since 4.0.0 + */ +export const tapCause: { + ( + f: (cause: Cause.Cause) => Effect + ): (self: Layer) => Layer> + ( + self: Layer, + f: (cause: Cause.Cause) => Effect + ): Layer> +} = dual(2, ( + self: Layer, + f: (cause: Cause.Cause) => Effect +): Layer> => + fromBuild((memoMap, scope) => + internalEffect.catchCause( + self.build(memoMap, scope), + (cause) => + Scope.provide(internalEffect.andThen(f(cause as Cause.Cause), internalEffect.failCause(cause)), scope) + ) + )) + +/** + * Converts layer construction failures into defects, removing them from the + * layer's error type. + * + * **Details** + * + * Use this only when failures should be treated as unrecoverable defects rather + * than typed errors that callers can handle. + * + * **Example** (Converting layer failures to defects) + * + * ```ts + * import { Context, Data, Effect, Layer } from "effect" + * + * class DatabaseError extends Data.TaggedError("DatabaseError")<{ + * message: string + * }> {} + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Layer that can fail during construction + * const flakyDatabaseLayer = Layer.effect(Database, Effect.gen(function*() { + * console.log("connecting") + * return yield* new DatabaseError({ message: "Connection failed" }) + * })) + * + * // Convert failures to fiber death - removes error from type + * const reliableDatabaseLayer = flakyDatabaseLayer.pipe(Layer.orDie) + * + * // Now the layer type is Layer - no error in type + * const program = Effect.gen(function*() { + * const database = yield* Database + * return yield* database.query("SELECT * FROM users") + * }).pipe( + * Effect.provide(reliableDatabaseLayer) + * ) + * + * // Running the program prints "connecting", then the DatabaseError is + * // converted into a fiber defect instead of remaining a typed error. + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const orDie = (self: Layer): Layer => + fromBuildUnsafe((memoMap, scope) => internalEffect.orDie(self.build(memoMap, scope))) + +const catch_: { + ( + onError: (error: E) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + onError: (error: E) => Layer + ): Layer +} = dual(2, ( + self: Layer, + onError: (error: E) => Layer +): Layer => + fromBuildUnsafe((memoMap, scope) => + internalEffect.catch_( + self.build(memoMap, scope), + (e) => onError(e).build(memoMap, scope) + ) as any + )) + +export { + /** + * Recovers from all typed errors by switching to another layer. + * + * **When to use** + * + * Use when every typed `Layer` construction error should use the same + * recovery path. + * + * @see {@link catchTag} for recovering from specific tagged errors + * @see {@link catchCause} for recovering with access to the full cause + * + * @category error handling + * @since 4.0.0 + */ + catch_ as catch +} + +/** + * Recovers from specific tagged errors. + * + * **When to use** + * + * Use when only some tagged `Layer` construction errors should be recovered. + * + * **Example** (Recovering from tagged layer errors) + * + * ```ts + * import { Context, Data, Effect, Layer } from "effect" + * + * class ConfigError extends Data.TaggedError("ConfigError") {} + * + * class Config extends Context.Service()("Config") {} + * + * const configLayer = Layer.effect(Config, Effect.fail(new ConfigError())) + * + * const fallbackLayer = Layer.succeed(Config, { apiUrl: "http://localhost" }) + * + * const recovered = configLayer.pipe( + * Layer.catchTag("ConfigError", () => fallbackLayer) + * ) + * ``` + * + * @see {@link catchCause} for recovering with access to the full cause + * + * @category error handling + * @since 4.0.0 + */ +export const catchTag: { + | NonEmptyReadonlyArray>, E, RIn2, E2, ROut2>( + k: K, + f: ( + e: Types.ExtractTag, K extends NonEmptyReadonlyArray ? K[number] : K> + ) => Layer + ): ( + self: Layer + ) => Layer< + ROut & ROut2, + E2 | Types.ExcludeTag ? K[number] : K>, + RIn2 | RIn + > + < + RIn, + E, + ROut, + const K extends Types.Tags | NonEmptyReadonlyArray>, + RIn2, + E2, + ROut2 + >( + self: Layer, + k: K, + f: (e: Types.ExtractTag ? K[number] : K>) => Layer + ): Layer< + ROut & ROut2, + E2 | Types.ExcludeTag ? K[number] : K>, + RIn | RIn2 + > +} = dual(3, < + RIn, + E, + ROut, + const K extends Types.Tags | NonEmptyReadonlyArray>, + RIn2, + E2, + ROut2 +>( + self: Layer, + k: K, + f: (e: Types.ExtractTag ? K[number] : K>) => Layer +): Layer ? K[number] : K>, RIn | RIn2> => + fromBuildUnsafe((memoMap, scope) => + internalEffect.catchTag( + self.build(memoMap, scope), + k, + (error) => f(error).build(memoMap, scope) + ) as any + )) + +/** + * Recovers from any failure cause by switching to another layer. + * + * **When to use** + * + * Use when you need `Layer` recovery to inspect more than the typed error, + * such as defects or interruption information. + * + * **Details** + * + * The handler receives the full `Cause` of the failed layer, including typed + * errors, unexpected defects, and interruption information, and returns the + * fallback layer to build instead. Finalizers for resources acquired by the + * failed layer are still run before the fallback layer is acquired. + * + * **Example** (Recovering from layer failures by cause) + * + * ```ts + * import { Context, Data, Effect, Layer } from "effect" + * + * class DatabaseError extends Data.TaggedError("DatabaseError")<{ + * message: string + * }> {} + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * const primaryDatabaseLayer = Layer.effect(Database, + * Effect.fail(new DatabaseError({ message: "Primary DB unreachable" })) + * ) + * + * const databaseWithFallback = primaryDatabaseLayer.pipe( + * Layer.catchCause(() => { + * return Layer.succeed(Database, { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Memory: ${sql}`)) + * }) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const database = yield* Database + * const result = yield* database.query("SELECT * FROM users") + * console.log(result) + * }).pipe( + * Effect.provide(databaseWithFallback) + * ) + * + * Effect.runPromise(program) + * // Memory: SELECT * FROM users + * ``` + * + * @see {@link catchTag} for recovering from specific tagged errors + * + * @category error handling + * @since 4.0.0 + */ +export const catchCause: { + ( + onError: (cause: Cause.Cause) => Layer + ): (self: Layer) => Layer + ( + self: Layer, + onError: (cause: Cause.Cause) => Layer + ): Layer +} = dual(2, ( + self: Layer, + onError: (cause: Cause.Cause) => Layer +): Layer => + fromBuildUnsafe((memoMap, scope) => + internalEffect.catchCause( + self.build(memoMap, scope), + (cause) => onError(cause).build(memoMap, scope) + ) as any + )) + +/** + * Updates a service in the context with a new implementation. + * + * **When to use** + * + * Use to adapt or extend a service's behavior during the creation of a + * layer. + * + * **Details** + * + * This function modifies the existing implementation of a service in the + * context. It retrieves the current service, applies the provided + * transformation function `f`, and replaces the old service with the + * transformed one. + * + * @category providing services + * @since 3.13.0 + */ +export const updateService: { + ( + service: Context.Key, + f: (a: Types.NoInfer) => A + ): (layer: Layer) => Layer + ( + layer: Layer, + service: Context.Key, + f: (a: Types.NoInfer) => A + ): Layer +} = dual( + 3, + ( + layer: Layer, + service: Context.Key, + f: (a: Types.NoInfer) => A + ): Layer => provide(layer, effect(service, internalEffect.map(service, f))) +) + +/** + * Creates a fresh version of this layer that will not be shared. + * + * **When to use** + * + * Use when you need two parts of an application to receive separate instances + * of a resource, such as two independent client sessions. + * + * **Gotchas** + * + * Do not use it just to work around confusing composition. By default, sharing + * the same layer value is usually the desired behavior. + * + * **Example** (Creating non-shared layer instances) + * + * ```ts + * import { Context, Effect, Layer, Ref } from "effect" + * + * class Counter extends Context.Service()("Counter") {} + * + * class Left extends Context.Service()("Left") {} + * + * class Right extends Context.Service()("Right") {} + * + * const leftLayer = Layer.effect(Left, Effect.gen(function*() { + * const counter = yield* Counter + * return { counterId: counter.id } + * })) + * + * const rightLayer = Layer.effect(Right, Effect.gen(function*() { + * const counter = yield* Counter + * return { counterId: counter.id } + * })) + * + * const showIds = Effect.gen(function*() { + * const left = yield* Left + * const right = yield* Right + * console.log(`same Counter: ${left.counterId === right.counterId}`) + * }) + * + * const program = Effect.gen(function*() { + * const nextId = yield* Ref.make(0) + * + * const counterLayer = Layer.effect(Counter, Effect.gen(function*() { + * const id = yield* Ref.updateAndGet(nextId, (n) => n + 1) + * console.log("constructed Counter") + * return { id } + * })) + * + * const shared = Layer.merge( + * Layer.provide(leftLayer, counterLayer), + * Layer.provide(rightLayer, counterLayer) + * ) + * + * yield* Effect.provide(showIds, shared) + * + * const freshCounterLayer = Layer.fresh(counterLayer) + * const fresh = Layer.merge( + * Layer.provide(leftLayer, freshCounterLayer), + * Layer.provide(rightLayer, freshCounterLayer) + * ) + * + * yield* Effect.provide(showIds, fresh) + * }) + * + * Effect.runPromise(program) + * // constructed Counter + * // same Counter: true + * // constructed Counter + * // constructed Counter + * // same Counter: false + * ``` + * + * @category layers + * @since 2.0.0 + */ +export const fresh = (self: Layer): Layer => + fromBuildUnsafe((_, scope) => self.build(makeMemoMapUnsafe(), scope)) + +/** + * Builds this layer and keeps it alive until the returned effect is interrupted. + * + * **When to use** + * + * Use when you model your entire application as a layer, such as an HTTP + * server. + * + * **Details** + * + * When the returned effect is interrupted, the layer scope is closed and all + * finalizers registered during layer acquisition are run. + * + * **Example** (Launching an application layer) + * + * ```ts + * import { Console, Context, Effect, Layer } from "effect" + * + * class HttpServer extends Context.Service Effect.Effect + * readonly stop: () => Effect.Effect + * }>()("HttpServer") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * // Server layer that starts an HTTP server + * const serverLayer = Layer.effect(HttpServer, Effect.gen(function*() { + * yield* Console.log("Starting HTTP server...") + * + * return { + * start: Effect.fn("HttpServer.start")(function*() { + * yield* Console.log("Server listening on port 3000") + * return "Server started" + * }), + * stop: Effect.fn("HttpServer.stop")(function*() { + * yield* Console.log("Server stopped gracefully") + * return "Server stopped" + * }) + * } + * })) + * + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Console.log(`[LOG] ${msg}`)) + * }) + * + * // Application layer combining all services + * const appLayer = Layer.mergeAll(serverLayer, loggerLayer) + * + * // Launch the application - runs until interrupted + * const application = appLayer.pipe( + * Layer.launch, + * Effect.tapError((error) => Console.log(`Application failed: ${error}`)), + * Effect.tap(() => Console.log("Application completed")) + * ) + * + * // This will run forever until externally interrupted + * // Effect.runFork(application) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const launch = (self: Layer): Effect => + internalEffect.scoped(internalEffect.andThen(build(self), internalEffect.never)) + +/** + * A utility type for creating partial mocks of services in testing. + * + * **When to use** + * + * Use to type partial test service implementations where only exercised + * effectful members are stubbed. + * + * **Details** + * + * This type makes `Effect`, `Stream`, and `Channel` values and functions + * returning them optional, while keeping non-effectful properties required. + * This allows you to provide only the methods you need to test while leaving + * others unimplemented. + * + * @see {@link mock} for creating a mock layer from a partial service implementation + * + * @category testing + * @since 3.17.0 + */ +export type PartialEffectful = Types.Simplify< + & { + [K in keyof A as A[K] extends AnyEffectOrStream ? K : never]?: A[K] + } + & { + [K in keyof A as A[K] extends AnyEffectOrStream ? never : K]: A[K] + } +> + +type AnyEffectOrStream = + | Effect + | Stream.Stream + | Channel.Channel + | ((...args: any) => Effect) + | ((...args: any) => Stream.Stream) + | ((...args: any) => Channel.Channel) + +/** + * Creates a mock layer for testing purposes. You can provide a partial + * implementation of the service. Any missing members that are `Effect`s, + * `Stream`s, `Channel`s, or functions returning them will fail with an + * unimplemented defect when used. + * + * **Details** + * + * Missing members are represented by a value that can be used as an `Effect`, + * `Stream`, `Channel`, or as a function returning an `Effect`. This lets the + * mock preserve the shape of common service methods while still failing loudly + * when an unimplemented member is exercised. + * + * **Example** (Mocking services for tests) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class UserService extends Context.Service Effect.Effect<{ id: string; name: string }, Error> + * readonly deleteUser: (id: string) => Effect.Effect + * readonly updateUser: ( + * id: string, + * data: object + * ) => Effect.Effect<{ id: string; name: string }, Error> + * }>()("UserService") {} + * + * // Create a partial mock - only implement what you need for testing + * const testUserLayer = Layer.mock(UserService, { + * config: { apiUrl: "https://test-api.com" }, // Required - non-Effect property + * getUser: (id: string) => Effect.succeed({ id, name: "Test User" }) // Mock implementation + * // deleteUser and updateUser are omitted - will throw UnimplementedError if called + * }) + * + * // Use in tests + * const testProgram = Effect.gen(function*() { + * const userService = yield* UserService + * + * // This works - we provided an implementation + * const user = yield* userService.getUser("123") + * console.log(user.name) // "Test User" + * + * // This would throw - we didn't implement deleteUser + * // yield* userService.deleteUser("123") // UnimplementedError + * }).pipe( + * Effect.provide(testUserLayer) + * ) + * ``` + * + * @category testing + * @since 3.17.0 + */ +export const mock: { + (service: Context.Key): (implementation: PartialEffectful) => Layer + (service: Context.Key, implementation: Types.NoInfer>): Layer +} = function() { + if (arguments.length === 1) { + return (implementation: any) => mockImpl(arguments[0], implementation) + } + return mockImpl(arguments[0], arguments[1]) +} as any + +const mockImpl = (service: Context.Key, implementation: PartialEffectful): Layer => + succeed(service)( + new Proxy({ ...implementation as object } as S, { + get(target, prop, _receiver) { + if (prop in target) { + return target[prop as keyof S] + } + const prevLimit = getStackTraceLimit() + setStackTraceLimit(2) + const error = new Error(`${service.key}: Unimplemented method "${prop.toString()}"`) + setStackTraceLimit(prevLimit) + error.name = "UnimplementedError" + return makeUnimplemented(error) + }, + has: constTrue + }) + ) + +const makeUnimplemented = (error: globalThis.Error) => { + const dead = Object.assign(internalEffect.die(error), { + [StreamTypeId]: StreamTypeId, + channel: { + [ChannelTypeId]: ChannelTypeId, + transform: () => internalEffect.succeed(dead), + pipe() { + return pipeArguments(this, arguments) + } + }, + [ChannelTypeId]: ChannelTypeId, + transform: () => internalEffect.succeed(dead) + }) + function unimplemented() { + return dead + } + // @effect-diagnostics-next-line floatingEffect:off + Object.assign(unimplemented, dead) + Object.setPrototypeOf(unimplemented, Object.getPrototypeOf(dead)) + return unimplemented +} + +const StreamTypeId: Stream.TypeId = "~effect/Stream" +const ChannelTypeId: Channel.TypeId = "~effect/Channel" + +// ----------------------------------------------------------------------------- +// Type constraints +// ----------------------------------------------------------------------------- + +/** + * Ensures that a layer's success type extends a given type `ROut`. + * + * **Details** + * + * This function provides compile-time type checking to ensure that the success + * value of a layer conforms to a specific type constraint. + * + * **Example** (Constraining layer success types) + * + * ```ts + * import { Layer } from "effect" + * + * declare const FortyTwoLayer: Layer.Layer<42, never, never> + * declare const StringLayer: Layer.Layer + * + * // Define a constraint that the success type must be a number + * const satisfiesNumber = Layer.satisfiesSuccessType() + * + * // This works - Layer<42, never, never> extends Layer + * const validLayer = satisfiesNumber(FortyTwoLayer) + * + * // This would cause a TypeScript compilation error: + * // const invalidLayer = satisfiesNumber(StringLayer) + * // ^^^^^^^^^^^ + * // Type 'string' is not assignable to type 'number' + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesSuccessType = + () => (layer: Layer): Layer => layer + +/** + * Ensures that a layer's error type extends a given type `E`. + * + * **Details** + * + * This function provides compile-time type checking to ensure that the error + * type of a layer conforms to a specific type constraint. + * + * **Example** (Constraining layer error types) + * + * ```ts + * import { Layer } from "effect" + * + * declare const ErrorLayer: Layer.Layer + * declare const TypeErrorLayer: Layer.Layer + * declare const StringLayer: Layer.Layer + * + * // Define a constraint that the error type must be an Error + * const satisfiesError = Layer.satisfiesErrorType() + * + * // This works - Layer extends Layer + * const validLayer = satisfiesError(TypeErrorLayer) + * + * // This would cause a TypeScript compilation error: + * // const invalidLayer = satisfiesError(StringLayer) + * // ^^^^^^^^^^^ + * // Type 'string' is not assignable to type 'Error' + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesErrorType = + () => (layer: Layer): Layer => layer + +/** + * Ensures that a layer's requirements type extends a given type `R`. + * + * **Details** + * + * This function provides compile-time type checking to ensure that the + * requirements type of a layer conforms to a specific type constraint. + * + * **Example** (Constraining layer service requirements) + * + * ```ts + * import { Layer } from "effect" + * + * declare const FortyTwoLayer: Layer.Layer + * declare const StringLayer: Layer.Layer + * + * // Define a constraint that the service requirements must be numbers + * const satisfiesNumber = Layer.satisfiesServicesType() + * + * // This works - Layer extends Layer + * const validLayer = satisfiesNumber(FortyTwoLayer) + * + * // This would cause a TypeScript compilation error: + * // const invalidLayer = satisfiesNumber(StringLayer) + * // ^^^^^^^^^^^ + * // Type 'string' is not assignable to type 'number' + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const satisfiesServicesType = + () => (layer: Layer): Layer => layer + +// ----------------------------------------------------------------------------- +// Tracing +// ----------------------------------------------------------------------------- + +/** + * Represents options that can be used to control the behavior of spans created + * for layers. + * + * **When to use** + * + * Use to configure tracing metadata, stack trace capture, and `onEnd` + * finalization for spans created by `Layer.span` and `Layer.withSpan` during + * layer construction. + * + * **Details** + * + * Extends `Tracer.SpanOptions` with `onEnd`, which runs when the layer span + * ends as the layer scope closes. + * + * @see {@link span} for creating a layer span + * @see {@link withSpan} for wrapping layer construction in a span + * + * @category options + * @since 4.0.0 + */ +export interface SpanOptions extends Tracer.SpanOptions { + /** + * Runs when the span associated with the layer ends, which happens when the + * layer scope is closed. + */ + readonly onEnd?: + | ((span: Tracer.Span, exit: Exit.Exit) => Effect) + | undefined +} + +/** + * Constructs a new `Layer` which creates a span and registers it as the current + * parent span. + * + * **Details** + * + * This allows you to create a traced scope for layer construction, making all + * operations within the layer constructor part of the same trace span. The span + * is automatically ended when the layer's scope is closed. If `onEnd` is + * provided, it receives the span and the layer scope's exit value when the span + * ends. + * + * **Example** (Tracing layer construction with a span) + * + * ```ts + * import { Console, Context, Effect, Layer } from "effect" + * import type { Tracer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Create a traced layer - all operations performed during construction of + * // the `Database` service are part of the "database-init" span + * const databaseLayer = Layer.effect(Database, Effect.gen(function*() { + * // These operations are traced under "database-init" span + * yield* Effect.log("Connecting to database") + * yield* Effect.sleep("100 millis") + * yield* Effect.log("Database connected") + * + * const parentSpan = yield* Effect.currentParentSpan + * yield* Console.log((parentSpan as Tracer.Span).name) // "database-init" + * + * return { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) + * } + * })).pipe(Layer.provide(Layer.span("database-init"))) + * + * // Can also use the `onEnd` callback to execute logic when the span ends + * const tracedLayer = Layer.span("service-initialization", { + * attributes: { version: "1.0.0" }, + * onEnd: (span, exit) => + * Effect.sync(() => { + * console.log(`Span ${span.name} ended with:`, exit._tag) + * }) + * }) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const span = ( + name: string, + options?: SpanOptions +): Layer => { + options = internalTracer.addSpanStackTrace(options) + return effect( + Tracer.ParentSpan, + options?.onEnd + ? internalEffect.tap( + internalEffect.makeSpanScoped(name, options), + (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : internalEffect.makeSpanScoped(name, options) + ) +} + +/** + * Constructs a layer that provides an existing span as the current parent span. + * + * **Details** + * + * The supplied span is made available through `Tracer.ParentSpan` for layers + * that are built with this layer. This API does not create, end, or close the + * span; the caller remains responsible for the span's lifetime. + * + * **Example** (Referencing an existing parent span) + * + * ```ts + * import { Console, Context, Effect, Layer, Tracer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * // Create a layer that uses an existing span as parent + * const databaseLayer = Layer.effect( + * Database, + * Effect.gen(function*() { + * yield* Effect.log("Initializing database") + * + * const parentSpan = yield* Effect.currentParentSpan + * yield* Console.log(parentSpan.spanId) // "42" + * + * return { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) + * } + * }) + * ).pipe(Layer.provide(Layer.parentSpan(Tracer.externalSpan({ + * spanId: "42", + * traceId: "000" + * })))) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const parentSpan = (span: Tracer.AnySpan): Layer => + succeedContext(Tracer.ParentSpan.context(span)) + +/** + * Wraps a `Layer` with a new tracing span, making all operations in the layer + * constructor part of the named trace span. + * + * **Details** + * + * This creates a new span for the layer's construction and execution. The span + * is automatically ended when the layer's scope is closed. This is useful for + * tracking the lifecycle and performance of layer initialization. + * + * **Example** (Wrapping a layer with a span) + * + * ```ts + * import { Context, Effect, Layer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Logger extends Context.Service Effect.Effect + * }>()("Logger") {} + * + * // Create layers with tracing + * const databaseLayer = Layer.effect(Database, Effect.gen(function*() { + * yield* Effect.log("Connecting to database") + * yield* Effect.sleep("100 millis") + * return { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) + * } + * })).pipe(Layer.withSpan("database-initialization", { + * attributes: { dbType: "postgres" } + * })) + * + * const loggerLayer = Layer.succeed(Logger, { + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * }).pipe(Layer.withSpan("logger-initialization")) + * + * // Combine traced layers + * const appLayer = Layer.mergeAll(databaseLayer, loggerLayer).pipe( + * Layer.withSpan("app-initialization", { + * onEnd: (span, exit) => + * Effect.sync(() => { + * console.log(`Application initialization completed: ${exit._tag}`) + * }) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const database = yield* Database + * const logger = yield* Logger + * + * yield* logger.log("Application ready") + * return yield* database.query("SELECT * FROM users") + * }).pipe(Effect.provide(appLayer)) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withSpan: { + ( + name: string, + options?: SpanOptions + ): ( + self: Layer + ) => Layer> + ( + self: Layer, + name: string, + options?: SpanOptions + ): Layer> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = internalTracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) as SpanOptions + if (dataFirst) { + const self = arguments[0] + return unwrap( + internalEffect.map( + options?.onEnd !== undefined + ? internalEffect.tap( + internalEffect.makeSpanScoped(name, options), + (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : internalEffect.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) + } + return (self: Layer) => + unwrap( + internalEffect.map( + options?.onEnd !== undefined + ? internalEffect.tap( + internalEffect.makeSpanScoped(name, options), + (span) => internalEffect.addFinalizer((exit) => options.onEnd!(span, exit)) + ) + : internalEffect.makeSpanScoped(name, options), + (span) => withParentSpan(self, span) + ) + ) +} as any + +/** + * Wraps a layer so spans created during its construction use the supplied span + * as their parent. + * + * **Details** + * + * Use this to attach layer construction to an existing trace hierarchy. This API + * does not create or end the supplied parent span. + * + * When the supplied span is a native `Span`, layer construction also receives + * diagnostic information that helps associate failures with the layer call site. + * External spans are only installed as the parent span and do not add this + * diagnostic call-site information. + * + * **Example** (Attaching layers to an existing parent span) + * + * ```ts + * import { Context, Effect, Layer, Tracer } from "effect" + * + * class Database extends Context.Service Effect.Effect + * }>()("Database") {} + * + * class Cache extends Context.Service Effect.Effect + * }>()("Cache") {} + * + * // Create layers + * const DatabaseLayer = Layer.effect(Database, Effect.gen(function*() { + * yield* Effect.log("Connecting to database") + * return { + * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`)) + * } + * })) + * + * const CacheLayer = Layer.effect(Cache, Effect.gen(function*() { + * yield* Effect.log("Connecting to cache") + * return { + * get: Effect.fn("Cache.get")((key: string) => Effect.succeed(`Cache: ${key}`)) + * } + * })) + * + * // Use with an existing parent span from Effect.withSpan + * const program = Effect.withSpan("application-startup")( + * Effect.gen(function*() { + * const parentSpan = yield* Tracer.ParentSpan + * + * // Both layers will be children of "application-startup" span + * const AppLayer = Layer.mergeAll(DatabaseLayer, CacheLayer).pipe( + * Layer.withParentSpan(parentSpan) + * ) + * + * const context = yield* Layer.build(AppLayer) + * const database = Context.get(context, Database) + * const cache = Context.get(context, Cache) + * + * const dbResult = yield* database.query("SELECT * FROM users") + * const cacheResult = yield* cache.get("user:123") + * + * return { dbResult, cacheResult } + * }) + * ) + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withParentSpan: { + ( + span: Tracer.AnySpan, + options?: Tracer.TraceOptions + ): ( + self: Layer + ) => Layer> + ( + self: Layer, + span: Tracer.AnySpan, + options?: Tracer.TraceOptions + ): Layer> +} = function() { + const dataFirst = isLayer(arguments[0]) + const span: Tracer.AnySpan = dataFirst ? arguments[1] : arguments[0] + let options = dataFirst ? arguments[2] : arguments[1] + let provideStackFrame: (self: Layer) => Layer = identity + if (span._tag === "Span") { + options = internalTracer.addSpanStackTrace(options) + provideStackFrame = provideSpanStackFrame(span.name, options?.captureStackTrace) + } + const parentSpanLayer = parentSpan(span) + if (dataFirst) { + return provide(provideStackFrame(arguments[0]), parentSpanLayer) + } + return (self: Layer) => provide(provideStackFrame(self), parentSpanLayer) +} as any + +const provideSpanStackFrame = (name: string, stack: (() => string | undefined) | undefined) => { + stack = typeof stack === "function" ? stack : constUndefined + return updateService(CurrentStackFrame, (parent) => ({ + name, + stack, + parent + })) +} diff --git a/repos/effect/packages/effect/src/LayerMap.ts b/repos/effect/packages/effect/src/LayerMap.ts new file mode 100644 index 0000000..ce63bbd --- /dev/null +++ b/repos/effect/packages/effect/src/LayerMap.ts @@ -0,0 +1,469 @@ +/** + * Caches scoped services selected by key and built from layers. + * + * A `LayerMap` turns a key into a cached service `Context` and + * exposes that context as either a `Layer` or a scoped effect. Entries can be + * invalidated explicitly or released after they sit unused. This is useful for + * keyed resource families such as tenant clients, regional connections, or + * environment-specific services. + * + * @since 3.14.0 + */ +import * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import { identity } from "./Function.ts" +import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" +import * as Layer from "./Layer.ts" +import * as RcMap from "./RcMap.ts" +import * as Scope from "./Scope.ts" +import type { Mutable, NoExcessProperties } from "./Types.ts" + +const TypeId = "~effect/LayerMap" + +type IdleTimeToLiveInput = Duration.Input | ((key: K) => Duration.Input) + +/** + * A scoped, keyed map of layer-built service contexts. + * + * **Details** + * + * A `LayerMap` builds resources for a key on demand, exposes them as a `Layer` + * or scoped `Context`, and can invalidate cached resources for a key. + * + * **Example** (Managing keyed layers) + * + * ```ts + * import { Context, Effect, Layer, LayerMap } from "effect" + * + * // Define a service key + * const DatabaseService = Context.Service<{ + * readonly query: (sql: string) => Effect.Effect + * }>("Database") + * + * // Create a LayerMap that provides different database configurations + * const createDatabaseLayerMap = LayerMap.make((env: string) => + * Layer.succeed(DatabaseService)({ + * query: Effect.fn("DatabaseService.query")((sql) => Effect.succeed(`${env}: ${sql}`)) + * }) + * ) + * + * // Use the LayerMap + * const program = Effect.gen(function*() { + * const layerMap = yield* createDatabaseLayerMap + * + * // Get a layer for a specific environment + * const devLayer = layerMap.get("development") + * + * // Get context directly + * const context = yield* layerMap.contextEffect("production") + * + * // Invalidate a cached layer + * yield* layerMap.invalidate("development") + * }) + * ``` + * + * @category models + * @since 3.14.0 + */ +export interface LayerMap { + readonly [TypeId]: typeof TypeId + + /** + * The internal RcMap that stores the resources. + */ + readonly rcMap: RcMap.RcMap, E> + + /** + * Retrieves a Layer for the resources associated with the key. + */ + get(key: K): Layer.Layer + + /** + * Retrieves the context associated with the key. + */ + contextEffect(key: K): Effect.Effect, E, Scope.Scope> + + /** + * Invalidates the resource associated with the key. + */ + invalidate(key: K): Effect.Effect +} + +/** + * Creates a `LayerMap` that dynamically provides resources based on a key. + * + * **Example** (Creating a layer map) + * + * ```ts + * import { Context, Effect, Layer, LayerMap } from "effect" + * + * // Define a service key + * const DatabaseService = Context.Service<{ + * readonly query: (sql: string) => Effect.Effect + * }>("Database") + * + * // Create a LayerMap that provides different database configurations + * const program = Effect.gen(function*() { + * const layerMap = yield* LayerMap.make( + * (env: string) => + * Layer.succeed(DatabaseService)({ + * query: Effect.fn("DatabaseService.query")((sql) => Effect.succeed(`${env}: ${sql}`)) + * }), + * { idleTimeToLive: "5 seconds" } + * ) + * + * // Get a layer for a specific environment + * const devLayer = layerMap.get("development") + * + * // Use the layer to provide the service + * const result = yield* Effect.provide( + * Effect.gen(function*() { + * const db = yield* DatabaseService + * return yield* db.query("SELECT * FROM users") + * }), + * devLayer + * ) + * + * console.log(result) // "development: SELECT * FROM users" + * }) + * ``` + * + * @category constructors + * @since 3.14.0 + */ +export const make: < + K, + L extends Layer.Layer, + PreloadKeys extends Iterable | undefined = undefined +>( + lookup: (key: K) => L, + options?: { + readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + readonly preloadKeys?: PreloadKeys + } | undefined +) => Effect.Effect< + LayerMap, Layer.Error>, + PreloadKeys extends undefined ? never : Layer.Error, + Scope.Scope | Layer.Services +> = Effect.fnUntraced(function*( + lookup: (key: K) => Layer.Layer, + options?: { + readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + } | undefined +) { + const context = yield* Effect.context() + const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) + + const rcMap = yield* RcMap.make({ + lookup: (key: K) => + Effect.contextWith((_: Context.Context) => + Layer.buildWithMemoMap(lookup(key), memoMap, Context.get(_, Scope.Scope)) + ), + idleTimeToLive: options?.idleTimeToLive + }) + + return identity>({ + [TypeId]: TypeId, + rcMap, + get: (key) => Layer.effectContext(RcMap.get(rcMap, key)), + contextEffect: (key) => RcMap.get(rcMap, key), + invalidate: (key) => RcMap.invalidate(rcMap, key) + }) +}) + +/** + * Creates a `LayerMap` from a record of predefined layers. + * + * **Details** + * + * The record keys become the keys accepted by the returned `LayerMap`, and the + * record values are the layers built for those keys. + * + * **Example** (Creating a layer map from a record) + * + * ```ts + * import { Context, Effect, Layer, LayerMap } from "effect" + * + * // Define service keys + * const DevDatabase = Context.Service<{ + * readonly query: (sql: string) => Effect.Effect + * }>("DevDatabase") + * + * const ProdDatabase = Context.Service<{ + * readonly query: (sql: string) => Effect.Effect + * }>("ProdDatabase") + * + * // Create predefined layers + * const layers = { + * development: Layer.succeed(DevDatabase)({ + * query: Effect.fn("DevDatabase.query")((sql) => Effect.succeed(`DEV: ${sql}`)) + * }), + * production: Layer.succeed(ProdDatabase)({ + * query: Effect.fn("ProdDatabase.query")((sql) => Effect.succeed(`PROD: ${sql}`)) + * }) + * } as const + * + * // Create a LayerMap from the record + * const program = Effect.gen(function*() { + * const layerMap = yield* LayerMap.fromRecord(layers, { + * idleTimeToLive: "10 seconds" + * }) + * + * // Get layers by key + * const devLayer = layerMap.get("development") + * const prodLayer = layerMap.get("production") + * + * console.log("LayerMap created from record") + * }) + * ``` + * + * @category constructors + * @since 3.14.0 + */ +export const fromRecord = < + const Layers extends Record>, + const Preload extends boolean = false +>( + layers: Layers, + options?: { + readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + readonly preload?: Preload | undefined + } | undefined +): Effect.Effect< + LayerMap< + keyof Layers, + Layer.Success, + Layer.Error + >, + Preload extends true ? Layer.Error : never, + Scope.Scope | (Layers[keyof Layers] extends Layer.Layer ? _R : never) +> => + make((key: keyof Layers) => layers[key], { + ...options, + preloadKeys: options?.preload ? Object.keys(layers) : undefined + }) as any + +/** + * Service class shape produced by `LayerMap.Service`. + * + * **When to use** + * + * Use as the public type for classes returned by `LayerMap.Service` when an API + * needs to accept, return, or alias the generated service class and its static + * helpers. + * + * **Details** + * + * It combines a `Context.Service` tag for the `LayerMap` with default layers + * and helper accessors for retrieving, using, and invalidating keyed resources. + * + * @see {@link Service} for creating concrete `LayerMap` service classes + * + * @category services + * @since 3.14.0 + */ +export interface TagClass< + in out Self, + in out Id extends string, + in out K, + in out I, + in out E, + in out R, + in out LE, + in out Deps extends Layer.Layer +> extends Context.ServiceClass> { + /** + * A default layer for the `LayerMap` service. + */ + readonly layer: Layer.Layer< + Self, + (Deps extends Layer.Layer ? _E : never) | LE, + | Exclude ? _A : never)> + | (Deps extends Layer.Layer ? _R : never) + > + + /** + * A default layer for the `LayerMap` service without the dependencies provided. + */ + readonly layerNoDeps: Layer.Layer + + /** + * Retrieves a Layer for the resources associated with the key. + */ + readonly get: (key: K) => Layer.Layer + + /** + * Retrieves the context associated with the key. + */ + readonly contextEffect: (key: K) => Effect.Effect, E, Scope.Scope | Self> + + /** + * Invalidates the resource associated with the key. + */ + readonly invalidate: (key: K) => Effect.Effect +} + +/** + * Create a `LayerMap` service that provides a dynamic set of resources based on + * a key. + * + * **Example** (Defining a layer map service) + * + * ```ts + * import { Console, Context, Effect, Layer, LayerMap } from "effect" + * + * // Define a service key + * const Greeter = Context.Service<{ + * readonly greet: Effect.Effect + * }>("Greeter") + * + * // Create a service that wraps a LayerMap + * class GreeterMap extends LayerMap.Service()("GreeterMap", { + * // Define the lookup function for the layer map + * lookup: (name: string) => + * Layer.succeed(Greeter)({ + * greet: Effect.succeed(`Hello, ${name}!`) + * }), + * + * // If a layer is not used for a certain amount of time, it can be removed + * idleTimeToLive: "5 seconds" + * }) {} + * + * // Usage + * const program = Effect.gen(function*() { + * // Access and use the Greeter service + * const greeter = yield* Greeter + * yield* Console.log(yield* greeter.greet) + * }).pipe( + * // Use the GreeterMap service to provide a variant of the Greeter service + * Effect.provide(GreeterMap.get("John")) + * ).pipe( + * // Provide the GreeterMap layer + * Effect.provide(GreeterMap.layer) + * ) + * ``` + * + * @category services + * @since 3.14.0 + */ +export const Service = () => +< + const Id extends string, + const Options extends + | NoExcessProperties<{ + readonly lookup: (key: any) => Layer.Layer + readonly dependencies?: ReadonlyArray> | undefined + readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + readonly preloadKeys?: + | Iterable any } ? K : never> + | undefined + }, Options> + | NoExcessProperties<{ + readonly layers: Record> + readonly dependencies?: ReadonlyArray> | undefined + readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + readonly preload?: boolean | undefined + }, Options> +>( + id: Id, + options: Options +): TagClass< + Self, + Id, + Options extends { readonly lookup: (key: infer K) => any } ? K + : Options extends { readonly layers: infer Layers } ? keyof Layers + : never, + Service.Success, + Options extends { readonly preload: true } ? never : Service.Error, + Service.Services, + Options extends { readonly preload: true } ? Service.Error + : Options extends { readonly preloadKeys: Iterable } ? Service.Error + : never, + Options extends { readonly dependencies: ReadonlyArray> } ? Options["dependencies"][number] + : never +> => { + const Err = globalThis.Error as any + const limit = getStackTraceLimit() + setStackTraceLimit(2) + const creationError = new Err() + setStackTraceLimit(limit) + + function TagClass() {} + const TagClass_ = TagClass as any as Mutable> + Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service(id))) + TagClass.key = id + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + + TagClass_.layerNoDeps = Layer.effect(TagClass_)( + "lookup" in options + ? make(options.lookup, options) + : fromRecord(options.layers as any, options) as any + ) + TagClass_.layer = options.dependencies && options.dependencies.length > 0 ? + Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) : + TagClass_.layerNoDeps + + TagClass_.get = (key: string) => Layer.unwrap(Effect.map(TagClass_, (layerMap) => layerMap.get(key))) + TagClass_.contextEffect = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.contextEffect(key)) + TagClass_.invalidate = (key: string) => Effect.flatMap(TagClass_, (layerMap) => layerMap.invalidate(key)) + + return TagClass as any +} + +/** + * Type helpers for values created with `LayerMap.Service`. + * + * @since 3.14.0 + */ +export declare namespace Service { + /** + * Extracts the key type accepted by a `LayerMap.Service` definition. + * + * @category services + * @since 3.14.0 + */ + export type Key = Options extends { readonly lookup: (key: infer K) => any } ? K + : Options extends { readonly layers: infer Layers } ? keyof Layers + : never + + /** + * Extracts the layer type produced by a `LayerMap.Service` definition. + * + * @category services + * @since 3.14.0 + */ + export type Layers = Options extends { readonly lookup: (key: infer _K) => infer Layers } ? Layers + : Options extends { readonly layers: infer Layers } ? Layers[keyof Layers] + : never + + /** + * Extracts the services provided by the layers in a `LayerMap.Service` + * definition. + * + * @category services + * @since 3.14.0 + */ + export type Success = Layers extends Layer.Layer ? _A : never + + /** + * Extracts the error type of the layers in a `LayerMap.Service` definition. + * + * @category services + * @since 3.14.0 + */ + export type Error = Layers extends Layer.Layer ? _E : never + + /** + * Extracts the service requirements of the layers in a `LayerMap.Service` + * definition. + * + * @category services + * @since 4.0.0 + */ + export type Services = Layers extends Layer.Layer ? _R : never +} diff --git a/repos/effect/packages/effect/src/LayerRef.ts b/repos/effect/packages/effect/src/LayerRef.ts new file mode 100644 index 0000000..387c794 --- /dev/null +++ b/repos/effect/packages/effect/src/LayerRef.ts @@ -0,0 +1,377 @@ +/** + * Creates refreshable references to a single layer-built service context. + * + * A `LayerRef` builds one `Layer` into a cached `Context`, exposes it + * back as a layer or scoped effect, and supports invalidation so later users can + * acquire a fresh context. + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import { identity } from "./Function.ts" +import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" +import * as Layer from "./Layer.ts" +import * as RcRef from "./RcRef.ts" +import type * as Schedule from "./Schedule.ts" +import * as Scope from "./Scope.ts" +import type { Mutable } from "./Types.ts" + +const TypeId = "~effect/LayerRef" + +/** + * A refreshable reference to a single layer-built service context. + * + * **When to use** + * + * Use when you want to share one scoped layer resource across many users while + * retaining the ability to invalidate it and rebuild it later. + * + * **Details** + * + * A `LayerRef` is the unkeyed counterpart to a layer cache: it lazily builds the + * layer on first use, reuses the resulting context while it is borrowed or kept + * idle, and can invalidate the cached context so the next use rebuilds it. + * + * @see {@link make} for constructing a `LayerRef` from a layer + * @see {@link Service} for defining a `LayerRef` as a service class + * + * @category models + * @since 4.0.0 + */ +export interface LayerRef { + readonly [TypeId]: typeof TypeId + + /** + * The underlying reference-counted cache that stores the built context. + */ + readonly rcRef: RcRef.RcRef, E> + + /** + * Layer that provides the currently cached context, acquiring it if needed. + */ + readonly get: Layer.Layer + + /** + * Scoped effect that retrieves the currently cached context, acquiring it if needed. + */ + readonly contextEffect: Effect.Effect, E, Scope.Scope> + + /** + * Invalidates the cached context so the next use rebuilds the layer. + */ + readonly invalidate: Effect.Effect + + /** + * Invalidates the cached context so the next use rebuilds the layer, and + * reacquires it. + */ + readonly refresh: Effect.Effect +} + +/** + * Creates a `LayerRef` from a `Layer`. + * + * **When to use** + * + * Use when you have one layer-built resource that should be shared, optionally + * kept alive while idle, and refreshed on demand. + * + * **Details** + * + * The layer is built lazily on first use unless `preload` is `true`. + * `idleTimeToLive` keeps the context cached after it stops being used, and + * `invalidationSchedule` can periodically invalidate it. When `preload` is + * `true`, scheduled invalidation also reacquires the context. + * + * **Gotchas** + * + * Invalidation does not revoke contexts already borrowed by active scopes; those + * contexts remain usable until their scopes close. + * + * **Example** (Sharing one layer-built service) + * + * ```ts + * import { Context, Effect, Layer, LayerRef } from "effect" + * + * class Database extends Context.Service + * }>()("Database") {} + * + * const databaseLayer = Layer.succeed(Database, { + * query: Effect.succeed("result") + * }) + * + * const query = Effect.gen(function*() { + * const database = yield* Database + * return yield* database.query + * }) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const ref = yield* LayerRef.make(databaseLayer, { + * idleTimeToLive: "5 seconds" + * }) + * + * const result = yield* Effect.provide(query, ref.get) + * + * yield* ref.invalidate + * + * return result + * }) + * ) + * ``` + * + * @see {@link Service} for defining a reusable service class around a `LayerRef` + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced( + function*( + layer: Layer.Layer, + options?: { + /** + * Duration to keep the resource alive after it is no longer used. + */ + readonly idleTimeToLive?: Duration.Input | undefined + /** + * Whether to acquire the resource during creation. + */ + readonly preload?: Preload | undefined + /** + * Schedule used to invalidate the cached resource. When `preload` is + * `true`, each scheduled invalidation also reacquires the resource. + */ + readonly invalidationSchedule?: Schedule.Schedule | undefined + } | undefined + ): Effect.fn.Return< + LayerRef, + [Preload] extends [true] ? E : never, + Scope.Scope | R | SR + > { + const context = yield* Effect.context() + const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) + + const rcRef = yield* RcRef.make({ + acquire: Effect.contextWith((_: Context.Context) => + Layer.buildWithMemoMap(layer, memoMap, Context.get(_, Scope.Scope)) + ), + idleTimeToLive: options?.idleTimeToLive + }) + + const refresh = RcRef.invalidate(rcRef).pipe( + Effect.andThen(Effect.scoped(RcRef.get(rcRef))), + Effect.asVoid + ) + + if (options?.preload) { + yield* refresh as Effect.Effect + } + + if (options?.invalidationSchedule) { + const onRefresh = options.preload ? refresh : RcRef.invalidate(rcRef) + yield* onRefresh.pipe( + Effect.ignoreCause, + Effect.schedule(options.invalidationSchedule), + Effect.forkScoped + ) + } + + return identity>({ + [TypeId]: TypeId, + rcRef, + get: Layer.effectContext(RcRef.get(rcRef)), + contextEffect: RcRef.get(rcRef), + invalidate: RcRef.invalidate(rcRef), + refresh + }) + } +) + +/** + * Service class shape produced by `LayerRef.Service`. + * + * **When to use** + * + * Use as the public type for classes returned by `LayerRef.Service` when an API + * needs to accept, return, or alias the generated service class and its static + * helpers. + * + * **Details** + * + * It combines a `Context.Service` tag for the `LayerRef` with default layers and + * helper accessors for retrieving, using, and invalidating the cached resource. + * + * @see {@link Service} for creating concrete `LayerRef` service classes + * + * @category services + * @since 4.0.0 + */ +export interface TagClass< + in out Self, + in out Id extends string, + in out I, + in out E, + in out R, + in out LE, + in out Deps extends Layer.Layer +> extends Context.ServiceClass> { + /** + * Default layer for the `LayerRef` service, with dependencies applied. + */ + readonly layer: Layer.Layer< + Self, + (Deps extends Layer.Layer ? _E : never) | LE, + | Exclude ? _A : never)> + | (Deps extends Layer.Layer ? _R : never) + > + + /** + * Default layer for the `LayerRef` service without provided dependencies. + */ + readonly layerNoDeps: Layer.Layer + + /** + * Layer that provides the currently cached context, requiring this service. + */ + readonly get: Layer.Layer + + /** + * Scoped effect that retrieves the currently cached context through this service. + */ + readonly contextEffect: Effect.Effect, E, Scope.Scope | Self> + + /** + * Invalidates the cached context through this service. + */ + readonly invalidate: Effect.Effect + + /** + * Invalidates the cached context through this service, and reacquires it. + */ + readonly refresh: Effect.Effect +} + +/** + * Creates a service class for a `LayerRef`. + * + * **When to use** + * + * Use when you want to name a shared layer reference as an application service + * and expose static helpers for providing, retrieving, and invalidating it. + * + * **Details** + * + * The returned class is a `Context.Service` whose value is a `LayerRef`. It also + * includes `.layer`, `.layerNoDeps`, `.get`, `.contextEffect`, and `.invalidate` + * helpers so callers do not need to access the `LayerRef` value directly. + * + * **Example** (Defining a refreshable service) + * + * ```ts + * import { Context, Effect, Layer, LayerRef } from "effect" + * + * class Database extends Context.Service + * }>()("Database") {} + * + * const databaseLayer = Layer.succeed(Database, { + * query: Effect.succeed("result") + * }) + * + * class DatabaseRef extends LayerRef.Service()("DatabaseRef", { + * layer: databaseLayer, + * preload: true + * }) {} + * + * const program = Effect.gen(function*() { + * const database = yield* Database + * return yield* database.query + * }).pipe( + * Effect.provide(DatabaseRef.get), + * Effect.provide(DatabaseRef.layer) + * ) + * ``` + * + * @see {@link make} for creating a `LayerRef` value without defining a service class + * + * @category services + * @since 4.0.0 + */ +export const Service = () => +< + const Id extends string, + I, + E, + R, + X, + const Deps extends ReadonlyArray> = [], + const Preload extends boolean = never, + SE = never, + SR = never +>( + id: Id, + options: { + /** + * Layer used to build the cached context. + */ + readonly layer: Layer.Layer + /** + * Layers used to satisfy dependencies of the `LayerRef` service layer. + */ + readonly dependencies?: Deps | undefined + /** + * Duration to keep the resource alive after it is no longer used. + */ + readonly idleTimeToLive?: Duration.Input | undefined + /** + * Whether to acquire the resource during creation. + */ + readonly preload?: Preload | undefined + /** + * Schedule used to invalidate the cached resource. When `preload` is + * `true`, each scheduled invalidation also reacquires the resource. + */ + readonly invalidationSchedule?: Schedule.Schedule | undefined + } +): TagClass< + Self, + Id, + I, + E, + R | SR, + [Preload] extends [true] ? E : never, + Deps[number] +> => { + const Err = globalThis.Error as any + const limit = getStackTraceLimit() + setStackTraceLimit(2) + const creationError = new Err() + setStackTraceLimit(limit) + + function TagClass() {} + const TagClass_ = TagClass as any as Mutable> + Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service(id))) + TagClass.key = id + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack + } + }) + + TagClass_.layerNoDeps = Layer.effect(TagClass_)( + make(options.layer, options) + ) + TagClass_.layer = options.dependencies && options.dependencies.length > 0 ? + Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) : + TagClass_.layerNoDeps + + TagClass_.get = Layer.unwrap(TagClass_.useSync((ref) => ref.get)) + TagClass_.contextEffect = TagClass_.use((ref) => ref.contextEffect) + TagClass_.invalidate = TagClass_.use((ref) => ref.invalidate) + TagClass_.refresh = TagClass_.use((ref) => ref.refresh) + + return TagClass as any +} diff --git a/repos/effect/packages/effect/src/LogLevel.ts b/repos/effect/packages/effect/src/LogLevel.ts new file mode 100644 index 0000000..f8002b8 --- /dev/null +++ b/repos/effect/packages/effect/src/LogLevel.ts @@ -0,0 +1,412 @@ +/** + * Log-level types and helpers used by Effect logging. The module defines all + * accepted log levels, the concrete emitted severities, the ordered level list, + * equality and ordering instances, threshold comparison helpers, and an effect + * for checking whether a level is enabled by the current logging settings. + * + * @since 2.0.0 + */ +import type * as Effect from "./Effect.ts" +import * as Equ from "./Equivalence.ts" +import * as core from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as Ord from "./Order.ts" +import * as References from "./References.ts" + +/** + * Represents every level used by Effect logging, including concrete message + * severities and the `All` and `None` sentinel levels. + * + * **When to use** + * + * Use to type values that may be either concrete log message severities or + * logging configuration sentinels. + * + * **Details** + * + * The levels are ordered from most severe to least severe: + * - `All` - Special level that allows all messages + * - `Fatal` - System is unusable, immediate attention required + * - `Error` - Error conditions that should be investigated + * - `Warn` - Warning conditions that may indicate problems + * - `Info` - Informational messages about normal operation + * - `Debug` - Debug information useful during development + * - `Trace` - Very detailed trace information + * - `None` - Special level that suppresses all messages + * + * **Example** (Using log levels) + * + * ```ts + * import { Effect } from "effect" + * + * // Using log levels with Effect logging + * const program = Effect.gen(function*() { + * yield* Effect.logFatal("System failure") + * yield* Effect.logError("Database error") + * yield* Effect.logWarning("High memory usage") + * yield* Effect.logInfo("User logged in") + * yield* Effect.logDebug("Processing request") + * yield* Effect.logTrace("Variable state") + * }) + * + * // Type-safe log level variables + * const errorLevel = "Error" // LogLevel + * const debugLevel = "Debug" // LogLevel + * ``` + * + * @category models + * @since 2.0.0 + */ +export type LogLevel = "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None" + +/** + * Log levels that represent actual message severities, excluding the `All` and + * `None` sentinel levels. + * + * **When to use** + * + * Use when typing emitted log message severities, such as explicit log calls, + * current log level references, or error-report severity annotations, where + * `All` and `None` are not valid values. + * + * @see {@link LogLevel} for the wider log-level type that also accepts the + * `All` and `None` sentinel levels + * @see {@link values} for the runtime list of all accepted `LogLevel` values, + * including sentinels + * + * @category models + * @since 4.0.0 + */ +export type Severity = "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" + +/** + * Returns all `LogLevel` values in order from `All` through the concrete severities to + * `None`. + * + * **When to use** + * + * Use to enumerate or validate all accepted `LogLevel` string values, including + * the `All` and `None` sentinel levels. + * + * **Details** + * + * The array order matches the module severity order: `All`, concrete + * severities from `Fatal` to `Trace`, then `None`. + * + * **Gotchas** + * + * This list includes `All` and `None`, so it is not limited to concrete emitted + * severities. + * + * @see {@link Severity} for the concrete message severity type that excludes `All` and `None` + * @see {@link Order} for comparing these levels by severity order + * + * @category models + * @since 4.0.0 + */ +export const values: ReadonlyArray = ["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"] + +/** + * Order instance for `LogLevel` that defines the severity ordering. + * + * **When to use** + * + * Use to sort or compare log levels according to Effect's severity order. + * + * **Details** + * + * This order treats "All" as the least restrictive level and "None" as the most restrictive, + * with Fatal being the most severe actual log level. + * + * **Example** (Ordering log levels) + * + * ```ts + * import { LogLevel } from "effect" + * + * // Compare log levels using Order + * console.log(LogLevel.Order("Error", "Info")) // 1 (Error > Info) + * console.log(LogLevel.Order("Debug", "Error")) // -1 (Debug < Error) + * console.log(LogLevel.Order("Info", "Info")) // 0 (Info == Info) + * ``` + * + * @category ordering + * @since 2.0.0 + */ +export const Order: Ord.Order = effect.LogLevelOrder + +/** + * Equivalence instance for log levels using strict equality (`===`). + * + * **When to use** + * + * Use to compare two `LogLevel` values when only the exact same level should + * match. + * + * **Details** + * + * Each log level string, including `All` and `None`, only matches itself. + * + * **Example** (Comparing log levels) + * + * ```ts + * import { LogLevel } from "effect" + * + * console.log(LogLevel.Equivalence("Error", "Error")) // true + * console.log(LogLevel.Equivalence("Error", "Info")) // false + * ``` + * + * @see {@link Order} for severity ordering rather than exact level equality + * @see {@link isGreaterThanOrEqualTo} for minimum-threshold checks + * + * @category instances + * @since 4.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.strictEqual() + +/** + * Returns the ordinal value of the log level. + * + * **When to use** + * + * Use to project a `LogLevel` into the numeric sort key used by + * `LogLevel.Order` when custom ordering code or an integration needs a number + * instead of an `Order` comparison. + * + * **Details** + * + * The mapping is `All` to `Number.MIN_SAFE_INTEGER`, `Trace` to `0`, `Debug` to + * `10000`, `Info` to `20000`, `Warn` to `30000`, `Error` to `40000`, `Fatal` to + * `50000`, and `None` to `Number.MAX_SAFE_INTEGER`. + * + * **Gotchas** + * + * These ordinals are internal sort keys; do not treat them as external severity + * numbers. + * + * @see {@link Order} for comparing log levels without exposing numeric keys + * @see {@link isGreaterThanOrEqualTo} for minimum-threshold filtering + * + * @category ordering + * @since 4.0.0 + */ +export const getOrdinal = (self: LogLevel): number => effect.logLevelToOrder(self) + +/** + * Determines if the first log level is more severe than the second. + * + * **When to use** + * + * Use to check whether one log level is strictly more severe than another. + * + * **Details** + * + * Returns `true` if `self` represents a more severe level than `that`. + * + * **Example** (Checking higher severity) + * + * ```ts + * import { LogLevel } from "effect" + * + * // Check if Error is more severe than Info + * console.log(LogLevel.isGreaterThan("Error", "Info")) // true + * console.log(LogLevel.isGreaterThan("Debug", "Error")) // false + * + * // Use with filtering + * const isFatal = LogLevel.isGreaterThan("Fatal", "Warn") + * const isError = LogLevel.isGreaterThan("Error", "Warn") + * const isDebug = LogLevel.isGreaterThan("Debug", "Warn") + * console.log(isFatal) // true + * console.log(isError) // true + * console.log(isDebug) // false + * + * // Curried usage + * const isMoreSevereThanInfo = LogLevel.isGreaterThan("Info") + * console.log(isMoreSevereThanInfo("Error")) // true + * console.log(isMoreSevereThanInfo("Debug")) // false + * ``` + * + * @category ordering + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = effect.isLogLevelGreaterThan + +/** + * Determines if the first log level is more severe than or equal to the second. + * + * **When to use** + * + * Use to implement minimum log-level filtering by checking whether a message + * level meets a threshold. + * + * **Details** + * + * Returns `true` if `self` represents a level that is more severe than or equal to `that`. + * + * **Example** (Filtering by minimum log level) + * + * ```ts + * import { Logger, LogLevel } from "effect" + * + * // Check if level meets minimum threshold + * console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Error")) // true + * console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Info")) // true + * console.log(LogLevel.isGreaterThanOrEqualTo("Debug", "Info")) // false + * + * // Create a logger that only logs Info and above + * const infoLogger = Logger.make((options) => { + * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Info")) { + * console.log(`[${options.logLevel}] ${options.message}`) + * } + * }) + * + * // Production logger - only Error and Fatal + * const productionLogger = Logger.make((options) => { + * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Error")) { + * console.error( + * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` + * ) + * } + * }) + * + * // Curried usage for filtering + * const isInfoOrAbove = LogLevel.isGreaterThanOrEqualTo("Info") + * const shouldLog = isInfoOrAbove("Error") // true + * ``` + * + * @category ordering + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = Ord.isGreaterThanOrEqualTo(Order) + +/** + * Determines if the first log level is less severe than the second. + * + * **When to use** + * + * Use to check whether one log level is strictly less severe than another. + * + * **Details** + * + * Returns `true` if `self` represents a less severe level than `that`. + * + * **Example** (Checking lower severity) + * + * ```ts + * import { LogLevel } from "effect" + * + * // Check if Debug is less severe than Info + * console.log(LogLevel.isLessThan("Debug", "Info")) // true + * console.log(LogLevel.isLessThan("Error", "Info")) // false + * + * // Filter out verbose logs + * const isFatalVerbose = LogLevel.isLessThan("Fatal", "Info") + * const isErrorVerbose = LogLevel.isLessThan("Error", "Info") + * const isTraceVerbose = LogLevel.isLessThan("Trace", "Info") + * console.log(isFatalVerbose) // false (Fatal is not verbose) + * console.log(isErrorVerbose) // false (Error is not verbose) + * console.log(isTraceVerbose) // true (Trace is verbose) + * + * // Curried usage + * const isLessSevereThanError = LogLevel.isLessThan("Error") + * console.log(isLessSevereThanError("Info")) // true + * console.log(isLessSevereThanError("Fatal")) // false + * ``` + * + * @category ordering + * @since 4.0.0 + */ +export const isLessThan: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = Ord.isLessThan(Order) + +/** + * Determines if the first log level is less severe than or equal to the second. + * + * **When to use** + * + * Use to implement maximum log-level filtering by checking whether a level is + * at or below a threshold. + * + * **Details** + * + * Returns `true` if `self` represents a level that is less severe than or equal to `that`. + * + * **Example** (Filtering by maximum log level) + * + * ```ts + * import { Logger, LogLevel } from "effect" + * + * // Check if level is at or below threshold + * console.log(LogLevel.isLessThanOrEqualTo("Info", "Info")) // true + * console.log(LogLevel.isLessThanOrEqualTo("Debug", "Info")) // true + * console.log(LogLevel.isLessThanOrEqualTo("Error", "Info")) // false + * + * // Create a logger that suppresses verbose logs + * const quietLogger = Logger.make((options) => { + * if (LogLevel.isLessThanOrEqualTo(options.logLevel, "Info")) { + * console.log(`[${options.logLevel}] ${options.message}`) + * } + * }) + * + * // Development logger - suppress trace logs + * const devLogger = Logger.make((options) => { + * if (LogLevel.isLessThanOrEqualTo(options.logLevel, "Debug")) { + * console.log(`[${options.logLevel}] ${options.message}`) + * } + * }) + * + * // Curried usage for filtering + * const isInfoOrBelow = LogLevel.isLessThanOrEqualTo("Info") + * const shouldLog = isInfoOrBelow("Debug") // true + * ``` + * + * @category ordering + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: LogLevel): (self: LogLevel) => boolean + (self: LogLevel, that: LogLevel): boolean +} = Ord.isLessThanOrEqualTo(Order) + +/** + * Checks whether a given log level is enabled for the current fiber. + * + * **When to use** + * + * Use to check whether a log level would be emitted under the current fiber's + * minimum log level. + * + * **Details** + * + * A log level is enabled when it is greater than or equal to + * `References.MinimumLogLevel`. + * + * **Example** (Checking current fiber log level) + * + * ```ts + * import { Effect, LogLevel, References } from "effect" + * + * const program = Effect.gen(function*() { + * const debugEnabled = yield* LogLevel.isEnabled("Debug") + * const errorEnabled = yield* LogLevel.isEnabled("Error") + * + * console.log({ debugEnabled, errorEnabled }) + * }) + * + * const warnOnly = program.pipe( + * Effect.provideService(References.MinimumLogLevel, "Warn") + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const isEnabled = (self: LogLevel): Effect.Effect => + core.withFiber((fiber) => effect.succeed(!isGreaterThan(fiber.getRef(References.MinimumLogLevel), self))) diff --git a/repos/effect/packages/effect/src/Logger.ts b/repos/effect/packages/effect/src/Logger.ts new file mode 100644 index 0000000..9c37114 --- /dev/null +++ b/repos/effect/packages/effect/src/Logger.ts @@ -0,0 +1,1284 @@ +/** + * Defines loggers and log-event data for Effect programs. + * + * A `Logger` receives each log event as `Options` and turns it + * into output such as a formatted string, structured object, console write, + * file write, JSON line, or trace span event. This module also includes active + * logger references, console routing helpers, built-in formatters, batching, + * file logging, and layers for installing loggers. + * + * @since 2.0.0 + */ +import * as Array from "./Array.ts" +import type * as Cause from "./Cause.ts" +import type * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import type * as Effect from "./Effect.ts" +import type * as Fiber from "./Fiber.ts" +import * as FileSystem from "./FileSystem.ts" +import * as Formatter from "./Formatter.ts" +import { dual } from "./Function.ts" +import { isEffect, withFiber } from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as Layer from "./Layer.ts" +import type * as LogLevel from "./LogLevel.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { PlatformError } from "./PlatformError.ts" +import * as Predicate from "./Predicate.ts" +import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts" +import type * as Scope from "./Scope.ts" + +const TypeId = "~effect/Logger" + +/** + * A logger that transforms a runtime log event into an output value. + * + * **Details** + * + * The runtime calls `log` with the message, level, cause, fiber, and timestamp + * for each log event. Use `Logger.layer` to install one or more loggers for an + * effect. + * + * **Example** (Creating custom loggers) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Create a custom logger that accepts unknown messages and returns void + * const stringLogger = Logger.make((options) => { + * console.log(`[${options.logLevel}] ${options.message}`) + * }) + * + * // Create a logger that accepts any message type and returns a formatted string + * const formattedLogger = Logger.make((options) => + * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` + * ) + * + * // Use the logger in an Effect program + * const program = Effect.log("Hello World").pipe( + * Effect.provide(Logger.layer([stringLogger])) + * ) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Logger extends Pipeable { + readonly [TypeId]: typeof TypeId + log(options: Options): Output +} + +/** + * Information supplied to a `Logger` for a single log event. + * + * **Details** + * + * Includes the logged message, log level, cause, current fiber, and timestamp. + * + * **Example** (Accessing logger options) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Options interface provides all logging context + * const detailedLogger = Logger.make((options) => { + * const output = { + * message: options.message, + * level: options.logLevel, + * timestamp: options.date.toISOString(), + * fiberId: options.fiber.id, + * hasCause: options.cause !== undefined + * } + * console.log(JSON.stringify(output)) + * }) + * + * const program = Effect.log("Processing request").pipe( + * Effect.provide(Logger.layer([detailedLogger])) + * ) + * ``` + * + * @category options + * @since 2.0.0 + */ +export interface Options { + readonly message: Message + readonly logLevel: LogLevel.LogLevel + readonly cause: Cause.Cause + readonly fiber: Fiber.Fiber + readonly date: Date +} + +/** + * Returns `true` if the specified value is a `Logger`, otherwise returns `false`. + * + * **Example** (Checking logger values) + * + * ```ts + * import { Logger } from "effect" + * + * const myLogger = Logger.make((options) => { + * console.log(options.message) + * }) + * + * console.log(Logger.isLogger(myLogger)) // true + * console.log(Logger.isLogger("not a logger")) // false + * console.log(Logger.isLogger({ log: () => {} })) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isLogger = (u: unknown): u is Logger => Predicate.hasProperty(u, TypeId) + +/** + * Context reference containing the active loggers for the current fiber. + * + * **Details** + * + * By default this set includes the default logger and the tracer logger. + * Providing `Logger.layer` replaces or merges with this set depending on its + * options. + * + * **Example** (Accessing current loggers) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Access current loggers from fiber context + * const program = Effect.gen(function*() { + * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers) + * console.log(`Number of active loggers: ${currentLoggers.size}`) + * + * // Add a custom logger to the set + * const customLogger = Logger.make((options) => { + * console.log(`Custom: ${options.message}`) + * }) + * + * yield* Effect.log("Hello from custom logger").pipe( + * Effect.provide(Logger.layer([customLogger])) + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentLoggers: Context.Reference>> = effect.CurrentLoggers + +/** + * Context reference that routes the built-in default logger and TTY pretty + * console logger to stderr. + * + * **When to use** + * + * Use to route built-in logger output to stderr while keeping stdout reserved + * for protocol messages or data output. + * + * **Details** + * + * The reference defaults to `false`. Providing `true` makes the affected + * loggers call `console.error` instead of `console.log`. + * + * @see {@link defaultLogger} for the runtime logger affected by this reference + * @see {@link consolePretty} for the TTY-mode pretty console logger affected by this reference + * @see {@link withConsoleError} for routing a specific formatter logger to `console.error` + * + * @category references + * @since 4.0.0 + */ +export const LogToStderr: Context.Reference = effect.LogToStderr + +/** + * Transforms the output of a `Logger` using the provided function. + * + * **When to use** + * + * Use when an existing logger's output should be transformed without recreating the + * logging logic. + * + * **Example** (Transforming logger output) + * + * ```ts + * import { Logger } from "effect" + * + * // Create a logger that outputs objects + * const structuredLogger = Logger.make((options) => ({ + * level: options.logLevel, + * message: options.message, + * timestamp: options.date.toISOString() + * })) + * + * // Transform the output to JSON strings + * const jsonStringLogger = Logger.map( + * structuredLogger, + * (output) => JSON.stringify(output) + * ) + * + * // Transform to uppercase messages + * const uppercaseLogger = Logger.map( + * structuredLogger, + * (output) => ({ ...output, message: String(output.message).toUpperCase() }) + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map = dual< + ( + f: (output: Output) => Output2 + ) => ( + self: Logger + ) => Logger, + ( + self: Logger, + f: (output: Output) => Output2 + ) => Logger +>(2, (self, f) => effect.loggerMake((options) => f(self.log(options)))) + +/** + * Returns a new `Logger` that writes all output of the specified `Logger` to + * the console using `console.log`. + * + * **When to use** + * + * Use when a logger's string or object output should be routed to `console.log` for + * development or debugging. + * + * **Example** (Writing logger output with console.log) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Create a custom formatter + * const customFormatter = Logger.make((options) => + * `[${options.date.toISOString()}] ${options.logLevel}: ${options.message}` + * ) + * + * // Route to console + * const consoleLogger = Logger.withConsoleLog(customFormatter) + * + * const program = Effect.log("Hello World").pipe( + * Effect.provide(Logger.layer([consoleLogger])) + * ) + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const withConsoleLog = ( + self: Logger +): Logger => + effect.loggerMake((options) => { + const console = options.fiber.getRef(effect.ConsoleRef) + return console.log(self.log(options)) + }) +/** + * Returns a new `Logger` that writes all output of the specified `Logger` to + * the console using `console.error`. + * + * **When to use** + * + * Use when logger output should be routed to `console.error`, such as error logs that + * should appear on stderr instead of stdout. + * + * **Example** (Writing logger output with console.error) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Create an error-specific formatter + * const errorFormatter = Logger.make((options) => + * `ERROR [${options.date.toISOString()}]: ${options.message}` + * ) + * + * // Route to console.error + * const errorLogger = Logger.withConsoleError(errorFormatter) + * + * const program = Effect.logError("Database connection failed").pipe( + * Effect.provide(Logger.layer([errorLogger])) + * ) + * ``` + * + * @category logging + * @since 2.0.0 + */ +export const withConsoleError = ( + self: Logger +): Logger => + effect.loggerMake((options) => { + const console = options.fiber.getRef(effect.ConsoleRef) + return console.error(self.log(options)) + }) +/** + * Returns a new `Logger` that writes all output of the specified `Logger` to + * the console. + * + * **Details** + * + * Will use the appropriate console method (i.e. `console.log`, `console.error`, + * etc.) based upon the current `LogLevel`. + * + * `Debug` uses `console.debug`, `Info` uses `console.info`, `Trace` uses + * `console.trace`, `Warn` uses `console.warn`, `Error` and `Fatal` use + * `console.error`, and all other levels use `console.log`. + * + * **Example** (Writing logs with level-based console methods) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * const formatter = Logger.make((options) => + * `[${options.logLevel}] ${options.message}` + * ) + * + * const leveledLogger = Logger.withLeveledConsole(formatter) + * + * const program = Effect.gen(function*() { + * yield* Effect.logInfo("Info message") // -> console.info + * yield* Effect.logWarning("Warning") // -> console.warn + * yield* Effect.logError("Error occurred") // -> console.error + * yield* Effect.logDebug("Debug info") // -> console.debug + * }).pipe( + * Effect.provide(Logger.layer([leveledLogger])) + * ) + * ``` + * + * @category logging + * @since 3.8.0 + */ +export const withLeveledConsole = ( + self: Logger +): Logger => + effect.loggerMake((options) => { + const console = options.fiber.getRef(effect.ConsoleRef) + const output = self.log(options) + switch (options.logLevel) { + case "Debug": + return console.debug(output) + case "Info": + return console.info(output) + case "Trace": + return console.trace(output) + case "Warn": + return console.warn(output) + case "Error": + case "Fatal": + return console.error(output) + default: + return console.log(output) + } + }) + +/** + * Match strings that do not contain any whitespace characters, double quotes, + * or equal signs. + */ +const textOnly = /^[^\s"=]*$/ + +/** + * Escapes double quotes in a string. + */ +const escapeDoubleQuotes = (s: string) => `"${s.replace(/\\([\s\S])|(")/g, "\\$1$2")}"` + +/** + * Formats the identifier of a `Fiber` by prefixing it with a hash tag. + */ +const formatFiberId = (fiberId: number) => `#${fiberId}` + +/** + * Used by both {@link formatSimple} and {@link formatLogFmt} to render a log + * message. + * + * @internal + */ +const format = ( + quoteValue: (s: string) => string, + space?: number | string | undefined +) => +({ cause, date, fiber, logLevel, message }: Options): string => { + const formatUnknown = (value: unknown): string => + typeof value === "string" ? value : Formatter.format(value, { space }) + const formatValue = (value: string): string => value.match(textOnly) ? value : quoteValue(value) + const format = (label: string, value: string): string => `${effect.formatLabel(label)}=${formatValue(value)}` + const append = (label: string, value: string): string => " " + format(label, value) + + let out = format("timestamp", date.toISOString()) + out += append("level", logLevel) + out += append("fiber", formatFiberId(fiber.id)) + + const messages = Array.ensure(message) + for (let i = 0; i < messages.length; i++) { + out += append("message", formatUnknown(messages[i])) + } + + if (cause.reasons.length > 0) { + out += append("cause", effect.causePretty(cause)) + } + + const now = date.getTime() + const spans = fiber.getRef(CurrentLogSpans) + for (const span of spans) { + out += " " + effect.formatLogSpan(span, now) + } + + const annotations = fiber.getRef(CurrentLogAnnotations) + for (const [label, value] of Object.entries(annotations)) { + out += append(label, formatUnknown(value)) + } + + return out +} + +/** + * Creates a new `Logger` from a log function. + * + * **Details** + * + * The log function receives an options object containing the message, log level, + * cause, fiber information, and timestamp, and should return the desired output. + * + * **Example** (Creating loggers from functions) + * + * ```ts + * import { Effect, Logger, References } from "effect" + * + * // Simple text logger + * const textLogger = Logger.make((options) => + * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` + * ) + * + * // Structured object logger + * const objectLogger = Logger.make((options) => ({ + * timestamp: options.date.toISOString(), + * level: options.logLevel, + * message: options.message, + * fiberId: options.fiber.id, + * annotations: options.fiber.getRef(References.CurrentLogAnnotations) + * })) + * + * // Custom filtering logger + * const filteredLogger = Logger.make((options) => { + * if (options.logLevel === "Debug") { + * return // Skip debug messages + * } + * return `${options.logLevel}: ${options.message}` + * }) + * + * const program = Effect.log("Hello World").pipe( + * Effect.provide(Logger.layer([textLogger])) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: ( + log: (options: Options) => Output +) => Logger = effect.loggerMake + +/** + * The default logging implementation used by the Effect runtime. + * + * **Example** (Referencing the default logger) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the default logger (automatically used by Effect runtime) + * const program = Effect.gen(function*() { + * yield* Effect.log("This uses the default logger") + * yield* Effect.logInfo("Info message") + * yield* Effect.logError("Error message") + * }) + * + * // Explicitly use the default logger + * const withDefaultLogger = Effect.log("Explicit default").pipe( + * Effect.provide(Logger.layer([Logger.defaultLogger])) + * ) + * + * // Compare with custom logger + * const customLogger = Logger.make((options) => { + * console.log(`CUSTOM: ${options.message}`) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const defaultLogger: Logger = effect.defaultLogger + +/** + * A `Logger` which outputs logs as a string. + * + * **Details** + * + * For example, a simple log entry is rendered as + * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`. + * + * **Example** (Formatting logs as simple strings) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the simple format logger + * const simpleLoggerProgram = Effect.log("Hello Simple Format").pipe( + * Effect.provide(Logger.layer([Logger.formatSimple])) + * ) + * + * // Combine with console output + * const consoleSimpleLogger = Logger.withConsoleLog(Logger.formatSimple) + * + * const program = Effect.gen(function*() { + * yield* Effect.log("Application started") + * yield* Effect.logInfo("Processing data") + * yield* Effect.logWarning("Memory usage high") + * }).pipe( + * Effect.provide(Logger.layer([consoleSimpleLogger])) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const formatSimple = effect.loggerMake(format(escapeDoubleQuotes)) + +/** + * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) + * style. + * + * **Details** + * + * For example, a logfmt entry is rendered as + * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`. + * + * **Example** (Formatting logs as logfmt) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the logfmt format logger + * const logfmtLoggerProgram = Effect.log("Hello LogFmt Format").pipe( + * Effect.provide(Logger.layer([Logger.formatLogFmt])) + * ) + * + * // Perfect for structured logging systems + * const structuredProgram = Effect.gen(function*() { + * yield* Effect.log("User login", { userId: 123, method: "OAuth" }) + * yield* Effect.logInfo("Request processed", { + * duration: 45, + * status: "success" + * }) + * }).pipe( + * Effect.provide(Logger.layer([Logger.withConsoleLog(Logger.formatLogFmt)])) + * ) + * + * // Good for log aggregation systems like Splunk, ELK + * const productionLogger = Logger.formatLogFmt + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const formatLogFmt = effect.loggerMake(format(JSON.stringify, 0)) + +/** + * A `Logger` which outputs logs using a structured format. + * + * **Details** + * + * For example, a structured entry can contain `message: [ "hello" ]`, + * `level: "INFO"`, `timestamp: "2025-01-03T14:25:39.666Z"`, + * `annotations: { key: "value" }`, `spans: { label: 0 }`, and + * `fiberId: "#1"`. + * + * **Example** (Formatting logs as structured objects) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the structured format logger + * const structuredLoggerProgram = Effect.log("Hello Structured Format").pipe( + * Effect.provide(Logger.layer([Logger.formatStructured])) + * ) + * + * // Perfect for JSON processing and analytics + * const analyticsProgram = Effect.gen(function*() { + * yield* Effect.log("User action", { action: "click", element: "button" }) + * yield* Effect.logInfo("API call", { endpoint: "/users", duration: 150 }) + * }).pipe( + * Effect.annotateLogs("sessionId", "abc123"), + * Effect.withLogSpan("request"), + * Effect.provide(Logger.layer([Logger.formatStructured])) + * ) + * + * // Process structured output + * const processingLogger = Logger.map(Logger.formatStructured, (output) => { + * // Process the structured object + * const enhanced = { ...output, processed: true } + * return enhanced + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const formatStructured: Logger + readonly spans: Record +}> = effect.loggerMake(({ cause, date, fiber, logLevel, message }) => { + const annotationsObj: Record = {} + const spansObj: Record = {} + + const annotations = fiber.getRef(CurrentLogAnnotations) + for (const [key, value] of Object.entries(annotations)) { + annotationsObj[key] = effect.structuredMessage(value) + } + + const now = date.getTime() + const spans = fiber.getRef(CurrentLogSpans) + for (const [label, timestamp] of spans) { + spansObj[label] = now - timestamp + } + + const messageArr = Array.ensure(message) + return { + message: messageArr.length === 1 + ? effect.structuredMessage(messageArr[0]) + : messageArr.map(effect.structuredMessage), + level: logLevel.toUpperCase(), + timestamp: date.toISOString(), + cause: cause.reasons.length > 0 ? effect.causePretty(cause) : undefined, + annotations: annotationsObj, + spans: spansObj, + fiberId: formatFiberId(fiber.id) + } +}) + +/** + * A `Logger` which outputs logs using a structured format serialized as JSON + * on a single line. + * + * **Details** + * + * For example, a JSON entry can render as `{"message":["hello"],"level":"INFO", + * "timestamp":"2025-01-03T14:28:57.508Z","annotations":{"key":"value"}, + * "spans":{"label":0},"fiberId":"#1"}`. + * + * **Example** (Formatting logs as JSON) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the JSON format logger + * const jsonLoggerProgram = Effect.log("Hello JSON Format").pipe( + * Effect.provide(Logger.layer([Logger.formatJson])) + * ) + * + * // Perfect for log aggregation and processing systems + * const productionProgram = Effect.gen(function*() { + * yield* Effect.log("Server started", { port: 3000, env: "production" }) + * yield* Effect.logInfo("Request received", { + * method: "GET", + * path: "/api/users" + * }) + * yield* Effect.logError("Database error", { error: "Connection timeout" }) + * }).pipe( + * Effect.annotateLogs("service", "api-server"), + * Effect.withLogSpan("request-processing"), + * Effect.provide(Logger.layer([Logger.formatJson])) + * ) + * + * // Adapt the JSON string before giving it to an output sink + * const envelopedJsonLogger = Logger.map( + * Logger.formatJson, + * (jsonString) => `{"service":"api-server","entry":${jsonString}}` + * ) + * + * const envelopedConsoleLogger = Logger.withConsoleLog(envelopedJsonLogger) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const formatJson = map(formatStructured, Formatter.formatJson) + +/** + * Creates a scoped logger that batches the output of another logger. + * + * **Details** + * + * The returned effect starts a scoped background process that periodically + * passes buffered outputs to `flush`. When the scope closes, the background + * process is interrupted and any remaining buffered entries are flushed. + * + * **Example** (Batching logger output) + * + * ```ts + * import { Duration, Effect, Logger } from "effect" + * + * // Create a batched logger that flushes every 5 seconds + * const batchedLogger = Logger.batched(Logger.formatJson, { + * window: Duration.seconds(5), + * flush: (messages) => + * Effect.sync(() => { + * console.log(`Flushing ${messages.length} log entries:`) + * messages.forEach((msg, i) => console.log(`${i + 1}. ${msg}`)) + * }) + * }) + * + * const program = Effect.gen(function*() { + * const logger = yield* batchedLogger + * + * yield* Effect.provide( + * Effect.all([ + * Effect.log("Event 1"), + * Effect.log("Event 2"), + * Effect.log("Event 3"), + * Effect.sleep(Duration.seconds(6)), // Trigger flush + * Effect.log("Event 4") + * ]), + * Logger.layer([logger]) + * ) + * }) + * + * // Remote batch logging example + * const remoteBatchLogger = Logger.batched(Logger.formatStructured, { + * window: Duration.seconds(10), + * flush: (entries) => + * Effect.sync(() => { + * // Send batch to remote logging service + * console.log(`Sending ${entries.length} log entries to remote service`) + * }) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const batched = dual< + (options: { + readonly window: Duration.Input + readonly flush: (messages: Array>) => Effect.Effect + }) => ( + self: Logger + ) => Effect.Effect, never, Scope.Scope>, + ( + self: Logger, + options: { + readonly window: Duration.Input + readonly flush: (messages: Array>) => Effect.Effect + } + ) => Effect.Effect, never, Scope.Scope> +>(2, ( + self: Logger, + options: { + readonly window: Duration.Input + readonly flush: (messages: Array>) => Effect.Effect + } +): Effect.Effect, never, Scope.Scope> => + effect.flatMap(effect.scope, (scope) => { + let buffer: Array = [] + const flush = effect.suspend(() => { + if (buffer.length === 0) { + return effect.void + } + const arr = buffer + buffer = [] + return options.flush(arr) + }) + + return effect.uninterruptibleMask((restore) => + restore( + effect.sleep(options.window).pipe( + effect.andThen(flush), + effect.forever + ) + ).pipe( + effect.forkDetach, + effect.flatMap((fiber) => effect.scopeAddFinalizerExit(scope, () => effect.fiberInterrupt(fiber))), + effect.andThen(effect.addFinalizer(() => flush)), + effect.as( + effect.loggerMake((options) => { + buffer.push(self.log(options)) + }) + ) + ) + ) + })) + +/** + * A `Logger` which outputs logs in a "pretty" format and writes them to the + * console. + * + * **Details** + * + * For example, pretty output can render as + * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line + * such as `key: value`. + * + * **Example** (Logging with pretty console output) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the pretty console logger with default settings + * const basicPretty = Effect.log("Hello Pretty Format").pipe( + * Effect.provide(Logger.layer([Logger.consolePretty()])) + * ) + * + * // Configure pretty logger options + * const customPretty = Logger.consolePretty({ + * colors: true, + * stderr: false, + * mode: "tty", + * formatDate: (date) => date.toLocaleTimeString() + * }) + * + * // Perfect for development environment + * const developmentProgram = Effect.gen(function*() { + * yield* Effect.log("Application starting") + * yield* Effect.logInfo("Database connected") + * yield* Effect.logWarning("High memory usage detected") + * }).pipe( + * Effect.annotateLogs("environment", "development"), + * Effect.withLogSpan("startup"), + * Effect.provide(Logger.layer([customPretty])) + * ) + * + * // Disable colors for CI/CD environments + * const ciLogger = Logger.consolePretty({ colors: false }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const consolePretty: ( + options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined + } +) => Logger = effect.consolePretty + +/** + * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) + * style and writes them to the console. + * + * **Details** + * + * For example, a console logfmt entry is rendered as + * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=info`. + * + * **Example** (Logging logfmt output to the console) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the console logfmt logger + * const logfmtProgram = Effect.log("Hello LogFmt Console").pipe( + * Effect.provide(Logger.layer([Logger.consoleLogFmt])) + * ) + * + * // Great for production environments + * const productionProgram = Effect.gen(function*() { + * yield* Effect.log("Server started", { port: 8080, version: "1.0.0" }) + * yield* Effect.logInfo("Request processed", { userId: 123, duration: 45 }) + * yield* Effect.logError("Validation failed", { + * field: "email", + * value: "invalid" + * }) + * }).pipe( + * Effect.annotateLogs("service", "api"), + * Effect.withLogSpan("request-handler"), + * Effect.provide(Logger.layer([Logger.consoleLogFmt])) + * ) + * + * // Combine with other loggers + * const multiLoggerLive = Logger.layer([ + * Logger.consoleLogFmt, + * Logger.consolePretty() + * ]) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const consoleLogFmt: Logger = withConsoleLog(formatLogFmt) + +/** + * A `Logger` which outputs logs using a structured format and writes them to + * the console. + * + * **Details** + * + * For example, console structured output can contain + * `message: [ "info", "message" ]`, `level: "INFO"`, + * `timestamp: "2025-01-03T14:25:39.666Z"`, + * `annotations: { key: "value" }`, `spans: { label: 0 }`, and + * `fiberId: "#1"`. + * + * **Example** (Logging structured output to the console) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the console structured logger + * const structuredProgram = Effect.log("Hello Structured Console").pipe( + * Effect.provide(Logger.layer([Logger.consoleStructured])) + * ) + * + * // Perfect for development debugging + * const debugProgram = Effect.gen(function*() { + * yield* Effect.log("User event", { + * userId: 123, + * action: "login", + * ip: "192.168.1.1" + * }) + * yield* Effect.logInfo("API call", { + * endpoint: "/users", + * method: "GET", + * duration: 120 + * }) + * }).pipe( + * Effect.annotateLogs("requestId", "req-123"), + * Effect.withLogSpan("authentication"), + * Effect.provide(Logger.layer([Logger.consoleStructured])) + * ) + * + * // Easy to parse and inspect object structure + * const inspectionProgram = Effect.gen(function*() { + * yield* Effect.log("Complex data", { + * user: { id: 1, name: "John" }, + * metadata: { source: "api", version: 2 } + * }) + * }).pipe( + * Effect.provide(Logger.layer([Logger.consoleStructured])) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const consoleStructured: Logger = withConsoleLog(formatStructured) + +/** + * A `Logger` which outputs logs using a structured format serialized as JSON + * on a single line and writes them to the console. + * + * **Details** + * + * For example, console JSON output can render as + * `{"message":["hello"],"level":"INFO","timestamp":"2025-01-03T14:28:57.508Z", + * "annotations":{"key":"value"},"spans":{"label":0},"fiberId":"#1"}`. + * + * **Example** (Logging JSON output to the console) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Use the console JSON logger + * const jsonProgram = Effect.log("Hello JSON Console").pipe( + * Effect.provide(Logger.layer([Logger.consoleJson])) + * ) + * + * // Perfect for production logging and log aggregation + * const productionProgram = Effect.gen(function*() { + * yield* Effect.log("Server started", { port: 3000, env: "production" }) + * yield* Effect.logInfo("Request", { + * method: "POST", + * url: "/api/users", + * body: { name: "Alice" } + * }) + * yield* Effect.logError("Database error", { + * error: "Connection timeout", + * retryCount: 3 + * }) + * }).pipe( + * Effect.annotateLogs("service", "user-api"), + * Effect.annotateLogs("version", "1.2.3"), + * Effect.withLogSpan("request-processing"), + * Effect.provide(Logger.layer([Logger.consoleJson])) + * ) + * + * // Easy to pipe to log aggregation services + * const productionSetup = Logger.layer([ + * Logger.consoleJson, // For stdout JSON logs + * Logger.consolePretty() // For local debugging + * ]) + * + * // Ideal for containerized environments (Docker, Kubernetes) + * const containerProgram = Effect.log("Container ready", { + * containerId: "abc123", + * image: "myapp:latest" + * }).pipe( + * Effect.provide(Logger.layer([Logger.consoleJson])) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const consoleJson: Logger = withConsoleLog(formatJson) + +/** + * A `Logger` which includes log messages as tracer span events. + * + * **Details** + * + * This logger integrates logging with distributed tracing by recording + * all log messages as events on the current trace span, making them visible + * in tracing tools like OpenTelemetry, Jaeger, or Zipkin. + * + * This logger is included in the default set of loggers for all Effect programs, + * so log messages automatically appear as span events unless you override the + * default loggers. + * + * **Example** (Recording logs as trace span events) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Tracer logger is included by default - logs automatically become span events + * const defaultProgram = Effect.gen(function*() { + * yield* Effect.log("This automatically becomes a span event") + * yield* Effect.logInfo("Processing data") + * }) + * + * // Explicitly combine tracer logger with other loggers + * const observabilityProgram = Effect.gen(function*() { + * yield* Effect.log("Operation started") + * yield* Effect.logInfo("Processing data") + * yield* Effect.logError("Error occurred") + * }).pipe( + * Effect.withLogSpan("data-processing"), + * Effect.provide(Logger.layer([ + * Logger.tracerLogger, + * Logger.consoleJson + * ])) + * ) + * + * // Perfect for correlating logs with traces in distributed systems + * const distributedProgram = Effect.gen(function*() { + * yield* Effect.log("Step 1: Fetching user data") + * yield* Effect.sleep("100 millis") + * yield* Effect.log("Step 2: Processing payment") + * yield* Effect.sleep("200 millis") + * yield* Effect.log("Step 3: Sending confirmation") + * }).pipe( + * Effect.withLogSpan("payment-workflow"), + * Effect.annotateLogs("userId", "user-123"), + * Effect.provide(Logger.layer([Logger.tracerLogger])) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const tracerLogger: Logger = effect.tracerLogger + +/** + * Creates a `Layer` which will overwrite the current set of loggers with the + * specified array of `loggers`. + * + * **Details** + * + * If the specified array of `loggers` should be _merged_ with the current set + * of loggers (instead of overwriting them), set `mergeWithExisting` to `true`. + * + * **Example** (Providing logger layers) + * + * ```ts + * import { Effect, Logger } from "effect" + * + * // Single logger layer + * const JsonLoggerLive = Logger.layer([Logger.consoleJson]) + * + * // Multiple loggers layer + * const MultiLoggerLive = Logger.layer([ + * Logger.consoleJson, + * Logger.consolePretty(), + * Logger.formatStructured + * ]) + * + * // Merge with existing loggers + * const AdditionalLoggerLive = Logger.layer( + * [Logger.consoleJson], + * { mergeWithExisting: true } + * ) + * + * // Using multiple logger formats + * const jsonLogger = Logger.consoleJson + * const prettyLogger = Logger.consolePretty() + * + * const CustomLoggerLive = Logger.layer([jsonLogger, prettyLogger]) + * + * const program = Effect.log("Application started").pipe( + * Effect.provide(CustomLoggerLive) + * ) + * ``` + * + * @category context + * @since 4.0.0 + */ +export const layer = < + const Loggers extends ReadonlyArray | Effect.Effect, any, any>> +>( + loggers: Loggers, + options?: { readonly mergeWithExisting?: boolean | undefined } | undefined +): Layer.Layer< + never, + Loggers extends readonly [] ? never : Effect.Error, + Exclude< + Loggers extends readonly [] ? never : Effect.Services, + Scope.Scope + > +> => + Layer.effect( + CurrentLoggers, + withFiber(effect.fnUntraced(function*(fiber) { + const currentLoggers = new Set(options?.mergeWithExisting === true ? fiber.getRef(effect.CurrentLoggers) : []) + for (const logger of loggers) { + currentLoggers.add(isEffect(logger) ? yield* logger : logger) + } + return currentLoggers + })) + ) + +/** + * Creates a scoped logger that writes string logger output to a file. + * + * **Details** + * + * The returned effect requires `FileSystem` and `Scope`. The file logger batches + * string output, writes each batch to the specified path, and flushes remaining + * entries when the scope closes. + * + * **Example** (Writing JSON logs to a file) + * + * ```ts + * import { Effect, Layer, Logger } from "effect" + * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + * + * const fileLogger = Logger.formatJson.pipe( + * Logger.toFile("/tmp/log.txt") + * ) + * const LoggerLive = Logger.layer([fileLogger]).pipe( + * Layer.provide(NodeFileSystem.layer) + * ) + * + * Effect.log("a").pipe( + * Effect.andThen(Effect.log("b")), + * Effect.andThen(Effect.log("c")), + * Effect.provide(LoggerLive), + * NodeRuntime.runMain + * ) + * ``` + * + * **Example** (Writing logs to files) + * + * ```ts + * import { Duration, Effect, Logger } from "effect" + * import { NodeFileSystem } from "@effect/platform-node" + * + * // Basic file logging. The scope keeps the file open while logs are emitted + * // and flushes pending entries when it closes. + * const basicFileLogger = Effect.scoped( + * Effect.gen(function*() { + * const fileLogger = yield* Logger.formatJson.pipe( + * Logger.toFile("/tmp/app.log") + * ) + * + * yield* Effect.log("Application started").pipe( + * Effect.provide(Logger.layer([fileLogger])) + * ) + * }) + * ).pipe( + * Effect.provide(NodeFileSystem.layer) + * ) + * + * // File logger with custom batch window + * const batchedFileLogger = Effect.scoped( + * Effect.gen(function*() { + * const fileLogger = yield* Logger.formatLogFmt.pipe( + * Logger.toFile("/var/log/myapp.log", { + * flag: "a", + * batchWindow: Duration.seconds(5) + * }) + * ) + * + * yield* Effect.all([ + * Effect.log("Event 1"), + * Effect.log("Event 2"), + * Effect.log("Event 3") + * ]).pipe( + * Effect.provide(Logger.layer([fileLogger])) + * ) + * }) + * ).pipe( + * Effect.provide(NodeFileSystem.layer) + * ) + * + * // Multiple loggers: console + file + * const multiLogger = Effect.scoped( + * Effect.gen(function*() { + * const fileLogger = yield* Logger.formatJson.pipe( + * Logger.toFile("/tmp/production.log") + * ) + * + * const loggerLive = Logger.layer([ + * Logger.consolePretty(), + * fileLogger + * ]) + * + * yield* Effect.log("Production event").pipe( + * Effect.provide(loggerLive) + * ) + * }) + * ).pipe( + * Effect.provide(NodeFileSystem.layer) + * ) + * ``` + * + * @category file + * @since 4.0.0 + */ +export const toFile = dual< + ( + path: string, + options?: { + readonly flag?: FileSystem.OpenFlag | undefined + readonly mode?: number | undefined + readonly batchWindow?: Duration.Input | undefined + } | undefined + ) => ( + self: Logger + ) => Effect.Effect, PlatformError, Scope.Scope | FileSystem.FileSystem>, + ( + self: Logger, + path: string, + options?: { + readonly flag?: FileSystem.OpenFlag | undefined + readonly mode?: number | undefined + readonly batchWindow?: Duration.Input | undefined + } | undefined + ) => Effect.Effect, PlatformError, Scope.Scope | FileSystem.FileSystem> +>( + (args) => isLogger(args[0]), + (self, path, options) => + effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const logFile = yield* fs.open(path, { flag: "a+", ...options }) + const encoder = new TextEncoder() + return yield* batched(self, { + window: options?.batchWindow ?? 1000, + flush: (output) => effect.ignore(logFile.write(encoder.encode(output.join("\n") + "\n"))) + }) + }) +) diff --git a/repos/effect/packages/effect/src/ManagedRuntime.ts b/repos/effect/packages/effect/src/ManagedRuntime.ts new file mode 100644 index 0000000..f3252b7 --- /dev/null +++ b/repos/effect/packages/effect/src/ManagedRuntime.ts @@ -0,0 +1,381 @@ +/** + * Runs many effects against services built once from a `Layer`. + * + * A `ManagedRuntime` builds the services from a layer, keeps those services + * available for repeated effect runs, and releases acquired resources when it + * is disposed. This module includes the runtime type, a constructor, a guard, + * and runners for connecting Effect programs to JavaScript entry points such as + * promises, callbacks, and synchronous code. + * + * @since 2.0.0 + */ +import type * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import * as Layer from "./Layer.ts" +import { hasProperty } from "./Predicate.ts" +import * as Scope from "./Scope.ts" +import type { Mutable } from "./Types.ts" + +const TypeId = "~effect/ManagedRuntime" + +/** + * Checks whether the provided argument is a `ManagedRuntime`. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `ManagedRuntime`. + * + * **Details** + * + * The guard checks the internal `ManagedRuntime` marker property. It does not + * build the layer or inspect the runtime's services. + * + * **Gotchas** + * + * Disposed runtimes still carry the marker, so this guard does not prove the + * runtime is still usable. + * + * @see {@link make} for creating managed runtimes this guard recognizes + * + * @category guards + * @since 3.9.0 + */ +export const isManagedRuntime = (input: unknown): input is ManagedRuntime => + hasProperty(input, TypeId) + +/** + * Type helpers associated with `ManagedRuntime`. + * + * **When to use** + * + * Use to reference type-level helpers for extracting managed runtime services + * and layer errors. + * + * @since 3.4.0 + */ +export declare namespace ManagedRuntime { + /** + * Extracts the services available from a `ManagedRuntime`. + * + * **When to use** + * + * Use to derive the service requirements provided by an existing + * `ManagedRuntime` type. + * + * @category utility types + * @since 3.4.0 + */ + export type Services> = [T] extends [ManagedRuntime] ? R + : never + /** + * Extracts the layer construction error type of a `ManagedRuntime`. + * + * **When to use** + * + * Use to derive the layer construction error type from an existing + * `ManagedRuntime` type. + * + * @category utility types + * @since 3.4.0 + */ + export type Error> = [T] extends [ManagedRuntime] ? E : never +} + +/** + * A runtime built from a layer that can execute effects requiring that layer's + * services. + * + * **When to use** + * + * Use as the reusable runtime value returned by `make` when application entry + * points or integration code need to run many effects against the same + * layer-built services. + * + * **Details** + * + * The runtime builds and caches its service context and owns the scope for + * resources acquired by the layer. + * + * **Gotchas** + * + * Dispose the runtime with `dispose` or `disposeEffect` when it is no longer + * needed. + * + * @see {@link make} for constructing a managed runtime from a layer + * @see {@link Layer.build} for lower-level scoped layer construction + * + * @category models + * @since 2.0.0 + */ +export interface ManagedRuntime { + readonly [TypeId]: typeof TypeId + readonly memoMap: Layer.MemoMap + readonly contextEffect: Effect.Effect, ER> + readonly context: () => Promise> + + // internal + readonly scope: Scope.Closeable + // internal + cachedContext: Context.Context | undefined + + /** + * Executes the effect using the provided Scheduler or using the global + * Scheduler if not provided + * + * **When to use** + * + * Use to fork an effect against this runtime's services and get the running + * fiber. + */ + readonly runFork: ( + self: Effect.Effect, + options?: Effect.RunOptions + ) => Fiber.Fiber + + /** + * Executes the effect synchronously returning the exit. + * + * **When to use** + * + * Use when invoking this effectful method at the edges of your + * program. + */ + readonly runSyncExit: (effect: Effect.Effect) => Exit.Exit + + /** + * Executes the effect synchronously throwing in case of errors or async boundaries. + * + * **When to use** + * + * Use when invoking this effectful method at the edges of your + * program. + */ + readonly runSync: (effect: Effect.Effect) => A + + /** + * Executes the effect asynchronously, eventually passing the exit value to + * the specified callback. + * + * **When to use** + * + * Use when invoking this effectful method at the edges of your + * program. + */ + readonly runCallback: ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onExit: (exit: Exit.Exit) => void + } + | undefined + ) => (interruptor?: number | undefined) => void + + /** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the value of the effect once the effect has been executed, or will be + * rejected with the first error or exception throw by the effect. + * + * **When to use** + * + * Use when invoking this effectful method at the edges of your + * program. + */ + readonly runPromise: (effect: Effect.Effect, options?: Effect.RunOptions) => Promise + + /** + * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved + * with the `Exit` state of the effect once the effect has been executed. + * + * **When to use** + * + * Use when invoking this effectful method at the edges of your + * program. + */ + readonly runPromiseExit: ( + effect: Effect.Effect, + options?: Effect.RunOptions + ) => Promise> + + /** + * Dispose of the resources associated with the runtime. + * + * **When to use** + * + * Use to release this runtime's layer resources from Promise-based code. + */ + readonly dispose: () => Promise + + /** + * Dispose of the resources associated with the runtime. + * + * **When to use** + * + * Use to release this runtime's layer resources from an `Effect` workflow. + */ + readonly disposeEffect: Effect.Effect +} + +/** + * Creates a `ManagedRuntime` from a layer. + * + * **When to use** + * + * Use to create a reusable runtime from a `Layer` for application entry points + * or integration code that runs many effects without rebuilding services. + * + * **Details** + * + * The layer is built lazily on first use and its context is cached for + * subsequent runs. Resources acquired by the layer are owned by the runtime and + * are released when `dispose` or `disposeEffect` is run. `options.memoMap` can + * be used to share layer memoization with other layer builds. + * + * **Gotchas** + * + * Dispose the runtime when it is no longer needed. A runtime cannot be reused + * after disposal. + * + * **Example** (Creating a managed runtime) + * + * ```ts + * import { Context, Effect, Layer, ManagedRuntime } from "effect" + * + * class Notifications extends Context.Service Effect.Effect + * }>()("Notifications") { + * static readonly layer = Layer.succeed(this)({ + * notify: Effect.fn("Notifications.notify")((message) => + * Effect.sync(() => console.log(message)) + * ) + * }) + * } + * + * const runtime = ManagedRuntime.make(Notifications.layer) + * + * const program = Effect.flatMap( + * Notifications, + * (_) => _.notify("Hello, world!") + * ).pipe(Effect.ensuring(runtime.disposeEffect)) + * + * runtime.runPromise(program) + * // Hello, world! + * ``` + * + * @see {@link ManagedRuntime} for the returned runtime interface + * @see {@link Layer.MemoMap} for shared layer memoization + * @see {@link Layer.build} for lower-level scoped layer construction + * + * @category runtime class + * @since 2.0.0 + */ +export const make = ( + layer: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap | undefined + } | undefined +): ManagedRuntime => { + const memoMap = options?.memoMap ?? Layer.makeMemoMapUnsafe() + const scope = Scope.makeUnsafe("parallel") + const layerScope = Scope.forkUnsafe(scope, "sequential") + const defaultRunOptions: Effect.RunOptions = { + onFiberStart: Fiber.runIn(scope) + } + const mergeRunOptions = (options?: O): O => + options + ? { + ...options, + onFiberStart: options.onFiberStart ? + (fiber) => { + defaultRunOptions.onFiberStart!(fiber) + options.onFiberStart!(fiber) + } : + defaultRunOptions.onFiberStart + } + : defaultRunOptions as O + let buildFiber: Fiber.Fiber, ER> | undefined + const contextEffect = Effect.withFiber, ER>((fiber) => { + if (!buildFiber) { + buildFiber = Effect.runFork( + Effect.tap( + Layer.buildWithMemoMap(layer, memoMap, layerScope), + (context) => + Effect.sync(() => { + self.cachedContext = context + }) + ), + { ...defaultRunOptions, scheduler: fiber.currentScheduler } + ) + } + return Effect.flatten(Fiber.await(buildFiber)) + }) + const self: ManagedRuntime = { + [TypeId]: TypeId, + memoMap, + scope, + contextEffect: contextEffect, + cachedContext: undefined, + context() { + return self.cachedContext === undefined ? + Effect.runPromise(self.contextEffect) : + Promise.resolve(self.cachedContext) + }, + dispose(): Promise { + return Effect.runPromise(self.disposeEffect) + }, + disposeEffect: Effect.suspend(() => { + ;(self as Mutable>).contextEffect = Effect.die("ManagedRuntime disposed") + self.cachedContext = undefined + return Scope.close(self.scope, Exit.void) + }), + runFork(effect: Effect.Effect, options?: Effect.RunOptions): Fiber.Fiber { + return self.cachedContext === undefined ? + Effect.runFork(provide(self, effect), mergeRunOptions(options)) : + Effect.runForkWith(self.cachedContext)(effect, mergeRunOptions(options)) + }, + runCallback( + effect: Effect.Effect, + options?: Effect.RunOptions & { + readonly onExit: (exit: Exit.Exit) => void + } + ): (interruptor?: number | undefined) => void { + return self.cachedContext === undefined ? + Effect.runCallback(provide(self, effect), mergeRunOptions(options)) : + Effect.runCallbackWith(self.cachedContext)(effect, mergeRunOptions(options)) + }, + runSyncExit(effect: Effect.Effect): Exit.Exit { + return self.cachedContext === undefined ? + Effect.runSyncExit(provide(self, effect)) : + Effect.runSyncExitWith(self.cachedContext)(effect) + }, + runSync(effect: Effect.Effect): A { + return self.cachedContext === undefined ? + Effect.runSync(provide(self, effect)) : + Effect.runSyncWith(self.cachedContext)(effect) + }, + runPromiseExit(effect: Effect.Effect, options?: Effect.RunOptions): Promise> { + return self.cachedContext === undefined ? + Effect.runPromiseExit(provide(self, effect), mergeRunOptions(options)) : + Effect.runPromiseExitWith(self.cachedContext)(effect, mergeRunOptions(options)) + }, + runPromise(effect: Effect.Effect, options?: { + readonly signal?: AbortSignal | undefined + }): Promise { + return self.cachedContext === undefined ? + Effect.runPromise(provide(self, effect), mergeRunOptions(options)) : + Effect.runPromiseWith(self.cachedContext)(effect, mergeRunOptions(options)) + } + } + return self +} + +function provide( + managed: ManagedRuntime, + effect: Effect.Effect +): Effect.Effect { + return Effect.flatMap( + managed.contextEffect, + (context) => Effect.provideContext(effect, context) + ) +} diff --git a/repos/effect/packages/effect/src/Match.ts b/repos/effect/packages/effect/src/Match.ts new file mode 100644 index 0000000..2dfe2af --- /dev/null +++ b/repos/effect/packages/effect/src/Match.ts @@ -0,0 +1,2606 @@ +/** + * Builds pattern matchers for TypeScript values. + * + * `Match` lets you add ordered cases and then finish them with a result, + * fallback, `Option`, or exhaustive check. Use `Match.type` to define a + * reusable matcher for a type, or `Match.value` to match one value immediately. + * Cases can match literal values, predicates, object shapes, tags, negated + * patterns, and common checks such as strings, numbers, records, and class + * instances. + * + * @since 4.0.0 + */ +import * as internal from "./internal/matcher.ts" +import type * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import type * as Result from "./Result.ts" +import type * as T from "./Types.ts" +import type { Unify } from "./Unify.ts" + +const TypeId = internal.TypeId + +/** + * Union type for matchers created by `Match.type` and `Match.value`. + * + * **Details** + * + * A `Matcher` carries the input type, accumulated filters, remaining cases, + * result type, and, for value matchers, the provided value being matched. + * + * **Example** (Matching string and number values) + * + * ```ts + * import { Match } from "effect" + * + * // Simulated dynamic input that can be a string or a number + * const input: string | number = "some input" + * + * // ┌─── string + * // ▼ + * const result = Match.value(input).pipe( + * // Match if the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Match if the value is a string + * Match.when(Match.string, (s) => `string: ${s}`), + * // Ensure all possible cases are covered + * Match.exhaustive + * ) + * + * console.log(result) + * // Output: "string: some input" + * ``` + * + * @category models + * @since 4.0.0 + */ +export type Matcher = + | TypeMatcher + | ValueMatcher + +/** + * Represents a pattern matcher that operates on types rather than specific values. + * + * **Details** + * + * A `TypeMatcher` is created when using `Match.type()` and allows you to define + * patterns that will be applied to values of the specified type. It maintains + * type-level information about the input type, applied filters, remaining cases, + * and expected results. + * + * **Example** (Creating a type matcher) + * + * ```ts + * import { Match } from "effect" + * + * // Create a TypeMatcher for string | number + * const matcher = Match.type().pipe( + * Match.when(Match.string, (s) => `String: ${s}`), + * Match.when(Match.number, (n) => `Number: ${n}`), + * Match.exhaustive + * ) + * + * console.log(matcher("hello")) // "String: hello" + * console.log(matcher(42)) // "Number: 42" + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TypeMatcher extends Pipeable { + readonly _tag: "TypeMatcher" + readonly [TypeId]: { + readonly _input: T.Contravariant + readonly _filters: T.Covariant + readonly _remaining: T.Covariant + readonly _result: T.Covariant + readonly _return: T.Covariant + } + readonly cases: ReadonlyArray + add(_case: Case): TypeMatcher +} + +/** + * Represents a pattern matcher that operates on a specific provided value. + * + * **Details** + * + * A `ValueMatcher` is created when using `Match.value(someValue)` and contains + * the actual value to be matched against. It tracks both the provided value + * and the result of applying patterns to determine matches. + * + * **Example** (Creating a value matcher) + * + * ```ts + * import { Match } from "effect" + * + * const input = { type: "user", name: "Alice", age: 30 } + * + * // Create a ValueMatcher for the specific input + * const result = Match.value(input).pipe( + * Match.when({ type: "user" }, (user) => `User: ${user.name}`), + * Match.when({ type: "admin" }, (admin) => `Admin: ${admin.name}`), + * Match.orElse(() => "Unknown type") + * ) + * + * console.log(result) // "User: Alice" + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface ValueMatcher + extends Pipeable +{ + readonly _tag: "ValueMatcher" + readonly [TypeId]: { + readonly _input: T.Contravariant + readonly _filters: T.Covariant + readonly _result: T.Covariant + readonly _return: T.Covariant + } + readonly provided: Provided + readonly value: Result.Result + add(_case: Case): ValueMatcher +} + +/** + * Represents a single pattern matching case. + * + * **When to use** + * + * Use as the common public type for code that needs to inspect, store, or pass + * either positive or negative pattern matching cases. + * + * **Details** + * + * A `Case` can be either a positive match (`When`) or a negative match (`Not`). + * Cases are the building blocks of pattern matching logic and determine + * how values are tested and transformed. + * + * @see {@link When} for positive cases + * @see {@link Not} for negative cases + * + * @category models + * @since 4.0.0 + */ +export type Case = When | Not + +/** + * Represents a positive pattern matching case. + * + * **Details** + * + * A `When` case contains the logic to test if a value matches a specific pattern + * and the function to evaluate when the pattern matches. It's the primary + * building block for pattern matching conditions. + * + * **Example** (Creating positive match cases) + * + * ```ts + * import { Match } from "effect" + * + * // When creates cases that match specific patterns + * const stringMatcher = Match.type().pipe( + * Match.when(Match.string, (s: string) => `Got string: ${s}`), + * Match.when(Match.number, (n: number) => `Got number: ${n}`), + * Match.exhaustive + * ) + * + * console.log(stringMatcher("hello")) // "Got string: hello" + * console.log(stringMatcher(42)) // "Got number: 42" + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface When { + readonly _tag: "When" + guard(u: unknown): boolean + evaluate(input: unknown): any +} + +/** + * Represents a negative pattern matching case. + * + * **Details** + * + * A `Not` case contains the logic to test if a value does NOT match a specific + * pattern and the function to evaluate when the pattern doesn't match. It's used + * for exclusion-based pattern matching. + * + * **Example** (Creating negative match cases) + * + * ```ts + * import { Match } from "effect" + * + * // Not creates cases that exclude specific patterns + * const matcher = Match.type().pipe( + * // Match any string except "forbidden" + * Match.not("forbidden", (s) => `Allowed: ${s}`), + * Match.orElse(() => "This string is forbidden") + * ) + * + * console.log(matcher("hello")) // "Allowed: hello" + * console.log(matcher("forbidden")) // "This string is forbidden" + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Not { + readonly _tag: "Not" + guard(u: unknown): boolean + evaluate(input: unknown): any +} + +/** + * Creates a matcher for a specific type. + * + * **When to use** + * + * Use to build a reusable matcher function for values of a known input type. + * + * **Details** + * + * This function defines a `Matcher` that operates on a given type, allowing you + * to specify conditions for handling different cases. Once the matcher is + * created, you can use pattern-matching functions like {@link when} to define + * how different values should be processed. + * + * **Example** (Matching Numbers and Strings) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for values that are either strings or numbers + * // + * // ┌─── (u: string | number) => string + * // ▼ + * const match = Match.type().pipe( + * // Match when the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Match when the value is a string + * Match.when(Match.string, (s) => `string: ${s}`), + * // Ensure all possible cases are handled + * Match.exhaustive + * ) + * + * console.log(match(0)) + * // Output: "number: 0" + * + * console.log(match("hello")) + * // Output: "string: hello" + * ``` + * + * @see {@link value} for creating a matcher from a specific value. + * + * @category constructors + * @since 4.0.0 + */ +export const type: () => Matcher, I, never, never> = internal.type + +/** + * Creates a matcher from a specific value. + * + * **When to use** + * + * Use to match one concrete input immediately. + * + * **Details** + * + * This function allows you to define a `Matcher` directly from a given value, + * rather than from a type. This is useful when working with known values, + * enabling structured pattern matching on objects, primitives, or any data + * structure. + * + * Once the matcher is created, you can use pattern-matching functions like + * {@link when} to define how different cases should be handled. + * + * **Example** (Matching an Object by Property) + * + * ```ts + * import { Match } from "effect" + * + * const input = { name: "John", age: 30 } + * + * // Create a matcher for the specific object + * const result = Match.value(input).pipe( + * // Match when the 'name' property is "John" + * Match.when( + * { name: "John" }, + * (user) => `${user.name} is ${user.age} years old` + * ), + * // Provide a fallback if no match is found + * Match.orElse(() => "Oh, not John") + * ) + * + * console.log(result) + * // Output: "John is 30 years old" + * ``` + * + * @see {@link type} for creating a matcher from a specific type. + * + * @category constructors + * @since 4.0.0 + */ +export const value: ( + i: I +) => Matcher, I, never, I> = internal.value + +/** + * Creates a match function for a specific value with discriminated union handling. + * + * **Details** + * + * This function provides a convenient way to pattern match on discriminated unions + * by providing an object that maps each `_tag` value to its corresponding handler. + * It's similar to a switch statement but with better type safety and exhaustiveness checking. + * + * **Example** (Matching value tags) + * + * ```ts + * import { Match } from "effect" + * + * type Status = { readonly _tag: "Success"; readonly data: string } + * + * const success: Status = { _tag: "Success", data: "Hello" } + * + * // Simple valueTags usage + * const message = Match.valueTags(success, { + * Success: (result) => `Success: ${result.data}` + * }) + * + * console.log(message) // "Success: Hello" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const valueTags: { + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(fields: P): (input: I) => Unify> + < + const I, + P extends + & { readonly [Tag in Types.Tags<"_tag", I> & string]: (_: Extract) => any } + & { readonly [Tag in Exclude>]: never } + >(input: I, fields: P): Unify> +} = internal.valueTags + +/** + * Creates a type-safe match function for discriminated unions based on `_tag` field. + * + * **Details** + * + * This function allows you to define exhaustive pattern matching for discriminated unions + * by providing handlers for each possible `_tag` value. It ensures type safety and + * can optionally enforce a specific return type across all branches. + * + * **Example** (Matching type tags) + * + * ```ts + * import { Match } from "effect" + * + * type Result = + * | { readonly _tag: "Success"; readonly data: string } + * | { readonly _tag: "Error"; readonly message: string } + * | { readonly _tag: "Loading" } + * + * // Create a matcher with specific return type + * const formatResult = Match.typeTags()({ + * Success: (result) => `Data: ${result.data}`, + * Error: (result) => `Error: ${result.message}`, + * Loading: () => "Loading..." + * }) + * + * console.log(formatResult({ _tag: "Success", data: "Hello World" })) + * // Output: "Data: Hello World" + * + * console.log(formatResult({ _tag: "Error", message: "Network failed" })) + * // Output: "Error: Network failed" + * + * // Create a matcher with inferred return type + * const processResult = Match.typeTags()({ + * Success: (result) => ({ type: "ok", value: result.data }), + * Error: (result) => ({ type: "error", error: result.message }), + * Loading: () => ({ type: "pending" }) + * }) + * + * console.log(processResult({ _tag: "Loading" })) + * // Output: { type: "pending" } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const typeTags: { + (): < + P extends + & { + readonly [Tag in Types.Tags<"_tag", I> & string]: ( + _: Extract + ) => Ret + } + & { readonly [Tag in Exclude>]: never } + >(fields: P) => (input: I) => Ret + (): < + P extends + & { + readonly [Tag in Types.Tags<"_tag", I> & string]: ( + _: Extract + ) => any + } + & { readonly [Tag in Exclude>]: never } + >(fields: P) => (input: I) => Unify> +} = internal.typeTags + +/** + * Ensures that all branches of a matcher return a specific type. + * + * **Details** + * + * This function enforces a consistent return type across all pattern-matching + * branches. By specifying a return type, TypeScript will check that every + * matching condition produces a value of the expected type. + * + * **Important:** This function must be the first step in the matcher pipeline. + * If used later, TypeScript will not enforce type consistency correctly. + * + * **Example** (Validating return type consistency) + * + * ```ts + * import { Match } from "effect" + * + * const match = Match.type<{ a: number } | { b: string }>().pipe( + * // Ensure all branches return a string + * Match.withReturnType(), + * // ❌ Type error: 'number' is not assignable to type 'string' + * // @ts-expect-error + * Match.when({ a: Match.number }, (_) => _.a), + * // ✅ Correct: returns a string + * Match.when({ b: Match.string }, (_) => _.b), + * Match.exhaustive + * ) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const withReturnType: () => ( + self: Matcher +) => [Ret] extends [[A] extends [never] ? any : A] ? Matcher + : "withReturnType constraint does not extend Result type" = internal.withReturnType + +/** + * Defines a condition for matching values. + * + * **When to use** + * + * Use to add one positive pattern case to a `Match.type` or `Match.value` + * pipeline when a direct value, predicate, or structured object pattern should + * run a handler for matching input. + * + * **Details** + * + * Supports both direct value comparisons and predicate functions. If the + * pattern matches, the associated function is executed and the matched input is + * removed from the remaining cases tracked by the matcher. + * + * **Example** (Matching with values and predicates) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for objects with an "age" property + * const match = Match.type<{ age: number }>().pipe( + * // Match when age is greater than 18 + * Match.when( + * { age: (age: number) => age > 18 }, + * (user: { age: number }) => `Age: ${user.age}` + * ), + * // Match when age is exactly 18 + * Match.when({ age: 18 }, () => "You can vote"), + * // Fallback case for all other ages + * Match.orElse((user: { age: number }) => `${user.age} is too young`) + * ) + * + * console.log(match({ age: 20 })) + * // Output: "Age: 20" + * + * console.log(match({ age: 18 })) + * // Output: "You can vote" + * + * console.log(match({ age: 4 })) + * // Output: "4 is too young" + * ``` + * + * @see {@link whenOr} for handling any one of several patterns with the same handler + * @see {@link whenAnd} for requiring all provided patterns to match before running a handler + * @see {@link not} for handling inputs that do not match a pattern + * @see {@link orElse} for providing a fallback when no pattern case matches + * + * @category Defining patterns + * @since 4.0.0 + */ +export const when: < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.when + +/** + * Matches one of multiple patterns in a single condition. + * + * **Details** + * + * This function allows defining a condition where a value matches any of the + * provided patterns. If a match is found, the associated function is executed. + * It simplifies cases where multiple patterns share the same handling logic. + * + * Unlike {@link when}, which requires separate conditions for each pattern, + * this function enables combining them into a single statement, making the + * matcher more concise. + * + * **Example** (Matching one of several patterns) + * + * ```ts + * import { Match } from "effect" + * + * type ErrorType = + * | { readonly _tag: "NetworkError"; readonly message: string } + * | { readonly _tag: "TimeoutError"; readonly duration: number } + * | { readonly _tag: "ValidationError"; readonly field: string } + * + * const handleError = Match.type().pipe( + * Match.whenOr( + * { _tag: "NetworkError" }, + * { _tag: "TimeoutError" }, + * () => "Retry the request" + * ), + * Match.when({ _tag: "ValidationError" }, (_) => `Invalid field: ${_.field}`), + * Match.exhaustive + * ) + * + * console.log(handleError({ _tag: "NetworkError", message: "No connection" })) + * // Output: "Retry the request" + * + * console.log(handleError({ _tag: "ValidationError", field: "email" })) + * // Output: "Invalid field: email" + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const whenOr: < + R, + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.whenOr + +/** + * Matches a value that satisfies all provided patterns. + * + * **Details** + * + * This function allows defining a condition where a value must match all the + * given patterns simultaneously. If the value satisfies every pattern, the + * associated function is executed. + * + * Unlike {@link when}, which matches a single pattern at a time, this function + * ensures that multiple conditions are met before executing the callback. It is + * useful when checking for values that need to fulfill multiple criteria at + * once. + * + * **Example** (Matching all provided patterns) + * + * ```ts + * import { Match } from "effect" + * + * type User = { readonly age: number; readonly role: "admin" | "user" } + * + * const checkUser = Match.type().pipe( + * Match.whenAnd( + * { age: (n) => n >= 18 }, + * { role: "admin" }, + * () => "Admin access granted" + * ), + * Match.orElse(() => "Access denied") + * ) + * + * console.log(checkUser({ age: 20, role: "admin" })) + * // Output: "Admin access granted" + * + * console.log(checkUser({ age: 20, role: "user" })) + * // Output: "Access denied" + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const whenAnd: < + R, + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch>) => Ret +>( + ...args: [...patterns: P, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr +> = internal.whenAnd + +/** + * Matches values based on a specified discriminant field. + * + * **When to use** + * + * Use to match one or more exact values of a discriminator field. + * + * **Details** + * + * This function is used to define pattern matching on objects that follow a + * **discriminated union** structure, where a specific field (e.g., `type`, + * `kind`, `_tag`) determines the variant of the object. It allows matching + * multiple values of the discriminant and provides a function to handle the + * matched cases. + * + * **Example** (Matching on a discriminator field) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type< + * { type: "A"; a: string } | { type: "B"; b: number } | { + * type: "C" + * c: boolean + * } + * >(), + * Match.discriminator("type")("A", "B", (_) => `A or B: ${_.type}`), + * Match.discriminator("type")("C", (_) => `C(${_.c})`), + * Match.exhaustive + * ) + * ``` + * + * @see {@link discriminators} for defining several discriminator handlers at once + * @see {@link discriminatorStartsWith} for matching string discriminator values by prefix + * + * @category Defining patterns + * @since 4.0.0 + */ +export const discriminator: ( + field: D +) => & string, Ret, Fn extends (_: Extract>) => Ret>( + ...pattern: [first: P, ...values: Array

, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + ReturnType | A, + Pr, + Ret +> = internal.tag + +/** + * Matches values where the `_tag` field starts with a given prefix. + * + * **Details** + * + * This function allows you to match on values in a **discriminated union** + * based on whether the `_tag` field starts with a specified prefix. It is + * useful for handling hierarchical or namespaced tags, where multiple related + * cases share a common prefix. + * + * **Example** (Matching tag prefixes) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ _tag: "A" } | { _tag: "B" } | { _tag: "A.A" } | {}>(), + * Match.tagStartsWith("A", (_) => 1 as const), + * Match.tagStartsWith("B", (_) => 2 as const), + * Match.orElse((_) => 3 as const) + * ) + * + * console.log(match({ _tag: "A" })) // 1 + * console.log(match({ _tag: "B" })) // 2 + * console.log(match({ _tag: "A.A" })) // 1 + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const tagStartsWith: < + R, + P extends string, + Ret, + Fn extends (_: Extract>) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + ReturnType | A, + Pr, + Ret +> = internal.tagStartsWith + +/** + * Matches values based on their `_tag` field, mapping each tag to a + * corresponding handler. + * + * **Details** + * + * This function provides a way to handle discriminated unions by mapping `_tag` + * values to specific functions. Each handler receives the matched value and + * returns a transformed result. If all possible tags are handled, you can + * enforce exhaustiveness using `Match.exhaustive` to ensure no case is missed. + * + * **Example** (Mapping tag handlers) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type< + * { _tag: "A"; a: string } | { _tag: "B"; b: number } | { + * _tag: "C" + * c: boolean + * } + * >(), + * Match.tags({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }), + * Match.exhaustive + * ) + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const tags: < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags<"_tag", R> & string]?: ((_: Extract>) => Ret) | undefined } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.tags + +/** + * Matches values based on their `_tag` field and requires handling of all + * possible cases. + * + * **Details** + * + * This function is designed for **discriminated unions** where every possible + * `_tag` value must have a corresponding handler. Unlike {@link tags}, this + * function ensures **exhaustiveness**, meaning all cases must be explicitly + * handled. If a `_tag` value is missing from the mapping, TypeScript will + * report an error. + * + * **Example** (Handling all tag cases) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type< + * { _tag: "A"; a: string } | { _tag: "B"; b: number } | { + * _tag: "C" + * c: boolean + * } + * >(), + * Match.tagsExhaustive({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }) + * ) + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const tagsExhaustive: < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags<"_tag", R> & string]: (_: Extract>) => Ret } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.tagsExhaustive + +/** + * Creates a pattern that excludes a specific value while allowing all others. + * + * **When to use** + * + * Use to add a negative pattern case for inputs that should match when another + * pattern does not. + * + * **Details** + * + * Any excluded value bypasses the provided function and continues matching + * through later cases. + * + * **Example** (Ignoring a specific value) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match any value except "hi", returning "ok" + * Match.not("hi", () => "ok"), + * // Fallback case for when the value is "hi" + * Match.orElse(() => "fallback") + * ) + * + * console.log(match("hello")) + * // Output: "ok" + * + * console.log(match("hi")) + * // Output: "fallback" + * ``` + * + * @see {@link when} for adding a positive pattern case + * + * @category Defining patterns + * @since 4.0.0 + */ +export const not: < + R, + const P extends Types.PatternPrimitive | Types.PatternBase, + Ret, + Fn extends (_: Types.NotMatch) => Ret +>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddOnly>, + Types.ApplyFilters>>, + A | ReturnType, + Pr, + Ret +> = internal.not + +/** + * Matches non-empty strings. + * + * **When to use** + * + * Use to match strings whose length is greater than zero. + * + * **Details** + * + * This predicate matches any string that contains at least one character, + * effectively filtering out empty strings (""). + * + * **Example** (Matching non-empty strings) + * + * ```ts + * import { Match } from "effect" + * + * const processInput = Match.type() + * .pipe( + * Match.when(Match.nonEmptyString, (str) => `Valid input: ${str}`), + * Match.orElse(() => "Input cannot be empty") + * ) + * + * console.log(processInput("hello")) + * // Output: "Valid input: hello" + * + * console.log(processInput("")) + * // Output: "Input cannot be empty" + * + * console.log(processInput(" ")) + * // Output: "Valid input: " (whitespace-only strings are considered non-empty) + * ``` + * + * @see {@link string} for matching any string + * + * @category predicates + * @since 4.0.0 + */ +export const nonEmptyString: SafeRefinement = internal.nonEmptyString + +/** + * Matches a specific set of literal values (e.g., `Match.is("a", 42, true)`). + * + * **When to use** + * + * Use to match one of several literal primitive or null values. + * + * **Details** + * + * This function creates a predicate that matches any of the provided literal values. + * It's useful for matching against multiple specific values in a single pattern. + * + * **Example** (Matching literal values) + * + * ```ts + * import { Match } from "effect" + * + * const handleStatus = Match.type() + * .pipe( + * Match.when(Match.is("success", "ok", 200), () => "Operation successful"), + * Match.when(Match.is("error", "failed", 500), () => "Operation failed"), + * Match.when(Match.is(0, false, null), () => "Falsy value"), + * Match.orElse((value) => `Unknown status: ${value}`) + * ) + * + * console.log(handleStatus("success")) + * // Output: "Operation successful" + * + * console.log(handleStatus(200)) + * // Output: "Operation successful" + * + * console.log(handleStatus("failed")) + * // Output: "Operation failed" + * + * console.log(handleStatus(0)) + * // Output: "Falsy value" + * + * console.log(handleStatus("pending")) + * // Output: "Unknown status: pending" + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const is: < + Literals extends ReadonlyArray +>(...literals: Literals) => SafeRefinement = internal.is + +/** + * Matches values of type `string`. + * + * **Details** + * + * This predicate refines unknown values to strings, allowing pattern matching + * on string types. It's commonly used in type-based matchers to handle string cases. + * + * **Example** (Matching string values) + * + * ```ts + * import { Match } from "effect" + * + * const processValue = Match.type().pipe( + * Match.when(Match.string, (str) => `String: ${str.toUpperCase()}`), + * Match.when(Match.number, (num) => `Number: ${num * 2}`), + * Match.when(Match.boolean, (bool) => `Boolean: ${bool ? "yes" : "no"}`), + * Match.exhaustive + * ) + * + * console.log(processValue("hello")) // "String: HELLO" + * console.log(processValue(42)) // "Number: 84" + * console.log(processValue(true)) // "Boolean: yes" + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const string: Predicate.Refinement = Predicate.isString + +/** + * Matches values of type `number`. + * + * **When to use** + * + * Use to match primitive number values, including `NaN` and infinities. + * + * **Details** + * + * This predicate refines unknown values to numbers, allowing pattern matching + * on numeric types. It matches all number values including integers, floats, + * `Infinity`, `-Infinity`, and `NaN`. + * + * **Example** (Matching number values) + * + * ```ts + * import { Match } from "effect" + * + * const categorizeNumber = Match.type().pipe( + * Match.when(Match.number, (num) => { + * if (Number.isNaN(num)) return "Not a number" + * if (!Number.isFinite(num)) return "Infinite" + * if (Number.isInteger(num)) return `Integer: ${num}` + * return `Float: ${num.toFixed(2)}` + * }), + * Match.orElse(() => "Not a number type") + * ) + * + * console.log(categorizeNumber(42)) // "Integer: 42" + * console.log(categorizeNumber(3.14)) // "Float: 3.14" + * console.log(categorizeNumber(NaN)) // "Not a number" + * console.log(categorizeNumber("hello")) // "Not a number type" + * ``` + * + * @see {@link bigint} for matching primitive bigint values + * + * @category predicates + * @since 4.0.0 + */ +export const number: Predicate.Refinement = Predicate.isNumber + +/** + * Matches any value without restrictions. + * + * **When to use** + * + * Use to define an explicit catch-all pattern when the handler should receive + * the unmatched value. + * + * **Details** + * + * This predicate matches every input, including `undefined`, `null`, objects, + * primitives, and functions. + * + * **Gotchas** + * + * `Match.any` should usually be last because cases are checked in order and + * the first matching case wins. + * + * **Example** (Matching any remaining value) + * + * ```ts + * import { Match } from "effect" + * + * const describeValue = Match.type() + * .pipe( + * Match.when(Match.string, (str) => `String: ${str}`), + * Match.when(Match.number, (num) => `Number: ${num}`), + * Match.when(Match.boolean, (bool) => `Boolean: ${bool}`), + * Match.when(Match.any, (value) => `Other: ${typeof value}`), + * Match.exhaustive + * ) + * + * console.log(describeValue("hello")) + * // Output: "String: hello" + * + * console.log(describeValue(42)) + * // Output: "Number: 42" + * + * console.log(describeValue([1, 2, 3])) + * // Output: "Other: object" + * + * console.log(describeValue(null)) + * // Output: "Other: object" + * ``` + * + * @see {@link defined} for matching only non-nullish values + * @see {@link orElse} for providing a fallback after earlier cases + * + * @category predicates + * @since 4.0.0 + */ +export const any: SafeRefinement = internal.any + +/** + * Matches any defined (non-null and non-undefined) value. + * + * **When to use** + * + * Use to exclude only `null` and `undefined` from a match branch. + * + * **Details** + * + * This predicate matches values that are neither `null` nor `undefined`, + * effectively filtering out nullish values while preserving all other types. + * + * **Example** (Matching defined values) + * + * ```ts + * import { Match } from "effect" + * + * const processValue = Match.type() + * .pipe( + * Match.when(Match.defined, (value) => `Defined value: ${value}`), + * Match.orElse(() => "Value is null or undefined") + * ) + * + * console.log(processValue("hello")) + * // Output: "Defined value: hello" + * + * console.log(processValue(42)) + * // Output: "Defined value: 42" + * + * console.log(processValue(0)) + * // Output: "Defined value: 0" + * + * console.log(processValue("")) + * // Output: "Defined value: " + * + * console.log(processValue(null)) + * // Output: "Value is null or undefined" + * + * console.log(processValue(undefined)) + * // Output: "Value is null or undefined" + * ``` + * + * @see {@link any} for matching every value without excluding nullish inputs + * + * @category predicates + * @since 4.0.0 + */ +export const defined: (u: A) => u is A & {} = internal.defined + +/** + * Matches values of type `boolean`. + * + * **When to use** + * + * Use to match primitive boolean values. + * + * **Details** + * + * This predicate refines unknown values to booleans, allowing pattern matching + * on boolean types. It only matches the primitive boolean values `true` and `false`. + * + * **Example** (Matching boolean values) + * + * ```ts + * import { Match } from "effect" + * + * const describeTruthiness = Match.type().pipe( + * Match.when( + * Match.boolean, + * (bool) => bool ? "Definitely true" : "Definitely false" + * ), + * Match.when(0, () => "Falsy number"), + * Match.when("", () => "Empty string"), + * Match.when(Match.null, () => "Null value"), + * Match.orElse(() => "Some other truthy value") + * ) + * + * console.log(describeTruthiness(true)) // "Definitely true" + * console.log(describeTruthiness(false)) // "Definitely false" + * console.log(describeTruthiness(0)) // "Falsy number" + * console.log(describeTruthiness(1)) // "Some other truthy value" + * ``` + * + * @see {@link is} for matching specific literal boolean values + * + * @category predicates + * @since 4.0.0 + */ +export const boolean: Predicate.Refinement = Predicate.isBoolean + +const _undefined: Predicate.Refinement = Predicate.isUndefined +export { + /** + * Matches the value `undefined`. + * + * **When to use** + * + * Use when a matcher should handle only inputs with no defined value. + * + * **Details** + * + * This refinement is backed by `Predicate.isUndefined`, which checks + * `input === undefined`. + * + * @see {@link defined} for matching non-nullish values + * @see {@link is} for matching literal values + * + * @category predicates + * @since 4.0.0 + */ + _undefined as undefined +} + +const _null: Predicate.Refinement = Predicate.isNull +export { + /** + * Matches the value `null`. + * + * **When to use** + * + * Use to handle only the `null` literal in a match branch. + * + * **Details** + * + * This refinement is backed by `Predicate.isNull`, which checks + * `input === null`. + * + * @see {@link defined} for matching non-nullish values + * @see {@link is} for matching literal values + * + * @category predicates + * @since 4.0.0 + */ + _null as null +} + +/** + * Matches values of type `bigint`. + * + * **When to use** + * + * Use to match primitive bigint values. + * + * **Details** + * + * This predicate refines unknown values to bigints, allowing pattern matching + * on bigint types. BigInts are used for representing integers with arbitrary precision. + * + * **Example** (Matching bigint values) + * + * ```ts + * import { Match } from "effect" + * + * const processLargeNumber = Match.type().pipe( + * Match.when(Match.bigint, (big) => { + * if (big > 9007199254740991n) { + * return `Large integer: ${big.toString()}` + * } + * return `BigInt: ${big.toString()}` + * }), + * Match.when(Match.number, (num) => `Regular number: ${num}`), + * Match.orElse(() => "Not a numeric type") + * ) + * + * console.log(processLargeNumber(123n)) // "BigInt: 123" + * console.log(processLargeNumber(9007199254740992n)) // "Large integer: 9007199254740992" + * console.log(processLargeNumber(123)) // "Regular number: 123" + * console.log(processLargeNumber("123")) // "Not a numeric type" + * ``` + * + * @see {@link number} for matching primitive number values + * + * @category predicates + * @since 4.0.0 + */ +export const bigint: Predicate.Refinement = Predicate.isBigInt + +/** + * Matches values of type `symbol`. + * + * **Details** + * + * This predicate refines unknown values to symbols, allowing pattern matching + * on symbol types. Symbols are unique identifiers that are often used as + * object keys or for creating private properties. + * + * **Example** (Matching symbol values) + * + * ```ts + * import { Match } from "effect" + * + * const mySymbol = Symbol("my-symbol") + * const globalSymbol = Symbol.for("global-symbol") + * + * const handleSymbol = Match.type().pipe( + * Match.when(Match.symbol, (sym) => { + * const description = sym.description + * if (description) { + * return `Symbol with description: ${description}` + * } + * return "Symbol without description" + * }), + * Match.orElse(() => "Not a symbol") + * ) + * + * console.log(handleSymbol(mySymbol)) // "Symbol with description: my-symbol" + * console.log(handleSymbol(Symbol())) // "Symbol without description" + * console.log(handleSymbol("string")) // "Not a symbol" + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const symbol: Predicate.Refinement = Predicate.isSymbol + +/** + * Matches values that are instances of `Date`. + * + * **When to use** + * + * Use to match `Date` instances. + * + * **Details** + * + * This predicate refines unknown values to Date instances, allowing pattern + * matching on Date objects. It only matches actual Date instances, not + * date strings or timestamps. + * + * **Example** (Matching Date instances) + * + * ```ts + * import { Match } from "effect" + * + * const processDateValue = Match.type().pipe( + * Match.when(Match.date, (date) => { + * if (isNaN(date.getTime())) { + * return "Invalid date" + * } + * return `Date: ${date.toISOString().split("T")[0]}` + * }), + * Match.when(Match.string, (str) => `Date string: ${str}`), + * Match.orElse(() => "Not a date-related value") + * ) + * + * console.log(processDateValue(new Date("2024-01-01"))) // "Date: 2024-01-01" + * console.log(processDateValue(new Date("invalid"))) // "Invalid date" + * console.log(processDateValue("2024-01-01")) // "Date string: 2024-01-01" + * console.log(processDateValue(1704067200000)) // "Not a date-related value" + * ``` + * + * @see {@link instanceOf} for matching instances of any constructor + * + * @category predicates + * @since 4.0.0 + */ +export const date: Predicate.Refinement = Predicate.isDate + +/** + * Matches non-null objects other than arrays. + * + * **When to use** + * + * Use to match broad non-null, non-array object values. + * + * **Details** + * + * This predicate uses `Predicate.isObject`: it returns `true` for values whose + * runtime type is `"object"`, are not `null`, and are not arrays. It can match + * `Date`, `RegExp`, and class instances; use `instanceOf` or a more specific + * pattern when those cases need to be distinguished. + * + * **Example** (Matching record objects) + * + * ```ts + * import { Match } from "effect" + * + * const analyzeValue = Match.type().pipe( + * Match.when(Match.record, (obj) => { + * const keys = Object.keys(obj) + * const valueCount = keys.length + * return `Object with ${valueCount} properties: [${keys.join(", ")}]` + * }), + * Match.when( + * Match.instanceOf(Array), + * (arr) => `Array with ${arr.length} items` + * ), + * Match.orElse(() => "Not an object") + * ) + * + * console.log(analyzeValue({ name: "Alice", age: 30 })) // "Object with 2 properties: [name, age]" + * console.log(analyzeValue([1, 2, 3])) // "Array with 3 items" + * console.log(analyzeValue(null)) // "Not an object" + * console.log(analyzeValue("hello")) // "Not an object" + * ``` + * + * @see {@link instanceOf} for matching a specific constructor + * + * @category predicates + * @since 4.0.0 + */ +export const record: Predicate.Refinement = Predicate.isObject + +/** + * Matches instances of a given class. + * + * **When to use** + * + * Use to match values that are instances of a constructor with type-safe + * narrowing. + * + * **Details** + * + * This predicate checks if a value is an instance of the specified constructor, + * providing type-safe matching for class instances and built-in objects. + * + * **Example** (Matching class instances) + * + * ```ts + * import { Match } from "effect" + * + * class CustomError extends Error { + * constructor(message: string, public code: number) { + * super(message) + * } + * } + * + * const handleValue = Match.type() + * .pipe( + * Match.when( + * Match.instanceOf(CustomError), + * (err) => `Custom error: ${err.message} (code: ${err.code})` + * ), + * Match.when( + * Match.instanceOf(Error), + * (err) => `Standard error: ${err.message}` + * ), + * Match.when( + * Match.instanceOf(Array), + * (arr) => `Array with ${arr.length} items` + * ), + * Match.when( + * Match.instanceOf(Map), + * (map) => `Map with ${map.size} entries` + * ), + * Match.orElse((value) => `Other: ${typeof value}`) + * ) + * + * console.log(handleValue(new CustomError("Failed", 404))) // "Custom error: Failed (code: 404)" + * console.log(handleValue(new Error("Generic error"))) // "Standard error: Generic error" + * console.log(handleValue([1, 2, 3])) // "Array with 3 items" + * console.log(handleValue(new Map([["count", 1]]))) // "Map with 1 entries" + * ``` + * + * @see {@link instanceOfUnsafe} for constructor matching without the same type-safety guarantee + * @see {@link record} for matching broad non-null, non-array objects + * + * @category predicates + * @since 4.0.0 + */ +export const instanceOf: any>( + constructor: A +) => SafeRefinement, never> = internal.instanceOf + +/** + * Checks whether a value is an instance of a constructor without type-safe narrowing. + * + * **When to use** + * + * Use when you need constructor matching to use the unsafe refinement type. + * + * **Details** + * + * This predicate checks if a value is an instance of the specified constructor + * but doesn't provide the same type safety guarantees as the regular `instanceOf`. + * Use this when you need more flexibility but understand the type safety implications. + * + * **Example** (Matching class instances unsafely) + * + * ```ts + * import { Match } from "effect" + * + * class CustomError extends Error { + * constructor(message: string, public code: number) { + * super(message) + * } + * } + * + * // When you need to match instances but handle type narrowing manually + * const handleError = Match.type().pipe( + * Match.when(Match.instanceOfUnsafe(CustomError), (err: any) => { + * // Manual type assertion needed + * const customErr = err as CustomError + * return `Custom error ${customErr.code}: ${customErr.message}` + * }), + * Match.orElse(() => "Not a CustomError") + * ) + * ``` + * + * @see {@link instanceOf} for type-safe constructor matching + * + * @category predicates + * @since 4.0.0 + */ +export const instanceOfUnsafe: any>( + constructor: A +) => SafeRefinement, InstanceType> = internal.instanceOf + +/** + * Provides a fallback value when no patterns match. + * + * **When to use** + * + * Use to finalize a matcher with a fallback for unmatched input. + * + * **Details** + * + * This function ensures that a matcher always returns a valid result, even if + * no defined patterns match. It acts as a default case, similar to the + * `default` clause in a `switch` statement or the final `else` in an `if-else` + * chain. + * + * **Example** (Providing a default value when no patterns match) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match when the value is "a" + * Match.when("a", () => "ok"), + * // Fallback when no patterns match + * Match.orElse(() => "fallback") + * ) + * + * console.log(match("a")) + * // Output: "ok" + * + * console.log(match("b")) + * // Output: "fallback" + * ``` + * + * @see {@link option} for finalizing unmatched input as `Option.none` + * @see {@link result} for returning unmatched input as a `Result` failure + * @see {@link orElseAbsurd} for finalizing when unmatched input should be impossible + * + * @category completion + * @since 4.0.0 + */ +export const orElse: Ret>( + f: F +) => ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Unify | A> : Unify | A> = internal.orElse + +// TODO(4.0): Rename to "orThrow"? Like Result.getOrThrow +/** + * Returns a matcher that throws an error if no pattern matches. + * + * **When to use** + * + * Use to finalize a matcher when every remaining unmatched case should be + * impossible. + * + * **Details** + * + * This function finalizes a matcher by ensuring that if no patterns match, an + * error is thrown. It is useful when all cases should be covered, and any + * unexpected input should trigger an error instead of returning a default + * value. + * + * When used, this function removes the need for an explicit fallback case and + * ensures that an unmatched value is never silently ignored. + * + * **Example** (Throwing on unmatched input) + * + * ```ts + * import { Match } from "effect" + * + * const strictMatcher = Match.type<"a" | "b">().pipe( + * Match.when("a", () => "Found A"), + * Match.when("b", () => "Found B"), + * // Will throw if input is neither "a" nor "b" + * Match.orElseAbsurd + * ) + * + * console.log(strictMatcher("a")) // "Found A" + * console.log(strictMatcher("b")) // "Found B" + * + * // This would throw an error at runtime: + * // strictMatcher("c" as any) // throws + * ``` + * + * @see {@link exhaustive} for compile-time exhaustive matcher finalization + * @see {@link orElse} for providing a fallback for unmatched input + * + * @category completion + * @since 4.0.0 + */ +export const orElseAbsurd: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Unify : Unify = internal.orElseAbsurd + +/** + * Wraps the match result in a `Result`, distinguishing matched and unmatched + * cases. + * + * **Details** + * + * This function ensures that the result of a matcher is always wrapped in an + * `Result`, allowing clear differentiation between successful matches + * (`Ok(value)`) and cases where no pattern matched (`Err(unmatched + * value)`). + * + * This approach is particularly useful when handling optional values or when an + * unmatched case should be explicitly handled rather than returning a default + * value or throwing an error. + * + * **Example** (Extracting a user role with `Match.result`) + * + * ```ts + * import { Match } from "effect" + * + * type User = { readonly role: "admin" | "editor" | "viewer" } + * + * // Create a matcher to extract user roles + * const getRole = Match.type().pipe( + * Match.when({ role: "admin" }, () => "Has full access"), + * Match.when({ role: "editor" }, () => "Can edit content"), + * Match.result // Wrap the result in an Result + * ) + * + * console.log(getRole({ role: "admin" })) + * // Output: { _id: 'Result', _tag: 'Ok', ok: 'Has full access' } + * + * console.log(getRole({ role: "viewer" })) + * // Output: { _id: 'Result', _tag: 'Err', err: { role: 'viewer' } } + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const result: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Result.Result, R> : Result.Result, R> = internal.result + +/** + * Wraps the match result in an `Option`, representing an optional match. + * + * **When to use** + * + * Use to finalize a matcher when unmatched input is expected and should become + * `Option.none`. + * + * **Details** + * + * This function ensures that the result of a matcher is wrapped in an `Option`, + * making it easy to handle cases where no pattern matches. If a match is found, + * it returns `Some(value)`, otherwise, it returns `None`. + * + * This is useful in cases where a missing match is expected and should be + * handled explicitly rather than throwing an error or returning a default + * value. + * + * **Example** (Extracting a user role with `Match.option`) + * + * ```ts + * import { Match } from "effect" + * + * type User = { readonly role: "admin" | "editor" | "viewer" } + * + * // Create a matcher to extract user roles + * const getRole = Match.type().pipe( + * Match.when({ role: "admin" }, () => "Has full access"), + * Match.when({ role: "editor" }, () => "Can edit content"), + * Match.option // Wrap the result in an Option + * ) + * + * console.log(getRole({ role: "admin" })) + * // Output: { _id: 'Option', _tag: 'Some', value: 'Has full access' } + * + * console.log(getRole({ role: "viewer" })) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link result} for preserving unmatched input as a `Result` failure + * @see {@link orElse} for replacing unmatched input with a fallback value + * + * @category completion + * @since 4.0.0 + */ +export const option: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Option.Option> : Option.Option> = internal.option + +/** + * Completes a matcher that handles every remaining input case. + * + * **When to use** + * + * Use to require TypeScript to reject incomplete matcher definitions before the + * matcher is turned into a function. + * + * **Details** + * + * If any case is still unmatched, the matcher does not type-check as + * exhaustive. + * + * **Example** (Ensuring all cases are covered) + * + * ```ts + * import { Match } from "effect" + * + * // Create a matcher for string or number values + * const match = Match.type().pipe( + * // Match when the value is a number + * Match.when(Match.number, (n) => `number: ${n}`), + * // Mark the match as exhaustive, ensuring all cases are handled + * // TypeScript will throw an error if any case is missing + * // @ts-expect-error Type 'string' is not assignable to type 'never' + * Match.exhaustive + * ) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const exhaustive: ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify : Unify = internal.exhaustive + +const SafeRefinementId = "~effect/match/Match/SafeRefinement" + +/** + * A safe refinement that narrows types without runtime errors. + * + * **Details** + * + * `SafeRefinement` provides a way to refine types in pattern matching while + * maintaining type safety. Unlike regular predicates, safe refinements can + * transform the matched value's type without throwing runtime errors. + * + * **Example** (Using safe refinements) + * + * ```ts + * import { Match } from "effect" + * + * // Built-in safe refinements + * const processValue = Match.type().pipe( + * Match.when(Match.string, (s) => s.toUpperCase()), + * Match.when(Match.number, (n) => n * 2), + * Match.when(Match.defined, (value) => `Defined: ${value}`), + * Match.orElse(() => "Undefined or null") + * ) + * + * console.log(processValue("hello")) // "HELLO" + * console.log(processValue(21)) // 42 + * console.log(processValue(true)) // "Defined: true" + * console.log(processValue(null)) // "Undefined or null" + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface SafeRefinement { + readonly [SafeRefinementId]: (a: A) => R +} + +const Fail = Symbol.for("effect/Fail") +type Fail = typeof Fail + +/** + * A namespace containing utility types for Match operations. + * + * **Details** + * + * This namespace provides advanced type-level utilities used internally by the + * Match module to perform complex pattern matching, type narrowing, and filter + * application. These types enable the sophisticated type inference that makes + * pattern matching both type-safe and ergonomic. + * + * @since 4.0.0 + */ +export declare namespace Types { + /** + * Computes the matched type when a pattern P is applied to type R. + * + * **Details** + * + * This utility type determines what type a value will have after successfully + * matching against a pattern. It handles refinements, predicates, and complex + * object patterns to provide accurate type narrowing. + * + * **Example** (Computing matched types) + * + * ```ts + * import type { Match } from "effect" + * + * // WhenMatch computes the narrowed type after pattern matching + * type StringMatch = Match.Types.WhenMatch + * // Result: string + * + * type ObjectMatch = Match.Types.WhenMatch< + * { type: "user"; name: string } | { + * type: "admin" + * permissions: Array + * }, + * { type: "user" } + * > + * // Result: { type: "user"; name: string } + * ``` + * + * @category types + * @since 4.0.0 + */ + export type WhenMatch = + // check for any + [0] extends [1 & R] ? ResolvePred

: + P extends SafeRefinement ? SP + : P extends Predicate.Refinement + // try to narrow refinement + ? [Extract] extends [infer X] ? [X] extends [never] + // fallback to original refinement + ? RP + : X + : never + : P extends PredicateA ? PP + : ExtractMatch + + /** + * Computes the remaining type when a pattern P is excluded from type R. + * + * **Details** + * + * This utility type determines what type remains after a `Match.not` pattern + * excludes certain values. It's the complement of `WhenMatch`, calculating + * what's left after removing the matched portion. + * + * **Example** (Computing unmatched types) + * + * ```ts + * import type { Match } from "effect" + * + * // NotMatch computes what remains after exclusion + * type NotString = Match.Types.NotMatch< + * string | number | boolean, + * typeof Match.string + * > + * // Result: number | boolean + * + * type NotSpecificValue = Match.Types.NotMatch<"a" | "b" | "c", "a"> + * // Result: "b" | "c" + * ``` + * + * @category types + * @since 4.0.0 + */ + export type NotMatch = Exclude>> + + type PForNotMatch

= [ToInvertedRefinement

] extends [infer X] ? X + : never + + /** + * Resolves a pattern to its matched type for use in type computations. + * + * **Details** + * + * This utility type processes patterns (predicates, refinements, objects) + * and resolves them to their corresponding matched types. It's used internally + * to compute type transformations during pattern matching. + * + * **Example** (Resolving match patterns) + * + * ```ts + * import type { Match } from "effect" + * + * // PForMatch resolves patterns to their matched types + * type StringPattern = Match.Types.PForMatch + * // Result: string + * + * type ObjectPattern = Match.Types.PForMatch<{ name: string }> + * // Result: { name: string } + * ``` + * + * @category types + * @since 4.0.0 + */ + export type PForMatch

= [ResolvePred

] extends [infer X] ? X + : never + + /** + * Computes the excluded type when a pattern P is used for exclusion. + * + * **Details** + * + * This utility type determines what should be excluded from a union type + * when a pattern is used in filtering operations. It transforms patterns + * into their exclusion-safe representations. + * + * **Example** (Computing excluded patterns) + * + * ```ts + * import type { Match } from "effect" + * + * // PForExclude computes what to exclude from type operations + * type ExcludeString = Match.Types.PForExclude + * // Used internally to filter out string types + * + * type ExcludeObject = Match.Types.PForExclude<{ type: "admin" }> + * // Used internally to filter out admin objects + * ``` + * + * @category types + * @since 4.0.0 + */ + export type PForExclude

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

+ : A extends Predicate.Predicate ? SafeRefinement + : A extends SafeRefinement ? SafeRefinement<_R> + : A extends Record ? { [K in keyof A]: ToInvertedRefinement } + : NonLiteralsTo + + type NonLiteralsTo = [A] extends [string | number | boolean | bigint] ? [string] extends [A] ? T + : [number] extends [A] ? T + : [boolean] extends [A] ? T + : [bigint] extends [A] ? T + : A + : A + + /** + * Defines the structure for complex object and array patterns. + * + * **Details** + * + * This type represents patterns that can match against complex data structures + * like objects and arrays. It supports nested pattern matching and partial + * object matching, enabling sophisticated pattern compositions. + * + * **Example** (Describing complex object patterns) + * + * ```ts + * import { Match } from "effect" + * + * // PatternBase enables complex object patterns + * type UserPattern = Match.Types.PatternBase<{ + * name: string + * age: number + * role: "admin" | "user" + * }> + * // Allows: { name?: string | Predicate, age?: number | Predicate, ... } + * + * // Example usage: + * Match.value({ name: "Alice", age: 30, role: "admin" as const }).pipe( + * Match.when( + * { age: (n: number) => n >= 18, role: "admin" }, + * (user: { name: string; age: number; role: "admin" }) => + * `Admin: ${user.name}` + * ), + * Match.orElse(() => "Not an adult admin") + * ) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type PatternBase = A extends ReadonlyArray ? ReadonlyArray | PatternPrimitive + : A extends Record ? Partial< + { [K in keyof A]: PatternPrimitive | PatternBase } + > + : never + + /** + * Defines primitive patterns that can match simple values. + * + * **Details** + * + * This type represents the building blocks of pattern matching: predicates, + * literal values, and safe refinements. These are the atomic patterns that + * can be composed into more complex matching logic. + * + * @category types + * @since 4.0.0 + */ + export type PatternPrimitive = PredicateA | A | SafeRefinement + + /** + * Represents a filter that excludes specific types from a union. + * + * **Details** + * + * `Without` is used internally to track which types should be excluded + * from consideration during pattern matching. It helps implement the + * type-level logic for `Match.not` and other exclusion operations. + * + * **Example** (Tracking excluded types) + * + * ```ts + * import { Match } from "effect" + * + * // Without is used internally when you write: + * Match.type().pipe( + * Match.not(Match.string, (value) => `not string: ${value}`), + * // At this point, type system uses Without to track exclusion + * Match.orElse(() => "was a string") + * ) + * ``` + * + * @category types + * @since 4.0.0 + */ + export interface Without { + readonly _tag: "Without" + readonly _X: X + } + + /** + * Represents a filter that includes only specific types from a union. + * + * **Details** + * + * `Only` is used internally to track which types should be exclusively + * considered during pattern matching. It helps implement the type-level + * logic for positive matches and type narrowing. + * + * **Example** (Tracking included types) + * + * ```ts + * import { Match } from "effect" + * + * // Only is used internally when you write: + * Match.type().pipe( + * Match.when(Match.string, (s) => `string: ${s}`), + * // At this point, type system uses Only for the match + * Match.orElse((value) => `not string: ${value}`) + * ) + * ``` + * + * @category types + * @since 4.0.0 + */ + export interface Only { + readonly _tag: "Only" + readonly _X: X + } + + /** + * Adds a type to the exclusion filter, expanding what should be filtered out. + * + * **Details** + * + * This utility type manages the accumulation of excluded types during + * pattern matching. When multiple exclusions are applied, it combines + * them into a single filter representation. + * + * **Example** (Accumulating excluded types) + * + * ```ts + * import { Match } from "effect" + * + * // AddWithout is used when combining multiple exclusions: + * Match.type().pipe( + * Match.not(Match.string, () => "not string"), + * Match.not(Match.number, () => "not number"), + * // Type system uses AddWithout to combine exclusions + * Match.orElse(() => "was string or number") + * ) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type AddWithout = [A] extends [Without] ? Without + : [A] extends [Only] ? Only> + : never + + /** + * Adds a type to the inclusion filter, refining what should be included. + * + * **Details** + * + * This utility type manages the refinement of included types during + * pattern matching. It ensures that only the most specific type + * constraints are maintained when multiple positive matches are applied. + * + * **Example** (Refining included types) + * + * ```ts + * import { Match } from "effect" + * + * // AddOnly is used when refining positive matches: + * Match.type<{ type: "user" | "admin"; name: string }>().pipe( + * Match.when({ type: "admin" }, (admin) => admin.name), + * // Type system uses AddOnly to refine the constraint + * Match.orElse(() => "not admin") + * ) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type AddOnly = [A] extends [Without] ? [X] extends [WX] ? never + : Only + : [A] extends [Only] ? [X] extends [OX] ? Only + : never + : never + + /** + * Applies accumulated filters to an input type, producing the final narrowed type. + * + * **Details** + * + * This utility type takes the collected inclusion/exclusion filters and + * applies them to the input type to compute the final narrowed result. + * It's the culmination of the type-level filtering process. + * + * **Example** (Applying accumulated filters) + * + * ```ts + * import type { Match } from "effect" + * + * // ApplyFilters computes the final narrowed type: + * type Result = Match.Types.ApplyFilters< + * string | number | boolean, + * Match.Types.Only + * > + * // Result: string + * + * type ExclusionResult = Match.Types.ApplyFilters< + * string | number | boolean, + * Match.Types.Without + * > + * // Result: number | boolean + * ``` + * + * @category types + * @since 4.0.0 + */ + export type ApplyFilters = A extends Only ? X + : A extends Without ? Exclude + : never + + /** + * Extracts tag values from a discriminated union based on a discriminant field. + * + * **Details** + * + * This utility type extracts the possible values of a discriminant field + * from a union type. It's used internally to implement tag-based pattern + * matching for discriminated unions. + * + * **Example** (Extracting discriminator tags) + * + * ```ts + * import type { Match } from "effect" + * + * type Events = + * | { _tag: "click"; x: number; y: number } + * | { _tag: "keypress"; key: string } + * | { _tag: "scroll"; delta: number } + * + * type EventTags = Match.Types.Tags<"_tag", Events> + * // Result: "click" | "keypress" | "scroll" + * + * type CustomTags = Match.Types.Tags< + * "type", + * | { type: "user"; name: string } + * | { type: "admin"; permissions: Array } + * > + * // Result: "user" | "admin" + * ``` + * + * @category types + * @since 4.0.0 + */ + export type Tags = P extends Record ? X : never + + /** + * Converts an array type to an intersection of its element types. + * + * **Details** + * + * This utility type takes an array of types and converts them into a single + * intersection type. It's used internally when multiple patterns need to + * be satisfied simultaneously (like in `Match.whenAnd`). + * + * **Example** (Converting arrays to intersections) + * + * ```ts + * import type { Match } from "effect" + * + * type Combined = Match.Types.ArrayToIntersection<[ + * { name: string }, + * { age: number }, + * { active: boolean } + * ]> + * // Result: { name: string } & { age: number } & { active: boolean } + * // = { name: string; age: number; active: boolean } + * + * // This type utility enables complex type intersections + * // Complex type operations are handled by this utility type + * // for advanced pattern matching scenarios + * ``` + * + * @category types + * @since 4.0.0 + */ + export type ArrayToIntersection> = T.UnionToIntersection< + A[number] + > + + /** + * Extracts and narrows the matched type from an input type given a pattern. + * + * **Details** + * + * This is the core type utility that performs the actual type extraction + * and narrowing logic. It handles the complex type-level computation that + * determines what type results from applying a pattern to an input type. + * + * **Example** (Extracting matched types) + * + * ```ts + * import { Match } from "effect" + * + * type StringExtract = Match.Types.ExtractMatch< + * string | number | boolean, + * typeof Match.string + * > + * // Result: string + * + * type ObjectExtract = Match.Types.ExtractMatch< + * { type: "user"; name: string } | { type: "admin"; role: string }, + * { type: "user" } + * > + * // Result: { type: "user"; name: string } + * + * // This powers the type narrowing in: + * Match.when(Match.string, (s) => s.toUpperCase()) + * // ^^^ s is correctly typed as string + * ``` + * + * @category types + * @since 4.0.0 + */ + export type ExtractMatch = [ExtractAndNarrow] extends [infer EI] ? EI + : never + + type Replace = A extends Function ? A + : A extends Record ? { [K in keyof A]: K extends keyof B ? Replace : A[K] } + : [B] extends [A] ? B + : A + + type MaybeReplace = [P] extends [I] ? P + : [I] extends [P] ? Replace + : Fail + + type BuiltInObjects = + | Function + | Date + | RegExp + | Generator + | { readonly [Symbol.toStringTag]: string } + + type IsPlainObject = T extends BuiltInObjects ? false + : T extends Record ? true + : false + + type Simplify = { [K in keyof A]: A[K] } & {} + + type ExtractAndNarrow = P extends Predicate.Refinement ? + _Out extends Input ? Extract<_Out, Input> + : Extract : + P extends SafeRefinement ? [0] extends [1 & _R] ? Input + : _In extends Input ? Extract<_In, Input> + : Extract + : P extends Predicate.Predicate ? Extract + : Input extends infer I ? Exclude< + I extends ReadonlyArray ? P extends ReadonlyArray ? { + readonly [K in keyof I]: K extends keyof P ? ExtractAndNarrow + : I[K] + } extends infer R ? Fail extends R[keyof R] ? never + : R + : never + : never + : IsPlainObject extends true ? string extends keyof I ? I extends P ? I + : never + : symbol extends keyof I ? I extends P ? I + : never + : Simplify< + & { [RK in Extract]-?: ExtractAndNarrow } + & Omit + > extends infer R ? keyof P extends NonFailKeys ? R + : never + : never + : MaybeReplace extends infer R ? [I] extends [R] ? I + : R + : never, + Fail + > : + never + + type NonFailKeys = keyof A & {} extends infer K ? K extends keyof A ? A[K] extends Fail ? never : K + : never : + never +} diff --git a/repos/effect/packages/effect/src/Metric.ts b/repos/effect/packages/effect/src/Metric.ts new file mode 100644 index 0000000..d1a1cd4 --- /dev/null +++ b/repos/effect/packages/effect/src/Metric.ts @@ -0,0 +1,4058 @@ +/** + * Records and reads measurements from Effect programs. + * + * A `Metric` accepts typed update values and stores an aggregated + * state that can be read directly or included in a snapshot. Metrics are used + * for counters, gauges, frequencies, histograms, summaries, and timers. This + * module includes metric constructors, update and read helpers, attributes, + * histogram boundaries, registry snapshots, text dumps, and controls for + * enabling runtime metrics. + * + * @since 2.0.0 + */ + +import * as Arr from "./Array.ts" +import * as Context from "./Context.ts" +import * as Duration from "./Duration.ts" +import type { Effect } from "./Effect.ts" +import type { Exit } from "./Exit.ts" +import { constUndefined, dual } from "./Function.ts" +import * as InternalEffect from "./internal/effect.ts" +import * as InternalMetric from "./internal/metric.ts" +import * as Layer from "./Layer.ts" +import * as Order from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as _String from "./String.ts" +import type { Contravariant, Covariant } from "./Types.ts" + +/** + * A `Metric` represents a concurrent metric which accepts update + * values of type `Input` and are aggregated to a value of type `State`. + * + * **Details** + * + * For example, a counter metric would have type `Metric`, + * representing the fact that the metric can be updated with numbers (the amount + * to increment or decrement the counter by), and the state of the counter is a + * number. + * + * There are five primitive metric types supported by Effect: + * + * - Counters + * - Frequencies + * - Gauges + * - Histograms + * - Summaries + * + * **Example** (Using multiple metric types) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricExample extends Data.TaggedError("MetricExample")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of metrics + * const requestCounter: Metric.Counter = Metric.counter("requests", { + * description: "Total requests processed" + * }) + * + * const memoryGauge: Metric.Gauge = Metric.gauge("memory_usage", { + * description: "Current memory usage in MB" + * }) + * + * const statusFrequency: Metric.Frequency = Metric.frequency("status_codes", { + * description: "HTTP status code frequency" + * }) + * + * // All metrics share the same interface for updates and reads + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(memoryGauge, 128) + * yield* Metric.update(statusFrequency, "200") + * + * // All metrics can be read with Metric.value + * const counterState = yield* Metric.value(requestCounter) + * const gaugeState = yield* Metric.value(memoryGauge) + * const frequencyState = yield* Metric.value(statusFrequency) + * + * // Metrics have common properties accessible through the interface: + * // - id: unique identifier + * // - type: metric type ("Counter", "Gauge", "Frequency", etc.) + * // - description: optional human-readable description + * // - attributes: optional key-value attributes for tagging + * + * return { + * counter: { + * id: requestCounter.id, + * type: requestCounter.type, + * state: counterState + * }, + * gauge: { id: memoryGauge.id, type: memoryGauge.type, state: gaugeState }, + * frequency: { + * id: statusFrequency.id, + * type: statusFrequency.type, + * state: frequencyState + * } + * } + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Metric extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly Input: Contravariant + readonly State: Covariant + readonly id: string + readonly type: Metric.Type + readonly description: string | undefined + readonly attributes: Metric.AttributeSet | undefined + readonly valueUnsafe: (context: Context.Context) => State + readonly updateUnsafe: (input: Input, context: Context.Context) => void + readonly modifyUnsafe: (input: Input, context: Context.Context) => void +} + +/** + * A Counter metric that tracks cumulative values that typically only increase. + * + * **When to use** + * + * Use when counters are useful for tracking monotonically increasing values like request counts, + * bytes processed, errors encountered, or any value that accumulates over time. + * + * **Example** (Using counter metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class CounterInterfaceError extends Data.TaggedError("CounterInterfaceError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of counters + * const requestCounter: Metric.Counter = Metric.counter( + * "http_requests", + * { + * description: "Total HTTP requests processed", + * incremental: true // Only allows increments + * } + * ) + * + * const bytesCounter: Metric.Counter = Metric.counter( + * "bytes_processed", + * { + * description: "Total bytes processed", + * bigint: true, + * attributes: { service: "data-processor" } + * } + * ) + * + * // Update counters + * yield* Metric.update(requestCounter, 1) // Increment by 1 + * yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6) + * yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes + * + * // Read counter state + * const requestState: Metric.CounterState = yield* Metric.value( + * requestCounter + * ) + * const bytesState: Metric.CounterState = yield* Metric.value( + * bytesCounter + * ) + * + * // Counter state contains: + * // - count: current accumulated value + * // - incremental: whether only increments are allowed + * + * return { + * requests: { + * count: requestState.count, + * incremental: requestState.incremental + * }, + * bytes: { count: bytesState.count, incremental: bytesState.incremental } + * } + * }) + * ``` + * + * @category metrics + * @since 2.0.0 + */ +export interface Counter extends Metric> {} + +/** + * State interface for Counter metrics containing the current count and increment mode. + * + * **Example** (Reading counter state) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class CounterStateError extends Data.TaggedError("CounterStateError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of counters + * const requestCounter = Metric.counter("http_requests_total") + * const errorCounter = Metric.counter("errors_total", { incremental: true }) + * const byteCounter = Metric.counter("bytes_processed", { bigint: true }) + * + * // Update counters + * yield* Metric.update(requestCounter, 5) // Add 5 requests + * yield* Metric.update(requestCounter, -2) // Subtract 2 (allowed for non-incremental) + * yield* Metric.update(errorCounter, 3) // Add 3 errors + * yield* Metric.update(errorCounter, -1) // Attempt to subtract (ignored for incremental) + * yield* Metric.update(byteCounter, 1024000n) // Add bytes as bigint + * + * // Read counter states + * const requestState: Metric.CounterState = yield* Metric.value( + * requestCounter + * ) + * const errorState: Metric.CounterState = yield* Metric.value( + * errorCounter + * ) + * const byteState: Metric.CounterState = yield* Metric.value( + * byteCounter + * ) + * + * // CounterState contains: + * // - count: current count value (number or bigint based on counter type) + * // - incremental: whether counter only allows increases + * + * return { + * requests: { + * total: requestState.count, // 3 (5 - 2, decrements allowed) + * canDecrease: !requestState.incremental // true + * }, + * errors: { + * total: errorState.count, // 3 (subtract ignored) + * canDecrease: !errorState.incremental // false + * }, + * bytes: { + * total: byteState.count, // 1024000n + * canDecrease: !byteState.incremental // true + * } + * } + * }) + * ``` + * + * @category Counter + * @since 4.0.0 + */ +export interface CounterState { + readonly count: Input extends bigint ? bigint : number + readonly incremental: boolean +} + +/** + * A Frequency metric interface that counts occurrences of discrete string values. + * + * **When to use** + * + * Use when frequency metrics are ideal for tracking categorical data where you want to count + * how many times specific string values occur, such as HTTP status codes, user actions, + * error types, or any discrete string-based events. + * + * **Example** (Using frequency metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class FrequencyInterfaceError + * extends Data.TaggedError("FrequencyInterfaceError")<{ + * readonly operation: string + * }> + * {} + * + * // Function that accepts any Frequency metric + * const logFrequencyMetric = (freq: Metric.Frequency) => + * Effect.gen(function*() { + * const state = yield* Metric.value(freq) + * + * yield* Effect.log(`Frequency Metric: ${freq.id}`) + * yield* Effect.log(`Description: ${freq.description ?? "No description"}`) + * yield* Effect.log(`Type: ${freq.type}`) // "Frequency" + * + * // Access the frequency state + * const occurrences: ReadonlyMap = state.occurrences + * yield* Effect.log(`Total unique values: ${occurrences.size}`) + * + * // Iterate through all occurrences + * for (const [value, count] of occurrences) { + * yield* Effect.log(` "${value}": ${count} occurrences`) + * } + * + * // Find most frequent value + * let maxCount = 0 + * let mostFrequent = "" + * for (const [value, count] of occurrences) { + * if (count > maxCount) { + * maxCount = count + * mostFrequent = value + * } + * } + * + * return { mostFrequent, maxCount, totalUniqueValues: occurrences.size } + * }) + * + * const program = Effect.gen(function*() { + * // Create frequency metrics + * const statusCodes: Metric.Frequency = Metric.frequency("http_status", { + * description: "HTTP status code frequency" + * }) + * + * const userActions: Metric.Frequency = Metric.frequency("user_actions", { + * description: "User action frequency" + * }) + * + * // Record some occurrences + * yield* Metric.update(statusCodes, "200") + * yield* Metric.update(statusCodes, "200") + * yield* Metric.update(statusCodes, "404") + * yield* Metric.update(statusCodes, "500") + * yield* Metric.update(statusCodes, "200") + * + * yield* Metric.update(userActions, "login") + * yield* Metric.update(userActions, "view_dashboard") + * yield* Metric.update(userActions, "login") + * + * // Use the function with different frequency metrics + * const statusAnalysis = yield* logFrequencyMetric(statusCodes) + * const actionAnalysis = yield* logFrequencyMetric(userActions) + * + * return { statusAnalysis, actionAnalysis } + * }) + * ``` + * + * @category metrics + * @since 2.0.0 + */ +export interface Frequency extends Metric {} + +/** + * State interface for Frequency metrics containing occurrence counts for discrete string values. + * + * **Example** (Reading frequency state) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class FrequencyStateError extends Data.TaggedError("FrequencyStateError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create frequency metrics for different categories + * const statusCodeFreq = Metric.frequency("http_status_codes", { + * description: "HTTP status code distribution" + * }) + * + * const userActionFreq = Metric.frequency("user_actions", { + * description: "User action frequency" + * }) + * + * // Record occurrences + * yield* Metric.update(statusCodeFreq, "200") // Success + * yield* Metric.update(statusCodeFreq, "200") // Another success + * yield* Metric.update(statusCodeFreq, "404") // Not found + * yield* Metric.update(statusCodeFreq, "500") // Server error + * yield* Metric.update(statusCodeFreq, "200") // Another success + * + * yield* Metric.update(userActionFreq, "login") + * yield* Metric.update(userActionFreq, "click") + * yield* Metric.update(userActionFreq, "login") + * yield* Metric.update(userActionFreq, "scroll") + * yield* Metric.update(userActionFreq, "click") + * yield* Metric.update(userActionFreq, "click") + * + * // Read frequency states + * const statusState: Metric.FrequencyState = yield* Metric.value(statusCodeFreq) + * const actionState: Metric.FrequencyState = yield* Metric.value(userActionFreq) + * + * // FrequencyState contains: + * // - occurrences: ReadonlyMap with string values and their counts + * + * // Analyze frequency distributions + * const getMostFrequent = (occurrences: ReadonlyMap) => { + * let maxKey = "" + * let maxCount = 0 + * for (const [key, count] of occurrences) { + * if (count > maxCount) { + * maxKey = key + * maxCount = count + * } + * } + * return { key: maxKey, count: maxCount } + * } + * + * const topStatus = getMostFrequent(statusState.occurrences) + * const topAction = getMostFrequent(actionState.occurrences) + * + * return { + * statusCodes: { + * totalResponses: Array.from(statusState.occurrences.values()).reduce( + * (a, b) => a + b, + * 0 + * ), // 5 + * mostCommon: topStatus, // { key: "200", count: 3 } + * uniqueCodes: statusState.occurrences.size // 3 + * }, + * userActions: { + * totalActions: Array.from(actionState.occurrences.values()).reduce( + * (a, b) => a + b, + * 0 + * ), // 6 + * mostCommon: topAction, // { key: "click", count: 3 } + * uniqueActions: actionState.occurrences.size // 3 + * } + * } + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export interface FrequencyState { + readonly occurrences: ReadonlyMap +} + +/** + * A Gauge metric that tracks instantaneous values that can go up or down. + * + * **When to use** + * + * Use when gauges are useful for tracking current state values like memory usage, CPU load, + * active connections, queue sizes, or any value that represents a current level. + * + * **Example** (Using gauge metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class GaugeInterfaceError extends Data.TaggedError("GaugeInterfaceError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of gauges + * const memoryGauge: Metric.Gauge = Metric.gauge("memory_usage_mb", { + * description: "Current memory usage in megabytes" + * }) + * + * const diskSpaceGauge: Metric.Gauge = Metric.gauge("disk_free_bytes", { + * description: "Available disk space in bytes", + * bigint: true, + * attributes: { mount: "/var" } + * }) + * + * // Set gauge values (absolute values) + * yield* Metric.update(memoryGauge, 512) // Set to 512 MB + * yield* Metric.update(memoryGauge, 640) // Set to 640 MB (replaces 512) + * yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to ~5GB free + * + * // Modify gauge values (relative changes) + * yield* Metric.modify(memoryGauge, 128) // Add 128 MB (total: 768) + * yield* Metric.modify(memoryGauge, -64) // Subtract 64 MB (total: 704) + * + * // Read gauge state + * const memoryState: Metric.GaugeState = yield* Metric.value( + * memoryGauge + * ) + * const diskState: Metric.GaugeState = yield* Metric.value( + * diskSpaceGauge + * ) + * + * // Gauge state contains: + * // - value: current instantaneous value + * + * return { + * memory: { currentValue: memoryState.value }, // 704 + * disk: { currentValue: diskState.value } // 5000000000n + * } + * }) + * ``` + * + * @category metrics + * @since 2.0.0 + */ +export interface Gauge extends Metric> {} + +/** + * State interface for Gauge metrics containing the current instantaneous value. + * + * **Example** (Reading gauge state) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class GaugeStateError extends Data.TaggedError("GaugeStateError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of gauges + * const temperatureGauge = Metric.gauge("room_temperature_celsius", { + * description: "Current room temperature" + * }) + * + * const diskSpaceGauge = Metric.gauge("disk_usage_bytes", { + * description: "Current disk usage", + * bigint: true + * }) + * + * const queueSizeGauge = Metric.gauge("queue_size", { + * description: "Current queue size" + * }) + * + * // Set gauge values (absolute values) + * yield* Metric.update(temperatureGauge, 22.5) // Set to 22.5°C + * yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to 5GB usage + * yield* Metric.update(queueSizeGauge, 10) // Set to 10 items + * + * // Update gauge values (new absolute values) + * yield* Metric.update(temperatureGauge, 23.1) // Temperature changed + * yield* Metric.update(queueSizeGauge, 15) // Queue grew + * + * // Read gauge states + * const tempState: Metric.GaugeState = yield* Metric.value( + * temperatureGauge + * ) + * const diskState: Metric.GaugeState = yield* Metric.value( + * diskSpaceGauge + * ) + * const queueState: Metric.GaugeState = yield* Metric.value( + * queueSizeGauge + * ) + * + * // GaugeState contains: + * // - value: current instantaneous value (number or bigint based on gauge type) + * + * return { + * environment: { + * temperature: tempState.value, // 23.1 + * temperatureUnit: "°C" + * }, + * system: { + * diskUsage: diskState.value, // 5000000000n + * diskUsageGB: Number(diskState.value) / 1_000_000_000, // 5 + * queueSize: queueState.value // 15 + * } + * } + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export interface GaugeState { + readonly value: Input extends bigint ? bigint : number +} + +/** + * A Histogram metric that records observations in configurable buckets to analyze value distributions. + * + * **When to use** + * + * Use when histograms are ideal for measuring request durations, response sizes, and other continuous values + * where you need to understand the distribution of values rather than just aggregates. + * + * **Example** (Using histogram metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class HistogramInterfaceError + * extends Data.TaggedError("HistogramInterfaceError")<{ + * readonly operation: string + * }> + * {} + * + * const program = Effect.gen(function*() { + * // Create histograms with different boundary strategies + * const responseTimeHistogram: Metric.Histogram = Metric.histogram( + * "http_response_time_ms", + * { + * description: "HTTP response time distribution in milliseconds", + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) // 0, 50, 100, ..., 950 + * } + * ) + * + * const fileSizeHistogram: Metric.Histogram = Metric.histogram( + * "file_size_bytes", + * { + * description: "File size distribution in bytes", + * boundaries: Metric.exponentialBoundaries({ + * start: 1, + * factor: 2, + * count: 10 + * }) // 1, 2, 4, 8, ..., 512 + * } + * ) + * + * // Record observations (values get placed into appropriate buckets) + * yield* Metric.update(responseTimeHistogram, 125) // Goes into 100-150ms bucket + * yield* Metric.update(responseTimeHistogram, 75) // Goes into 50-100ms bucket + * yield* Metric.update(responseTimeHistogram, 200) // Goes into 150-200ms bucket + * yield* Metric.update(responseTimeHistogram, 45) // Goes into 0-50ms bucket + * + * yield* Metric.update(fileSizeHistogram, 3) // Goes into 2-4 bytes bucket + * yield* Metric.update(fileSizeHistogram, 15) // Goes into 8-16 bytes bucket + * yield* Metric.update(fileSizeHistogram, 100) // Goes into 64-128 bytes bucket + * + * // Read histogram state + * const responseTimeState: Metric.HistogramState = yield* Metric.value( + * responseTimeHistogram + * ) + * const fileSizeState: Metric.HistogramState = yield* Metric.value( + * fileSizeHistogram + * ) + * + * // Histogram state contains: + * // - buckets: Array of [boundary, cumulativeCount] pairs + * // - count: total number of observations + * // - min: smallest observed value + * // - max: largest observed value + * // - sum: sum of all observed values + * + * return { + * responseTime: { + * totalRequests: responseTimeState.count, // 4 + * fastestRequest: responseTimeState.min, // 45 + * slowestRequest: responseTimeState.max, // 200 + * totalTime: responseTimeState.sum, // 445 + * averageTime: responseTimeState.sum / responseTimeState.count // 111.25 + * }, + * fileSize: { + * totalFiles: fileSizeState.count, // 3 + * smallestFile: fileSizeState.min, // 3 + * largestFile: fileSizeState.max, // 100 + * totalBytes: fileSizeState.sum // 118 + * } + * } + * }) + * ``` + * + * @category metrics + * @since 2.0.0 + */ +export interface Histogram extends Metric {} + +/** + * State interface for Histogram metrics containing bucket distributions and aggregate statistics. + * + * **Example** (Reading histogram state) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class HistogramStateError extends Data.TaggedError("HistogramStateError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create histogram with linear boundaries + * const responseTimeHistogram = Metric.histogram("api_response_time_ms", { + * description: "API response time distribution", + * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) // 0, 100, 200, ..., 900 + * }) + * + * // Record observations + * yield* Metric.update(responseTimeHistogram, 50) // Fast response + * yield* Metric.update(responseTimeHistogram, 150) // Average response + * yield* Metric.update(responseTimeHistogram, 750) // Slow response + * yield* Metric.update(responseTimeHistogram, 250) // Average response + * yield* Metric.update(responseTimeHistogram, 95) // Fast response + * + * // Read histogram state + * const state: Metric.HistogramState = yield* Metric.value( + * responseTimeHistogram + * ) + * + * // HistogramState contains: + * // - buckets: Array of [boundary, cumulativeCount] pairs showing distribution + * // - count: total number of observations + * // - min: smallest observed value + * // - max: largest observed value + * // - sum: sum of all observed values + * + * // Analyze bucket distribution + * const analyzeBuckets = (buckets: ReadonlyArray<[number, number]>) => { + * const analysis: Array< + * { range: string; count: number; percentage: number } + * > = [] + * let previousCount = 0 + * const totalCount = buckets[buckets.length - 1]?.[1] ?? 0 + * + * for (let i = 0; i < buckets.length; i++) { + * const [boundary, cumulativeCount] = buckets[i] + * const bucketCount = cumulativeCount - previousCount + * const percentage = totalCount > 0 ? (bucketCount / totalCount) * 100 : 0 + * const prevBoundary = i === 0 ? 0 : buckets[i - 1][0] + * + * analysis.push({ + * range: `${prevBoundary}-${boundary}ms`, + * count: bucketCount, + * percentage: Math.round(percentage * 10) / 10 + * }) + * previousCount = cumulativeCount + * } + * return analysis + * } + * + * const bucketAnalysis = analyzeBuckets(state.buckets) + * + * return { + * responseTime: { + * totalRequests: state.count, // 5 + * fastestResponse: state.min, // 50 + * slowestResponse: state.max, // 750 + * averageResponse: state.sum / state.count, // 268 + * totalTime: state.sum, // 1340 + * distribution: bucketAnalysis + * // Example distribution: + * // [{ range: "0-100ms", count: 2, percentage: 40.0 }, + * // { range: "100-200ms", count: 1, percentage: 20.0 }, + * // { range: "200-300ms", count: 1, percentage: 20.0 }, + * // { range: "700-800ms", count: 1, percentage: 20.0 }] + * } + * } + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export interface HistogramState { + readonly buckets: ReadonlyArray<[number, number]> + readonly count: number + readonly min: number + readonly max: number + readonly sum: number +} + +/** + * A Summary metric that calculates quantiles over a sliding time window of observations. + * + * **When to use** + * + * Use when summaries provide statistical insights into value distributions by tracking specific quantiles + * (percentiles) such as median (50th), 95th percentile, 99th percentile, etc. They're ideal for + * understanding performance characteristics like response time distributions. + * + * **Example** (Using summary metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class SummaryInterfaceError extends Data.TaggedError("SummaryInterfaceError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create summaries with different quantile configurations + * const responseTimeSummary: Metric.Summary = Metric.summary( + * "api_response_time_ms", + * { + * description: "API response time distribution in milliseconds", + * maxAge: "5 minutes", // Keep observations for 5 minutes + * maxSize: 1000, // Keep up to 1000 observations + * quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles + * } + * ) + * + * const requestSizeSummary: Metric.Summary = Metric.summary( + * "request_size_bytes", + * { + * description: "Request payload size distribution", + * maxAge: "10 minutes", + * maxSize: 500, + * quantiles: [0.25, 0.5, 0.75, 0.9] // Track quartiles and 90th percentile + * } + * ) + * + * // Record observations (values are stored in time-based sliding window) + * yield* Metric.update(responseTimeSummary, 120) // Fast response + * yield* Metric.update(responseTimeSummary, 250) // Average response + * yield* Metric.update(responseTimeSummary, 45) // Very fast response + * yield* Metric.update(responseTimeSummary, 890) // Slow response + * yield* Metric.update(responseTimeSummary, 156) // Average response + * + * yield* Metric.update(requestSizeSummary, 1024) // 1KB request + * yield* Metric.update(requestSizeSummary, 512) // 512B request + * yield* Metric.update(requestSizeSummary, 2048) // 2KB request + * + * // Read summary state + * const responseTimeState: Metric.SummaryState = yield* Metric.value( + * responseTimeSummary + * ) + * const requestSizeState: Metric.SummaryState = yield* Metric.value( + * requestSizeSummary + * ) + * + * // Summary state contains: + * // - quantiles: Array of [quantile, optionalValue] pairs + * // - count: total number of observations in window + * // - min: smallest observed value in window + * // - max: largest observed value in window + * // - sum: sum of all observed values in window + * + * // Extract quantile values safely + * const getQuantileValue = ( + * quantiles: ReadonlyArray, + * q: number + * ) => quantiles.find(([quantile]) => quantile === q)?.[1] + * + * const median = getQuantileValue(responseTimeState.quantiles, 0.5) + * const p95 = getQuantileValue(responseTimeState.quantiles, 0.95) + * const p99 = getQuantileValue(responseTimeState.quantiles, 0.99) + * + * return { + * responseTime: { + * totalRequests: responseTimeState.count, // 5 + * fastestResponse: responseTimeState.min, // 45 + * slowestResponse: responseTimeState.max, // 890 + * totalTime: responseTimeState.sum, // 1461 + * averageTime: responseTimeState.sum / responseTimeState.count, // 292.2 + * medianTime: median ?? null, // ~156 + * p95Time: p95 ?? null, // ~890 + * p99Time: p99 ?? null // ~890 + * }, + * requestSize: { + * totalRequests: requestSizeState.count, // 3 + * averageSize: requestSizeState.sum / requestSizeState.count // ~1194.7 + * } + * } + * }) + * ``` + * + * @category metrics + * @since 2.0.0 + */ +export interface Summary extends Metric {} + +/** + * State interface for Summary metrics containing quantile calculations and aggregate statistics. + * + * **Example** (Reading summary state) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class SummaryStateError extends Data.TaggedError("SummaryStateError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create summary with specific quantiles + * const responseTimeSummary = Metric.summary("api_response_latency", { + * description: "API response time distribution with quantiles", + * maxAge: "5 minutes", + * maxSize: 1000, + * quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles + * }) + * + * // Record observations over time + * yield* Metric.update(responseTimeSummary, 120) // Fast response + * yield* Metric.update(responseTimeSummary, 250) // Average response + * yield* Metric.update(responseTimeSummary, 45) // Very fast response + * yield* Metric.update(responseTimeSummary, 890) // Slow response + * yield* Metric.update(responseTimeSummary, 156) // Average response + * yield* Metric.update(responseTimeSummary, 78) // Fast response + * yield* Metric.update(responseTimeSummary, 340) // Slower response + * + * // Read summary state + * const state: Metric.SummaryState = yield* Metric.value(responseTimeSummary) + * + * // SummaryState contains: + * // - quantiles: Array of [quantile, optionalValue] pairs showing percentile values + * // - count: total number of observations in current window + * // - min: smallest observed value in window + * // - max: largest observed value in window + * // - sum: sum of all observed values in window + * + * // Extract quantile information safely + * const extractQuantiles = ( + * quantiles: ReadonlyArray + * ) => { + * const result: Record = {} + * for (const [quantile, valueOption] of quantiles) { + * const percentile = Math.round(quantile * 100) + * result[`p${percentile}`] = valueOption ?? null + * } + * return result + * } + * + * const quantileValues = extractQuantiles(state.quantiles) + * + * return { + * latencyAnalysis: { + * totalRequests: state.count, // 7 + * fastestResponse: state.min, // 45 + * slowestResponse: state.max, // 890 + * averageResponse: state.sum / state.count, // ~268.4 + * totalLatency: state.sum, // 1879 + * percentiles: quantileValues, + * // Example percentiles: + * // { p50: 156, p95: 890, p99: 890 } + * performance: { + * fast: quantileValues.p50 !== null && quantileValues.p50 < 200 + * ? "Good" + * : "Needs improvement", + * reliability: quantileValues.p95 !== null && quantileValues.p95 < 500 + * ? "Reliable" + * : "Concerning" + * } + * } + * } + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export interface SummaryState { + readonly quantiles: ReadonlyArray + readonly count: number + readonly min: number + readonly max: number + readonly sum: number +} + +/** + * The `Metric` namespace provides a comprehensive system for collecting, aggregating, and observing + * application metrics in Effect applications. + * + * **Example** (Collecting application metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of metrics + * const requestCounter = Metric.counter("http_requests_total") + * const responseTimeHistogram = Metric.histogram("http_response_time", { + * boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 10 }) + * }) + * const activeConnectionsGauge = Metric.gauge("active_connections") + * const statusFrequency = Metric.frequency("http_status_codes") + * + * // Update metrics + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(responseTimeHistogram, 45.2) + * yield* Metric.update(activeConnectionsGauge, 12) + * yield* Metric.update(statusFrequency, "200") + * + * // Get metric values + * const counterValue = yield* Metric.value(requestCounter) + * const histogramValue = yield* Metric.value(responseTimeHistogram) + * const gaugeValue = yield* Metric.value(activeConnectionsGauge) + * const frequencyValue = yield* Metric.value(statusFrequency) + * + * return { + * counter: counterValue, + * histogram: histogramValue, + * gauge: gaugeValue, + * frequency: frequencyValue + * } + * }) + * ``` + * + * @since 2.0.0 + */ +export declare namespace Metric { + /** + * Union type representing all available metric types in the Effect metrics system. + * + * **Example** (Inspecting metric types) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricTypeError extends Data.TaggedError("MetricTypeError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different metric types + * const counter = Metric.counter("requests_total") + * const gauge = Metric.gauge("cpu_usage") + * const frequency = Metric.frequency("status_codes") + * const histogram = Metric.histogram("response_time", { + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) + * }) + * const summary = Metric.summary("latency", { + * maxAge: "5 minutes", + * maxSize: 1000, + * quantiles: [0.5, 0.95, 0.99] + * }) + * + * // Function that checks metric type + * const getMetricInfo = (metric: Metric.Metric) => ({ + * name: metric.id, + * type: metric.type + * }) + * + * // Get type information for each metric + * const counterInfo = getMetricInfo(counter) // { name: "requests_total", type: "Counter" } + * const gaugeInfo = getMetricInfo(gauge) // { name: "cpu_usage", type: "Gauge" } + * const frequencyInfo = getMetricInfo(frequency) // { name: "status_codes", type: "Frequency" } + * const histogramInfo = getMetricInfo(histogram) // { name: "response_time", type: "Histogram" } + * const summaryInfo = getMetricInfo(summary) // { name: "latency", type: "Summary" } + * + * // Pattern match on metric type + * const describeMetric = (type: string): string => { + * switch (type) { + * case "Counter": + * return "Cumulative values that increase over time" + * case "Gauge": + * return "Instantaneous values that can go up or down" + * case "Frequency": + * return "Counts of discrete string occurrences" + * case "Histogram": + * return "Distribution of values across buckets" + * case "Summary": + * return "Quantile calculations over time windows" + * default: + * return "Unknown metric type" + * } + * } + * + * return { + * metrics: [ + * counterInfo, + * gaugeInfo, + * frequencyInfo, + * histogramInfo, + * summaryInfo + * ], + * descriptions: { + * Counter: describeMetric("Counter"), + * Gauge: describeMetric("Gauge"), + * Frequency: describeMetric("Frequency"), + * Histogram: describeMetric("Histogram"), + * Summary: describeMetric("Summary") + * } + * } + * }) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type Type = "Counter" | "Frequency" | "Gauge" | "Histogram" | "Summary" + + /** + * Union type for metric attributes that can be provided as either an object or array of tuples. + * + * **Example** (Providing attributes in different formats) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class AttributesError extends Data.TaggedError("AttributesError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Different ways to specify attributes + * const attributesAsObject = { + * service: "api", + * environment: "production", + * version: "1.2.3" + * } + * + * const attributesAsArray: ReadonlyArray<[string, string]> = [ + * ["service", "api"], + * ["environment", "production"], + * ["version", "1.2.3"] + * ] + * + * // Create metrics with different attribute formats + * const requestCounter1 = Metric.counter("requests", { + * description: "Total requests", + * attributes: attributesAsObject // Using object format + * }) + * + * const requestCounter2 = Metric.counter("requests", { + * description: "Total requests", + * attributes: attributesAsArray // Using array format + * }) + * + * // Function to normalize attributes to object format + * const normalizeAttributes = ( + * attrs: typeof attributesAsObject | ReadonlyArray<[string, string]> + * ) => { + * if (Array.isArray(attrs)) { + * return Object.fromEntries(attrs) + * } + * return attrs + * } + * + * // Add runtime attributes using withAttributes + * const contextualCounter = Metric.withAttributes(requestCounter1, { + * method: "GET", + * endpoint: "/api/users" + * }) + * + * // Update metrics with different attribute combinations + * yield* Metric.update(contextualCounter, 1) + * + * // Both formats result in the same internal representation + * const normalizedObject = normalizeAttributes(attributesAsObject) + * const normalizedArray = normalizeAttributes(attributesAsArray) + * + * return { + * attributeFormats: { + * object: normalizedObject, // { service: "api", environment: "production", version: "1.2.3" } + * array: normalizedArray, // { service: "api", environment: "production", version: "1.2.3" } + * areEqual: + * JSON.stringify(normalizedObject) === JSON.stringify(normalizedArray) // true + * } + * } + * }) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type Attributes = AttributeSet | ReadonlyArray<[string, string]> + + /** + * Type for metric attributes as a readonly record of string key-value pairs. + * + * **Example** (Combining metric attribute sets) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class AttributeSetError extends Data.TaggedError("AttributeSetError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Define attribute sets for different contexts + * const serviceAttributes = { + * service: "user-api", + * version: "2.1.0", + * environment: "production" + * } + * + * const operationAttributes = { + * operation: "create_user", + * method: "POST", + * endpoint: "/api/users" + * } + * + * const infrastructureAttributes = { + * region: "us-east-1", + * datacenter: "dc1", + * host: "api-server-01" + * } + * + * // Create metrics with predefined attribute sets + * const requestCounter = Metric.counter("http_requests_total", { + * description: "Total HTTP requests", + * attributes: serviceAttributes + * }) + * + * // Combine attribute sets + * const combineAttributes = (...attributeSets: Array>) => + * Object.assign({}, ...attributeSets) + * + * const fullAttributes = combineAttributes( + * serviceAttributes, + * operationAttributes, + * infrastructureAttributes + * ) + * + * // Create metric with combined attributes + * const detailedCounter = Metric.withAttributes(requestCounter, fullAttributes) + * + * // Helper to validate attribute keys (all must be strings) + * const validateAttributeSet = (attrs: Record): boolean => { + * return Object.entries(attrs).every(([key, value]) => + * typeof key === "string" && typeof value === "string" + * ) + * } + * + * yield* Metric.update(detailedCounter, 1) + * + * return { + * attributes: { + * service: serviceAttributes, + * operation: operationAttributes, + * infrastructure: infrastructureAttributes, + * combined: fullAttributes, + * isValid: validateAttributeSet(fullAttributes), // true + * totalKeys: Object.keys(fullAttributes).length // 9 + * } + * } + * }) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type AttributeSet = Readonly> + + /** + * Utility type to extract the Input type from a Metric type. + * + * **Example** (Extracting metric input types) + * + * ```ts + * import { Metric } from "effect" + * + * // Create various metric types + * const numberCounter = Metric.counter("requests") + * const bigintCounter = Metric.counter("bytes", { bigint: true }) + * const stringFrequency = Metric.frequency("status_codes") + * const numberGauge = Metric.gauge("cpu_usage") + * const numberHistogram = Metric.histogram("response_time", { + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) + * }) + * + * // The Input utility type extracts the input type from metric types: + * // - Counter: number + * // - Counter: bigint + * // - Frequency: string + * // - Gauge: number + * // - Histogram: number + * + * // Helper function that works with any metric + * const createMetricInfo = (metric: Metric.Metric) => ({ + * id: metric.id, + * type: metric.type + * }) + * + * const metrics = [ + * createMetricInfo(numberCounter), // { id: "requests", type: "Counter" } + * createMetricInfo(bigintCounter), // { id: "bytes", type: "Counter" } + * createMetricInfo(stringFrequency), // { id: "status_codes", type: "Frequency" } + * createMetricInfo(numberGauge), // { id: "cpu_usage", type: "Gauge" } + * createMetricInfo(numberHistogram) // { id: "response_time", type: "Histogram" } + * ] + * + * // Type safety is enforced at compile time: + * // Metric.update(numberCounter, 123) // ✓ Valid (number) + * // Metric.update(numberCounter, "abc") // ✗ Type error + * // Metric.update(stringFrequency, "ok") // ✓ Valid (string) + * // Metric.update(stringFrequency, 404) // ✗ Type error + * ``` + * + * @category types + * @since 4.0.0 + */ + export type Input = A extends Metric ? _Input + : never + + /** + * Utility type to extract the State type from a Metric type. + * + * **Example** (Extracting metric state types) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * // Create various metric types + * const requestCounter = Metric.counter("requests") + * const cpuGauge = Metric.gauge("cpu_usage") + * const statusFrequency = Metric.frequency("status_codes") + * const responseHistogram = Metric.histogram("response_time", { + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) + * }) + * const latencySummary = Metric.summary("latency", { + * maxAge: "5 minutes", + * maxSize: 1000, + * quantiles: [0.5, 0.95, 0.99] + * }) + * + * // The State utility type extracts the state type from metric types: + * // - Counter: CounterState + * // - Gauge: GaugeState + * // - Frequency: FrequencyState + * // - Histogram: HistogramState + * // - Summary: SummaryState + * + * // Type-safe state analysis functions + * const program = Effect.gen(function*() { + * // Update metrics first + * yield* Metric.update(requestCounter, 10) + * yield* Metric.update(cpuGauge, 85.5) + * yield* Metric.update(statusFrequency, "200") + * yield* Metric.update(responseHistogram, 150) + * yield* Metric.update(latencySummary, 120) + * + * // Extract states with proper typing + * const counterState = yield* Metric.value(requestCounter) + * const gaugeState = yield* Metric.value(cpuGauge) + * const frequencyState = yield* Metric.value(statusFrequency) + * const histogramState = yield* Metric.value(responseHistogram) + * const summaryState = yield* Metric.value(latencySummary) + * + * return { + * counter: { count: counterState.count }, // { count: 10 } + * gauge: { value: gaugeState.value }, // { value: 85.5 } + * frequency: { uniqueValues: frequencyState.occurrences.size }, // { uniqueValues: 1 } + * histogram: { totalObservations: histogramState.count }, // { totalObservations: 1 } + * summary: { observations: summaryState.count } // { observations: 1 } + * } + * }) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type State = A extends Metric ? _State + : never + + /** + * Interface defining the core hooks for metric operations: get, update, and modify. + * + * **Example** (Using metric hooks) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class HooksError extends Data.TaggedError("HooksError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a counter metric + * const requestCounter = Metric.counter("requests_total", { + * description: "Total number of requests" + * }) + * + * // The Hooks interface provides three core operations for metrics: + * // 1. get: retrieve current state + * // 2. update: add/set a value + * // 3. modify: transform the current state + * + * // These are low-level APIs. Most users should use high-level APIs: + * // - Metric.value() for getting state + * // - Metric.update() for updating values + * // - Metric.modify() for modifying values + * + * // Example using high-level APIs (recommended) + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(requestCounter, 5) + * const state = yield* Metric.value(requestCounter) + * + * return { + * currentCount: state.count, // 6 + * isIncremental: state.incremental // false + * } + * }) + * ``` + * + * @category interfaces + * @since 4.0.0 + */ + export interface Hooks { + readonly get: (context: Context.Context) => State + readonly update: (input: Input, context: Context.Context) => void + readonly modify: (input: Input, context: Context.Context) => void + } + + /** + * Interface containing complete metadata information about a metric. + * + * **Example** (Inspecting metric metadata) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetadataError extends Data.TaggedError("MetadataError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create metrics with different configurations + * const requestCounter = Metric.counter("http_requests_total", { + * description: "Total number of HTTP requests", + * attributes: { service: "api", version: "1.0" } + * }) + * + * const memoryGauge = Metric.gauge("memory_usage_bytes", { + * description: "Current memory usage in bytes" + * }) + * + * const statusFrequency = Metric.frequency("http_status_codes") + * + * // The Metadata interface contains complete information about a metric: + * // - id: metric identifier + * // - type: metric type ("Counter", "Gauge", etc.) + * // - description: optional description + * // - attributes: optional key-value attributes + * // - hooks: low-level operations interface + * + * // Each metric has associated metadata that can be inspected + * yield* Metric.update(requestCounter, 10) + * yield* Metric.update(memoryGauge, 256000000) + * yield* Metric.update(statusFrequency, "200") + * + * return { + * counter: { + * id: requestCounter.id, // "http_requests_total" + * type: requestCounter.type, // "Counter" + * description: requestCounter.description // "Total number of HTTP requests" + * }, + * gauge: { + * id: memoryGauge.id, // "memory_usage_bytes" + * type: memoryGauge.type, // "Gauge" + * description: memoryGauge.description // "Current memory usage in bytes" + * }, + * frequency: { + * id: statusFrequency.id, // "http_status_codes" + * type: statusFrequency.type, // "Frequency" + * description: statusFrequency.description // undefined + * } + * } + * }) + * ``` + * + * @category interfaces + * @since 4.0.0 + */ + export interface Metadata { + readonly id: string + readonly type: Type + readonly description: string | undefined + readonly attributes: Metric.AttributeSet | undefined + readonly hooks: Hooks + } + + /** + * Protocol interface for metric snapshots containing metadata and current state. + * + * **Example** (Inspecting metric snapshot protocols) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class SnapshotProtoError extends Data.TaggedError("SnapshotProtoError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create and update metrics + * const requestCounter = Metric.counter("requests", { + * description: "Request count", + * attributes: { service: "api" } + * }) + * + * const responseTimeHistogram = Metric.histogram("response_time", { + * description: "Response time distribution", + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) + * }) + * + * yield* Metric.update(requestCounter, 25) + * yield* Metric.update(responseTimeHistogram, 150) + * yield* Metric.update(responseTimeHistogram, 75) + * + * // Take snapshot of all metrics + * const snapshots = yield* Metric.snapshot + * + * // Each snapshot follows the SnapshotProto interface: + * // - id: metric identifier + * // - type: specific metric type + * // - description: optional description + * // - attributes: optional attributes + * // - state: current metric state + * + * const counterSnapshot = snapshots.find((s) => s.id === "requests") + * const histogramSnapshot = snapshots.find((s) => s.id === "response_time") + * + * return { + * counter: counterSnapshot ? + * { + * id: counterSnapshot.id, // "requests" + * type: counterSnapshot.type, // "Counter" + * description: counterSnapshot.description, // "Request count" + * hasAttributes: counterSnapshot.attributes !== undefined, // true + * count: (counterSnapshot.state as any).count // 25 + * } : + * null, + * histogram: histogramSnapshot ? + * { + * id: histogramSnapshot.id, // "response_time" + * type: histogramSnapshot.type, // "Histogram" + * observations: (histogramSnapshot.state as any).count // 2 + * } : + * null + * } + * }) + * ``` + * + * @category interfaces + * @since 4.0.0 + */ + export interface SnapshotProto { + readonly id: string + readonly type: T + readonly description: string | undefined + readonly attributes: Metric.AttributeSet | undefined + readonly state: State + } + + /** + * Union type representing all possible metric snapshot types with their corresponding states. + * + * **Example** (Analyzing metric snapshots) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class SnapshotError extends Data.TaggedError("SnapshotError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create different types of metrics + * const requestCounter = Metric.counter("requests_total") + * const cpuGauge = Metric.gauge("cpu_usage_percent") + * const statusFrequency = Metric.frequency("http_status") + * const responseHistogram = Metric.histogram("response_time_ms", { + * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) + * }) + * const latencySummary = Metric.summary("request_latency", { + * maxAge: "1 minute", + * maxSize: 100, + * quantiles: [0.5, 0.95, 0.99] + * }) + * + * // Update all metrics + * yield* Metric.update(requestCounter, 150) + * yield* Metric.update(cpuGauge, 45.7) + * yield* Metric.update(statusFrequency, "200") + * yield* Metric.update(statusFrequency, "404") + * yield* Metric.update(responseHistogram, 250) + * yield* Metric.update(latencySummary, 120) + * + * // Take snapshot of all metrics + * const allSnapshots = yield* Metric.snapshot + * + * // Type-safe snapshot analysis using discriminated union + * const analyzeSnapshot = (snapshot: any) => { + * switch (snapshot.type) { + * case "Counter": + * return { type: "Counter", count: snapshot.state.count } + * case "Gauge": + * return { type: "Gauge", value: snapshot.state.value } + * case "Frequency": + * return { + * type: "Frequency", + * uniqueValues: snapshot.state.occurrences.size + * } + * case "Histogram": + * return { type: "Histogram", observations: snapshot.state.count } + * case "Summary": + * return { type: "Summary", observations: snapshot.state.count } + * } + * } + * + * const analysis = allSnapshots.map(analyzeSnapshot) + * + * return { + * totalMetrics: allSnapshots.length, // 5 + * metricTypes: allSnapshots.map((s) => s.type), // ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] + * analysis + * } + * }) + * ``` + * + * @category types + * @since 4.0.0 + */ + export type Snapshot = + | SnapshotProto<"Counter", CounterState> + | SnapshotProto<"Gauge", GaugeState> + | SnapshotProto<"Frequency", FrequencyState> + | SnapshotProto<"Histogram", HistogramState> + | SnapshotProto<"Summary", SummaryState> +} + +/** + * Service key for the current metric attributes context. + * + * **Example** (Accessing the current metric attributes key) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class AttributesKeyError extends Data.TaggedError("AttributesKeyError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // The key is used internally by the Effect runtime to manage metric attributes + * const key = Metric.CurrentMetricAttributesKey + * + * // Create metrics with base attributes + * const requestCounter = Metric.counter("requests_total", { + * description: "Total HTTP requests" + * }) + * + * // The CurrentMetricAttributes service provides default attributes + * // that get applied to all metrics in the current context + * const baseAttributes = { service: "api", version: "1.0" } + * + * // Use withAttributes to apply attributes to metrics + * const taggedCounter1 = Metric.withAttributes(requestCounter, baseAttributes) + * const program1 = Metric.update(taggedCounter1, 1) + * + * const taggedCounter2 = Metric.withAttributes(requestCounter, { + * ...baseAttributes, + * endpoint: "/users" + * }) + * const program2 = Metric.update(taggedCounter2, 5) + * + * yield* program1 + * yield* program2 + * + * return { + * keyValue: key, // "effect/Metric/CurrentMetricAttributes" + * keyType: typeof key, // "string" + * isConstant: key === "effect/Metric/CurrentMetricAttributes" // true + * } + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes" as const + +/** + * Context reference for metric attributes applied from the current Effect + * context. + * + * **When to use** + * + * Use to provide default attributes that should be merged into metric updates + * and reads in a scoped part of a program. + * + * **Details** + * + * The default value is an empty attribute set. Metric reads and updates merge + * these contextual attributes with the metric's own attributes to select the + * metric series being accessed. + * + * **Example** (Providing current metric attributes) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class AttributesError extends Data.TaggedError("AttributesError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Access current metric attributes + * const attributes = yield* Metric.CurrentMetricAttributes + * console.log("Current attributes:", attributes) + * + * // Set new attributes context + * const newAttributes = { service: "api", version: "1.0" } + * const result = yield* Effect.provideService( + * Effect.gen(function*() { + * const updatedAttributes = yield* Metric.CurrentMetricAttributes + * return updatedAttributes + * }), + * Metric.CurrentMetricAttributes, + * newAttributes + * ) + * + * return result + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentMetricAttributes = Context.Reference(CurrentMetricAttributesKey, { + defaultValue: () => ({}) +}) + +const MetricRegistryKey = "~effect/observability/Metric/MetricRegistryKey" + +/** + * Context reference for the metric registry in the current context. + * + * **When to use** + * + * Use when you need a custom metric registry for an isolated program or test + * instead of the default registry. + * + * **Details** + * + * By default, the reference creates an empty `Map` the first time it is + * resolved. Metrics register their metadata and hooks lazily in this map when + * they are read or updated. + * + * **Gotchas** + * + * Because `Context.Reference` caches default values, the default `Map` is + * shared by contexts that do not provide an override. Provide `MetricRegistry` + * with a fresh `Map` when isolation matters. + * + * @see {@link snapshot} for reading all registered metrics from the current `Effect` context + * @see {@link snapshotUnsafe} for reading all registered metrics from an explicit `Context` + * + * @category references + * @since 4.0.0 + */ +export const MetricRegistry = Context.Reference>>( + MetricRegistryKey, + { defaultValue: () => new Map() } +) + +const TypeId = "~effect/observability/Metric" + +abstract class Metric$ implements Metric { + readonly [TypeId] = TypeId + + abstract readonly type: Metric.Type + + declare readonly Input: Contravariant + declare readonly State: Covariant + + readonly #metadataCache = new WeakMap>() + #metadata: Metric.Metadata | undefined + + readonly id: string + readonly description: string | undefined + readonly attributes: Metric.AttributeSet | undefined + + constructor( + id: string, + description: string | undefined, + attributes: Metric.AttributeSet | undefined + ) { + this.id = id + this.description = description + this.attributes = attributes + } + + valueUnsafe(context: Context.Context): State { + return this.hook(context).get(context) + } + + modifyUnsafe(input: Input, context: Context.Context): void { + return this.hook(context).modify(input, context) + } + + updateUnsafe(input: Input, context: Context.Context): void { + return this.hook(context).update(input, context) + } + + abstract createHooks(): Metric.Hooks + + hook(context: Context.Context): Metric.Hooks { + const extraAttributes = Context.get(context, CurrentMetricAttributes) + if (Object.keys(extraAttributes).length === 0) { + if (Predicate.isNotUndefined(this.#metadata)) { + return this.#metadata.hooks + } + this.#metadata = this.getOrCreate(context, this.attributes) + return this.#metadata.hooks + } + const mergedAttributes = mergeAttributes(this.attributes, extraAttributes) + let metadata = this.#metadataCache.get(mergedAttributes) + if (Predicate.isNotUndefined(metadata)) { + return metadata.hooks + } + metadata = this.getOrCreate(context, mergedAttributes) + this.#metadataCache.set(mergedAttributes, metadata) + return metadata.hooks + } + + getOrCreate( + context: Context.Context, + attributes: Metric.Attributes | undefined + ): Metric.Metadata { + const key = makeKey(this, attributes) + const registry = Context.get(context, MetricRegistry) + if (registry.has(key)) { + return registry.get(key)! + } + const hooks = this.createHooks() + const meta: Metric.Metadata = { + id: this.id, + type: this.type, + description: this.description, + attributes: attributesToRecord(attributes), + hooks + } + registry.set(key, meta) + return meta + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +const bigint0 = BigInt(0) + +class CounterMetric extends Metric$> { + readonly type = "Counter" + readonly #bigint: boolean + readonly #incremental: boolean + + constructor(id: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint?: boolean | undefined + readonly incremental?: boolean | undefined + }) { + super(id, options?.description, attributesToRecord(options?.attributes)) + this.#bigint = options?.bigint ?? false + this.#incremental = options?.incremental ?? false + } + + createHooks(): Metric.Hooks> { + let count = (this.#bigint ? bigint0 : 0) as any + const canUpdate = this.#incremental + ? this.#bigint + ? (value: bigint | number) => value >= bigint0 + : (value: bigint | number) => value >= 0 + : (_value: bigint | number) => true + const update = (value: Input) => { + if (canUpdate(value)) { + count = (count as any) + value + } + } + return makeHooks(() => ({ count, incremental: this.#incremental }), update) + } +} + +class GaugeMetric extends Metric$> { + readonly type = "Gauge" + readonly #bigint: boolean + + constructor(id: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint?: boolean | undefined + }) { + super(id, options?.description, attributesToRecord(options?.attributes)) + this.#bigint = options?.bigint ?? false + } + + createHooks(): Metric.Hooks> { + let value = this.#bigint ? BigInt(0) as any : 0 + const update = (input: number | bigint) => { + value = input + } + const modify = (input: number | bigint) => { + value = value + input + } + return makeHooks(() => ({ value }), update, modify) + } +} + +class FrequencyMetric extends Metric$ { + readonly type = "Frequency" + readonly #preregisteredWords: ReadonlyArray | undefined + + constructor(id: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly preregisteredWords?: ReadonlyArray | undefined + }) { + super(id, options?.description, attributesToRecord(options?.attributes)) + this.#preregisteredWords = options?.preregisteredWords + } + + createHooks(): Metric.Hooks { + const occurrences = new Map() + if (Predicate.isNotUndefined(this.#preregisteredWords)) { + for (const word of this.#preregisteredWords) { + occurrences.set(word, 0) + } + } + const update = (word: string) => { + const count = occurrences.get(word) ?? 0 + occurrences.set(word, count + 1) + } + return makeHooks(() => ({ occurrences }), update) + } +} + +class HistogramMetric extends Metric$ { + readonly type = "Histogram" + readonly #boundaries: ReadonlyArray + + constructor(id: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly boundaries: ReadonlyArray + }) { + super(id, options?.description, attributesToRecord(options?.attributes)) + this.#boundaries = options.boundaries + } + + createHooks(): Metric.Hooks { + const bounds = this.#boundaries + const size = bounds.length + const values = new Uint32Array(size + 1) + const boundaries = new Float64Array(size) + let count = 0 + let sum = 0 + let min = Number.MAX_VALUE + let max = Number.MIN_VALUE + + Arr.map(Arr.sort(bounds, Order.Number), (n, i) => { + boundaries[i] = n + }) + + // Insert the value into the right bucket with a binary search + const update = (value: number) => { + let from = 0 + let to = size + while (from !== to) { + const mid = Math.floor(from + (to - from) / 2) + const boundary = boundaries[mid] + if (value <= boundary) { + to = mid + } else { + from = mid + } + // The special case when to / from have a distance of one + if (to === from + 1) { + if (value <= boundaries[from]) { + to = from + } else { + from = to + } + } + } + values[from] = values[from] + 1 + count = count + 1 + sum = sum + value + if (value < min) { + min = value + } + if (value > max) { + max = value + } + } + + const getBuckets = (): ReadonlyArray<[number, number]> => { + const builder: Array<[number, number]> = Arr.allocate(size) as any + let cumulated = 0 + for (let i = 0; i < size; i++) { + const boundary = boundaries[i] + const value = values[i] + cumulated = cumulated + value + builder[i] = [boundary, cumulated] + } + return builder + } + + return makeHooks(() => ({ buckets: getBuckets(), count, min, max, sum }), update) + } +} + +class SummaryMetric extends Metric$ { + readonly type = "Summary" + readonly #maxAge: number + readonly #maxSize: number + readonly #quantiles: ReadonlyArray + + constructor(id: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly maxAge: Duration.Input + readonly maxSize: number + readonly quantiles: ReadonlyArray + }) { + super(id, options?.description, attributesToRecord(options?.attributes)) + this.#maxAge = Math.max(Duration.toMillis(Duration.fromInputUnsafe(options.maxAge)), 0) + this.#maxSize = options.maxSize + this.#quantiles = options.quantiles + } + + createHooks(): Metric.Hooks { + const sortedQuantiles = Arr.sort(this.#quantiles, Order.Number) + const observations = Arr.allocate<[number, number]>(this.#maxSize) + + for (const quantile of this.#quantiles) { + if (quantile < 0 || quantile > 1) { + throw new Error(`Quantile must be between 0 and 1, found: ${quantile}`) + } + } + + let head = 0 + let count = 0 + let sum = 0 + let min = Number.MAX_VALUE + let max = Number.MIN_VALUE + + const snapshot = (now: number): ReadonlyArray<[number, number | undefined]> => { + const builder: Array = [] + let i = 0 + while (i < this.#maxSize) { + const observation = observations[i] + if (Predicate.isNotUndefined(observation)) { + const [timestamp, value] = observation + const age = now - timestamp + if (age >= 0 && age <= this.#maxAge) { + builder.push(value) + } + } + i = i + 1 + } + const samples = Arr.sort(builder, Order.Number) + const sampleSize = samples.length + if (sampleSize === 0) { + return sortedQuantiles.map((q) => [q, undefined]) + } + // Compute the value of the quantile in terms of rank: + // > For a given quantile `q`, return the maximum value `v` such that at + // > most `q * n` values are less than or equal to `v`. + return sortedQuantiles.map((q) => { + if (q <= 0) return [q, samples[0]] + if (q >= 1) return [q, samples[sampleSize - 1]] + const index = Math.ceil(q * sampleSize) - 1 + return [q, samples[index]] + }) + } + + const observe = (value: number, timestamp: number) => { + if (this.#maxSize > 0) { + const target = head % this.#maxSize + observations[target] = [timestamp, value] as const + head = head + 1 + } + count = count + 1 + sum = sum + value + if (value < min) { + min = value + } + if (value > max) { + max = value + } + } + + const get = (context: Context.Context) => { + const clock = Context.get(context, InternalEffect.ClockRef) + const quantiles = snapshot(clock.currentTimeMillisUnsafe()) + return { quantiles, count, min, max, sum } + } + + const update = ([value, timestamp]: readonly [value: number, timestamp: number]) => observe(value, timestamp) + + return makeHooks(get, update) + } +} + +class MetricTransform extends Metric$ { + type: Metric.Type + readonly metric: Metric + override readonly valueUnsafe: (context: Context.Context) => State + override readonly updateUnsafe: (input: Input2, context: Context.Context) => void + override readonly modifyUnsafe: (input: Input2, context: Context.Context) => void + + constructor( + metric: Metric, + valueUnsafe: (context: Context.Context) => State, + updateUnsafe: (input: Input2, context: Context.Context) => void, + modifyUnsafe: (input: Input2, context: Context.Context) => void + ) { + super(metric.id, metric.description, metric.attributes) + this.metric = metric + this.valueUnsafe = valueUnsafe + this.updateUnsafe = updateUnsafe + this.modifyUnsafe = modifyUnsafe + this.type = metric.type + } + createHooks(): Metric.Hooks { + return (this.metric as any).createHooks() + } +} + +/** + * Returns `true` if the specified value is a `Metric`, otherwise returns `false`. + * + * **When to use** + * + * Use when you need runtime type checking and ensuring that a value + * conforms to the Metric interface before performing metric operations. + * + * **Example** (Checking metric values) + * + * ```ts + * import { Metric } from "effect" + * + * const counter = Metric.counter("requests") + * const gauge = Metric.gauge("temperature") + * const notAMetric = { name: "fake-metric" } + * + * console.log(Metric.isMetric(counter)) // true + * console.log(Metric.isMetric(gauge)) // true + * console.log(Metric.isMetric(notAMetric)) // false + * console.log(Metric.isMetric(null)) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isMetric = (u: unknown): u is Metric => + Predicate.hasProperty(u, "~effect/Metric") && u["~effect/Metric"] === "~effect/Metric" + +/** + * Represents a Counter metric that tracks cumulative numerical values over + * time. Counters can be incremented and decremented and provide a running total + * of changes. + * + * **Details** + * + * The optional `description` describes the counter, and `attributes` attach + * dimensions to it. Set `bigint` to create a counter that accepts `bigint` + * inputs. Set `incremental` to `true` to create a counter that can only ever be + * incremented. + * + * **Example** (Creating counter metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class CounterError extends Data.TaggedError("CounterError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a basic counter for tracking requests + * const requestCounter = Metric.counter("http_requests_total", { + * description: "Total number of HTTP requests processed" + * }) + * + * // Create an incremental-only counter for events + * const eventCounter = Metric.counter("events_processed", { + * description: "Events processed (increment only)", + * incremental: true + * }) + * + * // Create a bigint counter for large values + * const bytesCounter = Metric.counter("bytes_transferred", { + * description: "Total bytes transferred", + * bigint: true, + * attributes: { service: "file-transfer" } + * }) + * + * // Update counters with values + * yield* Metric.update(requestCounter, 1) // Increment by 1 + * yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6) + * yield* Metric.update(eventCounter, 1) // Increment by 1 + * yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes + * + * // Get current counter values + * const requestValue = yield* Metric.value(requestCounter) + * const eventValue = yield* Metric.value(eventCounter) + * const bytesValue = yield* Metric.value(bytesCounter) + * + * return { requestValue, eventValue, bytesValue } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const counter: { + ( + name: string, + options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint?: false | undefined + readonly incremental?: boolean | undefined + } + ): Counter + ( + name: string, + options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint: true + readonly incremental?: boolean | undefined + } + ): Counter +} = (name, options) => new CounterMetric(name, options) as any + +/** + * Represents a `Gauge` metric that tracks and reports a single numerical value + * at a specific moment. + * + * **When to use** + * + * Use when you need a metric for instantaneous values, such as memory usage or + * CPU load. + * + * **Details** + * + * The optional `description` describes the gauge, and `attributes` attach + * dimensions to it. Set `bigint` to create a gauge that accepts `bigint` + * inputs. + * + * **Example** (Creating gauge metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class GaugeError extends Data.TaggedError("GaugeError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a gauge for tracking memory usage + * const memoryGauge = Metric.gauge("memory_usage_mb", { + * description: "Current memory usage in megabytes" + * }) + * + * // Create a gauge for CPU utilization + * const cpuGauge = Metric.gauge("cpu_utilization", { + * description: "Current CPU utilization percentage", + * attributes: { host: "server-01" } + * }) + * + * // Create a bigint gauge for large values + * const diskSpaceGauge = Metric.gauge("disk_free_bytes", { + * description: "Free disk space in bytes", + * bigint: true + * }) + * + * // Set gauge values (replaces current value) + * yield* Metric.update(memoryGauge, 512) // Set to 512 MB + * yield* Metric.update(cpuGauge, 85.5) // Set to 85.5% + * yield* Metric.update(diskSpaceGauge, 1024000000n) // Set to ~1GB + * + * // Modify gauge values (adds to current value) + * yield* Metric.modify(memoryGauge, 128) // Increase by 128 MB (total: 640) + * yield* Metric.modify(cpuGauge, -10.5) // Decrease by 10.5% (total: 75%) + * + * // Update with new absolute values + * yield* Metric.update(memoryGauge, 800) // Set to 800 MB (replaces 640) + * + * // Get current gauge values + * const memoryValue = yield* Metric.value(memoryGauge) + * const cpuValue = yield* Metric.value(cpuGauge) + * const diskValue = yield* Metric.value(diskSpaceGauge) + * + * return { memoryValue, cpuValue, diskValue } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const gauge: { + (name: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint?: false | undefined + }): Gauge + (name: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly bigint: true + }): Gauge +} = (name, options) => new GaugeMetric(name, options) as any + +/** + * Creates a `Frequency` metric which can be used to count the number of + * occurrences of a string. + * + * **When to use** + * + * Use when you need a metric for counting how often a specific event or + * incident occurs. + * + * **Details** + * + * The optional `description` describes the frequency, and `attributes` attach + * dimensions to it. Use `preregisteredWords` to initialize occurrence counts + * for known string values before updates arrive. + * + * **Example** (Creating frequency metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class FrequencyError extends Data.TaggedError("FrequencyError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a frequency metric for HTTP status codes + * const statusFrequency = Metric.frequency("http_status_codes", { + * description: "Frequency of HTTP response status codes", + * preregisteredWords: ["200", "404", "500"] // Pre-register common codes + * }) + * + * // Create a frequency metric for user actions + * const userActionFrequency = Metric.frequency("user_actions", { + * description: "Frequency of user actions performed", + * attributes: { application: "web-app" } + * }) + * + * // Create a frequency metric for error types + * const errorTypeFrequency = Metric.frequency("error_types", { + * description: "Frequency of different error types" + * }) + * + * // Record different occurrences + * yield* Metric.update(statusFrequency, "200") // Success response + * yield* Metric.update(statusFrequency, "200") // Another success + * yield* Metric.update(statusFrequency, "404") // Not found error + * yield* Metric.update(statusFrequency, "500") // Server error + * yield* Metric.update(statusFrequency, "200") // Another success + * + * yield* Metric.update(userActionFrequency, "login") + * yield* Metric.update(userActionFrequency, "view_dashboard") + * yield* Metric.update(userActionFrequency, "login") + * yield* Metric.update(userActionFrequency, "logout") + * + * yield* Metric.update(errorTypeFrequency, "ValidationError") + * yield* Metric.update(errorTypeFrequency, "NetworkError") + * yield* Metric.update(errorTypeFrequency, "ValidationError") + * + * // Get frequency counts + * const statusCounts = yield* Metric.value(statusFrequency) + * const actionCounts = yield* Metric.value(userActionFrequency) + * const errorCounts = yield* Metric.value(errorTypeFrequency) + * + * // statusCounts.occurrences will be: + * // Map { "200" => 3, "404" => 1, "500" => 1 } + * // actionCounts.occurrences will be: + * // Map { "login" => 2, "view_dashboard" => 1, "logout" => 1 } + * // errorCounts.occurrences will be: + * // Map { "ValidationError" => 2, "NetworkError" => 1 } + * + * return { statusCounts, actionCounts, errorCounts } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const frequency = (name: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly preregisteredWords?: ReadonlyArray | undefined +}): Frequency => new FrequencyMetric(name, options) + +/** + * Represents a `Histogram` metric that records observations into buckets. + * + * **When to use** + * + * Use when you need a metric for measuring the distribution of values within a + * range. + * + * **Details** + * + * The optional `description` describes the histogram, and `attributes` attach + * dimensions to it. The required `boundaries` option defines the histogram + * bucket boundaries. + * + * **Example** (Creating histogram metrics) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class HistogramError extends Data.TaggedError("HistogramError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a histogram for API response times + * const responseTimeHistogram = Metric.histogram("api_response_time", { + * description: "Distribution of API response times in milliseconds", + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) + * // Creates buckets: 0-50ms, 50-100ms, 100-150ms, ..., 400-450ms, 450ms+ + * }) + * + * // Create a histogram for request payload sizes + * const payloadSizeHistogram = Metric.histogram("payload_size", { + * description: "Distribution of request payload sizes in KB", + * boundaries: Metric.exponentialBoundaries({ start: 1, factor: 2, count: 8 }), + * // Creates exponential buckets: 1KB, 2KB, 4KB, 8KB, 16KB, 32KB, 64KB, 128KB+ + * attributes: { service: "api-gateway" } + * }) + * + * // Create a histogram with custom boundaries + * const customHistogram = Metric.histogram("custom_metric", { + * description: "Custom distribution metric", + * boundaries: [0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100] + * }) + * + * // Record various response times + * yield* Metric.update(responseTimeHistogram, 25) // Goes in 0-50ms bucket + * yield* Metric.update(responseTimeHistogram, 75) // Goes in 50-100ms bucket + * yield* Metric.update(responseTimeHistogram, 125) // Goes in 100-150ms bucket + * yield* Metric.update(responseTimeHistogram, 200) // Goes in 150-200ms bucket + * yield* Metric.update(responseTimeHistogram, 75) // Another 50-100ms + * + * // Record payload sizes + * yield* Metric.update(payloadSizeHistogram, 3) // Goes in 2-4KB bucket + * yield* Metric.update(payloadSizeHistogram, 15) // Goes in 8-16KB bucket + * yield* Metric.update(payloadSizeHistogram, 0.5) // Goes in 0-1KB bucket + * + * // Get histogram state with distribution data + * const responseTimeState = yield* Metric.value(responseTimeHistogram) + * const payloadSizeState = yield* Metric.value(payloadSizeHistogram) + * + * // responseTimeState will contain: + * // - buckets: [[50, 1], [100, 3], [150, 4], [200, 5], ...] + * // - count: 5, min: 25, max: 200, sum: 500 + * // - Useful for calculating percentiles, averages, etc. + * + * return { responseTimeState, payloadSizeState } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const histogram = (name: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly boundaries: ReadonlyArray +}): Histogram => new HistogramMetric(name, options) + +/** + * Creates a `Summary` metric that records observations and calculates quantiles + * which takes a value as input and uses the current time. + * + * **When to use** + * + * Use when you need a metric that records statistical information about a set + * of values, including quantiles. + * + * **Details** + * + * The optional `description` describes the summary, and `attributes` attach + * dimensions to it. `maxAge` controls how long observations are retained, + * `maxSize` controls how many observations are kept, and `quantiles` lists the + * quantiles to calculate, such as `[0.5, 0.9]`. + * + * **Example** (Creating summary metrics) + * + * ```ts + * import { Data, Duration, Effect, Metric } from "effect" + * + * class SummaryError extends Data.TaggedError("SummaryError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a summary for API response times + * const responseTimeSummary = Metric.summary("api_response_time", { + * description: "API response time quantiles over 5-minute windows", + * maxAge: Duration.minutes(5), // Keep observations for 5 minutes + * maxSize: 1000, // Maximum 1000 observations in memory + * quantiles: [0.5, 0.9, 0.95, 0.99] // 50th, 90th, 95th, 99th percentiles + * }) + * + * // Create a summary for request payload sizes + * const payloadSizeSummary = Metric.summary("request_payload_size", { + * description: "Request payload size distribution over 2-minute windows", + * maxAge: Duration.minutes(2), // Shorter window for recent trends + * maxSize: 500, // Smaller buffer for memory efficiency + * quantiles: [0.5, 0.75, 0.9], // Median, 75th, 90th percentiles + * attributes: { service: "upload-service" } + * }) + * + * // Record deterministic response times + * const responseTimes = [82, 96, 104, 118, 135, 170, 210, 240] + * for (const responseTime of responseTimes) { + * yield* Metric.update(responseTimeSummary, responseTime) + * } + * + * // Record some payload sizes + * yield* Metric.update(payloadSizeSummary, 1.2) // 1.2KB + * yield* Metric.update(payloadSizeSummary, 5.8) // 5.8KB + * yield* Metric.update(payloadSizeSummary, 15.6) // 15.6KB + * yield* Metric.update(payloadSizeSummary, 3.4) // 3.4KB + * + * // Get summary statistics with quantiles + * const responseStats = yield* Metric.value(responseTimeSummary) + * const payloadStats = yield* Metric.value(payloadSizeSummary) + * + * console.log({ + * count: responseStats.count, + * min: responseStats.min, + * max: responseStats.max, + * sum: responseStats.sum + * }) // { count: 8, min: 82, max: 240, sum: 1155 } + * + * console.log({ + * count: payloadStats.count, + * min: payloadStats.min, + * max: payloadStats.max, + * sum: payloadStats.sum + * }) // { count: 4, min: 1.2, max: 15.6, sum: 26 } + * + * // Both summaries include quantile information for their configured windows. + * + * return { responseStats, payloadStats } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const summary = (name: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly maxAge: Duration.Input + readonly maxSize: number + readonly quantiles: ReadonlyArray +}): Summary => + mapInput(summaryWithTimestamp(name, options), (input, context) => + [ + input, + Context.get(context, InternalEffect.ClockRef).currentTimeMillisUnsafe() + ] as [number, number]) + +/** + * Creates a `Summary` metric that records observations with explicit + * timestamps and calculates quantiles. + * + * **When to use** + * + * Use when you need a metric that records statistical information about a set + * of values together with timestamps. + * + * **Details** + * + * Inputs to this metric are `[value, timestamp]` pairs; the current clock is + * used when reading quantiles against the configured `maxAge`. + * + * The optional `description` describes the summary, and `attributes` attach + * dimensions to it. `maxAge` controls how long observations are retained, + * `maxSize` controls how many observations are kept, and `quantiles` lists the + * quantiles to calculate, such as `[0.5, 0.9]`. + * + * **Example** (Creating summaries with explicit timestamps) + * + * ```ts + * import { Metric } from "effect" + * + * const responseTimesSummary = Metric.summaryWithTimestamp( + * "response_times_summary", + * { + * description: "Measures the distribution of response times", + * maxAge: "60 seconds", // Retain observations for 60 seconds. + * maxSize: 1000, // Keep a maximum of 1000 observations. + * quantiles: [0.5, 0.9, 0.99] // Calculate 50th, 90th, and 99th quantiles. + * } + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const summaryWithTimestamp = (name: string, options: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly maxAge: Duration.Input + readonly maxSize: number + readonly quantiles: ReadonlyArray +}): Summary<[value: number, timestamp: number]> => new SummaryMetric(name, options) + +/** + * Creates a timer metric, based on a `Histogram`, which keeps track of + * durations in milliseconds. + * + * **Details** + * + * The unit of time will automatically be added to the metric as a tag (i.e. + * `"time_unit: milliseconds"`). + * + * If `options.boundaries` is not provided, the boundaries will be computed + * using `Metric.exponentialBoundaries({ start: 0.5, factor: 2, count: 35 })`. + * + * **Example** (Recording durations with a timer) + * + * ```ts + * import { Data, Duration, Effect, Metric } from "effect" + * + * class TimerError extends Data.TaggedError("TimerError")<{ + * readonly operation: string + * }> {} + * + * // Create a timer metric to track API request durations + * const apiRequestTimer = Metric.timer("api_request_duration", { + * description: "Duration of API requests", + * attributes: { service: "user-api" } + * }) + * + * // Record a measured API operation duration + * const apiOperation = Effect.gen(function*() { + * const duration = Duration.millis(120) + * yield* Metric.update(apiRequestTimer, duration) + * + * const state = yield* Metric.value(apiRequestTimer) + * console.log({ + * count: state.count, + * min: state.min, + * max: state.max, + * sum: state.sum + * }) // { count: 1, min: 120, max: 120, sum: 120 } + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const timer = (name: string, options?: { + readonly description?: string | undefined + readonly attributes?: Metric.Attributes | undefined + readonly boundaries?: ReadonlyArray +}): Histogram => { + const boundaries = Predicate.isNotUndefined(options?.boundaries) + ? options.boundaries + : exponentialBoundaries({ start: 0.5, factor: 2, count: 35 }) + const attributes = mergeAttributes(options?.attributes, { time_unit: "milliseconds" }) + const metric = new HistogramMetric(name, { ...options, boundaries, attributes }) + return mapInput(metric, Duration.toMillis) +} + +/** + * Retrieves the current state of the specified `Metric`. + * + * **Details** + * + * The returned state depends on the metric type. Counters return + * `CounterState` with `count` and `incremental`, gauges return + * `GaugeState` with `value`, frequencies return + * `FrequencyState` with `occurrences`, histograms return `HistogramState` with + * buckets, count, min, max, and sum, and summaries return `SummaryState` with + * quantiles, count, min, max, and sum. + * + * **Example** (Reading metric state) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const requestCounter = Metric.counter("requests") + * const responseTime = Metric.histogram("response_time", { + * boundaries: [100, 500, 1000, 2000] + * }) + * + * const program = Effect.gen(function*() { + * // Update metrics + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(responseTime, 750) + * + * // Get current values + * const counterState = yield* Metric.value(requestCounter) + * console.log(`Request count: ${counterState.count}`) + * + * const histogramState = yield* Metric.value(responseTime) + * console.log(`Response time stats:`, { + * count: histogramState.count, + * min: histogramState.min, + * max: histogramState.max, + * average: histogramState.sum / histogramState.count + * }) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const value = ( + self: Metric +): Effect => + InternalEffect.flatMap( + InternalEffect.context(), + (context) => InternalEffect.sync(() => self.valueUnsafe(context)) + ) + +/** + * Modifies the metric with the specified input. + * + * **Details** + * + * The behavior of `modify` depends on the metric type. Counters add the input + * value to the current count, gauges add the input value to the current gauge + * value, frequencies increment the occurrence count for the input string, + * histograms record the input value in the appropriate bucket, and summaries + * record the input observation. + * + * **Example** (Modifying metric values) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const temperatureGauge = Metric.gauge("temperature") + * const requestCounter = Metric.counter("requests") + * + * const program = Effect.gen(function*() { + * // Set initial temperature + * yield* Metric.update(temperatureGauge, 20) + * + * // Modify by adding/subtracting values + * yield* Metric.modify(temperatureGauge, 5) // Now 25 + * yield* Metric.modify(temperatureGauge, -3) // Now 22 + * + * // For counters, modify increments by the specified amount + * yield* Metric.modify(requestCounter, 10) // Add 10 to counter + * yield* Metric.modify(requestCounter, 5) // Add 5 more (total: 15) + * + * const temp = yield* Metric.value(temperatureGauge) + * const requests = yield* Metric.value(requestCounter) + * + * console.log(`Temperature: ${temp.value}°C`) // 22°C + * console.log(`Requests: ${requests.count}`) // 15 + * }) + * ``` + * + * @category mutations + * @since 3.6.5 + */ +export const modify: { + (input: Input): (self: Metric) => Effect + (self: Metric, input: Input): Effect +} = dual< + (input: Input) => (self: Metric) => Effect, + (self: Metric, input: Input) => Effect +>(2, (self, input) => + InternalEffect.flatMap( + InternalEffect.context(), + (context) => InternalEffect.sync(() => self.modifyUnsafe(input, context)) + )) + +/** + * Updates the metric with the specified input. + * + * **Details** + * + * The behavior of `update` depends on the metric type. Counters add the input + * value to the current count, gauges replace the current value with the input + * value, frequencies increment the occurrence count for the input string, + * histograms record the input value in the appropriate bucket, and summaries + * record the input value as a new observation. + * + * **Example** (Updating metric values) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const cpuUsage = Metric.gauge("cpu_usage_percent") + * const httpStatus = Metric.frequency("http_status_codes") + * const responseTime = Metric.histogram("response_time_ms", { + * boundaries: [100, 500, 1000, 2000] + * }) + * + * const program = Effect.gen(function*() { + * // Update gauge to specific values + * yield* Metric.update(cpuUsage, 45.2) + * yield* Metric.update(cpuUsage, 67.8) // Replaces previous value + * + * // Track HTTP status code occurrences + * yield* Metric.update(httpStatus, "200") + * yield* Metric.update(httpStatus, "404") + * yield* Metric.update(httpStatus, "200") // Increments 200 count + * + * // Record response times + * yield* Metric.update(responseTime, 250) + * yield* Metric.update(responseTime, 750) + * yield* Metric.update(responseTime, 1500) + * + * // Check current states + * const cpu = yield* Metric.value(cpuUsage) + * const statuses = yield* Metric.value(httpStatus) + * const times = yield* Metric.value(responseTime) + * + * console.log(`CPU Usage: ${cpu.value}%`) + * console.log(`Status 200 count: ${statuses.occurrences.get("200")}`) // 2 + * console.log(`Response time samples: ${times.count}`) // 3 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const update: { + (input: Input): (self: Metric) => Effect + (self: Metric, input: Input): Effect +} = dual< + (input: Input) => (self: Metric) => Effect, + (self: Metric, input: Input) => Effect +>( + 2, + (self, input) => + InternalEffect.contextWith((services) => InternalEffect.sync(() => self.updateUnsafe(input, services))) +) + +/** + * Returns a new metric that is powered by this one, but which accepts updates + * of the specified new type, which must be transformable to the input type of + * this metric. + * + * **Example** (Mapping metric inputs) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricError extends Data.TaggedError("MetricError")<{ + * readonly operation: string + * }> {} + * + * // Create a histogram that expects Duration values + * const durationHistogram = Metric.histogram("request_duration_ms", { + * description: "Request duration in milliseconds", + * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) + * }) + * + * // Transform to accept number values representing milliseconds + * const numberHistogram = Metric.mapInput( + * durationHistogram, + * (ms: number) => ms // Direct mapping from number to expected input + * ) + * + * const program = Effect.gen(function*() { + * // Now we can update with a plain number + * yield* Metric.update(numberHistogram, 250) + * + * // Get metric value to see the recorded state + * const value = yield* Metric.value(numberHistogram) + * return value + * }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + ( + f: (input: Input2, context: Context.Context) => Input + ): (self: Metric) => Metric + ( + self: Metric, + f: (input: Input2, context: Context.Context) => Input + ): Metric +} = dual< + ( + f: (input: Input2, context: Context.Context) => Input + ) => (self: Metric) => Metric, + ( + self: Metric, + f: (input: Input2, context: Context.Context) => Input + ) => Metric +>(2, ( + self: Metric, + f: (input: Input2, context: Context.Context) => Input +): Metric => + new MetricTransform( + self, + (context) => self.valueUnsafe(context), + (input, context) => self.updateUnsafe(f(input, context), context), + (input, context) => self.modifyUnsafe(f(input, context), context) + )) + +/** + * Returns a new metric that is powered by this one, but which accepts updates + * of any type, and translates them to updates with the specified constant + * update value. + * + * **Example** (Ignoring inputs with a constant value) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricError extends Data.TaggedError("MetricError")<{ + * readonly operation: string + * }> {} + * + * // Create a counter that normally expects a number increment + * const requestCounter = Metric.counter("total_requests", { + * description: "Total number of requests processed" + * }) + * + * // Create a version that always increments by 1, regardless of input + * const simpleRequestCounter = Metric.withConstantInput(requestCounter, 1) + * + * const program = Effect.gen(function*() { + * // These all increment the counter by 1, ignoring the input value + * yield* Metric.update(simpleRequestCounter, "any string") + * yield* Metric.update(simpleRequestCounter, { complex: "object" }) + * yield* Metric.update(simpleRequestCounter, 999) // Still increments by 1 + * + * const value = yield* Metric.value(simpleRequestCounter) + * return value // Counter state will show count: 3 + * }) + * ``` + * + * @category Input + * @since 2.0.0 + */ +export const withConstantInput: { + (input: Input): (self: Metric) => Metric + (self: Metric, input: Input): Metric +} = dual< + (input: Input) => (self: Metric) => Metric, + (self: Metric, input: Input) => Metric +>(2, (self, input) => mapInput(self, () => input)) + +/** + * Returns a new metric that applies the specified attributes to all operations. + * + * **Details** + * + * Attributes are key-value pairs that provide additional context for metrics, + * enabling filtering, grouping, and more detailed analysis. Each combination + * of attribute values creates a separate metric series. + * + * **Example** (Applying metric attributes) + * + * ```ts + * import { Effect, Metric } from "effect" + * + * const requestCounter = Metric.counter("http_requests_total", { + * description: "Total HTTP requests" + * }) + * + * // Create tagged versions of the metric + * const getRequests = Metric.withAttributes(requestCounter, { + * method: "GET", + * endpoint: "/api/users" + * }) + * + * const postRequests = Metric.withAttributes(requestCounter, { + * method: "POST", + * endpoint: "/api/users" + * }) + * + * const program = Effect.gen(function*() { + * // These will be tracked as separate metric series + * yield* Metric.update(getRequests, 1) // http_requests_total{method="GET", endpoint="/api/users"} + * yield* Metric.update(postRequests, 1) // http_requests_total{method="POST", endpoint="/api/users"} + * yield* Metric.update(getRequests, 1) // Increments the GET counter + * + * // You can also chain attributes + * const taggedMetric = requestCounter.pipe( + * Metric.withAttributes({ service: "user-api" }), + * Metric.withAttributes({ version: "v1" }) + * ) + * + * yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"} + * }) + * + * // When taking snapshots, each attribute combination appears as a separate metric + * const viewMetrics = Effect.gen(function*() { + * const snapshots = yield* Metric.snapshot + * for (const metric of snapshots) { + * if (metric.id === "http_requests_total") { + * console.log(`${metric.id}`, metric.attributes, metric.state) + * } + * } + * }) + * ``` + * + * @category Attributes + * @since 4.0.0 + */ +export const withAttributes: { + (attributes: Metric.Attributes): (self: Metric) => Metric + (self: Metric, attributes: Metric.Attributes): Metric +} = dual< + (attributes: Metric.Attributes) => (self: Metric) => Metric, + (self: Metric, attributes: Metric.Attributes) => Metric +>(2, ( + self: Metric, + attributes: Metric.Attributes +): Metric => + new MetricTransform( + self, + (context) => self.valueUnsafe(addAttributesToContext(context, attributes)), + (input, context) => self.updateUnsafe(input, addAttributesToContext(context, attributes)), + (input, context) => self.modifyUnsafe(input, addAttributesToContext(context, attributes)) + )) + +// Metric Snapshots + +/** + * Captures a snapshot of all registered metrics in the current context. + * + * **Details** + * + * Returns an array of metric snapshots, each containing the metric's metadata + * (name, description, type) and current state (values, counts, etc.). + * + * **Example** (Capturing metric snapshots) + * + * ```ts + * import { Console, Data, Effect, Metric } from "effect" + * + * class SnapshotError extends Data.TaggedError("SnapshotError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create and update some metrics + * const requestCounter = Metric.counter("http_requests", { + * description: "Total HTTP requests" + * }) + * const responseTime = Metric.histogram("response_time_ms", { + * description: "Response time in milliseconds", + * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 5 }) + * }) + * + * // Update the metrics with some values + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(responseTime, 150) + * yield* Metric.update(responseTime, 75) + * + * // Take a snapshot of all metrics + * const snapshots = yield* Metric.snapshot + * + * // Examine the snapshots + * for (const snapshot of snapshots) { + * yield* Console.log(`Metric: ${snapshot.id}`) + * yield* Console.log(`Description: ${snapshot.description}`) + * yield* Console.log(`Type: ${snapshot.type}`) + * yield* Console.log(`State:`, snapshot.state) + * } + * + * return snapshots + * }) + * ``` + * + * @category Snapshotting + * @since 2.0.0 + */ +export const snapshot: Effect> = InternalEffect.map( + InternalEffect.context(), + (context) => snapshotUnsafe(context) +) + +/** + * Returns a human-readable string representation of all currently registered + * metrics in a tabular format. + * + * **Details** + * + * This debugging utility captures a snapshot of all metrics and formats them + * in an easy-to-read table showing names, descriptions, types, attributes, + * and current state values. + * + * **Example** (Dumping metrics as text) + * + * ```ts + * import { Console, Data, Effect, Metric } from "effect" + * + * class DumpError extends Data.TaggedError("DumpError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create and update some metrics for demonstration + * const requestCounter = Metric.counter("http_requests_total", { + * description: "Total HTTP requests" + * }) + * const responseTime = Metric.gauge("response_time_ms", { + * description: "Current response time in milliseconds" + * }) + * const statusFreq = Metric.frequency("http_status_codes", { + * description: "Frequency of HTTP status codes" + * }) + * + * // Update metrics with some values + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(responseTime, 125) + * yield* Metric.update(statusFreq, "200") + * yield* Metric.update(statusFreq, "404") + * yield* Metric.update(statusFreq, "200") + * + * // Get formatted dump of all metrics + * const metricsReport = yield* Metric.dump + * yield* Console.log("Current Metrics:") + * yield* Console.log(metricsReport) + * + * // Output will look like a formatted table: + * // Name Description Type State + * // http_requests_total Total HTTP requests Counter [count: 2] + * // response_time_ms Current response time in milliseconds Gauge [value: 125] + * // http_status_codes Frequency of HTTP status codes Frequency [occurrences: 200 -> 2, 404 -> 1] + * + * return metricsReport + * }) + * ``` + * + * @category Debugging + * @since 4.0.0 + */ +export const dump: Effect = InternalEffect.flatMap(InternalEffect.context(), (context) => { + const metrics = snapshotUnsafe(context) + if (metrics.length > 0) { + const maxNameLength = metrics.reduce((max, metric) => { + const length = metric.id.length + return length > max ? length : max + }, 0) + 2 + const maxDescriptionLength = metrics.reduce((max, metric) => { + const length = Predicate.isNotUndefined(metric.description) ? metric.description.length : 0 + return length > max ? length : max + }, 0) + 2 + const maxTypeLength = metrics.reduce((max, metric) => { + const length = metric.type.length + return length > max ? length : max + }, 0) + 2 + const maxAttributesLength = metrics.reduce((max, metric) => { + const length = Predicate.isNotUndefined(metric.attributes) ? attributesToString(metric.attributes).length : 0 + return length > max ? length : max + }, 0) + 2 + const grouped = Object.entries(Arr.groupBy(metrics, (metric) => metric.id)) + const sorted = Arr.sortWith(grouped, (entry) => entry[0], _String.Order) + const rendered = sorted.map(([, group]) => + group.map((metric) => + renderName(metric, maxNameLength) + + renderDescription(metric, maxDescriptionLength) + + renderType(metric, maxTypeLength) + + renderAttributes(metric, maxAttributesLength) + + renderState(metric) + ).join("\n") + ).join("\n") + return InternalEffect.succeed(rendered) + } + return InternalEffect.succeed("") +}) + +/** + * Captures a snapshot of all registered metrics synchronously using the provided + * service context. + * + * **When to use** + * + * Use to read metric snapshots from an explicit `Context` in low-level + * integrations, exporters, or debugging tools that already have the context. + * + * **Details** + * + * This is the "unsafe" version that bypasses Effect's safety guarantees and requires + * manual handling of the services context. Use the safe `snapshot` function for normal + * application code. + * + * **Example** (Capturing snapshots from a context) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class UnsafeSnapshotError extends Data.TaggedError("UnsafeSnapshotError")<{ + * readonly operation: string + * }> {} + * + * // Use unsafeSnapshot in performance-critical scenarios or internal implementations + * const performanceMetricsExporter = Effect.gen(function*() { + * // Create some metrics first + * const requestCounter = Metric.counter("http_requests", { + * description: "Total HTTP requests" + * }) + * const responseTime = Metric.gauge("response_time_ms", { + * description: "Current response time" + * }) + * + * // Update metrics + * yield* Metric.update(requestCounter, 1) + * yield* Metric.update(responseTime, 150) + * + * // Get services context for unsafe operations + * const services = yield* Effect.context() + * + * // Use snapshotUnsafe for direct, synchronous access + * const snapshots = Metric.snapshotUnsafe(services) + * const exportBatchCreatedAt = 1_700_000_000_000 + * + * // Process snapshots immediately (useful for exporters, debugging tools) + * const exportData = snapshots.map((snapshot) => ({ + * name: snapshot.id, + * type: snapshot.type, + * value: snapshot.state, + * timestamp: exportBatchCreatedAt + * })) + * + * // This is synchronous and doesn't involve Effect overhead + * // Useful for performance-critical metric export operations + * return exportData + * }) + * + * // For normal application use, prefer the safe snapshot function: + * const safeSnapshotExample = Effect.gen(function*() { + * // This automatically handles the services context + * const snapshots = yield* Metric.snapshot + * return snapshots + * }) + * ``` + * + * @category Snapshotting + * @since 4.0.0 + */ +export const snapshotUnsafe = (context: Context.Context): ReadonlyArray => { + const registry = Context.get(context, MetricRegistry) + return Array.from(registry.values()).map(({ hooks, ...meta }) => ({ + ...meta, + state: hooks.get(context) + })) +} + +const renderName = (metric: Metric.Snapshot, padTo: number): string => `name=${metric.id.padEnd(padTo, " ")}` + +const renderDescription = (metric: Metric.Snapshot, padTo: number): string => + `description=${(metric.description ?? "").padEnd(padTo, " ")}` + +const renderType = (metric: Metric.Snapshot, padTo: number): string => `type=${metric.type.padEnd(padTo, " ")}` + +const renderAttributes = (metric: Metric.Snapshot, padTo: number): string => { + const attrs = attributesToString(metric.attributes ?? {}) + const padding = " ".repeat(Math.max(0, padTo - attrs.length)) + return `${attrs}${padding}` +} + +const renderState = (metric: Metric.Snapshot): string => { + const prefix: string = "state=" + switch (metric.type) { + case "Counter": { + const state = metric.state as CounterState + return `${prefix}[count: [${state.count}]]` + } + case "Frequency": { + const state = metric.state as FrequencyState + return `${prefix}[occurrences: ${renderKeyValues(state.occurrences)}]` + } + case "Gauge": { + const state = metric.state as GaugeState + return `${prefix}[value: [${state.value}]]` + } + case "Histogram": { + const state = metric.state as HistogramState + const buckets = `buckets: [${renderKeyValues(state.buckets)}]` + const count = `count: [${state.count}]` + const min = `min: [${state.min}]` + const max = `max: [${state.max}]` + const sum = `sum: [${state.sum}]` + return `${prefix}[${buckets}, ${count}, ${min}, ${max}, ${sum}]` + } + case "Summary": { + const state = metric.state as SummaryState + const printableQuantiles = state.quantiles.map(([key, value]) => [key, value ?? 0] as [number, number]) + const quantiles = `quantiles: [${renderKeyValues(printableQuantiles)}]` + const count = `count: [${state.count}]` + const min = `min: [${state.min}]` + const max = `max: [${state.max}]` + const sum = `sum: [${state.sum}]` + return `${prefix}[${quantiles}, ${count}, ${min}, ${max}, ${sum}]` + } + } +} + +const renderKeyValues = (keyValues: Iterable<[number | string, string | number]>): string => + Array.from(keyValues).map(([key, value]) => `(${key} -> ${value})`).join(", ") + +const attributesToString = (attributes: Metric.AttributeSet): string => { + const attrs = Object.entries(attributes) + const sorted = Arr.sortWith(attrs, (attr) => attr[0], _String.Order) + return `attributes=[${sorted.map(([key, value]) => `${key}: ${value}`).join(", ")}]` +} + +// Metric Boundaries + +/** + * Creates histogram bucket boundaries from an iterable set of values. + * + * **Details** + * + * Processes any iterable of numbers by removing duplicates, filtering out + * non-positive values, and automatically appending positive infinity as the + * final boundary. + * + * **Example** (Creating boundaries from values) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class BoundaryError extends Data.TaggedError("BoundaryError")<{ + * readonly operation: string + * }> {} + * + * // Create boundaries from an array of custom values + * const customBoundaries = Metric.boundariesFromIterable([ + * 10, + * 25, + * 50, + * 100, + * 250, + * 500, + * 1000 + * ]) + * console.log(customBoundaries) // [10, 25, 50, 100, 250, 500, 1000, Infinity] + * + * // Automatically removes duplicates and negative values + * const messyBoundaries = Metric.boundariesFromIterable([ + * -5, + * 0, + * 10, + * 10, + * 25, + * 25, + * 50, + * -1 + * ]) + * console.log(messyBoundaries) // [10, 25, 50, Infinity] + * + * // Works with any iterable (Set, generator functions, etc.) + * const setBoundaries = Metric.boundariesFromIterable( + * new Set([100, 200, 300, 200, 100]) + * ) + * console.log(setBoundaries) // [100, 200, 300, Infinity] + * + * // Use with histogram metric + * const responseTimeHistogram = Metric.histogram("response_times", { + * description: "API response time distribution", + * boundaries: customBoundaries + * }) + * + * const program = Effect.gen(function*() { + * yield* Metric.update(responseTimeHistogram, 75) // Goes in 50-100ms bucket + * yield* Metric.update(responseTimeHistogram, 150) // Goes in 100-250ms bucket + * + * const value = yield* Metric.value(responseTimeHistogram) + * return value + * }) + * ``` + * + * @category boundaries + * @since 4.0.0 + */ +export const boundariesFromIterable = (iterable: Iterable): ReadonlyArray => + Arr.append(Arr.filter(new Set(iterable), (n) => n > 0), Number.POSITIVE_INFINITY) + +/** + * Creates histogram bucket boundaries from a linear sequence and appends + * positive infinity. + * + * **Details** + * + * Generates `count - 1` finite boundaries using `start + width + index` for + * each zero-based index, then applies the same normalization as + * `boundariesFromIterable`: non-positive values are removed, duplicates are + * collapsed, and `Infinity` is appended. + * + * **Example** (Creating linear boundaries) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class BoundaryError extends Data.TaggedError("BoundaryError")<{ + * readonly operation: string + * }> {} + * + * // Create boundaries for response time histogram + * const responseBoundaries = Metric.linearBoundaries({ + * start: 0, // Starting point + * width: 100, // Offset used for the first boundary + * count: 5 // Creates 4 boundaries + infinity + * }) + * console.log(responseBoundaries) // [100, 101, 102, 103, Infinity] + * + * // Create a histogram using these boundaries + * const responseTimeHistogram = Metric.histogram("api_response_time", { + * description: "API response time distribution", + * boundaries: responseBoundaries + * }) + * + * const program = Effect.gen(function*() { + * // Record some response times + * yield* Metric.update(responseTimeHistogram, 85) + * yield* Metric.update(responseTimeHistogram, 101) + * yield* Metric.update(responseTimeHistogram, 450) + * + * const value = yield* Metric.value(responseTimeHistogram) + * return value + * }) + * ``` + * + * @category boundaries + * @since 4.0.0 + */ +export const linearBoundaries = (options: { + readonly start: number + readonly width: number + readonly count: number +}): ReadonlyArray => + boundariesFromIterable(Arr.makeBy(options.count - 1, (n) => options.start + n + options.width)) + +/** + * Creates histogram bucket boundaries with exponentially increasing values. + * + * **Details** + * + * Creates boundaries that grow exponentially, useful for metrics that span + * multiple orders of magnitude. Each boundary is calculated as start * factor^i. + * + * **Example** (Creating exponential boundaries) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class BoundaryError extends Data.TaggedError("BoundaryError")<{ + * readonly operation: string + * }> {} + * + * // Create exponential boundaries for request size histogram + * // Buckets: 0-1KB, 1-2KB, 2-4KB, 4-8KB, 8KB+ + * const sizeBoundaries = Metric.exponentialBoundaries({ + * start: 1, // Starting at 1KB + * factor: 2, // Each boundary doubles the previous + * count: 5 // Creates 4 boundaries + infinity + * }) + * console.log(sizeBoundaries) // [1, 2, 4, 8, Infinity] + * + * // Create a histogram for tracking request payload sizes + * const requestSizeHistogram = Metric.histogram("request_size_kb", { + * description: "Request payload size distribution in KB", + * boundaries: sizeBoundaries + * }) + * + * // For very wide ranges, use larger factors + * const latencyBoundaries = Metric.exponentialBoundaries({ + * start: 0.1, // Start at 0.1ms + * factor: 10, // Each boundary is 10x larger + * count: 6 // Creates ranges: 0.1ms, 1ms, 10ms, 100ms, 1000ms+ + * }) + * + * const program = Effect.gen(function*() { + * // Record different request sizes + * yield* Metric.update(requestSizeHistogram, 1.5) // Goes in 1-2KB bucket + * yield* Metric.update(requestSizeHistogram, 3.2) // Goes in 2-4KB bucket + * yield* Metric.update(requestSizeHistogram, 12) // Goes in 8KB+ bucket + * + * const value = yield* Metric.value(requestSizeHistogram) + * return value + * }) + * ``` + * + * @category boundaries + * @since 4.0.0 + */ +export const exponentialBoundaries = (options: { + readonly start: number + readonly factor: number + readonly count: number +}): ReadonlyArray => + boundariesFromIterable(Arr.makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i))) + +// Fiber Runtime Metrics + +const fibersActive = gauge("child_fibers_active", { + description: "The current count of active child fibers" +}) +const fibersStarted = counter("child_fibers_started", { + description: "The total number of child fibers that have been started", + incremental: true +}) +const fiberSuccesses = counter("child_fiber_successes", { + description: "The total number of child fibers that have succeeded", + incremental: true +}) +const fiberFailures = counter("child_fiber_failures", { + description: "The total number of child fibers that have failed", + incremental: true +}) + +/** + * Service key for the fiber runtime metrics service. + * + * **Example** (Accessing the fiber runtime metrics key) + * + * ```ts + * import { Data, Effect, Layer, Metric } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // The key is used internally by the Effect runtime to manage fiber metrics + * const key = Metric.FiberRuntimeMetricsKey + * console.log("Fiber metrics key:", key) + * + * // Enable runtime metrics using the key + * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)( + * Metric.FiberRuntimeMetricsImpl + * ) + * + * return yield* Effect.gen(function*() { + * // This Effect will have fiber metrics automatically collected + * yield* Effect.sleep("100 millis") + * + * // Create a test counter to demonstrate the key usage + * const testCounter = Metric.counter("test_counter") + * yield* Metric.update(testCounter, 1) + * return yield* Metric.value(testCounter) + * }).pipe(Effect.provide(layer)) + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const FiberRuntimeMetricsKey: "effect/observability/Metric/FiberRuntimeMetricsKey" = + InternalMetric.FiberRuntimeMetricsKey + +/** + * Interface for the fiber runtime metrics service that tracks fiber lifecycle events. + * + * **Example** (Providing a custom fiber metrics service) + * + * ```ts + * import { Data, Effect, Layer, Metric } from "effect" + * import type { Context, Exit } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * // Custom implementation of the metrics service + * const customMetricsService: Metric.FiberRuntimeMetricsService = { + * recordFiberStart: (context: Context.Context) => { + * console.log("Fiber started") + * // Custom logic for tracking fiber starts + * }, + * recordFiberEnd: ( + * context: Context.Context, + * exit: Exit.Exit + * ) => { + * console.log("Fiber completed with exit:", exit) + * // Custom logic for tracking fiber completion based on exit status + * } + * } + * + * const program = Effect.gen(function*() { + * // Use the custom metrics service + * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)(customMetricsService) + * + * return yield* Effect.sleep("100 millis").pipe(Effect.provide(layer)) + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export interface FiberRuntimeMetricsService { + readonly recordFiberStart: (context: Context.Context) => void + readonly recordFiberEnd: (context: Context.Context, exit: Exit) => void +} + +/** + * Context reference for the optional service that records fiber runtime + * metrics. + * + * **When to use** + * + * Use to provide or inspect the service that receives fiber start and end + * notifications for automatic runtime metrics. + * + * **Details** + * + * When provided, the runtime can notify the service about child-fiber start and + * end events. When the reference is `undefined`, automatic fiber runtime metric + * collection is disabled. + * + * **Example** (Accessing the fiber runtime metrics service) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Access the fiber runtime metrics service + * const metricsService = yield* Metric.FiberRuntimeMetrics + * + * if (metricsService) { + * console.log("Runtime metrics are enabled") + * } else { + * console.log("Runtime metrics are disabled") + * } + * + * // Enable runtime metrics for the application + * const enabledLayer = Metric.enableRuntimeMetricsLayer + * + * return yield* Effect.gen(function*() { + * // Create some concurrent fibers to see metrics in action + * yield* Effect.all([ + * Effect.sleep("100 millis"), + * Effect.sleep("200 millis"), + * Effect.sleep("300 millis") + * ], { concurrency: "unbounded" }) + * + * // Create test metrics to demonstrate the service + * const testCounter = Metric.counter("test_counter") + * yield* Metric.update(testCounter, 5) + * const counterValue = yield* Metric.value(testCounter) + * + * return { counterValue, metricsEnabled: true } + * }).pipe(Effect.provide(enabledLayer)) + * }) + * ``` + * + * @category runtime metrics + * @since 4.0.0 + */ +export const FiberRuntimeMetrics = Context.Reference( + InternalMetric.FiberRuntimeMetricsKey, + { defaultValue: constUndefined } +) + +/** + * Default implementation of the fiber runtime metrics service. + * + * **Example** (Accessing the default fiber metrics implementation) + * + * ```ts + * import { Data, Effect, Layer, Metric } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Use the default metrics implementation + * const metrics = Metric.FiberRuntimeMetricsImpl + * console.log("Metrics implementation:", metrics) + * + * // Enable runtime metrics using the default implementation + * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)(metrics) + * + * return yield* Effect.gen(function*() { + * // Run some Effects to trigger metric collection + * yield* Effect.forkChild(Effect.sleep("50 millis")) + * yield* Effect.forkChild(Effect.sleep("100 millis")) + * + * // Wait a bit and check the metrics + * yield* Effect.sleep("200 millis") + * + * // Create test metrics to demonstrate the implementation + * const testCounter = Metric.counter("test_counter") + * const testGauge = Metric.gauge("test_gauge") + * yield* Metric.update(testCounter, 3) + * yield* Metric.update(testGauge, 42) + * + * const counterValue = yield* Metric.value(testCounter) + * const gaugeValue = yield* Metric.value(testGauge) + * + * return { counter: counterValue, gauge: gaugeValue } + * }).pipe(Effect.provide(layer)) + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const FiberRuntimeMetricsImpl: FiberRuntimeMetricsService = { + recordFiberStart(context: Context.Context) { + fibersStarted.updateUnsafe(1, context) + fibersActive.modifyUnsafe(1, context) + }, + recordFiberEnd(context: Context.Context, exit: Exit) { + fibersActive.modifyUnsafe(-1, context) + if (InternalEffect.exitIsSuccess(exit)) { + fiberSuccesses.updateUnsafe(1, context) + } else { + fiberFailures.updateUnsafe(1, context) + } + } +} + +/** + * Layer that enables automatic collection of fiber runtime metrics across + * an entire Effect application. + * + * **When to use** + * + * Use when you need runtime metrics collection for all Effects in the + * application context rather than wrapping individual Effects. + * + * **Example** (Enabling runtime metrics with a layer) + * + * ```ts + * import { Console, Data, Effect, Layer, Metric } from "effect" + * + * class AppError extends Data.TaggedError("AppError")<{ + * readonly operation: string + * }> {} + * + * // Define your application logic + * const userService = Effect.gen(function*() { + * // Simulate user operations with concurrent processing + * const fetchUser = (id: number) => + * Effect.gen(function*() { + * yield* Effect.sleep(`${50 + id * 10} millis`) + * if (id % 7 === 0) { + * return yield* new AppError({ operation: `fetch-user-${id}` }) + * } + * return { id, name: `User ${id}`, email: `user${id}@example.com` } + * }) + * + * // Process multiple users concurrently (ignoring failures for demo) + * const userIds = Array.from({ length: 10 }, (_, i) => i + 1) + * const userTasks = userIds.map((id) => + * fetchUser(id).pipe(Effect.catchTag("AppError", () => Effect.succeed(null))) + * ) + * const allUsers = yield* Effect.all(userTasks, { concurrency: 4 }) + * const successfulUsers = allUsers.filter((user) => user !== null) + * return successfulUsers + * }) + * + * const analyticsService = Effect.gen(function*() { + * // Simulate analytics processing + * const tasks = Array.from({ length: 8 }, (_, i) => + * Effect.gen(function*() { + * yield* Effect.sleep(`${100 + i * 25} millis`) + * return `Analytics task ${i} completed` + * })) + * return yield* Effect.all(tasks, { concurrency: 3 }) + * }) + * + * // Main application that uses multiple services + * const application = Effect.gen(function*() { + * yield* Console.log("Starting application with runtime metrics...") + * + * // Run services concurrently + * const [users, analytics] = yield* Effect.all([ + * userService, + * analyticsService + * ], { concurrency: 2 }) + * + * yield* Console.log( + * `Processed ${users.length} users and ${analytics.length} analytics tasks` + * ) + * + * // Inspect the automatically collected runtime metrics + * const metrics = yield* Metric.snapshot + * const runtimeMetrics = metrics.filter((m) => m.id.startsWith("child_fiber")) + * + * yield* Console.log("Runtime Metrics Collected:") + * for (const metric of runtimeMetrics) { + * yield* Console.log(` ${metric.id}: ${JSON.stringify(metric.state)}`) + * } + * + * return { users, analytics, metricsCount: runtimeMetrics.length } + * }) + * + * // Create the base application layer + * const AppLayer = Layer.empty // Add your application layers here (database, HTTP, etc.) + * + * // Add runtime metrics layer at the end + * const AppLayerWithMetrics = AppLayer.pipe( + * Layer.provide(Metric.enableRuntimeMetricsLayer) + * ) + * + * // Run the application with runtime metrics enabled + * const program = application.pipe( + * Effect.provide(AppLayerWithMetrics) + * ) + * + * // Alternative: Provide runtime metrics directly to the application + * const programWithDirectMetrics = application.pipe( + * Effect.provide(Metric.enableRuntimeMetricsLayer) + * ) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(FiberRuntimeMetricsImpl) + +/** + * Layer that disables automatic collection of fiber runtime metrics. + * + * **Example** (Disabling runtime metrics with a layer) + * + * ```ts + * import { Data, Effect, Metric } from "effect" + * + * class MetricsError extends Data.TaggedError("MetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Disable runtime metrics collection + * const disabledLayer = Metric.disableRuntimeMetricsLayer + * + * return yield* Effect.gen(function*() { + * // Check that metrics service is disabled + * const metricsService = yield* Metric.FiberRuntimeMetrics + * console.log("Metrics enabled:", metricsService !== undefined) // false + * + * // Run some Effects - no metrics will be collected + * yield* Effect.forkChild(Effect.sleep("50 millis")) + * yield* Effect.forkChild(Effect.sleep("100 millis")) + * yield* Effect.sleep("200 millis") + * + * // Create test metrics to show they still work + * const testCounter = Metric.counter("test_counter") + * yield* Metric.update(testCounter, 1) + * const counterValue = yield* Metric.value(testCounter) + * + * return { counterValue, metricsEnabled: metricsService !== undefined } + * }).pipe(Effect.provide(disabledLayer)) + * }) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const disableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(undefined) + +/** + * Enables automatic collection of fiber runtime metrics for the provided Effect. + * + * **Details** + * + * When enabled, automatically tracks fiber lifecycle metrics including active fibers, + * started fibers, successful completions, and failures. These metrics provide valuable + * insights into the concurrency patterns and health of your Effect application. + * + * **Example** (Enabling runtime metrics for an effect) + * + * ```ts + * import { Console, Data, Effect, Layer, Metric } from "effect" + * + * class RuntimeMetricsError extends Data.TaggedError("RuntimeMetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // Create a concurrent workload to demonstrate fiber metrics + * const heavyWorkload = Effect.gen(function*() { + * // Simulate concurrent operations + * const tasks = Array.from({ length: 10 }, (_, i) => + * Effect.gen(function*() { + * yield* Effect.sleep(`${100 + i * 50} millis`) + * if (i % 4 === 0) { + * // Simulate some failures + * return yield* new RuntimeMetricsError({ operation: `task-${i}` }) + * } + * return `Task ${i} completed` + * }).pipe( + * Effect.catchTag("RuntimeMetricsError", () => + * Effect.succeed(`Task ${i} failed`)) + * )) + * + * // Run tasks concurrently + * const results = yield* Effect.all(tasks, { concurrency: 5 }) + * return results + * }) + * + * // Enable runtime metrics collection for our workload + * const workloadWithMetrics = Metric.enableRuntimeMetrics(heavyWorkload) + * + * // Execute the workload + * const results = yield* workloadWithMetrics + * + * // After execution, we can inspect the runtime metrics + * // The following metrics are automatically collected: + * // - child_fibers_active: Current number of active child fibers (Gauge) + * // - child_fibers_started: Total child fibers started (Counter, incremental) + * // - child_fiber_successes: Total successful child fibers (Counter, incremental) + * // - child_fiber_failures: Total failed child fibers (Counter, incremental) + * + * yield* Console.log(`Workload completed with ${results.length} results`) + * + * // Get all metrics including the runtime metrics + * const allMetrics = yield* Metric.snapshot + * const runtimeMetrics = allMetrics.filter((m) => + * m.id.startsWith("child_fiber") || m.id.includes("fiber") + * ) + * + * yield* Console.log("Runtime Metrics:") + * for (const metric of runtimeMetrics) { + * yield* Console.log(` ${metric.id}: ${JSON.stringify(metric.state)}`) + * } + * + * return results + * }) + * + * // Alternative: Use the layer version for broader application coverage + * const BaseAppLayer = Layer.empty // Your base application layers + * const AppLayerWithMetrics = BaseAppLayer.pipe( + * Layer.provide(Metric.enableRuntimeMetricsLayer) + * ) + * const programWithLayer = program.pipe( + * Effect.provide(AppLayerWithMetrics) + * ) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const enableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService( + FiberRuntimeMetrics, + FiberRuntimeMetricsImpl +) + +/** + * Disables automatic collection of fiber runtime metrics for the provided Effect. + * + * **When to use** + * + * Use when you need to disable runtime metrics for a specific effect while + * keeping them enabled elsewhere. + * + * **Example** (Disabling runtime metrics for an effect) + * + * ```ts + * import { Console, Data, Effect, Layer, Metric } from "effect" + * + * class DisableMetricsError extends Data.TaggedError("DisableMetricsError")<{ + * readonly operation: string + * }> {} + * + * const program = Effect.gen(function*() { + * // This section will have runtime metrics enabled + * const normalOperation = Effect.gen(function*() { + * const tasks = Array.from({ length: 5 }, (_, i) => + * Effect.gen(function*() { + * yield* Effect.sleep(`${100 + i * 20} millis`) + * return `Normal task ${i} completed` + * })) + * return yield* Effect.all(tasks, { concurrency: 3 }) + * }) + * + * // This section will have runtime metrics disabled for performance + * const highPerformanceOperation = Metric.disableRuntimeMetrics( + * Effect.gen(function*() { + * // Performance-critical code where metrics overhead should be avoided + * const hotPath = Array.from( + * { length: 1000 }, + * (_, i) => + * Effect.gen(function*() { + * // Simulate intensive computation + * const result = i * i + (i % 10) / 10 + * return result + * }) + * ) + * return yield* Effect.all(hotPath, { concurrency: 100 }) + * }) + * ) + * + * yield* Console.log("Running operations with selective metrics...") + * + * // Run both operations + * const [normalResults, performanceResults] = yield* Effect.all([ + * normalOperation, // Will generate fiber metrics + * highPerformanceOperation // Will NOT generate fiber metrics + * ]) + * + * // Check collected metrics - should only see metrics from normalOperation + * const metrics = yield* Metric.snapshot + * const runtimeMetrics = metrics.filter((m) => m.id.startsWith("child_fiber")) + * + * yield* Console.log(`Normal operation results: ${normalResults.length}`) + * yield* Console.log( + * `Performance operation results: ${performanceResults.length}` + * ) + * yield* Console.log(`Runtime metrics collected: ${runtimeMetrics.length}`) + * + * // The runtime metrics will only reflect the fibers from normalOperation + * // The highPerformanceOperation fibers were not tracked due to disableRuntimeMetrics + * + * return { normalResults, performanceResults, runtimeMetrics } + * }) + * + * // Enable runtime metrics globally, then selectively disable where needed + * const BaseAppLayer = Layer.empty // Your base application layers + * const AppLayerWithMetrics = BaseAppLayer.pipe( + * Layer.provide(Metric.enableRuntimeMetricsLayer) + * ) + * const finalProgram = program.pipe( + * Effect.provide(AppLayerWithMetrics) + * ) + * ``` + * + * @category metrics + * @since 4.0.0 + */ +export const disableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService( + FiberRuntimeMetrics, + undefined +) + +// Utilities + +function makeKey( + metric: Metric, + attributes: Metric.Attributes | undefined +) { + let key = `${metric.type}:${metric.id}` + if (Predicate.isNotUndefined(metric.description)) { + key += `:${metric.description}` + } + if (Predicate.isNotUndefined(attributes)) { + key += `:${serializeAttributes(attributes)}` + } + return key +} + +function makeHooks( + get: (context: Context.Context) => State, + update: (input: Input, context: Context.Context) => void, + modify?: (input: Input, context: Context.Context) => void +): Metric.Hooks { + return { get, update, modify: modify ?? update } +} + +function serializeAttributes(attributes: Metric.Attributes): string { + return serializeEntries(Array.isArray(attributes) ? attributes : Object.entries(attributes)) +} + +function serializeEntries(entries: ReadonlyArray<[string, string]>): string { + return entries.map(([key, value]) => `${key}=${value}`).join(",") +} + +function mergeAttributes( + self: Metric.Attributes | undefined, + other: Metric.Attributes | undefined +): Metric.AttributeSet { + return { ...attributesToRecord(self), ...attributesToRecord(other) } +} + +function attributesToRecord(attributes?: Metric.Attributes): Metric.AttributeSet | undefined { + if (Predicate.isNotUndefined(attributes) && Array.isArray(attributes)) { + return attributes.reduce((acc, [key, value]) => { + acc[key] = value + return acc + }, {} as Metric.AttributeSet) + } + return attributes as Metric.AttributeSet | undefined +} + +function addAttributesToContext( + context: Context.Context, + attributes: Metric.Attributes +): Context.Context { + const current = Context.get(context, CurrentMetricAttributes) + const updated = mergeAttributes(current, attributes) + return Context.add(context, CurrentMetricAttributes, updated) +} diff --git a/repos/effect/packages/effect/src/MutableHashMap.ts b/repos/effect/packages/effect/src/MutableHashMap.ts new file mode 100644 index 0000000..25aa087 --- /dev/null +++ b/repos/effect/packages/effect/src/MutableHashMap.ts @@ -0,0 +1,845 @@ +/** + * Stores key/value entries in a mutable hash map. + * + * `MutableHashMap` updates the same collection in place and supports fast + * lookup, insertion, removal, clearing, and iteration. It combines a native + * `Map` for ordinary JavaScript keys with hash buckets for keys that implement + * Effect `Equal` and `Hash`, so callers can mix reference-based and structural + * lookup in the same collection. + * + * @since 2.0.0 + */ +import type { NonEmptyArray } from "./Array.ts" +import * as Equal from "./Equal.ts" +import { format } from "./Formatter.ts" +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { type Inspectable, NodeInspectSymbol, toJson } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" + +const TypeId = "~effect/collections/MutableHashMap" + +/** + * A mutable hash map that stores key-value pairs and supports both referential + * and Effect structural equality. + * + * **When to use** + * + * Use as a mutable key-value map when in-place updates are acceptable and keys + * may rely on Effect structural equality. + * + * **Details** + * + * Operations mutate the map in place. Keys that implement `Equal` / `Hash` can + * be looked up structurally; other keys use normal JavaScript reference or + * primitive equality. + * + * **Example** (Using a mutable hash map) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * // Create a mutable hash map with string keys and number values + * const map: MutableHashMap.MutableHashMap = MutableHashMap + * .empty() + * + * // Add some data + * MutableHashMap.set(map, "count", 42) + * MutableHashMap.set(map, "total", 100) + * + * // Use as iterable + * for (const [key, value] of map) { + * console.log(`${key}: ${value}`) + * } + * // Output: + * // count: 42 + * // total: 100 + * + * // Convert to array + * const entries = Array.from(map) + * console.log(entries) // [["count", 42], ["total", 100]] + * ``` + * + * @see {@link empty} for creating an empty mutable hash map + * @see {@link get} for reading values by key + * @see {@link set} for mutating entries by key + * + * @category models + * @since 2.0.0 + */ +export interface MutableHashMap extends Iterable<[K, V]>, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + readonly backing: Map + readonly buckets: Map> +} + +/** + * Checks whether the specified value is a `MutableHashMap`, `false` otherwise. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a mutable hash map. + * + * **Details** + * + * The check looks for the `MutableHashMap` runtime marker. + * + * **Gotchas** + * + * The check does not validate the key or value types carried by the map. + * + * @see {@link MutableHashMap} for the mutable hash map interface + * + * @category refinements + * @since 4.0.0 + */ +export const isMutableHashMap = (value: unknown): value is MutableHashMap => hasProperty(value, TypeId) + +const MutableHashMapProto: Omit, "backing" | "buckets" | "bucketsSize"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableHashMap): Iterator<[unknown, unknown]> { + return this.backing[Symbol.iterator]() + }, + toString() { + return `MutableHashMap(${format(Array.from(this))})` + }, + toJSON() { + return { + _id: "MutableHashMap", + values: toJson(Array.from(this)) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Creates an empty MutableHashMap. + * + * **When to use** + * + * Use to create a fresh mutable map before adding entries over time. + * + * **Details** + * + * Each call returns a new empty map instance. + * + * **Example** (Creating an empty map) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.empty() + * + * // Add some entries + * MutableHashMap.set(map, "key1", 42) + * MutableHashMap.set(map, "key2", 100) + * + * console.log(MutableHashMap.size(map)) // 2 + * ``` + * + * @see {@link make} for creating a map from explicit entries + * @see {@link fromIterable} for creating a map from an iterable of entries + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): MutableHashMap => { + const self = Object.create(MutableHashMapProto) + self.backing = new Map() + self.buckets = new Map() + return self +} + +/** + * Creates a MutableHashMap from a variable number of key-value pairs. + * + * **When to use** + * + * Use to create a mutable hash map from explicit entries known at the call site. + * + * **Example** (Creating a map from entries) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make( + * ["key1", 42], + * ["key2", 100], + * ["key3", 200] + * ) + * + * console.log(MutableHashMap.get(map, "key1")) // Some(42) + * console.log(MutableHashMap.size(map)) // 3 + * ``` + * + * @see {@link empty} for creating an empty map + * @see {@link fromIterable} for creating a map from an iterable of entries + * + * @category constructors + * @since 2.0.0 + */ +export const make: >( + ...entries: Entries +) => MutableHashMap< + Entries[number] extends readonly [infer K, any] ? K : never, + Entries[number] extends readonly [any, infer V] ? V : never +> = (...entries) => fromIterable(entries) + +/** + * Creates a MutableHashMap from an iterable collection of key-value pairs. + * + * **When to use** + * + * Use to create a mutable hash map from an existing iterable of entries. + * + * **Example** (Creating a map from an iterable) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const entries = [ + * ["apple", 1], + * ["banana", 2], + * ["cherry", 3] + * ] as const + * + * const map = MutableHashMap.fromIterable(entries) + * + * console.log(MutableHashMap.get(map, "banana")) // Some(2) + * console.log(MutableHashMap.size(map)) // 3 + * + * // Works with any iterable + * const fromMap = MutableHashMap.fromIterable(new Map([["x", 10], ["y", 20]])) + * console.log(MutableHashMap.get(fromMap, "x")) // Some(10) + * ``` + * + * @see {@link make} for creating a map from explicit entries + * @see {@link empty} for creating an empty map + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (entries: Iterable): MutableHashMap => { + const self = empty() + for (const [key, value] of entries) { + set(self, key, value) + } + return self +} + +/** + * Looks up a key in the `MutableHashMap` safely. + * + * **When to use** + * + * Use to safely read a `MutableHashMap` value for a key as an `Option`. + * + * **Details** + * + * Returns `Some(value)` when an equal key is present and `None` when the key is + * absent. + * + * **Example** (Getting a value) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make(["key1", 42], ["key2", 100]) + * + * console.log(MutableHashMap.get(map, "key1")) // Some(42) + * console.log(MutableHashMap.get(map, "key3")) // None + * + * // Pipe-able version + * const getValue = MutableHashMap.get("key1") + * console.log(getValue(map)) // Some(42) + * ``` + * + * @see {@link has} for checking only whether a key is present + * @see {@link set} for inserting or replacing a value by key + * + * @category elements + * @since 2.0.0 + */ +export const get: { + (key: K): (self: MutableHashMap) => Option.Option + (self: MutableHashMap, key: K): Option.Option +} = dual< + (key: K) => (self: MutableHashMap) => Option.Option, + (self: MutableHashMap, key: K) => Option.Option +>(2, (self: MutableHashMap, key: K): Option.Option => { + if (self.backing.has(key)) { + return Option.some(self.backing.get(key)!) + } else if (isSimpleKey(key)) { + return Option.none() + } + const refKey = referentialKeysCache.get(self) + if (refKey !== undefined) { + return self.backing.has(refKey) ? Option.some(self.backing.get(refKey)!) : Option.none() + } + const hash = Hash.hash(key) + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return Option.none() + } + return getFromBucket(self, bucket, key) +}) + +const referentialKeysCache = new WeakMap() +const isSimpleKey = (u: unknown): boolean => typeof u !== "object" && typeof u !== "function" + +/** + * Returns an iterable over the keys in the `MutableHashMap`. + * + * **When to use** + * + * Use to iterate over the keys currently stored in a mutable hash map. + * + * **Example** (Reading keys) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make( + * ["apple", 1], + * ["banana", 2], + * ["cherry", 3] + * ) + * + * const allKeys = Array.from(MutableHashMap.keys(map)) + * console.log(allKeys) // ["apple", "banana", "cherry"] + * + * // Useful for iteration or validation + * const hasRequiredKeys = allKeys.includes("apple") && allKeys.includes("banana") + * ``` + * + * @see {@link values} for iterating over stored values + * @see {@link has} for checking one key without iterating + * + * @category elements + * @since 3.8.0 + */ +export const keys = (self: MutableHashMap): Iterable => self.backing.keys() + +/** + * Returns an iterable over the values in the `MutableHashMap`. + * + * **When to use** + * + * Use to iterate over the values currently stored in a mutable hash map. + * + * **Example** (Reading values) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make( + * ["apple", 1], + * ["banana", 2], + * ["cherry", 3] + * ) + * + * const allValues = Array.from(MutableHashMap.values(map)) + * console.log(allValues) // [1, 2, 3] + * + * // Useful for calculations + * const total = allValues.reduce((sum, value) => sum + value, 0) + * console.log(total) // 6 + * + * // Filter values + * const largeValues = allValues.filter((value) => value > 1) + * console.log(largeValues) // [2, 3] + * ``` + * + * @see {@link keys} for iterating over stored keys + * + * @category elements + * @since 3.8.0 + */ +export const values = (self: MutableHashMap): Iterable => self.backing.values() + +const getFromBucket = ( + self: MutableHashMap, + bucket: NonEmptyArray, + key: K +): Option.Option => { + for (let i = 0, len = bucket.length; i < len; i++) { + if (Equal.equals(key, bucket[i])) { + const refKey = bucket[i] + referentialKeysCache.set(key, refKey) + return Option.some(self.backing.get(refKey)!) + } + } + return Option.none() +} + +/** + * Checks whether the MutableHashMap contains the specified key. + * + * **When to use** + * + * Use to test whether a key is present in a `MutableHashMap` without reading + * its value. + * + * **Example** (Checking for a key) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make(["key1", 42], ["key2", 100]) + * + * console.log(MutableHashMap.has(map, "key1")) // true + * console.log(MutableHashMap.has(map, "key3")) // false + * + * // Pipe-able version + * const hasKey = MutableHashMap.has("key1") + * console.log(hasKey(map)) // true + * ``` + * + * @see {@link get} for reading the value as an `Option` + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (key: K): (self: MutableHashMap) => boolean + (self: MutableHashMap, key: K): boolean +} = dual< + (key: K) => (self: MutableHashMap) => boolean, + (self: MutableHashMap, key: K) => boolean +>(2, (self, key) => Option.isSome(get(self, key))) + +/** + * Sets a key-value pair in the MutableHashMap, mutating the map in place. + * If the key already exists, its value is updated. + * + * **When to use** + * + * Use to insert a new `MutableHashMap` entry or replace an existing entry in + * place. + * + * **Example** (Setting key-value pairs) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.empty() + * + * // Add new entries + * MutableHashMap.set(map, "key1", 42) + * MutableHashMap.set(map, "key2", 100) + * + * console.log(MutableHashMap.get(map, "key1")) // Some(42) + * console.log(MutableHashMap.size(map)) // 2 + * + * // Update existing entry + * MutableHashMap.set(map, "key1", 999) + * console.log(MutableHashMap.get(map, "key1")) // Some(999) + * + * // Pipe-able version + * const setKey = MutableHashMap.set("key3", 300) + * setKey(map) + * console.log(MutableHashMap.size(map)) // 3 + * ``` + * + * @see {@link modify} for updating an existing value with a function + * @see {@link modifyAt} for setting or removing based on the current optional value + * @see {@link remove} for deleting an entry by key + * + * @category mutations + * @since 2.0.0 + */ +export const set: { + (key: K, value: V): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, value: V): MutableHashMap +} = dual< + (key: K, value: V) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K, value: V) => MutableHashMap +>(3, (self: MutableHashMap, key: K, value: V) => { + if (self.backing.has(key) || isSimpleKey(key)) { + self.backing.set(key, value) + return self + } + let refKey = referentialKeysCache.get(self) + if (refKey !== undefined && self.backing.has(refKey)) { + self.backing.set(refKey, value) + return self + } + + const hash = Hash.hash(key) + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + self.buckets.set(hash, [key]) + self.backing.set(key, value) + return self + } + + refKey = getRefKey(bucket, key) + if (refKey === undefined) { + bucket.push(key) + refKey = key + } + self.backing.set(refKey, value) + return self +}) + +const getRefKey = ( + bucket: NonEmptyArray, + key: K +) => { + for (let i = 0, len = bucket.length; i < len; i++) { + if (Equal.equals(key, bucket[i])) { + referentialKeysCache.set(key, bucket[i]) + return bucket[i] + } + } +} + +/** + * Updates the value of the specified key within the MutableHashMap if it exists. + * If the key doesn't exist, the map remains unchanged. + * + * **When to use** + * + * Use to transform an existing `MutableHashMap` value in place without + * inserting missing keys. + * + * **Example** (Modifying existing values) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make(["count", 5], ["total", 100]) + * + * // Increment existing value + * MutableHashMap.modify(map, "count", (n) => n + 1) + * console.log(MutableHashMap.get(map, "count")) // Some(6) + * + * // Double existing value + * MutableHashMap.modify(map, "total", (n) => n * 2) + * console.log(MutableHashMap.get(map, "total")) // Some(200) + * + * // Try to modify non-existent key (no effect) + * MutableHashMap.modify(map, "missing", (n) => n + 1) + * console.log(MutableHashMap.has(map, "missing")) // false + * + * // Pipe-able version + * const increment = MutableHashMap.modify("count", (n: number) => n + 1) + * increment(map) + * ``` + * + * @see {@link set} for inserting or replacing a value directly + * @see {@link modifyAt} for handling both missing and existing keys + * + * @category mutations + * @since 2.0.0 + */ +export const modify: { + (key: K, f: (v: V) => V): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, f: (v: V) => V): MutableHashMap +} = dual< + (key: K, f: (v: V) => V) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K, f: (v: V) => V) => MutableHashMap +>(3, (self: MutableHashMap, key: K, f: (v: V) => V) => { + const hasKey = self.backing.has(key) + if (hasKey || isSimpleKey(key)) { + if (hasKey) { + self.backing.set(key, f(self.backing.get(key)!)) + } + return self + } + let refKey = referentialKeysCache.get(self) + if (refKey !== undefined && self.backing.has(refKey)) { + self.backing.set(refKey, f(self.backing.get(refKey)!)) + return self + } + + const hash = Hash.hash(key) + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return self + } + + refKey = getRefKey(bucket, key) + if (refKey === undefined) { + return self + } + self.backing.set(refKey, f(self.backing.get(refKey)!)) + return self +}) + +/** + * Updates or removes the specified key using a function from the current + * optional value to the next optional value. + * + * **When to use** + * + * Use to decide whether to insert, update, or remove a key based on its current + * optional value. + * + * **Example** (Updating or removing a key) + * + * ```ts + * import { MutableHashMap, Option } from "effect" + * + * const map = MutableHashMap.make(["count", 5]) + * + * // Update existing key + * MutableHashMap.modifyAt( + * map, + * "count", + * (option) => Option.map(option, (n) => n * 2) + * ) + * console.log(MutableHashMap.get(map, "count")) // Some(10) + * + * // Add new key + * MutableHashMap.modifyAt( + * map, + * "new", + * (option) => Option.isNone(option) ? Option.some(42) : option + * ) + * console.log(MutableHashMap.get(map, "new")) // Some(42) + * + * // Remove key by returning None + * MutableHashMap.modifyAt(map, "count", () => Option.none()) + * console.log(MutableHashMap.has(map, "count")) // false + * + * // Conditional update + * MutableHashMap.modifyAt( + * map, + * "new", + * (option) => Option.filter(option, (n) => n > 50) // Remove if <= 50 + * ) + * console.log(MutableHashMap.has(map, "new")) // false (42 <= 50) + * ``` + * + * @see {@link modify} for updating only when the key already exists + * @see {@link set} for inserting or replacing directly + * @see {@link remove} for deleting directly + * + * @category mutations + * @since 2.0.0 + */ +export const modifyAt: { + (key: K, f: (value: Option.Option) => Option.Option): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K, f: (value: Option.Option) => Option.Option): MutableHashMap +} = dual< + ( + key: K, + f: (value: Option.Option) => Option.Option + ) => (self: MutableHashMap) => MutableHashMap, + ( + self: MutableHashMap, + key: K, + f: (value: Option.Option) => Option.Option + ) => MutableHashMap +>(3, (self, key, f) => { + const current = get(self, key) + const result = f(current) + if (Option.isNone(result)) { + if (Option.isSome(current)) { + remove(self, key) + } + return self + } + set(self, key, result.value) + return self +}) + +/** + * Removes the specified key from the MutableHashMap, mutating the map in place. + * If the key doesn't exist, the map remains unchanged. + * + * **When to use** + * + * Use to delete one key from a mutable hash map in place. + * + * **Example** (Removing a key) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make( + * ["key1", 42], + * ["key2", 100], + * ["key3", 200] + * ) + * + * console.log(MutableHashMap.size(map)) // 3 + * + * // Remove existing key + * MutableHashMap.remove(map, "key2") + * console.log(MutableHashMap.size(map)) // 2 + * console.log(MutableHashMap.has(map, "key2")) // false + * + * // Remove non-existent key (no effect) + * MutableHashMap.remove(map, "nonexistent") + * console.log(MutableHashMap.size(map)) // 2 + * + * // Pipe-able version + * const removeKey = MutableHashMap.remove("key1") + * removeKey(map) + * console.log(MutableHashMap.size(map)) // 1 + * ``` + * + * @see {@link clear} for removing all entries + * @see {@link modifyAt} for conditionally removing based on the current value + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (key: K): (self: MutableHashMap) => MutableHashMap + (self: MutableHashMap, key: K): MutableHashMap +} = dual< + (key: K) => (self: MutableHashMap) => MutableHashMap, + (self: MutableHashMap, key: K) => MutableHashMap +>(2, (self: MutableHashMap, key_: K) => { + if (isSimpleKey(key_)) { + self.backing.delete(key_) + return self + } + + const key = referentialKeysCache.get(self) ?? key_ + const hash = Hash.hash(key) + const bucket = self.buckets.get(hash) + if (bucket === undefined) { + return self + } + for (let i = 0, len = bucket.length; i < len; i++) { + const bkey = bucket[i] + if (bkey === key || Equal.equals(key, bkey)) { + self.backing.delete(bkey) + bucket.splice(i, 1) + break + } + } + if (bucket.length === 0) { + self.buckets.delete(hash) + } + return self +}) + +/** + * Removes all key-value pairs from the MutableHashMap, mutating the map in place. + * The map becomes empty after this operation. + * + * **When to use** + * + * Use to empty a mutable hash map while keeping the same map instance. + * + * **Example** (Clearing all entries) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.make( + * ["key1", 42], + * ["key2", 100], + * ["key3", 200] + * ) + * + * console.log(MutableHashMap.size(map)) // 3 + * + * // Clear all entries + * MutableHashMap.clear(map) + * + * console.log(MutableHashMap.size(map)) // 0 + * console.log(MutableHashMap.has(map, "key1")) // false + * + * // Can still add new entries after clearing + * MutableHashMap.set(map, "new", 999) + * console.log(MutableHashMap.size(map)) // 1 + * ``` + * + * @see {@link remove} for deleting one key + * @see {@link empty} for creating a fresh empty map + * + * @category mutations + * @since 2.0.0 + */ +export const clear = (self: MutableHashMap) => { + self.backing.clear() + self.buckets.clear() + return self +} + +/** + * Returns the number of key-value pairs in the MutableHashMap. + * + * **When to use** + * + * Use to read how many entries are currently stored in the mutable hash map. + * + * **Example** (Checking map size) + * + * ```ts + * import { MutableHashMap } from "effect" + * + * const map = MutableHashMap.empty() + * console.log(MutableHashMap.size(map)) // 0 + * + * MutableHashMap.set(map, "key1", 42) + * MutableHashMap.set(map, "key2", 100) + * console.log(MutableHashMap.size(map)) // 2 + * + * MutableHashMap.remove(map, "key1") + * console.log(MutableHashMap.size(map)) // 1 + * + * MutableHashMap.clear(map) + * console.log(MutableHashMap.size(map)) // 0 + * ``` + * + * @see {@link isEmpty} for checking whether the map has no entries + * + * @category elements + * @since 2.0.0 + */ +export const size = (self: MutableHashMap): number => self.backing.size + +/** + * Returns `true` when the `MutableHashMap` contains no key-value pairs. + * + * **When to use** + * + * Use to branch on whether a mutable map currently has any entries. + * + * @see {@link size} for reading the exact number of entries + * + * @category predicates + * @since 2.0.0 + */ +export const isEmpty = (self: MutableHashMap): boolean => self.backing.size === 0 + +/** + * Runs a callback for each key-value pair in the `MutableHashMap`. + * + * **When to use** + * + * Use to run a synchronous side-effecting callback for every key-value pair in + * an existing mutable map. + * + * **Details** + * + * Iteration follows the backing map's order. The callback receives the value + * first and the key second, matching `Map.prototype.forEach`. + * + * @see {@link keys} for iterating only keys + * @see {@link values} for iterating only values + * + * @category traversing + * @since 2.0.0 + */ +export const forEach: { + (f: (value: V, key: K) => void): (self: MutableHashMap) => void + (self: MutableHashMap, f: (value: V, key: K) => void): void +} = dual(2, (self: MutableHashMap, f: (value: V, key: K) => void) => { + self.backing.forEach(f) +}) diff --git a/repos/effect/packages/effect/src/MutableHashSet.ts b/repos/effect/packages/effect/src/MutableHashSet.ts new file mode 100644 index 0000000..1c5dc0a --- /dev/null +++ b/repos/effect/packages/effect/src/MutableHashSet.ts @@ -0,0 +1,422 @@ +/** + * Stores unique values in a mutable hash set. + * + * `MutableHashSet` updates the same collection in place and supports fast + * membership checks, insertion, removal, clearing, and iteration. It is built + * on `MutableHashMap`: each set value is stored as a map key, so uniqueness + * follows the same hashing and equality rules as the underlying mutable hash + * map. + * + * @since 2.0.0 + */ +import { format } from "./Formatter.ts" +import * as Dual from "./Function.ts" +import { type Inspectable, NodeInspectSymbol, toJson } from "./Inspectable.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" + +const TypeId = "~effect/collections/MutableHashSet" + +/** + * A mutable hash set for storing unique values with Effect structural equality + * support. + * + * **When to use** + * + * Use to store and mutate a collection of unique values with Effect hashing and + * equality semantics. + * + * **Details** + * + * Operations mutate the set in place. Values that implement `Equal` / `Hash` + * can be de-duplicated structurally; other values use normal JavaScript + * reference or primitive equality. + * + * **Example** (Using a mutable hash set) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * // Create a mutable hash set + * const set: MutableHashSet.MutableHashSet = MutableHashSet.make( + * "apple", + * "banana" + * ) + * + * // Add elements + * MutableHashSet.add(set, "cherry") + * + * // Check if elements exist + * console.log(MutableHashSet.has(set, "apple")) // true + * console.log(MutableHashSet.has(set, "grape")) // false + * + * // Iterate over elements + * for (const value of set) { + * console.log(value) // "apple", "banana", "cherry" + * } + * + * // Get size + * console.log(MutableHashSet.size(set)) // 3 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface MutableHashSet extends Iterable, Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + readonly keyMap: MutableHashMap.MutableHashMap +} + +/** + * Checks whether the specified value is a `MutableHashSet`, `false` otherwise. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a mutable hash set. + * + * **Details** + * + * The check looks for the `MutableHashSet` runtime marker. + * + * **Gotchas** + * + * Native `Set` values do not satisfy this check. + * + * @see {@link MutableHashSet} for the mutable hash set interface + * + * @category refinements + * @since 4.0.0 + */ +export const isMutableHashSet = (value: unknown): value is MutableHashSet => hasProperty(value, TypeId) + +const MutableHashSetProto: Omit, "keyMap"> = { + [TypeId]: TypeId, + [Symbol.iterator](this: MutableHashSet): Iterator { + return Array.from(this.keyMap).map(([_]) => _)[Symbol.iterator]() + }, + toString() { + return `MutableHashSet(${format(Array.from(this))})` + }, + toJSON() { + return { + _id: "MutableHashSet", + values: toJson(Array.from(this)) + } + }, + [NodeInspectSymbol]() { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const fromHashMap = (keyMap: MutableHashMap.MutableHashMap): MutableHashSet => { + const set = Object.create(MutableHashSetProto) + set.keyMap = keyMap + return set +} + +/** + * Creates an empty MutableHashSet. + * + * **When to use** + * + * Use to create a fresh mutable set before adding values over time. + * + * **Details** + * + * Each call returns a new empty set backed by an empty `MutableHashMap`. + * + * **Example** (Creating an empty set) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.empty() + * + * // Add some values + * MutableHashSet.add(set, "apple") + * MutableHashSet.add(set, "banana") + * MutableHashSet.add(set, "apple") // Duplicate, no effect + * + * console.log(MutableHashSet.size(set)) // 2 + * console.log(Array.from(set)) // ["apple", "banana"] + * ``` + * + * @see {@link make} for creating a set from explicit values + * @see {@link fromIterable} for creating a set from an iterable of values + * @see {@link clear} for emptying an existing mutable set + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): MutableHashSet => fromHashMap(MutableHashMap.empty()) + +/** + * Creates a MutableHashSet from an iterable collection of values. + * Duplicates are automatically removed. + * + * **When to use** + * + * Use to build a mutable hash set from any iterable of values. + * + * **Example** (Creating a set from an iterable) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const values = ["apple", "banana", "apple", "cherry", "banana"] + * const set = MutableHashSet.fromIterable(values) + * + * console.log(MutableHashSet.size(set)) // 3 + * console.log(Array.from(set)) // ["apple", "banana", "cherry"] + * + * // Works with any iterable + * const fromSet = MutableHashSet.fromIterable(new Set([1, 2, 3])) + * console.log(MutableHashSet.size(fromSet)) // 3 + * + * // From string characters + * const fromString = MutableHashSet.fromIterable("hello") + * console.log(Array.from(fromString)) // ["h", "e", "l", "o"] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (keys: Iterable): MutableHashSet => + fromHashMap(MutableHashMap.fromIterable(Array.from(keys).map((k) => [k, true]))) + +/** + * Creates a MutableHashSet from a variable number of values. + * Duplicates are automatically removed. + * + * **When to use** + * + * Use to build a mutable hash set from explicit values. + * + * **Example** (Creating a set from values) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.make("apple", "banana", "apple", "cherry") + * + * console.log(MutableHashSet.size(set)) // 3 + * console.log(Array.from(set)) // ["apple", "banana", "cherry"] + * + * // With numbers + * const numbers = MutableHashSet.make(1, 2, 3, 2, 1) + * console.log(MutableHashSet.size(numbers)) // 3 + * console.log(Array.from(numbers)) // [1, 2, 3] + * + * // Mixed types + * const mixed = MutableHashSet.make("hello", 42, true, "hello") + * console.log(MutableHashSet.size(mixed)) // 3 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = >( + ...keys: Keys +): MutableHashSet => fromIterable(keys) + +/** + * Adds a value to the MutableHashSet, mutating the set in place. + * If the value already exists, the set remains unchanged. + * + * **When to use** + * + * Use to insert a value into a mutable set while keeping uniqueness. + * + * **Example** (Adding values) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.empty() + * + * // Add new values + * MutableHashSet.add(set, "apple") + * MutableHashSet.add(set, "banana") + * + * console.log(MutableHashSet.size(set)) // 2 + * console.log(MutableHashSet.has(set, "apple")) // true + * + * // Add duplicate (no effect) + * MutableHashSet.add(set, "apple") + * console.log(MutableHashSet.size(set)) // 2 + * + * // Pipe-able version + * const addFruit = MutableHashSet.add("cherry") + * addFruit(set) + * console.log(MutableHashSet.size(set)) // 3 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const add: { + (key: V): (self: MutableHashSet) => MutableHashSet + (self: MutableHashSet, key: V): MutableHashSet +} = Dual.dual< + (key: V) => (self: MutableHashSet) => MutableHashSet, + (self: MutableHashSet, key: V) => MutableHashSet +>(2, (self, key) => (MutableHashMap.set(self.keyMap, key, true), self)) + +/** + * Checks whether the MutableHashSet contains the specified value. + * + * **When to use** + * + * Use to test whether a mutable set currently contains a value. + * + * **Details** + * + * Membership follows the same hashing and equality rules as the underlying + * `MutableHashMap`. + * + * **Example** (Checking for a value) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.make("apple", "banana", "cherry") + * + * console.log(MutableHashSet.has(set, "apple")) // true + * console.log(MutableHashSet.has(set, "grape")) // false + * + * // Pipe-able version + * const hasApple = MutableHashSet.has("apple") + * console.log(hasApple(set)) // true + * + * // Check after adding + * MutableHashSet.add(set, "grape") + * console.log(MutableHashSet.has(set, "grape")) // true + * ``` + * + * @see {@link add} for adding a value to the set + * @see {@link remove} for removing a value from the set + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (key: V): (self: MutableHashSet) => boolean + (self: MutableHashSet, key: V): boolean +} = Dual.dual< + (key: V) => (self: MutableHashSet) => boolean, + (self: MutableHashSet, key: V) => boolean +>(2, (self, key) => MutableHashMap.has(self.keyMap, key)) + +/** + * Removes the specified value from the MutableHashSet, mutating the set in place. + * If the value doesn't exist, the set remains unchanged. + * + * **When to use** + * + * Use to delete a value from a mutable set if it is present. + * + * **Example** (Removing a value) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.make("apple", "banana", "cherry") + * + * console.log(MutableHashSet.size(set)) // 3 + * + * // Remove existing value + * MutableHashSet.remove(set, "banana") + * console.log(MutableHashSet.size(set)) // 2 + * console.log(MutableHashSet.has(set, "banana")) // false + * + * // Remove non-existent value (no effect) + * MutableHashSet.remove(set, "grape") + * console.log(MutableHashSet.size(set)) // 2 + * + * // Pipe-able version + * const removeFruit = MutableHashSet.remove("apple") + * removeFruit(set) + * console.log(MutableHashSet.size(set)) // 1 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (key: V): (self: MutableHashSet) => MutableHashSet + (self: MutableHashSet, key: V): MutableHashSet +} = Dual.dual< + (key: V) => (self: MutableHashSet) => MutableHashSet, + (self: MutableHashSet, key: V) => MutableHashSet +>(2, (self, key) => (MutableHashMap.remove(self.keyMap, key), self)) + +/** + * Returns the number of unique values in the MutableHashSet. + * + * **When to use** + * + * Use to read how many unique values are currently stored in the set. + * + * **Example** (Checking set size) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.empty() + * console.log(MutableHashSet.size(set)) // 0 + * + * MutableHashSet.add(set, "apple") + * MutableHashSet.add(set, "banana") + * MutableHashSet.add(set, "apple") // Duplicate + * console.log(MutableHashSet.size(set)) // 2 + * + * MutableHashSet.remove(set, "apple") + * console.log(MutableHashSet.size(set)) // 1 + * + * MutableHashSet.clear(set) + * console.log(MutableHashSet.size(set)) // 0 + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const size = (self: MutableHashSet): number => MutableHashMap.size(self.keyMap) + +/** + * Removes all values from the MutableHashSet, mutating the set in place. + * The set becomes empty after this operation. + * + * **When to use** + * + * Use to empty a mutable set while keeping the same set instance. + * + * **Example** (Clearing all values) + * + * ```ts + * import { MutableHashSet } from "effect" + * + * const set = MutableHashSet.make("apple", "banana", "cherry") + * + * console.log(MutableHashSet.size(set)) // 3 + * + * // Clear all values + * MutableHashSet.clear(set) + * + * console.log(MutableHashSet.size(set)) // 0 + * console.log(MutableHashSet.has(set, "apple")) // false + * console.log(Array.from(set)) // [] + * + * // Can still add new values after clearing + * MutableHashSet.add(set, "new") + * console.log(MutableHashSet.size(set)) // 1 + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const clear = (self: MutableHashSet): MutableHashSet => (MutableHashMap.clear(self.keyMap), self) diff --git a/repos/effect/packages/effect/src/MutableList.ts b/repos/effect/packages/effect/src/MutableList.ts new file mode 100644 index 0000000..7dbe56f --- /dev/null +++ b/repos/effect/packages/effect/src/MutableList.ts @@ -0,0 +1,967 @@ +/** + * Mutable lists for collecting ordered values and draining them from the front. + * A `MutableList` can append values to the end, prepend values to the + * beginning, take one or more values from the front, inspect its contents as an + * array, filter values, remove values, and clear itself. All operations update + * the same list object in place and keep its `length` field current. Taking + * from an empty list returns the `Empty` symbol. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" + +/** + * A mutable linked list data structure optimized for high-throughput operations. + * MutableList provides efficient append/prepend operations and is ideal for + * producer-consumer patterns, queues, and streaming scenarios. + * + * **Example** (Creating and consuming a mutable list) + * + * ```ts + * import { MutableList } from "effect" + * + * // Create a mutable list + * const list: MutableList.MutableList = MutableList.make() + * + * // Add elements + * MutableList.append(list, 1) + * MutableList.append(list, 2) + * MutableList.prepend(list, 0) + * + * // Access properties + * console.log(list.length) // 3 + * console.log(list.head?.array) // Contains elements from head bucket + * console.log(list.tail?.array) // Contains elements from tail bucket + * + * // Take elements + * console.log(MutableList.take(list)) // 0 + * console.log(MutableList.take(list)) // 1 + * console.log(MutableList.take(list)) // 2 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface MutableList { + head: MutableList.Bucket | undefined + tail: MutableList.Bucket | undefined + length: number +} + +/** + * The MutableList namespace contains type definitions and utilities for working + * with mutable linked lists. + * + * **Example** (Typing queue processors) + * + * ```ts + * import { MutableList } from "effect" + * + * // Type annotation using the namespace + * const processQueue = (queue: MutableList.MutableList) => { + * while (queue.length > 0) { + * const item = MutableList.take(queue) + * if (item !== MutableList.Empty) { + * console.log("Processing:", item) + * } + * } + * } + * + * // Using the namespace for type definitions + * const createProcessor = (): { + * queue: MutableList.MutableList + * add: (item: T) => void + * process: () => Array + * } => { + * const queue = MutableList.make() + * return { + * queue, + * add: (item) => MutableList.append(queue, item), + * process: () => MutableList.takeAll(queue) + * } + * } + * ``` + * + * @since 2.0.0 + */ +export declare namespace MutableList { + /** + * Storage node used by the exposed `head` and `tail` fields of a + * `MutableList`. + * + * **Details** + * + * Most code should treat buckets as an implementation detail and use + * `MutableList` operations such as `append`, `prepend`, and `take` instead + * of constructing or mutating buckets directly. + * + * **Example** (Inspecting buckets) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.append(list, 1) + * MutableList.append(list, 2) + * + * // Access bucket information (for debugging or advanced usage) + * const inspectBucket = ( + * bucket: MutableList.MutableList.Bucket | undefined + * ) => { + * if (bucket) { + * console.log("Bucket array:", bucket.array) + * console.log("Bucket offset:", bucket.offset) + * console.log("Bucket mutable:", bucket.mutable) + * console.log("Has next bucket:", bucket.next !== undefined) + * } + * } + * + * inspectBucket(list.head) + * inspectBucket(list.tail) + * ``` + * + * @category models + * @since 4.0.0 + */ + export interface Bucket { + readonly array: Array + mutable: boolean + offset: number + next: Bucket | undefined + } +} + +/** + * Defines the unique symbol used to represent an empty result when taking elements from a MutableList. + * This symbol is returned by `take` when the list is empty, allowing for safe type checking. + * + * **When to use** + * + * Use to detect that `take` returned no element before handling the result as a + * list item. + * + * **Example** (Checking for empty results) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * + * // Take from empty list returns Empty symbol + * const result = MutableList.take(list) + * console.log(result === MutableList.Empty) // true + * + * // Safe pattern for checking emptiness + * const processNext = (queue: MutableList.MutableList) => { + * const item = MutableList.take(queue) + * if (item === MutableList.Empty) { + * console.log("Queue is empty") + * return null + * } + * return item.toUpperCase() + * } + * + * // Compare with other empty results + * MutableList.append(list, "hello") + * const next = MutableList.take(list) + * console.log(next !== MutableList.Empty) // true, got "hello" + * + * const empty = MutableList.take(list) + * console.log(empty === MutableList.Empty) // true, list is empty + * ``` + * + * @category symbols + * @since 4.0.0 + */ +export const Empty: unique symbol = Symbol.for("effect/MutableList/Empty") + +/** + * The type of the Empty symbol, used for type checking when taking elements from a MutableList. + * This provides compile-time safety when checking for empty results. + * + * **Example** (Handling empty results type-safely) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * + * // Type-safe handling of empty results + * const takeAndDouble = ( + * queue: MutableList.MutableList + * ): number | null => { + * const item: number | MutableList.Empty = MutableList.take(queue) + * + * if (item === MutableList.Empty) { + * return null + * } + * + * // TypeScript knows item is number here + * return item * 2 + * } + * + * console.log(takeAndDouble(list)) // null (empty list) + * + * MutableList.append(list, 5) + * console.log(takeAndDouble(list)) // 10 + * + * // Type guard function + * const isEmpty = ( + * result: number | MutableList.Empty + * ): result is MutableList.Empty => { + * return result === MutableList.Empty + * } + * + * const value = MutableList.take(list) + * if (isEmpty(value)) { + * console.log("List is empty") + * } else { + * console.log("Got value:", value) + * } + * ``` + * + * @category symbols + * @since 4.0.0 + */ +export type Empty = typeof Empty + +/** + * Creates an empty MutableList. + * + * **Example** (Creating an empty mutable list) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * + * // Add elements + * MutableList.append(list, "first") + * MutableList.append(list, "second") + * MutableList.prepend(list, "beginning") + * + * console.log(list.length) // 3 + * + * // Take elements in FIFO order (from head) + * console.log(MutableList.take(list)) // "beginning" + * console.log(MutableList.take(list)) // "first" + * console.log(MutableList.take(list)) // "second" + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): MutableList => ({ + head: undefined, + tail: undefined, + length: 0 +}) + +const emptyBucket = (): MutableList.Bucket => ({ + array: [], + mutable: true, + offset: 0, + next: undefined +}) + +/** + * Appends an element to the end of the MutableList. + * This operation is optimized for high-frequency usage. + * + * **Example** (Appending elements) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * + * // Append elements one by one + * MutableList.append(list, 1) + * MutableList.append(list, 2) + * MutableList.append(list, 3) + * + * console.log(list.length) // 3 + * + * // Elements are taken from head (FIFO) + * console.log(MutableList.take(list)) // 1 + * console.log(MutableList.take(list)) // 2 + * console.log(MutableList.take(list)) // 3 + * + * // High-throughput usage + * for (let i = 0; i < 10000; i++) { + * MutableList.append(list, i) + * } + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const append = (self: MutableList, message: A): void => { + if (!self.tail) { + self.head = self.tail = emptyBucket() + } else if (!self.tail.mutable) { + self.tail.next = emptyBucket() + self.tail = self.tail.next + } + self.tail!.array.push(message) + self.length++ +} + +/** + * Prepends an element to the beginning of the MutableList. + * This operation is optimized for high-frequency usage. + * + * **Example** (Prepending elements) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * + * // Prepend elements (they'll be at the front) + * MutableList.prepend(list, "third") + * MutableList.prepend(list, "second") + * MutableList.prepend(list, "first") + * + * console.log(list.length) // 3 + * + * // Elements taken from head (most recently prepended first) + * console.log(MutableList.take(list)) // "first" + * console.log(MutableList.take(list)) // "second" + * console.log(MutableList.take(list)) // "third" + * + * // Use case: priority items or stack-like behavior + * MutableList.append(list, "normal") + * MutableList.prepend(list, "priority") // This will be taken first + * console.log(MutableList.take(list)) // "priority" + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const prepend = (self: MutableList, message: A): void => { + self.head = { + array: [message], + mutable: true, + offset: 0, + next: self.head + } + self.length++ +} + +/** + * Prepends all elements from an iterable to the beginning of the MutableList. + * The elements are added in order, so the first element in the iterable becomes + * the new head of the list. + * + * **Example** (Prepending multiple elements) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.append(list, 4) + * MutableList.append(list, 5) + * + * // Prepend multiple elements + * MutableList.prependAll(list, [1, 2, 3]) + * + * console.log(list.length) // 5 + * + * // Elements are taken in order: [1, 2, 3, 4, 5] + * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5] + * + * // Works with any iterable + * const newList = MutableList.make() + * MutableList.prependAll(newList, "hello") // Prepends each character + * console.log(MutableList.takeAll(newList)) // ["h", "e", "l", "l", "o"] + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const prependAll = (self: MutableList, messages: Iterable): void => + prependAllUnsafe(self, Arr.fromIterable(messages), !Array.isArray(messages)) + +/** + * Prepends all elements from a ReadonlyArray to the beginning of the MutableList. + * This is an optimized version that can reuse the array when mutable=true. + * + * **When to use** + * + * Use when prepending a trusted array directly is worth the optimized path and + * you control whether the input may be reused. + * + * **Gotchas** + * + * When mutable=true, the input array may be modified internally. Only use + * mutable=true when you control the array lifecycle. + * + * **Example** (Prepending arrays with optional mutation) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.append(list, 4) + * + * // Safe usage (default mutable=false) + * const items = [1, 2, 3] + * MutableList.prependAllUnsafe(list, items) + * console.log(items) // [1, 2, 3] - unchanged + * + * // Unsafe but efficient usage (mutable=true) + * const mutableItems = [10, 20, 30] + * MutableList.prependAllUnsafe(list, mutableItems, true) + * // mutableItems may be modified internally for efficiency + * + * console.log(MutableList.takeAll(list)) // [10, 20, 30, 1, 2, 3, 4] + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const prependAllUnsafe = (self: MutableList, messages: ReadonlyArray, mutable = false): void => { + self.head = { + array: messages as Array, + mutable, + offset: 0, + next: self.head + } + self.length += self.head.array.length +} + +/** + * Appends all elements from an iterable to the end of the MutableList. + * Returns the number of elements added. + * + * **Example** (Appending multiple elements) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.append(list, 1) + * MutableList.append(list, 2) + * + * // Append multiple elements + * const added = MutableList.appendAll(list, [3, 4, 5]) + * console.log(added) // 3 + * console.log(list.length) // 5 + * + * // Elements maintain order: [1, 2, 3, 4, 5] + * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5] + * + * // Works with any iterable + * const newList = MutableList.make() + * MutableList.appendAll(newList, new Set(["a", "b", "c"])) + * console.log(MutableList.takeAll(newList)) // ["a", "b", "c"] + * + * // Useful for bulk loading + * const bulkList = MutableList.make() + * const count = MutableList.appendAll( + * bulkList, + * Array.from({ length: 1000 }, (_, i) => i) + * ) + * console.log(count) // 1000 + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const appendAll = (self: MutableList, messages: Iterable): number => + appendAllUnsafe(self, Arr.fromIterable(messages), !Array.isArray(messages)) + +/** + * Appends all elements from a ReadonlyArray to the end of the MutableList. + * This is an optimized version that can reuse the array when mutable=true. + * Returns the number of elements added. + * + * **When to use** + * + * Use when appending a trusted array directly is worth the optimized path and + * you control whether the input may be reused. + * + * **Gotchas** + * + * When mutable=true, the input array may be modified internally. Only use + * mutable=true when you control the array lifecycle. + * + * **Example** (Appending arrays with optional mutation) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.append(list, 1) + * + * // Safe usage (default mutable=false) + * const items = [2, 3, 4] + * const added = MutableList.appendAllUnsafe(list, items) + * console.log(added) // 3 + * console.log(items) // [2, 3, 4] - unchanged + * + * // Unsafe but efficient usage (mutable=true) + * const mutableItems = [5, 6, 7] + * MutableList.appendAllUnsafe(list, mutableItems, true) + * // mutableItems may be modified internally for efficiency + * + * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5, 6, 7] + * + * // High-performance bulk operations + * const bigArray = new Array(10000).fill(0).map((_, i) => i) + * MutableList.appendAllUnsafe(list, bigArray, true) // Very efficient + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const appendAllUnsafe = (self: MutableList, messages: ReadonlyArray, mutable = false): number => { + if (messages.length === 0) { + return 0 + } + const chunk: MutableList.Bucket = { + array: messages as Array, + mutable, + offset: 0, + next: undefined + } + if (self.head) { + self.tail = self.tail!.next = chunk + } else { + self.head = self.tail = chunk + } + self.length += messages.length + return messages.length +} + +/** + * Removes all elements from the MutableList, resetting it to an empty state. + * This operation is highly optimized and releases all internal memory. + * + * **Example** (Clearing a mutable list) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, [1, 2, 3, 4, 5]) + * + * console.log(list.length) // 5 + * + * // Clear all elements + * MutableList.clear(list) + * + * console.log(list.length) // 0 + * console.log(MutableList.take(list)) // Empty + * + * // Can still use the list after clearing + * MutableList.append(list, 42) + * console.log(list.length) // 1 + * + * // Useful for resetting queues or buffers + * function resetBuffer(buffer: MutableList.MutableList) { + * MutableList.clear(buffer) + * console.log("Buffer cleared and ready for reuse") + * } + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const clear = (self: MutableList): void => { + self.head = self.tail = undefined + self.length = 0 +} + +/** + * Takes up to N elements from the beginning of the MutableList and returns them as an array. + * The taken elements are removed from the list. This operation is optimized for performance + * and includes zero-copy optimizations when possible. + * + * **Example** (Taking batches) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + * + * console.log(list.length) // 10 + * + * // Take first 3 elements + * const first3 = MutableList.takeN(list, 3) + * console.log(first3) // [1, 2, 3] + * console.log(list.length) // 7 + * + * // Take more than available + * const remaining = MutableList.takeN(list, 20) + * console.log(remaining) // [4, 5, 6, 7, 8, 9, 10] + * console.log(list.length) // 0 + * + * // Take from empty list + * const empty = MutableList.takeN(list, 5) + * console.log(empty) // [] + * + * // Batch processing pattern + * const queue = MutableList.make() + * MutableList.appendAll(queue, ["task1", "task2", "task3", "task4", "task5"]) + * + * while (queue.length > 0) { + * const batch = MutableList.takeN(queue, 2) // Process 2 at a time + * console.log("Processing batch:", batch) + * } + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const takeN = (self: MutableList, n: number): Array => { + if (n <= 0 || !self.head) return [] + n = Math.min(n, self.length) + if (n === self.length && self.head?.offset === 0 && !self.head.next) { + const array = self.head.array + clear(self) + return array + } + const array = new Array(n) + let index = 0 + let chunk: MutableList.Bucket | undefined = self.head + while (chunk) { + while (chunk.offset < chunk.array.length) { + array[index++] = chunk.array[chunk.offset] + if (chunk.mutable) chunk.array[chunk.offset] = undefined as any + chunk.offset++ + if (index === n) { + self.head = chunk + self.length -= n + if (self.length === 0) clear(self) + return array + } + } + chunk = chunk.next + } + clear(self) + return array +} + +/** + * Removes up to `n` elements from the beginning of the `MutableList` without + * returning them. + * + * **When to use** + * + * Use to discard a bounded number of values from the head of a `MutableList` + * when the removed values are not needed. + * + * **Details** + * + * If `n` is less than or equal to zero, or the list is empty, the list is left + * unchanged. If `n` is greater than or equal to the current length, the list is + * cleared. + * + * @see {@link takeN} for removing up to `n` values and returning them as an array + * @see {@link clear} for removing every value from the list + * + * @category elements + * @since 4.0.0 + */ +export const takeNVoid = (self: MutableList, n: number): void => { + if (n <= 0 || !self.head) return + n = Math.min(n, self.length) + if (n === self.length && self.head?.offset === 0 && !self.head.next) { + clear(self) + return + } + let count = 0 + let chunk: MutableList.Bucket | undefined = self.head + while (chunk) { + const size = chunk.array.length - chunk.offset + if (count + size > n) { + chunk.offset += n - count + self.head = chunk + self.length -= n + return + } + count += size + chunk = chunk.next + } + clear(self) + return +} + +/** + * Takes all elements from the MutableList and returns them as an array. + * The list becomes empty after this operation. This is equivalent to takeN(list, list.length). + * + * **Example** (Draining all elements) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, ["apple", "banana", "cherry"]) + * + * console.log(list.length) // 3 + * + * // Take all elements + * const allItems = MutableList.takeAll(list) + * console.log(allItems) // ["apple", "banana", "cherry"] + * console.log(list.length) // 0 + * + * // Useful for converting to array and clearing + * const queue = MutableList.make() + * MutableList.appendAll(queue, [1, 2, 3, 4, 5]) + * + * const snapshot = MutableList.takeAll(queue) + * console.log("Queue contents:", snapshot) + * console.log("Queue is now empty:", queue.length === 0) + * + * // Drain pattern for processing + * function drainAndProcess( + * list: MutableList.MutableList, + * processor: (items: Array) => void + * ) { + * if (list.length > 0) { + * const items = MutableList.takeAll(list) + * processor(items) + * } + * } + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const takeAll = (self: MutableList): Array => takeN(self, self.length) + +/** + * Takes a single element from the beginning of the MutableList. + * Returns the element if available, or the Empty symbol if the list is empty. + * The taken element is removed from the list. + * + * **Example** (Taking one element) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, ["first", "second", "third"]) + * + * // Take elements one by one + * console.log(MutableList.take(list)) // "first" + * console.log(list.length) // 2 + * + * console.log(MutableList.take(list)) // "second" + * console.log(MutableList.take(list)) // "third" + * console.log(list.length) // 0 + * + * // Take from empty list + * console.log(MutableList.take(list)) // Empty symbol + * + * // Check for empty using the Empty symbol + * const result = MutableList.take(list) + * if (result === MutableList.Empty) { + * console.log("List is empty") + * } else { + * console.log("Got element:", result) + * } + * + * // Consumer pattern + * function processNext( + * queue: MutableList.MutableList, + * processor: (item: T) => void + * ): boolean { + * const item = MutableList.take(queue) + * if (item !== MutableList.Empty) { + * processor(item) + * return true + * } + * return false + * } + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const take = (self: MutableList): Empty | A => { + if (!self.head) return Empty + const message = self.head.array[self.head.offset] + if (self.head.mutable) self.head.array[self.head.offset] = undefined as any + self.head.offset++ + self.length-- + if (self.head.offset === self.head.array.length) { + if (self.head.next) { + self.head = self.head.next + } else { + clear(self) + } + } + return message +} + +/** + * Copies up to `n` elements from the beginning of the `MutableList` into a new + * array without modifying the list. + * + * **When to use** + * + * Use when you need to inspect or snapshot a bounded prefix of the list without + * consuming it. + * + * @see {@link takeN} for removing up to `n` values and returning them as an array + * + * @category elements + * @since 4.0.0 + */ +export const toArrayN = (self: MutableList, n: number): Array => { + const length = Math.min(n, self.length) + const out = new Array(length) + let index = 0 + let bucket = self.head + while (bucket) { + for (let i = bucket.offset; i < bucket.array.length; i++) { + out[index++] = bucket.array[i] + if (index === length) return out + } + bucket = bucket.next + } + return out +} + +/** + * Copies all current elements of the `MutableList` into a new array without + * modifying the list. + * + * **When to use** + * + * Use when you need a snapshot of all current elements while keeping the list + * unchanged. + * + * @see {@link takeAll} for converting all elements to an array and clearing the list + * + * @category elements + * @since 4.0.0 + */ +export const toArray = (self: MutableList): Array => toArrayN(self, self.length) + +/** + * Filters the MutableList in place, keeping only elements that satisfy the predicate. + * This operation modifies the list and rebuilds its internal structure for efficiency. + * + * **Example** (Filtering in place) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + * + * console.log(list.length) // 10 + * + * // Keep only even numbers + * MutableList.filter(list, (n) => n % 2 === 0) + * + * console.log(MutableList.takeAll(list)) // [2, 4, 6, 8, 10] + * + * // Filter with index + * const indexed = MutableList.make() + * MutableList.appendAll(indexed, ["a", "b", "c", "d", "e"]) + * + * // Keep elements at even indices + * MutableList.filter(indexed, (value, index) => index % 2 === 0) + * console.log(MutableList.takeAll(indexed)) // ["a", "c", "e"] + * + * // Real-world example: filtering a log queue + * const logs = MutableList.make<{ level: string; message: string }>() + * MutableList.appendAll(logs, [ + * { level: "INFO", message: "App started" }, + * { level: "ERROR", message: "Connection failed" }, + * { level: "DEBUG", message: "Cache hit" }, + * { level: "ERROR", message: "Timeout" } + * ]) + * + * // Keep only errors + * MutableList.filter(logs, (log) => log.level === "ERROR") + * console.log(MutableList.takeAll(logs).map((log) => log.message)) // ["Connection failed", "Timeout"] + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const filter = (self: MutableList, f: (value: A, i: number) => boolean): void => { + const array: Array = [] + let chunk: MutableList.Bucket | undefined = self.head + while (chunk) { + for (let i = chunk.offset; i < chunk.array.length; i++) { + if (f(chunk.array[i], i)) { + array.push(chunk.array[i]) + } + } + chunk = chunk.next + } + self.head = self.tail = { + array, + mutable: true, + offset: 0, + next: undefined + } + self.length = array.length +} + +/** + * Removes all occurrences of a value from the `MutableList` using JavaScript + * strict equality semantics. + * + * **When to use** + * + * Use when in-place removal should use JavaScript identity/strict equality + * rather than Effect structural equality. + * + * **Details** + * + * The list is modified in place. + * + * **Gotchas** + * + * Values are compared with `!==`, so this does not use Effect structural + * equality. + * + * **Example** (Removing matching values) + * + * ```ts + * import { MutableList } from "effect" + * + * const list = MutableList.make() + * MutableList.appendAll(list, ["apple", "banana", "apple", "cherry", "apple"]) + * + * console.log(list.length) // 5 + * + * // Remove all occurrences of "apple" + * MutableList.remove(list, "apple") + * + * console.log(MutableList.takeAll(list)) // ["banana", "cherry"] + * + * // Remove non-existent value (no effect) + * const colors = MutableList.make() + * MutableList.appendAll(colors, ["red", "blue"]) + * MutableList.remove(colors, "green") + * console.log(MutableList.takeAll(colors)) // ["red", "blue"] + * + * // Real-world example: removing completed tasks + * const tasks = MutableList.make<{ id: number; status: string }>() + * MutableList.appendAll(tasks, [ + * { id: 1, status: "pending" }, + * { id: 2, status: "completed" }, + * { id: 3, status: "pending" }, + * { id: 4, status: "completed" } + * ]) + * + * // Remove completed tasks by filtering status + * MutableList.filter(tasks, (task) => task.status !== "completed") + * console.log(MutableList.takeAll(tasks).map((task) => task.id)) // [1, 3] + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const remove = (self: MutableList, value: A): void => filter(self, (v) => v !== value) diff --git a/repos/effect/packages/effect/src/MutableRef.ts b/repos/effect/packages/effect/src/MutableRef.ts new file mode 100644 index 0000000..b8a042e --- /dev/null +++ b/repos/effect/packages/effect/src/MutableRef.ts @@ -0,0 +1,822 @@ +/** + * Stores synchronous mutable state in a small reference object. + * + * A `MutableRef` stores one current value and exposes it through `.current`. + * Unlike `Ref`, its operations are synchronous and update the same object in + * place. This module includes pipeable helpers for reading, setting, comparing, + * and updating the value, plus numeric increment/decrement helpers and a + * boolean toggle helper. + * + * @since 2.0.0 + */ +import * as Equal from "./Equal.ts" +import * as Dual from "./Function.ts" +import { type Inspectable, toJson } from "./Inspectable.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import type { Pipeable } from "./Pipeable.ts" + +const TypeId = "~effect/MutableRef" + +/** + * A synchronous mutable reference that stores a current value. + * + * **When to use** + * + * Use to keep local mutable state in a stable, pipeable reference. + * + * **Details** + * + * Read or write the value directly through `.current`, or use the `MutableRef` + * helpers for pipeable updates such as `get`, `set`, `update`, and + * `compareAndSet`. All operations mutate the same reference in place. + * + * **Example** (Creating and updating refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * // Create a mutable reference + * const ref: MutableRef.MutableRef = MutableRef.make(42) + * + * // Read the current value + * console.log(ref.current) // 42 + * console.log(MutableRef.get(ref)) // 42 + * + * // Update the value + * ref.current = 100 + * console.log(MutableRef.get(ref)) // 100 + * + * // Use with complex types + * interface Config { + * timeout: number + * retries: number + * } + * + * const config: MutableRef.MutableRef = MutableRef.make({ + * timeout: 5000, + * retries: 3 + * }) + * + * // Update through the interface + * config.current = { timeout: 10000, retries: 5 } + * console.log(config.current.timeout) // 10000 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface MutableRef extends Pipeable, Inspectable { + readonly [TypeId]: typeof TypeId + current: T +} + +const MutableRefProto: Omit, "current"> = { + [TypeId]: TypeId, + ...PipeInspectableProto, + toJSON(this: MutableRef) { + return { + _id: "MutableRef", + current: toJson(this.current) + } + } +} + +/** + * Creates a new MutableRef with the specified initial value. + * + * **When to use** + * + * Use to create a synchronous `MutableRef` initialized with a value. + * + * **Example** (Creating mutable refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * // Create a counter reference + * const counter = MutableRef.make(0) + * console.log(MutableRef.get(counter)) // 0 + * + * // Create a configuration reference + * const config = MutableRef.make({ debug: false, timeout: 5000 }) + * console.log(MutableRef.get(config)) // { debug: false, timeout: 5000 } + * + * // Create a string reference + * const status = MutableRef.make("idle") + * MutableRef.set(status, "running") + * console.log(MutableRef.get(status)) // "running" + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (value: T): MutableRef => { + const ref = Object.create(MutableRefProto) + ref.current = value + return ref +} + +/** + * Sets the value to newValue atomically if the current value equals oldValue. + * Returns true if the value was updated, false otherwise. + * Uses Effect's Equal interface for value comparison. + * + * **When to use** + * + * Use to replace a `MutableRef` value only when the current value still matches + * an expected value. + * + * **Example** (Comparing and setting values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const ref = MutableRef.make("initial") + * + * // Successful compare and set + * const updated = MutableRef.compareAndSet(ref, "initial", "updated") + * console.log(updated) // true + * console.log(MutableRef.get(ref)) // "updated" + * + * // Failed compare and set (value doesn't match) + * const failed = MutableRef.compareAndSet(ref, "initial", "failed") + * console.log(failed) // false + * console.log(MutableRef.get(ref)) // "updated" (unchanged) + * + * // Thread-safe counter increment + * const counter = MutableRef.make(5) + * let current: number + * do { + * current = MutableRef.get(counter) + * } while (!MutableRef.compareAndSet(counter, current, current + 1)) + * + * // Pipe-able version + * const casUpdate = MutableRef.compareAndSet("updated", "final") + * console.log(casUpdate(ref)) // true + * ``` + * + * @category general + * @since 2.0.0 + */ +export const compareAndSet: { + (oldValue: T, newValue: T): (self: MutableRef) => boolean + (self: MutableRef, oldValue: T, newValue: T): boolean +} = Dual.dual< + (oldValue: T, newValue: T) => (self: MutableRef) => boolean, + (self: MutableRef, oldValue: T, newValue: T) => boolean +>(3, (self, oldValue, newValue) => { + if (Equal.equals(oldValue, self.current)) { + self.current = newValue + return true + } + return false +}) + +/** + * Decrements a numeric MutableRef by 1 and returns the reference. + * + * **When to use** + * + * Use when you need an in-place `MutableRef` decrement that returns the same + * `MutableRef`. + * + * **Example** (Decrementing numeric refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Decrement the counter + * MutableRef.decrement(counter) + * console.log(MutableRef.get(counter)) // 4 + * + * // Chain operations + * MutableRef.decrement(counter) + * MutableRef.decrement(counter) + * console.log(MutableRef.get(counter)) // 2 + * + * // Useful for countdown scenarios + * const countdown = MutableRef.make(10) + * while (MutableRef.get(countdown) > 0) { + * console.log(MutableRef.get(countdown)) + * MutableRef.decrement(countdown) + * } + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const decrement = (self: MutableRef): MutableRef => update(self, (n) => n - 1) + +/** + * Decrements a numeric MutableRef by 1 and returns the new value. + * + * **When to use** + * + * Use to decrement a numeric `MutableRef` and immediately read the updated + * value. + * + * **Example** (Decrementing and reading refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Decrement and get the new value + * const newValue = MutableRef.decrementAndGet(counter) + * console.log(newValue) // 4 + * console.log(MutableRef.get(counter)) // 4 + * + * // Use in expressions + * const lives = MutableRef.make(3) + * console.log(`Lives remaining: ${MutableRef.decrementAndGet(lives)}`) // "Lives remaining: 2" + * + * // Conditional logic based on decremented value + * const attempts = MutableRef.make(3) + * while (MutableRef.decrementAndGet(attempts) >= 0) { + * console.log("Retrying...") + * // retry logic + * } + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const decrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n - 1) + +/** + * Gets the current value of the MutableRef. + * + * **When to use** + * + * Use to read the current `MutableRef` value without mutating it. + * + * **Example** (Reading current values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const ref = MutableRef.make("hello") + * console.log(MutableRef.get(ref)) // "hello" + * + * MutableRef.set(ref, "world") + * console.log(MutableRef.get(ref)) // "world" + * + * // Reading complex objects + * const config = MutableRef.make({ port: 3000, host: "localhost" }) + * const currentConfig = MutableRef.get(config) + * console.log(currentConfig.port) // 3000 + * + * // Multiple reads return the same value + * const value1 = MutableRef.get(ref) + * const value2 = MutableRef.get(ref) + * console.log(value1 === value2) // true + * ``` + * + * @category general + * @since 2.0.0 + */ +export const get = (self: MutableRef): T => self.current + +/** + * Decrements a numeric MutableRef by 1 and returns the previous value. + * + * **When to use** + * + * Use to read the current numeric `MutableRef` value before decrementing it. + * + * **Example** (Reading before decrementing) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Get current value and then decrement + * const previousValue = MutableRef.getAndDecrement(counter) + * console.log(previousValue) // 5 + * console.log(MutableRef.get(counter)) // 4 + * + * // Useful for processing where you need the original value + * const itemsLeft = MutableRef.make(10) + * while (MutableRef.get(itemsLeft) > 0) { + * const currentItem = MutableRef.getAndDecrement(itemsLeft) + * console.log(`Processing item ${currentItem}`) + * } + * + * // Post-decrement semantics (like i-- in other languages) + * const index = MutableRef.make(3) + * const currentIndex = MutableRef.getAndDecrement(index) + * console.log(`Current: ${currentIndex}, Next: ${MutableRef.get(index)}`) // "Current: 3, Next: 2" + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const getAndDecrement = (self: MutableRef): number => getAndUpdate(self, (n) => n - 1) + +/** + * Increments a numeric MutableRef by 1 and returns the previous value. + * + * **When to use** + * + * Use to read the current numeric `MutableRef` value before incrementing it. + * + * **Example** (Reading before incrementing) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Get current value and then increment + * const previousValue = MutableRef.getAndIncrement(counter) + * console.log(previousValue) // 5 + * console.log(MutableRef.get(counter)) // 6 + * + * // Useful for ID generation + * const idGenerator = MutableRef.make(0) + * const getId = () => MutableRef.getAndIncrement(idGenerator) + * + * console.log(getId()) // 0 + * console.log(getId()) // 1 + * console.log(getId()) // 2 + * + * // Post-increment semantics (like i++ in other languages) + * const position = MutableRef.make(0) + * const currentPos = MutableRef.getAndIncrement(position) + * console.log(`Was at: ${currentPos}, Now at: ${MutableRef.get(position)}`) // "Was at: 0, Now at: 1" + * + * // Useful for iteration counters + * const iterations = MutableRef.make(0) + * while (MutableRef.get(iterations) < 5) { + * const iteration = MutableRef.getAndIncrement(iterations) + * console.log(`Iteration ${iteration}`) + * } + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const getAndIncrement = (self: MutableRef): number => getAndUpdate(self, (n) => n + 1) + +/** + * Sets the MutableRef to a new value and returns the previous value. + * + * **When to use** + * + * Use to replace the current `MutableRef` value while keeping the previous + * value. + * + * **Example** (Reading before setting) + * + * ```ts + * import { MutableRef } from "effect" + * + * const ref = MutableRef.make("old") + * + * // Set new value and get the previous one + * const previous = MutableRef.getAndSet(ref, "new") + * console.log(previous) // "old" + * console.log(MutableRef.get(ref)) // "new" + * + * // Swapping values + * const counter = MutableRef.make(5) + * const oldValue = MutableRef.getAndSet(counter, 10) + * console.log(`Changed from ${oldValue} to ${MutableRef.get(counter)}`) // "Changed from 5 to 10" + * + * // Pipe-able version + * const setValue = MutableRef.getAndSet("final") + * const previousValue = setValue(ref) + * console.log(previousValue) // "new" + * + * // Useful for atomic swaps in algorithms + * const buffer = MutableRef.make>(["a", "b", "c"]) + * const oldBuffer = MutableRef.getAndSet(buffer, []) + * console.log(oldBuffer) // ["a", "b", "c"] + * console.log(MutableRef.get(buffer)) // [] + * ``` + * + * @category general + * @since 2.0.0 + */ +export const getAndSet: { + (value: T): (self: MutableRef) => T + (self: MutableRef, value: T): T +} = Dual.dual< + (value: T) => (self: MutableRef) => T, + (self: MutableRef, value: T) => T +>(2, (self, value) => { + const ret = self.current + self.current = value + return ret +}) + +/** + * Updates the MutableRef with the result of applying a function to its current value, + * and returns the previous value. + * + * **When to use** + * + * Use to transform the current `MutableRef` value while keeping the previous + * value. + * + * **Example** (Reading before updating) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Increment and get the old value + * const oldValue = MutableRef.getAndUpdate(counter, (n) => n + 1) + * console.log(oldValue) // 5 + * console.log(MutableRef.get(counter)) // 6 + * + * // Double the value and get the previous one + * const previous = MutableRef.getAndUpdate(counter, (n) => n * 2) + * console.log(previous) // 6 + * console.log(MutableRef.get(counter)) // 12 + * + * // Transform string and get old value + * const message = MutableRef.make("hello") + * const oldMessage = MutableRef.getAndUpdate(message, (s) => s.toUpperCase()) + * console.log(oldMessage) // "hello" + * console.log(MutableRef.get(message)) // "HELLO" + * + * // Pipe-able version + * const addOne = MutableRef.getAndUpdate((n: number) => n + 1) + * const result = addOne(counter) + * console.log(result) // Previous value before increment + * + * // Useful for implementing atomic operations + * const list = MutableRef.make>([1, 2, 3]) + * const oldList = MutableRef.getAndUpdate(list, (arr) => [...arr, 4]) + * console.log(oldList) // [1, 2, 3] + * console.log(MutableRef.get(list)) // [1, 2, 3, 4] + * ``` + * + * @category general + * @since 2.0.0 + */ +export const getAndUpdate: { + (f: (value: T) => T): (self: MutableRef) => T + (self: MutableRef, f: (value: T) => T): T +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => T, + (self: MutableRef, f: (value: T) => T) => T +>(2, (self, f) => getAndSet(self, f(get(self)))) + +/** + * Increments a numeric MutableRef by 1 and returns the reference. + * + * **When to use** + * + * Use when you need an in-place `MutableRef` increment that returns the same + * `MutableRef`. + * + * **Example** (Incrementing numeric refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Increment the counter + * MutableRef.increment(counter) + * console.log(MutableRef.get(counter)) // 6 + * + * // Chain operations + * MutableRef.increment(counter) + * MutableRef.increment(counter) + * console.log(MutableRef.get(counter)) // 8 + * + * // Useful for simple counting + * const visits = MutableRef.make(0) + * MutableRef.increment(visits) // User visited + * MutableRef.increment(visits) // Another visit + * console.log(MutableRef.get(visits)) // 2 + * + * // Returns the reference for chaining + * const result = MutableRef.increment(counter) + * console.log(result === counter) // true + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const increment = (self: MutableRef): MutableRef => update(self, (n) => n + 1) + +/** + * Increments a numeric MutableRef by 1 and returns the new value. + * + * **When to use** + * + * Use to increment a numeric `MutableRef` and immediately read the updated + * value. + * + * **Example** (Incrementing and reading refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Increment and get the new value + * const newValue = MutableRef.incrementAndGet(counter) + * console.log(newValue) // 6 + * console.log(MutableRef.get(counter)) // 6 + * + * // Use in expressions + * const score = MutableRef.make(100) + * console.log(`New score: ${MutableRef.incrementAndGet(score)}`) // "New score: 101" + * + * // Pre-increment semantics (like ++i in other languages) + * const level = MutableRef.make(0) + * const nextLevel = MutableRef.incrementAndGet(level) + * console.log(`Reached level ${nextLevel}`) // "Reached level 1" + * + * // Conditional logic based on incremented value + * const attempts = MutableRef.make(0) + * if (MutableRef.incrementAndGet(attempts) > 3) { + * console.log("Too many attempts") + * } + * ``` + * + * @category numeric + * @since 2.0.0 + */ +export const incrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n + 1) + +/** + * Sets the MutableRef to a new value and returns the reference. + * + * **When to use** + * + * Use when you need an in-place `MutableRef` replacement that returns the same + * `MutableRef`. + * + * **Example** (Setting values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const ref = MutableRef.make("initial") + * + * // Set a new value + * MutableRef.set(ref, "updated") + * console.log(MutableRef.get(ref)) // "updated" + * + * // Chain set operations (since it returns the ref) + * const result = MutableRef.set(ref, "final") + * console.log(result === ref) // true (same reference) + * console.log(MutableRef.get(ref)) // "final" + * + * // Set complex objects + * const config = MutableRef.make({ debug: false, verbose: false }) + * MutableRef.set(config, { debug: true, verbose: true }) + * console.log(MutableRef.get(config)) // { debug: true, verbose: true } + * + * // Pipe-able version + * const setValue = MutableRef.set("new value") + * setValue(ref) + * console.log(MutableRef.get(ref)) // "new value" + * + * // Useful for state management + * const state = MutableRef.make<"idle" | "loading" | "success" | "error">("idle") + * MutableRef.set(state, "loading") + * // ... perform async operation + * MutableRef.set(state, "success") + * ``` + * + * @category general + * @since 2.0.0 + */ +export const set: { + (value: T): (self: MutableRef) => MutableRef + (self: MutableRef, value: T): MutableRef +} = Dual.dual< + (value: T) => (self: MutableRef) => MutableRef, + (self: MutableRef, value: T) => MutableRef +>(2, (self, value) => { + self.current = value + return self +}) + +/** + * Sets the MutableRef to a new value and returns the new value. + * + * **When to use** + * + * Use to replace the current `MutableRef` value and immediately read the + * replacement. + * + * **Example** (Setting and reading values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const ref = MutableRef.make("old") + * + * // Set and get the new value + * const newValue = MutableRef.setAndGet(ref, "new") + * console.log(newValue) // "new" + * console.log(MutableRef.get(ref)) // "new" + * + * // Useful for assignments that need the value + * const counter = MutableRef.make(0) + * const currentValue = MutableRef.setAndGet(counter, 42) + * console.log(`Counter set to: ${currentValue}`) // "Counter set to: 42" + * + * // Pipe-able version + * const setValue = MutableRef.setAndGet("final") + * const result = setValue(ref) + * console.log(result) // "final" + * + * // Difference from set: returns value instead of reference + * const ref1 = MutableRef.make(1) + * const returnedRef = MutableRef.set(ref1, 2) // Returns MutableRef + * const returnedValue = MutableRef.setAndGet(ref1, 3) // Returns value + * console.log(returnedValue) // 3 + * ``` + * + * @category general + * @since 2.0.0 + */ +export const setAndGet: { + (value: T): (self: MutableRef) => T + (self: MutableRef, value: T): T +} = Dual.dual< + (value: T) => (self: MutableRef) => T, + (self: MutableRef, value: T) => T +>(2, (self, value) => { + self.current = value + return self.current +}) + +/** + * Updates the MutableRef with the result of applying a function to its current value, + * and returns the reference. + * + * **When to use** + * + * Use when you need an in-place `MutableRef` value transformation that returns + * the same `MutableRef`. + * + * **Example** (Updating values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Increment the counter + * MutableRef.update(counter, (n) => n + 1) + * console.log(MutableRef.get(counter)) // 6 + * + * // Chain updates (since it returns the ref) + * const result = MutableRef.update(counter, (n) => n * 2) + * console.log(result === counter) // true (same reference) + * console.log(MutableRef.get(counter)) // 12 + * + * // Transform string + * const message = MutableRef.make("hello") + * MutableRef.update(message, (s) => s.toUpperCase()) + * console.log(MutableRef.get(message)) // "HELLO" + * + * // Update complex objects + * const user = MutableRef.make({ name: "Alice", age: 30 }) + * MutableRef.update(user, (u) => ({ ...u, age: u.age + 1 })) + * console.log(MutableRef.get(user)) // { name: "Alice", age: 31 } + * + * // Pipe-able version + * const double = MutableRef.update((n: number) => n * 2) + * double(counter) + * console.log(MutableRef.get(counter)) // 24 + * + * // Array operations + * const list = MutableRef.make>([1, 2, 3]) + * MutableRef.update(list, (arr) => [...arr, 4]) + * console.log(MutableRef.get(list)) // [1, 2, 3, 4] + * ``` + * + * @category general + * @since 2.0.0 + */ +export const update: { + (f: (value: T) => T): (self: MutableRef) => MutableRef + (self: MutableRef, f: (value: T) => T): MutableRef +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => MutableRef, + (self: MutableRef, f: (value: T) => T) => MutableRef +>(2, (self, f) => set(self, f(get(self)))) + +/** + * Updates the MutableRef with the result of applying a function to its current value, + * and returns the new value. + * + * **When to use** + * + * Use to transform the current `MutableRef` value and immediately read the + * updated value. + * + * **Example** (Updating and reading values) + * + * ```ts + * import { MutableRef } from "effect" + * + * const counter = MutableRef.make(5) + * + * // Increment and get the new value + * const newValue = MutableRef.updateAndGet(counter, (n) => n + 1) + * console.log(newValue) // 6 + * console.log(MutableRef.get(counter)) // 6 + * + * // Double the value and get the result + * const doubled = MutableRef.updateAndGet(counter, (n) => n * 2) + * console.log(doubled) // 12 + * + * // Transform string and get result + * const message = MutableRef.make("hello") + * const upperCase = MutableRef.updateAndGet(message, (s) => s.toUpperCase()) + * console.log(upperCase) // "HELLO" + * + * // Pipe-able version + * const increment = MutableRef.updateAndGet((n: number) => n + 1) + * const result = increment(counter) + * console.log(result) // 13 (new value) + * + * // Useful for calculations that need the result + * const score = MutableRef.make(100) + * const bonus = 50 + * const newScore = MutableRef.updateAndGet(score, (s) => s + bonus) + * console.log(`New score: ${newScore}`) // "New score: 150" + * + * // Array transformations + * const list = MutableRef.make>([1, 2, 3]) + * const newList = MutableRef.updateAndGet(list, (arr) => arr.map((x) => x * 2)) + * console.log(newList) // [2, 4, 6] + * console.log(MutableRef.get(list)) // [2, 4, 6] + * ``` + * + * @category general + * @since 2.0.0 + */ +export const updateAndGet: { + (f: (value: T) => T): (self: MutableRef) => T + (self: MutableRef, f: (value: T) => T): T +} = Dual.dual< + (f: (value: T) => T) => (self: MutableRef) => T, + (self: MutableRef, f: (value: T) => T) => T +>(2, (self, f) => setAndGet(self, f(get(self)))) + +/** + * Switches a boolean `MutableRef` between `true` and `false`, then returns the + * reference. + * + * **When to use** + * + * Use when you need an in-place boolean `MutableRef` toggle that returns the + * same `MutableRef`. + * + * **Example** (Toggling boolean refs) + * + * ```ts + * import { MutableRef } from "effect" + * + * const flag = MutableRef.make(false) + * + * // Toggle the flag + * MutableRef.toggle(flag) + * console.log(MutableRef.get(flag)) // true + * + * // Toggle again + * MutableRef.toggle(flag) + * console.log(MutableRef.get(flag)) // false + * + * // Useful for state switches + * const isVisible = MutableRef.make(true) + * MutableRef.toggle(isVisible) // Hide + * console.log(MutableRef.get(isVisible)) // false + * + * // Toggle button implementation + * const darkMode = MutableRef.make(false) + * const toggleDarkMode = () => { + * MutableRef.toggle(darkMode) + * console.log(`Dark mode: ${MutableRef.get(darkMode) ? "ON" : "OFF"}`) + * } + * + * toggleDarkMode() // "Dark mode: ON" + * toggleDarkMode() // "Dark mode: OFF" + * + * // Returns the reference for chaining + * const result = MutableRef.toggle(flag) + * console.log(result === flag) // true + * ``` + * + * @category boolean + * @since 2.0.0 + */ +export const toggle = (self: MutableRef): MutableRef => update(self, (_) => !_) diff --git a/repos/effect/packages/effect/src/Newtype.ts b/repos/effect/packages/effect/src/Newtype.ts new file mode 100644 index 0000000..ea64786 --- /dev/null +++ b/repos/effect/packages/effect/src/Newtype.ts @@ -0,0 +1,319 @@ +/** + * Creates compile-time-only wrappers around existing value types. + * + * A newtype lets TypeScript distinguish values with the same runtime shape, such + * as two different ids that are both strings. The tag exists only in the type + * system, so wrapping does not allocate a runtime object. This module includes + * the base `Newtype` interface, wrapping and unwrapping helpers, optics, and + * helpers for reusing carrier instances such as `Equivalence`, `Order`, + * `Combiner`, and `Reducer`. + * + * @since 4.0.0 + */ +import type * as Combiner from "./Combiner.ts" +import type * as Equivalence from "./Equivalence.ts" +import { cast } from "./Function.ts" +import * as Optic from "./Optic.ts" +import type * as Order from "./Order.ts" +import type * as Reducer from "./Reducer.ts" + +const TypeId = "~effect/Newtype" + +/** + * A tagged interface that wraps a carrier type under a unique key, preventing + * accidental interchange of structurally identical values. + * + * **When to use** + * + * Use to define a newtype as an `interface` extending + * `Newtype<"MyKey", CarrierType>` when structurally identical carrier types + * should remain distinct in TypeScript. + * + * **Details** + * + * The tag is compile-time only, so no runtime wrapper is allocated. Use + * {@link makeIso} to create a two-way conversion, or {@link value} to unwrap. + * + * **Example** (Defining a newtype) + * + * ```ts + * import { Newtype } from "effect" + * + * interface UserId extends Newtype.Newtype<"UserId", number> {} + * interface OrderId extends Newtype.Newtype<"OrderId", number> {} + * + * // UserId and OrderId are not assignable to each other + * // even though both wrap `number`. + * ``` + * + * @see {@link makeIso} — create an iso to wrap and unwrap + * @see {@link value} — unwrap a newtype value + * + * @category models + * @since 4.0.0 + */ +export interface Newtype { + readonly [TypeId]: { + readonly key: Key + readonly carrier: Carrier + } +} + +/** + * Namespace containing type-level helpers for `Newtype` values, including + * constraints and utilities for extracting a newtype's key and carrier type. + * + * **When to use** + * + * Use to access generic constraints and type-level utilities for `Newtype` + * values. + * + * @since 4.0.0 + */ +export declare namespace Newtype { + /** + * A type that matches any `Newtype`, useful as a generic constraint: + * ``. + * + * **When to use** + * + * Use as a generic constraint when a type parameter can be any `Newtype`. + * + * @see {@link Newtype} — the base tagged interface + * + * @category utility types + * @since 4.0.0 + */ + export type Any = Newtype + + /** + * Extracts the key literal type from a newtype. + * + * **When to use** + * + * Use to inspect or constrain a newtype's key in generic code. + * + * @category utility types + * @since 4.0.0 + */ + export type Key = N extends Newtype ? Key : never + + /** + * Extracts the carrier (underlying) type from a newtype. + * + * **When to use** + * + * Use when you need to refer to the wrapped type in generic utilities. + * + * @category utility types + * @since 4.0.0 + */ + export type Carrier = N extends Newtype ? Carrier : never +} + +/** + * Unwraps a newtype value, returning the underlying carrier value. + * + * **When to use** + * + * Use when you need the carrier value from an existing newtype without + * constructing a new newtype value at the same call site. + * + * **Details** + * + * This has zero runtime cost because it is an identity cast. + * + * **Example** (Unwrapping a newtype) + * + * ```ts + * import { Newtype } from "effect" + * + * interface Label extends Newtype.Newtype<"Label", string> {} + * + * const iso = Newtype.makeIso` can be consumed anywhere an `Iterable` is expected, + * while also carrying the guarantee that reading the first element is safe. + * + * @since 2.0.0 + */ + +/** + * Defines the type-level symbol used to brand the `NonEmptyIterable` type. + * + * **When to use** + * + * Use as the property key for the type-level brand that marks an `Iterable` as + * non-empty. + * + * **Details** + * + * `NonEmptyIterable` includes `readonly [nonEmpty]: A`, which makes it + * distinct from a plain `Iterable` at compile time while preserving the + * normal iteration shape. + * + * @see {@link NonEmptyIterable} for the branded iterable type that uses this symbol + * + * @category symbols + * @since 2.0.0 + */ +export declare const nonEmpty: unique symbol + +/** + * Represents an iterable that is guaranteed to contain at least one element. + * + * **When to use** + * + * Use to require an iterable input that must provide at least one element. + * + * **Details** + * + * `NonEmptyIterable` extends the standard `Iterable` interface with a type-level + * guarantee of non-emptiness. This allows for safe operations that would otherwise + * require runtime checks or could throw exceptions. + * + * The type is branded with a unique symbol to ensure type safety while maintaining + * full compatibility with JavaScript's iteration protocol. + * + * **Example** (Working with non-empty iterables) + * + * ```ts + * import { Array, Chunk, NonEmptyIterable } from "effect" + * + * // Function that requires non-empty data + * function getFirst(data: NonEmptyIterable.NonEmptyIterable): A { + * // Safe - guaranteed to have at least one element + * const [first] = NonEmptyIterable.unprepend(data) + * return first + * } + * + * // Works with any non-empty iterable + * const numbers = Array.make( + * 1, + * 2, + * 3 + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * const firstNumber = getFirst(numbers) // 1 + * + * const chars = "hello" as unknown as NonEmptyIterable.NonEmptyIterable + * const firstChar = getFirst(chars) // "h" + * + * const entries = new Map([["a", 1], [ + * "b", + * 2 + * ]]) as unknown as NonEmptyIterable.NonEmptyIterable<[string, number]> + * const firstEntry = getFirst(entries) // ["a", 1] + * + * // Custom generator + * function* countdown(): Generator { + * yield 3 + * yield 2 + * yield 1 + * } + * const firstCount = getFirst( + * Chunk.fromIterable( + * countdown() + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * ) // 3 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface NonEmptyIterable extends Iterable { + readonly [nonEmpty]: A +} + +/** + * Extracts the first element and remaining elements from a non-empty iterable safely. + * + * **When to use** + * + * Use to split a non-empty iterable into its first element and an iterator for + * the remaining elements. + * + * **Details** + * + * This function provides a safe way to deconstruct a `NonEmptyIterable` into its + * head (first element) and tail (remaining elements as an iterator). Since the + * iterable is guaranteed to be non-empty, the first element is always available. + * + * **Example** (Extracting first and remaining elements) + * + * ```ts + * import { Array, Chunk, NonEmptyIterable } from "effect" + * + * // Helper to make iterator iterable for Array.from + * const iteratorToIterable = (iterator: Iterator): Iterable => ({ + * [Symbol.iterator]() { + * return iterator + * } + * }) + * + * // With NonEmptyArray from Array.make (cast to NonEmptyIterable) + * const numbers = Array.make( + * 1, + * 2, + * 3, + * 4, + * 5 + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * const [first, rest] = NonEmptyIterable.unprepend(numbers) + * console.log(first) // 1 + * console.log(globalThis.Array.from(iteratorToIterable(rest))) // [2, 3, 4, 5] + * + * // With strings (assert when known to be non-empty) + * const text = "hello" as unknown as NonEmptyIterable.NonEmptyIterable + * const [firstChar, restChars] = NonEmptyIterable.unprepend(text) + * console.log(firstChar) // "h" + * console.log(globalThis.Array.from(iteratorToIterable(restChars)).join("")) // "ello" + * + * // With Sets (assert when known to be non-empty) + * const uniqueNumbers = new Set([ + * 10, + * 20, + * 30 + * ]) as unknown as NonEmptyIterable.NonEmptyIterable + * const [firstUnique, restUnique] = NonEmptyIterable.unprepend(uniqueNumbers) + * console.log(firstUnique) // 10 (or any element, Set order is not guaranteed) + * console.log(globalThis.Array.from(iteratorToIterable(restUnique))) // [20, 30] (in some order) + * + * // With Maps (assert when known to be non-empty) + * const keyValuePairs = new Map([["a", 1], ["b", 2], [ + * "c", + * 3 + * ]]) as unknown as NonEmptyIterable.NonEmptyIterable<[string, number]> + * const [firstPair, restPairs] = NonEmptyIterable.unprepend(keyValuePairs) + * console.log(firstPair) // ["a", 1] + * console.log(globalThis.Array.from(iteratorToIterable(restPairs))) // [["b", 2], ["c", 3]] + * + * // With custom generators + * function* fibonacci(): Generator { + * let a = 1, b = 1 + * yield a + * for (let i = 0; i < 10; i++) { + * yield b + * const next = a + b + * a = b + * b = next + * } + * } + * + * const generator = Chunk.fromIterable( + * fibonacci() + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * const [firstFib, restFib] = NonEmptyIterable.unprepend(generator) + * console.log(firstFib) // 1 + * console.log(globalThis.Array.from(iteratorToIterable(restFib))) // [1, 2, 3, 5, 8, 13, 21, 34, 55, 89] + * + * // Practical usage: implementing reduce for non-empty iterables + * function reduceNonEmpty( + * data: NonEmptyIterable.NonEmptyIterable, + * f: (acc: B, current: A) => B, + * initial: B + * ): B { + * const [first, rest] = NonEmptyIterable.unprepend(data) + * let result = f(initial, first) + * + * // Convert iterator to iterable for iteration + * const iterable = { + * [Symbol.iterator]() { + * return rest + * } + * } + * for (const item of iterable) { + * result = f(result, item) + * } + * + * return result + * } + * + * const data = Array.make( + * 1, + * 2, + * 3, + * 4 + * ) as unknown as NonEmptyIterable.NonEmptyIterable + * const sum = reduceNonEmpty(data, (acc, x) => acc + x, 0) // 10 + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const unprepend = (self: NonEmptyIterable): [firstElement: A, remainingElements: Iterator] => { + const iterator = self[Symbol.iterator]() + const next = iterator.next() + if (next.done) { + throw new Error( + "BUG: NonEmptyIterator should not be empty - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return [next.value, iterator] +} diff --git a/repos/effect/packages/effect/src/Number.ts b/repos/effect/packages/effect/src/Number.ts new file mode 100644 index 0000000..4fefbae --- /dev/null +++ b/repos/effect/packages/effect/src/Number.ts @@ -0,0 +1,879 @@ +/** + * Works with TypeScript `number` values. + * + * This module exposes the native `Number` constructor together with helpers for + * checking, parsing, arithmetic, safe division, comparison, range checks, + * clamping, rounding, ordering, equivalence, and numeric aggregation. + * + * @since 2.0.0 + */ +import * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import type { Ordering } from "./Ordering.ts" +import * as predicate from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Exposes the global number constructor. + * + * **When to use** + * + * Use to access native JavaScript numeric coercion from the Effect module + * namespace. + * + * **Gotchas** + * + * This follows native `Number` coercion rules, including empty strings + * becoming `0` and invalid numeric strings becoming `NaN`. + * + * @see {@link parse} for parsing strings into an `Option` + * + * **Example** (Coercing values to numbers) + * + * ```ts + * import { Number as N } from "effect" + * + * const num = N.Number("42") + * console.log(num) // 42 + * + * const float = N.Number("3.14") + * console.log(float) // 3.14 + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const Number = globalThis.Number + +/** + * Checks whether a value is a `number`. + * + * **When to use** + * + * Use to validate unknown input and narrow it to `number`. + * + * **Example** (Checking for numbers) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.isNumber(2), true) + * assert.deepStrictEqual(Number.isNumber("2"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNumber: (input: unknown) => input is number = predicate.isNumber + +/** + * Provides an addition operation on `number`s. + * + * **When to use** + * + * Use to add two numbers. + * + * **Example** (Adding numbers) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.sum(2, 3), 5) + * ``` + * + * @see {@link sumAll} for summing an iterable of numbers + * + * @category math + * @since 2.0.0 + */ +export const sum: { + (that: number): (self: number) => number + (self: number, that: number): number +} = dual(2, (self: number, that: number): number => self + that) + +/** + * Provides a multiplication operation on `number`s. + * + * **When to use** + * + * Use to multiply two numbers. + * + * **Example** (Multiplying numbers) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.multiply(2, 3), 6) + * ``` + * + * @see {@link multiplyAll} for multiplying an iterable of numbers + * + * @category math + * @since 2.0.0 + */ +export const multiply: { + (that: number): (self: number) => number + (self: number, that: number): number +} = dual(2, (self: number, that: number): number => self * that) + +/** + * Provides a subtraction operation on `number`s. + * + * **When to use** + * + * Use to subtract one number from another. + * + * **Example** (Subtracting numbers) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.subtract(2, 3), -1) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const subtract: { + (that: number): (self: number) => number + (self: number, that: number): number +} = dual(2, (self: number, that: number): number => self - that) + +/** + * Divides `number`s safely, returning `Option.none()` if the divisor is `0`. + * + * **When to use** + * + * Use to divide numbers while representing division by zero as `Option.none`. + * + * **Example** (Dividing numbers safely) + * + * ```ts + * import { Number } from "effect" + * + * Number.divide(6, 3) // Option.some(2) + * Number.divide(6, 0) // Option.none() + * ``` + * + * @see {@link divideUnsafe} for division that throws when the divisor is zero + * @see {@link remainder} for the numeric remainder operation + * + * @category math + * @since 2.0.0 + */ +export const divide: { + (that: number): (self: number) => Option.Option + (self: number, that: number): Option.Option +} = dual( + 2, + (self: number, that: number): Option.Option => that === 0 ? Option.none() : Option.some(self / that) +) + +/** + * Divides two `number` values without returning an `Option`. + * + * **When to use** + * + * Use to divide `number` values where the divisor is known to be non-zero and + * a plain `number` result is preferred over handling `Option.none`. + * + * **Gotchas** + * + * Throws a `RangeError` if the divisor is `0`. + * + * **Example** (Dividing numbers unsafely) + * + * ```ts + * import { Number } from "effect" + * + * console.log(Number.divideUnsafe(6, 3)) // 2 + * + * // Passing 0 as the divisor throws a RangeError("Division by zero"). + * ``` + * + * @see {@link divide} for division that returns `Option.none` when the divisor is zero + * + * @category math + * @since 4.0.0 + */ +export const divideUnsafe: { + (that: number): (self: number) => number + (self: number, that: number): number +} = dual( + 2, + (self: number, that: number): number => + Option.getOrThrowWith(divide(self, that), () => new RangeError("Division by zero")) +) + +/** + * Returns the result of adding `1` to a given number. + * + * **When to use** + * + * Use to increment a numeric counter by one. + * + * **Example** (Incrementing a number) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.increment(2), 3) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const increment = (n: number): number => n + 1 + +/** + * Decrements a number by `1`. + * + * **When to use** + * + * Use to decrement a numeric counter by one. + * + * **Example** (Decrementing a number) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.decrement(3), 2) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const decrement = (n: number): number => n - 1 + +/** + * Order instance for `number` values. + * + * **When to use** + * + * Use when you need to sort or compare numbers through APIs that accept an + * ordering instance. + * + * **Example** (Comparing numbers) + * + * ```ts + * import { Number } from "effect" + * + * console.log(Number.Order(1, 2)) // -1 + * console.log(Number.Order(2, 1)) // 1 + * console.log(Number.Order(1, 1)) // 0 + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.Number + +/** + * Equivalence instance for numbers where `NaN` is considered equal to `NaN`. + * + * **When to use** + * + * Use when checking numeric equality through APIs that accept an equivalence + * relation. + * + * **Example** (Comparing numbers for equivalence) + * + * ```ts + * import { Number } from "effect" + * + * console.log(Number.Equivalence(1, 1)) // true + * console.log(Number.Equivalence(1, 2)) // false + * console.log(Number.Equivalence(NaN, NaN)) // true + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.Number + +/** + * Returns `true` if the first argument is less than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one number is strictly less than another. + * + * **Example** (Checking less-than comparisons) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.isLessThan(2, 3), true) + * assert.deepStrictEqual(Number.isLessThan(3, 3), false) + * assert.deepStrictEqual(Number.isLessThan(4, 3), false) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThan: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.isLessThan(Order) + +/** + * Returns a function that checks if a given `number` is less than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one number is less than or equal to another. + * + * **Example** (Checking less-than-or-equal comparisons) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.isLessThanOrEqualTo(2, 3), true) + * assert.deepStrictEqual(Number.isLessThanOrEqualTo(3, 3), true) + * assert.deepStrictEqual(Number.isLessThanOrEqualTo(4, 3), false) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.isLessThanOrEqualTo(Order) + +/** + * Returns `true` if the first argument is greater than the second, otherwise `false`. + * + * **When to use** + * + * Use to test whether one number is strictly greater than another. + * + * **Example** (Checking greater-than comparisons) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.isGreaterThan(2, 3), false) + * assert.deepStrictEqual(Number.isGreaterThan(3, 3), false) + * assert.deepStrictEqual(Number.isGreaterThan(4, 3), true) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.isGreaterThan(Order) + +/** + * Returns a function that checks if a given `number` is greater than or equal to the provided one. + * + * **When to use** + * + * Use to test whether one number is greater than or equal to another. + * + * **Example** (Checking greater-than-or-equal comparisons) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(2, 3), false) + * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(3, 3), true) + * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(4, 3), true) + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo: { + (that: number): (self: number) => boolean + (self: number, that: number): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** + * Checks whether a `number` is between a `minimum` and `maximum` value (inclusive). + * + * **When to use** + * + * Use to test whether a number falls inside an inclusive range. + * + * **Example** (Checking inclusive ranges) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * const between = Number.between({ minimum: 0, maximum: 5 }) + * + * assert.deepStrictEqual(between(3), true) + * assert.deepStrictEqual(between(-1), false) + * assert.deepStrictEqual(between(6), false) + * ``` + * + * @see {@link clamp} for forcing a number into an inclusive range + * + * @category predicates + * @since 2.0.0 + */ +export const between: { + (options: { + minimum: number + maximum: number + }): (self: number) => boolean + (self: number, options: { + minimum: number + maximum: number + }): boolean +} = order.isBetween(Order) + +/** + * Restricts the given `number` to be within the range specified by the `minimum` and `maximum` values. + * + * **When to use** + * + * Use to force a number into an inclusive range. + * + * **Details** + * + * - If the `number` is less than the `minimum` value, the function returns the `minimum` value. + * - If the `number` is greater than the `maximum` value, the function returns the `maximum` value. + * - Otherwise, it returns the original `number`. + * + * **Example** (Clamping to a range) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * const clamp = Number.clamp({ minimum: 1, maximum: 5 }) + * + * assert.equal(clamp(3), 3) + * assert.equal(clamp(0), 1) + * assert.equal(clamp(6), 5) + * ``` + * + * @see {@link between} for checking whether a number is already inside a range + * + * @category math + * @since 2.0.0 + */ +export const clamp: { + (options: { + minimum: number + maximum: number + }): (self: number) => number + (self: number, options: { + minimum: number + maximum: number + }): number +} = order.clamp(Order) + +/** + * Returns the minimum between two `number`s. + * + * **When to use** + * + * Use to select the smaller of two numbers. + * + * **Example** (Finding the minimum) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.min(2, 3), 2) + * ``` + * + * @see {@link max} for selecting the larger value + * + * @category math + * @since 2.0.0 + */ +export const min: { + (that: number): (self: number) => number + (self: number, that: number): number +} = order.min(Order) + +/** + * Returns the maximum between two `number`s. + * + * **When to use** + * + * Use to select the larger of two numbers. + * + * **Example** (Finding the maximum) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.max(2, 3), 3) + * ``` + * + * @see {@link min} for selecting the smaller value + * + * @category math + * @since 2.0.0 + */ +export const max: { + (that: number): (self: number) => number + (self: number, that: number): number +} = order.max(Order) + +/** + * Determines the sign of a given `number`. + * + * **When to use** + * + * Use to classify a number as negative, zero, or positive. + * + * **Example** (Determining the sign) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.sign(-5), -1) + * assert.deepStrictEqual(Number.sign(0), 0) + * assert.deepStrictEqual(Number.sign(5), 1) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const sign = (n: number): Ordering => Order(n, 0) + +/** + * Takes an `Iterable` of `number`s and returns their sum as a single `number`. + * + * **When to use** + * + * Use to sum all numbers in an iterable. + * + * **Example** (Summing an iterable) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.sumAll([2, 3, 4]), 9) + * ``` + * + * @see {@link sum} for adding two numbers + * @see {@link ReducerSum} for summing through APIs that consume a `Reducer` + * + * @category math + * @since 2.0.0 + */ +export const sumAll = (collection: Iterable): number => { + let out = 0 + for (const n of collection) { + out += n + } + return out +} + +/** + * Takes an `Iterable` of `number`s and returns their multiplication as a single `number`. + * + * **When to use** + * + * Use to multiply all numbers in an iterable. + * + * **Example** (Multiplying an iterable) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.multiplyAll([2, 3, 4]), 24) + * ``` + * + * @see {@link multiply} for multiplying two numbers + * @see {@link ReducerMultiply} for multiplying through APIs that consume a `Reducer` + * + * @category math + * @since 2.0.0 + */ +export const multiplyAll = (collection: Iterable): number => { + let out = 1 + for (const n of collection) { + if (n === 0) { + return 0 + } + out *= n + } + return out +} + +/** + * Returns the remainder left over when one operand is divided by a second operand, always taking the sign of the dividend. + * + * **When to use** + * + * Use to compute a numeric remainder while preserving decimal precision better + * than direct JavaScript `%` for decimal operands. + * + * **Example** (Calculating remainders) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.remainder(2, 2), 0) + * assert.deepStrictEqual(Number.remainder(3, 2), 1) + * assert.deepStrictEqual(Number.remainder(-4, 2), -0) + * ``` + * + * @see {@link divide} for quotient calculation with division-by-zero represented as `Option.none` + * + * @category math + * @since 2.0.0 + */ +export const remainder: { + (divisor: number): (self: number) => number + (self: number, divisor: number): number +} = dual(2, (self: number, divisor: number): number => { + const selfString = self.toString() + const divisorString = divisor.toString() + if (selfString.includes("e") || divisorString.includes("e")) { + if (!globalThis.Number.isFinite(self) || !globalThis.Number.isFinite(divisor) || divisor === 0) { + return NaN + } + return remainderWithScientificNotation(self, divisor) + } + const selfDecCount = (selfString.split(".")[1] || "").length + const divisorDecCount = (divisorString.split(".")[1] || "").length + const decCount = selfDecCount > divisorDecCount ? selfDecCount : divisorDecCount + const selfInt = parseInt(self.toFixed(decCount).replace(".", "")) + const divisorInt = parseInt(divisor.toFixed(decCount).replace(".", "")) + return (selfInt % divisorInt) / Math.pow(10, decCount) +}) + +function remainderWithScientificNotation(self: number, divisor: number): number { + const [selfCoefficient, selfExponent] = toScientificInteger(self) + const [divisorCoefficient, divisorExponent] = toScientificInteger(divisor) + const exponent = Math.min(selfExponent, divisorExponent) + const selfInteger = selfCoefficient * BigInt(10) ** BigInt(selfExponent - exponent) + const divisorInteger = divisorCoefficient * BigInt(10) ** BigInt(divisorExponent - exponent) + const out = selfInteger % divisorInteger + if (out === BigInt(0)) { + return self < 0 || Object.is(self, -0) ? -0 : 0 + } + const remainder = globalThis.Number(`${out}e${exponent}`) + return remainder === 0 ? Math.sign(self) * globalThis.Number.MIN_VALUE : remainder +} + +function toScientificInteger(n: number): readonly [coefficient: bigint, exponent: number] { + const scientific = Math.abs(n).toExponential() + const eIndex = scientific.indexOf("e") + const digits = scientific.slice(0, eIndex).replace(".", "") + const coefficient = BigInt(digits) * (n < 0 ? -BigInt(1) : BigInt(1)) + return [coefficient, globalThis.Number(scientific.slice(eIndex + 1)) - digits.length + 1] +} + +/** + * Returns the next power of 2 from the given number. + * + * **When to use** + * + * Use to round a number up to the next power of two. + * + * **Example** (Finding the next power of two) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.nextPow2(5), 8) + * assert.deepStrictEqual(Number.nextPow2(17), 32) + * ``` + * + * @category math + * @since 2.0.0 + */ +export const nextPow2 = (n: number): number => { + const nextPow = Math.ceil(Math.log(n) / Math.log(2)) + return Math.max(Math.pow(2, nextPow), 2) +} + +/** + * Parses a `number` from a `string` safely using the `Number()` function. + * The following special string values are supported: "NaN", "Infinity", "-Infinity". + * + * **When to use** + * + * Use to parse numeric text without throwing on invalid input. + * + * **Example** (Parsing numbers from strings) + * + * ```ts + * import { Number } from "effect" + * + * Number.parse("42") // Option.some(42) + * Number.parse("3.14") // Option.some(3.14) + * Number.parse("NaN") // Option.some(NaN) + * Number.parse("Infinity") // Option.some(Infinity) + * Number.parse("-Infinity") // Option.some(-Infinity) + * Number.parse("not a number") // Option.none() + * ``` + * + * @see {@link Number} for native constructor coercion + * + * @category constructors + * @since 2.0.0 + */ +export const parse = (s: string): Option.Option => { + if (s === "NaN") { + return Option.some(NaN) + } + if (s === "Infinity") { + return Option.some(Infinity) + } + if (s === "-Infinity") { + return Option.some(-Infinity) + } + if (s.trim() === "") { + return Option.none() + } + const n = Number(s) + return Number.isNaN(n) ? Option.none() : Option.some(n) +} + +/** + * Returns the number rounded with the given precision. + * + * **When to use** + * + * Use to round a number to a fixed number of decimal places. + * + * **Example** (Rounding with precision) + * + * ```ts + * import { Number } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Number.round(1.1234, 2), 1.12) + * assert.deepStrictEqual(Number.round(1.567, 2), 1.57) + * ``` + * + * @category math + * @since 3.8.0 + */ +export const round: { + (precision: number): (self: number) => number + (self: number, precision: number): number +} = dual(2, (self: number, precision: number): number => { + const factor = Math.pow(10, precision) + return Math.round(self * factor) / factor +}) + +/** + * Reducer for combining `number`s using addition. + * + * **When to use** + * + * Use to sum many numbers through APIs that consume a `Reducer`. + * + * **Details** + * + * The reducer starts from `0`, so `combineAll([])` returns `0`. + * + * @see {@link sumAll} for summing an iterable directly + * @see {@link ReducerMultiply} for multiplying number values + * + * @category math + * @since 4.0.0 + */ +export const ReducerSum: Reducer.Reducer = Reducer.make((a, b) => a + b, 0) + +/** + * Reducer for combining `number`s using multiplication. + * + * **When to use** + * + * Use to multiply many numbers through APIs that consume a `Reducer`. + * + * **Details** + * + * The reducer starts from `1`, so reducing an empty collection returns `1`. + * + * **Gotchas** + * + * Reducing an iterable short-circuits when it sees `0`, so later elements are + * not consumed. + * + * @see {@link multiplyAll} for multiplying an iterable directly + * + * @category math + * @since 4.0.0 + */ +export const ReducerMultiply: Reducer.Reducer = Reducer.make((a, b) => a * b, 1, (collection) => { + let acc = 1 + for (const n of collection) { + if (n === 0) return 0 + acc *= n + } + return acc +}) + +/** + * Reducer for reducing `number`s by keeping the maximum value. + * + * **When to use** + * + * Use to keep the largest number through APIs that consume a `Reducer`. + * + * **Details** + * + * The reducer starts from `-Infinity`, so reducing an empty collection returns + * `-Infinity`. + * + * **Gotchas** + * + * `NaN` values propagate through `Math.max`. + * + * @see {@link ReducerMin} for keeping the smallest number + * @see {@link max} for comparing two numbers directly + * + * @category math + * @since 4.0.0 + */ +export const ReducerMax: Reducer.Reducer = Reducer.make((a, b) => Math.max(a, b), -Infinity) + +/** + * Reducer for reducing `number`s by keeping the minimum value. + * + * **When to use** + * + * Use to keep the smallest number through APIs that consume a `Reducer`. + * + * **Details** + * + * The reducer starts from `Infinity`, so reducing an empty collection returns + * `Infinity`. + * + * **Gotchas** + * + * `NaN` values propagate through `Math.min`. + * + * @see {@link ReducerMax} for keeping the largest number + * @see {@link min} for comparing two numbers directly + * + * @category math + * @since 4.0.0 + */ +export const ReducerMin: Reducer.Reducer = Reducer.make((a, b) => Math.min(a, b), Infinity) diff --git a/repos/effect/packages/effect/src/Optic.ts b/repos/effect/packages/effect/src/Optic.ts new file mode 100644 index 0000000..e9b3de8 --- /dev/null +++ b/repos/effect/packages/effect/src/Optic.ts @@ -0,0 +1,1701 @@ +/** + * Reads and updates focused parts of values without mutating the original + * value. + * + * An optic describes where to look inside a value, such as a record field, a + * union variant, an optional value, or several values in a collection. Different + * optic types describe different kinds of focus: some always find a value, + * some may not, and some can find many. This module includes the optic types, + * constructors, focusing helpers, and operations for replacing, modifying, or + * collecting focused values. + * + * @since 4.0.0 + */ + +import { format } from "./Formatter.ts" +import { identity, memoize } from "./Function.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import * as SchemaAST from "./SchemaAST.ts" +import type * as SchemaIssue from "./SchemaIssue.ts" +import * as Struct from "./Struct.ts" +import type { IsUnion } from "./Types.ts" + +/** + * A lossless, reversible conversion between types `S` and `A`. + * + * **When to use** + * + * Use when you have a pair of functions that convert back and forth without losing + * information (e.g. `Record ↔ entries`, `Celsius ↔ Fahrenheit`). + * - You want the strongest optic that can be composed with any other. + * + * **Details** + * + * - `get(s)` always succeeds and returns an `A`. + * - `set(a)` always succeeds and returns an `S`. + * - `get(set(a)) === a` and `set(get(s))` equals `s` (round-trip laws). + * - Extends both {@link Lens} and {@link Prism}. + * + * **Example** (Converting between Celsius and Fahrenheit) + * + * ```ts + * import { Optic } from "effect" + * + * const fahrenheit = Optic.makeIso( + * (c) => c * 9 / 5 + 32, + * (f) => (f - 32) * 5 / 9 + * ) + * + * console.log(fahrenheit.get(100)) + * // Output: 212 + * + * console.log(fahrenheit.set(32)) + * // Output: 0 + * ``` + * + * @see {@link makeIso} — constructor + * @see {@link Lens} — when you only need a one-directional focus into a whole + * @see {@link Prism} — when the focus may not be present + * + * @category Iso + * @since 4.0.0 + */ +export interface Iso extends Lens, Prism {} + +/** + * Creates an {@link Iso} from a pair of conversion functions. + * + * **When to use** + * + * Use when you have two pure conversion functions that preserve all information + * between `S` and `A`. + * + * **Details** + * + * The returned optic can be composed with any other optic. + * + * **Example** (Wrapping and unwrapping a branded type) + * + * ```ts + * import { Optic } from "effect" + * + * type Meters = { readonly value: number } + * const meters = Optic.makeIso( + * (m) => m.value, + * (n) => ({ value: n }) + * ) + * + * console.log(meters.get({ value: 100 })) + * // Output: 100 + * + * console.log(meters.set(42)) + * // Output: { value: 42 } + * ``` + * + * @see {@link Iso} — the type this function returns + * @see {@link id} — identity iso (no conversion) + * + * @category constructors + * @since 4.0.0 + */ +export function makeIso(get: (s: S) => A, set: (a: A) => S): Iso { + return make(new IsoNode(get, set)) +} + +/** + * Focuses on exactly one part `A` inside a whole `S`. + * + * **When to use** + * + * Use when you always have a value to read and need the original `S` to produce + * the updated whole, unlike `Iso`. + * + * **Details** + * + * - `get(s)` always succeeds and returns `A`. + * - `replace(a, s)` returns a new `S` with the focused part replaced. + * - Extends {@link Optional}. + * - Composing a Lens with a {@link Prism} or {@link Optional} produces an + * {@link Optional}. + * + * **Example** (Focusing on a struct field) + * + * ```ts + * import { Optic } from "effect" + * + * type Person = { readonly name: string; readonly age: number } + * + * const _name = Optic.id().key("name") + * + * console.log(_name.get({ name: "Alice", age: 30 })) + * // Output: "Alice" + * ``` + * + * @see {@link makeLens} — constructor + * @see {@link Iso} — when conversion is lossless in both directions + * @see {@link Optional} — when reading can also fail + * + * @category Lens + * @since 4.0.0 + */ +export interface Lens extends Optional { + readonly get: (s: S) => A +} + +/** + * Creates a {@link Lens} from a getter and a replacer. + * + * **When to use** + * + * Use when you can always extract `A` from `S` and produce a new `S` by + * substituting a new `A`. + * + * **Details** + * + * - `replace(a, s)` should return a structurally new `S` with `a` in place + * of the old focus. + * + * **Example** (Focusing on the first element of a pair) + * + * ```ts + * import { Optic } from "effect" + * + * const _first = Optic.makeLens( + * (pair) => pair[0], + * (s, pair) => [s, pair[1]] + * ) + * + * console.log(_first.get(["hello", 42])) + * // Output: "hello" + * + * console.log(_first.replace("world", ["hello", 42])) + * // Output: ["world", 42] + * ``` + * + * @see {@link Lens} — the type this function returns + * @see {@link makeIso} — when no original `S` is needed for `set` + * + * @category constructors + * @since 4.0.0 + */ +export function makeLens(get: (s: S) => A, replace: (a: A, s: S) => S): Lens { + return make(new LensNode(get, replace)) +} + +/** + * Focuses on a part `A` of `S` that may not be present (e.g. a union + * variant or a validated subset). + * + * **When to use** + * + * Use when the focus is conditional — reading can fail (wrong variant, failed + * validation). + * - Building a new `S` from `A` does **not** require the original `S`. + * + * **Details** + * + * - `getResult(s)` returns `Result.Success` when the focus matches, or + * `Result.Failure` with an error message. + * - `set(a)` always succeeds and returns a new `S`. + * - Extends {@link Optional}. + * - Composing two Prisms produces a Prism; composing a Prism with a + * {@link Lens} produces an {@link Optional}. + * + * **Example** (Narrowing a tagged union) + * + * ```ts + * import { Optic, Result } from "effect" + * + * type Shape = + * | { readonly _tag: "Circle"; readonly radius: number } + * | { readonly _tag: "Rect"; readonly width: number } + * + * const _circle = Optic.id().tag("Circle") + * + * console.log(Result.isSuccess(_circle.getResult({ _tag: "Circle", radius: 5 }))) + * // Output: true + * + * console.log(Result.isFailure(_circle.getResult({ _tag: "Rect", width: 10 }))) + * // Output: true + * ``` + * + * @see {@link makePrism} — constructor + * @see {@link fromChecks} — build a Prism from schema checks + * @see {@link Lens} — when reading always succeeds + * + * @category Prism + * @since 4.0.0 + */ +export interface Prism extends Optional { + readonly set: (a: A) => S +} + +/** + * Creates a {@link Prism} from a fallible getter and an infallible setter. + * + * **When to use** + * + * Use when reading can fail (the part may not exist in `S`), but building `S` + * from `A` always succeeds. + * + * **Details** + * + * - `getResult` should return `Result.fail(message)` on mismatch. + * + * **Example** (Parsing a string to a number) + * + * ```ts + * import { Optic, Result } from "effect" + * + * const numeric = Optic.makePrism( + * (s) => { + * const n = Number(s) + * return Number.isNaN(n) ? Result.fail("not a number") : Result.succeed(n) + * }, + * String + * ) + * + * console.log(Result.isSuccess(numeric.getResult("42"))) + * // Output: true + * + * console.log(numeric.set(42)) + * // Output: "42" + * ``` + * + * @see {@link Prism} — the type this function returns + * @see {@link fromChecks} — build from `Schema` checks instead + * + * @category constructors + * @since 4.0.0 + */ +export function makePrism(getResult: (s: S) => Result.Result, set: (a: A) => S): Prism { + return make(new PrismNode(getResult, set)) +} + +/** + * Creates a {@link Prism} from one or more `Schema` validation checks. + * + * **When to use** + * + * Use when you want to narrow `T` to the subset that passes certain validation + * rules (e.g. positive integer). + * - You already have `Schema.isGreaterThan`, `Schema.isInt`, etc. + * + * **Details** + * + * - `getResult` runs all checks; fails with a combined error message when + * any check fails. + * - `set` is identity — the value passes through unchanged. + * + * **Example** (Creating a positive integer prism) + * + * ```ts + * import { Optic, Result, Schema } from "effect" + * + * const posInt = Optic.fromChecks( + * Schema.isGreaterThan(0), + * Schema.isInt() + * ) + * + * console.log(Result.isSuccess(posInt.getResult(3))) + * // Output: true + * + * console.log(Result.isFailure(posInt.getResult(-1))) + * // Output: true + * ``` + * + * @see {@link makePrism} — constructor with custom getter/setter + * @see {@link Prism} — the type this function returns + * + * @category constructors + * @since 4.0.0 + */ +export function fromChecks(...checks: readonly [SchemaAST.Check, ...Array>]): Prism { + return make(new CheckNode(checks)) +} + +type Node = + | IdentityNode + | IsoNode + | LensNode + | PrismNode + | OptionalNode + | PathNode + | CheckNode + | CompositionNode + +class IdentityNode { + readonly _tag = "IdentityNode" +} + +const identityNode = new IdentityNode() + +class CompositionNode { + readonly _tag = "CompositionNode" + readonly nodes: readonly [Node, ...Array] + + constructor(nodes: readonly [Node, ...Array]) { + this.nodes = nodes + } +} + +class IsoNode { + readonly _tag = "IsoNode" + readonly get: (s: S) => A + readonly set: (a: A) => S + + constructor(get: (s: S) => A, set: (a: A) => S) { + this.get = get + this.set = set + } +} + +class LensNode { + readonly _tag = "LensNode" + readonly get: (s: S) => A + readonly set: (a: A, s: S) => S + + constructor(get: (s: S) => A, set: (a: A, s: S) => S) { + this.get = get + this.set = set + } +} + +class PrismNode { + readonly _tag = "PrismNode" + readonly get: (s: S) => Result.Result + readonly set: (a: A) => S + + constructor(get: (s: S) => Result.Result, set: (a: A) => S) { + this.get = get + this.set = set + } +} + +class OptionalNode { + readonly _tag = "OptionalNode" + readonly get: (s: S) => Result.Result + readonly set: (a: A, s: S) => Result.Result + + constructor(get: (s: S) => Result.Result, set: (a: A, s: S) => Result.Result) { + this.get = get + this.set = set + } +} + +class PathNode { + readonly _tag = "PathNode" + readonly path: ReadonlyArray + + constructor(path: ReadonlyArray) { + this.path = path + } +} + +class CheckNode { + readonly _tag = "CheckNode" + readonly checks: readonly [SchemaAST.Check, ...Array>] + + constructor(checks: readonly [SchemaAST.Check, ...Array>]) { + this.checks = checks + } +} + +// Nodes that can appear in a normalized chain (no Identity/Composition) +type NormalizedNode = Exclude + +// Fuse with tail when possible, else push. +function pushNormalized(acc: Array, node: NormalizedNode): void { + const last = acc[acc.length - 1] + if (last) { + if (last._tag === "PathNode" && node._tag === "PathNode") { + // fuse Path + acc[acc.length - 1] = new PathNode([...last.path, ...node.path]) + return + } + if (last._tag === "CheckNode" && node._tag === "CheckNode") { + // fuse Checks + acc[acc.length - 1] = new CheckNode([...last.checks, ...node.checks]) + return + } + } + acc.push(node) +} + +// Collect nodes from a node into `acc`, flattening & normalizing on the fly. +function collect(node: Node, acc: Array): void { + if (node._tag === "IdentityNode") return + if (node._tag === "CompositionNode") { + // flatten without extra arrays + for (let i = 0; i < node.nodes.length; i++) collect(node.nodes[i], acc) + return + } + // primitive node + pushNormalized(acc, node) +} + +function compose(a: Node, b: Node): Node { + const nodes: Array = [] + collect(a, nodes) + collect(b, nodes) + + switch (nodes.length) { + case 0: + return identityNode + case 1: + return nodes[0] + default: + return new CompositionNode(nodes as [Node, ...Array]) + } +} + +type ForbidUnion = IsUnion extends true ? [Message] : [] + +/** + * The most general optic — both reading and writing can fail. + * + * **When to use** + * + * Use when the focus may not exist in `S` and writing a new `A` back may also + * fail, for example when the source no longer matches the expected shape. This + * is the base type extended by {@link Iso}, {@link Lens}, {@link Prism}, and + * {@link Traversal}. + * + * **Details** + * + * - `getResult(s)` returns `Result.Success` or `Result.Failure`. + * - `replaceResult(a, s)` returns `Result.Success` or + * `Result.Failure`. + * - `replace(a, s)` returns the original `s` on failure (never throws). + * - `modify(f)` returns the original `s` on failure (never throws). + * - All operations are pure; inputs are never mutated. + * + * **Example** (Focusing on an optional record key) + * + * ```ts + * import { Optic, Result } from "effect" + * + * type Env = { [key: string]: string } + * const _home = Optic.id().at("HOME") + * + * console.log(Result.isSuccess(_home.getResult({ HOME: "/root" }))) + * // Output: true + * + * console.log(Result.isFailure(_home.getResult({ PATH: "/bin" }))) + * // Output: true + * + * // replace returns original on failure + * console.log(_home.replace("/new", { PATH: "/bin" })) + * // Output: { PATH: "/bin" } + * ``` + * + * @see {@link makeOptional} — constructor + * @see {@link Lens} — when reading always succeeds + * @see {@link Prism} — when writing always succeeds + * + * @category Optional + * @since 4.0.0 + */ +export interface Optional { + readonly node: Node + /** + * Attempts to read the focus `A` from the whole `S`. Returns + * `Result.Success` when the focus exists, or + * `Result.Failure` with a descriptive error otherwise. + */ + readonly getResult: (s: S) => Result.Result + /** + * Replaces the focus in `S` with a new `A`. Returns the original `s` + * unchanged when the optic cannot focus (never throws). + */ + readonly replace: (a: A, s: S) => S + /** + * Like {@link replace}, but returns an explicit `Result` so callers can + * detect and handle failure. + */ + readonly replaceResult: (a: A, s: S) => Result.Result + /** + * Composes this optic with another. The result type is the weakest of + * the two: Iso + Iso = Iso, Lens + Prism = Optional, etc. + * + * **Example** (Composing a lens with a prism) + * + * ```ts + * import { Optic, Option } from "effect" + * + * type State = { value: Option.Option } + * + * const _inner = Optic.id().key("value").compose(Optic.some()) + * // _inner is Optional + * ``` + * + * @see {@link id} — start a composition chain + */ + compose(this: Iso, that: Iso): Iso + compose(this: Lens, that: Lens): Lens + compose(this: Prism, that: Prism): Prism + compose(this: Optional, that: Optional): Optional + + /** + * Returns a function `(s: S) => S` that applies `f` to the focused value. + * If the optic cannot focus, the original `s` is returned unchanged. + * + * **Example** (Incrementing a nested field) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly a: { readonly b: number } } + * const _b = Optic.id().key("a").key("b") + * + * const inc = _b.modify((n) => n + 1) + * console.log(inc({ a: { b: 1 } })) + * // Output: { a: { b: 2 } } + * ``` + */ + modify(f: (a: A) => A): (s: S) => S + + /** + * Focuses on a property of the current struct/tuple focus. + * + * **Details** + * + * - On a {@link Lens}, returns a Lens. + * - On an {@link Optional}, returns an Optional. + * - Does **not** work on union types (compile error). + * + * **Example** (Drilling into nested structs) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly a: { readonly b: number } } + * const _b = Optic.id().key("a").key("b") + * + * console.log(_b.get({ a: { b: 42 } })) + * // Output: 42 + * ``` + */ + key( + this: Lens, + key: Key, + ..._err: ForbidUnion + ): Lens + key( + this: Optional, + key: Key, + ..._err: ForbidUnion + ): Optional + + /** + * Focuses on a key where setting `undefined` **removes** the key from the + * struct (or splices the element from an array/tuple). + * + * **Details** + * + * - The focus type becomes `A[Key] | undefined`. + * - Does **not** work on union types (compile error). + * + * **Example** (Deleting an optional key) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly a?: number } + * const _a = Optic.id().optionalKey("a") + * + * console.log(_a.replace(undefined, { a: 1 })) + * // Output: {} + * + * console.log(_a.replace(2, {})) + * // Output: { a: 2 } + * ``` + */ + optionalKey( + this: Lens, + key: Key, + ..._err: ForbidUnion + ): Lens + optionalKey( + this: Optional, + key: Key, + ..._err: ForbidUnion + ): Optional + + /** + * Adds one or more `Schema` validation checks to the optic chain. + * `getResult` fails when any check fails; `set` passes through unchanged. + * + * **Details** + * + * - On a {@link Prism}, returns a Prism. + * - On an {@link Optional}, returns an Optional. + * + * **Example** (Focusing only on positive numbers) + * + * ```ts + * import { Optic, Result, Schema } from "effect" + * + * const _pos = Optic.id().check(Schema.isGreaterThan(0)) + * + * console.log(Result.isSuccess(_pos.getResult(5))) + * // Output: true + * + * console.log(Result.isFailure(_pos.getResult(-1))) + * // Output: true + * ``` + * + * @see {@link fromChecks} — standalone prism from checks + */ + check(this: Prism, ...checks: readonly [SchemaAST.Check, ...Array>]): Prism + check( + this: Optional, + ...checks: readonly [SchemaAST.Check, ...Array>] + ): Optional + + /** + * Narrows the focus to a subtype `B` using a type guard. + * + * **Details** + * + * - On a {@link Prism}, returns a Prism. + * - On an {@link Optional}, returns an Optional. + * - Pass optional `annotations` to customize the error message. + * + * **Example** (Narrowing a union) + * + * ```ts + * import { Optic, Result } from "effect" + * + * type B = { readonly _tag: "b"; readonly b: number } + * type S = { readonly _tag: "a"; readonly a: string } | B + * + * const _b = Optic.id().refine( + * (s: S): s is B => s._tag === "b", + * { expected: `"b" tag` } + * ) + * + * console.log(Result.isSuccess(_b.getResult({ _tag: "b", b: 1 }))) + * // Output: true + * ``` + * + * @see `.tag()` — shorthand for narrowing by `_tag` + */ + refine( + this: Prism, + refinement: (a: A) => a is B, + annotations?: Schema.Annotations.Filter + ): Prism + refine( + this: Optional, + refinement: (a: A) => a is B, + annotations?: Schema.Annotations.Filter + ): Optional + + /** + * Narrows the focus to the variant of a tagged union with the given + * `_tag` value. + * + * **Details** + * + * - On a {@link Prism}, returns a Prism. + * - On an {@link Optional}, returns an Optional. + * - Shorthand for `.refine(s => s._tag === tag)`. + * + * **Example** (Focusing a tagged variant) + * + * ```ts + * import { Optic, Result } from "effect" + * + * type Shape = + * | { readonly _tag: "Circle"; readonly radius: number } + * | { readonly _tag: "Rect"; readonly width: number } + * + * const _radius = Optic.id().tag("Circle").key("radius") + * + * console.log(Result.isSuccess(_radius.getResult({ _tag: "Circle", radius: 5 }))) + * // Output: true + * + * console.log(Result.isFailure(_radius.getResult({ _tag: "Rect", width: 10 }))) + * // Output: true + * ``` + * + * @see `.refine()` — for arbitrary type guards + */ + tag( + this: Prism, + tag: Tag + ): Prism> + tag( + this: Optional, + tag: Tag + ): Optional> + + /** + * Focuses on a key only if it exists (`Object.hasOwn`). Both + * `getResult` and `replaceResult` fail when the key is absent. + * + * **Details** + * + * Unlike `.key()`, which always succeeds on the read side, `.at()` is + * useful for Records or arrays where the key/index may not be present. + * + * - Always returns an {@link Optional}. + * - Does **not** work on union types (compile error). + * + * **Example** (Accessing records safely) + * + * ```ts + * import { Optic, Result } from "effect" + * + * type Env = { [key: string]: number } + * const _x = Optic.id().at("x") + * + * console.log(Result.isSuccess(_x.getResult({ x: 1 }))) + * // Output: true + * + * console.log(Result.isFailure(_x.getResult({ y: 2 }))) + * // Output: true + * ``` + * + * @see `.key()` — when the key is always present + */ + at( + this: Optional, + key: Key, + ..._err: ForbidUnion + ): Optional + + /** + * Focuses on a subset of keys of the current struct focus. + * + * **Details** + * + * - On a {@link Lens}, returns a Lens. + * - On an {@link Optional}, returns an Optional. + * - Does **not** work on union types (compile error). + * + * **Example** (Picking keys) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly a: string; readonly b: number; readonly c: boolean } + * + * const _ac = Optic.id().pick(["a", "c"]) + * + * console.log(_ac.get({ a: "hi", b: 1, c: true })) + * // Output: { a: "hi", c: true } + * ``` + * + * @see `.omit()` — the inverse operation + */ + pick>( + this: Lens, + keys: Keys, + ..._err: ForbidUnion + ): Lens> + pick>( + this: Optional, + keys: Keys, + ..._err: ForbidUnion + ): Optional> + + /** + * Focuses on all keys **except** the specified ones. + * + * **Details** + * + * - On a {@link Lens}, returns a Lens. + * - On an {@link Optional}, returns an Optional. + * - Does **not** work on union types (compile error). + * + * **Example** (Omitting keys) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly a: string; readonly b: number; readonly c: boolean } + * + * const _ac = Optic.id().omit(["b"]) + * + * console.log(_ac.get({ a: "hi", b: 1, c: true })) + * // Output: { a: "hi", c: true } + * ``` + * + * @see `.pick()` — the inverse operation + * + * @since 4.0.0 + */ + omit>( + this: Lens, + keys: Keys, + ..._err: ForbidUnion + ): Lens> + omit>( + this: Optional, + keys: Keys, + ..._err: ForbidUnion + ): Optional> + + /** + * Filters out `undefined` from the focus, producing a {@link Prism}. + * `getResult` fails when the focus is `undefined`. + * + * **Example** (Filtering undefined values) + * + * ```ts + * import { Optic, Result } from "effect" + * + * const _defined = Optic.id().notUndefined() + * + * console.log(Result.isSuccess(_defined.getResult(42))) + * // Output: true + * + * console.log(Result.isFailure(_defined.getResult(undefined))) + * // Output: true + * ``` + * + * @since 4.0.0 + */ + notUndefined(): Prism> + notUndefined(): Optional> + + /** + * Focuses **all elements** of an array-like focus and optionally narrows + * to a subset using an element-level optic. + * Available only on {@link Traversal} (i.e. when `A` is + * `ReadonlyArray`). Returns a new Traversal focused on the + * selected elements. + * + * **Details** + * + * - **getResult** collects the values focused by `f(id())` for each + * element. Non-focusable elements are skipped. + * - **replaceResult** expects exactly as many values as were collected by + * `getResult` and writes them back in order. Fails with a + * length-mismatch error if counts differ. + * + * **Example** (Incrementing liked posts) + * + * ```ts + * import { Optic, Schema } from "effect" + * + * type Post = { title: string; likes: number } + * type S = { user: { posts: ReadonlyArray } } + * + * const _likes = Optic.id() + * .key("user") + * .key("posts") + * .forEach((post) => post.key("likes").check(Schema.isGreaterThan(0))) + * + * const addLike = _likes.modifyAll((n) => n + 1) + * + * console.log( + * addLike({ + * user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }] } + * }) + * ) + * // Output: { user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 2 }] } } + * ``` + * + * @see {@link getAll} — extract all focused elements as an array + * @see `.modifyAll()` — apply a function to every focused element + */ + forEach(this: Traversal, f: (iso: Iso) => Optional): Traversal + + /** + * Applies a function to **every** element focused by the traversal. + * + * **Details** + * + * Available only on {@link Traversal}. Returns a function `(s: S) => S`. + * If the traversal cannot focus, the original `s` is returned unchanged. + * + * Unlike `.modify()`, which operates on the whole array, `modifyAll` + * maps `f` over each individual element. + * + * **Example** (Doubling all focused values) + * + * ```ts + * import { Optic, Schema } from "effect" + * + * type S = { readonly items: ReadonlyArray } + * + * const _positive = Optic.id() + * .key("items") + * .forEach((n) => n.check(Schema.isGreaterThan(0))) + * + * const doubled = _positive.modifyAll((n) => n * 2) + * + * console.log(doubled({ items: [1, -2, 3] })) + * // Output: { items: [2, -2, 6] } + * ``` + * + * @see `.forEach()` — create a sub-traversal + * @see {@link getAll} — extract focused elements + */ + modifyAll(this: Traversal, f: (a: A) => A): (s: S) => S +} + +/** + * Creates an {@link Optional} from a fallible getter and a fallible setter. + * + * **When to use** + * + * Use when you need an optic for a focus that may be missing on read and may + * reject updates on write. + * + * **Details** + * + * - `getResult` should return `Result.fail(message)` on mismatch. + * - `set` should return `Result.fail(message)` when the update cannot be + * applied. + * + * **Example** (Accessing record keys safely) + * + * ```ts + * import { Optic, Result } from "effect" + * + * const atKey = (key: string) => + * Optic.makeOptional, number>( + * (s) => + * Object.hasOwn(s, key) + * ? Result.succeed(s[key]) + * : Result.fail(`Key "${key}" not found`), + * (a, s) => + * Object.hasOwn(s, key) + * ? Result.succeed({ ...s, [key]: a }) + * : Result.fail(`Key "${key}" not found`) + * ) + * + * console.log(Result.isSuccess(atKey("x").getResult({ x: 1 }))) + * // Output: true + * ``` + * + * @see {@link Optional} — the type this function returns + * @see {@link makeLens} — when reading always succeeds + * @see {@link makePrism} — when writing always succeeds + * + * @category constructors + * @since 4.0.0 + */ +export function makeOptional( + getResult: (s: S) => Result.Result, + set: (a: A, s: S) => Result.Result +): Optional { + return make(new OptionalNode(getResult, set)) +} + +/** + * An optic that focuses on **zero or more** elements of type `A` inside `S`. + * + * **When to use** + * + * Use when you want to read/update multiple elements at once (e.g. all items in + * an array, or a filtered subset). + * + * **Details** + * + * - Technically `Optional>` — the focused value is an + * array of all matched elements. + * - Use `.forEach()` to add per-element sub-optics (filtering, drilling + * deeper). + * - Use `.modifyAll(f)` to map a function over every focused element. + * - Use {@link getAll} to extract all focused elements as a plain array. + * + * **Example** (Traversing array elements with a filter) + * + * ```ts + * import { Optic, Schema } from "effect" + * + * type S = { readonly items: ReadonlyArray } + * + * const _positive = Optic.id() + * .key("items") + * .forEach((n) => n.check(Schema.isGreaterThan(0))) + * + * const getPositive = Optic.getAll(_positive) + * + * console.log(getPositive({ items: [1, -2, 3] })) + * // Output: [1, 3] + * ``` + * + * @see {@link getAll} — extract focused elements + * @see {@link Optional} — the base type + * + * @category Traversal + * @since 4.0.0 + */ +export interface Traversal extends Optional> {} + +class OptionalImpl implements Optional { + readonly node: Node + readonly getResult: (s: S) => Result.Result + readonly replaceResult: (a: A, s: S) => Result.Result + constructor( + node: Node, + getResult: (s: S) => Result.Result, + replaceResult: (a: A, s: S) => Result.Result + ) { + this.node = node + this.getResult = getResult + this.replaceResult = replaceResult + } + replace(a: A, s: S): S { + return Result.getOrElse(this.replaceResult(a, s), () => s) + } + modify(f: (a: A) => A): (s: S) => S { + return (s) => Result.getOrElse(Result.flatMap(this.getResult(s), (a) => this.replaceResult(f(a), s)), () => s) + } + compose(that: any): any { + return make(compose(this.node, that.node)) + } + key(key: PropertyKey): any { + return make(compose(this.node, new PathNode([key]))) + } + optionalKey(key: PropertyKey): any { + return make( + compose( + this.node, + new LensNode( + (s) => s[key], + (a, s) => { + const copy = cloneShallow(s) + if (a === undefined) { + if (Array.isArray(copy) && typeof key === "number") { + copy.splice(key, 1) + } else { + delete copy[key] + } + } else { + copy[key] = a + } + return copy + } + ) + ) + ) + } + check(...checks: readonly [SchemaAST.Check, ...Array>]): any { + return make(compose(this.node, new CheckNode(checks))) + } + refine(refinement: (a: A) => a is B, annotations?: Schema.Annotations.Filter): any { + return make(compose(this.node, new CheckNode([SchemaAST.makeFilterByGuard(refinement, annotations)]))) + } + tag(tag: string): any { + return make( + compose( + this.node, + new PrismNode( + (s) => + s._tag === tag + ? Result.succeed(s) + : Result.fail(`Expected ${format(tag)} tag, got ${format(s._tag)}`), + identity + ) + ) + ) + } + at(key: PropertyKey, ..._rest: Array): any { + const err = Result.fail(`Key ${format(key)} not found`) + return make( + compose( + this.node, + new OptionalNode( + (s) => Object.hasOwn(s, key) ? Result.succeed(s[key]) : err, + (a, s) => { + if (Object.hasOwn(s, key)) { + const copy = cloneShallow(s) + copy[key] = a + return Result.succeed(copy) + } else { + return err + } + } + ) + ) + ) + } + pick(keys: any) { + return this.compose(makeLens(Struct.pick(keys), (p, a) => ({ ...a, ...p }))) + } + omit(keys: any) { + return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...a, ...o }))) + } + notUndefined(): Prism> { + return this.refine(Predicate.isNotUndefined, { expected: "a value other than `undefined`" }) + } + forEach(this: Traversal, f: (iso: Iso) => Optional): Traversal { + const inner = f(id()) + return makeOptional>( + // GET: collect focused Bs + (s) => + Result.map(this.getResult(s), (as) => { + const bs: Array = [] + for (let i = 0; i < as.length; i++) { + const r = inner.getResult(as[i]) + if (Result.isSuccess(r)) bs.push(r.success) + } + return bs + }), + // SET: bs must match the number of focusable elements + (bs, s) => + Result.flatMap(this.getResult(s), (as) => { + // 1) collect focusable indices + const idxs: Array = [] + for (let i = 0; i < as.length; i++) { + if (Result.isSuccess(inner.getResult(as[i]))) idxs.push(i) + } + + // 2) arity check + if (bs.length !== idxs.length) { + return Result.fail( + `each: replacement length mismatch: ${bs.length} !== ${idxs.length}` + ) + } + + // 3) update those indices + const out: Array = as.slice() + for (let k = 0; k < idxs.length; k++) { + const i = idxs[k] + const r = inner.replaceResult(bs[k], as[i]) + if (Result.isFailure(r)) { + return Result.fail(`each: could not set element ${i}`) + } + out[i] = r.success + } + return this.replaceResult(out, s) + }) + ) + } + modifyAll(this: Traversal, f: (a: A) => A): (s: S) => S { + return (s) => + Result.getOrElse( + Result.flatMap(this.getResult(s), (as) => this.replaceResult(as.map(f), s)), + () => s + ) + } +} + +class IsoImpl extends OptionalImpl implements Iso { + readonly get: (s: S) => A + readonly set: (a: A) => S + constructor(node: Node, get: (s: S) => A, set: (a: A) => S) { + super(node, (s) => Result.succeed(get(s)), (a) => Result.succeed(set(a))) + this.get = get + this.set = set + } + override replace(a: A, _: S): S { + return this.set(a) + } + override modify(f: (a: A) => A): (s: S) => S { + return (s) => this.set(f(this.get(s))) + } +} + +class LensImpl extends OptionalImpl implements Lens { + readonly get: (s: S) => A + constructor(node: Node, get: (s: S) => A, replace: (a: A, s: S) => S) { + super(node, (s) => Result.succeed(get(s)), (a, s) => Result.succeed(replace(a, s))) + this.get = get + this.replace = replace + } + override modify(f: (a: A) => A): (s: S) => S { + return (s) => this.replace(f(this.get(s)), s) + } +} + +class PrismImpl extends OptionalImpl implements Prism { + readonly set: (a: A) => S + constructor(node: Node, getResult: (s: S) => Result.Result, set: (a: A) => S) { + super(node, getResult, (a, _) => Result.succeed(set(a))) + this.set = set + } + override replace(a: A, _: S): S { + return this.set(a) + } + override modify(f: (a: A) => A): (s: S) => S { + return (s) => Result.getOrElse(Result.map(this.getResult(s), (a) => this.set(f(a))), () => s) + } +} + +function make(node: Node): any { + const op = recur(node) + switch (op._tag) { + case "IsoNode": + return new IsoImpl(node, op.get, op.set) + case "LensNode": + return new LensImpl(node, op.get, op.set) + case "PrismNode": + return new PrismImpl(node, op.get, op.set) + case "OptionalNode": + return new OptionalImpl(node, op.get, op.set) + } +} + +function cloneShallow(pojo: T): T { + if (Array.isArray(pojo)) return pojo.slice() as T + if (typeof pojo === "object" && pojo !== null) { + const proto = Object.getPrototypeOf(pojo) + if (proto !== Object.prototype && proto !== null) { + throw new Error("Cannot clone object with non-Object constructor or null prototype") + } + return { ...pojo } as T + } + return pojo +} + +type Op = { + readonly _tag: "IsoNode" | "LensNode" | "PrismNode" | "OptionalNode" + readonly get: (s: unknown) => any + readonly set: (a: unknown, s?: unknown) => any +} + +const recur = memoize((node: Node): Op => { + switch (node._tag) { + case "IdentityNode": + return { _tag: "IsoNode", get: identity, set: identity } + case "IsoNode": + case "LensNode": + case "PrismNode": + case "OptionalNode": + return { _tag: node._tag, get: node.get, set: node.set } + case "PathNode": { + return { + _tag: "LensNode", + get: (s: any) => { + const path = node.path + let out: any = s + for (let i = 0, n = path.length; i < n; i++) { + out = out[path[i]] + } + return out + }, + set: (a: any, s: any) => { + const path = node.path + const out = cloneShallow(s) + + let current = out + let i = 0 + for (; i < path.length - 1; i++) { + const key = path[i] + current[key] = cloneShallow(current[key]) + current = current[key] + } + + const finalKey = path[i] + current[finalKey] = a + + return out + } + } + } + case "CheckNode": + return { + _tag: "PrismNode", + get: (s: any) => Result.mapError(SchemaAST.runChecks(node.checks, s), String), + set: identity + } + case "CompositionNode": { + const ops = node.nodes.map(recur) + const _tag = ops.reduce((tag, op) => getCompositionTag(tag, op._tag), "IsoNode") + return { + _tag, + get: (s: any) => { + for (let i = 0; i < ops.length; i++) { + const op = ops[i] + const result = op.get(s) + if (hasFailingGet(op._tag)) { + if (Result.isFailure(result)) { + return result + } + s = result.success + } else { + s = result + } + } + return hasFailingGet(_tag) ? Result.succeed(s) : s + }, + set: (a: any, s: any) => { + const source = s + const len = ops.length + const ss = new Array(len + 1) + ss[0] = s + for (let i = 0; i < len; i++) { + const op = ops[i] + if (hasFailingGet(op._tag)) { + const result = op.get(s) + if (Result.isFailure(result)) { + return _tag === "OptionalNode" ? result : source + } + s = result.success + } else { + s = op.get(s) + } + ss[i + 1] = s + } + for (let i = len - 1; i >= 0; i--) { + const op = ops[i] + if (hasSet(op._tag)) { + a = op.set(a) + } else if (op._tag === "LensNode") { + a = op.set(a, ss[i]) + } else { + const result = op.set(a, ss[i]) + if (Result.isFailure(result)) { + return result + } + a = result.success + } + } + return _tag === "OptionalNode" ? Result.succeed(a) : a + } + } + } + } +}) + +function hasFailingGet(tag: Op["_tag"]): boolean { + return tag === "PrismNode" || tag === "OptionalNode" +} + +function hasSet(tag: Op["_tag"]): boolean { + return tag === "IsoNode" || tag === "PrismNode" +} + +function getCompositionTag(a: Op["_tag"], b: Op["_tag"]): Op["_tag"] { + switch (a) { + case "IsoNode": + return b + case "LensNode": + return hasFailingGet(b) ? "OptionalNode" : "LensNode" + case "PrismNode": + return hasSet(b) ? "PrismNode" : "OptionalNode" + case "OptionalNode": + return "OptionalNode" + } +} +// --------------------------------------------- +// Derived APIs +// --------------------------------------------- + +/** + * Returns a function that extracts all elements focused by a + * {@link Traversal} as a plain mutable array. + * + * **When to use** + * + * Use when you need the focused values as a simple `Array` for further + * processing. + * + * **Details** + * + * - Returns an empty array when the traversal cannot focus. + * - Always returns a fresh array (safe to mutate). + * + * **Example** (Collecting positive numbers) + * + * ```ts + * import { Optic, Schema } from "effect" + * + * type S = { readonly values: ReadonlyArray } + * + * const _pos = Optic.id() + * .key("values") + * .forEach((n) => n.check(Schema.isGreaterThan(0))) + * + * const getPositive = Optic.getAll(_pos) + * + * console.log(getPositive({ values: [3, -1, 5] })) + * // Output: [3, 5] + * + * console.log(getPositive({ values: [-1, -2] })) + * // Output: [] + * ``` + * + * @see {@link Traversal} — the optic type this operates on + * + * @category Traversal + * @since 4.0.0 + */ +export function getAll(traversal: Traversal): (s: S) => Array { + return (s) => + Result.match(traversal.getResult(s), { + onFailure: () => [], + onSuccess: (as) => [...as] + }) +} + +// --------------------------------------------- +// Built-in Optics +// --------------------------------------------- + +const identityIso = make(identityNode) + +/** + * Iso that focuses on the whole value unchanged. + * + * **When to use** + * + * Use when you need to start an optic chain with a focus on the whole value. + * + * **Details** + * + * - `get(s)` returns `s`. + * - `set(a)` returns `a`. + * - Singleton — every call returns the same instance. + * + * **Example** (Starting an optic chain) + * + * ```ts + * import { Optic } from "effect" + * + * type S = { readonly x: number } + * + * const _x = Optic.id().key("x") + * + * console.log(_x.get({ x: 42 })) + * // Output: 42 + * ``` + * + * @see {@link Iso} — the type this function returns + * + * @category Iso + * @since 4.0.0 + */ +export function id(): Iso { + return identityIso +} + +/** + * Iso that converts a `Record` to an array of + * `[key, value]` entries and back. + * + * **When to use** + * + * Use when you want to traverse or manipulate record entries as an array (e.g. + * with `.forEach()`). + * + * **Details** + * + * - `get` uses `Object.entries`. + * - `set` uses `Object.fromEntries`. + * - Round-trip is lossless for `Record`. + * + * **Example** (Traversing record values) + * + * ```ts + * import { Optic, Schema } from "effect" + * + * const _positiveValues = Optic.entries() + * .forEach((entry) => entry.key(1).check(Schema.isGreaterThan(0))) + * + * const inc = _positiveValues.modifyAll((n) => n + 1) + * + * console.log(inc({ a: 0, b: 3, c: -1 })) + * // Output: { a: 0, b: 4, c: -1 } + * ``` + * + * @see {@link Iso} — the type this function returns + * @see {@link id} — identity iso + * + * @category Iso + * @since 4.0.0 + */ +export function entries(): Iso, ReadonlyArray> { + return make(new IsoNode(Object.entries, Object.fromEntries)) +} + +/** + * Prism that focuses on the value inside `Option.Some`. + * + * **When to use** + * + * Use when you have an `Option` and want to read/update the inner value only + * when it is `Some`. + * + * **Details** + * + * - `getResult` fails with an error message when the option is `None`. + * - `set(a)` wraps `a` in `Option.some(a)`. + * + * **Example** (Accessing Some value) + * + * ```ts + * import { Optic, Option, Result } from "effect" + * + * const _some = Optic.id>().compose(Optic.some()) + * + * console.log(Result.isSuccess(_some.getResult(Option.some(42)))) + * // Output: true + * + * console.log(Result.isFailure(_some.getResult(Option.none()))) + * // Output: true + * + * console.log(_some.set(10)) + * // Output: { _tag: "Some", value: 10 } + * ``` + * + * @see {@link none} — focuses on `None` instead + * @see {@link Prism} — the type this function returns + * + * @category Prism + * @since 4.0.0 + */ +export function some(): Prism, A> { + const run = runRefinement(Option.isSome, { expected: "a Some value" }) + return makePrism( + (s) => + Result.mapBoth(run(s), { + onFailure: String, + onSuccess: (s) => s.value + }), + Option.some + ) +} + +/** + * Prism that focuses on `Option.None`, exposing `undefined`. + * + * **When to use** + * + * Use when you want to match or construct `None` values within an optic chain. + * + * **Details** + * + * - `getResult` succeeds with `undefined` when the option is `None`. + * - `getResult` fails when the option is `Some`. + * - `set(undefined)` produces `Option.none()`. + * + * **Example** (Matching None) + * + * ```ts + * import { Optic, Option, Result } from "effect" + * + * const _none = Optic.id>().compose(Optic.none()) + * + * console.log(Result.isSuccess(_none.getResult(Option.none()))) + * // Output: true + * + * console.log(Result.isFailure(_none.getResult(Option.some(1)))) + * // Output: true + * ``` + * + * @see {@link some} — focuses on `Some` instead + * @see {@link Prism} — the type this function returns + * + * @category Prism + * @since 4.0.0 + */ +export function none(): Prism, undefined> { + const run = runRefinement(Option.isNone, { expected: "a None value" }) + return makePrism( + (s) => + Result.mapBoth(run(s), { + onFailure: String, + onSuccess: () => undefined + }), + () => Option.none() + ) +} + +/** + * Prism that focuses on the success value of a `Result`. + * + * **When to use** + * + * Use when you have a `Result` and want to read/update `A` only when it + * is a `Success`. + * + * **Details** + * + * - `getResult` fails when the result is a `Failure`. + * - `set(a)` produces `Result.succeed(a)`. + * + * **Example** (Accessing success) + * + * ```ts + * import { Optic, Result } from "effect" + * + * const _ok = Optic.id>().compose(Optic.success()) + * + * console.log(Result.isSuccess(_ok.getResult(Result.succeed(42)))) + * // Output: true + * + * console.log(Result.isFailure(_ok.getResult(Result.fail("err")))) + * // Output: true + * ``` + * + * @see {@link failure} — focuses on the failure side + * @see {@link Prism} — the type this function returns + * + * @category Prism + * @since 4.0.0 + */ +export function success(): Prism, A> { + const run = runRefinement(Result.isSuccess, { expected: "a Result.Success value" }) + return makePrism( + (s) => + Result.mapBoth(run(s), { + onFailure: String, + onSuccess: (s) => s.success + }), + Result.succeed + ) +} + +/** + * Prism that focuses on the failure value of a `Result`. + * + * **When to use** + * + * Use when you have a `Result` and want to read/update `E` only when it + * is a `Failure`. + * + * **Details** + * + * - `getResult` fails when the result is a `Success`. + * - `set(e)` produces `Result.fail(e)`. + * + * **Example** (Accessing failure) + * + * ```ts + * import { Optic, Result } from "effect" + * + * const _err = Optic.id>().compose(Optic.failure()) + * + * console.log(Result.isSuccess(_err.getResult(Result.fail("oops")))) + * // Output: true + * + * console.log(Result.isFailure(_err.getResult(Result.succeed(42)))) + * // Output: true + * ``` + * + * @see {@link success} — focuses on the success side + * @see {@link Prism} — the type this function returns + * + * @category Prism + * @since 4.0.0 + */ +export function failure(): Prism, E> { + const run = runRefinement(Result.isFailure, { expected: "a Result.Failure value" }) + return makePrism( + (s) => + Result.mapBoth(run(s), { + onFailure: String, + onSuccess: (s) => s.failure + }), + Result.fail + ) +} + +function runRefinement( + refinement: (e: E) => e is T, + annotations?: Schema.Annotations.Filter +): (e: E) => Result.Result { + return (e) => SchemaAST.runChecks([SchemaAST.makeFilterByGuard(refinement, annotations)], e) as any +} diff --git a/repos/effect/packages/effect/src/Option.ts b/repos/effect/packages/effect/src/Option.ts new file mode 100644 index 0000000..e292f0f --- /dev/null +++ b/repos/effect/packages/effect/src/Option.ts @@ -0,0 +1,2664 @@ +/** + * Models a value that may be present or absent. + * + * An `Option` is `Some` when a value is available and `None` when it is + * not. This lets code handle missing values explicitly instead of relying on + * `null` or `undefined`. The module includes helpers for creating, checking, + * transforming, combining, and extracting optional values, plus conversions to + * and from common nullable or result-like shapes. It also includes `Option.gen` + * for writing small generator-based computations that stop at the first `None`. + * + * @since 2.0.0 + */ +import * as Combiner from "./Combiner.ts" +import * as Equal from "./Equal.ts" +import * as Equivalence from "./Equivalence.ts" +import type * as Filter from "./Filter.ts" +import type { LazyArg } from "./Function.ts" +import { constNull, constUndefined, dual, identity } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as doNotation from "./internal/doNotation.ts" +import * as option from "./internal/option.ts" +import * as result from "./internal/result.ts" +import type { Order } from "./Order.ts" +import * as order from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Predicate, Refinement } from "./Predicate.ts" +import { isFunction } from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" +import type { Result } from "./Result.ts" +import type { Covariant, NoInfer, NotFunction } from "./Types.ts" +import type * as Unify from "./Unify.ts" +import type * as Gen from "./Utils.ts" + +const TypeId = "~effect/data/Option" + +/** + * The `Option` data type represents optional values. An `Option` is either + * `Some`, containing a value of type `A`, or `None`, representing absence. + * + * **When to use** + * + * Use to represent initial values that may not yet exist + * - Returning from partial functions (not defined for all inputs) + * - Managing optional fields in data structures + * + * @see {@link some} for creating a `Some` + * @see {@link none} for creating a `None` + * @see {@link match} for pattern matching + * + * @category models + * @since 2.0.0 + */ +export type Option = None | Some + +/** + * Represents the absence of a value within an {@link Option}. + * + * **When to use** + * + * Use as a type guard target when narrowing via {@link isNone} + * + * **Details** + * + * - `_tag` is always `"None"` + * - Implements `Pipeable`, `Inspectable`, and structural equality + * + * @see {@link isNone} to check if an `Option` is `None` + * @see {@link none} to construct a `None` + * + * @category models + * @since 2.0.0 + */ +export interface None extends Pipeable, Inspectable { + readonly _tag: "None" + readonly _op: "None" + readonly valueOrUndefined: undefined + readonly [TypeId]: { + readonly _A: Covariant + } + [Symbol.iterator](): OptionIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: OptionUnify + [Unify.ignoreSymbol]?: OptionUnifyIgnore +} + +/** + * Iterator protocol used to yield an `Option` inside {@link gen}, returning the + * contained value type back to the generator. + * + * **When to use** + * + * Use when defining or typing `[Symbol.iterator]()` for `Option` values so + * `yield*` can pass the contained value type back into `Option.gen`. + * + * @see {@link gen} for writing generator-based `Option` code that consumes this iterator protocol + * + * @category generators + * @since 4.0.0 + */ +export interface OptionIterator> { + next( + ...args: ReadonlyArray + ): IteratorResult> +} + +/** + * Represents the presence of a value within an {@link Option}. + * + * **When to use** + * + * Use as a type guard target when narrowing via {@link isSome} + * - Access the inner value via `.value` + * + * **Details** + * + * - `_tag` is always `"Some"` + * - `.value` holds the contained value of type `A` + * - Implements `Pipeable`, `Inspectable`, and structural equality + * + * @see {@link isSome} to check if an `Option` is `Some` + * @see {@link some} to construct a `Some` + * + * @category models + * @since 2.0.0 + */ +export interface Some extends Pipeable, Inspectable { + readonly _tag: "Some" + readonly _op: "Some" + readonly value: A + readonly valueOrUndefined: A + readonly [TypeId]: { + readonly _A: Covariant + } + [Symbol.iterator](): OptionIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: OptionUnify + [Unify.ignoreSymbol]?: OptionUnifyIgnore +} + +/** + * Type-level unification support for `Option` values. + * + * **When to use** + * + * Use when extending Effect's type-level unification support for `Option`. + * + * **Details** + * + * This is used by Effect's `Unify` machinery to preserve the contained value + * type when generic code returns or combines `Option` values. Users normally + * do not need to reference this interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface OptionUnify { + Option?: () => A[Unify.typeSymbol] extends Option | infer _ ? Option : never +} + +/** + * Namespace containing utility types for `Option`. + * + * **When to use** + * + * Use to access type-level helpers associated with `Option`. + * + * @since 2.0.0 + */ +export declare namespace Option { + /** + * Extracts the type of the value contained in an `Option`. + * + * **When to use** + * + * Use to infer the inner value type from an existing `Option` type. + * + * **Example** (Extracting the value type) + * + * ```ts + * import type { Option } from "effect" + * + * declare const myOption: Option.Option + * + * // ┌─── string + * // ▼ + * type MyType = Option.Option.Value + * ``` + * + * @category Type-level Utils + * @since 2.0.0 + */ + export type Value> = [T] extends [Option] ? _A : never +} + +/** + * Marker interface used by Effect's `Unify` machinery for `Option` values. + * + * **When to use** + * + * Use when marking generic code so `Option` unification should be ignored. + * + * **Details** + * + * This supports type-level unification behavior for `Option`. Users normally + * do not need to reference this interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface OptionUnifyIgnore {} + +/** + * Type lambda interface for higher-kinded type encodings with `Option`. + * + * **When to use** + * + * Use when defining higher-kinded abstractions that must accept optional-value + * types as one of their type-lambda inputs. + * + * @category type lambdas + * @since 2.0.0 + */ +export interface OptionTypeLambda extends TypeLambda { + readonly type: Option +} + +/** + * Creates an `Option` representing the absence of a value. + * + * **When to use** + * + * Use to represent a missing or uninitialized value, such as returning "no + * result" from a function. + * + * **Details** + * + * - Returns `Option`, which is a subtype of `Option` for any `A` + * - Always returns the same singleton instance + * + * **Example** (Creating an empty Option) + * + * ```ts + * import { Option } from "effect" + * + * // ┌─── Option + * // ▼ + * const noValue = Option.none() + * + * console.log(noValue) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link some} for the opposite operation. + * + * @category constructors + * @since 2.0.0 + */ +export const none = (): Option => option.none + +/** + * Wraps the given value into an `Option` to represent its presence. + * + * **When to use** + * + * Use to wrap a known present value as `Option` + * - Returning a successful result from a partial function + * + * **Details** + * + * - Always returns `Some` + * - Does not filter `null` or `undefined`; use {@link fromNullishOr} for that + * + * **Example** (Wrapping a value) + * + * ```ts + * import { Option } from "effect" + * + * // ┌─── Option + * // ▼ + * const value = Option.some(1) + * + * console.log(value) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @see {@link none} for the opposite operation. + * + * @category constructors + * @since 2.0.0 + */ +export const some: (value: A) => Option = option.some + +/** + * Determines whether the given value is an `Option`. + * + * **When to use** + * + * Use to validate unknown values at runtime boundaries, such as type-narrowing + * in union types. + * + * **Details** + * + * - Returns `true` for both `Some` and `None` instances + * - Acts as a type guard, narrowing the input to `Option` + * + * **Example** (Checking if a value is an Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isOption(Option.some(1))) + * // Output: true + * + * console.log(Option.isOption(Option.none())) + * // Output: true + * + * console.log(Option.isOption({})) + * // Output: false + * ``` + * + * @see {@link isNone} to check for `None` specifically + * @see {@link isSome} to check for `Some` specifically + * + * @category guards + * @since 2.0.0 + */ +export const isOption: (input: unknown) => input is Option = option.isOption + +/** + * Checks whether an `Option` is `None` (absent). + * + * **When to use** + * + * Use when you need to branch on an absent `Option` before accessing `.value`. + * + * **Details** + * + * - Acts as a type guard, narrowing to `None` + * + * **Example** (Checking for None) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isNone(Option.some(1))) + * // Output: false + * + * console.log(Option.isNone(Option.none())) + * // Output: true + * ``` + * + * @see {@link isSome} for the opposite check. + * + * @category guards + * @since 2.0.0 + */ +export const isNone: (self: Option) => self is None = option.isNone + +/** + * Checks whether an `Option` contains a value (`Some`). + * + * **When to use** + * + * Use when you need to branch on a present `Option` before accessing `.value`. + * + * **Details** + * + * - Acts as a type guard, narrowing to `Some` + * + * **Example** (Checking for Some) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.isSome(Option.some(1))) + * // Output: true + * + * console.log(Option.isSome(Option.none())) + * // Output: false + * ``` + * + * @see {@link isNone} for the opposite check. + * + * @category guards + * @since 2.0.0 + */ +export const isSome: (self: Option) => self is Some = option.isSome + +/** + * Pattern-matches on an `Option`, handling both `None` and `Some` cases. + * + * **When to use** + * + * Use when you need to handle both `Some` and `None` in one expression and + * transform an `Option` into a plain value. + * + * **Details** + * + * - If `None`, calls `onNone` and returns its result + * - If `Some`, calls `onSome` with the value and returns its result + * - Supports the `dual` API (data-last and data-first) + * + * **Example** (Matching on an Option) + * + * ```ts + * import { Option } from "effect" + * + * const message = Option.match(Option.some(1), { + * onNone: () => "Option is empty", + * onSome: (value) => `Option has a value: ${value}` + * }) + * + * console.log(message) + * // Output: "Option has a value: 1" + * ``` + * + * @see {@link getOrElse} for unwrapping with a default + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): (self: Option) => B | C + (self: Option, options: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): B | C +} = dual( + 2, + (self: Option, { onNone, onSome }: { + readonly onNone: LazyArg + readonly onSome: (a: A) => C + }): B | C => isNone(self) ? onNone() : onSome(self.value) +) + +/** + * Converts an `Option`-returning function into a type guard (refinement). + * + * **When to use** + * + * Use when you need to turn an `Option`-returning parser into a type-narrowing + * predicate, such as for `Array.prototype.filter`. + * + * **Details** + * + * - Returns `true` when the original function returns `Some` + * - Returns `false` when the original function returns `None` + * - Narrows the input type to `B` on success + * + * **Example** (Converting a parser to a type guard) + * + * ```ts + * import { Option } from "effect" + * + * type MyData = string | number + * + * const parseString = (data: MyData): Option.Option => + * typeof data === "string" ? Option.some(data) : Option.none() + * + * // ┌─── (a: MyData) => a is string + * // ▼ + * const isString = Option.toRefinement(parseString) + * + * console.log(isString("a")) + * // Output: true + * + * console.log(isString(1)) + * // Output: false + * ``` + * + * @see {@link liftPredicate} for the reverse direction + * + * @category converting + * @since 2.0.0 + */ +export const toRefinement = (f: (a: A) => Option): (a: A) => a is B => (a: A): a is B => isSome(f(a)) + +/** + * Wraps the first element of an `Iterable` in a `Some`, or returns `None` if + * the iterable is empty. + * + * **When to use** + * + * Use when you need to safely extract the head of a collection, including + * generators or lazy iterables. + * + * **Details** + * + * - Only consumes the first element; does not iterate the rest + * - Returns `None` for empty iterables + * + * **Example** (Getting the first element) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromIterable([1, 2, 3])) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(Option.fromIterable([])) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link toArray} for the inverse direction + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (collection: Iterable): Option => { + for (const a of collection) { + return some(a) + } + return none() +} + +/** + * Converts a `Result` into an `Option`, keeping only the success value. + * + * **When to use** + * + * Use when you need to discard a `Result` failure and keep only the success + * value as an `Option`. + * + * **Details** + * + * - `Success` becomes `Some` with the success value + * - `Failure` becomes `None` and the failure value is discarded + * + * **Example** (Extracting the success side) + * + * ```ts + * import { Option, Result } from "effect" + * + * console.log(Option.getSuccess(Result.succeed("ok"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'ok' } + * + * console.log(Option.getSuccess(Result.fail("err"))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link getFailure} for the opposite operation. + * + * @category converting + * @since 4.0.0 + */ +export const getSuccess: (self: Result) => Option = result.getSuccess + +/** + * Converts a `Result` into an `Option`, keeping only the failure value. + * + * **When to use** + * + * Use when you need to discard a `Result` success and keep only the failure + * value as an `Option`. + * + * **Details** + * + * - `Failure` becomes `Some` with the failure value + * - `Success` becomes `None` and the success value is discarded + * + * **Example** (Extracting the failure side) + * + * ```ts + * import { Option, Result } from "effect" + * + * console.log(Option.getFailure(Result.succeed("ok"))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.getFailure(Result.fail("err"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'err' } + * ``` + * + * @see {@link getSuccess} for the opposite operation. + * + * @category converting + * @since 4.0.0 + */ +export const getFailure: (self: Result) => Option = result.getFailure + +/** + * Extracts the value from a `Some`, or evaluates a fallback thunk on `None`. + * + * **When to use** + * + * Use when providing a default value for an absent `Option` + * - Unwrapping with lazy evaluation of the fallback + * + * **Details** + * + * - `Some` → returns the inner value + * - `None` → calls `onNone()` and returns its result + * - `onNone` is only called when needed (lazy) + * + * **Example** (Unwrapping with a fallback) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.some(1).pipe(Option.getOrElse(() => 0))) + * // Output: 1 + * + * console.log(Option.none().pipe(Option.getOrElse(() => 0))) + * // Output: 0 + * ``` + * + * @see {@link getOrNull} to fall back to `null` + * @see {@link getOrUndefined} to fall back to `undefined` + * @see {@link getOrThrow} to throw on `None` + * + * @category getters + * @since 2.0.0 + */ +export const getOrElse: { + (onNone: LazyArg): (self: Option) => B | A + (self: Option, onNone: LazyArg): A | B +} = dual( + 2, + (self: Option, onNone: LazyArg): A | B => isNone(self) ? onNone() : self.value +) + +/** + * Returns the fallback `Option` if `self` is `None`; otherwise returns `self`. + * + * **When to use** + * + * Use when you need a lazy fallback `Option`, such as when building priority + * chains of optional values. + * + * **Details** + * + * - `Some` → returns `self` unchanged + * - `None` → evaluates and returns `that()` + * - `that` is lazily evaluated + * + * **Example** (Providing a fallback Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.none().pipe(Option.orElse(() => Option.some("b")))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'b' } + * + * console.log(Option.some("a").pipe(Option.orElse(() => Option.some("b")))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * ``` + * + * @see {@link orElseSome} to wrap the fallback value in `Some` automatically + * @see {@link firstSomeOf} to pick the first `Some` from a collection + * + * @category error handling + * @since 2.0.0 + */ +export const orElse: { + (that: LazyArg>): (self: Option) => Option + (self: Option, that: LazyArg>): Option +} = dual( + 2, + (self: Option, that: LazyArg>): Option => isNone(self) ? that() : self +) + +/** + * Returns `Some` of the fallback value if `self` is `None`; otherwise returns + * `self`. + * + * **When to use** + * + * Use when providing a default plain value (not an `Option`) as fallback + * + * **Details** + * + * - `Some` → returns `self` unchanged + * - `None` → calls `onNone()`, wraps result in `Some`, and returns it + * + * **Example** (Providing a fallback value) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.none().pipe(Option.orElseSome(() => "b"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'b' } + * + * console.log(Option.some("a").pipe(Option.orElseSome(() => "b"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'a' } + * ``` + * + * @see {@link orElse} when the fallback is itself an `Option` + * + * @category error handling + * @since 2.0.0 + */ +export const orElseSome: { + (onNone: LazyArg): (self: Option) => Option + (self: Option, onNone: LazyArg): Option +} = dual( + 2, + (self: Option, onNone: LazyArg): Option => isNone(self) ? some(onNone()) : self +) + +/** + * Returns the first available value and marks whether it came from the fallback. + * + * **When to use** + * + * Use when you need to know whether a present value came from the primary or + * fallback `Option`. + * + * **Details** + * + * - `self` is `Some` → `Some(Result.fail(value))` (value from primary) + * - `self` is `None`, `that()` is `Some` → `Some(Result.succeed(value))` (value from fallback) + * - Both `None` → `None` + * + * **Example** (Tracking value source) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.orElseResult(Option.some("primary"), () => Option.some("fallback"))) + * // Output: { _id: 'Option', _tag: 'Some', value: { _tag: 'Failure', value: 'primary' } } + * + * console.log(Option.orElseResult(Option.none(), () => Option.some("fallback"))) + * // Output: { _id: 'Option', _tag: 'Some', value: { _tag: 'Success', value: 'fallback' } } + * ``` + * + * @see {@link orElse} for the simpler variant without source tracking + * + * @category error handling + * @since 4.0.0 + */ +export const orElseResult: { + (that: LazyArg>): (self: Option) => Option> + (self: Option, that: LazyArg>): Option> +} = dual( + 2, + (self: Option, that: LazyArg>): Option> => + isNone(self) ? map(that(), result.succeed) : map(self, result.fail) +) + +/** + * Returns the first `Some` found in an iterable of `Option`s, or `None` if + * all are `None`. + * + * **When to use** + * + * Use when you need the first available `Some` value from a priority list. + * + * **Details** + * + * - Short-circuits on the first `Some` + * - Returns `None` only when every element is `None` + * + * **Example** (Finding the first Some) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.firstSomeOf([ + * Option.none(), + * Option.some(1), + * Option.some(2) + * ])) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @see {@link orElse} for a two-option fallback + * + * @category error handling + * @since 2.0.0 + */ +export const firstSomeOf = > = Iterable>>( + collection: C +): [C] extends [Iterable>] ? Option : never => { + let out: Option = none() + for (out of collection) { + if (isSome(out)) { + return out as any + } + } + return out as any +} + +/** + * Converts a nullable value (`null` or `undefined`) into an `Option`. + * + * **When to use** + * + * Use when you need JavaScript nullish values to become absence at an API + * boundary while all other values, including falsy ones, remain present. + * + * **Details** + * + * - `null` or `undefined` → `None` + * - Any other value → `Some` (typed as `NonNullable`) + * + * **Example** (Converting nullable values to an Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromNullishOr(undefined)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromNullishOr(null)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromNullishOr(1)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ``` + * + * @see {@link fromNullOr} to only treat `null` as absent + * @see {@link fromUndefinedOr} to only treat `undefined` as absent + * @see {@link liftNullishOr} to lift a nullable-returning function + * + * @category converting + * @since 4.0.0 + */ +export const fromNullishOr = ( + a: A +): Option> => (a == null ? none() : some(a as NonNullable)) + +/** + * Converts a possibly `undefined` value into an `Option`, leaving `null` + * as a valid `Some`. + * + * **When to use** + * + * Use when you want to treat only `undefined` as absent while preserving `null` + * as a meaningful value. + * + * **Details** + * + * - `undefined` → `None` + * - Any other value (including `null`) → `Some` + * + * **Example** (Converting possibly undefined values to an Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromUndefinedOr(undefined)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromUndefinedOr(null)) + * // Output: { _id: 'Option', _tag: 'Some', value: null } + * + * console.log(Option.fromUndefinedOr(42)) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @see {@link fromNullishOr} to treat both `null` and `undefined` as absent + * @see {@link fromNullOr} to only treat `null` as absent + * + * @category converting + * @since 4.0.0 + */ +export const fromUndefinedOr = ( + a: A +): Option> => (a === undefined ? none() : some(a as Exclude)) + +/** + * Converts a possibly `null` value into an `Option`, leaving `undefined` + * as a valid `Some`. + * + * **When to use** + * + * Use when you want to treat only `null` as absent while preserving + * `undefined` as a meaningful value. + * + * **Details** + * + * - `null` → `None` + * - Any other value (including `undefined`) → `Some` + * + * **Example** (Converting possibly null values to an Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.fromNullOr(null)) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(Option.fromNullOr(undefined)) + * // Output: { _id: 'Option', _tag: 'Some', value: undefined } + * + * console.log(Option.fromNullOr(42)) + * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * ``` + * + * @see {@link fromNullishOr} to treat both `null` and `undefined` as absent + * @see {@link fromUndefinedOr} to only treat `undefined` as absent + * + * @category converting + * @since 4.0.0 + */ +export const fromNullOr = ( + a: A +): Option> => (a === null ? none() : some(a as Exclude)) + +/** + * Lifts a function that may return `null` or `undefined` into one that returns + * an `Option`. + * + * **When to use** + * + * Use to wrap existing nullable-returning functions for use in `Option` pipelines + * + * **Details** + * + * - Calls the original function with the given arguments + * - Wraps the result via {@link fromNullishOr} + * + * **Example** (Lifting a parser) + * + * ```ts + * import { Option } from "effect" + * + * const parse = (s: string): number | undefined => { + * const n = parseFloat(s) + * return isNaN(n) ? undefined : n + * } + * + * const parseOption = Option.liftNullishOr(parse) + * + * console.log(parseOption("1")) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parseOption("not a number")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link fromNullishOr} for converting a single value + * @see {@link liftThrowable} for functions that throw instead + * + * @category converting + * @since 4.0.0 + */ +export const liftNullishOr = , B>( + f: (...a: A) => B +): (...a: A) => Option> => +(...a) => fromNullishOr(f(...a)) + +/** + * Extracts the value from a `Some`, or returns `null` for `None`. + * + * **When to use** + * + * Use when you need to pass absent `Option` values to APIs that expect `null`. + * + * **Details** + * + * - `Some` → the inner value + * - `None` → `null` + * + * **Example** (Unwrapping to null) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrNull(Option.some(1))) + * // Output: 1 + * + * console.log(Option.getOrNull(Option.none())) + * // Output: null + * ``` + * + * @see {@link getOrUndefined} to return `undefined` instead + * @see {@link getOrElse} for a custom fallback + * + * @category getters + * @since 2.0.0 + */ +export const getOrNull: (self: Option) => A | null = getOrElse(constNull) + +/** + * Extracts the value from a `Some`, or returns `undefined` for `None`. + * + * **When to use** + * + * Use when you need to pass absent `Option` values to APIs that expect + * `undefined`. + * + * **Details** + * + * - `Some` → the inner value + * - `None` → `undefined` + * + * **Example** (Unwrapping to undefined) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrUndefined(Option.some(1))) + * // Output: 1 + * + * console.log(Option.getOrUndefined(Option.none())) + * // Output: undefined + * ``` + * + * @see {@link getOrNull} to return `null` instead + * @see {@link getOrElse} for a custom fallback + * + * @category getters + * @since 2.0.0 + */ +export const getOrUndefined: (self: Option) => A | undefined = getOrElse(constUndefined) + +/** + * Lifts a function that may throw into one that returns an `Option`. + * + * **When to use** + * + * Use to wrap exception-throwing APIs (e.g. `JSON.parse`) for safe usage + * + * **Details** + * + * - If the function returns normally → `Some` with the result + * - If the function throws → `None` (exception is swallowed) + * + * **Example** (Lifting JSON.parse) + * + * ```ts + * import { Option } from "effect" + * + * const parse = Option.liftThrowable(JSON.parse) + * + * console.log(parse("1")) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parse("")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link liftNullishOr} for nullable-returning functions + * + * @category converting + * @since 2.0.0 + */ +export const liftThrowable = , B>( + f: (...a: A) => B +): (...a: A) => Option => +(...a) => { + try { + return some(f(...a)) + } catch { + return none() + } +} + +/** + * Extracts the value from a `Some`, or throws a custom error for `None`. + * + * **When to use** + * + * Use when you need fail-fast unwrapping of an `Option` for unexpected absence + * and want to provide a descriptive debugging error. + * + * **Details** + * + * - `Some` → returns the inner value + * - `None` → throws the value returned by `onNone()` + * + * **Example** (Throwing a custom error) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrThrowWith(Option.some(1), () => new Error("missing"))) + * // Output: 1 + * + * Option.getOrThrowWith(Option.none(), () => new Error("missing")) + * // throws Error: missing + * ``` + * + * @see {@link getOrThrow} for a version with a default error + * @see {@link getOrElse} for a non-throwing alternative + * + * @category converting + * @since 2.0.0 + */ +export const getOrThrowWith: { + (onNone: () => unknown): (self: Option) => A + (self: Option, onNone: () => unknown): A +} = dual(2, (self: Option, onNone: () => unknown): A => { + if (isSome(self)) { + return self.value + } + throw onNone() +}) + +/** + * Extracts the value from a `Some`, or throws a default `Error` for `None`. + * + * **When to use** + * + * Use when you need quick fail-fast unwrapping of an `Option` and a generic + * error is acceptable. + * + * **Details** + * + * - `Some` → returns the inner value + * - `None` → throws `new Error("getOrThrow called on a None")` + * + * **Example** (Throwing a default error) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.getOrThrow(Option.some(1))) + * // Output: 1 + * + * Option.getOrThrow(Option.none()) + * // throws Error: getOrThrow called on a None + * ``` + * + * @see {@link getOrThrowWith} for a custom error + * @see {@link getOrElse} for a non-throwing alternative + * + * @category converting + * @since 2.0.0 + */ +export const getOrThrow: (self: Option) => A = getOrThrowWith(() => new Error("getOrThrow called on a None")) + +/** + * Transforms the value inside a `Some` using the provided function, leaving + * `None` unchanged. + * + * **When to use** + * + * Use to apply a pure transformation to an `Option`'s present value, especially + * when chaining transformations in a pipeline. + * + * **Details** + * + * - `Some` → applies `f` and wraps the result in a new `Some` + * - `None` → returns `None` unchanged + * + * **Example** (Mapping over an Option) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.map(Option.some(2), (n) => n * 2)) + * // Output: { _id: 'Option', _tag: 'Some', value: 4 } + * + * console.log(Option.map(Option.none(), (n: number) => n * 2)) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link flatMap} when `f` returns an `Option` + * @see {@link as} to replace the value with a constant + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => B): (self: Option) => Option + (self: Option, f: (a: A) => B): Option +} = dual( + 2, + (self: Option, f: (a: A) => B): Option => isNone(self) ? none() : some(f(self.value)) +) + +/** + * Replaces the value inside a `Some` with a constant, leaving `None` unchanged. + * + * **When to use** + * + * Use when you need to replace a present `Option` value while preserving + * whether it was `Some` or `None`. + * + * **Example** (Replacing a value) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.as(Option.some(42), "new value")) + * // Output: { _id: 'Option', _tag: 'Some', value: 'new value' } + * + * console.log(Option.as(Option.none(), "new value")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link asVoid} to replace with `undefined` + * @see {@link map} for a general transformation + * + * @category mapping + * @since 2.0.0 + */ +export const as: { + (b: B): (self: Option) => Option + (self: Option, b: B): Option +} = dual(2, (self: Option, b: B): Option => map(self, () => b)) + +/** + * Replaces the value inside a `Some` with `void` (`undefined`), leaving `None` + * unchanged. + * + * **When to use** + * + * Use when you need to discard a present `Option` value while preserving + * whether it was `Some` or `None`. + * + * **Example** (Voiding the value) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.asVoid(Option.some(42))) + * // Output: { _id: 'Option', _tag: 'Some', value: undefined } + * + * console.log(Option.asVoid(Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link as} to replace with a specific constant + * + * @category mapping + * @since 2.0.0 + */ +export const asVoid: <_>(self: Option<_>) => Option = as(undefined) + +const void_: Option = some(undefined) +export { + /** + * Provides a pre-built `Some(undefined)` constant. + * + * **When to use** + * + * Use to return a "success with no meaningful value" from an `Option`-returning function + * + * **Example** (Referencing Option.void) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.void) + * // Output: { _id: 'Option', _tag: 'Some', value: undefined } + * ``` + * + * @see {@link asVoid} to convert an existing `Option` to `Option` + * + * @category constructors + * @since 2.0.0 + */ + void_ as void +} + +/** + * Applies a function that returns an `Option` to the value of a `Some`, + * flattening the result. Returns `None` if the input is `None`. + * + * **When to use** + * + * Use when you need to chain dependent `Option` computations where each step + * may return `None`. + * + * **Details** + * + * - `Some` → applies `f` to the value and returns its `Option` result + * - `None` → returns `None` without calling `f` + * - Equivalent to `map` followed by {@link flatten} + * + * **Example** (Chaining optional lookups) + * + * ```ts + * import { Option } from "effect" + * + * interface User { + * readonly name: string + * readonly address: Option.Option<{ readonly street: Option.Option }> + * } + * + * const user: User = { + * name: "John", + * address: Option.some({ street: Option.some("123 Main St") }) + * } + * + * const street = user.address.pipe( + * Option.flatMap((addr) => addr.street) + * ) + * + * console.log(street) + * // Output: { _id: 'Option', _tag: 'Some', value: '123 Main St' } + * ``` + * + * @see {@link map} when `f` returns a plain value + * @see {@link andThen} for a more flexible variant + * @see {@link flatten} to unwrap a nested `Option>` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + (f: (a: A) => Option): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option +} = dual( + 2, + (self: Option, f: (a: A) => Option): Option => isNone(self) ? none() : f(self.value) +) + +/** + * Chains a second computation onto an `Option`. The second value can be a + * plain value, an `Option`, or a function returning either. + * + * **When to use** + * + * Use when you need to chain an `Option` with a next step that may be another + * `Option`, a plain value, or a function. + * + * **Details** + * + * - If `self` is `None`, returns `None` immediately + * - If `f` is a function, calls it with the `Some` value + * - If `f` returns an `Option`, returns it as-is; if a plain value, wraps in `Some` + * - If `f` is not a function, uses it directly (same wrapping rules) + * + * **Example** (Chaining with andThen) + * + * ```ts + * import { Option } from "effect" + * + * // Chain with a function returning Option + * console.log(Option.andThen(Option.some(5), (x) => Option.some(x * 2))) + * // Output: { _id: 'Option', _tag: 'Some', value: 10 } + * + * // Chain with a static value + * console.log(Option.andThen(Option.some(5), "hello")) + * // Output: { _id: 'Option', _tag: 'Some', value: "hello" } + * + * // Chain with None - skips + * console.log(Option.andThen(Option.none(), (x) => Option.some(x * 2))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link flatMap} for the standard monadic bind + * @see {@link map} when you always return a plain value + * + * @category sequencing + * @since 2.0.0 + */ +export const andThen: { + (f: (a: A) => Option): (self: Option) => Option + (f: Option): (self: Option) => Option + (f: (a: A) => B): (self: Option) => Option + (f: NotFunction): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option + (self: Option, f: Option): Option + (self: Option, f: (a: A) => B): Option + (self: Option, f: NotFunction): Option +} = dual( + 2, + (self: Option, f: (a: A) => Option | Option): Option => + flatMap(self, (a) => { + const b = isFunction(f) ? f(a) : f + return isOption(b) ? b : some(b) + }) +) + +/** + * Combines {@link flatMap} with {@link fromNullishOr}: applies a function that + * may return `null`/`undefined` to the value of a `Some`. + * + * **When to use** + * + * Use when you need to chain optional computations that use `null` or + * `undefined` instead of `Option`, such as nested property access. + * + * **Details** + * + * - `None` → `None` + * - `Some` → applies `f`, then wraps via {@link fromNullishOr} + * + * **Example** (Navigating optional properties) + * + * ```ts + * import { Option } from "effect" + * + * interface Employee { + * company?: { address?: { street?: { name?: string } } } + * } + * + * const emp: Employee = { + * company: { address: { street: { name: "high street" } } } + * } + * + * console.log( + * Option.some(emp).pipe( + * Option.flatMapNullishOr((e) => e.company?.address?.street?.name) + * ) + * ) + * // Output: { _id: 'Option', _tag: 'Some', value: 'high street' } + * ``` + * + * @see {@link flatMap} when the function already returns `Option` + * @see {@link fromNullishOr} for single-value conversion + * + * @category sequencing + * @since 4.0.0 + */ +export const flatMapNullishOr: { + (f: (a: A) => B): (self: Option) => Option> + (self: Option, f: (a: A) => B): Option> +} = dual( + 2, + (self: Option, f: (a: A) => B): Option> => + isNone(self) ? none() : fromNullishOr(f(self.value)) +) + +/** + * Flattens a nested `Option>` into `Option`. + * + * **When to use** + * + * Use when you need to remove one layer of nested `Option`. + * + * **Details** + * + * - `Some(Some(value))` → `Some(value)` + * - `Some(None)` → `None` + * - `None` → `None` + * + * **Example** (Flattening nested Options) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.flatten(Option.some(Option.some("value")))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'value' } + * + * console.log(Option.flatten(Option.some(Option.none()))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link flatMap} which is `map` + `flatten` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatten: (self: Option>) => Option = flatMap(identity) + +/** + * Sequences two `Option`s, keeping the value from the second if both are `Some`. + * + * **When to use** + * + * Use when you need two `Option` values to both be `Some`, then keep only the + * second value. + * + * **Details** + * + * - Both `Some` → returns `that` + * - Either `None` → returns `None` + * + * **Example** (Keeping the second value) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.zipRight(Option.some(1), Option.some("hello"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } + * + * console.log(Option.zipRight(Option.none(), Option.some("hello"))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link zipLeft} to keep the first value instead + * @see {@link zipWith} to combine both values + * + * @category zipping + * @since 2.0.0 + */ +export const zipRight: { + (that: Option): <_>(self: Option<_>) => Option + (self: Option, that: Option): Option +} = dual(2, (self: Option, that: Option): Option => flatMap(self, () => that)) + +/** + * Sequences two `Option`s, keeping the value from the first if both are `Some`. + * + * **When to use** + * + * Use when you need two `Option` values to both be `Some`, then keep only the + * first value. + * + * **Details** + * + * - Both `Some` → returns `self` + * - Either `None` → returns `None` + * + * **Example** (Keeping the first value) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.zipLeft(Option.some("hello"), Option.some(1))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } + * + * console.log(Option.zipLeft(Option.some("hello"), Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link zipRight} to keep the second value instead + * @see {@link zipWith} to combine both values + * + * @category zipping + * @since 2.0.0 + */ +export const zipLeft: { + <_>(that: Option<_>): (self: Option) => Option + (self: Option, that: Option): Option +} = dual(2, (self: Option, that: Option): Option => tap(self, () => that)) + +/** + * Composes two `Option`-returning functions into a single function that chains + * them together. + * + * **When to use** + * + * Use when you need to compose two functions that each return an `Option`, so + * `None` short-circuits without calling the next function. + * + * **Details** + * + * - Calls `afb(a)`, then if `Some`, calls `bfc` with its value + * - Short-circuits to `None` if either function returns `None` + * + * **Example** (Composing parsers) + * + * ```ts + * import { Option } from "effect" + * + * const parse = (s: string): Option.Option => + * isNaN(Number(s)) ? Option.none() : Option.some(Number(s)) + * + * const double = (n: number): Option.Option => + * n > 0 ? Option.some(n * 2) : Option.none() + * + * const parseAndDouble = Option.composeK(parse, double) + * + * console.log(parseAndDouble("42")) + * // Output: { _id: 'Option', _tag: 'Some', value: 84 } + * + * console.log(parseAndDouble("not a number")) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link flatMap} for single-step chaining + * + * @category sequencing + * @since 2.0.0 + */ +export const composeK: { + (bfc: (b: B) => Option): (afb: (a: A) => Option) => (a: A) => Option + (afb: (a: A) => Option, bfc: (b: B) => Option): (a: A) => Option +} = dual(2, (afb: (a: A) => Option, bfc: (b: B) => Option) => (a: A): Option => flatMap(afb(a), bfc)) + +/** + * Runs a side-effecting `Option`-returning function on the value of a `Some`, + * returning the original `Option` if the function returns `Some`, or `None` + * if it returns `None`. + * + * **When to use** + * + * Use to validate an `Option`'s present value without transforming it, such as + * adding a side-condition check in a pipeline. + * + * **Details** + * + * - `None` → `None` + * - `Some` → calls `f(value)`; if result is `Some`, returns original `self`; if `None`, returns `None` + * + * **Example** (Validating without transforming) + * + * ```ts + * import { Option } from "effect" + * + * const getInteger = (n: number) => + * Number.isInteger(n) ? Option.some(n) : Option.none() + * + * console.log(Option.tap(Option.some(1), getInteger)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(Option.tap(Option.some(1.14), getInteger)) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link flatMap} when you want to transform the value + * @see {@link filter} for predicate-based filtering + * + * @category sequencing + * @since 2.0.0 + */ +export const tap: { + (f: (a: A) => Option): (self: Option) => Option + (self: Option, f: (a: A) => Option): Option +} = dual(2, (self: Option, f: (a: A) => Option): Option => flatMap(self, (a) => map(f(a), () => a))) + +/** + * Combines two `Option`s into a `Some` containing a tuple `[A, B]` if both + * are `Some`. + * + * **When to use** + * + * Use when you need to require two `Option` values to both be `Some` and keep + * both values as a tuple. + * + * **Details** + * + * - Both `Some` → `Some([a, b])` + * - Either `None` → `None` + * + * **Example** (Pairing two Options) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.product(Option.some("hello"), Option.some(42))) + * // Output: { _id: 'Option', _tag: 'Some', value: ['hello', 42] } + * + * console.log(Option.product(Option.none(), Option.some(42))) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link zipWith} to combine with a function instead of a tuple + * @see {@link all} to combine many `Option`s + * + * @category combining + * @since 2.0.0 + */ +export const product = (self: Option, that: Option): Option<[A, B]> => + isSome(self) && isSome(that) ? some([self.value, that.value]) : none() + +/** + * Combines a primary `Option` with an iterable of `Option`s into a tuple if + * all are `Some`. + * + * **When to use** + * + * Use when you need several `Option` values of the same type to all be `Some` + * and return them as a non-empty tuple. + * + * **Details** + * + * - All `Some` → `Some([self.value, ...rest])` + * - Any `None` → `None` + * + * **Example** (Combining many Options) + * + * ```ts + * import { Option } from "effect" + * + * const first = Option.some(1) + * const rest = [Option.some(2), Option.some(3)] + * + * console.log(Option.productMany(first, rest)) + * // Output: { _id: 'Option', _tag: 'Some', value: [1, 2, 3] } + * + * console.log(Option.productMany(first, [Option.some(2), Option.none()])) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link product} for combining exactly two + * @see {@link all} for tuples, structs, and iterables + * + * @category combining + * @since 2.0.0 + */ +export const productMany = ( + self: Option, + collection: Iterable> +): Option<[A, ...Array]> => { + if (isNone(self)) { + return none() + } + const out: [A, ...Array] = [self.value] + for (const o of collection) { + if (isNone(o)) { + return none() + } + out.push(o.value) + } + return some(out) +} + +/** + * Combines a structure of `Option`s (tuple, struct, or iterable) into a single + * `Option` containing the unwrapped structure. + * + * **When to use** + * + * Use when you need to combine multiple `Option` values into one while + * preserving the input shape, with any `None` making the result `None`. + * + * **Details** + * + * - Tuple input → `Option` of a tuple with the same length + * - Struct input → `Option` of a struct with the same keys + * - Iterable input → `Option` of an `Array` + * - Any `None` in the input → entire result is `None` + * + * **Example** (Combining a tuple and a struct) + * + * ```ts + * import { Option } from "effect" + * + * const maybeName: Option.Option = Option.some("John") + * const maybeAge: Option.Option = Option.some(25) + * + * // ┌─── Option<[string, number]> + * // ▼ + * const tuple = Option.all([maybeName, maybeAge]) + * console.log(tuple) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: [ 'John', 25 ] } + * + * // ┌─── Option<{ name: string; age: number; }> + * // ▼ + * const struct = Option.all({ name: maybeName, age: maybeAge }) + * console.log(struct) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'John', age: 25 } } + * ``` + * + * @see {@link product} for combining exactly two + * @see {@link productMany} for a homogeneous collection + * + * @category combining + * @since 2.0.0 + */ +// @ts-expect-error +export const all: > | Record>>( + input: I +) => [I] extends [ReadonlyArray>] ? Option< + { -readonly [K in keyof I]: [I[K]] extends [Option] ? A : never } + > + : [I] extends [Iterable>] ? Option> + : Option<{ -readonly [K in keyof I]: [I[K]] extends [Option] ? A : never }> = ( + input: Iterable> | Record> + ): Option => { + if (Symbol.iterator in input) { + const out: Array> = [] + for (const o of (input as Iterable>)) { + if (isNone(o)) { + return none() + } + out.push(o.value) + } + return some(out) + } + + const out: Record = {} + for (const key of Object.keys(input)) { + const o = input[key] + if (isNone(o)) { + return none() + } + out[key] = o.value + } + return some(out) + } + +/** + * Combines two `Option`s using a provided function. + * + * **When to use** + * + * Use when you need to combine two present `Option` values into a computed + * result. + * + * **Details** + * + * - Both `Some` → applies `f(a, b)` and wraps in `Some` + * - Either `None` → `None` + * + * **Example** (Combining with a function) + * + * ```ts + * import { Option } from "effect" + * + * const person = Option.zipWith( + * Option.some("John"), + * Option.some(25), + * (name, age) => ({ name: name.toUpperCase(), age }) + * ) + * + * console.log(person) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } + * ``` + * + * @see {@link product} to combine into a tuple instead + * @see {@link lift2} to lift a binary function + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + (that: Option, f: (a: A, b: B) => C): (self: Option) => Option + (self: Option, that: Option, f: (a: A, b: B) => C): Option +} = dual( + 3, + (self: Option, that: Option, f: (a: A, b: B) => C): Option => + map(product(self, that), ([a, b]) => f(a, b)) +) + +/** + * Reduces an iterable of `Option`s to a single value, skipping `None` entries. + * + * **When to use** + * + * Use when you need to aggregate values from a collection where some may be + * absent. + * + * **Details** + * + * - Iterates through the collection, applying `f` only to `Some` values + * - `None` values are skipped entirely + * - Returns the accumulated result + * + * **Example** (Summing present values) + * + * ```ts + * import { Option, pipe } from "effect" + * + * const items = [Option.some(1), Option.none(), Option.some(2), Option.none()] + * + * console.log(pipe(items, Option.reduceCompact(0, (b, a) => b + a))) + * // Output: 3 + * ``` + * + * @category reducing + * @since 2.0.0 + */ +export const reduceCompact: { + (b: B, f: (b: B, a: A) => B): (self: Iterable>) => B + (self: Iterable>, b: B, f: (b: B, a: A) => B): B +} = dual( + 3, + (self: Iterable>, b: B, f: (b: B, a: A) => B): B => { + let out: B = b + for (const oa of self) { + if (isSome(oa)) { + out = f(out, oa.value) + } + } + return out + } +) + +/** + * Converts an `Option` into an `Array`. + * + * **When to use** + * + * Use when you need to pass an `Option` to array-based APIs or spread optional + * values into collections. + * + * **Details** + * + * - `Some` → single-element array `[value]` + * - `None` → empty array `[]` + * + * **Example** (Converting to an array) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.toArray(Option.some(1))) + * // Output: [1] + * + * console.log(Option.toArray(Option.none())) + * // Output: [] + * ``` + * + * @see {@link fromIterable} for the inverse direction + * + * @category converting + * @since 2.0.0 + */ +export const toArray = (self: Option): Array => isNone(self) ? [] : [self.value] + +/** + * Splits an `Option` into two `Option`s using a function that returns a `Result`. + * + * **When to use** + * + * Use when you need to split an optional value into "left" and "right" + * channels using a `Result`-returning function. + * + * **Details** + * + * - `None` → `[None, None]` + * - `Some` where `f` returns `Err` → `[Some(error), None]` + * - `Some` where `f` returns `Ok` → `[None, Some(value)]` + * + * **Example** (Partitioning by Result) + * + * ```ts + * import { Option, Result } from "effect" + * + * const parseNumber = (s: string): Result.Result => { + * const n = Number(s) + * return isNaN(n) ? Result.fail("Not a number") : Result.succeed(n) + * } + * + * console.log(Option.partitionMap(Option.some("42"), parseNumber)) + * // Output: [{ _id: 'Option', _tag: 'None' }, { _id: 'Option', _tag: 'Some', value: 42 }] + * + * console.log(Option.partitionMap(Option.some("abc"), parseNumber)) + * // Output: [{ _id: 'Option', _tag: 'Some', value: 'Not a number' }, { _id: 'Option', _tag: 'None' }] + * + * console.log(Option.partitionMap(Option.none(), parseNumber)) + * // Output: [{ _id: 'Option', _tag: 'None' }, { _id: 'Option', _tag: 'None' }] + * ``` + * + * @see {@link filter} for simple predicate-based filtering + * + * @category filtering + * @since 2.0.0 + */ +export const partitionMap: { + (f: (a: A) => Result): (self: Option) => [left: Option, right: Option] + (self: Option, f: (a: A) => Result): [left: Option, right: Option] +} = dual(2, ( + self: Option, + f: (a: A) => Result +): [excluded: Option, satisfying: Option] => { + if (isNone(self)) { + return [none(), none()] + } + const e = f(self.value) + return result.isFailure(e) ? [some(e.failure), none()] : [none(), some(e.success)] +}) + +/** + * Transforms and filters an `Option` using a `Filter` callback. + * + * **When to use** + * + * Use to transform an `Option`'s present value and discard it when the `Filter` + * fails. + * + * **Details** + * + * The callback returns a `Result`: `Result.succeed` keeps and transforms the + * value, while `Result.fail` discards it. + * + * **Example** (Filtering and transforming) + * + * ```ts + * import { Option, Result } from "effect" + * + * console.log(Option.filterMap( + * Option.some(2), + * (n) => (n % 2 === 0 ? Result.succeed(`Even: ${n}`) : Result.failVoid) + * )) + * // Output: { _id: 'Option', _tag: 'Some', value: 'Even: 2' } + * ``` + * + * @see {@link filter} for predicate-based filtering + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: Filter.Filter): (self: Option) => Option + (self: Option, f: Filter.Filter): Option +} = dual(2, (self: Option, f: Filter.Filter): Option => { + if (isNone(self)) { + return none() + } + const next = f(self.value) + return result.isSuccess(next) ? some(next.success) : none() +}) + +/** + * Filters an `Option` using a predicate. Returns `None` if the predicate is + * not satisfied or the input is `None`. + * + * **When to use** + * + * Use when you need to discard an `Option`'s present value when it does not + * meet a condition, while narrowing the type via a refinement predicate. + * + * **Details** + * + * - `None` → `None` + * - `Some` where `predicate(value)` is `true` → `Some(value)` + * - `Some` where `predicate(value)` is `false` → `None` + * - Supports refinements for type narrowing + * + * **Example** (Filtering with a predicate) + * + * ```ts + * import { Option } from "effect" + * + * const removeEmpty = (input: Option.Option) => + * Option.filter(input, (value) => value !== "") + * + * console.log(removeEmpty(Option.some("hello"))) + * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } + * + * console.log(removeEmpty(Option.some(""))) + * // Output: { _id: 'Option', _tag: 'None' } + * + * console.log(removeEmpty(Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link filterMap} to transform and filter simultaneously + * @see {@link exists} to test without filtering + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: Refinement): (self: Option) => Option + (predicate: Predicate): (self: Option) => Option + (self: Option, refinement: Refinement): Option + (self: Option, predicate: Predicate): Option +} = dual( + 2, + (self: Option, predicate: Predicate): Option => + isNone(self) ? none() : predicate(self.value) ? some(self.value) : none() +) + +/** + * Creates an `Equivalence` for `Option` from an `Equivalence` for `A`. + * + * **When to use** + * + * Use when you need equality to treat two `None` values as equal and compare + * two `Some` values with a supplied equality rule. + * + * **Details** + * + * - `None` vs `None` → `true` + * - `Some` vs `None` (or vice versa) → `false` + * - `Some(a)` vs `Some(b)` → delegates to the provided `Equivalence` + * + * **Example** (Comparing Options) + * + * ```ts + * import { Equivalence, Option } from "effect" + * + * const eq = Option.makeEquivalence(Equivalence.strictEqual()) + * + * console.log(eq(Option.some(1), Option.some(1))) + * // Output: true + * + * console.log(eq(Option.some(1), Option.some(2))) + * // Output: false + * + * console.log(eq(Option.none(), Option.none())) + * // Output: true + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((x, y) => isNone(x) ? isNone(y) : isNone(y) ? false : isEquivalent(x.value, y.value)) + +/** + * Creates an `Order` for `Option` from an `Order` for `A`. + * + * **When to use** + * + * Use when you need to sort `Some` and `None` values, with `None` ordered + * before present values and present values compared by a supplied ordering + * rule. + * + * **Details** + * + * - `None` is considered less than any `Some` + * - Two `Some` values are compared using the provided `Order` + * - Two `None` values are equal (returns `0`) + * + * **Example** (Ordering Options) + * + * ```ts + * import { Number as N, Option } from "effect" + * + * const ord = Option.makeOrder(N.Order) + * + * console.log(ord(Option.none(), Option.some(1))) + * // Output: -1 + * + * console.log(ord(Option.some(1), Option.none())) + * // Output: 1 + * + * console.log(ord(Option.some(1), Option.some(2))) + * // Output: -1 + * ``` + * + * @category sorting + * @since 4.0.0 + */ +export const makeOrder = (O: Order): Order> => + order.make((self, that) => isSome(self) ? (isSome(that) ? O(self.value, that.value) : 1) : -1) + +/** + * Lifts a binary function to operate on two `Option` values. + * + * **When to use** + * + * Use when you need to reuse an existing binary function with two `Option` + * values. + * + * **Details** + * + * - Both `Some` → applies `f` and wraps in `Some` + * - Either `None` → `None` + * + * **Example** (Lifting addition) + * + * ```ts + * import { Option } from "effect" + * + * const addOptions = Option.lift2((a: number, b: number) => a + b) + * + * console.log(addOptions(Option.some(2), Option.some(3))) + * // Output: { _id: 'Option', _tag: 'Some', value: 5 } + * + * console.log(addOptions(Option.some(2), Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link zipWith} for a non-lifted variant + * + * @category lifting + * @since 2.0.0 + */ +export const lift2 = (f: (a: A, b: B) => C): { + (that: Option): (self: Option) => Option + (self: Option, that: Option): Option +} => dual(2, (self: Option, that: Option): Option => zipWith(self, that, f)) + +/** + * Lifts a `Predicate` or `Refinement` into the `Option` context: returns + * `Some(value)` when the predicate holds, `None` otherwise. + * + * **When to use** + * + * Use to convert a boolean check into an `Option`-returning function + * - Validating input and wrapping it in `Option` + * + * **Details** + * + * - `predicate(value)` is `true` → `Some(value)` + * - `predicate(value)` is `false` → `None` + * - Supports refinements for type narrowing + * + * **Example** (Validating positive numbers) + * + * ```ts + * import { Option } from "effect" + * + * const parsePositive = Option.liftPredicate((n: number) => n > 0) + * + * console.log(parsePositive(1)) + * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * + * console.log(parsePositive(-1)) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link filter} to apply a predicate to an existing `Option` + * @see {@link toRefinement} for the inverse direction + * + * @category lifting + * @since 2.0.0 + */ +export const liftPredicate: { // Note: I intentionally avoid using the NoInfer pattern here. + (refinement: Refinement): (a: A) => Option + (predicate: Predicate): (b: B) => Option + ( + self: A, + refinement: Refinement + ): Option + ( + self: B, + predicate: Predicate + ): Option +} = dual( + 2, + (b: B, predicate: Predicate): Option => predicate(b) ? some(b) : none() +) + +/** + * Checks whether an `Option` contains a value equivalent to the given one, using a + * custom `Equivalence`. + * + * **When to use** + * + * Use when you need to test whether an `Option` contains a value using a + * custom equality check. + * + * **Details** + * + * - `Some` where `isEquivalent(value, a)` is `true` → `true` + * - `Some` where not equivalent, or `None` → `false` + * + * **Example** (Checking with custom equivalence) + * + * ```ts + * import { Equivalence, Option } from "effect" + * + * const check = Option.containsWith(Equivalence.strictEqual()) + * + * console.log(Option.some(2).pipe(check(2))) + * // Output: true + * + * console.log(Option.some(1).pipe(check(2))) + * // Output: false + * + * console.log(Option.none().pipe(check(2))) + * // Output: false + * ``` + * + * @see {@link contains} for a version using default equality + * + * @category elements + * @since 2.0.0 + */ +export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { + (a: A): (self: Option) => boolean + (self: Option, a: A): boolean +} => dual(2, (self: Option, a: A): boolean => isNone(self) ? false : isEquivalent(self.value, a)) + +/** + * Checks whether an `Option` contains a value equal to the given one, using default + * structural equality. + * + * **When to use** + * + * Use when you need a quick membership test for an `Option` value using + * standard equality. + * + * **Details** + * + * - `Some` where `Equal.equals(value, a)` is `true` → `true` + * - `Some` where not equal, or `None` → `false` + * + * **Example** (Checking containment) + * + * ```ts + * import { Option } from "effect" + * + * console.log(Option.some(2).pipe(Option.contains(2))) + * // Output: true + * + * console.log(Option.some(1).pipe(Option.contains(2))) + * // Output: false + * + * console.log(Option.none().pipe(Option.contains(2))) + * // Output: false + * ``` + * + * @see {@link containsWith} for custom equality + * @see {@link exists} to test with a predicate + * + * @category elements + * @since 2.0.0 + */ +export const contains: { + (a: A): (self: Option) => boolean + (self: Option, a: A): boolean +} = containsWith(Equal.asEquivalence()) + +/** + * Checks whether the value in a `Some` satisfies a predicate or refinement. + * + * **When to use** + * + * Use to check a condition on an optional value without unwrapping + * + * **Details** + * + * - `None` → `false` + * - `Some` where `predicate(value)` is `true` → `true` + * - `Some` where `predicate(value)` is `false` → `false` + * - With a refinement, narrows the `Option` type on `true` + * + * **Example** (Testing a condition) + * + * ```ts + * import { Option } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * + * console.log(Option.some(2).pipe(Option.exists(isEven))) + * // Output: true + * + * console.log(Option.some(1).pipe(Option.exists(isEven))) + * // Output: false + * + * console.log(Option.none().pipe(Option.exists(isEven))) + * // Output: false + * ``` + * + * @see {@link filter} to keep or discard based on a predicate + * @see {@link contains} to test for a specific value + * + * @category elements + * @since 2.0.0 + */ +export const exists: { + (refinement: Refinement, B>): (self: Option) => self is Option + (predicate: Predicate>): (self: Option) => boolean + (self: Option, refinement: Refinement): self is Option + (self: Option, predicate: Predicate): boolean +} = dual( + 2, + (self: Option, refinement: Refinement): self is Option => + isNone(self) ? false : refinement(self.value) +) + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * Gives a name to the value of an `Option`, creating a single-key record + * inside `Some`. Starting point for the do notation pipeline. + * + * **When to use** + * + * Use when you need to start an `Option` do notation chain by naming the first + * value. + * + * **Example** (Starting do notation) + * + * ```ts + * import { Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const result = pipe( + * Option.some(2), + * Option.bindTo("x"), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} for starting with an empty record + * @see {@link bind} to add `Option` values + * @see {@link let_ let} to add plain values + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Option) => Option<{ [K in N]: A }> + (self: Option, name: N): Option<{ [K in N]: A }> +} = doNotation.bindTo(map) + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): (self: Option) => Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: Option, + name: Exclude, + f: (a: NoInfer) => B + ): Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.let_(map) + +export { + /** + * Adds a computed plain value to the do notation record. + * + * **When to use** + * + * Use when you need to bind a derived non-`Option` value in an `Option` do + * notation pipeline. + * + * **Example** (Adding a computed value) + * + * ```ts + * import { Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} for starting the chain + * @see {@link bind} to add `Option` values + * @see {@link bindTo} to start by naming an existing `Option` + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Adds an `Option` value to the do notation record under a given name. If the + * `Option` is `None`, the whole pipeline short-circuits to `None`. + * + * **When to use** + * + * Use when you need to sequence `Option` computations in do notation. + * + * **Example** (Binding Option values) + * + * ```ts + * import { Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link Do} for starting the chain + * @see {@link let_ let} to add plain values + * @see {@link bindTo} to start by naming an existing `Option` + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Option + ): (self: Option) => Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> + ( + self: Option, + name: Exclude, + f: (a: NoInfer) => Option + ): Option<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }> +} = doNotation.bind(map, flatMap) + +/** + * Provides an `Option` containing an empty record `{}`, used as the starting point for + * do notation chains. + * + * **When to use** + * + * Use when you need to start an `Option` do notation pipeline before adding + * bindings. + * + * **Example** (Building Option pipelines with do notation) + * + * ```ts + * import { Option, pipe } from "effect" + * import * as assert from "node:assert" + * + * const result = pipe( + * Option.Do, + * Option.bind("x", () => Option.some(2)), + * Option.bind("y", () => Option.some(3)), + * Option.let("sum", ({ x, y }) => x + y), + * Option.filter(({ x, y }) => x * y > 5) + * ) + * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ``` + * + * @see {@link bind} to add `Option` values + * @see {@link let_ let} to add plain values + * @see {@link bindTo} to start by naming an existing `Option` + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Option<{}> = some({}) + +/** + * Provides generator-based syntax for `Option`, similar to `async`/`await` but for + * optional values. Yielding a `None` short-circuits the generator to `None`. + * + * **When to use** + * + * Use when you need generator syntax for a sequence of `Option` steps that + * should short-circuit on `None`. + * + * **Details** + * + * - Each `yield*` unwraps a `Some` value or short-circuits to `None` + * - The return value is wrapped in `Some` + * - No `Effect` runtime is needed + * + * **Example** (Sequencing Option computations with generator syntax) + * + * ```ts + * import { Option } from "effect" + * + * const maybeName: Option.Option = Option.some("John") + * const maybeAge: Option.Option = Option.some(25) + * + * const person = Option.gen(function*() { + * const name = (yield* maybeName).toUpperCase() + * const age = yield* maybeAge + * return { name, age } + * }) + * + * console.log(person) + * // Output: + * // { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } + * ``` + * + * @see {@link Do} / {@link bind} for the do notation alternative + * + * @category generators + * @since 2.0.0 + */ +export const gen: Gen.Gen = (...args) => { + const f = args.length === 1 ? args[0] : args[1].bind(args[0]) + const iterator = f() + let state: IteratorResult = iterator.next() + while (!state.done) { + const current = state.value + if (isNone(current)) { + return current + } + state = iterator.next(current.value as never) + } + return some(state.value) +} + +/** + * Creates a `Reducer` for `Option` that prioritizes the first non-`None` + * value and combines values when both are `Some`. + * + * **When to use** + * + * Use to build an `Option` reducer that falls back to the first available value + * when either side may be absent. + * + * **Details** + * + * - `None` + `None` → `None` + * - `Some(a)` + `None` → `Some(a)` + * - `None` + `Some(b)` → `Some(b)` + * - `Some(a)` + `Some(b)` → `Some(combine(a, b))` + * - Initial value is `None` + * + * **Example** (Reducing with first-wins semantics) + * + * ```ts + * import { Number, Option } from "effect" + * + * const reducer = Option.makeReducer(Number.ReducerSum) + * console.log(reducer.combineAll([Option.some(1), Option.none(), Option.some(2)])) + * // Output: { _id: 'Option', _tag: 'Some', value: 3 } + * ``` + * + * @see {@link makeReducerFailFast} for fail-fast semantics + * + * @category Reducer + * @since 4.0.0 + */ +export function makeReducer(combiner: Combiner.Combiner): Reducer.Reducer> { + return Reducer.make((self, that) => { + if (isNone(self)) return that + if (isNone(that)) return self + return some(combiner.combine(self.value, that.value)) + }, none()) +} + +/** + * Creates a `Combiner` for `Option` with fail-fast semantics: returns `None` + * if either operand is `None`. + * + * **When to use** + * + * Use when you need an `Option` combiner that returns `None` unless both + * operands are `Some`. + * + * **Details** + * + * - `None` + anything → `None` + * - anything + `None` → `None` + * - `Some(a)` + `Some(b)` → `Some(combine(a, b))` + * + * **Example** (Fail-fast combining) + * + * ```ts + * import { Number, Option } from "effect" + * + * const combiner = Option.makeCombinerFailFast(Number.ReducerSum) + * console.log(combiner.combine(Option.some(1), Option.some(2))) + * // Output: { _id: 'Option', _tag: 'Some', value: 3 } + * + * console.log(combiner.combine(Option.some(1), Option.none())) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link makeReducerFailFast} to get a full `Reducer` + * + * @category Combiner + * @since 4.0.0 + */ +export function makeCombinerFailFast(combiner: Combiner.Combiner): Combiner.Combiner> { + return Combiner.make((self, that) => { + if (isNone(self) || isNone(that)) return none() + return some(combiner.combine(self.value, that.value)) + }) +} + +/** + * Creates a `Reducer` for `Option` by lifting an existing `Reducer` with + * fail-fast semantics. + * + * **When to use** + * + * Use when you need to reduce `Option` values with fail-fast semantics, where + * any `None` aborts the entire result instead of being skipped. + * + * **Details** + * + * - Initial value is `Some(reducer.initialValue)` + * - Combines only when both operands are `Some` + * - Any `None` causes the result to become `None` immediately + * + * **Example** (Fail-fast reducing) + * + * ```ts + * import { Number, Option } from "effect" + * + * const reducer = Option.makeReducerFailFast(Number.ReducerSum) + * console.log(reducer.combineAll([Option.some(1), Option.some(2)])) + * // Output: { _id: 'Option', _tag: 'Some', value: 3 } + * + * console.log(reducer.combineAll([Option.some(1), Option.none()])) + * // Output: { _id: 'Option', _tag: 'None' } + * ``` + * + * @see {@link makeCombinerFailFast} for just the combiner + * @see {@link makeReducer} for non-fail-fast semantics + * + * @category Reducer + * @since 4.0.0 + */ +export function makeReducerFailFast(reducer: Reducer.Reducer): Reducer.Reducer> { + const combine = makeCombinerFailFast(reducer).combine + const initialValue = some(reducer.initialValue) + return Reducer.make(combine, initialValue, (collection) => { + let out = initialValue + for (const value of collection) { + out = combine(out, value) + if (isNone(out)) return out + } + return out + }) +} diff --git a/repos/effect/packages/effect/src/Order.ts b/repos/effect/packages/effect/src/Order.ts new file mode 100644 index 0000000..dcc7bf4 --- /dev/null +++ b/repos/effect/packages/effect/src/Order.ts @@ -0,0 +1,982 @@ +/** + * Defines comparison functions for ordered values. + * + * An `Order` compares two `A` values and returns whether the first is less + * than, equal to, or greater than the second. Orders are used for sorting, + * choosing minimum or maximum values, checking ranges, and building ordered data + * structures. This module includes built-in orders, constructors for custom + * orders, tools for reversing and combining comparisons, tuple and struct + * helpers, comparison predicates, clamping, and reducer support. + * + * @since 2.0.0 + */ +import { dual } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import type { Ordering } from "./Ordering.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Represents a total ordering for values of type `A`. + * + * **When to use** + * + * Use when you need to define how values of a type are compared. + * + * **Details** + * + * An order returns `-1` when the first value is less than the second, `0` when + * the values are equal according to this ordering, and `1` when the first value + * is greater than the second. It must satisfy total ordering laws: totality, + * antisymmetry, and transitivity. + * + * **Example** (Defining a custom Order) + * + * ```ts + * import { Order } from "effect" + * + * const byAge: Order.Order<{ name: string; age: number }> = (self, that) => { + * if (self.age < that.age) return -1 + * if (self.age > that.age) return 1 + * return 0 + * } + * + * const person1 = { name: "Alice", age: 30 } + * const person2 = { name: "Bob", age: 25 } + * console.log(byAge(person1, person2)) // 1 + * ``` + * + * @see {@link make} to create an order from a comparison function + * @see {@link Ordering} for the result type of comparisons + * @category type class + * @since 2.0.0 + */ +export interface Order { + (self: A, that: A): Ordering +} + +/** + * Type lambda for the `Order` type class, used internally for higher-kinded type operations. + * + * **When to use** + * + * Use when you need to abstract over `Order` in higher-kinded type code. + * + * **Details** + * + * This is type-level only, has no runtime representation, and is used + * internally by the Effect type system. + * + * @category type lambdas + * @since 2.0.0 + */ +export interface OrderTypeLambda extends TypeLambda { + readonly type: Order +} + +/** + * Creates a new `Order` instance from a comparison function. + * + * **When to use** + * + * Use when you need a sorting rule not covered by the built-in orders or input + * mapping helpers, and you can provide a total comparison. + * + * **Details** + * + * Uses reference equality (`===`) as a shortcut: if `self === that`, it returns + * `0` without calling the comparison function. The comparison function should + * return `-1`, `0`, or `1`, and the returned order satisfies total ordering + * laws when the comparison function does. + * + * **Example** (Creating an Order) + * + * ```ts + * import { Order } from "effect" + * + * const byAge = Order.make<{ name: string; age: number }>((self, that) => { + * if (self.age < that.age) return -1 + * if (self.age > that.age) return 1 + * return 0 + * }) + * + * console.log(byAge({ name: "Alice", age: 30 }, { name: "Bob", age: 25 })) // 1 + * console.log(byAge({ name: "Alice", age: 25 }, { name: "Bob", age: 30 })) // -1 + * ``` + * + * @see {@link mapInput} to transform an order by mapping the input type + * @see {@link combine} to combine multiple orders + * @category constructors + * @since 2.0.0 + */ +export function make( + compare: (self: A, that: A) => -1 | 0 | 1 +): Order { + return (self, that) => self === that ? 0 : compare(self, that) +} + +/** + * Order instance for strings that compares them lexicographically using JavaScript's `<` operator. + * + * **When to use** + * + * Use when you need lexicographic string ordering. + * + * **Details** + * + * Uses lexicographic dictionary ordering. The empty string is less than any + * non-empty string, and comparisons are case-sensitive. + * + * **Example** (Ordering strings) + * + * ```ts + * import { Order } from "effect" + * + * console.log(Order.String("apple", "banana")) // -1 + * console.log(Order.String("banana", "apple")) // 1 + * console.log(Order.String("apple", "apple")) // 0 + * ``` + * + * @see {@link mapInput} to compare objects by a string property + * @see {@link Struct} to combine with other orders for struct comparison + * @category instances + * @since 4.0.0 + */ +export const String: Order = make((self, that) => self < that ? -1 : 1) + +/** + * Order instance for numbers that compares them numerically. + * + * **When to use** + * + * Use when you need numeric ordering for numbers. + * + * **Details** + * + * `0` is considered equal to `-0`. All `NaN` values are considered equal to + * each other, and any `NaN` is considered less than any non-`NaN` number. All + * other values use standard numeric comparison. + * + * **Example** (Ordering numbers) + * + * ```ts + * import { Order } from "effect" + * + * console.log(Order.Number(1, 1)) // 0 + * console.log(Order.Number(1, 2)) // -1 + * console.log(Order.Number(2, 1)) // 1 + * + * console.log(Order.Number(0, -0)) // 0 + * console.log(Order.Number(NaN, 1)) // -1 + * ``` + * + * @see {@link mapInput} to compare objects by a number property + * @see {@link BigInt} for bigint comparisons + * @category instances + * @since 4.0.0 + */ +export const Number: Order = make((self, that) => { + if (globalThis.Number.isNaN(self) && globalThis.Number.isNaN(that)) return 0 + if (globalThis.Number.isNaN(self)) return -1 // NaN < any number + if (globalThis.Number.isNaN(that)) return 1 // any number > NaN + return self < that ? -1 : 1 +}) + +/** + * Order instance for booleans where `false` is considered less than `true`. + * + * **When to use** + * + * Use when you need boolean ordering where `false` comes before `true`. + * + * **Details** + * + * `false` is less than `true`, and equal values return `0`. + * + * **Example** (Ordering booleans) + * + * ```ts + * import { Order } from "effect" + * + * console.log(Order.Boolean(false, true)) // -1 + * console.log(Order.Boolean(true, false)) // 1 + * console.log(Order.Boolean(true, true)) // 0 + * ``` + * + * @see {@link mapInput} to compare objects by a boolean property + * @category instances + * @since 4.0.0 + */ +export const Boolean: Order = make((self, that) => self < that ? -1 : 1) + +/** + * Order instance for bigints that compares them numerically. + * + * **When to use** + * + * Use when you need numeric ordering for `bigint` values. + * + * **Details** + * + * Uses standard numeric comparison for bigint values and handles arbitrarily + * large integers. + * + * **Example** (Ordering BigInts) + * + * ```ts + * import { Order } from "effect" + * + * console.log(Order.BigInt(1n, 2n)) // -1 + * console.log(Order.BigInt(2n, 1n)) // 1 + * console.log(Order.BigInt(1n, 1n)) // 0 + * ``` + * + * @see {@link Number} for regular number comparisons + * @see {@link mapInput} to compare objects by a bigint property + * @category instances + * @since 4.0.0 + */ +export const BigInt: Order = make((self, that) => self < that ? -1 : 1) + +/** + * Creates a new `Order` that reverses the comparison order of the input `Order`. + * + * **When to use** + * + * Use when you need the reverse of an existing order. + * + * **Details** + * + * Returns a new order that swaps the arguments before comparison. If the + * original order returns `-1`, the flipped order returns `1`, and vice versa. + * Equal comparisons remain `0`. + * + * **Example** (Reversing an Order) + * + * ```ts + * import { Order } from "effect" + * + * const flip = Order.flip(Order.Number) + * + * console.log(flip(1, 2)) // 1 + * console.log(flip(2, 1)) // -1 + * console.log(flip(1, 1)) // 0 + * ``` + * + * @see {@link combine} to combine orders for multi-criteria comparison + * @category combinators + * @since 4.0.0 + */ +export function flip(O: Order): Order { + return make((self, that) => O(that, self)) +} + +/** + * Combines two `Order` instances to create a new `Order` that first compares using the first `Order`, + * and if the values are equal, then compares using the second `Order`. + * + * **When to use** + * + * Use when you need tie-breaking with exactly two orders. + * + * **Details** + * + * First applies the first order. If the result is non-zero, that result is + * returned; otherwise, the second order is applied. The result is the first + * non-zero comparison result, or `0` if both orders return `0`. + * + * **Example** (Combining two Orders) + * + * ```ts + * import { Order } from "effect" + * + * const byAge = Order.mapInput( + * Order.Number, + * (person: { name: string; age: number }) => person.age + * ) + * const byName = Order.mapInput( + * Order.String, + * (person: { name: string; age: number }) => person.name + * ) + * const byAgeAndName = Order.combine(byAge, byName) + * + * const person1 = { name: "Alice", age: 30 } + * const person2 = { name: "Bob", age: 30 } + * const person3 = { name: "Charlie", age: 25 } + * + * console.log(byAgeAndName(person1, person2)) // -1 (Same age, Alice < Bob) + * console.log(byAgeAndName(person1, person3)) // 1 (Alice (30) > Charlie (25)) + * ``` + * + * @see {@link combineAll} to combine multiple orders from a collection + * @see {@link mapInput} to transform orders to work with different types + * @category combining + * @since 2.0.0 + */ +export const combine: { + (that: Order): (self: Order) => Order + (self: Order, that: Order): Order +} = dual(2, (self: Order, that: Order): Order => + make((a1, a2) => { + const out = self(a1, a2) + if (out !== 0) { + return out + } + return that(a1, a2) + })) + +/** + * Creates an `Order` that considers all values as equal. + * + * **When to use** + * + * Use when you need an order that treats all values as equal. + * + * **Details** + * + * Always returns `0` regardless of input values, making it useful as a neutral + * element in order composition. + * + * **Example** (Ordering with an always-equal Order) + * + * ```ts + * import { Order } from "effect" + * + * const alwaysEqualOrder = Order.alwaysEqual() + * + * console.log(alwaysEqualOrder(1, 2)) // 0 + * console.log(alwaysEqualOrder(2, 1)) // 0 + * console.log(alwaysEqualOrder(1, 1)) // 0 + * ``` + * + * @see {@link combine} to combine with other orders + * @category constructors + * @since 4.0.0 + */ +export function alwaysEqual(): Order { + return make(() => 0) +} + +/** + * Combines all `Order` instances in the provided collection into a single `Order`. + * The resulting `Order` compares using each `Order` in sequence until a non-zero result is found. + * + * **When to use** + * + * Use when you need tie-breaking across a variable number of orders. + * + * **Details** + * + * Applies orders in iteration order and short-circuits on the first non-zero + * result. It returns `0` only if all orders return `0`. + * + * **Example** (Combining multiple Orders) + * + * ```ts + * import { Order } from "effect" + * + * const byAge = Order.mapInput( + * Order.Number, + * (person: { name: string; age: number }) => person.age + * ) + * const byName = Order.mapInput( + * Order.String, + * (person: { name: string; age: number }) => person.name + * ) + * + * const combinedOrder = Order.combineAll([byAge, byName]) + * + * const person1 = { name: "Alice", age: 30 } + * const person2 = { name: "Bob", age: 30 } + * + * console.log(combinedOrder(person1, person2)) // -1 (Same age, Alice < Bob) + * ``` + * + * @see {@link combine} to combine two orders + * @see {@link makeReducer} to create a reducer for combining orders + * @category combining + * @since 2.0.0 + */ +export function combineAll(collection: Iterable>): Order { + return make((a1, a2) => { + let out: Ordering = 0 + for (const O of collection) { + out = O(a1, a2) + if (out !== 0) { + return out + } + } + return out + }) +} + +/** + * Transforms an `Order` on type `A` into an `Order` on type `B` by providing a function that + * maps values of type `B` to values of type `A`. + * + * **When to use** + * + * Use when you need to adapt an `Order` to compare a larger value by one + * derived property. + * + * **Details** + * + * Applies the mapping function to both values before comparison. The mapping + * function should be pure and not have side effects so the ordering properties + * of the original order are preserved. + * + * **Example** (Mapping Input) + * + * ```ts + * import { Order } from "effect" + * + * const byLength = Order.mapInput(Order.Number, (s: string) => s.length) + * + * console.log(byLength("a", "bb")) // -1 + * console.log(byLength("bb", "a")) // 1 + * console.log(byLength("aa", "bb")) // 0 + * ``` + * + * @see {@link combine} to combine mapped orders for multi-criteria comparison + * @see {@link Struct} to create orders for structs with multiple fields + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Order) => Order + (self: Order, f: (b: B) => A): Order +} = dual( + 2, + (self: Order, f: (b: B) => A): Order => make((b1, b2) => self(f(b1), f(b2))) +) + +/** + * Order instance for `Date` objects that compares them chronologically by their timestamp. + * + * **When to use** + * + * Use when you need chronological ordering for JavaScript date values. + * + * **Details** + * + * Compares dates by their underlying timestamp in milliseconds since the epoch. + * Earlier dates are less than later dates. Invalid dates are compared through + * their `getTime()` result. + * + * **Example** (Ordering Dates) + * + * ```ts + * import { Order } from "effect" + * + * const date1 = new Date("2023-01-01") + * const date2 = new Date("2023-01-02") + * + * console.log(Order.Date(date1, date2)) // -1 + * console.log(Order.Date(date2, date1)) // 1 + * console.log(Order.Date(date1, date1)) // 0 + * ``` + * + * @see {@link mapInput} to compare objects by a date property + * @category instances + * @since 2.0.0 + */ +export const Date: Order = mapInput(Number, (date) => date.getTime()) + +/** + * Creates an `Order` for a tuple type based on orders for each element. + * + * **When to use** + * + * Use when you need fixed-length tuple ordering with per-position orders. + * + * **Details** + * + * Compares tuples element-by-element using the corresponding order and stops at + * the first non-zero comparison result. Tuples must have the same length as the + * order collection, and the result is `0` only if all elements are equal. + * + * **Example** (Ordering tuples) + * + * ```ts + * import { Order } from "effect" + * + * const tupleOrder = Order.Tuple([Order.Number, Order.String]) + * + * console.log(tupleOrder([1, "a"], [2, "b"])) // -1 + * console.log(tupleOrder([1, "b"], [1, "a"])) // 1 + * console.log(tupleOrder([1, "a"], [1, "a"])) // 0 + * ``` + * + * @see {@link Array} to compare arrays with length consideration + * @category combinators + * @since 4.0.0 + */ +export function Tuple>>( + elements: Elements +): Order<{ readonly [I in keyof Elements]: [Elements[I]] extends [Order] ? A : never }> { + return make((self, that) => { + const len = elements.length + for (let i = 0; i < len; i++) { + const o = elements[i](self[i], that[i]) + if (o !== 0) { + return o + } + } + return 0 + }) +} + +/** + * @since 4.0.0 + */ +function Array_(O: Order): Order> { + return make((self, that) => { + const aLen = self.length + const bLen = that.length + const len = Math.min(aLen, bLen) + for (let i = 0; i < len; i++) { + const o = O(self[i], that[i]) + if (o !== 0) { + return o + } + } + return Number(aLen, bLen) + }) +} + +export { + /** + * Creates an `Order` for arrays by applying the given `Order` to each element, then comparing by length if all elements are equal. + * + * **When to use** + * + * Use when you need lexicographic ordering for arrays of one element type. + * + * **Details** + * + * Compares arrays element-by-element using the provided order and stops at the + * first non-zero comparison result. If all elements are equal, shorter arrays + * are less than longer arrays. The result is `0` only if arrays have the same + * length and all elements are equal. + * + * **Example** (Ordering array elements) + * + * ```ts + * import { Order } from "effect" + * + * const arrayOrder = Order.Array(Order.Number) + * + * console.log(arrayOrder([1, 2], [1, 3])) // -1 + * console.log(arrayOrder([1, 2], [1, 2, 3])) // -1 (shorter array is less) + * console.log(arrayOrder([1, 2, 3], [1, 2])) // 1 (longer array is greater) + * console.log(arrayOrder([1, 2], [1, 2])) // 0 + * ``` + * + * @see {@link Tuple} for type-safe tuple ordering + * @category combinators + * @since 4.0.0 + */ + Array_ as Array +} + +/** + * Creates an `Order` for structs by applying the given `Order`s to each property in sequence. + * + * **When to use** + * + * Use when you need multi-field ordering for objects with known properties. + * + * **Details** + * + * Compares structs field-by-field in the key order of the fields object and + * stops at the first non-zero comparison result. Field order matters: earlier + * fields take precedence. The result is `0` only if all fields are equal. + * + * **Example** (Ordering structs) + * + * ```ts + * import { Order } from "effect" + * + * const personOrder = Order.Struct({ + * name: Order.String, + * age: Order.Number + * }) + * + * const person1 = { name: "Alice", age: 30 } + * const person2 = { name: "Bob", age: 25 } + * const person3 = { name: "Alice", age: 25 } + * + * console.log(personOrder(person1, person2)) // -1 (Alice < Bob) + * console.log(personOrder(person1, person3)) // 1 (same name, 30 > 25) + * console.log(personOrder(person1, person1)) // 0 + * ``` + * + * @see {@link combine} to combine orders manually + * @see {@link mapInput} to extract and compare by a single property + * @category combinators + * @since 4.0.0 + */ +export function Struct }>( + fields: R +): Order<{ [K in keyof R]: [R[K]] extends [Order] ? A : never }> { + const keys = Object.keys(fields) + return make((self, that) => { + for (const key of keys) { + const o = fields[key](self[key], that[key]) + if (o !== 0) { + return o + } + } + return 0 + }) +} + +/** + * Checks whether one value is strictly less than another according to the given order. + * + * **When to use** + * + * Use when you need a boolean less-than predicate using an `Order`. + * + * **Details** + * + * Returns `true` if the order returns `-1`, meaning the first value is less + * than the second. Equal or greater values return `false`. + * + * **Example** (Checking less-than comparisons) + * + * ```ts + * import { Order } from "effect" + * + * const isLessThanNumber = Order.isLessThan(Order.Number) + * + * console.log(isLessThanNumber(1, 2)) // true + * console.log(isLessThanNumber(2, 1)) // false + * console.log(isLessThanNumber(1, 1)) // false + * ``` + * + * @see {@link isLessThanOrEqualTo} for non-strict less than or equal + * @see {@link isGreaterThan} for strict greater than + * @category predicates + * @since 4.0.0 + */ +export const isLessThan = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) === -1) + +/** + * Checks whether one value is strictly greater than another according to the given order. + * + * **When to use** + * + * Use when you need a boolean greater-than predicate using an `Order`. + * + * **Details** + * + * Returns `true` if the order returns `1`, meaning the first value is greater + * than the second. Equal or lesser values return `false`. + * + * **Example** (Checking greater-than comparisons) + * + * ```ts + * import { Order } from "effect" + * + * const isGreaterThanNumber = Order.isGreaterThan(Order.Number) + * + * console.log(isGreaterThanNumber(2, 1)) // true + * console.log(isGreaterThanNumber(1, 2)) // false + * console.log(isGreaterThanNumber(1, 1)) // false + * ``` + * + * @see {@link isGreaterThanOrEqualTo} for non-strict greater than or equal + * @see {@link isLessThan} for strict less than + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThan = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) === 1) + +/** + * Checks whether one value is less than or equal to another according to the given order. + * + * **When to use** + * + * Use when you need a boolean less-than-or-equal predicate using an `Order`. + * + * **Details** + * + * Returns `true` if the order returns `-1` or `0`, and returns `false` only if + * the order returns `1`. + * + * **Example** (Checking less-than-or-equal comparisons) + * + * ```ts + * import { Order } from "effect" + * + * const isLessThanOrEqualToNumber = Order.isLessThanOrEqualTo(Order.Number) + * + * console.log(isLessThanOrEqualToNumber(1, 2)) // true + * console.log(isLessThanOrEqualToNumber(1, 1)) // true + * console.log(isLessThanOrEqualToNumber(2, 1)) // false + * ``` + * + * @see {@link isLessThan} for strict less than + * @see {@link isGreaterThan} for strict greater than + * @category predicates + * @since 4.0.0 + */ +export const isLessThanOrEqualTo = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) !== 1) + +/** + * Checks whether one value is greater than or equal to another according to the given order. + * + * **When to use** + * + * Use when you need a boolean greater-than-or-equal predicate using an + * `Order`. + * + * **Details** + * + * Returns `true` if the order returns `1` or `0`, and returns `false` only if + * the order returns `-1`. + * + * **Example** (Checking greater-than-or-equal comparisons) + * + * ```ts + * import { Order } from "effect" + * + * const isGreaterThanOrEqualToNumber = Order.isGreaterThanOrEqualTo(Order.Number) + * + * console.log(isGreaterThanOrEqualToNumber(2, 1)) // true + * console.log(isGreaterThanOrEqualToNumber(1, 1)) // true + * console.log(isGreaterThanOrEqualToNumber(1, 2)) // false + * ``` + * + * @see {@link isGreaterThan} for strict greater than + * @see {@link isLessThanOrEqualTo} for less than or equal + * @category predicates + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo = (O: Order): { + (that: A): (self: A) => boolean + (self: A, that: A): boolean +} => dual(2, (self: A, that: A) => O(self, that) !== -1) + +/** + * Returns the minimum of two values according to the given order. If they are equal, returns the first argument. + * + * **When to use** + * + * Use when you need to select the smaller of two values according to an + * `Order`. + * + * **Details** + * + * Returns the value that compares as less than or equal to the other value. If + * values are equal, the first argument is returned. + * + * **Example** (Selecting the minimum value) + * + * ```ts + * import { Order } from "effect" + * + * const minNumber = Order.min(Order.Number) + * + * console.log(minNumber(1, 2)) // 1 + * console.log(minNumber(2, 1)) // 1 + * console.log(minNumber(1, 1)) // 1 + * ``` + * + * @see {@link max} for the maximum of two values + * @see {@link clamp} to clamp a value between min and max + * @category comparisons + * @since 2.0.0 + */ +export const min = (O: Order): { + (that: A): (self: A) => A + (self: A, that: A): A +} => dual(2, (self: A, that: A) => self === that || O(self, that) < 1 ? self : that) + +/** + * Returns the maximum of two values according to the given order. If they are equal, returns the first argument. + * + * **When to use** + * + * Use when you need to select the larger of two values according to an + * `Order`. + * + * **Details** + * + * Returns the value that compares as greater than or equal to the other value. + * If values are equal, the first argument is returned. + * + * **Example** (Selecting the maximum value) + * + * ```ts + * import { Order } from "effect" + * + * const maxNumber = Order.max(Order.Number) + * + * console.log(maxNumber(1, 2)) // 2 + * console.log(maxNumber(2, 1)) // 2 + * console.log(maxNumber(1, 1)) // 1 + * ``` + * + * @see {@link min} for the minimum of two values + * @see {@link clamp} to clamp a value between min and max + * @category comparisons + * @since 2.0.0 + */ +export const max = (O: Order): { + (that: A): (self: A) => A + (self: A, that: A): A +} => dual(2, (self: A, that: A) => self === that || O(self, that) > -1 ? self : that) + +/** + * Restricts a value between a minimum and a maximum according to the given order. + * + * **When to use** + * + * Use when you need to clamp a value to an inclusive range according to an + * `Order`. + * + * **Details** + * + * Returns the value itself when it is between minimum and maximum, inclusive. + * Values below the range return minimum, and values above the range return + * maximum. The minimum must be less than or equal to the maximum according to + * the order. + * + * **Example** (Clamping values) + * + * ```ts + * import { Order } from "effect" + * + * const clamp = Order.clamp(Order.Number)({ minimum: 1, maximum: 5 }) + * + * console.log(clamp(3)) // 3 + * console.log(clamp(0)) // 1 + * console.log(clamp(6)) // 5 + * ``` + * + * @see {@link min} for the minimum of two values + * @see {@link max} for the maximum of two values + * @see {@link isBetween} to check if a value is within a range + * @category comparisons + * @since 2.0.0 + */ +export const clamp = (O: Order): { + (options: { + minimum: A + maximum: A + }): (self: A) => A + (self: A, options: { + minimum: A + maximum: A + }): A +} => + dual( + 2, + (self: A, options: { + minimum: A + maximum: A + }): A => min(O)(options.maximum, max(O)(options.minimum, self)) + ) + +/** + * Checks whether a value is between a minimum and a maximum (inclusive) according to the given order. + * + * **When to use** + * + * Use when you need range checks that respect domain-specific ordering, such as + * dates, versions, or custom priorities, instead of JavaScript numeric + * comparison. + * + * **Details** + * + * Returns `true` when the value is greater than or equal to minimum and less + * than or equal to maximum. Values outside the range return `false`. Both + * bounds are inclusive. + * + * **Example** (Checking ranges) + * + * ```ts + * import { Order } from "effect" + * + * const betweenNumber = Order.isBetween(Order.Number) + * + * console.log(betweenNumber(5, { minimum: 1, maximum: 10 })) // true + * console.log(betweenNumber(1, { minimum: 1, maximum: 10 })) // true + * console.log(betweenNumber(10, { minimum: 1, maximum: 10 })) // true + * console.log(betweenNumber(0, { minimum: 1, maximum: 10 })) // false + * console.log(betweenNumber(11, { minimum: 1, maximum: 10 })) // false + * ``` + * + * @see {@link clamp} to clamp a value to a range + * @see {@link isLessThanOrEqualTo} for less than or equal check + * @see {@link isGreaterThanOrEqualTo} for greater than or equal check + * @category predicates + * @since 4.0.0 + */ +export const isBetween = (O: Order): { + (options: { + minimum: A + maximum: A + }): (self: A) => boolean + (self: A, options: { + minimum: A + maximum: A + }): boolean +} => + dual( + 2, + (self: A, options: { + minimum: A + maximum: A + }): boolean => !isLessThan(O)(self, options.minimum) && !isGreaterThan(O)(self, options.maximum) + ) + +/** + * Creates a `Reducer` for combining `Order` instances, useful for aggregating orders in collections. + * + * **When to use** + * + * Use when you need a reducer that combines orders. + * + * **Details** + * + * Returns a reducer that combines orders using `combine`, uses `alwaysEqual` as + * the identity element for empty collections, and uses `combineAll` for + * combining collections of orders. The reducer can be used with fold operations + * on collections. + * + * **Example** (Creating a Reducer) + * + * ```ts + * import { Order } from "effect" + * + * const reducer = Order.makeReducer() + * const orders = [Order.Number, Order.flip(Order.Number)] + * + * const combined = reducer.combineAll(orders) + * console.log(combined(1, 2)) // -1 (uses first order) + * ``` + * + * @see {@link combine} to combine two orders + * @see {@link combineAll} to combine multiple orders + * @see {@link Reducer} for reducing orders as a collection operation + * @category constructors + * @since 4.0.0 + */ +export function makeReducer() { + return Reducer.make>( + combine, + () => 0, + combineAll + ) +} diff --git a/repos/effect/packages/effect/src/Ordering.ts b/repos/effect/packages/effect/src/Ordering.ts new file mode 100644 index 0000000..8afcdb4 --- /dev/null +++ b/repos/effect/packages/effect/src/Ordering.ts @@ -0,0 +1,182 @@ +/** + * The standard result of comparing two values. An `Ordering` is `-1` when the + * first value is less than the second, `0` when both values compare as equal, + * and `1` when the first value is greater than the second. This module also + * provides helpers for reversing an ordering, matching on the three cases, and + * combining ordered comparison results with a reducer. + * + * @since 2.0.0 + */ +import type { LazyArg } from "./Function.ts" +import { dual } from "./Function.ts" +import * as Reducer_ from "./Reducer.ts" + +/** + * Represents the result of comparing two values. + * + * **When to use** + * + * Use to model a normalized comparison result that is exactly less than, + * equal to, or greater than. + * + * **Details** + * + * - `-1` indicates the first value is less than the second + * - `0` indicates the values are equal + * - `1` indicates the first value is greater than the second + * + * **Example** (Defining comparison results) + * + * ```ts + * import type { Ordering } from "effect" + * + * // Custom comparison function + * const compareNumbers = (a: number, b: number): Ordering.Ordering => { + * if (a < b) return -1 + * if (a > b) return 1 + * return 0 + * } + * + * console.log(compareNumbers(5, 10)) // -1 (5 < 10) + * console.log(compareNumbers(10, 5)) // 1 (10 > 5) + * console.log(compareNumbers(5, 5)) // 0 (5 == 5) + * + * // Using with string comparison + * const compareStrings = (a: string, b: string): Ordering.Ordering => { + * return a.localeCompare(b) as Ordering.Ordering + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export type Ordering = -1 | 0 | 1 + +/** + * Reverses the ordering of the input Ordering. + * This is useful for creating descending sort orders from ascending ones. + * + * **When to use** + * + * Use to flip an ordering result when reversing sort direction or comparison + * priority. + * + * **Example** (Reversing comparison order) + * + * ```ts + * import { Ordering } from "effect" + * + * // Basic reversal + * console.log(Ordering.reverse(1)) // -1 (greater becomes less) + * console.log(Ordering.reverse(-1)) // 1 (less becomes greater) + * console.log(Ordering.reverse(0)) // 0 (equal stays equal) + * + * // Creating descending sort from ascending comparison + * const compareNumbers = (a: number, b: number): Ordering.Ordering => + * a < b ? -1 : a > b ? 1 : 0 + * + * const compareDescending = (a: number, b: number): Ordering.Ordering => + * Ordering.reverse(compareNumbers(a, b)) + * + * const numbers = [3, 1, 4, 1, 5] + * numbers.sort(compareNumbers) // [1, 1, 3, 4, 5] (ascending) + * numbers.sort(compareDescending) // [5, 4, 3, 1, 1] (descending) + * + * // Useful for toggling sort direction + * const createSorter = (ascending: boolean) => (a: number, b: number) => { + * const ordering = compareNumbers(a, b) + * return ascending ? ordering : Ordering.reverse(ordering) + * } + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const reverse = (o: Ordering): Ordering => (o === -1 ? 1 : o === 1 ? -1 : 0) + +/** + * Matches an `Ordering` value and returns the branch selected by that ordering. + * + * **When to use** + * + * Use to branch on the three possible comparison outcomes in one expression. + * + * **Example** (Pattern matching on orderings) + * + * ```ts + * import { Function, Ordering } from "effect" + * import * as assert from "node:assert" + * + * const toMessage = Ordering.match({ + * onLessThan: Function.constant("less than"), + * onEqual: Function.constant("equal"), + * onGreaterThan: Function.constant("greater than") + * }) + * + * assert.deepStrictEqual(toMessage(-1), "less than") + * assert.deepStrictEqual(toMessage(0), "equal") + * assert.deepStrictEqual(toMessage(1), "greater than") + * ``` + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + ( + options: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } + ): (self: Ordering) => A | B | C + ( + o: Ordering, + options: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } + ): A | B | C +} = dual(2, ( + self: Ordering, + { onEqual, onGreaterThan, onLessThan }: { + readonly onLessThan: LazyArg + readonly onEqual: LazyArg + readonly onGreaterThan: LazyArg + } +): A | B | C => self === -1 ? onLessThan() : self === 0 ? onEqual() : onGreaterThan()) + +/** + * Reducer for combining `Ordering`s. + * + * **When to use** + * + * Use to combine multiple comparison results in priority order, such as + * checking secondary criteria only when earlier criteria compare as equal. + * + * **Details** + * + * If any of the `Ordering`s is non-zero, the result is the first non-zero `Ordering`. + * If all the `Ordering`s are zero, the result is zero. + * + * **Gotchas** + * + * `combineAll` stops consuming the iterable as soon as it finds a non-zero + * `Ordering`. + * + * @category ordering + * @since 4.0.0 + */ +export const Reducer: Reducer_.Reducer = Reducer_.make( + (self, that) => self !== 0 ? self : that, + 0, + (collection) => { + let ordering: Ordering = 0 + for (ordering of collection) { + if (ordering !== 0) { + return ordering + } + } + return ordering + } +) diff --git a/repos/effect/packages/effect/src/PartitionedSemaphore.ts b/repos/effect/packages/effect/src/PartitionedSemaphore.ts new file mode 100644 index 0000000..b5c2e8c --- /dev/null +++ b/repos/effect/packages/effect/src/PartitionedSemaphore.ts @@ -0,0 +1,561 @@ +/** + * The `PartitionedSemaphore` module provides a semaphore for limiting + * concurrency across a shared permit pool while keeping waiters grouped by + * partition key. A `PartitionedSemaphore` is useful when many independent + * groups of work compete for the same bounded resource and each group should + * make progress without one busy group monopolizing released permits. + * + * @since 4.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as Option from "./Option.ts" + +/** + * Runtime type identifier used to mark values that implement + * `PartitionedSemaphore`. + * + * **Details** + * + * This marker is part of the runtime representation of partitioned semaphore + * values. + * + * @category type IDs + * @since 4.0.0 + */ +export const PartitionedTypeId: PartitionedTypeId = "~effect/PartitionedSemaphore" + +/** + * Literal type of the `PartitionedSemaphore` runtime type identifier. + * + * **When to use** + * + * Use to type fields that store the exact `PartitionedSemaphore` runtime marker. + * + * **Details** + * + * Use this type when declaring fields that must contain the exact + * `PartitionedTypeId` marker value. + * + * @category type IDs + * @since 4.0.0 + */ +export type PartitionedTypeId = "~effect/PartitionedSemaphore" + +/** + * A `PartitionedSemaphore` controls access to a shared permit pool while + * tracking waiters by partition key. + * + * **When to use** + * + * Use to coordinate shared permits across partition keys so waiting groups make + * progress without one group monopolizing the pool. + * + * **Details** + * + * Waiting permits are distributed across partitions in round-robin order. + * + * @category models + * @since 3.19.4 + */ +export interface PartitionedSemaphore { + readonly [PartitionedTypeId]: PartitionedTypeId + readonly capacity: number + readonly available: Effect.Effect + readonly take: (key: K, permits: number) => Effect.Effect + readonly release: (permits: number) => Effect.Effect + readonly withPermits: ( + key: K, + permits: number + ) => (effect: Effect.Effect) => Effect.Effect + readonly withPermit: (key: K) => (effect: Effect.Effect) => Effect.Effect + readonly withPermitsIfAvailable: ( + permits: number + ) => (effect: Effect.Effect) => Effect.Effect, E, R> +} + +/** + * Alias interface for a `PartitionedSemaphore` keyed by values of type `K`. + * + * **When to use** + * + * Use as an alternate exported name for a partitioned permit pool keyed by `K`. + * + * **Details** + * + * This interface does not add members beyond `PartitionedSemaphore`; it + * provides an alternate exported name for APIs that refer to a partitioned + * permit pool. + * + * @category models + * @since 4.0.0 + */ +export interface Partitioned extends PartitionedSemaphore {} + +/** + * Constructs a `PartitionedSemaphore` synchronously, outside of `Effect`. + * + * **When to use** + * + * Use when you need to construct a partitioned semaphore synchronously outside + * an `Effect` workflow. + * + * **Details** + * + * Negative permit counts are clamped to `0`. Non-finite permit counts create + * an unbounded semaphore whose acquire and release operations complete + * immediately. + * + * @see {@link make} for creating a partitioned semaphore inside `Effect` + * + * @category constructors + * @since 3.19.4 + */ +export const makeUnsafe = (options: { + readonly permits: number +}): PartitionedSemaphore => { + const maxPermits = Math.max(0, options.permits) + + if (!Number.isFinite(maxPermits)) { + return { + [PartitionedTypeId]: PartitionedTypeId, + capacity: maxPermits, + available: Effect.succeed(maxPermits), + take: () => Effect.void, + release: () => Effect.succeed(maxPermits), + withPermits: () => (effect) => effect, + withPermit: () => (effect) => effect, + withPermitsIfAvailable: () => (effect) => Effect.asSome(effect) + } + } + + let totalPermits = maxPermits + let waitingPermits = 0 + + type Waiter = { + permits: number + readonly resume: () => void + } + + const partitions = MutableHashMap.empty>() + let iterator = partitions[Symbol.iterator]() + + const releaseUnsafe = (permits: number): number => { + while (permits > 0) { + if (waitingPermits === 0) { + totalPermits = Math.min(maxPermits, totalPermits + permits) + return totalPermits + } + + let state = iterator.next() + if (state.done) { + iterator = partitions[Symbol.iterator]() + state = iterator.next() + if (state.done) { + return totalPermits + } + } + + const waiter = state.value[1].values().next().value + if (waiter === undefined) { + continue + } + + waiter.permits -= 1 + waitingPermits -= 1 + + if (waiter.permits === 0) { + waiter.resume() + } + + permits -= 1 + } + + return totalPermits + } + + const take = (key: K, permits: number): Effect.Effect => { + if (permits <= 0) { + return Effect.void + } + + return Effect.callback((resume) => { + if (maxPermits < permits) { + resume(Effect.never) + return + } + + if (totalPermits >= permits) { + totalPermits -= permits + resume(Effect.void) + return + } + + const needed = permits - totalPermits + const taken = permits - needed + if (totalPermits > 0) { + totalPermits = 0 + } + waitingPermits += needed + + const waiters = Option.getOrElse( + MutableHashMap.get(partitions, key), + () => { + const set = new Set() + MutableHashMap.set(partitions, key, set) + return set + } + ) + + const entry: Waiter = { + permits: needed, + resume: () => { + cleanup() + resume(Effect.void) + } + } + + const cleanup = () => { + waiters.delete(entry) + if (waiters.size === 0) { + MutableHashMap.remove(partitions, key) + } + } + + waiters.add(entry) + + return Effect.sync(() => { + cleanup() + waitingPermits -= entry.permits + if (taken > 0) { + releaseUnsafe(taken) + } + }) + }) + } + + const withPermits = + (key: K, permits: number) => (effect: Effect.Effect): Effect.Effect => { + if (permits <= 0) { + return effect + } + + const takePermits = take(key, permits) + return Effect.uninterruptibleMask((restore) => + Effect.flatMap( + restore(takePermits), + () => + Effect.ensuring( + restore(effect), + Effect.sync(() => { + releaseUnsafe(permits) + }) + ) + ) + ) + } + + const tryTake = (permits: number): boolean => { + if (permits <= 0) { + return true + } + + if (maxPermits < permits || totalPermits < permits) { + return false + } + + totalPermits -= permits + return true + } + + return { + [PartitionedTypeId]: PartitionedTypeId, + capacity: maxPermits, + available: Effect.sync(() => totalPermits), + take, + release: (permits) => Effect.sync(() => releaseUnsafe(permits)), + withPermits, + withPermit: (key) => withPermits(key, 1), + withPermitsIfAvailable: + (permits) => (effect: Effect.Effect): Effect.Effect, E, R> => { + if (permits <= 0) { + return Effect.asSome(effect) + } + + return Effect.suspend(() => { + if (!tryTake(permits)) { + return Effect.succeed(Option.none()) + } + + return Effect.ensuring( + Effect.asSome(effect), + Effect.sync(() => { + releaseUnsafe(permits) + }) + ) + }) + } + } +} + +/** + * Creates a `PartitionedSemaphore` inside an `Effect`. + * + * **When to use** + * + * Use when semaphore construction should stay inside an `Effect` workflow. + * + * **Details** + * + * The `permits` option sets the shared permit capacity. The resulting + * semaphore tracks waiters by partition key and distributes released permits + * across waiting partitions in round-robin order. + * + * **Gotchas** + * + * Negative permit counts are clamped to `0`. Non-finite permit counts create + * an unbounded semaphore. + * + * @see {@link makeUnsafe} for synchronous construction + * + * @category constructors + * @since 3.19.4 + */ +export const make = (options: { + readonly permits: number +}): Effect.Effect> => Effect.sync(() => makeUnsafe(options)) + +/** + * Gets the current number of available permits. + * + * **When to use** + * + * Use to inspect a snapshot of how many permits are currently free. + * + * **Details** + * + * Running the returned effect reads the semaphore's current availability. + * Taking permits decreases availability, and releasing permits can increase it + * up to the semaphore capacity. + * + * **Gotchas** + * + * Reading availability does not reserve permits. + * + * @see {@link capacity} for the fixed total permit capacity + * @see {@link release} for returning permits to the shared pool + * @see {@link withPermitsIfAvailable} for running only when permits are immediately available + * + * @category combinators + * @since 4.0.0 + */ +export const available = (self: PartitionedSemaphore): Effect.Effect => self.available + +/** + * Gets the total capacity. + * + * **When to use** + * + * Use to inspect the fixed number of permits configured for the semaphore. + * + * **Details** + * + * Capacity is stored when the semaphore is created and does not change as + * permits are acquired or released. + * + * @see {@link available} for the current number of free permits + * + * @category getters + * @since 4.0.0 + */ +export const capacity = (self: PartitionedSemaphore): number => self.capacity + +/** + * Returns an effect that acquires the requested number of permits for the + * given partition key. + * + * **When to use** + * + * Use when you need manual permit acquisition for a partition and want to + * control acquisition and release as separate effects. + * + * **Details** + * + * If enough permits are available, the effect completes immediately. Otherwise + * it waits until released permits are assigned to this partition. + * + * **Gotchas** + * + * Requests for more permits than the semaphore capacity never complete. + * Requests for zero or a negative number of permits complete without acquiring + * anything. + * + * @see {@link release} for manually returning permits to the shared pool + * @see {@link withPermits} for automatic acquire and release around an effect + * @see {@link withPermit} for acquiring exactly one permit around an effect + * + * @category combinators + * @since 4.0.0 + */ +export const take: { + (key: K, permits: number): (self: PartitionedSemaphore) => Effect.Effect + (self: PartitionedSemaphore, key: K, permits: number): Effect.Effect +} = dual(3, (self: PartitionedSemaphore, key: K, permits: number): Effect.Effect => self.take(key, permits)) + +/** + * Returns an effect that releases permits back to the shared pool and returns + * the current available permit count. + * + * **When to use** + * + * Use when you need to return permits acquired with `take` in a lower-level + * partitioned permit protocol with explicit release control. + * + * **Details** + * + * Released permits are first assigned to waiting partitions in round-robin + * order. Only permits not needed by waiters increase the available count, + * which is capped at the semaphore capacity. + * + * @see {@link take} for manual acquisition + * @see {@link withPermits} for automatic acquire and release around an effect + * @see {@link available} for reading the permit count without releasing + * + * @category combinators + * @since 4.0.0 + */ +export const release: { + (permits: number): (self: PartitionedSemaphore) => Effect.Effect + (self: PartitionedSemaphore, permits: number): Effect.Effect +} = dual(2, (self: PartitionedSemaphore, permits: number): Effect.Effect => self.release(permits)) + +/** + * Runs an effect after acquiring permits for a partition, then releases those + * permits when the effect exits. + * + * **When to use** + * + * Use to guard weighted partitioned work with automatic permit acquisition and + * release around an effect. + * + * **Details** + * + * Permit acquisition may wait according to `take` semantics. Once acquired, + * the permits are released even if the wrapped effect fails or is interrupted. + * + * **Gotchas** + * + * Requests for more permits than the semaphore capacity never complete. + * Requests for zero or a negative number of permits run the effect without + * acquiring anything. + * + * @see {@link withPermit} for the single-permit variant + * @see {@link withPermitsIfAvailable} for running only when permits are + * immediately available + * @see {@link take} for manual acquisition + * @see {@link release} for manual release + * + * @category combinators + * @since 4.0.0 + */ +export const withPermits: { + ( + self: PartitionedSemaphore, + key: K, + permits: number + ): (effect: Effect.Effect) => Effect.Effect + ( + self: PartitionedSemaphore, + key: K, + permits: number, + effect: Effect.Effect + ): Effect.Effect +} = ((...args: Array) => { + if (args.length === 3) { + const [self, key, permits] = args + return (effect: Effect.Effect) => self.withPermits(key, permits)(effect) + } + const [self, key, permits, effect] = args + return self.withPermits(key, permits)(effect) +}) as any + +/** + * Runs an effect after acquiring one permit for a partition, then releases the + * permit when the effect exits. + * + * **When to use** + * + * Use to guard partitioned work with exactly one permit and automatic release + * when the effect exits. + * + * **Details** + * + * This is the single-permit variant of `withPermits`. The permit is released + * even if the wrapped effect fails or is interrupted. + * + * @see {@link withPermits} for acquiring a weighted number of permits + * @see {@link withPermitsIfAvailable} for running only when permits are + * immediately available + * @see {@link take} for manual acquisition + * @see {@link release} for manual release + * + * @category combinators + * @since 4.0.0 + */ +export const withPermit: { + (self: PartitionedSemaphore, key: K): (effect: Effect.Effect) => Effect.Effect + ( + self: PartitionedSemaphore, + key: K, + effect: Effect.Effect + ): Effect.Effect +} = ((...args: Array) => { + if (args.length === 2) { + const [self, key] = args + return (effect: Effect.Effect) => self.withPermit(key)(effect) + } + const [self, key, effect] = args + return self.withPermit(key)(effect) +}) as any + +/** + * Runs an effect only when the requested permits can be acquired immediately, + * returning the result in `Some`. + * + * **When to use** + * + * Use when guarded work should run only if the shared permit pool can provide + * the requested permits immediately. + * + * **Details** + * + * If the permits are not available, the effect is not run and the result is + * `None`. When permits are acquired, they are released after the wrapped + * effect completes, fails, or is interrupted. Requests for zero or a negative + * number of permits run the effect and return `Some`. + * + * @see {@link withPermits} for the keyed variant that waits until permits are + * available for a partition + * + * @category combinators + * @since 4.0.0 + */ +export const withPermitsIfAvailable: { + ( + self: PartitionedSemaphore, + permits: number + ): (effect: Effect.Effect) => Effect.Effect, E, R> + ( + self: PartitionedSemaphore, + permits: number, + effect: Effect.Effect + ): Effect.Effect, E, R> +} = ((...args: Array) => { + if (args.length === 2) { + const [self, permits] = args + return (effect: Effect.Effect) => self.withPermitsIfAvailable(permits)(effect) + } + const [self, permits, effect] = args + return self.withPermitsIfAvailable(permits)(effect) +}) as any diff --git a/repos/effect/packages/effect/src/Path.ts b/repos/effect/packages/effect/src/Path.ts new file mode 100644 index 0000000..8acb1fd --- /dev/null +++ b/repos/effect/packages/effect/src/Path.ts @@ -0,0 +1,870 @@ +/** + * Provides path operations through the Effect environment. + * + * The `Path` service works with file system paths without tying code to one + * concrete platform module. It exposes common operations such as joining, + * normalizing, parsing, formatting, resolving, and converting paths to or from + * file URLs. This module includes the service interface, parsed path type, + * service tag, runtime marker, and built-in POSIX path layer. + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import { identity } from "./Function.ts" +import * as Layer from "./Layer.ts" +import { BadArgument } from "./PlatformError.ts" + +/** + * Runtime type identifier used to mark implementations of the `Path` service. + * + * **Details** + * + * The marker is the exact string stored on `Path` service implementations. + * Most code should depend on the `Path` service instead of inspecting this + * value directly. + * + * @see {@link layer} for the built-in POSIX `Path` service layer + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId = "~effect/platform/Path" + +/** + * Defines the service interface for platform-specific path manipulation. + * + * **When to use** + * + * Use to depend on path operations through the Effect environment instead of a + * concrete host path module. + * + * **Details** + * + * The service exposes operations for joining, normalizing, parsing, + * formatting, and converting file system paths. URL conversion methods return + * `Effect`s because invalid file URLs or paths can fail with `BadArgument`. + * + * **Example** (Using path operations) + * + * ```ts + * import { Effect, Path } from "effect" + * + * const program = Effect.gen(function*() { + * const path = yield* Path.Path + * + * // Use various path operations + * const joined = path.join("home", "user", "documents") + * const normalized = path.normalize("./path/../to/file.txt") + * const basename = path.basename("/path/to/file.txt") + * const dirname = path.dirname("/path/to/file.txt") + * const extname = path.extname("file.txt") + * const isAbs = path.isAbsolute("/absolute/path") + * const parsed = path.parse("/path/to/file.txt") + * const relative = path.relative("/from/path", "/to/path") + * const resolved = path.resolve("relative", "path") + * + * console.log({ + * joined, + * normalized, + * basename, + * dirname, + * extname, + * isAbs, + * parsed, + * relative, + * resolved + * }) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Path { + readonly [TypeId]: typeof TypeId + readonly sep: string + readonly basename: (path: string, suffix?: string) => string + readonly dirname: (path: string) => string + readonly extname: (path: string) => string + readonly format: (pathObject: Partial) => string + readonly fromFileUrl: (url: URL) => Effect.Effect + readonly isAbsolute: (path: string) => boolean + readonly join: (...paths: ReadonlyArray) => string + readonly normalize: (path: string) => string + readonly parse: (path: string) => Path.Parsed + readonly relative: (from: string, to: string) => string + readonly resolve: (...pathSegments: ReadonlyArray) => string + readonly toFileUrl: (path: string) => Effect.Effect + readonly toNamespacedPath: (path: string) => string +} + +/** + * Namespace containing types associated with the `Path` service. + * + * **When to use** + * + * Use to reference types associated with path parsing and formatting. + * + * **Example** (Working with parsed paths) + * + * ```ts + * import { Effect, Path } from "effect" + * + * // Access types and utilities in the Path namespace + * const program = Effect.gen(function*() { + * const path = yield* Path.Path + * + * // Parse a path and get a Path.Parsed object + * const parsed = path.parse("/home/user/file.txt") + * + * // The parsed object conforms to the Path.Parsed interface + * const exampleParsed = { + * root: "/", + * dir: "/home/user", + * base: "file.txt", + * ext: ".txt", + * name: "file" + * } + * + * console.log(parsed, exampleParsed) + * }) + * ``` + * + * @since 4.0.0 + */ +export declare namespace Path { + /** + * Structured representation of a parsed file system path. + * + * **When to use** + * + * Use to model the object form produced by `Path.parse` and consumed by + * `Path.format`. + * + * **Details** + * + * The fields correspond to the path root, directory, base filename, + * extension, and filename without extension, matching the shape consumed by + * `Path.format`. + * + * **Example** (Parsing and formatting paths) + * + * ```ts + * import { Effect, Path } from "effect" + * + * const program = Effect.gen(function*() { + * const path = yield* Path.Path + * + * // Parse a path into its components + * const parsed = path.parse("/home/user/documents/file.txt") + * console.log(parsed) + * // { + * // root: "/", + * // dir: "/home/user/documents", + * // base: "file.txt", + * // ext: ".txt", + * // name: "file" + * // } + * + * // Format a path from its components + * const formatted = path.format({ + * dir: "/home/user", + * name: "newfile", + * ext: ".ts" + * }) + * console.log(formatted) // "/home/user/newfile.ts" + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ + export interface Parsed { + readonly root: string + readonly dir: string + readonly base: string + readonly ext: string + readonly name: string + } +} + +/** + * Service tag for accessing the current `Path` implementation. + * + * **When to use** + * + * Use when you need path operations supplied by an effect's environment. + * + * **Example** (Providing a custom Path service) + * + * ```ts + * import { Effect, Layer, Path } from "effect" + * + * // Create a custom path implementation + * const customPath: Path.Path = { + * [Path.TypeId]: Path.TypeId, + * sep: "/", + * basename: (path: string, suffix?: string) => { + * const base = path.split("/").pop() || "" + * return suffix && base.endsWith(suffix) + * ? base.slice(0, -suffix.length) + * : base + * }, + * dirname: (path: string) => path.split("/").slice(0, -1).join("/") || "/", + * extname: (path: string) => { + * const match = path.match(/\.[^.]*$/) + * return match ? match[0] : "" + * }, + * format: (pathObject) => { + * const dir = pathObject.dir || "" + * const name = pathObject.name || "" + * const ext = pathObject.ext || "" + * return dir ? `${dir}/${name}${ext}` : `${name}${ext}` + * }, + * fromFileUrl: (url: URL) => Effect.succeed(url.pathname), + * isAbsolute: (path: string) => path.startsWith("/"), + * join: (...paths: ReadonlyArray) => paths.join("/"), + * normalize: (path: string) => path.replace(/\/+/g, "/"), + * parse: (path: string) => ({ + * root: path.startsWith("/") ? "/" : "", + * dir: path.split("/").slice(0, -1).join("/") || "/", + * base: path.split("/").pop() || "", + * ext: path.match(/\.[^.]*$/)?.[0] || "", + * name: path.split("/").pop()?.replace(/\.[^.]*$/, "") || "" + * }), + * relative: (from: string, to: string) => to.replace(from, ""), + * resolve: (...pathSegments: ReadonlyArray) => pathSegments.join("/"), + * toFileUrl: (path: string) => Effect.succeed(new URL(`file://${path}`)), + * toNamespacedPath: (path: string) => path + * } + * + * // Provide the path service + * const customPathLayer = Layer.succeed(Path.Path)(customPath) + * + * const program = Effect.gen(function*() { + * const path = yield* Path.Path + * const joined = path.join("home", "user", "file.txt") + * console.log(joined) // "home/user/file.txt" + * }) + * + * // Run with custom path implementation + * const result = Effect.provide(program, customPathLayer) + * ``` + * + * @category services + * @since 4.0.0 + */ +export const Path: Context.Service = Context.Service("effect/Path") + +/** + * The following functions are adapted from the Node.js source code: + * https://github.com/nodejs/node/blob/main/lib/internal/url.js + * + * The following license applies to these functions: + * - MIT + */ + +// Resolves . and .. elements in a path with directory names +function normalizeStringPosix(path: string, allowAboveRoot: boolean) { + let res = "" + let lastSegmentLength = 0 + let lastSlash = -1 + let dots = 0 + let code + for (let i = 0; i <= path.length; ++i) { + if (i < path.length) { + code = path.charCodeAt(i) + } else if (code === 47 /*/*/) { + break + } else { + code = 47 /*/*/ + } + if (code === 47 /*/*/) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if ( + res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 /*.*/ || + res.charCodeAt(res.length - 2) !== 46 /*.*/ + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf("/") + if (lastSlashIndex !== res.length - 1) { + if (lastSlashIndex === -1) { + res = "" + lastSegmentLength = 0 + } else { + res = res.slice(0, lastSlashIndex) + lastSegmentLength = res.length - 1 - res.lastIndexOf("/") + } + lastSlash = i + dots = 0 + continue + } + } else if (res.length === 2 || res.length === 1) { + res = "" + lastSegmentLength = 0 + lastSlash = i + dots = 0 + continue + } + } + if (allowAboveRoot) { + if (res.length > 0) { + res += "/.." + } else { + res = ".." + } + lastSegmentLength = 2 + } + } else { + if (res.length > 0) { + res += "/" + path.slice(lastSlash + 1, i) + } else { + res = path.slice(lastSlash + 1, i) + } + lastSegmentLength = i - lastSlash - 1 + } + lastSlash = i + dots = 0 + } else if (code === 46 /*.*/ && dots !== -1) { + ++dots + } else { + dots = -1 + } + } + return res +} + +function _format(sep: string, pathObject: Partial) { + const dir = pathObject.dir || pathObject.root + const base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "") + if (!dir) { + return base + } + if (dir === pathObject.root) { + return dir + base + } + return dir + sep + base +} + +function fromFileUrl(url: URL): Effect.Effect { + if (url.protocol !== "file:") { + return Effect.fail( + new BadArgument({ + module: "Path", + method: "fromFileUrl", + description: "URL must be of scheme file" + }) + ) + } else if (url.hostname !== "") { + return Effect.fail( + new BadArgument({ + module: "Path", + method: "fromFileUrl", + description: "Invalid file URL host" + }) + ) + } + const pathname = url.pathname + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === "%") { + const third = pathname.codePointAt(n + 2)! | 0x20 + if (pathname[n + 1] === "2" && third === 102) { + return Effect.fail( + new BadArgument({ + module: "Path", + method: "fromFileUrl", + description: "must not include encoded / characters" + }) + ) + } + } + } + return Effect.succeed(decodeURIComponent(pathname)) +} + +const resolve: Path["resolve"] = function resolve() { + let resolvedPath = "" + let resolvedAbsolute = false + let cwd: string | undefined = undefined + + for (let i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + let path: string + if (i >= 0) { + path = arguments[i] + } else { + const process = (globalThis as any).process + if ( + cwd === undefined && "process" in globalThis && + typeof process === "object" && + process !== null && + typeof process.cwd === "function" + ) { + cwd = process.cwd() + } + path = cwd! + } + + // Skip empty entries + if (path.length === 0) { + continue + } + + resolvedPath = path + "/" + resolvedPath + resolvedAbsolute = path.charCodeAt(0) === 47 /*/*/ + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute) + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) { + return "/" + resolvedPath + } else { + return "/" + } + } else if (resolvedPath.length > 0) { + return resolvedPath + } else { + return "." + } +} + +const CHAR_FORWARD_SLASH = 47 + +function toFileUrl(filepath: string) { + const outURL = new URL("file://") + let resolved = resolve(filepath) + // path.resolve strips trailing slashes so we must add them back + const filePathLast = filepath.charCodeAt(filepath.length - 1) + if ( + (filePathLast === CHAR_FORWARD_SLASH) && + resolved[resolved.length - 1] !== "/" + ) { + resolved += "/" + } + outURL.pathname = encodePathChars(resolved) + return Effect.succeed(outURL) +} + +const percentRegExp = /%/g +const backslashRegExp = /\\/g +const newlineRegExp = /\n/g +const carriageReturnRegExp = /\r/g +const tabRegExp = /\t/g + +function encodePathChars(filepath: string) { + if (filepath.includes("%")) { + filepath = filepath.replace(percentRegExp, "%25") + } + if (filepath.includes("\\")) { + filepath = filepath.replace(backslashRegExp, "%5C") + } + if (filepath.includes("\n")) { + filepath = filepath.replace(newlineRegExp, "%0A") + } + if (filepath.includes("\r")) { + filepath = filepath.replace(carriageReturnRegExp, "%0D") + } + if (filepath.includes("\t")) { + filepath = filepath.replace(tabRegExp, "%09") + } + return filepath +} + +const posixImpl = Path.of({ + [TypeId]: TypeId, + resolve, + normalize(path) { + if (path.length === 0) return "." + + const isAbsolute = path.charCodeAt(0) === 47 /*/*/ + const trailingSeparator = path.charCodeAt(path.length - 1) === 47 /*/*/ + + // Normalize the path + path = normalizeStringPosix(path, !isAbsolute) + + if (path.length === 0 && !isAbsolute) path = "." + if (path.length > 0 && trailingSeparator) path += "/" + + if (isAbsolute) return "/" + path + return path + }, + + isAbsolute(path) { + return path.length > 0 && path.charCodeAt(0) === 47 /*/*/ + }, + + join() { + if (arguments.length === 0) { + return "." + } + let joined + for (let i = 0; i < arguments.length; ++i) { + const arg = arguments[i] + if (arg.length > 0) { + if (joined === undefined) { + joined = arg + } else { + joined += "/" + arg + } + } + } + if (joined === undefined) { + return "." + } + return posixImpl.normalize(joined) + }, + + relative(from, to) { + if (from === to) return "" + + from = posixImpl.resolve(from) + to = posixImpl.resolve(to) + + if (from === to) return "" + + // Trim any leading backslashes + let fromStart = 1 + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 47 /*/*/) { + break + } + } + const fromEnd = from.length + const fromLen = fromEnd - fromStart + + // Trim any leading backslashes + let toStart = 1 + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 47 /*/*/) { + break + } + } + const toEnd = to.length + const toLen = toEnd - toStart + + // Compare paths to find the longest common path from root + const length = fromLen < toLen ? fromLen : toLen + let lastCommonSep = -1 + let i = 0 + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === 47 /*/*/) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(toStart + i + 1) + } else if (i === 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return to.slice(toStart + i) + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === 47 /*/*/) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = i + } else if (i === 0) { + // We get here if `to` is the root. + // For example: from='/foo'; to='/' + lastCommonSep = 0 + } + } + break + } + const fromCode = from.charCodeAt(fromStart + i) + const toCode = to.charCodeAt(toStart + i) + if (fromCode !== toCode) { + break + } else if (fromCode === 47 /*/*/) { + lastCommonSep = i + } + } + + let out = "" + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 47 /*/*/) { + if (out.length === 0) { + out += ".." + } else { + out += "/.." + } + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) { + return out + to.slice(toStart + lastCommonSep) + } else { + toStart += lastCommonSep + if (to.charCodeAt(toStart) === 47 /*/*/) { + ++toStart + } + return to.slice(toStart) + } + }, + + dirname(path) { + if (path.length === 0) return "." + let code = path.charCodeAt(0) + const hasRoot = code === 47 /*/*/ + let end = -1 + let matchedSlash = true + for (let i = path.length - 1; i >= 1; --i) { + code = path.charCodeAt(i) + if (code === 47 /*/*/) { + if (!matchedSlash) { + end = i + break + } + } else { + // We saw the first non-path separator + matchedSlash = false + } + } + + if (end === -1) return hasRoot ? "/" : "." + if (hasRoot && end === 1) return "//" + return path.slice(0, end) + }, + + basename(path, ext) { + let start = 0 + let end = -1 + let matchedSlash = true + let i + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return "" + let extIdx = ext.length - 1 + let firstNonSlashEnd = -1 + for (i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i) + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1 + break + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false + firstNonSlashEnd = i + 1 + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1 + end = firstNonSlashEnd + } + } + } + } + + if (start === end) end = firstNonSlashEnd + else if (end === -1) end = path.length + return path.slice(start, end) + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1 + break + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false + end = i + 1 + } + } + + if (end === -1) return "" + return path.slice(start, end) + } + }, + + extname(path) { + let startDot = -1 + let startPart = 0 + let end = -1 + let matchedSlash = true + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0 + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i) + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1 + break + } + continue + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false + end = i + 1 + } + if (code === 46 /*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) { + startDot = i + } else if (preDotState !== 1) { + preDotState = 1 + } + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1 + } + } + + if ( + startDot === -1 || end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1 + ) { + return "" + } + return path.slice(startDot, end) + }, + + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError("The \"pathObject\" argument must be of type Object. Received type " + typeof pathObject) + } + return _format("/", pathObject) + }, + + parse(path) { + const ret = { root: "", dir: "", base: "", ext: "", name: "" } + if (path.length === 0) return ret + let code = path.charCodeAt(0) + const isAbsolute = code === 47 /*/*/ + let start + if (isAbsolute) { + ret.root = "/" + start = 1 + } else { + start = 0 + } + let startDot = -1 + let startPart = 0 + let end = -1 + let matchedSlash = true + let i = path.length - 1 + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0 + + // Get non-dir info + for (; i >= start; --i) { + code = path.charCodeAt(i) + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1 + break + } + continue + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false + end = i + 1 + } + if (code === 46 /*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i + else if (preDotState !== 1) preDotState = 1 + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1 + } + } + + if ( + startDot === -1 || end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1 + ) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end) + else ret.base = ret.name = path.slice(startPart, end) + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot) + ret.base = path.slice(1, end) + } else { + ret.name = path.slice(startPart, startDot) + ret.base = path.slice(startPart, end) + } + ret.ext = path.slice(startDot, end) + } + + if (startPart > 0) ret.dir = path.slice(0, startPart - 1) + else if (isAbsolute) ret.dir = "/" + + return ret + }, + + sep: "/", + fromFileUrl, + toFileUrl, + toNamespacedPath: identity +}) + +/** + * Layer that provides the built-in POSIX `Path` implementation. + * + * **When to use** + * + * Use when you need an effect that requires the `Path` service to run with the + * built-in POSIX path implementation. + * + * **Details** + * + * The layer provides a static service whose separator is `/` and whose + * operations use POSIX path semantics. + * + * @see {@link Path} for accessing the `Path` service from an effect + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.succeed(Path)(posixImpl) diff --git a/repos/effect/packages/effect/src/Pipeable.ts b/repos/effect/packages/effect/src/Pipeable.ts new file mode 100644 index 0000000..e58dfac --- /dev/null +++ b/repos/effect/packages/effect/src/Pipeable.ts @@ -0,0 +1,674 @@ +/** + * The `Pipeable` module defines the shared interface and implementation helpers + * for values that support Effect-style method chaining with `.pipe(...)`. + * + * A `Pipeable` value can pass itself through a sequence of unary functions from + * left to right, so code can be written as `value.pipe(f, g, h)` instead of + * deeply nesting calls. This is the method form used by many Effect data types + * to compose transformations, validations, and effectful operations while + * keeping the original value as the starting point of the pipeline. + * + * @since 2.0.0 + */ + +/** + * Interface for values that support method-style `pipe` composition. + * + * **When to use** + * + * Use to type values that expose an Effect-style `.pipe(...)` method. + * + * **Details** + * + * Calling `value.pipe(f, g, h)` passes the value through each function from + * left to right, returning the final result. Many Effect data types implement + * this so operations can be chained without nesting function calls. + * + * **Example** (Chaining operations with pipe) + * + * ```ts + * import { Effect } from "effect" + * + * // The Pipeable interface allows Effect values to be chained using the pipe method + * const program = Effect.succeed(1).pipe( + * Effect.map((x) => x + 1), + * Effect.flatMap((x) => Effect.succeed(x * 2)), + * Effect.tap((x) => Effect.log(`Result: ${x}`)) + * ) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Pipeable { + pipe(this: A): A + pipe(this: A, ab: (_: A) => B): B + pipe(this: A, ab: (_: A) => B, bc: (_: B) => C): C + pipe(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E + ): E + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F + ): F + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G + ): G + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H + ): H + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I + ): I + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J + ): J + pipe( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K + ): K + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L + ): L + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M + ): M + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N + ): N + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O + ): O + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P + ): P + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q + ): Q + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R + ): R + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S + ): S + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T + ): T + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never, + U = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T, + tu: (_: T) => U + ): U + pipe< + A, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S = never, + T = never, + U = never + >( + this: A, + ab: (_: A) => B, + bc: (_: B) => C, + cd: (_: C) => D, + de: (_: D) => E, + ef: (_: E) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => L, + lm: (_: L) => M, + mn: (_: M) => N, + no: (_: N) => O, + op: (_: O) => P, + pq: (_: P) => Q, + qr: (_: Q) => R, + rs: (_: R) => S, + st: (_: S) => T, + tu: (_: T) => U + ): U +} + +/** + * Applies a `pipe` method's variadic arguments to an initial value from left + * to right. + * + * **When to use** + * + * Use to implement a custom `.pipe(...)` method from JavaScript's `arguments` + * object. + * + * **Details** + * + * This helper is intended for implementing `Pipeable.pipe` methods that + * receive JavaScript's `arguments` object. With no functions it returns the + * original value; otherwise it feeds each result into the next function. + * + * **Example** (Implementing a pipe method) + * + * ```ts + * import { Pipeable } from "effect" + * + * class NumberBox { + * constructor(readonly value: number) {} + * + * pipe(..._fns: ReadonlyArray<(value: number) => number>): number { + * return Pipeable.pipeArguments(this.value, arguments) as number + * } + * } + * + * const result = new NumberBox(5).pipe( + * (n) => n + 2, + * (n) => n * 3 + * ) + * console.log(result) // 21 + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const pipeArguments = (self: A, args: IArguments): unknown => { + switch (args.length) { + case 0: + return self + case 1: + return args[0](self) + case 2: + return args[1](args[0](self)) + case 3: + return args[2](args[1](args[0](self))) + case 4: + return args[3](args[2](args[1](args[0](self)))) + case 5: + return args[4](args[3](args[2](args[1](args[0](self))))) + case 6: + return args[5](args[4](args[3](args[2](args[1](args[0](self)))))) + case 7: + return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))) + case 8: + return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))) + case 9: + return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))))) + default: { + let ret = self + for (let i = 0, len = args.length; i < len; i++) { + ret = args[i](ret) + } + return ret + } + } +} + +/** + * Reusable prototype that implements `Pipeable.pipe`. + * + * **When to use** + * + * Use when classes or object prototypes can reuse this value when they need the + * standard pipe implementation backed by `pipeArguments`. + * + * @category prototypes + * @since 3.15.0 + */ +export const Prototype: Pipeable = { + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Provides a base constructor whose instances implement the standard `Pipeable.pipe` + * method. + * + * **When to use** + * + * Use when you need to define a class that supports Effect-style method + * chaining through `.pipe(...)`. + * + * @category constructors + * @since 3.15.0 + */ +export const Class: new() => Pipeable = (function() { + function PipeableBase() {} + PipeableBase.prototype = Prototype + return PipeableBase as any +})() + +/** + * Constructor type for classes whose instances implement `Pipeable`. + * + * **When to use** + * + * Use as the constructor-side type when a class value should be known to create + * instances that support Effect-style method chaining with `.pipe(...)`. + * + * @see {@link Pipeable} for the instance-side contract + * @see {@link Class} for the base constructor + * @see {@link Mixin} for wrapping an existing class constructor + * + * @category models + * @since 3.15.0 + */ +export interface PipeableConstructor { + new(...args: ReadonlyArray): Pipeable +} + +/** + * Returns a subclass of the provided class that adds the standard `pipe` + * method. + * + * **When to use** + * + * Use to add pipe support to an existing class without extending a base class + * or modifying its prototype. + * + * **Details** + * + * The original constructor and instance members are preserved, and the added + * method delegates to `pipeArguments`. + * + * @see {@link Prototype} for a reusable prototype object + * @see {@link Class} for a base constructor to extend + * @category constructors + * @since 4.0.0 + */ +export const Mixin = ) => any>( + klass: TBase +): TBase & PipeableConstructor => (class extends klass { + pipe() { + return pipeArguments(this, arguments) + } +}) diff --git a/repos/effect/packages/effect/src/PlatformError.ts b/repos/effect/packages/effect/src/PlatformError.ts new file mode 100644 index 0000000..8b3dd7c --- /dev/null +++ b/repos/effect/packages/effect/src/PlatformError.ts @@ -0,0 +1,221 @@ +/** + * Normalized errors for platform APIs. + * + * Platform services such as file systems, terminals, and sockets use + * `PlatformError` to report host-level failures in a consistent shape. The + * wrapper records whether the problem came from an invalid argument or from the + * operating system, while preserving useful details such as the module, method, + * path, descriptor, description, and original cause when available. + * + * @since 4.0.0 + */ +import * as Data from "./Data.ts" + +const TypeId = "~effect/platform/PlatformError" + +/** + * Error data for an invalid argument passed to a platform API. + * + * **When to use** + * + * Use when you need to model caller input rejected before a platform operation + * runs, including invalid-argument reason data. + * + * **Details** + * + * The error records the module and method that rejected the argument, with an + * optional description and cause. It is usually wrapped in `PlatformError`. + * + * @see {@link badArgument} for creating a wrapped `PlatformError` whose reason is `BadArgument` + * @see {@link SystemError} for failures reported by the host platform or operating system + * @see {@link PlatformError} for the wrapper used by most platform APIs + * + * @category models + * @since 4.0.0 + */ +export class BadArgument extends Data.TaggedError("BadArgument")<{ + module: string + method: string + description?: string | undefined + cause?: unknown +}> { + /** + * Formats the module, method, and optional description that rejected the argument. + * + * **When to use** + * + * Use to read the formatted error message for a rejected platform argument. + * + * @since 4.0.0 + */ + override get message(): string { + return `${this.module}.${this.method}${this.description ? `: ${this.description}` : ""}` + } +} + +/** + * Normalized category for failures reported by platform or system operations. + * + * **When to use** + * + * Use to type or match the normalized `_tag` on `SystemError` values reported + * by platform operations. + * + * **Details** + * + * The tags group lower-level platform errors into a stable set such as + * `NotFound`, `PermissionDenied`, `TimedOut`, and `Unknown`. + * + * @see {@link SystemError} for the error data that carries this tag on its `_tag` field + * @see {@link systemError} for creating a `PlatformError` from a system failure with one of these tags + * + * @category models + * @since 4.0.0 + */ +export type SystemErrorTag = + | "AlreadyExists" + | "BadResource" + | "Busy" + | "InvalidData" + | "NotFound" + | "PermissionDenied" + | "TimedOut" + | "UnexpectedEof" + | "Unknown" + | "WouldBlock" + | "WriteZero" + +/** + * Error data for a platform or system operation failure. + * + * **When to use** + * + * Use when you need normalized reason data for a platform or system operation + * failure, including the operation details. + * + * **Details** + * + * The error records a normalized `_tag`, the module and method that failed, + * and optional details such as the syscall, path or descriptor, description, + * and original cause. It is usually wrapped in `PlatformError`. + * + * @see {@link systemError} for creating the usual `PlatformError` wrapper from this reason data + * @see {@link BadArgument} for platform API failures caused by rejected caller input before an operation runs + * @see {@link SystemErrorTag} for the normalized tag values stored in `_tag` + * + * @category models + * @since 4.0.0 + */ +export class SystemError extends Data.Error<{ + _tag: SystemErrorTag + module: string + method: string + description?: string | undefined + syscall?: string | undefined + pathOrDescriptor?: string | number | undefined + cause?: unknown +}> { + /** + * Formats the normalized system error tag with operation and path details. + * + * **When to use** + * + * Use to read the formatted error message for a normalized system failure. + * + * @since 4.0.0 + */ + override get message(): string { + return `${this._tag}: ${this.module}.${this.method}${ + this.pathOrDescriptor !== undefined ? ` (${this.pathOrDescriptor})` : "" + }${this.description ? `: ${this.description}` : ""}` + } +} + +/** + * Tagged error used by platform APIs to report either invalid arguments or + * system-level failures. + * + * **When to use** + * + * Use as the shared error type for platform APIs that expose invalid arguments + * and host or operating-system failures through a single `Effect` error + * channel. + * + * **Details** + * + * The `reason` field contains the underlying `BadArgument` or `SystemError`. + * When that reason has a cause, the cause is preserved on the wrapper. + * + * @see {@link BadArgument} for invalid inputs rejected before an operation runs + * @see {@link SystemError} for failures reported by the host platform or operating system + * @see {@link badArgument} for creating this wrapper from rejected caller input + * @see {@link systemError} for creating this wrapper from a host or operating-system failure + * + * @category models + * @since 4.0.0 + */ +export class PlatformError extends Data.TaggedError("PlatformError")<{ + reason: BadArgument | SystemError +}> { + constructor(reason: BadArgument | SystemError) { + if ("cause" in reason) { + super({ reason, cause: reason.cause } as any) + } else { + super({ reason }) + } + } + + /** + * Marks this value as a platform error wrapper for runtime guards. + * + * **When to use** + * + * Use to identify `PlatformError` values through their runtime type marker. + * + * @since 4.0.0 + */ + readonly [TypeId]: typeof TypeId = TypeId + + override get message(): string { + return this.reason.message + } +} + +/** + * Creates a `PlatformError` whose reason is a `SystemError`. + * + * **When to use** + * + * Use to adapt an operating-system or platform failure into the normalized + * platform error model. + * + * @category constructors + * @since 4.0.0 + */ +export const systemError = (options: { + readonly _tag: SystemErrorTag + readonly module: string + readonly method: string + readonly description?: string | undefined + readonly syscall?: string | undefined + readonly pathOrDescriptor?: string | number | undefined + readonly cause?: unknown +}): PlatformError => new PlatformError(new SystemError(options)) + +/** + * Creates a `PlatformError` whose reason is a `BadArgument`. + * + * **When to use** + * + * Use to report a platform API rejecting caller input before performing the + * underlying operation. + * + * @category constructors + * @since 4.0.0 + */ +export const badArgument = (options: { + readonly module: string + readonly method: string + readonly description?: string | undefined + readonly cause?: unknown +}): PlatformError => new PlatformError(new BadArgument(options)) diff --git a/repos/effect/packages/effect/src/Pool.ts b/repos/effect/packages/effect/src/Pool.ts new file mode 100644 index 0000000..66d034f --- /dev/null +++ b/repos/effect/packages/effect/src/Pool.ts @@ -0,0 +1,699 @@ +/** + * Shares scoped resources across fibers. + * + * A `Pool` acquires resource-backed values with a scoped effect, lets + * fibers borrow them with `get`, can invalidate broken values, and releases all + * acquired values when the pool scope closes. This module includes fixed-size + * pools, pools that resize with a time-to-live policy, custom strategy pools, + * per-item concurrency limits, and runtime state types used by pool strategies. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.ts" +import { Clock } from "./Clock.ts" +import * as Context from "./Context.ts" +import * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import type * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import { dual, identity } from "./Function.ts" +import * as Iterable from "./Iterable.ts" +import * as Latch from "./Latch.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as Queue from "./Queue.ts" +import { UnhandledLogLevel } from "./References.ts" +import * as Scope from "./Scope.ts" +import * as Semaphore from "./Semaphore.ts" + +const TypeId = "~effect/Pool" + +/** + * A `Pool` is a pool of items of type `A`, each of which may be + * associated with the acquisition and release of resources. An attempt to get + * an item `A` from a pool may fail with an error of type `E`. + * + * **When to use** + * + * Use when you need to share a bounded set of scoped resources across fibers + * while the pool manages acquisition, reuse, and release. + * + * @see {@link make} for creating a pool with size bounds + * @see {@link makeWithTTL} for creating a pool with idle item expiration + * @see {@link makeWithStrategy} for creating a pool with a custom strategy + * @see {@link get} for acquiring an item from a pool + * @see {@link invalidate} for removing a broken item from the pool + * + * @category models + * @since 2.0.0 + */ +export interface Pool extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly config: Config + readonly state: State +} + +/** + * Normalized configuration used by a `Pool`. + * + * **When to use** + * + * Use as the normalized, read-only description of how a pool acquires, sizes, + * shares, and resizes its items after construction. + * + * **Details** + * + * The config stores the acquire effect, size bounds, per-item concurrency, + * target utilization, and resizing strategy used by the pool implementation. + * + * @see {@link Pool} for the value exposing this configuration + * @see {@link State} for mutable runtime state instead of static configuration + * @see {@link Strategy} for the resizing and reclamation contract stored on the config + * + * @category models + * @since 4.0.0 + */ +export interface Config { + readonly acquire: Effect.Effect + readonly concurrency: number + readonly minSize: number + readonly maxSize: number + readonly strategy: Strategy + readonly targetUtilization: number +} + +/** + * Mutable runtime state maintained by a `Pool`. + * + * **When to use** + * + * Use when you need to inspect or support the runtime state backing a `Pool`, + * including its scope, item sets, semaphores, waiters, invalidation tracking, + * and shutdown flag. + * + * **Details** + * + * This state tracks the pool scope, active and available items, invalidated + * items, semaphores, waiters, and shutdown status. It is exposed for + * inspection and implementation support; user code should prefer the + * high-level pool operations. + * + * @see {@link Pool} for the pool value exposing this state + * @see {@link PoolItem} for the entries stored in the runtime item sets + * @see {@link get} for acquiring items through the high-level API + * @see {@link invalidate} for invalidating items through the high-level API + * + * @category models + * @since 4.0.0 + */ +export interface State { + readonly scope: Scope.Scope + isShuttingDown: boolean + readonly semaphore: Semaphore.Semaphore + readonly resizeSemaphore: Semaphore.Semaphore + readonly items: Set> + readonly available: Set> + readonly availableLatch: Latch.Latch + readonly invalidated: Set> + waiters: number +} + +/** + * Internal record for a value managed by a `Pool`. + * + * **When to use** + * + * Use when implementing a custom pool `Strategy` that needs to inspect + * acquired items, track reference counts, or return reclaimable items to the + * pool. + * + * **Details** + * + * Each item stores the acquisition `Exit`, its finalizer, the current + * reference count, and whether automatic reclaiming has been disabled because + * the item was invalidated. + * + * @see {@link Strategy} for the custom strategy callbacks that receive and return pool items + * @see {@link State} for the runtime sets that store active, available, and invalidated pool items + * + * @category models + * @since 4.0.0 + */ +export interface PoolItem { + readonly exit: Exit.Exit + finalizer: Effect.Effect + refCount: number + disableReclaim: boolean +} + +/** + * Strategy used by a `Pool` to manage background resizing and item + * reclamation. + * + * **When to use** + * + * Use when defining a custom pool lifecycle policy that needs to run background + * work, observe acquired items, or choose items for reclamation. + * + * **Details** + * + * `run` starts any strategy-specific background work, `onAcquire` is invoked + * when an item is acquired, and `reclaim` selects an item that can be removed + * or replaced. + * + * @see {@link makeWithStrategy} for constructing a pool from a custom `Strategy` + * + * @category models + * @since 4.0.0 + */ +export interface Strategy { + readonly run: (pool: Pool) => Effect.Effect + readonly onAcquire: (item: PoolItem) => Effect.Effect + readonly reclaim: (pool: Pool) => Effect.Effect | undefined> +} + +/** + * Returns `true` if the specified value is a `Pool`, `false` otherwise. + * + * **When to use** + * + * Use to validate unknown values at runtime boundaries before treating them as + * `Pool` values. + * + * **Details** + * + * This predicate narrows the input to `Pool`. + * + * @category refinements + * @since 2.0.0 + */ +export const isPool = (u: unknown): u is Pool => hasProperty(u, TypeId) + +/** + * Makes a new pool of the specified fixed size. + * + * **When to use** + * + * Use when you need a fixed-size pool with no growth or shrinkage. + * + * **Details** + * + * The pool is returned in a `Scope`, which governs the lifetime of the pool. + * When the pool is shutdown because the `Scope` is closed, the individual + * items allocated by the pool will be released in some unspecified order. + * + * By setting the `concurrency` parameter, you can control the level of concurrent + * access per pool item. By default, the number of permits is set to `1`. + * + * `targetUtilization` determines when to create new pool items. It is a value + * between 0 and 1, where 1 means only create new pool items when all the existing + * items are fully utilized. + * + * A `targetUtilization` of 0.5 will create new pool items when the existing items are + * 50% utilized. + * + * @see {@link makeWithTTL} for pools with min/max sizes and a TTL-based shrinking policy + * @see {@link makeWithStrategy} for pools with a custom resizing and reclamation strategy + * @category constructors + * @since 2.0.0 + */ +export const make = (options: { + readonly acquire: Effect.Effect + readonly size: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined +}): Effect.Effect, never, R | Scope.Scope> => + makeWithStrategy({ ...options, min: options.size, max: options.size, strategy: strategyNoop() }) + +/** + * Creates a scoped pool with minimum and maximum sizes and a time-to-live + * policy for shrinking unused excess items. + * + * **When to use** + * + * Use to create an elastic scoped pool that can grow up to a maximum size and + * later reclaim unused excess items. + * + * **Details** + * + * The returned pool requires `Scope`; when that scope is closed, allocated + * items are released in an unspecified order. `concurrency` controls how many + * fibers may use each pool item at once and defaults to `1`. + * + * `targetUtilization` controls when new items are created and is clamped by the + * pool implementation. A value of `1` waits until existing items are fully + * utilized before creating more items. + * + * `timeToLiveStrategy` controls when excess items expire: `"creation"` measures + * from item creation, while `"usage"` measures from pool usage. The default is + * `"usage"`. + * + * **Example** (Creating a connection pool) + * + * ```ts + * import { Duration, Effect, Pool } from "effect" + * + * interface Connection { + * readonly execute: (sql: string) => Effect.Effect> + * readonly close: Effect.Effect + * } + * + * const acquireDBConnection = Effect.acquireRelease( + * Effect.succeed({ + * execute: (sql) => Effect.succeed([`executed: ${sql}`]), + * close: Effect.void + * } satisfies Connection), + * (connection) => connection.close + * ) + * + * const program = Effect.scoped( + * Effect.flatMap( + * Pool.makeWithTTL({ + * acquire: acquireDBConnection, + * min: 10, + * max: 20, + * timeToLive: Duration.seconds(60) + * }), + * (pool) => Effect.flatMap(Pool.get(pool), (connection) => connection.execute("select 1")) + * ) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const makeWithTTL = (options: { + readonly acquire: Effect.Effect + readonly min: number + readonly max: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly timeToLive: Duration.Input + readonly timeToLiveStrategy?: "creation" | "usage" | undefined +}): Effect.Effect, never, R | Scope.Scope> => + Effect.flatMap( + options.timeToLiveStrategy === "creation" ? + strategyCreationTTL(options.timeToLive) : + strategyUsageTTL(options.timeToLive), + (strategy) => makeWithStrategy({ ...options, strategy }) + ) + +/** + * Creates a scoped pool using a custom resizing and reclamation strategy. + * + * **When to use** + * + * Use to build a pool whose item lifecycle is controlled by an explicit + * `Strategy`, such as custom background resizing, replacement, or reclamation. + * + * **Details** + * + * The returned pool requires `Scope`; closing the scope shuts down the pool and + * releases allocated items. + * + * @see {@link make} for fixed-size pools without custom resizing or reclamation + * @see {@link makeWithTTL} for min/max pools that shrink excess items with a TTL policy + * @see {@link Strategy} for the custom strategy contract consumed by this constructor + * + * @category constructors + * @since 4.0.0 + */ +export const makeWithStrategy = (options: { + readonly acquire: Effect.Effect + readonly min: number + readonly max: number + readonly concurrency?: number | undefined + readonly targetUtilization?: number | undefined + readonly strategy: Strategy +}): Effect.Effect, never, Scope.Scope | R> => + Effect.uninterruptibleMask(Effect.fnUntraced(function*(restore) { + const services = yield* Effect.context() + const scope = Context.get(services, Scope.Scope) + const acquire = Effect.updateContext( + options.acquire, + (input) => Context.merge(services, input) + ) as Effect.Effect + const concurrency = options.concurrency ?? 1 + + const config: Config = { + acquire, + concurrency, + minSize: options.min, + maxSize: options.max, + strategy: options.strategy, + targetUtilization: Math.min(Math.max(options.targetUtilization ?? 1, 0.1), 1) + } + const state: State = { + scope, + isShuttingDown: false, + semaphore: Semaphore.makeUnsafe(concurrency * options.max), + resizeSemaphore: Semaphore.makeUnsafe(1), + items: new Set(), + available: new Set(), + availableLatch: Latch.makeUnsafe(false), + invalidated: new Set(), + waiters: 0 + } + const self: Pool = { + [TypeId]: TypeId, + config, + state, + pipe() { + return pipeArguments(this, arguments) + } + } + yield* Scope.addFinalizer(scope, shutdown(self)) + yield* Effect.tap( + Effect.forkDetach(restore(resize(self))), + (fiber) => Scope.addFinalizer(scope, Fiber.interrupt(fiber)) + ) + yield* Effect.tap( + Effect.forkDetach(restore(options.strategy.run(self))), + (fiber) => Scope.addFinalizer(scope, Fiber.interrupt(fiber)) + ) + return self + })) + +const shutdown = Effect.fnUntraced(function*(self: Pool) { + if (self.state.isShuttingDown) return + self.state.isShuttingDown = true + const size = self.state.items.size + const semaphore = Semaphore.makeUnsafe(size) + for (const item of self.state.items) { + if (item.refCount > 0) { + item.finalizer = Effect.tap(item.finalizer, semaphore.release(1)) + self.state.invalidated.add(item) + yield* semaphore.take(1) + } else { + self.state.items.delete(item) + self.state.available.delete(item) + self.state.invalidated.delete(item) + yield* item.finalizer + } + } + yield* semaphore.releaseAll + self.state.availableLatch.openUnsafe() + yield* semaphore.take(size) +}) + +/** + * Retrieves an item from the pool in a scoped effect. + * + * **When to use** + * + * Use to borrow a pooled resource for the lifetime of the current scope so it + * is automatically returned when that scope closes. + * + * **Details** + * + * The returned effect waits for an available item when the pool is at capacity. + * If acquiring a new item fails, the effect fails with the acquisition error. + * + * **Gotchas** + * + * Retrying a failed `get` can repeat the acquisition attempt. + * + * @see {@link invalidate} for removing an unhealthy item from future reuse + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: Pool): Effect.Effect => + Effect.suspend(() => { + if (self.state.isShuttingDown) { + return Effect.interrupt + } + return Effect.flatMap(getPoolItem(self), (item) => item.exit) + }) + +const getPoolItem = (self: Pool): Effect.Effect, never, Scope.Scope> => + Effect.uninterruptibleMask((restore) => + restore(self.state.semaphore.take(1)).pipe( + Effect.flatMap(() => Effect.scope), + Effect.flatMap((scope) => + getPoolItemInner(self).pipe( + Effect.ensuring(Effect.sync(() => self.state.waiters--)), + Effect.tap((item) => { + if (item.exit._tag === "Failure") { + self.state.items.delete(item) + self.state.invalidated.delete(item) + self.state.available.delete(item) + return self.state.semaphore.release(1) + } + item.refCount++ + self.state.available.delete(item) + if (item.refCount < self.config.concurrency) { + self.state.available.add(item) + } + return Scope.addFinalizerExit(scope, () => + Effect.flatMap( + Effect.suspend(() => { + item.refCount-- + if (self.state.invalidated.has(item)) { + return invalidatePoolItem(self, item) + } + self.state.available.add(item) + return Effect.void + }), + () => self.state.semaphore.release(1) + )) + }), + Effect.onInterrupt(() => self.state.semaphore.release(1)) + ) + ) + ) + ) + +const getPoolItemInner = Effect.fnUntraced(function*( + self: Pool +) { + self.state.waiters++ + if (self.state.isShuttingDown) { + return yield* Effect.interrupt + } else if (targetSize(self) > activeSize(self)) { + while (true) { + yield* self.state.resizeSemaphore.withPermitsIfAvailable(1)( + Effect.forkIn(Effect.interruptible(resize(self)), self.state.scope) + ) + if (self.state.isShuttingDown) { + return yield* Effect.interrupt + } else if (self.state.available.size > 0) { + return Iterable.headUnsafe(self.state.available) + } + self.state.availableLatch.closeUnsafe() + yield* self.state.availableLatch.await + } + } + return Iterable.headUnsafe(self.state.available) +}) + +/** + * Invalidates the specified item so the pool can remove it and reallocate the + * item, lazily if needed. + * + * **When to use** + * + * Use to prevent a pooled item from being reused after it becomes unsuitable, + * such as a stale connection or a resource that failed a health check. + * + * **Gotchas** + * + * The item is matched with strict equality. Passing an equivalent but different + * object instance does nothing. + * + * @see {@link get} for retrieving scoped items from the pool + * + * @category combinators + * @since 2.0.0 + */ +export const invalidate: { + (item: A): (self: Pool) => Effect.Effect + (self: Pool, item: A): Effect.Effect +} = dual(2, (self: Pool, item: A): Effect.Effect => + Effect.suspend(() => { + if (self.state.isShuttingDown) return Effect.void + for (const poolItem of self.state.items) { + if (poolItem.exit._tag === "Success" && poolItem.exit.value === item) { + poolItem.disableReclaim = true + return Effect.uninterruptible(invalidatePoolItem(self, poolItem)) + } + } + return Effect.void + })) + +const invalidatePoolItem = (self: Pool, poolItem: PoolItem): Effect.Effect => + Effect.suspend(() => { + if (!self.state.items.has(poolItem)) { + return Effect.void + } else if (poolItem.refCount === 0) { + self.state.items.delete(poolItem) + self.state.available.delete(poolItem) + self.state.invalidated.delete(poolItem) + return Effect.asVoid(Effect.flatMap( + poolItem.finalizer, + () => Effect.forkIn(Effect.interruptible(resize(self)), self.state.scope) + )) + } + self.state.invalidated.add(poolItem) + self.state.available.delete(poolItem) + return Effect.void + }) + +const resize = (self: Pool): Effect.Effect => + self.state.resizeSemaphore.withPermits(1)(resizeLoop(self)) + +const resizeLoop = (self: Pool): Effect.Effect => + Effect.suspend(() => { + const active = activeSize(self) + const target = targetSize(self) + if (active >= target) { + return Effect.void + } + const toAcquire = target - active + return self.config.strategy.reclaim(self).pipe( + Effect.flatMap((item) => item ? Effect.succeed(item) : allocate(self)), + Effect.replicateEffect(toAcquire, { concurrency: toAcquire }), + Effect.tap(self.state.availableLatch.open), + Effect.flatMap((items) => items.some((_) => _.exit._tag === "Failure") ? Effect.void : resizeLoop(self)) + ) + }) + +const allocate = (self: Pool): Effect.Effect> => + Effect.acquireUseRelease( + Scope.make(), + (scope) => + self.config.acquire.pipe( + Scope.provide(scope), + Effect.exit, + Effect.flatMap((exit) => { + const item: PoolItem = { + exit, + finalizer: Effect.catchCause(Scope.close(scope, exit), reportUnhandledError), + refCount: 0, + disableReclaim: false + } + self.state.items.add(item) + self.state.available.add(item) + return Effect.as( + exit._tag === "Success" + ? self.config.strategy.onAcquire(item) + : Effect.flatMap(item.finalizer, () => self.config.strategy.onAcquire(item)), + item + ) + }) + ), + (scope, exit) => exit._tag === "Failure" ? Scope.close(scope, exit) : Effect.void + ) + +const currentUsage = (self: Pool) => { + let count = self.state.waiters + for (const item of self.state.items) { + count += item.refCount + } + return count +} + +const targetSize = (self: Pool) => { + if (self.state.isShuttingDown) return 0 + const utilization = currentUsage(self) / self.config.targetUtilization + const target = Math.ceil(utilization / self.config.concurrency) + return Math.min(Math.max(self.config.minSize, target), self.config.maxSize) +} + +const activeSize = (self: Pool) => { + return self.state.items.size - self.state.invalidated.size +} + +// ----------------------------------------------------------------------------- +// Strategy +// ----------------------------------------------------------------------------- + +const strategyNoop = (): Strategy => ({ + run: (_) => Effect.void, + onAcquire: (_) => Effect.void, + reclaim: (_) => Effect.undefined +}) + +const strategyCreationTTL = Effect.fnUntraced(function*(ttl: Duration.Input) { + const clock = yield* Clock + const queue = yield* Queue.unbounded>() + const ttlMillis = Duration.toMillis(Duration.fromInputUnsafe(ttl)) + const creationTimes = new WeakMap, number>() + return identity>({ + run: (pool) => { + const process = (item: PoolItem): Effect.Effect => + Effect.suspend(() => { + if (!pool.state.items.has(item) || pool.state.invalidated.has(item)) { + return Effect.void + } + const now = clock.currentTimeMillisUnsafe() + const created = creationTimes.get(item)! + const remaining = ttlMillis - (now - created) + return remaining > 0 + ? Effect.delay(process(item), remaining) + : invalidatePoolItem(pool, item) + }) + return Queue.take(queue).pipe( + Effect.tap(process), + Effect.forever({ disableYield: true }) + ) + }, + onAcquire: (item) => + Effect.suspend(() => { + creationTimes.set(item, clock.currentTimeMillisUnsafe()) + return Queue.offer(queue, item) + }), + reclaim: (_) => Effect.undefined + }) +}) + +const strategyUsageTTL = Effect.fnUntraced(function*(ttl: Duration.Input) { + const queue = yield* Queue.unbounded>() + return identity>({ + run: (pool) => { + const process: Effect.Effect = Effect.suspend(() => { + const excess = activeSize(pool) - targetSize(pool) + if (excess <= 0) return Effect.void + return Queue.take(queue).pipe( + Effect.tap((item) => invalidatePoolItem(pool, item)), + Effect.flatMap(() => process) + ) + }) + return process.pipe( + Effect.delay(ttl), + Effect.forever({ disableYield: true }) + ) + }, + onAcquire: (item) => Queue.offer(queue, item), + reclaim(pool) { + return Effect.suspend((): Effect.Effect | undefined> => { + if (pool.state.invalidated.size === 0) { + return Effect.undefined + } + const item = Iterable.head( + Iterable.filter(pool.state.invalidated, (item) => !item.disableReclaim) + ) + if (item._tag === "None") { + return Effect.undefined + } + pool.state.invalidated.delete(item.value) + if (item.value.refCount < pool.config.concurrency) { + pool.state.available.add(item.value) + } + return Effect.as(Queue.offer(queue, item.value), item.value) + }) + } + }) +}) + +const reportUnhandledError = (cause: Cause.Cause) => + Effect.withFiber((fiber) => { + const unhandledLogLevel = fiber.getRef(UnhandledLogLevel) + if (unhandledLogLevel) { + return Effect.logWithLevel(unhandledLogLevel)( + "Unhandled error in pool finalizer", + cause + ) + } + return Effect.void + }) diff --git a/repos/effect/packages/effect/src/Predicate.ts b/repos/effect/packages/effect/src/Predicate.ts new file mode 100644 index 0000000..b5ac504 --- /dev/null +++ b/repos/effect/packages/effect/src/Predicate.ts @@ -0,0 +1,1872 @@ +/** + * Defines runtime checks for values. + * + * A `Predicate` returns `true` or `false` for an `A`. A + * `Refinement` is a predicate that also narrows the TypeScript type when + * it succeeds. This module includes guards for common JavaScript values, + * property and tag checks, tuple and struct checks, boolean combinators, and + * helpers for composing predicates and refinements. + * + * @since 2.0.0 + */ +import { dual } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import type { TupleOf, TupleOfAtLeast } from "./Types.ts" + +/** + * A function that decides whether a value of type `A` satisfies a condition. + * + * **When to use** + * + * Use when you want a reusable boolean check for `A`, especially when you plan + * to combine checks with {@link and}/{@link or} or pass a predicate to arrays + * and iterables. + * + * **Details** + * + * A predicate returns `true` or `false` and never throws by itself. It does not + * narrow types unless you use `Refinement`. + * + * **Example** (Defining a predicate) + * + * ```ts + * import { Predicate } from "effect" + * + * const isPositive: Predicate.Predicate = (n) => n > 0 + * + * console.log(isPositive(1)) + * ``` + * + * @see {@link Refinement} + * @see {@link mapInput} + * @see {@link and} + * @category models + * @since 2.0.0 + */ +export interface Predicate { + (a: A): boolean +} + +/** + * Type-level lambda for higher-kinded usage of {@link Predicate}. + * + * **When to use** + * + * Use when you are defining APIs that abstract over predicates with HKTs and + * need a `TypeLambda` instance for predicate-based type classes. + * + * **Details** + * + * This is type-only, creates no runtime value, and does not affect emitted + * JavaScript. + * + * **Example** (Type-level usage) + * + * ```ts + * import { Predicate } from "effect" + * + * type P = Predicate.Predicate + * type TL = Predicate.PredicateTypeLambda + * ``` + * + * @see {@link Predicate} + * @category type lambdas + * @since 2.0.0 + */ +export interface PredicateTypeLambda extends TypeLambda { + readonly type: Predicate +} + +/** + * A predicate that also narrows the input type when it returns `true`. + * + * **When to use** + * + * Use when you want a runtime check that refines `A` to `B` for TypeScript, + * especially when composing type guards with {@link compose} or safely + * checking `unknown` values. + * + * **Details** + * + * A refinement returns a type predicate (`a is B`). Use it with `if` or + * `filter` to narrow types. + * + * **Example** (Narrowing unknown values) + * + * ```ts + * import { Predicate } from "effect" + * + * const isString: Predicate.Refinement = (u): u is string => typeof u === "string" + * + * const data: unknown = "hello" + * if (isString(data)) { + * console.log(data.toUpperCase()) + * } + * ``` + * + * @see {@link Predicate} + * @see {@link compose} + * @see {@link isString} + * @category models + * @since 2.0.0 + */ +export interface Refinement { + (a: A): a is B +} + +/** + * Type-level utilities for working with {@link Predicate} types. + * + * **When to use** + * + * Use when you need to extract input types from predicate signatures while + * writing generic helpers over predicate types. + * + * **Details** + * + * These utilities are type-only, create no runtime values, and the namespace is + * erased at runtime. + * + * **Example** (Extracting predicate input) + * + * ```ts + * import { Predicate } from "effect" + * + * type IsString = Predicate.Predicate + * type Input = Predicate.Predicate.In + * ``` + * + * @see {@link Predicate} + * @see {@link Refinement} + * @since 3.6.0 + */ +export declare namespace Predicate { + /** + * Extracts the input type `A` from a `Predicate`. + * + * **When to use** + * + * Use when you want to infer the input type from a predicate type while + * defining generic utilities over predicates. + * + * **Details** + * + * This is type-only and creates no runtime value. It resolves to `never` if + * the type does not match `Predicate`. + * + * **Example** (Inferring the input type) + * + * ```ts + * import { Predicate } from "effect" + * + * type P = Predicate.Predicate + * type Input = Predicate.Predicate.In

+ * ``` + * + * @see {@link Predicate.Any} + * @see {@link Refinement.In} + * @category utility types + * @since 3.6.0 + */ + export type In = [T] extends [Predicate] ? _A : never + + /** + * A utility type representing any predicate type. + * + * **When to use** + * + * Use when you need a constraint for "any predicate" in generic code. + * + * **Details** + * + * This is type-only and creates no runtime value. + * + * **Example** (Using generic constraints) + * + * ```ts + * import { Predicate } from "effect" + * + * type AnyPredicate = Predicate.Predicate.Any + * ``` + * + * @see {@link Predicate.In} + * @category utility types + * @since 3.6.0 + */ + export type Any = Predicate +} + +/** + * Type-level utilities for working with {@link Refinement} types. + * + * **When to use** + * + * Use when you need to extract input and output types from refinement + * signatures while writing generic helpers over refinements. + * + * **Details** + * + * These utilities are type-only, create no runtime values, and the namespace is + * erased at runtime. + * + * **Example** (Extracting refinement types) + * + * ```ts + * import { Predicate } from "effect" + * + * type IsString = Predicate.Refinement + * type Input = Predicate.Refinement.In + * type Output = Predicate.Refinement.Out + * ``` + * + * @see {@link Refinement} + * @see {@link Predicate} + * @since 3.6.0 + */ +export declare namespace Refinement { + /** + * Extracts the input type `A` from a `Refinement`. + * + * **When to use** + * + * Use when you want to infer the input type from a refinement type. + * + * **Details** + * + * This is type-only and creates no runtime value. It resolves to `never` if + * the type does not match `Refinement`. + * + * **Example** (Inferring the input type) + * + * ```ts + * import { Predicate } from "effect" + * + * type R = Predicate.Refinement + * type Input = Predicate.Refinement.In + * ``` + * + * @see {@link Refinement.Out} + * @see {@link Predicate.In} + * @category utility types + * @since 3.6.0 + */ + + export type In = [T] extends [Refinement] ? _A : never + + /** + * Extracts the output type `B` from a `Refinement`. + * + * **When to use** + * + * Use when you want to infer the narrowed type from a refinement type. + * + * **Details** + * + * This is type-only and creates no runtime value. It resolves to `never` if + * the type does not match `Refinement`. + * + * **Example** (Inferring the output type) + * + * ```ts + * import { Predicate } from "effect" + * + * type R = Predicate.Refinement + * type Output = Predicate.Refinement.Out + * ``` + * + * @see {@link Refinement.In} + * @category utility types + * @since 3.6.0 + */ + export type Out = [T] extends [Refinement] ? _B : never + + /** + * A utility type representing any refinement type. + * + * **When to use** + * + * Use when you need a constraint for "any refinement" in generic code. + * + * **Details** + * + * This is type-only and creates no runtime value. + * + * **Example** (Using generic constraints) + * + * ```ts + * import { Predicate } from "effect" + * + * type AnyRefinement = Predicate.Refinement.Any + * ``` + * + * @see {@link Refinement.In} + * @see {@link Refinement.Out} + * @category utility types + * @since 3.6.0 + */ + export type Any = Refinement +} + +/** + * Transforms the input of a predicate using a mapping function. + * + * **When to use** + * + * Use when you have a predicate on `A` and want to check `B` values by mapping + * each `B` to an `A`, such as checking lengths or projections. + * + * **Details** + * + * Returns a new predicate that applies `f` before `self`. There is no + * additional short-circuiting beyond what `self` does. + * + * **Example** (Checking string length) + * + * ```ts + * import { Predicate } from "effect" + * + * const isLongerThan2 = Predicate.mapInput((s: string) => s.length)( + * (n: number) => n > 2 + * ) + * + * console.log(isLongerThan2("hello")) + * ``` + * + * @see {@link Predicate} + * @see {@link and} + * @see {@link not} + * @category combinators + * @since 2.0.0 + */ +export const mapInput: { + (f: (b: B) => A): (self: Predicate) => Predicate + (self: Predicate, f: (b: B) => A): Predicate +} = dual(2, (self: Predicate, f: (b: B) => A): Predicate => (b) => self(f(b))) + +/** + * Checks whether a readonly array has exactly `n` elements. + * + * **When to use** + * + * Use when you need a `Predicate` guard for exact tuple length that narrows + * `ReadonlyArray` to `TupleOf`. + * + * **Details** + * + * This only checks length, not element types, and returns a refinement on the + * array type. + * + * **Example** (Checking exact length) + * + * ```ts + * import { Predicate } from "effect" + * + * const isPair = Predicate.isTupleOf(2) + * + * console.log(isPair([1, 2])) + * ``` + * + * @see {@link isTupleOfAtLeast} + * @see {@link Tuple} + * @category guards + * @since 3.3.0 + */ +export const isTupleOf: { + (n: N): (self: ReadonlyArray) => self is TupleOf + (self: ReadonlyArray, n: N): self is TupleOf +} = dual(2, (self: ReadonlyArray, n: N): self is TupleOf => self.length === n) + +/** + * Checks whether a readonly array has at least `n` elements. + * + * **When to use** + * + * Use when you need a `Predicate` guard for tuple-like minimum length that + * narrows `ReadonlyArray` to `TupleOfAtLeast`. + * + * **Details** + * + * This only checks length, not element types, and returns a refinement on the + * array type. + * + * **Example** (Checking minimum length) + * + * ```ts + * import { Predicate } from "effect" + * + * const hasAtLeast2 = Predicate.isTupleOfAtLeast(2) + * + * console.log(hasAtLeast2([1, 2, 3])) + * ``` + * + * @see {@link isTupleOf} + * @see {@link Tuple} + * @category guards + * @since 3.3.0 + */ +export const isTupleOfAtLeast: { + (n: N): (self: ReadonlyArray) => self is TupleOfAtLeast + (self: ReadonlyArray, n: N): self is TupleOfAtLeast +} = dual(2, (self: ReadonlyArray, n: N): self is TupleOfAtLeast => self.length >= n) + +/** + * Checks whether a value is truthy. + * + * **When to use** + * + * Use when you want a predicate that mirrors JavaScript truthiness and filters + * out falsy values like `0`, `""`, and `false`. + * + * **Details** + * + * This uses `!!input` and treats `0`, `""`, `false`, `null`, and `undefined` + * as false. + * + * **Example** (Filtering truthy values) + * + * ```ts + * import { Predicate } from "effect" + * + * const values = [0, 1, "", "ok", false] + * const truthy = values.filter(Predicate.isTruthy) + * + * console.log(truthy) + * ``` + * + * @see {@link isNullish} + * @see {@link isNotNullish} + * @category guards + * @since 2.0.0 + */ +export function isTruthy(input: unknown): boolean { + return !!input +} + +/** + * Checks whether a value is a `Set`. + * + * **When to use** + * + * Use when you need a `Predicate` runtime guard for `Set` values. + * + * **Details** + * + * Uses `instanceof Set`. + * + * **Example** (Guarding a Set) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = new Set([1, 2]) + * + * if (Predicate.isSet(data)) { + * console.log(data.size) + * } + * ``` + * + * @see {@link isMap} + * @see {@link isIterable} + * @category guards + * @since 2.0.0 + */ +export function isSet(input: unknown): input is Set { + return input instanceof Set +} + +/** + * Checks whether a value is a `Map`. + * + * **When to use** + * + * Use when you need a `Predicate` runtime guard for `Map` values. + * + * **Details** + * + * Uses `instanceof Map`. + * + * **Example** (Guarding a Map) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = new Map([["a", 1]]) + * + * if (Predicate.isMap(data)) { + * console.log(data.size) + * } + * ``` + * + * @see {@link isSet} + * @see {@link isIterable} + * @category guards + * @since 2.0.0 + */ +export function isMap(input: unknown): input is Map { + return input instanceof Map +} + +/** + * Checks whether a value is a `string`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * string. + * + * **Details** + * + * Uses `typeof input === "string"`. + * + * **Example** (Guarding strings) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = "hi" + * + * if (Predicate.isString(data)) { + * console.log(data.toUpperCase()) + * } + * ``` + * + * @see {@link isNumber} + * @see {@link isBoolean} + * @see {@link Refinement} + * @category guards + * @since 2.0.0 + */ +export function isString(input: unknown): input is string { + return typeof input === "string" +} + +/** + * Checks whether a value is a `number`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * number. + * + * **Details** + * + * Uses `typeof input === "number"` and does not exclude `NaN` or `Infinity`. + * + * **Example** (Guarding numbers) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = 42 + * + * if (Predicate.isNumber(data)) { + * console.log(data + 1) + * } + * ``` + * + * @see {@link isBigInt} + * @see {@link isString} + * @category guards + * @since 2.0.0 + */ +export function isNumber(input: unknown): input is number { + return typeof input === "number" +} + +/** + * Checks whether a value is a `boolean`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * boolean. + * + * **Details** + * + * Uses `typeof input === "boolean"`. + * + * **Example** (Guarding booleans) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = true + * + * if (Predicate.isBoolean(data)) { + * console.log(data ? "yes" : "no") + * } + * ``` + * + * @see {@link isString} + * @see {@link isNumber} + * @category guards + * @since 2.0.0 + */ +export function isBoolean(input: unknown): input is boolean { + return typeof input === "boolean" +} + +/** + * Checks whether a value is a `bigint`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * bigint. + * + * **Details** + * + * Uses `typeof input === "bigint"`. + * + * **Example** (Guarding bigints) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = 1n + * + * if (Predicate.isBigInt(data)) { + * console.log(data + 2n) + * } + * ``` + * + * @see {@link isNumber} + * @category guards + * @since 2.0.0 + */ +export function isBigInt(input: unknown): input is bigint { + return typeof input === "bigint" +} + +/** + * Checks whether a value is a `symbol`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * symbol. + * + * **Details** + * + * Uses `typeof input === "symbol"`. + * + * **Example** (Guarding symbols) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = Symbol.for("id") + * + * if (Predicate.isSymbol(data)) { + * console.log(data.description) + * } + * ``` + * + * @see {@link isPropertyKey} + * @category guards + * @since 2.0.0 + */ +export function isSymbol(input: unknown): input is symbol { + return typeof input === "symbol" +} + +/** + * Checks whether a value is a valid `PropertyKey` (string, number, or symbol). + * + * **When to use** + * + * Use when you need a `Predicate` guard for unknown property keys before + * indexing. + * + * **Details** + * + * Uses `isString`, `isNumber`, and `isSymbol`. + * + * **Example** (Guarding property keys) + * + * ```ts + * import { Predicate } from "effect" + * + * const key: unknown = "name" + * const obj: Record = { name: "Ada" } + * + * if (Predicate.isPropertyKey(key) && key in obj) { + * console.log(obj[key]) + * } + * ``` + * + * @see {@link isString} + * @see {@link isNumber} + * @see {@link isSymbol} + * @category guards + * @since 4.0.0 + */ +export function isPropertyKey(u: unknown): u is PropertyKey { + return isString(u) || isNumber(u) || isSymbol(u) +} + +/** + * Checks whether a value is a `function`. + * + * **When to use** + * + * Use when you need a `Predicate` guard to narrow an `unknown` value to a + * callable function. + * + * **Details** + * + * Uses `typeof input === "function"`. + * + * **Example** (Guarding functions) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = () => 1 + * + * if (Predicate.isFunction(data)) { + * console.log(data()) + * } + * ``` + * + * @see {@link isObjectKeyword} + * @category guards + * @since 2.0.0 + */ +export function isFunction(input: unknown): input is Function { + return typeof input === "function" +} + +/** + * Checks whether a value is `undefined`. + * + * **When to use** + * + * Use when you need a `Predicate` guard for values that are exactly + * `undefined`. + * + * **Details** + * + * Uses `input === undefined`. + * + * **Example** (Guarding undefined values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = undefined + * + * console.log(Predicate.isUndefined(data)) + * ``` + * + * @see {@link isNotUndefined} + * @see {@link isNullish} + * @category guards + * @since 2.0.0 + */ +export function isUndefined(input: unknown): input is undefined { + return input === undefined +} + +/** + * Checks whether a value is not `undefined`. + * + * **When to use** + * + * Use when you need a `Predicate` refinement that filters out `undefined` + * while preserving other falsy values. + * + * **Details** + * + * Returns a refinement that excludes `undefined`. + * + * **Example** (Filtering undefined values) + * + * ```ts + * import { Predicate } from "effect" + * + * const values = [1, undefined, 2] + * const defined = values.filter(Predicate.isNotUndefined) + * + * console.log(defined) + * ``` + * + * @see {@link isUndefined} + * @see {@link isNotNullish} + * @category guards + * @since 2.0.0 + */ +export function isNotUndefined(input: A): input is Exclude { + return input !== undefined +} + +/** + * Checks whether a value is `null`. + * + * **When to use** + * + * Use when you need a `Predicate` guard for nullable values. + * + * **Details** + * + * Uses `input === null`. + * + * **Example** (Guarding null values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = null + * + * console.log(Predicate.isNull(data)) + * ``` + * + * @see {@link isNotNull} + * @see {@link isNullish} + * @category guards + * @since 2.0.0 + */ +export function isNull(input: unknown): input is null { + return input === null +} + +/** + * Checks whether a value is not `null`. + * + * **When to use** + * + * Use when you need a `Predicate` refinement that filters out `null` while + * preserving other falsy values. + * + * **Details** + * + * Returns a refinement that excludes `null`. + * + * **Example** (Filtering null values) + * + * ```ts + * import { Predicate } from "effect" + * + * const values = [1, null, 2] + * const nonNull = values.filter(Predicate.isNotNull) + * + * console.log(nonNull) + * ``` + * + * @see {@link isNull} + * @see {@link isNotNullish} + * @category guards + * @since 2.0.0 + */ +export function isNotNull(input: A): input is Exclude { + return input !== null +} + +/** + * Checks whether a value is `null` or `undefined`. + * + * **When to use** + * + * Use when you need a `Predicate` guard for nullish values. + * + * **Details** + * + * Uses `input === null || input === undefined`. + * + * **Example** (Guarding nullish values) + * + * ```ts + * import { Predicate } from "effect" + * + * const values = [0, null, "", undefined] + * const nullish = values.filter(Predicate.isNullish) + * + * console.log(nullish) + * ``` + * + * @see {@link isNotNullish} + * @see {@link isUndefined} + * @see {@link isNull} + * @category guards + * @since 4.0.0 + */ +export function isNullish(input: A): input is A & (null | undefined) { + return input === null || input === undefined +} + +/** + * Checks whether a value is not `null` and not `undefined`. + * + * **When to use** + * + * Use when you need a `Predicate` refinement that filters out nullish values + * but keeps other falsy ones. + * + * **Details** + * + * Uses `input != null`. + * + * **Example** (Filtering non-nullish values) + * + * ```ts + * import { Predicate } from "effect" + * + * const values = [0, null, "", undefined] + * const present = values.filter(Predicate.isNotNullish) + * + * console.log(present) + * ``` + * + * @see {@link isNullish} + * @see {@link isNotNull} + * @see {@link isNotUndefined} + * @category guards + * @since 4.0.0 + */ +export function isNotNullish(input: A): input is NonNullable { + return input != null +} + +/** + * Type guard that always returns `false`. + * + * **When to use** + * + * Use when you need a `Predicate` that never accepts, e.g. in default branches. + * + * **Example** (Matching no values) + * + * ```ts + * import { Predicate } from "effect" + * + * console.log(Predicate.isNever("anything")) + * ``` + * + * @see {@link isUnknown} + * @category guards + * @since 2.0.0 + */ +export function isNever(_: unknown): _ is never { + return false +} + +/** + * Type guard that always returns `true`. + * + * **When to use** + * + * Use when you need a `Predicate` that always accepts, e.g. as a placeholder. + * + * **Example** (Matching every value) + * + * ```ts + * import { Predicate } from "effect" + * + * console.log(Predicate.isUnknown(123)) + * ``` + * + * @see {@link isNever} + * @category guards + * @since 2.0.0 + */ +export function isUnknown(_: unknown): _ is unknown { + return true +} + +/** + * Checks whether a value is an object or an array (non-null object). + * + * **When to use** + * + * Use when you need a `Predicate` guard that accepts plain objects and arrays, + * but not `null`. + * + * **Details** + * + * Uses `typeof input === "object" && input !== null` and includes arrays. + * + * **Example** (Checking objects or arrays) + * + * ```ts + * import { Predicate } from "effect" + * + * console.log(Predicate.isObjectOrArray([])) + * ``` + * + * @see {@link isObject} + * @see {@link isObjectKeyword} + * @category guards + * @since 4.0.0 + */ +export function isObjectOrArray(input: unknown): input is { [x: PropertyKey]: unknown } | Array { + return typeof input === "object" && input !== null +} + +/** + * Checks whether a value is a non-null object value that is not an array. + * + * **When to use** + * + * Use to narrow unknown input to a non-null, non-array object with a + * `Predicate` guard. + * + * **Details** + * + * This is a structural runtime check using `typeof input === "object"`, so it + * also accepts object instances such as `Date`, `Map`, class instances, and + * typed arrays. It excludes `null` and arrays. + * + * **Example** (Guarding objects) + * + * ```ts + * import { Predicate } from "effect" + * + * console.log(Predicate.isObject({ a: 1 })) + * console.log(Predicate.isObject([1, 2])) + * ``` + * + * @see {@link isObjectOrArray} + * @see {@link isReadonlyObject} + * @category guards + * @since 2.0.0 + */ +export function isObject(input: unknown): input is { [x: PropertyKey]: unknown } { + return typeof input === "object" && input !== null && !Array.isArray(input) +} + +/** + * Checks whether a value is a non-null, non-array object and narrows it to a + * readonly indexable object type. + * + * **When to use** + * + * Use to narrow unknown input to a readonly view of a non-null, non-array + * object with a `Predicate` guard. + * + * **Details** + * + * Readonly-ness is a TypeScript type-level view; it is not observable at + * runtime. This delegates to `isObject`, so class instances and built-in object + * instances are accepted. + * + * **Example** (Checking readonly objects) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = { a: 1 } + * + * console.log(Predicate.isReadonlyObject(data)) + * ``` + * + * @see {@link isObject} + * @category guards + * @since 4.0.0 + */ +export function isReadonlyObject(input: unknown): input is { readonly [x: PropertyKey]: unknown } { + return isObject(input) +} + +/** + * Checks whether a value is an `object` in the JavaScript sense (objects, arrays, functions). + * + * **When to use** + * + * Use when you need a `Predicate` guard that accepts arrays and functions as + * well as objects. + * + * **Details** + * + * Returns `true` for arrays and functions, and `false` for `null`. + * + * **Example** (Checking object keywords) + * + * ```ts + * import { Predicate } from "effect" + * + * console.log(Predicate.isObjectKeyword(() => 1)) + * console.log(Predicate.isObjectKeyword(null)) + * ``` + * + * @see {@link isObject} + * @see {@link isObjectOrArray} + * @category guards + * @since 4.0.0 + */ +export function isObjectKeyword(input: unknown): input is object { + return (typeof input === "object" && input !== null) || isFunction(input) +} + +/** + * Checks whether a value has a given property key. + * + * **When to use** + * + * Use when you need a `Predicate` guard for property access on `unknown` + * values with a simple structural object check. + * + * **Details** + * + * Uses the `in` operator and `isObjectKeyword`. This does not check property + * value types. + * + * **Example** (Guarding object properties) + * + * ```ts + * import { Predicate } from "effect" + * + * const hasName = Predicate.hasProperty("name") + * const data: unknown = { name: "Ada" } + * + * if (hasName(data)) { + * console.log(data.name) + * } + * ``` + * + * @see {@link isTagged} + * @see {@link isObjectKeyword} + * @category guards + * @since 2.0.0 + */ +export const hasProperty: { +

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

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

(self: unknown, property: P): self is { [K in P]: unknown } => + isObjectKeyword(self) && (property in self) +) + +/** + * Checks whether a value has a `_tag` property equal to the given tag. + * + * **When to use** + * + * Use when you model tagged unions with a `_tag` field and want a quick + * `Predicate` guard for tagged values. + * + * **Details** + * + * Uses `hasProperty` and strict equality on `_tag`. + * + * **Example** (Guarding tagged values) + * + * ```ts + * import { Predicate } from "effect" + * + * const isOk = Predicate.isTagged("Ok") + * + * console.log(isOk({ _tag: "Ok", value: 1 })) + * ``` + * + * @see {@link hasProperty} + * @category guards + * @since 2.0.0 + */ +export const isTagged: { + (tag: K): (self: unknown) => self is { _tag: K } + (self: unknown, tag: K): self is { _tag: K } +} = dual( + 2, + (self: unknown, tag: K): self is { _tag: K } => hasProperty(self, "_tag") && self["_tag"] === tag +) + +/** + * Checks whether a value is an `Error`. + * + * **When to use** + * + * Use when you need a `Predicate` guard for errors caught from unknown sources. + * + * **Details** + * + * Uses `instanceof Error`. + * + * **Example** (Guarding errors) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = new Error("boom") + * + * console.log(Predicate.isError(data)) + * ``` + * + * @see {@link isUnknown} + * @category guards + * @since 2.0.0 + */ +export function isError(input: unknown): input is Error { + return input instanceof Error +} + +/** + * Checks whether a value is a `Uint8Array`. + * + * **When to use** + * + * Use when you need a `Predicate` runtime guard for binary data. + * + * **Details** + * + * Uses `instanceof Uint8Array`. + * + * **Example** (Guarding Uint8Array values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = new Uint8Array([1, 2]) + * + * console.log(Predicate.isUint8Array(data)) + * ``` + * + * @see {@link isIterable} + * @see {@link isSet} + * @category guards + * @since 2.0.0 + */ +export function isUint8Array(input: unknown): input is Uint8Array { + return input instanceof Uint8Array +} + +/** + * Checks whether a value is a `Date`. + * + * **When to use** + * + * Use when you need a `Predicate` runtime guard for dates. + * + * **Details** + * + * Uses `instanceof Date`. + * + * **Example** (Guarding Date values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = new Date() + * + * console.log(Predicate.isDate(data)) + * ``` + * + * @see {@link isRegExp} + * @category guards + * @since 2.0.0 + */ +export function isDate(input: unknown): input is Date { + return input instanceof Date +} + +/** + * Checks whether a value is iterable. + * + * **When to use** + * + * Use when you need a `Predicate` guard before iterating an unknown value. + * + * **Details** + * + * Accepts strings as iterable and uses `hasProperty` for `Symbol.iterator`. + * + * **Example** (Guarding iterables) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = [1, 2, 3] + * + * console.log(Predicate.isIterable(data)) + * ``` + * + * @see {@link isSet} + * @see {@link isMap} + * @category guards + * @since 2.0.0 + */ +export function isIterable(input: unknown): input is Iterable { + return hasProperty(input, Symbol.iterator) || isString(input) +} + +/** + * Checks whether a value is a `Promise`-like object with `then` and `catch`. + * + * **When to use** + * + * Use when you need a `Predicate` guard for promise instances across realms. + * + * **Details** + * + * Performs a structural check for `then` and `catch` functions. + * + * **Example** (Guarding promises) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = Promise.resolve(1) + * + * console.log(Predicate.isPromise(data)) + * ``` + * + * @see {@link isPromiseLike} + * @category guards + * @since 2.0.0 + */ +export function isPromise(input: unknown): input is Promise { + return hasProperty(input, "then") && "catch" in input && isFunction(input.then) && isFunction(input.catch) +} + +/** + * Checks whether a value is `PromiseLike` (has a `then` method). + * + * **When to use** + * + * Use when you need a `Predicate` guard for promise-like values with a + * callable `then` method. + * + * **Details** + * + * Performs a structural check for a callable `then`. + * + * **Example** (Guarding promise-like values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = { then: () => {} } + * + * console.log(Predicate.isPromiseLike(data)) + * ``` + * + * @see {@link isPromise} + * @category guards + * @since 2.0.0 + */ +export function isPromiseLike(input: unknown): input is PromiseLike { + return hasProperty(input, "then") && isFunction(input.then) +} + +/** + * Checks whether a value is a `RegExp`. + * + * **When to use** + * + * Use when you need a `Predicate` runtime guard for regular expressions. + * + * **Details** + * + * Uses `instanceof RegExp`. + * + * **Example** (Guarding RegExp values) + * + * ```ts + * import { Predicate } from "effect" + * + * const data: unknown = /abc/ + * + * console.log(Predicate.isRegExp(data)) + * ``` + * + * @see {@link isDate} + * @category guards + * @since 3.9.0 + */ +export function isRegExp(input: unknown): input is RegExp { + return input instanceof RegExp +} + +/** + * Composes two predicates or refinements into one. + * + * **When to use** + * + * Use when you want to compose two `Predicate` checks in sequence, especially + * when chaining refinements for progressive narrowing. + * + * **Details** + * + * For refinements, the output type is narrowed by both checks. Evaluation + * short-circuits on the first `false`. + * + * **Example** (Composing refinements) + * + * ```ts + * import { Predicate } from "effect" + * + * const isNumber: Predicate.Refinement = (u): u is number => typeof u === "number" + * const isInteger: Predicate.Refinement = (n): n is number => Number.isInteger(n) + * + * const isIntegerNumber = Predicate.compose(isNumber, isInteger) + * + * console.log(isIntegerNumber(1)) + * ``` + * + * @see {@link and} + * @see {@link Refinement} + * @category combinators + * @since 2.0.0 + */ +export const compose: { + (bc: Refinement): (ab: Refinement) => Refinement + (bc: Predicate>): (ab: Refinement) => Refinement + (ab: Refinement, bc: Refinement): Refinement + (ab: Refinement, bc: Predicate>): Refinement +} = dual( + 2, + (ab: Refinement, bc: Refinement): Refinement => (a): a is C => + ab(a) && bc(a) +) + +/** + * Creates a predicate for tuples by applying predicates to each element. + * + * **When to use** + * + * Use when you want to validate tuple positions independently by lifting + * element predicates into a tuple predicate. + * + * **Details** + * + * Returns a refinement if any element predicate is a refinement. Evaluation + * stops at the first failing element. + * + * **Example** (Checking tuples) + * + * ```ts + * import { Predicate } from "effect" + * + * const tupleCheck = Predicate.Tuple([(n: number) => n > 0, Predicate.isString]) + * + * console.log(tupleCheck([1, "ok"])) + * ``` + * + * @see {@link Struct} + * @see {@link isTupleOf} + * @category combinators + * @since 4.0.0 + */ +export function Tuple>( + elements: T +): [Extract] extends [never] ? Predicate<{ readonly [I in keyof T]: Predicate.In }> + : Refinement< + { readonly [I in keyof T]: T[I] extends Refinement.Any ? Refinement.In : Predicate.In }, + { readonly [I in keyof T]: T[I] extends Refinement.Any ? Refinement.Out : Predicate.In } + > +{ + return ((as: Array) => { + for (let i = 0; i < elements.length; i++) { + if (elements[i](as[i]) === false) { + return false + } + } + return true + }) as any +} + +/** + * Creates a predicate for objects by applying predicates to named properties. + * + * **When to use** + * + * Use when you want to validate a record shape at runtime by lifting property + * predicates into an object predicate. + * + * **Details** + * + * Returns a refinement if any field predicate is a refinement. Only the + * specified keys are checked, and extra keys are ignored. + * + * **Example** (Checking structs) + * + * ```ts + * import { Predicate } from "effect" + * + * const userCheck = Predicate.Struct({ + * id: Predicate.isNumber, + * name: Predicate.isString + * }) + * + * console.log(userCheck({ id: 1, name: "Ada" })) + * ``` + * + * @see {@link Tuple} + * @see {@link hasProperty} + * @category combinators + * @since 4.0.0 + */ +export function Struct>( + fields: R +): [Extract] extends [never] ? Predicate<{ readonly [K in keyof R]: Predicate.In }> : + Refinement< + { readonly [K in keyof R]: R[K] extends Refinement.Any ? Refinement.In : Predicate.In }, + { readonly [K in keyof R]: R[K] extends Refinement.Any ? Refinement.Out : Predicate.In } + > +{ + const keys = Object.keys(fields) + return ((a: Record) => { + for (const key of keys) { + if (!fields[key](a[key] as never)) { + return false + } + } + return true + }) as any +} + +/** + * Negates a predicate. + * + * **When to use** + * + * Use when you want the inverse of an existing predicate. + * + * **Details** + * + * Returns a new predicate that flips the boolean result. + * + * **Example** (Negating a predicate) + * + * ```ts + * import { Predicate } from "effect" + * + * const isNotString = Predicate.not(Predicate.isString) + * + * console.log(isNotString(1)) + * ``` + * + * @see {@link and} + * @see {@link or} + * @see {@link xor} + * @category combinators + * @since 2.0.0 + */ +export function not(self: Predicate): Predicate { + return (a) => !self(a) +} + +/** + * Creates a predicate that returns `true` if either predicate is `true`. + * + * **When to use** + * + * Use when you want to combine `Predicate`s with OR, accepting values that + * satisfy at least one condition, including refinements that narrow to a union. + * + * **Details** + * + * Evaluation short-circuits on the first `true`. For refinements, the output + * type is a union. + * + * **Example** (Checking either condition) + * + * ```ts + * import { Predicate } from "effect" + * + * const isStringOrNumber = Predicate.or(Predicate.isString, Predicate.isNumber) + * + * console.log(isStringOrNumber("a")) + * ``` + * + * @see {@link and} + * @see {@link xor} + * @category combinators + * @since 2.0.0 + */ +export const or: { + (that: Refinement): (self: Refinement) => Refinement + (self: Refinement, that: Refinement): Refinement + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) || that(a)) + +/** + * Creates a predicate that returns `true` only if both predicates are `true`. + * + * **When to use** + * + * Use when you want to combine `Predicate`s with AND, accepting values that + * satisfy multiple conditions, including refinements that narrow to an + * intersection. + * + * **Details** + * + * Evaluation short-circuits on the first `false`. For refinements, the output + * type is an intersection. + * + * **Example** (Checking both conditions) + * + * ```ts + * import { Predicate } from "effect" + * + * const hasAAndB = Predicate.and( + * Predicate.hasProperty("a"), + * Predicate.hasProperty("b") + * ) + * + * const input: unknown = JSON.parse(`{"a":1,"b":"ok"}`) + * if (hasAAndB(input)) { + * // input has both properties at this point + * const a = input.a + * const b = input.b + * } + * ``` + * + * @see {@link or} + * @see {@link not} + * @category combinators + * @since 2.0.0 + */ +export const and: { + (that: Refinement): (self: Refinement) => Refinement + (self: Refinement, that: Refinement): Refinement + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) && that(a)) + +/** + * Creates a predicate that returns `true` if exactly one predicate is `true`. + * + * **When to use** + * + * Use when you want to combine two `Predicate`s with exclusive-or semantics. + * + * **Details** + * + * Returns `true` when results differ. + * + * **Example** (Checking exclusive-or conditions) + * + * ```ts + * import { Predicate } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * const isPositive = (n: number) => n > 0 + * const either = Predicate.xor(isEven, isPositive) + * + * console.log(either(-2)) + * ``` + * + * @see {@link or} + * @see {@link and} + * @category combinators + * @since 2.0.0 + */ +export const xor: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) !== that(a)) + +/** + * Creates a predicate that returns `true` when both predicates agree. + * + * **When to use** + * + * Use when you want to check equivalence of two `Predicate`s. + * + * **Details** + * + * Returns `true` when both results are equal. + * + * **Example** (Defining equivalence) + * + * ```ts + * import { Predicate } from "effect" + * + * const isEven = (n: number) => n % 2 === 0 + * const same = Predicate.eqv(isEven, isEven) + * + * console.log(same(3)) + * ``` + * + * @see {@link xor} + * @category combinators + * @since 2.0.0 + */ +export const eqv: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual(2, (self: Predicate, that: Predicate): Predicate => (a) => self(a) === that(a)) + +/** + * Creates a predicate representing logical implication: if `antecedent`, then `consequent`. + * + * **When to use** + * + * Use when you need to encode logical implication between `Predicate` rules, + * where one rule only applies when a precondition holds. + * + * **Details** + * + * Models constraints like "if A then B" and returns `true` when the antecedent + * is `false`. + * + * **Example** (Checking implication) + * + * ```ts + * import { Predicate } from "effect" + * + * const isAdult = (age: number) => age >= 18 + * const canVote = (age: number) => age >= 18 + * const implies = Predicate.implies(isAdult, canVote) + * + * console.log(implies(16)) + * ``` + * + * @see {@link and} + * @see {@link or} + * @category combinators + * @since 2.0.0 + */ +export const implies: { + (consequent: Predicate): (antecedent: Predicate) => Predicate + (antecedent: Predicate, consequent: Predicate): Predicate +} = dual( + 2, + (antecedent: Predicate, consequent: Predicate): Predicate => (a) => antecedent(a) ? consequent(a) : true +) + +/** + * Creates a predicate that returns `true` when neither predicate is `true`. + * + * **When to use** + * + * Use when you want to combine two `Predicate`s with logical NOR semantics. + * + * **Details** + * + * Returns the negation of `or`. + * + * **Example** (Checking NOR conditions) + * + * ```ts + * import { Predicate } from "effect" + * + * const neither = Predicate.nor(Predicate.isString, Predicate.isNumber) + * + * console.log(neither(true)) + * ``` + * + * @see {@link or} + * @see {@link not} + * @category combinators + * @since 2.0.0 + */ +export const nor: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual( + 2, + (self: Predicate, that: Predicate): Predicate => (a) => !(self(a) || that(a)) +) + +/** + * Creates a predicate that returns `true` unless both predicates are `true`. + * + * **When to use** + * + * Use when you want to combine two `Predicate`s with logical NAND semantics. + * + * **Details** + * + * Returns the negation of `and`. + * + * **Example** (Checking NAND conditions) + * + * ```ts + * import { Predicate } from "effect" + * + * const notBoth = Predicate.nand(Predicate.isString, Predicate.isNumber) + * + * console.log(notBoth("a")) + * ``` + * + * @see {@link and} + * @see {@link not} + * @category combinators + * @since 2.0.0 + */ +export const nand: { + (that: Predicate): (self: Predicate) => Predicate + (self: Predicate, that: Predicate): Predicate +} = dual( + 2, + (self: Predicate, that: Predicate): Predicate => (a) => !(self(a) && that(a)) +) + +/** + * Creates a predicate that returns `true` if all predicates in the collection return `true`. + * + * **When to use** + * + * Use when you have a dynamic list of predicates to apply. + * + * **Details** + * + * Evaluation short-circuits on the first `false`. The collection is iterated + * each time the predicate is called. + * + * **Example** (Checking all predicates) + * + * ```ts + * import { Predicate } from "effect" + * + * const allChecks = Predicate.every([Predicate.isNumber, (n: number) => n > 0]) + * + * console.log(allChecks(2)) + * ``` + * + * @see {@link some} + * @see {@link and} + * @category elements + * @since 2.0.0 + */ +export function every(collection: Iterable>): Predicate { + return (a) => { + for (const p of collection) { + if (!p(a)) { + return false + } + } + return true + } +} + +/** + * Creates a predicate that returns `true` if any predicate in the collection returns `true`. + * + * **When to use** + * + * Use when you have a dynamic list of predicates and only need one to pass. + * + * **Details** + * + * Evaluation short-circuits on the first `true`. The collection is iterated + * each time the predicate is called. + * + * **Example** (Checking any predicate) + * + * ```ts + * import { Predicate } from "effect" + * + * const anyCheck = Predicate.some([Predicate.isString, Predicate.isNumber]) + * + * console.log(anyCheck("ok")) + * ``` + * + * @see {@link every} + * @see {@link or} + * @category elements + * @since 2.0.0 + */ +export function some(collection: Iterable>): Predicate { + return (a) => { + for (const p of collection) { + if (p(a)) { + return true + } + } + return false + } +} diff --git a/repos/effect/packages/effect/src/PrimaryKey.ts b/repos/effect/packages/effect/src/PrimaryKey.ts new file mode 100644 index 0000000..8353ad9 --- /dev/null +++ b/repos/effect/packages/effect/src/PrimaryKey.ts @@ -0,0 +1,123 @@ +/** + * The `PrimaryKey` module defines a small protocol for values that can expose + * a stable, string-based identifier. A value participates by implementing a + * method at {@link symbol}; consumers can check unknown values with + * {@link isPrimaryKey} and read the key with {@link value}. + * + * @since 2.0.0 + */ + +import { hasProperty } from "./Predicate.ts" + +/** + * Defines the unique identifier used to identify objects that implement the `PrimaryKey` interface. + * + * **When to use** + * + * Use to implement the `PrimaryKey` protocol as a computed property key on + * classes or object literals that expose a stable string identifier. + * + * @see {@link PrimaryKey} for the protocol interface that declares the method keyed by this symbol + * @see {@link value} for reading the string key from a `PrimaryKey` value + * @see {@link isPrimaryKey} for checking whether an unknown value carries this method + * + * @category symbols + * @since 2.0.0 + */ +export const symbol = "~effect/interfaces/PrimaryKey" + +/** + * An interface for objects that can provide a string-based primary key. + * + * **When to use** + * + * Use to define values that expose a stable string identifier for equality, + * hashing, caching, or persistence. + * + * **Details** + * + * Objects implementing this interface must provide a method that returns + * a unique string identifier. + * + * **Example** (Implementing a primary key) + * + * ```ts + * import { PrimaryKey } from "effect" + * + * class ProductId implements PrimaryKey.PrimaryKey { + * constructor(private category: string, private id: number) {} + * + * [PrimaryKey.symbol](): string { + * return `${this.category}-${this.id}` + * } + * } + * + * const productId = new ProductId("electronics", 42) + * console.log(PrimaryKey.value(productId)) // "electronics-42" + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface PrimaryKey { + [symbol](): string +} + +/** + * Checks whether a value implements the `PrimaryKey` protocol. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `PrimaryKey`. + * + * **Details** + * + * This is a structural guard for the `PrimaryKey.symbol` property. + * + * **Gotchas** + * + * This guard does not call the method or verify that it returns a string. + * + * @see {@link PrimaryKey} for the protocol being checked + * @see {@link value} for extracting the string value after narrowing + * + * @category models + * @since 4.0.0 + */ +export const isPrimaryKey = (u: unknown): u is PrimaryKey => hasProperty(u, symbol) + +/** + * Extracts the string value from a `PrimaryKey`. + * + * **When to use** + * + * Use to read the stable string identifier from a value that implements + * `PrimaryKey`. + * + * **Example** (Reading primary key values) + * + * ```ts + * import { PrimaryKey } from "effect" + * + * class OrderId implements PrimaryKey.PrimaryKey { + * constructor(private timestamp: number, private sequence: number) {} + * + * [PrimaryKey.symbol](): string { + * return `order_${this.timestamp}_${this.sequence}` + * } + * } + * + * const orderId = new OrderId(1640995200000, 1) + * console.log(PrimaryKey.value(orderId)) // "order_1640995200000_1" + * + * // Can also be used with simple string-based implementations + * const simpleKey = { + * [PrimaryKey.symbol]: () => "simple-key-123" + * } + * console.log(PrimaryKey.value(simpleKey)) // "simple-key-123" + * ``` + * + * @category accessors + * @since 2.0.0 + */ +export const value = (self: PrimaryKey): string => self[symbol]() diff --git a/repos/effect/packages/effect/src/PubSub.ts b/repos/effect/packages/effect/src/PubSub.ts new file mode 100644 index 0000000..dcbfbf1 --- /dev/null +++ b/repos/effect/packages/effect/src/PubSub.ts @@ -0,0 +1,2797 @@ +/** + * Broadcasts values from publishers to many subscribers. + * + * Publishers add messages with `publish` or `publishAll`, and each active + * `Subscription` receives its own copy of every accepted message. Unlike a + * queue, subscribers do not compete for messages. This module includes bounded, + * dropping, sliding, and unbounded hubs, optional replay buffers for late + * subscribers, message-taking helpers, capacity and shutdown operations, and + * low-level types for custom hub strategies. + * + * @since 2.0.0 + */ +import * as Arr from "./Array.ts" +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import type { LazyArg } from "./Function.ts" +import { dual, identity } from "./Function.ts" +import * as Latch from "./Latch.ts" +import * as MutableList from "./MutableList.ts" +import * as MutableRef from "./MutableRef.ts" +import { nextPow2 } from "./Number.ts" +import * as Option from "./Option.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import * as Scope from "./Scope.ts" +import type { Covariant, Invariant } from "./Types.ts" + +const TypeId = "~effect/PubSub" + +/** + * A `PubSub` is an asynchronous message hub into which publishers can publish + * messages of type `A` and subscribers can subscribe to take messages of type + * `A`. + * + * **Example** (Publishing and subscribing to messages) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a bounded PubSub with capacity 10 + * const pubsub = yield* PubSub.bounded(10) + * + * // Subscribe and consume messages + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish messages + * yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.publish(pubsub, "World") + * + * const message1 = yield* PubSub.take(subscription) + * const message2 = yield* PubSub.take(subscription) + * console.log(message1, message2) // "Hello", "World" + * })) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface PubSub extends Pipeable { + readonly [TypeId]: { + readonly _A: Invariant + } + readonly pubsub: PubSub.Atomic + readonly subscribers: PubSub.Subscribers + readonly scope: Scope.Closeable + readonly shutdownHook: Latch.Latch + readonly shutdownFlag: MutableRef.MutableRef + readonly strategy: PubSub.Strategy +} + +/** + * Companion namespace containing the low-level building blocks used by + * `PubSub`, including atomic implementations, backing subscriptions, replay + * windows, and delivery strategies. + * + * @since 2.0.0 + */ +export declare namespace PubSub { + /** + * Low-level atomic PubSub interface that handles the core message storage and retrieval. + * + * @category models + * @since 4.0.0 + */ + export interface Atomic { + readonly capacity: number + isEmpty(): boolean + isFull(): boolean + size(): number + publish(value: A): boolean + publishAll(elements: Iterable): Array + slide(): void + subscribe(): BackingSubscription + replayWindow(): ReplayWindow + } + + /** + * Low-level subscription interface that handles message polling for individual subscribers. + * + * @category models + * @since 4.0.0 + */ + export interface BackingSubscription { + isEmpty(): boolean + size(): number + poll(): A | MutableList.Empty + pollUpTo(n: number): Array + unsubscribe(): void + } + + /** + * Tracks the pollers currently waiting on each backing subscription. + * + * **Details** + * + * This type is part of the low-level `PubSub.Strategy` contract. Most + * application code should use `subscribe`, `take`, and the other `PubSub` + * operations instead of manipulating subscriber maps directly. + * + * @category models + * @since 4.0.0 + */ + export type Subscribers = Map< + BackingSubscription, + Set>> + > + + /** + * Interface for accessing replay buffer contents for late subscribers. + * + * @category models + * @since 4.0.0 + */ + export interface ReplayWindow { + take(): A | undefined + takeN(n: number): Array + takeAll(): Array + readonly remaining: number + } + + /** + * Strategy interface defining how PubSub handles backpressure and message distribution. + * + * @category models + * @since 4.0.0 + */ + export interface Strategy { + /** + * Describes any finalization logic associated with this strategy. + */ + readonly shutdown: Effect.Effect + + /** + * Describes how publishers should signal to subscribers that they are + * waiting for space to become available in the `PubSub`. + */ + handleSurplus( + pubsub: Atomic, + subscribers: Subscribers, + elements: Iterable, + isShutdown: MutableRef.MutableRef + ): Effect.Effect + + /** + * Describes how subscribers should signal to publishers waiting for space + * to become available in the `PubSub` that space may be available. + */ + onPubSubEmptySpaceUnsafe( + pubsub: Atomic, + subscribers: Subscribers + ): void + + /** + * Describes how subscribers waiting for additional values from the `PubSub` + * should take those values and signal to publishers that they are no + * longer waiting for additional values. + */ + completePollersUnsafe( + pubsub: Atomic, + subscribers: Subscribers, + subscription: BackingSubscription, + pollers: MutableList.MutableList> + ): void + + /** + * Describes how publishers should signal to subscribers waiting for + * additional values from the `PubSub` that new values are available. + */ + completeSubscribersUnsafe( + pubsub: Atomic, + subscribers: Subscribers + ): void + } +} + +const SubscriptionTypeId = "~effect/PubSub/Subscription" + +/** + * A subscription represents a consumer's connection to a PubSub, allowing them to take messages. + * + * **Example** (Taking messages from a subscription) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Subscribe within a scope for automatic cleanup + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription: PubSub.Subscription = yield* PubSub.subscribe( + * pubsub + * ) + * + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * + * // Take individual messages + * const message = yield* PubSub.take(subscription) + * console.log(message) // "msg1" + * + * // Take multiple messages + * const messages = yield* PubSub.takeUpTo(subscription, 1) + * console.log(messages) // ["msg2"] + * const allMessages = yield* PubSub.takeAll(subscription) + * console.log(allMessages) // ["msg3"] + * })) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Subscription extends Pipeable { + readonly [SubscriptionTypeId]: { + readonly _A: Covariant + } + readonly pubsub: PubSub.Atomic + readonly subscribers: PubSub.Subscribers + readonly subscription: PubSub.BackingSubscription + readonly pollers: MutableList.MutableList> + readonly shutdownHook: Latch.Latch + readonly shutdownFlag: MutableRef.MutableRef + readonly strategy: PubSub.Strategy + readonly replayWindow: PubSub.ReplayWindow +} + +/** + * Creates a PubSub with a custom atomic implementation and strategy. + * + * **Example** (Creating a PubSub with a custom strategy) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create custom PubSub with specific atomic implementation and strategy + * const pubsub = yield* PubSub.make({ + * atomicPubSub: () => PubSub.makeAtomicBounded(100), + * strategy: () => new PubSub.BackPressureStrategy() + * }) + * + * // Use the created PubSub + * yield* PubSub.publish(pubsub, "Hello") + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + options: { + readonly atomicPubSub: LazyArg> + readonly strategy: LazyArg> + } +): Effect.Effect> => + Effect.sync(() => + makePubSubUnsafe( + options.atomicPubSub(), + new Map(), + Scope.makeUnsafe(), + Latch.makeUnsafe(false), + MutableRef.make(false), + options.strategy() + ) + ) + +/** + * Creates a bounded `PubSub` that applies backpressure when it reaches + * capacity. + * + * **Details** + * + * Published messages are retained until all current subscribers have taken + * them. When the capacity is full, publishers suspend until space is available. + * Pass an options object to configure both `capacity` and an optional replay + * buffer for late subscribers. + * + * **Example** (Creating a bounded PubSub) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create bounded PubSub with capacity 100 + * const pubsub = yield* PubSub.bounded(100) + * + * // Create with replay buffer for late subscribers + * const pubsubWithReplay = yield* PubSub.bounded({ + * capacity: 100, + * replay: 10 // Last 10 messages replayed to new subscribers + * }) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const bounded = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + make({ + atomicPubSub: () => makeAtomicBounded(capacity), + strategy: () => new BackPressureStrategy() + }) + +/** + * Creates a bounded `PubSub` with the dropping strategy. The `PubSub` will drop new + * messages if the `PubSub` is at capacity. + * + * **Details** + * + * For best performance use capacities that are powers of two. + * + * **Example** (Dropping messages when full) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create dropping PubSub that drops new messages when full + * const pubsub = yield* PubSub.dropping(3) + * + * // With replay buffer for late subscribers + * const pubsubWithReplay = yield* PubSub.dropping({ + * capacity: 3, + * replay: 5 + * }) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Fill the PubSub and see dropping behavior + * yield* PubSub.publish(pubsub, "msg1") // succeeds + * yield* PubSub.publish(pubsub, "msg2") // succeeds + * yield* PubSub.publish(pubsub, "msg3") // succeeds + * const dropped = yield* PubSub.publish(pubsub, "msg4") // returns false (dropped) + * console.log("Message dropped:", !dropped) // true + * + * const messages = yield* PubSub.takeAll(subscription) + * console.log(messages) // ["msg1", "msg2", "msg3"] + * })) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const dropping = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + make({ + atomicPubSub: () => makeAtomicBounded(capacity), + strategy: () => new DroppingStrategy() + }) + +/** + * Creates a bounded `PubSub` with the sliding strategy. The `PubSub` will add new + * messages and drop old messages if the `PubSub` is at capacity. + * + * **Details** + * + * For best performance use capacities that are powers of two. + * + * **Example** (Sliding old messages when full) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create sliding PubSub that evicts old messages when full + * const pubsub = yield* PubSub.sliding(3) + * + * // With replay buffer + * const pubsubWithReplay = yield* PubSub.sliding({ + * capacity: 3, + * replay: 2 + * }) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Fill and overflow the PubSub + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") + * yield* PubSub.publish(pubsub, "msg3") + * yield* PubSub.publish(pubsub, "msg4") // "msg1" is evicted + * + * const messages = yield* PubSub.takeAll(subscription) + * console.log(messages) // ["msg2", "msg3", "msg4"] + * })) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sliding = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + make({ + atomicPubSub: () => makeAtomicBounded(capacity), + strategy: () => new SlidingStrategy() + }) + +/** + * Creates an unbounded `PubSub`. + * + * **Example** (Creating an unbounded PubSub) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create unbounded PubSub + * const pubsub = yield* PubSub.unbounded() + * + * // With replay buffer for late subscribers + * const pubsubWithReplay = yield* PubSub.unbounded({ + * replay: 10 + * }) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Can publish unlimited messages + * for (let i = 0; i < 3; i++) { + * yield* PubSub.publish(pubsub, `message-${i}`) + * } + * + * const message = yield* PubSub.take(subscription) + * console.log("First message:", message) // "message-0" + * })) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unbounded = (options?: { + readonly replay?: number | undefined +}): Effect.Effect> => + make({ + atomicPubSub: () => makeAtomicUnbounded(options), + strategy: () => new DroppingStrategy() + }) + +/** + * Creates a bounded atomic PubSub implementation with optional replay buffer. + * + * **When to use** + * + * Use to provide bounded message storage when building a custom `PubSub` with + * `make` and an explicit delivery strategy. + * + * **Details** + * + * Pass either a capacity number or an options object with `capacity` and + * optional `replay`. A positive `replay` value enables a replay buffer for late + * subscribers, and fractional replay sizes are rounded up. + * + * **Gotchas** + * + * The capacity must be greater than zero; invalid capacities throw + * synchronously before an atomic implementation is created. + * + * @see {@link make} for constructing a `PubSub` from an atomic implementation and delivery strategy + * @see {@link makeAtomicUnbounded} for an atomic implementation without a bounded capacity + * @see {@link bounded} for the higher-level backpressure constructor + * @see {@link dropping} for the higher-level dropping constructor + * @see {@link sliding} for the higher-level sliding constructor + * + * @category constructors + * @since 4.0.0 + */ +export const makeAtomicBounded = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): PubSub.Atomic => { + const options = typeof capacity === "number" ? { capacity } : capacity + ensureCapacity(options.capacity) + const replayBuffer = options.replay && options.replay > 0 ? new ReplayBuffer(Math.ceil(options.replay)) : undefined + if (options.capacity === 1) { + return new BoundedPubSubSingle(replayBuffer) + } else if (nextPow2(options.capacity) === options.capacity) { + return new BoundedPubSubPow2(options.capacity, replayBuffer) + } else { + return new BoundedPubSubArb(options.capacity, replayBuffer) + } +} + +/** + * Creates an unbounded atomic PubSub implementation with optional replay buffer. + * + * **When to use** + * + * Use to create the low-level storage layer for a custom `PubSub` whose active + * subscribers may retain an unbounded number of pending messages. + * + * **Gotchas** + * + * Messages published while subscribers are active can be retained without a + * capacity limit until those subscribers take them or unsubscribe. + * + * @see {@link makeAtomicBounded} for a bounded atomic implementation that enforces capacity + * @see {@link make} for wrapping an atomic implementation with a delivery strategy + * @see {@link unbounded} for the high-level effectful constructor for unbounded `PubSub` values + * + * @category constructors + * @since 4.0.0 + */ +export const makeAtomicUnbounded = (options?: { + readonly replay?: number | undefined +}): PubSub.Atomic => new UnboundedPubSub(options?.replay ? new ReplayBuffer(options.replay) : undefined) + +/** + * Returns the number of elements the queue can hold. + * + * **Example** (Getting PubSub capacity) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(100) + * const cap = PubSub.capacity(pubsub) + * console.log("PubSub capacity:", cap) // 100 + * + * const unboundedPubsub = yield* PubSub.unbounded() + * const unboundedCap = PubSub.capacity(unboundedPubsub) + * console.log("Unbounded capacity:", unboundedCap) // Number.MAX_SAFE_INTEGER + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const capacity = (self: PubSub): number => self.pubsub.capacity + +/** + * Returns the current number of messages retained by the `PubSub` for active + * subscribers. + * + * **Details** + * + * If the `PubSub` has been shut down, the returned effect succeeds with `0`. + * The size is not a count of waiting subscribers or suspended publishers. + * + * **Example** (Getting PubSub size) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Initially empty + * const initialSize = yield* PubSub.size(pubsub) + * console.log("Initial size:", initialSize) // 0 + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish some messages for the active subscription + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") + * + * const afterPublish = yield* PubSub.size(pubsub) + * console.log("After publishing:", afterPublish) // 2 + * + * yield* PubSub.takeAll(subscription) + * })) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: PubSub): Effect.Effect => Effect.sync(() => sizeUnsafe(self)) +/** + * Returns the current number of messages retained by the `PubSub` for active + * subscribers synchronously. + * + * **When to use** + * + * Use when an immediate `PubSub` size snapshot is needed outside effectful code + * and concurrent changes between the check and later use are acceptable. + * + * **Details** + * + * Returns `0` after shutdown. Because this is an unsafe synchronous snapshot, + * prefer `size` in effectful code. + * + * **Example** (Reading size synchronously) + * + * ```ts + * import { PubSub } from "effect" + * + * // Unsafe synchronous size check + * declare const pubsub: PubSub.PubSub + * + * const size = PubSub.sizeUnsafe(pubsub) + * console.log("Current size:", size) + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const sizeUnsafe = (self: PubSub): number => { + if (MutableRef.get(self.shutdownFlag)) { + return 0 + } + return self.pubsub.size() +} + +/** + * Returns `true` when the `PubSub` has reached its configured capacity. + * + * **Details** + * + * For unbounded PubSubs this is normally `false`. + * + * **Example** (Checking whether a PubSub is full) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(2) + * + * // Initially not full + * const initiallyFull = yield* PubSub.isFull(pubsub) + * console.log("Initially full:", initiallyFull) // false + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Fill the PubSub for the active subscription + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") + * + * const nowFull = yield* PubSub.isFull(pubsub) + * console.log("Now full:", nowFull) // true + * + * yield* PubSub.takeAll(subscription) + * })) + * }) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isFull = (self: PubSub): Effect.Effect => + Effect.map(size(self), (size) => size === self.pubsub.capacity) + +/** + * Returns `true` if the `Pubsub` contains zero elements, `false` otherwise. + * + * **Example** (Checking whether a PubSub is empty) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Initially empty + * const initiallyEmpty = yield* PubSub.isEmpty(pubsub) + * console.log("Initially empty:", initiallyEmpty) // true + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish a message for the active subscription + * yield* PubSub.publish(pubsub, "Hello") + * + * const nowEmpty = yield* PubSub.isEmpty(pubsub) + * console.log("Now empty:", nowEmpty) // false + * + * yield* PubSub.take(subscription) + * })) + * }) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isEmpty = (self: PubSub): Effect.Effect => Effect.map(size(self), (size) => size === 0) + +/** + * Shuts down the `PubSub`, interrupting suspended publishers and subscribers + * and finalizing active subscriptions. + * + * **Details** + * + * After shutdown, `publish` and `publishAll` succeed with `false`, + * `publishUnsafe` returns `false`, and subscription operations such as `take` + * interrupt. + * + * **Example** (Shutting down a PubSub) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(1) + * + * // Shutdown the PubSub + * yield* PubSub.shutdown(pubsub) + * + * const isShutdown = yield* PubSub.isShutdown(pubsub) + * console.log("Is shutdown:", isShutdown) // true + * + * // Publishing after shutdown returns false + * const published = yield* PubSub.publish(pubsub, "msg1") + * console.log("Published after shutdown:", published) // false + * }) + * ``` + * + * @category lifecycle + * @since 2.0.0 + */ +export const shutdown = (self: PubSub): Effect.Effect => + Effect.uninterruptible(Effect.withFiber((fiber) => { + MutableRef.set(self.shutdownFlag, true) + return Scope.close(self.scope, Exit.interrupt(fiber.id)).pipe( + Effect.andThen(self.strategy.shutdown), + Effect.when(self.shutdownHook.open), + Effect.asVoid + ) + })) + +/** + * Checks effectfully whether `shutdown` has been called, returning `true` + * after shutdown and `false` otherwise. + * + * **Example** (Checking whether a PubSub is shut down) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Initially not shutdown + * const initiallyShutdown = yield* PubSub.isShutdown(pubsub) + * console.log("Initially shutdown:", initiallyShutdown) // false + * + * // Shutdown the PubSub + * yield* PubSub.shutdown(pubsub) + * + * const nowShutdown = yield* PubSub.isShutdown(pubsub) + * console.log("Now shutdown:", nowShutdown) // true + * }) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isShutdown = (self: PubSub): Effect.Effect => Effect.sync(() => isShutdownUnsafe(self)) + +/** + * Checks synchronously whether `shutdown` has been called, returning `true` + * after shutdown and `false` otherwise. + * + * **When to use** + * + * Use when an immediate `PubSub` shutdown-state snapshot is needed outside + * effectful code and racing shutdown changes are acceptable. + * + * **Example** (Checking shutdown synchronously) + * + * ```ts + * import { PubSub } from "effect" + * + * declare const pubsub: PubSub.PubSub + * + * // Unsafe synchronous shutdown check + * const isDown = PubSub.isShutdownUnsafe(pubsub) + * if (isDown) { + * console.log("PubSub is shutdown, cannot publish") + * } else { + * console.log("PubSub is active") + * } + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isShutdownUnsafe = (self: PubSub): boolean => self.shutdownFlag.current + +/** + * Waits until the queue is shutdown. The `Effect` returned by this method will + * not resume until the queue has been shutdown. If the queue is already + * shutdown, the `Effect` will resume right away. + * + * **Example** (Waiting for shutdown) + * + * ```ts + * import { Effect, Fiber, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Start a fiber that waits for shutdown + * const waiterFiber = yield* Effect.forkChild( + * Effect.gen(function*() { + * yield* PubSub.awaitShutdown(pubsub) + * console.log("PubSub has been shutdown!") + * }) + * ) + * + * // Do some work... + * yield* Effect.sleep("100 millis") + * + * // Shutdown the PubSub + * yield* PubSub.shutdown(pubsub) + * + * // The waiter will now complete + * yield* Fiber.join(waiterFiber) + * }) + * ``` + * + * @category lifecycle + * @since 2.0.0 + */ +export const awaitShutdown = (self: PubSub): Effect.Effect => self.shutdownHook.await + +/** + * Publishes a message to the `PubSub` as an `Effect`, returning whether the + * message was accepted. + * + * **When to use** + * + * Use when you need to publish from effectful code and let the configured + * PubSub strategy handle surplus messages. + * + * **Details** + * + * The effect succeeds with `false` if the `PubSub` is shut down. If the message + * cannot be accepted immediately, the configured strategy decides how surplus + * messages are handled. + * + * **Example** (Publishing a message) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Publish a message + * const published = yield* PubSub.publish(pubsub, "Hello World") + * console.log("Message published:", published) // true + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * yield* PubSub.publish(pubsub, "Hello") + * const message = yield* PubSub.take(subscription) + * console.log("Received:", message) // "Hello" + * })) + * }) + * ``` + * + * @see {@link publishUnsafe} for a synchronous non-blocking attempt that does not run effectful surplus handling + * + * @category publishing + * @since 2.0.0 + */ +export const publish: { + (value: A): (self: PubSub) => Effect.Effect + (self: PubSub, value: A): Effect.Effect +} = dual(2, (self: PubSub, value: A): Effect.Effect => + Effect.suspend(() => { + if (self.shutdownFlag.current) { + return Effect.succeed(false) + } + + if (self.pubsub.publish(value)) { + self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers) + return Effect.succeed(true) + } + + return self.strategy.handleSurplus( + self.pubsub, + self.subscribers, + [value], + self.shutdownFlag + ) + })) + +/** + * Attempts to publish a message synchronously without applying the PubSub + * strategy's effectful surplus handling. + * + * **When to use** + * + * Use when you need a non-blocking synchronous publish attempt where `false` + * is an acceptable result when the message cannot be accepted immediately. + * + * **Details** + * + * Returns `false` if the `PubSub` is shut down or the message cannot be + * accepted immediately, for example when a bounded PubSub is full. Prefer + * `publish` when backpressure or sliding behavior should be honored. + * + * **Example** (Publishing without suspending) + * + * ```ts + * import { PubSub } from "effect" + * + * declare const pubsub: PubSub.PubSub + * + * // Unsafe synchronous publish (non-blocking) + * const published = PubSub.publishUnsafe(pubsub, "Hello") + * if (published) { + * console.log("Message published successfully") + * } else { + * console.log("Message dropped (PubSub full or shutdown)") + * } + * + * // Useful for scenarios where you don't want to suspend + * const messages = ["msg1", "msg2", "msg3"] + * const publishedCount = + * messages.filter((msg) => PubSub.publishUnsafe(pubsub, msg)).length + * console.log(`Published ${publishedCount} out of ${messages.length} messages`) + * ``` + * + * @see {@link publish} for effectful publishing that honors the configured surplus strategy + * + * @category publishing + * @since 4.0.0 + */ +export const publishUnsafe: { + (value: A): (self: PubSub) => boolean + (self: PubSub, value: A): boolean +} = dual(2, (self: PubSub, value: A): boolean => { + if (self.shutdownFlag.current) return false + if (self.pubsub.publish(value)) { + self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers) + return true + } + return false +}) + +/** + * Publishes all of the specified messages to the `PubSub`, returning whether they + * were published to the `PubSub`. + * + * **Example** (Publishing multiple messages) + * + * ```ts + * import { Effect, Fiber, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Publish multiple messages at once + * const messages = ["Hello", "World", "from", "Effect"] + * const allPublished = yield* PubSub.publishAll(pubsub, messages) + * console.log("All messages published:", allPublished) // true + * + * // With a smaller capacity and an active subscription + * const smallPubsub = yield* PubSub.bounded(2) + * const manyMessages = ["msg1", "msg2", "msg3", "msg4"] + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(smallPubsub) + * + * // Will suspend until space becomes available for all messages + * const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, manyMessages)) + * + * const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2) + * console.log("First batch:", firstBatch) // ["msg1", "msg2"] + * + * const result = yield* Fiber.join(fiber) + * console.log("All messages eventually published:", result) // true + * + * const secondBatch = yield* PubSub.takeAll(subscription) + * console.log("Second batch:", secondBatch) // ["msg3", "msg4"] + * })) + * }) + * ``` + * + * @category publishing + * @since 2.0.0 + */ +export const publishAll: { + (elements: Iterable): (self: PubSub) => Effect.Effect + (self: PubSub, elements: Iterable): Effect.Effect +} = dual(2, (self: PubSub, elements: Iterable): Effect.Effect => + Effect.suspend(() => { + if (self.shutdownFlag.current) { + return Effect.succeed(false) + } + const surplus = self.pubsub.publishAll(elements) + self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers) + if (surplus.length === 0) { + return Effect.succeed(true) + } + return self.strategy.handleSurplus( + self.pubsub, + self.subscribers, + surplus, + self.shutdownFlag + ) + })) + +/** + * Subscribes to receive messages from the `PubSub`. The resulting subscription can + * be evaluated multiple times within the scope to take a message from the `PubSub` + * each time. + * + * **Example** (Subscribing to messages) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * // Subscribe within a scope for automatic cleanup + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish some messages + * yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.publish(pubsub, "World") + * + * // Take messages one by one + * const msg1 = yield* PubSub.take(subscription) + * const msg2 = yield* PubSub.take(subscription) + * console.log(msg1, msg2) // "Hello", "World" + * + * // Subscription is automatically cleaned up when scope exits + * })) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const sub1 = yield* PubSub.subscribe(pubsub) + * const sub2 = yield* PubSub.subscribe(pubsub) + * + * // Multiple subscribers can receive the same messages + * yield* PubSub.publish(pubsub, "Broadcast") + * + * const [msg1, msg2] = yield* Effect.all([ + * PubSub.take(sub1), + * PubSub.take(sub2) + * ]) + * console.log("Both received:", msg1, msg2) // "Broadcast", "Broadcast" + * })) + * }) + * ``` + * + * @category subscriptions + * @since 2.0.0 + */ +export const subscribe = (self: PubSub): Effect.Effect, never, Scope.Scope> => + Effect.uninterruptible( + Effect.contextWith((services) => { + const localScope = Context.get(services, Scope.Scope) + const scope = Scope.forkUnsafe(self.scope) + const subscription = makeSubscriptionUnsafe(self.pubsub, self.subscribers, self.strategy) + return Scope.addFinalizer(scope, unsubscribe(subscription)).pipe( + Effect.andThen(Scope.addFinalizerExit(localScope, (exit) => Scope.close(scope, exit))), + Effect.as(subscription) + ) + }) + ) + +const unsubscribe = (self: Subscription): Effect.Effect => + Effect.uninterruptible( + Effect.withFiber((state) => { + MutableRef.set(self.shutdownFlag, true) + return Effect.forEach( + MutableList.takeAll(self.pollers), + (d) => Deferred.interruptWith(d, state.id), + { discard: true, concurrency: "unbounded" } + ).pipe( + Effect.tap(() => + Effect.sync(() => { + self.subscribers.delete(self.subscription) + self.subscription.unsubscribe() + self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) + }) + ), + Effect.when(self.shutdownHook.open), + Effect.asVoid + ) + }) + ) + +/** + * Takes a single message from the subscription. If no messages are available, + * this will suspend until a message becomes available. + * + * **Example** (Taking a message) + * + * ```ts + * import { Effect, Fiber, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Start a fiber to take a message (will suspend) + * const takeFiber = yield* Effect.forkChild( + * PubSub.take(subscription) + * ) + * + * // Publish a message + * yield* PubSub.publish(pubsub, "Hello") + * + * // The take will now complete + * const message = yield* Fiber.join(takeFiber) + * console.log("Received:", message) // "Hello" + * })) + * }) + * ``` + * + * @category subscriptions + * @since 4.0.0 + */ +export const take = (self: Subscription): Effect.Effect => + Effect.suspend(() => { + if (self.shutdownFlag.current) { + return Effect.interrupt + } + if (self.replayWindow.remaining > 0) { + const message = self.replayWindow.take()! + return Effect.succeed(message) + } + const message = self.pollers.length === 0 + ? self.subscription.poll() + : MutableList.Empty + if (message === MutableList.Empty) { + return pollForItem(self) + } else { + self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) + return Effect.succeed(message) + } + }) + +/** + * Takes all available messages from the subscription, suspending if no items + * are available. + * + * **Example** (Taking all available messages) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish multiple messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * + * // Take all available messages at once + * const allMessages = yield* PubSub.takeAll(subscription) + * console.log("All messages:", allMessages) // ["msg1", "msg2", "msg3"] + * })) + * }) + * ``` + * + * @category subscriptions + * @since 4.0.0 + */ +export const takeAll = (self: Subscription): Effect.Effect> => + Effect.suspend(function loop(value?: [A]): Effect.Effect> { + if (self.shutdownFlag.current) { + return Effect.interrupt + } + let as = self.pollers.length === 0 + ? self.subscription.pollUpTo(Number.POSITIVE_INFINITY) + : [] + if (value) { + as = value.concat(as) + } + self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) + if (self.replayWindow.remaining > 0) { + return Effect.succeed(self.replayWindow.takeAll().concat(as) as Arr.NonEmptyArray) + } else if (!Arr.isArrayNonEmpty(as)) { + return Effect.flatMap(pollForItem(self), (item) => loop([item])) + } + return Effect.succeed(as) + }) + +const pollForItem = (self: Subscription) => { + const deferred = Deferred.makeUnsafe() + let set = self.subscribers.get(self.subscription) + if (!set) { + set = new Set() + self.subscribers.set(self.subscription, set) + } + set.add(self.pollers) + MutableList.append(self.pollers, deferred) + self.strategy.completePollersUnsafe( + self.pubsub, + self.subscribers, + self.subscription, + self.pollers + ) + return Effect.onInterrupt( + Deferred.await(deferred), + () => { + MutableList.remove(self.pollers, deferred) + return Effect.void + } + ) +} + +/** + * Takes up to the specified number of messages from the subscription without suspending. + * + * **Example** (Taking up to a maximum number of messages) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish multiple messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"]) + * + * // Take up to 3 messages + * const upTo3 = yield* PubSub.takeUpTo(subscription, 3) + * console.log("Up to 3:", upTo3) // ["msg1", "msg2", "msg3"] + * + * // Take up to 5 more (only 2 remaining) + * const upTo5 = yield* PubSub.takeUpTo(subscription, 5) + * console.log("Up to 5:", upTo5) // ["msg4", "msg5"] + * + * // No more messages available + * const noMore = yield* PubSub.takeUpTo(subscription, 10) + * console.log("No more:", noMore) // [] + * })) + * }) + * ``` + * + * @category subscriptions + * @since 4.0.0 + */ +export const takeUpTo: { + (max: number): (self: Subscription) => Effect.Effect> + (self: Subscription, max: number): Effect.Effect> +} = dual(2, (self: Subscription, max: number): Effect.Effect> => + Effect.suspend(() => { + if (self.shutdownFlag.current) return Effect.interrupt + let replay: Array | undefined = undefined + if (self.replayWindow.remaining >= max) { + return Effect.succeed(self.replayWindow.takeN(max)) + } else if (self.replayWindow.remaining > 0) { + replay = self.replayWindow.takeAll() + max = max - replay.length + } + const as = self.pollers.length === 0 + ? self.subscription.pollUpTo(max) + : [] + self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) + return replay ? Effect.succeed(replay.concat(as)) : Effect.succeed(as) + })) + +/** + * Takes between the specified minimum and maximum number of messages from the subscription. + * Will suspend if the minimum number is not immediately available. + * + * **Example** (Taking between a minimum and maximum) + * + * ```ts + * import { Effect, Fiber, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Start taking between 2 and 5 messages (will suspend) + * const takeFiber = yield* Effect.forkChild( + * PubSub.takeBetween(subscription, 2, 5) + * ) + * + * // Publish 3 messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * + * // Now the take will complete with 3 messages + * const messages = yield* Fiber.join(takeFiber) + * console.log("Between 2-5:", messages) // ["msg1", "msg2", "msg3"] + * })) + * }) + * ``` + * + * @category subscriptions + * @since 4.0.0 + */ +export const takeBetween: { + (min: number, max: number): (self: Subscription) => Effect.Effect> + (self: Subscription, min: number, max: number): Effect.Effect> +} = dual( + 3, + (self: Subscription, min: number, max: number): Effect.Effect> => + Effect.suspend(() => takeRemainderLoop(self, min, max, [])) +) + +const takeRemainderLoop = ( + self: Subscription, + min: number, + max: number, + acc: Array +): Effect.Effect> => { + if (max < min) { + return Effect.succeed(acc) + } + return Effect.flatMap(takeUpTo(self, max), (bs) => { + acc.push(...bs) + const remaining = min - bs.length + if (remaining === 1) { + return Effect.map(take(self), (b) => { + acc.push(b) + return acc + }) + } + if (remaining > 1) { + return Effect.flatMap(take(self), (b) => { + acc.push(b) + return takeRemainderLoop( + self, + remaining - 1, + max - bs.length - 1, + acc + ) + }) + } + return Effect.succeed(acc) + }) +} + +/** + * Returns the number of messages currently available in the subscription as an + * `Effect`. + * + * **When to use** + * + * Use when checking a subscription from effectful code and shutdown should + * interrupt the effect. + * + * **Details** + * + * The count includes replay-buffered messages. If the subscription has been + * shut down, the effect interrupts. + * + * **Example** (Checking remaining messages) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(10) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish some messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * + * // Check how many messages are available + * const count = yield* PubSub.remaining(subscription) + * console.log("Messages available:", count) // 3 + * + * // Take one message + * yield* PubSub.take(subscription) + * + * const remaining = yield* PubSub.remaining(subscription) + * console.log("Messages remaining:", remaining) // 2 + * })) + * }) + * ``` + * + * @see {@link remainingUnsafe} for a synchronous check that reports shutdown as `Option.none()` + * + * @category getters + * @since 4.0.0 + */ +export const remaining = (self: Subscription): Effect.Effect => + Effect.suspend(() => + self.shutdownFlag.current + ? Effect.interrupt + : Effect.succeed(self.subscription.size() + self.replayWindow.remaining) + ) + +/** + * Synchronously returns the number of messages currently available in the + * subscription, or `Option.none()` when it is shut down. + * + * **When to use** + * + * Use when you need synchronous polling outside a managed workflow and want + * shutdown observed as data instead of interruption. + * + * **Example** (Checking remaining messages synchronously) + * + * ```ts + * import { PubSub } from "effect" + * + * declare const subscription: PubSub.Subscription + * + * // Unsafe synchronous check for remaining messages + * const remainingOption = PubSub.remainingUnsafe(subscription) + * if (remainingOption._tag === "Some") { + * console.log("Messages available:", remainingOption.value) + * } else { + * console.log("Subscription is shutdown") + * } + * + * // Useful for polling or batching scenarios + * if (remainingOption._tag === "Some" && remainingOption.value > 10) { + * // Process messages in batch + * } + * ``` + * + * @see {@link remaining} for the effectful variant that interrupts on shutdown + * + * @category getters + * @since 4.0.0 + */ +export const remainingUnsafe = (self: Subscription): Option.Option => { + if (self.shutdownFlag.current) { + return Option.none() + } + return Option.some(self.subscription.size() + self.replayWindow.remaining) +} + +// ----------------------------------------------------------------------------- +// internal +// ----------------------------------------------------------------------------- + +const AbsentValue = Symbol.for("effect/PubSub/AbsentValue") +type AbsentValue = typeof AbsentValue + +const addSubscribers = ( + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> +) => { + if (!subscribers.has(subscription)) { + subscribers.set(subscription, new Set()) + } + const set = subscribers.get(subscription)! + set.add(pollers) +} + +const removeSubscribers = ( + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> +) => { + if (!subscribers.has(subscription)) { + return + } + const set = subscribers.get(subscription)! + set.delete(pollers) + if (set.size === 0) { + subscribers.delete(subscription) + } +} + +const makeSubscriptionUnsafe = ( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + strategy: PubSub.Strategy +): Subscription => + new SubscriptionImpl( + pubsub, + subscribers, + pubsub.subscribe(), + MutableList.make>(), + Latch.makeUnsafe(false), + MutableRef.make(false), + strategy, + pubsub.replayWindow() + ) + +class BoundedPubSubArb implements PubSub.Atomic { + array: Array + publisherIndex = 0 + subscribers: Array + subscriberCount = 0 + subscribersIndex = 0 + + readonly capacity: number + readonly replayBuffer: ReplayBuffer | undefined + + constructor(capacity: number, replayBuffer: ReplayBuffer | undefined) { + this.capacity = capacity + this.replayBuffer = replayBuffer + this.array = Array.from({ length: capacity }) + this.subscribers = Array.from({ length: capacity }) + } + + replayWindow(): PubSub.ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherIndex === this.subscribersIndex + } + + isFull(): boolean { + return this.publisherIndex === this.subscribersIndex + this.capacity + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + const index = this.publisherIndex % this.capacity + this.array[index] = value + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Array { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return [] + } + const chunk = Arr.fromIterable(elements) + const n = chunk.length + const size = this.publisherIndex - this.subscribersIndex + const available = this.capacity - size + const forPubSub = Math.min(n, available) + if (forPubSub === 0) { + return chunk + } + let iteratorIndex = 0 + const publishAllIndex = this.publisherIndex + forPubSub + while (this.publisherIndex !== publishAllIndex) { + const a = chunk[iteratorIndex++] + const index = this.publisherIndex % this.capacity + this.array[index] = a + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(a) + } + } + return chunk.slice(iteratorIndex) + } + + slide(): void { + if (this.subscribersIndex !== this.publisherIndex) { + const index = this.subscribersIndex % this.capacity + this.array[index] = AbsentValue as unknown as A + this.subscribers[index] = 0 + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): PubSub.BackingSubscription { + this.subscriberCount += 1 + return new BoundedPubSubArbSubscription(this, this.publisherIndex, false) + } +} + +class BoundedPubSubArbSubscription implements PubSub.BackingSubscription { + private self: BoundedPubSubArb + private subscriberIndex: number + private unsubscribed: boolean + + constructor( + self: BoundedPubSubArb, + subscriberIndex: number, + unsubscribed: boolean + ) { + this.self = self + this.subscriberIndex = subscriberIndex + this.unsubscribed = unsubscribed + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.publisherIndex === this.subscriberIndex || + this.self.publisherIndex === this.self.subscribersIndex + ) + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(): A | MutableList.Empty { + if (this.unsubscribed) { + return MutableList.Empty + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + if (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex % this.self.capacity + const elem = this.self.array[index]! + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + return elem + } + return MutableList.Empty + } + + pollUpTo(n: number): Array { + if (this.unsubscribed) { + return [] + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + const size = this.self.publisherIndex - this.subscriberIndex + const toPoll = Math.min(n, size) + if (toPoll <= 0) { + return [] + } + const builder: Array = [] + const pollUpToIndex = this.subscriberIndex + toPoll + while (this.subscriberIndex !== pollUpToIndex) { + const index = this.subscriberIndex % this.self.capacity + const a = this.self.array[index] as A + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + builder.push(a) + this.subscriberIndex += 1 + } + + return builder + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + while (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex % this.self.capacity + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + } + } + } +} + +class BoundedPubSubPow2 implements PubSub.Atomic { + array: Array + mask: number + publisherIndex = 0 + subscribers: Array + subscriberCount = 0 + subscribersIndex = 0 + + readonly capacity: number + readonly replayBuffer: ReplayBuffer | undefined + + constructor(capacity: number, replayBuffer: ReplayBuffer | undefined) { + this.capacity = capacity + this.replayBuffer = replayBuffer + this.array = Array.from({ length: capacity }) + this.mask = capacity - 1 + this.subscribers = Array.from({ length: capacity }) + } + + replayWindow(): PubSub.ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherIndex === this.subscribersIndex + } + + isFull(): boolean { + return this.publisherIndex === this.subscribersIndex + this.capacity + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + const index = this.publisherIndex & this.mask + this.array[index] = value + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Array { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return [] + } + const chunk = Arr.fromIterable(elements) + const n = chunk.length + const size = this.publisherIndex - this.subscribersIndex + const available = this.capacity - size + const forPubSub = Math.min(n, available) + if (forPubSub === 0) { + return chunk + } + let iteratorIndex = 0 + const publishAllIndex = this.publisherIndex + forPubSub + while (this.publisherIndex !== publishAllIndex) { + const elem = chunk[iteratorIndex++] + const index = this.publisherIndex & this.mask + this.array[index] = elem + this.subscribers[index] = this.subscriberCount + this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(elem) + } + } + return chunk.slice(iteratorIndex) + } + + slide(): void { + if (this.subscribersIndex !== this.publisherIndex) { + const index = this.subscribersIndex & this.mask + this.array[index] = AbsentValue as unknown as A + this.subscribers[index] = 0 + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): PubSub.BackingSubscription { + this.subscriberCount += 1 + return new BoundedPubSubPow2Subscription(this, this.publisherIndex, false) + } +} + +class BoundedPubSubPow2Subscription implements PubSub.BackingSubscription { + private self: BoundedPubSubPow2 + private subscriberIndex: number + private unsubscribed: boolean + + constructor( + self: BoundedPubSubPow2, + subscriberIndex: number, + unsubscribed: boolean + ) { + this.self = self + this.subscriberIndex = subscriberIndex + this.unsubscribed = unsubscribed + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.publisherIndex === this.subscriberIndex || + this.self.publisherIndex === this.self.subscribersIndex + ) + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(): A | MutableList.Empty { + if (this.unsubscribed) { + return MutableList.Empty + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + if (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex & this.self.mask + const elem = this.self.array[index]! + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + return elem + } + return MutableList.Empty + } + + pollUpTo(n: number): Array { + if (this.unsubscribed) { + return [] + } + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + const size = this.self.publisherIndex - this.subscriberIndex + const toPoll = Math.min(n, size) + if (toPoll <= 0) { + return [] + } + const builder: Array = [] + const pollUpToIndex = this.subscriberIndex + toPoll + while (this.subscriberIndex !== pollUpToIndex) { + const index = this.subscriberIndex & this.self.mask + const elem = this.self.array[index] as A + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + builder.push(elem) + this.subscriberIndex += 1 + } + return builder + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex) + while (this.subscriberIndex !== this.self.publisherIndex) { + const index = this.subscriberIndex & this.self.mask + this.self.subscribers[index] -= 1 + if (this.self.subscribers[index] === 0) { + this.self.array[index] = AbsentValue as unknown as A + this.self.subscribersIndex += 1 + } + this.subscriberIndex += 1 + } + } + } +} + +class BoundedPubSubSingle implements PubSub.Atomic { + publisherIndex = 0 + subscriberCount = 0 + subscribers = 0 + value: A = AbsentValue as unknown as A + + readonly capacity = 1 + readonly replayBuffer: ReplayBuffer | undefined + + constructor(replayBuffer: ReplayBuffer | undefined) { + this.replayBuffer = replayBuffer + } + + replayWindow(): PubSub.ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + pipe() { + return pipeArguments(this, arguments) + } + + isEmpty(): boolean { + return this.subscribers === 0 + } + + isFull(): boolean { + return !this.isEmpty() + } + + size(): number { + return this.isEmpty() ? 0 : 1 + } + + publish(value: A): boolean { + if (this.isFull()) { + return false + } + if (this.subscriberCount !== 0) { + this.value = value + this.subscribers = this.subscriberCount + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Array { + if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return [] + } + const chunk = Arr.fromIterable(elements) + if (chunk.length === 0) { + return chunk + } + if (this.publish(chunk[0])) { + return chunk.slice(1) + } else { + return chunk + } + } + + slide(): void { + if (this.isFull()) { + this.subscribers = 0 + this.value = AbsentValue as unknown as A + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): PubSub.BackingSubscription { + this.subscriberCount += 1 + return new BoundedPubSubSingleSubscription(this, this.publisherIndex, false) + } +} + +class BoundedPubSubSingleSubscription implements PubSub.BackingSubscription { + private self: BoundedPubSubSingle + private subscriberIndex: number + private unsubscribed: boolean + + constructor( + self: BoundedPubSubSingle, + subscriberIndex: number, + unsubscribed: boolean + ) { + this.self = self + this.subscriberIndex = subscriberIndex + this.unsubscribed = unsubscribed + } + + isEmpty(): boolean { + return ( + this.unsubscribed || + this.self.subscribers === 0 || + this.subscriberIndex === this.self.publisherIndex + ) + } + + size() { + return this.isEmpty() ? 0 : 1 + } + + poll(): A | MutableList.Empty { + if (this.isEmpty()) { + return MutableList.Empty + } + const elem = this.self.value + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + this.subscriberIndex += 1 + return elem + } + + pollUpTo(n: number): Array { + if (this.isEmpty() || n < 1) { + return [] + } + const a = this.self.value + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + this.subscriberIndex += 1 + return [a] + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.subscriberCount -= 1 + if (this.subscriberIndex !== this.self.publisherIndex) { + this.self.subscribers -= 1 + if (this.self.subscribers === 0) { + this.self.value = AbsentValue as unknown as A + } + } + } + } +} + +interface Node { + value: A | AbsentValue + subscribers: number + next: Node | null +} + +class UnboundedPubSub implements PubSub.Atomic { + publisherHead: Node = { + value: AbsentValue, + subscribers: 0, + next: null + } + publisherTail = this.publisherHead + publisherIndex = 0 + subscribersIndex = 0 + + readonly capacity = Number.MAX_SAFE_INTEGER + readonly replayBuffer: ReplayBuffer | undefined + + constructor(replayBuffer: ReplayBuffer | undefined) { + this.replayBuffer = replayBuffer + } + + replayWindow(): PubSub.ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } + + isEmpty(): boolean { + return this.publisherHead === this.publisherTail + } + + isFull(): boolean { + return false + } + + size(): number { + return this.publisherIndex - this.subscribersIndex + } + + publish(value: A): boolean { + const subscribers = this.publisherTail.subscribers + if (subscribers !== 0) { + this.publisherTail.next = { + value, + subscribers, + next: null + } + this.publisherTail = this.publisherTail.next + this.publisherIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } + return true + } + + publishAll(elements: Iterable): Array { + if (this.publisherTail.subscribers !== 0) { + for (const a of elements) { + this.publish(a) + } + } else if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } + return [] + } + + slide(): void { + if (this.publisherHead !== this.publisherTail) { + this.publisherHead = this.publisherHead.next! + this.publisherHead.value = AbsentValue + this.subscribersIndex += 1 + } + if (this.replayBuffer) { + this.replayBuffer.slide() + } + } + + subscribe(): PubSub.BackingSubscription { + this.publisherTail.subscribers += 1 + return new UnboundedPubSubSubscription( + this, + this.publisherTail, + this.publisherIndex, + false + ) + } +} + +class UnboundedPubSubSubscription implements PubSub.BackingSubscription { + private self: UnboundedPubSub + private subscriberHead: Node + private subscriberIndex: number + private unsubscribed: boolean + + constructor( + self: UnboundedPubSub, + subscriberHead: Node, + subscriberIndex: number, + unsubscribed: boolean + ) { + this.self = self + this.subscriberHead = subscriberHead + this.subscriberIndex = subscriberIndex + this.unsubscribed = unsubscribed + } + + isEmpty(): boolean { + if (this.unsubscribed) { + return true + } + let empty = true + let loop = true + while (loop) { + if (this.subscriberHead === this.self.publisherTail) { + loop = false + } else { + if (this.subscriberHead.next!.value !== AbsentValue) { + empty = false + loop = false + } else { + this.subscriberHead = this.subscriberHead.next! + this.subscriberIndex += 1 + } + } + } + return empty + } + + size() { + if (this.unsubscribed) { + return 0 + } + return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex) + } + + poll(): A | MutableList.Empty { + if (this.unsubscribed) { + return MutableList.Empty + } + let loop = true + let polled: A | MutableList.Empty = MutableList.Empty + while (loop) { + if (this.subscriberHead === this.self.publisherTail) { + loop = false + } else { + const elem = this.subscriberHead.next!.value + if (elem !== AbsentValue) { + polled = elem + this.subscriberHead.subscribers -= 1 + if (this.subscriberHead.subscribers === 0) { + this.self.publisherHead = this.self.publisherHead.next! + this.self.publisherHead.value = AbsentValue + this.self.subscribersIndex += 1 + } + loop = false + } + this.subscriberHead = this.subscriberHead.next! + this.subscriberIndex += 1 + } + } + return polled + } + + pollUpTo(n: number): Array { + const builder: Array = [] + let i = 0 + while (i !== n) { + const a = this.poll() + if (a === MutableList.Empty) { + i = n + } else { + builder.push(a) + i += 1 + } + } + return builder + } + + unsubscribe(): void { + if (!this.unsubscribed) { + this.unsubscribed = true + this.self.publisherTail.subscribers -= 1 + while (this.subscriberHead !== this.self.publisherTail) { + if (this.subscriberHead.next!.value !== AbsentValue) { + this.subscriberHead.subscribers -= 1 + if (this.subscriberHead.subscribers === 0) { + this.self.publisherHead = this.self.publisherHead.next! + this.self.publisherHead.value = AbsentValue + this.self.subscribersIndex += 1 + } + } + this.subscriberHead = this.subscriberHead.next! + } + } + } +} + +class SubscriptionImpl implements Subscription { + readonly [SubscriptionTypeId] = { + _A: identity + } + + readonly pubsub: PubSub.Atomic + readonly subscribers: PubSub.Subscribers + readonly subscription: PubSub.BackingSubscription + readonly pollers: MutableList.MutableList> + readonly shutdownHook: Latch.Latch + readonly shutdownFlag: MutableRef.MutableRef + readonly strategy: PubSub.Strategy + readonly replayWindow: PubSub.ReplayWindow + + constructor( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList>, + shutdownHook: Latch.Latch, + shutdownFlag: MutableRef.MutableRef, + strategy: PubSub.Strategy, + replayWindow: PubSub.ReplayWindow + ) { + this.pubsub = pubsub + this.subscribers = subscribers + this.subscription = subscription + this.pollers = pollers + this.shutdownHook = shutdownHook + this.shutdownFlag = shutdownFlag + this.strategy = strategy + this.replayWindow = replayWindow + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +class PubSubImpl implements PubSub { + readonly [TypeId] = { + _A: identity + } + + readonly pubsub: PubSub.Atomic + readonly subscribers: PubSub.Subscribers + readonly scope: Scope.Closeable + readonly shutdownHook: Latch.Latch + readonly shutdownFlag: MutableRef.MutableRef + readonly strategy: PubSub.Strategy + + constructor( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + scope: Scope.Closeable, + shutdownHook: Latch.Latch, + shutdownFlag: MutableRef.MutableRef, + strategy: PubSub.Strategy + ) { + this.pubsub = pubsub + this.subscribers = subscribers + this.scope = scope + this.shutdownHook = shutdownHook + this.shutdownFlag = shutdownFlag + this.strategy = strategy + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +const makePubSubUnsafe = ( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + scope: Scope.Closeable, + shutdownHook: Latch.Latch, + shutdownFlag: MutableRef.MutableRef, + strategy: PubSub.Strategy +): PubSub => new PubSubImpl(pubsub, subscribers, scope, shutdownHook, shutdownFlag, strategy) + +const ensureCapacity = (capacity: number): void => { + if (capacity <= 0) { + throw new Error(`Cannot construct PubSub with capacity of ${capacity}`) + } +} + +// ----------------------------------------------------------------------------- +// PubSub.Strategy +// ----------------------------------------------------------------------------- + +/** + * Represents the back-pressure strategy for bounded `PubSub` values. + * + * **When to use** + * + * Use to preserve every message for current subscribers when a bounded custom + * `PubSub` should make publishers wait for capacity instead of dropping or + * evicting messages. + * + * **Details** + * + * Publishers wait when the `PubSub` is at capacity, so all current subscribers + * can receive every published message. + * + * **Gotchas** + * + * A slow subscriber can slow down publishers and other subscribers. + * + * @see {@link bounded} for creating bounded PubSubs with back pressure by default + * @see {@link DroppingStrategy} for dropping new messages when capacity is full + * @see {@link SlidingStrategy} for evicting old messages when capacity is full + * + * @category models + * @since 4.0.0 + */ +export class BackPressureStrategy implements PubSub.Strategy { + publishers: MutableList.MutableList< + readonly [A, Deferred.Deferred, boolean] + > = MutableList.make() + + get shutdown(): Effect.Effect { + return Effect.withFiber((fiber) => + Effect.forEach( + MutableList.takeAll(this.publishers), + ([_, deferred, last]) => last ? Deferred.interruptWith(deferred, fiber.id) : Effect.void, + { concurrency: "unbounded", discard: true } + ) + ) + } + + handleSurplus( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + elements: Iterable, + isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return Effect.suspend(() => { + const deferred = Deferred.makeUnsafe() + this.offerUnsafe(elements, deferred) + this.onPubSubEmptySpaceUnsafe(pubsub, subscribers) + this.completeSubscribersUnsafe(pubsub, subscribers) + return (MutableRef.get(isShutdown) ? Effect.interrupt : Deferred.await(deferred)).pipe( + Effect.onInterrupt(() => { + this.removeUnsafe(deferred) + return Effect.void + }) + ) + }) + } + + onPubSubEmptySpaceUnsafe( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers + ): void { + let keepPolling = true + while (keepPolling && !pubsub.isFull()) { + const publisher = MutableList.take(this.publishers) + if (publisher === MutableList.Empty) { + keepPolling = false + } else { + const [value, deferred] = publisher + const published = pubsub.publish(value) + if (published && publisher[2]) { + Deferred.doneUnsafe(deferred, Exit.succeed(true)) + } else if (!published) { + MutableList.prepend(this.publishers, publisher) + } + this.completeSubscribersUnsafe(pubsub, subscribers) + } + } + } + + completePollersUnsafe( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> + ): void { + return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers) + } + + completeSubscribersUnsafe(pubsub: PubSub.Atomic, subscribers: PubSub.Subscribers): void { + return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers) + } + + private offerUnsafe(elements: Iterable, deferred: Deferred.Deferred): void { + const iterator = elements[Symbol.iterator]() + let next: IteratorResult = iterator.next() + if (!next.done) { + // oxlint-disable-next-line no-constant-condition + while (1) { + const value = next.value + next = iterator.next() + if (next.done) { + MutableList.append(this.publishers, [value, deferred, true]) + break + } + MutableList.append(this.publishers, [value, deferred, false]) + } + } + } + + removeUnsafe(deferred: Deferred.Deferred): void { + MutableList.filter(this.publishers, ([_, d]) => d !== deferred) + } +} + +/** + * Represents the dropping strategy for bounded `PubSub` values. + * + * **When to use** + * + * Use to keep publishers fast by dropping new messages when the `PubSub` is at + * capacity. + * + * **Details** + * + * A publish that arrives while the `PubSub` is full is dropped instead of + * waiting for capacity. + * + * **Gotchas** + * + * Subscribers may miss messages published while they are subscribed. + * + * **Example** (Applying a dropping strategy) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create PubSub with dropping strategy + * const pubsub = yield* PubSub.dropping(2) + * + * // Or explicitly create with dropping strategy + * const customPubsub = yield* PubSub.make({ + * atomicPubSub: () => PubSub.makeAtomicBounded(2), + * strategy: () => new PubSub.DroppingStrategy() + * }) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Fill the PubSub + * const pub1 = yield* PubSub.publish(pubsub, "msg1") // true + * const pub2 = yield* PubSub.publish(pubsub, "msg2") // true + * const pub3 = yield* PubSub.publish(pubsub, "msg3") // false (dropped) + * + * console.log("Publication results:", [pub1, pub2, pub3]) // [true, true, false] + * + * // Subscribers will only see the first two messages + * const messages = yield* PubSub.takeAll(subscription) + * console.log("Received messages:", messages) // ["msg1", "msg2"] + * })) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export class DroppingStrategy implements PubSub.Strategy { + get shutdown(): Effect.Effect { + return Effect.void + } + + handleSurplus( + _pubsub: PubSub.Atomic, + _subscribers: PubSub.Subscribers, + _elements: Iterable, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return Effect.succeed(false) + } + + onPubSubEmptySpaceUnsafe( + _pubsub: PubSub.Atomic, + _subscribers: PubSub.Subscribers + ): void { + // + } + + completePollersUnsafe( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> + ): void { + return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers) + } + + completeSubscribersUnsafe(pubsub: PubSub.Atomic, subscribers: PubSub.Subscribers): void { + return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers) + } +} + +/** + * Represents the sliding strategy for bounded `PubSub` values. + * + * **When to use** + * + * Use to keep the most recent messages when the `PubSub` is at capacity. + * + * **Details** + * + * New messages are accepted by evicting older messages from the bounded + * `PubSub`. + * + * **Gotchas** + * + * Slow subscribers may miss older messages that are evicted before they are + * consumed. + * + * **Example** (Applying a sliding strategy) + * + * ```ts + * import { Effect, PubSub } from "effect" + * + * const program = Effect.gen(function*() { + * // Create PubSub with sliding strategy + * const pubsub = yield* PubSub.sliding(2) + * + * // Or explicitly create with sliding strategy + * const customPubsub = yield* PubSub.make({ + * atomicPubSub: () => PubSub.makeAtomicBounded(2), + * strategy: () => new PubSub.SlidingStrategy() + * }) + * + * yield* Effect.scoped(Effect.gen(function*() { + * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publish messages that exceed capacity + * yield* PubSub.publish(pubsub, "msg1") // stored + * yield* PubSub.publish(pubsub, "msg2") // stored + * yield* PubSub.publish(pubsub, "msg3") // "msg1" evicted, "msg3" stored + * yield* PubSub.publish(pubsub, "msg4") // "msg2" evicted, "msg4" stored + * + * // Subscribers will see the most recent messages + * const messages = yield* PubSub.takeAll(subscription) + * console.log("Recent messages:", messages) // ["msg3", "msg4"] + * })) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export class SlidingStrategy implements PubSub.Strategy { + get shutdown(): Effect.Effect { + return Effect.void + } + + handleSurplus( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + elements: Iterable, + _isShutdown: MutableRef.MutableRef + ): Effect.Effect { + return Effect.sync(() => { + this.slidingPublishUnsafe(pubsub, elements) + this.completeSubscribersUnsafe(pubsub, subscribers) + return true + }) + } + + onPubSubEmptySpaceUnsafe( + _pubsub: PubSub.Atomic, + _subscribers: PubSub.Subscribers + ): void { + // + } + + completePollersUnsafe( + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> + ): void { + return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers) + } + + completeSubscribersUnsafe(pubsub: PubSub.Atomic, subscribers: PubSub.Subscribers): void { + return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers) + } + + slidingPublishUnsafe(pubsub: PubSub.Atomic, elements: Iterable): void { + const it = elements[Symbol.iterator]() + let next = it.next() + if (!next.done && pubsub.capacity > 0) { + let a = next.value + let loop = true + while (loop) { + pubsub.slide() + const pub = pubsub.publish(a) + if (pub && (next = it.next()) && !next.done) { + a = next.value + } else if (pub) { + loop = false + } + } + } + } +} + +const strategyCompletePollersUnsafe = ( + strategy: PubSub.Strategy, + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers, + subscription: PubSub.BackingSubscription, + pollers: MutableList.MutableList> +): void => { + let keepPolling = true + while (keepPolling && !subscription.isEmpty()) { + const poller = MutableList.take(pollers) + if (poller === MutableList.Empty) { + removeSubscribers(subscribers, subscription, pollers) + if (pollers.length === 0) { + keepPolling = false + } else { + addSubscribers(subscribers, subscription, pollers) + } + } else { + const pollResult = subscription.poll() + if (pollResult === MutableList.Empty) { + MutableList.prepend(pollers, poller) + } else { + Deferred.doneUnsafe(poller, Exit.succeed(pollResult)) + strategy.onPubSubEmptySpaceUnsafe(pubsub, subscribers) + } + } + } +} + +const strategyCompleteSubscribersUnsafe = ( + strategy: PubSub.Strategy, + pubsub: PubSub.Atomic, + subscribers: PubSub.Subscribers +): void => { + for ( + const [subscription, pollersSet] of subscribers + ) { + for (const pollers of pollersSet) { + strategy.completePollersUnsafe(pubsub, subscribers, subscription, pollers) + } + } +} + +interface ReplayNode { + value: A | AbsentValue + next: ReplayNode | null +} + +class ReplayBuffer { + readonly capacity: number + head: ReplayNode = { value: AbsentValue, next: null } + tail: ReplayNode = this.head + size = 0 + index = 0 + + constructor(capacity: number) { + this.capacity = capacity + } + + slide() { + this.index++ + } + offer(a: A): void { + this.tail.value = a + this.tail.next = { + value: AbsentValue, + next: null + } + this.tail = this.tail.next + if (this.size === this.capacity) { + this.head = this.head.next! + } else { + this.size += 1 + } + } + offerAll(as: Iterable): void { + for (const a of as) { + this.offer(a) + } + } +} + +class ReplayWindowImpl implements PubSub.ReplayWindow { + head: ReplayNode + index: number + remaining: number + readonly buffer: ReplayBuffer + + constructor(buffer: ReplayBuffer) { + this.buffer = buffer + this.index = buffer.index + this.remaining = buffer.size + this.head = buffer.head + } + fastForward() { + while (this.index < this.buffer.index) { + this.head = this.head.next! + this.index++ + } + } + take(): A | undefined { + if (this.remaining === 0) { + return undefined + } else if (this.index < this.buffer.index) { + this.fastForward() + } + this.remaining-- + const value = this.head.value + this.head = this.head.next! + return value as A + } + takeN(n: number): Array { + if (this.remaining === 0) { + return [] + } else if (this.index < this.buffer.index) { + this.fastForward() + } + const len = Math.min(n, this.remaining) + const items = new Array(len) + for (let i = 0; i < len; i++) { + const value = this.head.value as A + this.head = this.head.next! + items[i] = value + } + this.remaining -= len + return items + } + takeAll(): Array { + return this.takeN(this.remaining) + } +} + +const emptyReplayWindow: PubSub.ReplayWindow = { + remaining: 0, + take: () => undefined, + takeN: () => [], + takeAll: () => [] +} diff --git a/repos/effect/packages/effect/src/Pull.ts b/repos/effect/packages/effect/src/Pull.ts new file mode 100644 index 0000000..5409219 --- /dev/null +++ b/repos/effect/packages/effect/src/Pull.ts @@ -0,0 +1,376 @@ +/** + * Models one low-level pull step for stream-like consumers. + * + * A `Pull` is an `Effect` that can produce one `A`, fail with an + * ordinary error `E`, or signal end-of-input with `Cause.Done`. The + * separate done signal lets low-level consumers distinguish normal completion + * from failure. This module includes type extractors and helpers for detecting, + * filtering, catching, converting, and matching done signals separately from + * ordinary failures. + * + * @since 4.0.0 + */ +import * as Cause from "./Cause.ts" +import type { Effect } from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Filter from "./Filter.ts" +import { dual } from "./Function.ts" +import * as internalEffect from "./internal/effect.ts" +import * as Result from "./Result.ts" + +/** + * An effectful pull step that either produces a value, fails with `E`, or + * signals completion with `Cause.Done`. + * + * **When to use** + * + * Use to model one low-level pull step when a consumer repeatedly evaluates an + * effect that may emit a value, fail normally, or signal normal completion + * through `Cause.Done`. + * + * **Details** + * + * `Pull` represents completion in the error channel so low-level stream + * consumers can distinguish ordinary failures from end-of-input and carry a + * leftover value when needed. + * + * @category models + * @since 4.0.0 + */ +export interface Pull + extends Effect, R> +{} + +/** + * Extracts the success type from a Pull type. + * + * **When to use** + * + * Use to derive the value produced by an existing `Pull` when declaring + * reusable type aliases, low-level stream helpers, or function signatures. + * + * @see {@link Error} for extracting the ordinary failure type + * @see {@link Leftover} for extracting the completion leftover type + * @see {@link Services} for extracting the required services type instead + * + * @category type extractors + * @since 4.0.0 + */ +export type Success

= P extends Effect ? _A : never + +/** + * Extracts the error type from a Pull type, excluding Done errors. + * + * **When to use** + * + * Use to derive only the ordinary failure type from a `Pull` when declaring + * wrappers or APIs that handle completion separately. + * + * @see {@link Success} for extracting the pulled value type instead + * @see {@link Leftover} for extracting the completion leftover type + * @see {@link Services} for extracting the required services type instead + * @see {@link ExcludeDone} for excluding `Cause.Done` from an error union + * + * @category type extractors + * @since 4.0.0 + */ +export type Error

= P extends Effect ? _E extends Cause.Done ? never : _E + : never + +/** + * Extracts the leftover type from a Pull type. + * + * **When to use** + * + * Use to derive the completion leftover type from an existing `Pull` when + * declaring reusable type aliases or helper signatures that preserve a pull's + * done value. + * + * @see {@link Success} for extracting the pulled value type instead + * @see {@link Error} for extracting the ordinary failure type, excluding `Cause.Done` + * @see {@link Services} for extracting the required services type instead + * + * @category type extractors + * @since 4.0.0 + */ +export type Leftover

= P extends Effect ? _E extends Cause.Done ? _L : never + : never + +/** + * Extracts the service requirements (context) type from a Pull type. + * + * **When to use** + * + * Use to derive the context requirements of a generic or inferred `Pull` + * without restating its `R` type parameter. + * + * @see {@link Success} for extracting the pulled value type instead + * @see {@link Error} for extracting the ordinary failure type + * @see {@link Leftover} for extracting the completion leftover type + * + * @category type extractors + * @since 4.0.0 + */ +export type Services

= P extends Effect ? _R : never + +/** + * Excludes `Cause.Done` completion signals from an error type union. + * + * **When to use** + * + * Use to describe the ordinary error type that remains after `Cause.Done` + * completion signals have been handled or filtered out of an error union. + * + * @see {@link Error} for extracting ordinary failures from a `Pull` + * @see {@link Leftover} for extracting the completion leftover type + * + * @category type extractors + * @since 4.0.0 + */ +export type ExcludeDone = Exclude> + +// ----------------------------------------------------------------------------- +// Done +// ----------------------------------------------------------------------------- + +/** + * Handles `Cause.Done` failures in an effect while leaving ordinary failures + * in the error channel. + * + * **When to use** + * + * Use to recover from a `Cause.Done` completion signal in an effect, such as + * turning a pull leftover value into a successful recovery effect while + * preserving ordinary failures. + * + * **Details** + * + * The handler receives the done leftover value and may recover with a new + * effect. Non-done errors are preserved. + * + * @see {@link matchEffect} for handling success, ordinary failure, and done outcomes explicitly + * @see {@link filterDoneLeftover} for extracting a done leftover from an existing `Cause` + * + * @category Done + * @since 4.0.0 + */ +export const catchDone: { + (f: (leftover: Cause.Done.Extract) => Effect): ( + self: Effect + ) => Effect | E2, R | R2> + ( + self: Effect, + f: (leftover: Cause.Done.Extract) => Effect + ): Effect | E2, R | R2> +} = dual(2, ( + effect: Effect, + f: (leftover: Cause.Done.Extract) => Effect +): Effect | E2, R | R2> => + internalEffect.catchCauseFilter(effect, filterDoneLeftover as any, (l: any) => f(l)) as any) + +/** + * Checks whether a Cause contains any done errors. + * + * **When to use** + * + * Use when you need to test whether a pull failure cause represents normal + * completion and only need a boolean result. + * + * @see {@link isDoneFailure} for checking a single `Cause.Reason` + * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` + * @see {@link filterNoDone} for selecting causes with no done failures + * + * @category Done + * @since 4.0.0 + */ +export const isDoneCause = (cause: Cause.Cause): boolean => cause.reasons.some(isDoneFailure) + +/** + * Checks whether a `Cause.Reason` is a `Fail` reason whose error is a + * `Cause.Done` signal. + * + * **When to use** + * + * Use when you need to identify done completion reasons while traversing + * `cause.reasons`, before handling ordinary failures. + * + * @see {@link isDoneCause} for checking an entire `Cause` for any done reason + * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` + * + * @category Done + * @since 4.0.0 + */ +export const isDoneFailure = ( + failure: Cause.Reason +): failure is Cause.Fail> => failure._tag === "Fail" && Cause.isDone(failure.error) + +/** + * Finds a `Cause.Done` failure in a `Cause`. + * + * **When to use** + * + * Use to separate `Cause.Done` completion from ordinary causes while preserving + * the typed done value. + * + * **Details** + * + * Returns a successful `Result` with the `Cause.Done` value when one is + * present, otherwise returns a failed `Result` containing the non-done cause. + * + * @category Done + * @since 4.0.0 + */ +export const filterDone: ( + input: Cause.Cause +) => Result.Result, Cause.Cause>> = Filter + .composePassthrough( + Cause.findError, + (e) => Cause.isDone(e) ? Result.succeed(e) : Result.fail(e) + ) as any + +/** + * Finds a `Cause.Done` failure in a cause whose done value is not used. + * + * **When to use** + * + * Use to detect `Cause.Done` completion in a `Cause` when the completion value + * is not part of the downstream logic. + * + * **Details** + * + * Returns a successful `Result` with the done marker when present, otherwise + * returns a failed `Result` with the non-done cause. + * + * @see {@link filterDone} for preserving the typed `Cause.Done` value when the done payload matters + * @see {@link filterDoneLeftover} for extracting only the done leftover value + * @see {@link filterNoDone} for the inverse filter that succeeds only when no done failure is present + * + * @category Done + * @since 4.0.0 + */ +export const filterDoneVoid: ( + input: Cause.Cause +) => Result.Result>> = Filter.composePassthrough( + Cause.findError, + (e) => Cause.isDone(e) ? Result.succeed(e) : Result.fail(e) +) as any + +/** + * Keeps a `Cause` only when it contains no `Cause.Done` failures. + * + * **When to use** + * + * Use to select ordinary failure causes for handling while leaving `Cause.Done` + * completion causes outside that handler. + * + * **Details** + * + * Returns a successful `Result` with the cause when every failure is non-done; + * otherwise returns a failed `Result` with the original cause. + * + * @see {@link filterDone} for the inverse typed done filter + * @see {@link filterDoneVoid} for done detection when the payload is not needed + * + * @category Done + * @since 4.0.0 + */ +export const filterNoDone: ( + input: Cause.Cause +) => Result.Result< + Cause.Cause>, + Cause.Cause +> = Filter.fromPredicate((cause: Cause.Cause) => + cause.reasons.every((failure) => !isDoneFailure(failure)) +) as any + +/** + * Filters a Cause to extract the leftover value from done errors. + * + * **When to use** + * + * Use to extract only the leftover value carried by a `Cause.Done` completion + * signal. + * + * @category Done + * @since 4.0.0 + */ +export const filterDoneLeftover: ( + cause: Cause.Cause +) => Result.Result, Cause.Cause>> = Filter.composePassthrough( + Cause.findError, + (e) => Cause.isDone(e) ? Result.succeed(e.value) : Result.fail(e) +) as any + +/** + * Converts a `Cause` into an `Exit`, treating `Cause.Done` as successful + * completion. + * + * **When to use** + * + * Use to produce an `Exit` for finalizing a low-level pull workflow when a + * `Cause.Done` signal should be treated as success and any remaining cause + * should fail. + * + * **Details** + * + * If the cause contains a done value, that leftover becomes the successful + * value. Otherwise the non-done cause becomes the failure cause. + * + * @see {@link filterDone} for extracting the done signal without converting the cause to an `Exit` + * @see {@link matchEffect} for handling `Pull` success, failure, and done outcomes directly + * + * @category Done + * @since 4.0.0 + */ +export const doneExitFromCause = (cause: Cause.Cause): Exit.Exit, ExcludeDone> => { + const halt = filterDone(cause) + return !Result.isFailure(halt) ? Exit.succeed(halt.success.value as any) : Exit.failCause(halt.failure) +} + +/** + * Pattern matches on a Pull, handling success, failure, and done cases. + * + * **When to use** + * + * Use to handle all three `Pull` outcomes with effectful handlers. + * + * **Example** (Matching Pull outcomes) + * + * ```ts + * import { Cause, Effect, Pull } from "effect" + * + * const pull = Cause.done("stream ended") + * + * const result = Pull.matchEffect(pull, { + * onSuccess: (value) => Effect.succeed(`Got value: ${value}`), + * onFailure: (cause) => Effect.succeed(`Got error: ${cause}`), + * onDone: (leftover) => Effect.succeed(`Stream halted with: ${leftover}`) + * }) + * ``` + * + * @category pattern matching + * @since 4.0.0 + */ +export const matchEffect: { + (options: { + readonly onSuccess: (value: A) => Effect + readonly onFailure: (failure: Cause.Cause) => Effect + readonly onDone: (leftover: L) => Effect + }): (self: Pull) => Effect + (self: Pull, options: { + readonly onSuccess: (value: A) => Effect + readonly onFailure: (failure: Cause.Cause) => Effect + readonly onDone: (leftover: L) => Effect + }): Effect +} = dual(2, (self: Pull, options: { + readonly onSuccess: (value: A) => Effect + readonly onFailure: (failure: Cause.Cause) => Effect + readonly onDone: (leftover: L) => Effect +}): Effect => + internalEffect.matchCauseEffect(self, { + onSuccess: options.onSuccess, + onFailure: (cause): Effect => { + const halt = filterDone(cause) + return !Result.isFailure(halt) ? options.onDone(halt.success.value as L) : options.onFailure(halt.failure) + } + })) diff --git a/repos/effect/packages/effect/src/Queue.ts b/repos/effect/packages/effect/src/Queue.ts new file mode 100644 index 0000000..bd0b4a2 --- /dev/null +++ b/repos/effect/packages/effect/src/Queue.ts @@ -0,0 +1,1962 @@ +/** + * Passes values asynchronously between fibers. + * + * A `Queue` accepts values, hands each value to one consumer in offer + * order, and can complete, fail, interrupt, or shut down. Queues can be bounded + * or unbounded, and bounded queues can suspend, drop, or slide values when + * producers are faster than consumers. + * + * @since 3.8.0 + */ +import * as Arr from "./Array.ts" +import type { Cause, Done } from "./Cause.ts" +import type { Effect } from "./Effect.ts" +import type { Exit, Failure } from "./Exit.ts" +import { constant, constTrue, dual, identity } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as core from "./internal/core.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as internalEffect from "./internal/effect.ts" +import * as MutableList from "./MutableList.ts" +import * as Option from "./Option.ts" +import { hasProperty } from "./Predicate.ts" +import * as Pull from "./Pull.ts" +import type { SchedulerDispatcher } from "./Scheduler.ts" +import type * as Types from "./Types.ts" + +const TypeId = "~effect/Queue" +const EnqueueTypeId = "~effect/Queue/Enqueue" +const DequeueTypeId = "~effect/Queue/Dequeue" + +/** + * Type guard to check if a value is a Queue. + * + * **When to use** + * + * Use to narrow an unknown value to a full `Queue` before passing it to APIs + * that need both offering and taking capabilities. + * + * @see {@link isEnqueue} for checking values that only need write access + * @see {@link isDequeue} for checking values that only need read access + * + * @category guards + * @since 2.0.0 + */ +export const isQueue = ( + u: unknown +): u is Queue => hasProperty(u, TypeId) + +/** + * Type guard to check if a value is an Enqueue. + * + * **When to use** + * + * Use to narrow an unknown value before calling queue operations that require + * write-side access. + * + * **Gotchas** + * + * A full `Queue` also satisfies this guard because every queue includes the + * enqueue side. + * + * @see {@link isQueue} for checking for a full read-write queue handle + * @see {@link isDequeue} for checking for the read side of a queue + * @see {@link asEnqueue} for narrowing an existing `Queue` to its write-only interface + * + * @category guards + * @since 2.0.0 + */ +export const isEnqueue = ( + u: unknown +): u is Enqueue => hasProperty(u, EnqueueTypeId) + +/** + * Type guard to check if a value is a Dequeue. + * + * **When to use** + * + * Use to narrow an unknown value before passing it to read-side queue + * operations. + * + * @see {@link Dequeue} for the read-side queue handle checked by this guard + * @see {@link isQueue} for checking for a full read-write queue handle + * @see {@link isEnqueue} for checking for the write side of a queue + * @see {@link asDequeue} for narrowing an existing `Queue` to its read-only interface + * + * @category guards + * @since 2.0.0 + */ +export const isDequeue = ( + u: unknown +): u is Dequeue => hasProperty(u, DequeueTypeId) + +/** + * Converts a `Queue` to its write-only `Enqueue` interface. + * + * **When to use** + * + * Use to expose only the producer side of a `Queue` to code that should offer + * values or signal queue lifecycle. + * + * **Gotchas** + * + * This is a type-level capability restriction. It returns the same queue + * object, so it does not hide read operations at runtime. + * + * @see {@link asDequeue} for exposing only the read side of a `Queue` + * @see {@link Enqueue} for the write-only queue handle returned by this conversion + * + * @category converting + * @since 4.0.0 + */ +export const asEnqueue = (self: Queue): Enqueue => self + +/** + * Narrows a `Queue` to a `Dequeue`, exposing the consumer side of the queue. + * + * **When to use** + * + * Use to pass a queue to code that should consume values while keeping + * producer-side operations out of that code's TypeScript type. + * + * **Gotchas** + * + * This is a type-level narrowing operation. It returns the same queue object + * and does not create a runtime wrapper. + * + * @see {@link asEnqueue} for narrowing a queue to its producer side + * @see {@link Dequeue} for the consumer-side queue handle returned by this function + * + * @category converting + * @since 4.0.0 + */ +export const asDequeue: (self: Queue) => Dequeue = identity + +/** + * An `Enqueue` is a queue that can be offered to. + * + * **Details** + * + * This interface represents the write-only part of a Queue, allowing you to offer + * elements to the queue but not take elements from it. + * + * **Example** (Offering through enqueue handles) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * // Function that only needs write access to a queue + * const producer = (enqueue: Queue.Enqueue) => + * Effect.gen(function*() { + * yield* Queue.offer(enqueue, "hello") + * yield* Queue.offerAll(enqueue, ["world", "!"]) + * }) + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * yield* producer(queue) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Enqueue extends Inspectable { + readonly [EnqueueTypeId]: Enqueue.Variance + readonly strategy: "suspend" | "dropping" | "sliding" + readonly dispatcher: SchedulerDispatcher + capacity: number + messages: MutableList.MutableList + state: Queue.State + scheduleRunning: boolean +} + +/** + * Companion namespace containing type-level metadata for the `Enqueue` + * write-only queue interface. + * + * @since 2.0.0 + */ +export declare namespace Enqueue { + /** + * Type-level variance marker for `Enqueue`. + * + * **Details** + * + * `Enqueue` is contravariant in both its offered value type `A` and failure + * type `E`, because values and failures flow into the queue through this + * handle. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + _A: Types.Contravariant + _E: Types.Contravariant + } +} + +/** + * A `Dequeue` is a queue that can be taken from. + * + * **Details** + * + * This interface represents the read-only part of a Queue, allowing you to take + * elements from the queue but not offer elements to it. + * + * **Example** (Taking through dequeue handles) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // A Dequeue can only take elements + * const dequeue: Queue.Dequeue = queue + * + * // Pre-populate the queue + * yield* Queue.offerAll(queue, ["a", "b", "c"]) + * + * // Take elements using dequeue interface + * const item = yield* Queue.take(dequeue) + * console.log(item) // "a" + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Dequeue extends Inspectable { + readonly [DequeueTypeId]: Dequeue.Variance + readonly strategy: "suspend" | "dropping" | "sliding" + readonly dispatcher: SchedulerDispatcher + capacity: number + messages: MutableList.MutableList + state: Queue.State + scheduleRunning: boolean +} + +/** + * Companion namespace containing type-level metadata for the `Dequeue` + * read-only queue interface. + * + * @since 2.0.0 + */ +export declare namespace Dequeue { + /** + * Type-level variance marker for `Dequeue`. + * + * **Details** + * + * `Dequeue` is covariant in both the taken value type `A` and failure type + * `E`, because values and failures are observed through this handle. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + _A: Types.Covariant + _E: Types.Covariant + } +} + +/** + * A `Queue` is an asynchronous queue that can be offered to and taken from. + * + * **Details** + * + * It also supports signaling that it is done or failed. + * + * **Example** (Offering and taking queue values) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a bounded queue + * const queue = yield* Queue.bounded(10) + * + * // Producer: offer items to the queue + * yield* Queue.offer(queue, "hello") + * yield* Queue.offerAll(queue, ["world", "!"]) + * + * // Consumer: take items from the queue + * const item1 = yield* Queue.take(queue) + * const item2 = yield* Queue.take(queue) + * const item3 = yield* Queue.take(queue) + * + * console.log([item1, item2, item3]) // ["hello", "world", "!"] + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Queue extends Enqueue, Dequeue { + readonly [TypeId]: Queue.Variance +} + +/** + * Companion namespace containing type-level metadata and low-level state types + * for `Queue`. + * + * @since 2.0.0 + */ +export declare namespace Queue { + /** + * Type-level variance marker for `Queue`. + * + * **Details** + * + * A full `Queue` is invariant in both `A` and `E` because the same handle can + * both produce and consume values and failures. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + _A: Types.Invariant + _E: Types.Invariant + } + + /** + * Tagged state of a `Queue`. + * + * **Details** + * + * `Open` queues can accept offers and takers, `Closing` queues are + * completing with a stored failure exit, and `Done` queues have finished. + * This is low-level metadata exposed by the queue model; most users should + * inspect queues through the public operations. + * + * @category models + * @since 4.0.0 + */ + export type State = + | { + readonly _tag: "Open" + readonly takers: Set<(_: Effect) => void> + readonly offers: Set> + readonly awaiters: Set<(_: Effect) => void> + } + | { + readonly _tag: "Closing" + readonly takers: Set<(_: Effect) => void> + readonly offers: Set> + readonly awaiters: Set<(_: Effect) => void> + readonly exit: Failure + } + | { + readonly _tag: "Done" + readonly exit: Failure + } + + /** + * Represents a suspended offer waiting to be admitted to a bounded queue. + * + * **Details** + * + * An entry is either a single message or a batch with an offset into its + * remaining messages, plus a resume callback that completes the suspended + * offer when the queue can accept more input. + * + * @category models + * @since 4.0.0 + */ + export type OfferEntry = + | { + readonly _tag: "Array" + readonly remaining: Array + offset: number + readonly resume: (_: Effect>) => void + } + | { + readonly _tag: "Single" + readonly message: A + readonly resume: (_: Effect) => void + } +} + +const variance = { + _A: identity, + _E: identity +} +const QueueProto = { + [TypeId]: variance, + [EnqueueTypeId]: variance, + [DequeueTypeId]: variance, + ...PipeInspectableProto, + toJSON(this: Queue) { + return { + _id: "effect/Queue", + state: this.state._tag, + size: sizeUnsafe(this) + } + } +} + +/** + * Creates a `Queue` with optional capacity and overflow strategy. + * + * **Details** + * + * By default the queue is unbounded and uses the `"suspend"` strategy. Provide + * `capacity` for a bounded queue and choose `"suspend"`, `"dropping"`, or + * `"sliding"` to control what happens when the queue is full. The returned + * queue can be offered to, taken from, failed, ended, interrupted, or shut down. + * + * **Example** (Creating queues) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * Effect.gen(function*() { + * const queue = yield* Queue.make() + * + * // add messages to the queue + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * yield* Queue.offerAll(queue, [3, 4, 5]) + * + * // take messages from the queue + * const messages = yield* Queue.takeAll(queue) + * console.log(messages) // [1, 2, 3, 4, 5] + * + * // signal that the queue is done + * yield* Queue.end(queue) + * const done = yield* Effect.flip(Queue.take(queue)) + * console.log(Cause.isDone(done)) // true + * + * // signal that another queue has failed + * const failedQueue = yield* Queue.make() + * const failed = yield* Queue.fail(failedQueue, "boom") + * console.log(failed) // true + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + options?: { + readonly capacity?: number | undefined + readonly strategy?: "suspend" | "dropping" | "sliding" | undefined + } | undefined +): Effect> => + core.withFiber((fiber) => { + const self = Object.create(QueueProto) + self.dispatcher = fiber.currentDispatcher + self.capacity = options?.capacity ?? Number.POSITIVE_INFINITY + self.strategy = options?.strategy ?? "suspend" + self.messages = MutableList.make() + self.scheduleRunning = false + self.state = { + _tag: "Open", + takers: new Set(), + offers: new Set(), + awaiters: new Set() + } + return internalEffect.succeed(self) + }) + +/** + * Creates a bounded queue with the specified capacity that uses backpressure strategy. + * + * **Details** + * + * When the queue reaches capacity, producers will be suspended until space becomes available. + * This ensures all messages are processed but may slow down producers. + * + * **Example** (Creating bounded queues) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(5) + * + * // This will succeed as queue has capacity + * yield* Queue.offer(queue, "first") + * yield* Queue.offer(queue, "second") + * + * const size = yield* Queue.size(queue) + * console.log(size) // 2 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const bounded = (capacity: number): Effect> => make({ capacity }) + +/** + * Creates a bounded queue with sliding strategy. When the queue reaches capacity, + * new elements are added and the oldest elements are dropped. + * + * **When to use** + * + * Use when you need producer offers not to block and can accept dropping the + * oldest messages, such as when maintaining a rolling window of recent values. + * + * **Example** (Creating sliding queues) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.sliding(3) + * + * // Fill the queue to capacity + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * yield* Queue.offer(queue, 3) + * + * // This will succeed, dropping the oldest element (1) + * yield* Queue.offer(queue, 4) + * + * const all = yield* Queue.takeAll(queue) + * console.log(all) // [2, 3, 4] - oldest element (1) was dropped + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sliding = (capacity: number): Effect> => make({ capacity, strategy: "sliding" }) + +/** + * Creates a bounded queue with dropping strategy. When the queue reaches capacity, + * new elements are dropped and the offer operation returns false. + * + * **When to use** + * + * Use when you need producer offers not to block while preserving existing + * queued messages, even if new messages may be dropped when the queue is full. + * + * **Example** (Creating dropping queues) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.dropping(2) + * + * // Fill the queue to capacity + * const success1 = yield* Queue.offer(queue, 1) + * const success2 = yield* Queue.offer(queue, 2) + * console.log(success1, success2) // true, true + * + * // This will be dropped + * const success3 = yield* Queue.offer(queue, 3) + * console.log(success3) // false + * + * const all = yield* Queue.takeAll(queue) + * console.log(all) // [1, 2] - element 3 was dropped + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const dropping = (capacity: number): Effect> => + make({ capacity, strategy: "dropping" }) + +/** + * Creates an unbounded queue that can grow to any size without blocking producers. + * + * **When to use** + * + * Use when you need producers to add messages without backpressure and accept + * unbounded memory growth. + * + * **Example** (Creating unbounded queues) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * + * // Producers can always add messages without blocking + * yield* Queue.offer(queue, "message1") + * yield* Queue.offer(queue, "message2") + * yield* Queue.offerAll(queue, ["message3", "message4", "message5"]) + * + * // Check current size + * const size = yield* Queue.size(queue) + * console.log(size) // 5 + * + * // Take all messages + * const messages = yield* Queue.takeAll(queue) + * console.log(messages) // ["message1", "message2", "message3", "message4", "message5"] + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unbounded = (): Effect> => make() + +/** + * Adds a message to the queue. Returns `false` if the queue is done. + * + * **Details** + * + * For bounded queues, this operation may suspend if the queue is at capacity, + * depending on the backpressure strategy. For dropping/sliding queues, it may + * return false or succeed immediately by dropping/sliding existing messages. + * + * **Example** (Offering a value) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * // Successfully add messages to queue + * const success1 = yield* Queue.offer(queue, 1) + * const success2 = yield* Queue.offer(queue, 2) + * console.log(success1, success2) // true, true + * + * // Queue state + * const size = yield* Queue.size(queue) + * console.log(size) // 2 + * }) + * ``` + * + * @category Offering + * @since 2.0.0 + */ +export const offer = (self: Enqueue, message: Types.NoInfer): Effect => + internalEffect.suspend(() => { + if (self.state._tag !== "Open") { + return exitFalse + } else if (self.messages.length >= self.capacity) { + switch (self.strategy) { + case "dropping": + return exitFalse + case "suspend": + if (self.capacity <= 0 && self.state.takers.size > 0) { + MutableList.append(self.messages, message) + releaseTakers(self as Queue) + return exitTrue + } + return offerRemainingSingle(self as Queue, message) + case "sliding": + MutableList.take(self.messages) + MutableList.append(self.messages, message) + return exitTrue + } + } + MutableList.append(self.messages, message) + scheduleReleaseTaker(self as Queue) + return exitTrue + }) + +/** + * Adds a message to the queue synchronously. Returns `false` if the queue is done. + * + * **When to use** + * + * Use when you are already in synchronous queue internals or a performance + * boundary where wrapping the mutation in `Effect` is intentionally avoided. + * + * **Gotchas** + * + * This is an unsafe operation that directly modifies the queue without Effect wrapping. + * Use this only when you're certain about the synchronous nature of the operation. + * + * **Example** (Offering a value synchronously) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * // Create a queue effect and extract the queue for unsafe operations + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * // Add messages synchronously using unsafe API + * const success1 = Queue.offerUnsafe(queue, 1) + * const success2 = Queue.offerUnsafe(queue, 2) + * console.log(success1, success2) // true, true + * + * // Check current size + * const size = Queue.sizeUnsafe(queue) + * console.log(size) // 2 + * }) + * ``` + * + * @category Offering + * @since 4.0.0 + */ +export const offerUnsafe = (self: Enqueue, message: Types.NoInfer): boolean => { + if (self.state._tag !== "Open") { + return false + } else if (self.messages.length >= self.capacity) { + if (self.strategy === "sliding") { + MutableList.take(self.messages) + MutableList.append(self.messages, message) + return true + } else if (self.capacity <= 0 && self.state.takers.size > 0) { + MutableList.append(self.messages, message) + releaseTakers(self as Queue) + return true + } + return false + } + MutableList.append(self.messages, message) + scheduleReleaseTaker(self as Queue) + return true +} + +/** + * Adds multiple messages to the queue. Returns the remaining messages that + * were not added. + * + * **When to use** + * + * Use when producers can submit a batch at once and need to know which messages + * did not fit under the queue's capacity strategy. + * + * **Details** + * + * For bounded queues, this operation may suspend if the queue doesn't have + * enough capacity. The operation returns an array of messages that couldn't + * be added (empty array means all messages were successfully added). + * + * **Example** (Offering multiple values) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.dropping(3) + * + * // Try to add more messages than capacity without suspending + * const remaining1 = yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + * console.log(remaining1) // [4, 5] - couldn't fit the last 2 + * }) + * ``` + * + * @category Offering + * @since 2.0.0 + */ +export const offerAll = (self: Enqueue, messages: Iterable): Effect> => + internalEffect.suspend(() => { + if (self.state._tag !== "Open") { + return internalEffect.succeed(Arr.fromIterable(messages)) + } + const remaining = offerAllUnsafe(self as Queue, messages) + if (remaining.length === 0) { + return core.exitSucceed([]) + } else if (self.strategy === "dropping") { + return internalEffect.succeed(remaining) + } + return offerRemainingArray(self as Queue, remaining) + }) + +/** + * Adds multiple messages to the queue synchronously. Returns the remaining messages that + * were not added. + * + * **When to use** + * + * Use when queue internals or a performance boundary need a synchronous batch + * offer and can handle any messages that do not fit. + * + * **Gotchas** + * + * This is an unsafe operation that directly modifies the queue without Effect wrapping. + * + * **Example** (Offering multiple values synchronously) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * // Create a bounded queue and use unsafe API + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * // Try to add 5 messages to capacity-3 queue using unsafe API + * const remaining = Queue.offerAllUnsafe(queue, [1, 2, 3, 4, 5]) + * console.log(remaining) // [4, 5] - couldn't fit the last 2 + * + * // Check what's in the queue + * const size = Queue.sizeUnsafe(queue) + * console.log(size) // 3 + * }) + * ``` + * + * @category Offering + * @since 4.0.0 + */ +export const offerAllUnsafe = (self: Enqueue, messages: Iterable): Array => { + if (self.state._tag !== "Open") { + return Arr.fromIterable(messages) + } else if ( + self.capacity === Number.POSITIVE_INFINITY || + self.strategy === "sliding" + ) { + MutableList.appendAll(self.messages, messages) + if (self.strategy === "sliding") { + MutableList.takeN(self.messages, self.messages.length - self.capacity) + } + scheduleReleaseTaker(self as Queue) + return [] + } + const free = self.capacity <= 0 + ? self.state.takers.size + : self.capacity - self.messages.length + if (free === 0) { + return Arr.fromIterable(messages) + } + const remaining: Array = [] + let i = 0 + for (const message of messages) { + if (i < free) { + MutableList.append(self.messages, message) + } else { + remaining.push(message) + } + i++ + } + scheduleReleaseTaker(self as Queue) + return remaining +} + +/** + * Fails the queue with an error. If the queue is already done, `false` is + * returned. + * + * **Example** (Failing queues with an error) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Fail the queue with an error + * const failed = yield* Queue.fail(queue, "Something went wrong") + * console.log(failed) // true + * + * // Taking from the failed queue fails with the error + * const error = yield* Effect.flip(Queue.take(queue)) + * console.log(error) // "Something went wrong" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const fail = (self: Enqueue, error: E) => failCause(self, core.causeFail(error)) + +/** + * Fails the queue with a cause. If the queue is already done, `false` is + * returned. + * + * **Example** (Failing queues with a cause) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Create a cause and fail the queue + * const cause = Cause.fail("Queue processing failed") + * const failed = yield* Queue.failCause(queue, cause) + * console.log(failed) // true + * + * // The queue is now done with the specified failure cause + * console.log(queue.state._tag) // "Done" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const failCause: { + (cause: Cause): (self: Enqueue) => Effect + (self: Enqueue, cause: Cause): Effect +} = dual( + 2, + (self: Enqueue, cause: Cause): Effect => + internalEffect.sync(() => failCauseUnsafe(self, cause)) +) + +/** + * Fails the queue with a cause synchronously. If the queue is already done, `false` is + * returned. + * + * **When to use** + * + * Use when queue completion must be driven from synchronous internals while + * preserving the full failure `Cause`. + * + * **Gotchas** + * + * This is an unsafe operation that directly modifies the queue without Effect wrapping. + * + * **Example** (Failing queues with a cause synchronously) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Create a cause and fail the queue synchronously + * const cause = Cause.fail("Processing error") + * const failed = Queue.failCauseUnsafe(queue, cause) + * console.log(failed) // true + * + * // The queue is now done with the specified failure cause + * console.log(queue.state._tag) // "Done" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const failCauseUnsafe = (self: Enqueue, cause: Cause): boolean => { + if (self.state._tag !== "Open") { + return false + } + const exit = core.exitFailCause(cause) + const fail = internalEffect.exitZipRight(exit, exitFailDone) as Failure + if ( + self.state.offers.size === 0 && + self.messages.length === 0 + ) { + finalize(self, fail) + return true + } + self.state = { ...self.state, _tag: "Closing", exit: fail } + return true +} + +/** + * Signals queue completion. + * + * **When to use** + * + * Use to stop accepting new offers while allowing already queued messages to be + * consumed. + * + * **Details** + * + * Returns `false` if the queue is already done. + * + * **Example** (Ending queues) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Signal completion - no more messages will be accepted + * const ended = yield* Queue.end(queue) + * console.log(ended) // true + * + * // Trying to offer more messages will return false + * const offerResult = yield* Queue.offer(queue, 3) + * console.log(offerResult) // false + * + * // But we can still take existing messages + * const message = yield* Queue.take(queue) + * console.log(message) // 1 + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const end = (self: Enqueue): Effect => failCause(self, core.causeFail(core.Done())) + +/** + * Signals queue completion synchronously. + * + * **When to use** + * + * Use when implementing low-level queue integrations that must complete a queue + * without wrapping the operation in `Effect`. + * + * **Details** + * + * Returns `false` if the queue is already done. + * + * **Gotchas** + * + * This is an unsafe operation that directly modifies the queue without Effect wrapping. + * + * **Example** (Ending queues synchronously) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * // Create a queue and use unsafe operations + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * Queue.offerUnsafe(queue, 1) + * Queue.offerUnsafe(queue, 2) + * + * // End the queue synchronously + * const ended = Queue.endUnsafe(queue) + * console.log(ended) // true + * + * // Existing messages can still be consumed while the queue is closing + * console.log(queue.state._tag) // "Closing" + * + * Queue.takeUnsafe(queue) + * Queue.takeUnsafe(queue) + * + * // After buffered messages are consumed, the queue is done + * console.log(queue.state._tag) // "Done" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const endUnsafe = (self: Enqueue) => failCauseUnsafe(self, core.causeFail(core.Done())) + +/** + * Interrupts the queue gracefully, transitioning it to a closing state. + * + * **Details** + * + * This operation stops accepting new offers but allows existing messages to be consumed. + * Once all messages are drained, the queue transitions to the Done state with an interrupt cause. + * + * **Example** (Interrupting queues gracefully) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Interrupt gracefully - no more offers accepted, but messages can be consumed + * const interrupted = yield* Queue.interrupt(queue) + * console.log(interrupted) // true + * + * // Trying to offer more messages will return false + * const offerResult = yield* Queue.offer(queue, 3) + * console.log(offerResult) // false + * + * // But we can still take existing messages + * const message1 = yield* Queue.take(queue) + * console.log(message1) // 1 + * + * const message2 = yield* Queue.take(queue) + * console.log(message2) // 2 + * + * // After all messages are consumed, queue is done + * const isDone = queue.state._tag === "Done" + * console.log(isDone) // true + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const interrupt = (self: Enqueue): Effect => + core.withFiber((fiber) => failCause(self, internalEffect.causeInterrupt(fiber.id))) + +/** + * Shuts down the queue immediately, discarding buffered messages and resuming + * pending operations. + * + * **Details** + * + * The operation is idempotent and returns `true`, including when the queue has + * already been shut down or completed. + * + * **Example** (Shutting down queues) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(2) + * + * // Add messages + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Shutdown clears buffered messages and prevents further offers + * const wasShutdown = yield* Queue.shutdown(queue) + * console.log(wasShutdown) // true + * + * // Queue is now done and cleared + * const size = yield* Queue.size(queue) + * console.log(size) // 0 + * }) + * ``` + * + * @category completion + * @since 2.0.0 + */ +export const shutdown = (self: Enqueue): Effect => + internalEffect.sync(() => { + if (self.state._tag === "Done") { + return true + } + MutableList.clear(self.messages) + const offers = self.state.offers + finalize(self, self.state._tag === "Open" ? exitInterrupt : self.state.exit) + if (offers.size > 0) { + for (const entry of offers) { + if (entry._tag === "Single") { + entry.resume(exitFalse) + } else { + entry.resume(core.exitSucceed(entry.remaining.slice(entry.offset))) + } + } + offers.clear() + } + return true + }) + +/** + * Takes and returns all currently buffered messages without waiting for more. + * + * **Details** + * + * Returns an empty array when the queue is empty or has completed normally. If + * the queue has failed, the effect fails with the queue's error. + * + * **Example** (Clearing queued values) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add several messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * // Clear all messages from the queue + * const messages = yield* Queue.clear(queue) + * console.log(messages) // [1, 2, 3, 4, 5] + * + * // Queue is now empty + * const size = yield* Queue.size(queue) + * console.log(size) // 0 + * + * // Clearing empty queue returns empty array + * const empty = yield* Queue.clear(queue) + * console.log(empty) // [] + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const clear = (self: Dequeue): Effect, Pull.ExcludeDone> => + internalEffect.suspend(() => { + if (self.state._tag === "Done") { + if (Pull.isDoneCause(self.state.exit.cause)) { + return internalEffect.succeed([]) + } + return self.state.exit + } + const messages = takeAllUnsafe(self) + releaseCapacity(self) + return internalEffect.succeed(messages) + }) + +/** + * Takes all currently available messages, waiting until at least one message + * is available when the queue is empty. + * + * **When to use** + * + * Use when consumers should process the next non-empty batch of buffered + * messages instead of repeatedly taking one message at a time. + * + * **Details** + * + * Returns a non-empty array. If the queue completes or fails before a message + * can be taken, the effect fails with the queue's terminal error. + * + * **Example** (Taking all available values) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(5) + * + * // Add several messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * // Take all available messages + * const messages1 = yield* Queue.takeAll(queue) + * console.log(messages1) // [1, 2, 3, 4, 5] + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const takeAll = (self: Dequeue): Effect, E> => + takeBetween(self, 1, Number.POSITIVE_INFINITY) as any + +/** + * Takes all messages from the queue, until the queue has errored or is done. + * + * **Example** (Collecting values until completion) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(5) + * + * // Add several messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + * // Some time later, end the queue + * yield* Effect.forkChild(Queue.end(queue)) + * + * // Collect all available messages + * const messages = yield* Queue.collect(queue) + * console.log(messages) // [1, 2, 3, 4, 5] + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const collect = (self: Dequeue): Effect, Pull.ExcludeDone> => + internalEffect.suspend(() => { + const out = Arr.empty() + return internalEffect.as( + Pull.catchDone( + internalEffect.whileLoop({ + while: constTrue, + body: constant(takeAll(self)), + step(items: Arr.NonEmptyArray) { + for (let i = 0; i < items.length; i++) { + out.push(items[i]) + } + } + }), + () => internalEffect.void + ), + out + ) + }) as any + +/** + * Takes up to `n` messages from the queue. + * + * **Details** + * + * The operation may wait until enough messages are available to satisfy the + * queue's batching rules. If `n` is less than or equal to zero, it succeeds + * with an empty array. If the queue completes or fails before messages can be + * taken, the effect fails with the queue's terminal error. + * + * **Example** (Taking a fixed number of values) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add several messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7]) + * + * // Take exactly 3 messages + * const first3 = yield* Queue.takeN(queue, 3) + * console.log(first3) // [1, 2, 3] + * + * // Take exactly 2 more messages + * const next2 = yield* Queue.takeN(queue, 2) + * console.log(next2) // [4, 5] + * + * // Take remaining messages + * const remaining = yield* Queue.takeN(queue, 2) + * console.log(remaining) // [6, 7] + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const takeN = ( + self: Dequeue, + n: number +): Effect, E> => takeBetween(self, n, n) + +/** + * Takes between `min` and `max` messages from the queue. + * + * **Details** + * + * The operation waits when fewer than the required minimum messages are + * available. It returns at most `max` messages. If the queue completes or fails + * before the minimum can be satisfied, the effect fails with the queue's + * terminal error. + * + * **Example** (Taking a bounded batch of values) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add several messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7, 8]) + * + * // Take between 2 and 5 messages + * const batch1 = yield* Queue.takeBetween(queue, 2, 5) + * console.log(batch1) // [1, 2, 3, 4, 5] - took 5 (up to max) + * + * // Take between 1 and 10 messages (but only 3 remain) + * const batch2 = yield* Queue.takeBetween(queue, 1, 10) + * console.log(batch2) // [6, 7, 8] - took 3 (all remaining) + * + * // No more messages available, will wait or return done + * // const batch3 = yield* Queue.takeBetween(queue, 1, 3) + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const takeBetween = ( + self: Dequeue, + min: number, + max: number +): Effect, E> => + internalEffect.suspend(() => + takeBetweenUnsafe(self, min, max) ?? internalEffect.andThen(awaitTake(self), takeBetween(self, 1, max)) + ) + +/** + * Takes a single message from the queue, or wait for a message to be + * available. + * + * **Details** + * + * If the queue is done, it will fail with `Done`. If the + * queue fails, the Effect will fail with the error. + * + * **Example** (Taking one value) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * // Add some messages + * yield* Queue.offer(queue, "first") + * yield* Queue.offer(queue, "second") + * + * // Take messages one by one + * const msg1 = yield* Queue.take(queue) + * const msg2 = yield* Queue.take(queue) + * console.log(msg1, msg2) // "first", "second" + * + * // End the queue + * yield* Queue.end(queue) + * + * // Taking from an ended queue fails with Done + * const result = yield* Effect.match(Queue.take(queue), { + * onFailure: (error: Cause.Done) => true, + * onSuccess: (value: string) => false + * }) + * console.log("Queue ended:", result) // true + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const take = (self: Dequeue): Effect => + internalEffect.suspend( + () => takeUnsafe(self) ?? internalEffect.andThen(awaitTake(self), take(self)) + ) + +/** + * Attempts to take one item from the queue without waiting. + * + * **Details** + * + * Returns `Option.some` when an item is immediately available. Returns + * `Option.none` when no item is available, when the queue is done, or when the + * immediate take observes a queue failure. + * + * **Example** (Polling without blocking) + * + * ```ts + * import { Effect, Option, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Poll returns Option.none if empty + * const maybe1 = yield* Queue.poll(queue) + * console.log(Option.isNone(maybe1)) // true + * + * // Add an item + * yield* Queue.offer(queue, 42) + * + * // Poll returns Option.some with the item + * const maybe2 = yield* Queue.poll(queue) + * console.log(Option.getOrNull(maybe2)) // 42 + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const poll = (self: Dequeue): Effect> => + internalEffect.suspend(() => { + const result = takeUnsafe(self) + if (result === undefined) { + return internalEffect.succeed(Option.none()) + } + if (result._tag === "Success") { + return internalEffect.succeed(Option.some(result.value)) + } + return internalEffect.succeed(Option.none()) + }) + +/** + * Peeks at the next item without removing it. + * + * **Details** + * + * Blocks until an item is available. If the queue is done or fails, the error is propagated. + * + * **Example** (Peeking at the next value) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * yield* Queue.offer(queue, 42) + * + * // Peek at the next item without removing it + * const item = yield* Queue.peek(queue) + * console.log(item) // 42 + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const peek = (self: Dequeue): Effect => + internalEffect.suspend(() => { + if (self.state._tag === "Done") { + return self.state.exit + } + if (self.messages.length > 0 && self.messages.head) { + return internalEffect.succeed(self.messages.head.array[self.messages.head.offset]) + } + return internalEffect.andThen(awaitTake(self), peek(self)) + }) + +/** + * Attempts to take one message from the queue synchronously. + * + * **When to use** + * + * Use when polling queue internals must not suspend or register a waiting taker, + * and `undefined` is an acceptable result for an empty queue. + * + * **Details** + * + * Returns an `Exit` for an immediately available message or for the queue's + * terminal state. Returns `undefined` when no message is immediately available. + * This operation does not wait or register a taker. + * + * **Example** (Taking one value synchronously) + * + * ```ts + * import { Effect, Queue } from "effect" + * + * // Create a queue and use unsafe operations + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * Queue.offerUnsafe(queue, 1) + * Queue.offerUnsafe(queue, 2) + * + * // Take a message synchronously + * const result1 = Queue.takeUnsafe(queue) + * console.log(result1) // Success(1) or Exit containing value 1 + * + * const result2 = Queue.takeUnsafe(queue) + * console.log(result2) // Success(2) + * + * // No more messages - returns undefined + * const result3 = Queue.takeUnsafe(queue) + * console.log(result3) // undefined + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const takeUnsafe = (self: Dequeue): Exit | undefined => { + if (self.state._tag === "Done") { + return self.state.exit + } + if (self.messages.length > 0) { + const message = MutableList.take(self.messages)! + releaseCapacity(self) + return core.exitSucceed(message) + } else if (self.capacity <= 0 && self.state.offers.size > 0) { + self.capacity = 1 + releaseCapacity(self) + self.capacity = 0 + const message = MutableList.take(self.messages)! + releaseCapacity(self) + return core.exitSucceed(message) + } + return undefined +} + +const await_ = (self: Dequeue): Effect> => + internalEffect.callback>((resume) => { + if (self.state._tag === "Done") { + if (Pull.isDoneCause(self.state.exit.cause)) { + return resume(internalEffect.exitVoid) + } + return resume(self.state.exit) + } + self.state.awaiters.add(resume) + return internalEffect.sync(() => { + if (self.state._tag !== "Done") { + self.state.awaiters.delete(resume) + } + }) + }) + +export { + /** + * Waits until a queue reaches the `Done` state. + * + * **When to use** + * + * Use to suspend a fiber until no further values can be taken from the queue + * and its terminal outcome is known. + * + * **Details** + * + * The effect succeeds with `void` for normal `Done` completion. Other + * terminal causes are preserved, so failures and interruptions complete this + * effect with the same terminal outcome. + * + * **Gotchas** + * + * A queue can be closing before it is done. `await` resumes at `Done`, not at + * the first completion signal, so buffered messages may need to be drained + * first. + * + * @see {@link end} for signaling normal completion while preserving buffered messages for consumers + * @see {@link fail} for signaling an error while preserving buffered messages for consumers + * @see {@link interrupt} for graceful interruption after buffered messages are drained + * @see {@link shutdown} for immediately discarding buffered messages and resuming pending operations + * + * @category completion + * @since 4.0.0 + */ + await_ as await +} + +/** + * Returns the current number of buffered messages in the queue. + * + * **Details** + * + * Completed queues report a size of `0`. + * + * **Example** (Checking queue size) + * + * ```ts + * import { Cause, Effect, Option, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Check size of empty queue + * const size1 = yield* Queue.size(queue) + * console.log(size1) // 0 + * + * // Add some messages + * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * // Check size after adding messages + * const size2 = yield* Queue.size(queue) + * console.log(size2) // 5 + * + * // End the queue + * yield* Queue.end(queue) + * + * // Size of ended queue is 0 + * const size3 = yield* Queue.size(queue) + * console.log(size3) // 0 + * }) + * ``` + * + * @category sizes + * @since 2.0.0 + */ +export const size = (self: Dequeue): Effect => internalEffect.sync(() => sizeUnsafe(self)) + +/** + * Checks whether the queue is full. + * + * **Example** (Checking if queues are full) + * + * ```ts + * import { Cause, Effect, Option, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * console.log(yield* Queue.isFull(queue)) // false + * + * // Add some messages + * yield* Queue.offerAll(queue, [1, 2, 3]) + * + * console.log(yield* Queue.isFull(queue)) // true + * }) + * ``` + * + * @category sizes + * @since 2.0.0 + */ +export const isFull = (self: Dequeue): Effect => internalEffect.sync(() => isFullUnsafe(self)) + +/** + * Returns the current number of buffered messages in the queue synchronously. + * + * **When to use** + * + * Use when you need an immediate `Queue` size snapshot for diagnostics or + * internals and do not need the read wrapped in `Effect`. + * + * **Details** + * + * Completed queues report a size of `0`. This unsafe operation reads the queue + * state directly without Effect wrapping. + * + * **Example** (Checking queue size synchronously) + * + * ```ts + * import { Cause, Effect, Option, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Check size of empty queue + * const size1 = Queue.sizeUnsafe(queue) + * console.log(size1) // 0 + * + * // Add some messages + * Queue.offerUnsafe(queue, 1) + * Queue.offerUnsafe(queue, 2) + * Queue.offerUnsafe(queue, 3) + * + * // Check size after adding messages + * const size2 = Queue.sizeUnsafe(queue) + * console.log(size2) // 3 + * + * // End the queue + * Queue.endUnsafe(queue) + * + * // Size of ended queue is 0 + * const size3 = Queue.sizeUnsafe(queue) + * console.log(size3) // 0 + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const sizeUnsafe = (self: Dequeue): number => self.state._tag === "Done" ? 0 : self.messages.length + +/** + * Checks whether the queue is full synchronously. + * + * **When to use** + * + * Use when an immediate `Queue` capacity snapshot is needed outside effectful + * code and racing queue changes are acceptable. + * + * **Example** (Checking fullness synchronously) + * + * ```ts + * import { Cause, Effect, Option, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(3) + * + * console.log(Queue.isFullUnsafe(queue)) // false + * + * // Add some messages + * yield* Queue.offerAll(queue, [1, 2, 3]) + * + * console.log(Queue.isFullUnsafe(queue)) // true + * }) + * ``` + * + * @category sizes + * @since 4.0.0 + */ +export const isFullUnsafe = (self: Dequeue): boolean => sizeUnsafe(self) === self.capacity + +/** + * Runs an `Effect` into a `Queue`, where success ends the queue and failure + * fails the queue. + * + * **Example** (Running effects into queues) + * + * ```ts + * import { Cause, Effect, Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Create an effect that succeeds + * const dataProcessing = Effect.gen(function*() { + * yield* Effect.sleep("100 millis") + * return "Processing completed successfully" + * }) + * + * // Pipe the effect into the queue + * // If dataProcessing succeeds, queue ends successfully + * // If dataProcessing fails, queue fails with the error + * const effectIntoQueue = Queue.into(queue)(dataProcessing) + * + * const wasCompleted = yield* effectIntoQueue + * console.log("Queue operation completed:", wasCompleted) // true + * + * // Queue state now reflects the effect's outcome + * console.log("Queue state:", queue.state._tag) // "Done" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const into: { + ( + self: Enqueue + ): ( + effect: Effect + ) => Effect + ( + effect: Effect, + self: Enqueue + ): Effect +} = dual( + 2, + ( + effect: Effect, + self: Enqueue + ): Effect => + internalEffect.uninterruptibleMask((restore) => + internalEffect.matchCauseEffect(restore(effect), { + onFailure: (cause) => failCause(self, cause), + onSuccess: (_) => end(self) + }) + ) +) + +// ----------------------------------------------------------------------------- +// internals +// ----------------------------------------------------------------------------- +// + +const exitFalse = core.exitSucceed(false) +const exitTrue = core.exitSucceed(true) +const exitFailDone = core.exitFail(core.Done()) as Failure +const exitInterrupt = internalEffect.exitInterrupt() as Failure + +const releaseTakers = (self: Enqueue) => { + self.scheduleRunning = false + if (self.state._tag === "Done" || self.state.takers.size === 0) { + return + } + for (const taker of self.state.takers) { + self.state.takers.delete(taker) + taker(internalEffect.exitVoid) + if (self.messages.length === 0) { + break + } + } +} + +const scheduleReleaseTaker = (self: Enqueue) => { + if (self.scheduleRunning || self.state._tag === "Done" || self.state.takers.size === 0) { + return + } + self.scheduleRunning = true + self.dispatcher.scheduleTask(() => releaseTakers(self), 0) +} + +const takeBetweenUnsafe = ( + self: Dequeue, + min: number, + max: number +): Exit, E> | undefined => { + if (self.state._tag === "Done") { + return self.state.exit + } else if (max <= 0 || min <= 0) { + return core.exitSucceed([]) + } else if (self.capacity <= 0 && self.state.offers.size > 0) { + self.capacity = 1 + releaseCapacity(self) + self.capacity = 0 + const messages = [MutableList.take(self.messages)!] + releaseCapacity(self) + return core.exitSucceed(messages) + } + min = Math.min(min, self.capacity || 1) + if (min <= self.messages.length) { + const messages = MutableList.takeN(self.messages, max) + releaseCapacity(self) + return core.exitSucceed(messages) + } +} + +const offerRemainingSingle = (self: Enqueue, message: A) => { + return internalEffect.callback((resume) => { + if (self.state._tag !== "Open") { + return resume(exitFalse) + } + const entry: Queue.OfferEntry = { _tag: "Single", message, resume } + self.state.offers.add(entry) + return internalEffect.sync(() => { + if (self.state._tag === "Open") { + self.state.offers.delete(entry) + } + }) + }) +} + +const offerRemainingArray = (self: Enqueue, remaining: Array) => { + return internalEffect.callback>((resume) => { + if (self.state._tag !== "Open") { + return resume(core.exitSucceed(remaining)) + } + const entry: Queue.OfferEntry = { + _tag: "Array", + remaining, + offset: 0, + resume + } + self.state.offers.add(entry) + return internalEffect.sync(() => { + if (self.state._tag === "Open") { + self.state.offers.delete(entry) + } + }) + }) +} + +const releaseCapacity = (self: Dequeue): boolean => { + if (self.state._tag === "Done") { + return Pull.isDoneCause(self.state.exit.cause) + } else if (self.state.offers.size === 0) { + if ( + self.state._tag === "Closing" && + self.messages.length === 0 + ) { + finalize(self, self.state.exit) + return Pull.isDoneCause(self.state.exit.cause) + } + return false + } + let n = self.capacity - self.messages.length + for (const entry of self.state.offers) { + if (n === 0) break + else if (entry._tag === "Single") { + MutableList.append(self.messages, entry.message) + n-- + entry.resume(exitTrue) + self.state.offers.delete(entry) + } else { + for (; entry.offset < entry.remaining.length; entry.offset++) { + if (n === 0) return false + MutableList.append(self.messages, entry.remaining[entry.offset]) + n-- + } + entry.resume(core.exitSucceed([])) + self.state.offers.delete(entry) + } + } + return false +} + +const awaitTake = (self: Dequeue) => + internalEffect.callback((resume) => { + if (self.state._tag === "Done") { + return resume(self.state.exit) + } + self.state.takers.add(resume) + return internalEffect.sync(() => { + if (self.state._tag !== "Done") { + self.state.takers.delete(resume) + } + }) + }) + +const takeAllUnsafe = (self: Dequeue) => { + if (self.messages.length > 0) { + const messages = MutableList.takeAll(self.messages) + releaseCapacity(self) + return messages + } else if (self.state._tag !== "Done" && self.state.offers.size > 0) { + self.capacity = 1 + releaseCapacity(self) + self.capacity = 0 + const messages = [MutableList.take(self.messages)!] + releaseCapacity(self) + return messages + } + return [] +} + +const finalize = (self: Enqueue | Dequeue, exit: Failure) => { + if (self.state._tag === "Done") { + return + } + const openState = self.state + self.state = { _tag: "Done", exit } + for (const taker of openState.takers) { + taker(exit) + } + openState.takers.clear() + for (const awaiter of openState.awaiters) { + awaiter(exit) + } + openState.awaiters.clear() +} diff --git a/repos/effect/packages/effect/src/Random.ts b/repos/effect/packages/effect/src/Random.ts new file mode 100644 index 0000000..53078f1 --- /dev/null +++ b/repos/effect/packages/effect/src/Random.ts @@ -0,0 +1,591 @@ +/** + * Provides pseudo-random generation through an Effect service. + * + * This module exposes effectful generators for booleans, doubles, safe + * integers, bounded numbers, shuffling, and deterministic seeded runs. Because + * random generation is a service, tests and applications can replace the + * generator used by Effect programs. + * + * @since 4.0.0 + */ +import type * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import type * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import * as random from "./internal/random.ts" +import type * as NonEmptyIterable from "./NonEmptyIterable.ts" +import * as Predicate from "./Predicate.ts" + +/** + * Represents a service for generating pseudo-random numbers. + * + * **When to use** + * + * Use to access or provide the random-number generator service used by Effect + * programs. + * + * **Gotchas** + * + * The default implementation is based on `Math.random` and is not + * cryptographically secure. Replace the service with a cryptographically secure + * implementation before using these generators for security-sensitive values. + * + * **Example** (Accessing the random service) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const float = yield* Random.next + * const integer = yield* Random.nextInt + * const inRange = yield* Random.nextIntBetween(1, 100) + * + * console.log("Float:", float) + * console.log("Integer:", integer) + * console.log("In range:", inRange) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const Random: Context.Reference<{ + nextIntUnsafe(): number + nextDoubleUnsafe(): number +}> = random.Random + +const randomWith = (f: (random: typeof Random["Service"]) => A): Effect.Effect => + Effect.withFiber((fiber) => Effect.succeed(f(fiber.getRef(Random)))) + +/** + * Generates a random number between 0 (inclusive) and 1 (exclusive). + * + * **When to use** + * + * Use to generate a pseudo-random floating-point number in the standard + * `[0, 1)` range. + * + * **Example** (Generating a random number) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const randomDouble = yield* Random.next + * console.log("Random double:", randomDouble) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const next: Effect.Effect = randomWith((r) => r.nextDoubleUnsafe()) + +/** + * Generates a random boolean value. + * + * **When to use** + * + * Use to make a pseudo-random true-or-false choice. + * + * **Example** (Generating a random boolean) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const value = yield* Random.nextBoolean + * console.log("Random boolean:", value) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const nextBoolean: Effect.Effect = randomWith((r) => r.nextDoubleUnsafe() > 0.5) + +/** + * Generates a random integer between `Number.MIN_SAFE_INTEGER` (inclusive) + * and `Number.MAX_SAFE_INTEGER` (inclusive). + * + * **When to use** + * + * Use to generate a pseudo-random safe integer across the full safe-integer + * range. + * + * **Example** (Generating a random integer) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const randomInt = yield* Random.nextInt + * console.log("Random integer:", randomInt) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe()) + +/** + * Generates a random number between `min` (inclusive) and `max` (exclusive). + * + * **When to use** + * + * Use to generate a pseudo-random floating-point number within a numeric range. + * + * **Example** (Generating a bounded random number) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const randomDouble = yield* Random.nextBetween(0, 1) + * console.log("Random double: ", randomDouble) + * }) + * ``` + * + * @category Random Number Generators + * @since 4.0.0 + */ +export const nextBetween = (min: number, max: number): Effect.Effect => + randomWith((r) => r.nextDoubleUnsafe() * (max - min) + min) + +/** + * Generates a random integer between `min` and `max`. + * + * **When to use** + * + * Use to generate a pseudo-random integer within a rounded numeric range. + * + * **Details** + * + * The lower bound is rounded up with `Math.ceil` and the upper bound is + * rounded down with `Math.floor`. By default the range is inclusive; set + * `options.halfOpen: true` to exclude the upper bound. + * + * **Example** (Generating a bounded random integer) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const diceRoll1 = yield* Random.nextIntBetween(1, 6) + * const diceRoll2 = yield* Random.nextIntBetween(1, 6, { + * halfOpen: true + * }) + * const diceRoll3 = yield* Random.nextIntBetween(0, 10) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const nextIntBetween = (min: number, max: number, options?: { + readonly halfOpen?: boolean +}): Effect.Effect => { + const extra = options?.halfOpen === true ? 0 : 1 + return randomWith((r) => { + const minInt = Math.ceil(min) + const maxInt = Math.floor(max) + return Math.floor(r.nextDoubleUnsafe() * (maxInt - minInt + extra)) + minInt + }) +} + +/** + * Uses the pseudo-random number generator to shuffle the specified iterable. + * + * **When to use** + * + * Use to randomly reorder an iterable using the active `Random` service. + * + * **Example** (Shuffling values) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Random.shuffle([1, 2, 3, 4, 5]) + * console.log(values) + * }) + * ``` + * + * @category Random Number Generators + * @since 2.0.0 + */ +export const shuffle = (elements: Iterable): Effect.Effect> => + randomWith((r) => { + const buffer = Array.from(elements) + for (let i = buffer.length - 1; i >= 1; i = i - 1) { + const index = Math.min(i, Math.floor(r.nextDoubleUnsafe() * (i + 1))) + const value = buffer[i]! + buffer[i] = buffer[index]! + buffer[index] = value + } + return buffer + }) + +/** + * Gets a random element from an iterable. + * + * **When to use** + * + * Use to select one value uniformly from a collection using the active `Random` + * service. + * + * **Details** + * + * If the input type is known to be non-empty, the returned effect cannot fail. + * Otherwise, empty iterables fail with `Cause.NoSuchElementError`. + * + * **Example** (Choosing a random value) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const value = yield* Random.choice(["red", "green", "blue"] as const) + * console.log(value) + * }) + * ``` + * + * @category Random Number Generators + * @since 3.6.0 + */ +export const choice: >( + elements: Self +) => Self extends NonEmptyIterable.NonEmptyIterable ? Effect.Effect + : Self extends Arr.NonEmptyReadonlyArray ? Effect.Effect + : Self extends Iterable ? Effect.Effect + : never = ((elements: Iterable) => { + const buffer = Array.from(elements) + return buffer.length === 0 + ? Effect.fail(new Cause.NoSuchElementError("Cannot select a random element from an empty array")) + : randomWith((r) => buffer[Math.min(buffer.length - 1, Math.floor(r.nextDoubleUnsafe() * buffer.length))]!) + }) as any + +/** + * Seeds the pseudo-random number generator with the specified value. + * + * **When to use** + * + * Use to run an effect with a deterministic pseudo-random sequence. + * + * **Details** + * + * Using the same seed produces the same random sequence, which is useful for + * tests and reproducible simulations. + * + * **Gotchas** + * + * Use an unpredictable seed when uniqueness or unpredictability matters. + * + * **Example** (Seeding random generation) + * + * ```ts + * import { Effect, Random } from "effect" + * + * const program = Effect.gen(function*() { + * const value1 = yield* Random.next + * const value2 = yield* Random.next + * console.log(value1, value2) + * }) + * + * // Same seed produces same sequence + * const seeded1 = program.pipe(Random.withSeed("my-seed")) + * const seeded2 = program.pipe(Random.withSeed("my-seed")) + * + * // Both will output identical values + * Effect.runPromise(seeded1) + * Effect.runPromise(seeded2) + * ``` + * + * @category Seeding + * @since 4.0.0 + */ +export const withSeed: { + (seed: string | number): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, seed: string | number): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + seed: string | number +) => Effect.provideService(self, Random, ISAAC_CSPRNG(seed))) + +/*/////////////////////////////////////////////////////////////////////////////////////////////////// +This is a derivative work copyright (c) 2025 Effectful Technologies Inc, under MIT license. +This is a derivative work copyright (c) 2018, William P. "Mac" McMeans, under BSD license. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of isaacCSPRNG nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Original work copyright (c) 2012 Yves-Marie K. Rinquin, under MIT license. +https://github.com/rubycon/isaac.js +///////////////////////////////////////////////////////////////////////////////////////////////////*/ +function ISAAC_CSPRNG(userSeed?: string | number) { + // Internal State + const memory = new Array(256) + const result = new Array(256) + let accumulator = 0 + let lastResult = 0 + let generation = 0 + let counter = 0 + + // Initial Seed + const internalSeed = Predicate.isUndefined(userSeed) ? getInitialSeed() : userSeed + seed(internalSeed) + + function getInitialSeed() { + const uint32a = new Uint32Array(2) + crypto.getRandomValues(uint32a) + return uint32a[0] + uint32a[1] + } + + function reset() { + accumulator = 0 + lastResult = 0 + counter = 0 + for (let i = 0; i < 256; ++i) { + memory[i] = 0 + result[i] = 0 + } + generation = 0 + } + + function seed(userSeed: string | number): void { + // The golden ratio ( 2654435769 ) + // See https://stackoverflow.com/questions/4948780/magic-number-in-boosthash-combine + const magicNumber = 0x9e3779b9 + let a = magicNumber + let b = magicNumber + let c = magicNumber + let d = magicNumber + let e = magicNumber + let f = magicNumber + let g = magicNumber + let h = magicNumber + let i = 0 + + let seed: Array + if (Predicate.isString(userSeed)) { + seed = toIntArray(userSeed) + } else { + seed = [userSeed] + } + + reset() + + for (i = 0; i < seed.length; i++) { + result[i & 0xff] += seed[i] + } + + function mix() { + a ^= b << 11 + d = add32(d, a) + b = add32(b, c) + + b ^= c >>> 2 + e = add32(e, b) + c = add32(c, d) + + c ^= d << 8 + f = add32(f, c) + d = add32(d, e) + + d ^= e >>> 16 + g = add32(g, d) + e = add32(e, f) + + e ^= f << 10 + h = add32(h, e) + f = add32(f, g) + + f ^= g >>> 4 + a = add32(a, f) + g = add32(g, h) + + g ^= h << 8 + b = add32(b, g) + h = add32(h, a) + + h ^= a >>> 9 + c = add32(c, h) + a = add32(a, b) + } + + // Scramble the seed + for (i = 0; i < 4; i++) { + mix() + } + + for (i = 0; i < 256; i += 8) { + // Use all the information in the seed + a = add32(a, result[i]) + b = add32(b, result[i + 1]) + c = add32(c, result[i + 2]) + d = add32(d, result[i + 3]) + e = add32(e, result[i + 4]) + f = add32(f, result[i + 5]) + g = add32(g, result[i + 6]) + h = add32(h, result[i + 7]) + + mix() + + // Fill in the memory with messy stuff + memory[i] = a + memory[i + 1] = b + memory[i + 2] = c + memory[i + 3] = d + memory[i + 4] = e + memory[i + 5] = f + memory[i + 6] = g + memory[i + 7] = h + } + + // Second pass to make sure seed affects memory + for (i = 0; i < 256; i += 8) { + a = add32(a, memory[i]) + b = add32(b, memory[i + 1]) + c = add32(c, memory[i + 2]) + d = add32(d, memory[i + 3]) + e = add32(e, memory[i + 4]) + f = add32(f, memory[i + 5]) + g = add32(g, memory[i + 6]) + h = add32(h, memory[i + 7]) + + mix() + + // Fill in the memory with messy stuff (again) + memory[i] = a + memory[i + 1] = b + memory[i + 2] = c + memory[i + 3] = d + memory[i + 4] = e + memory[i + 5] = f + memory[i + 6] = g + memory[i + 7] = h + } + + pnrg() + + generation = 256 + } + + function pnrg(n?: number): void { + let i = 0 + let x = 0 + let y = 0 + + n = Predicate.isUndefined(n) ? 1 : Math.abs(Math.floor(n)) + + while (n--) { + counter = add32(counter, 1) + lastResult = add32(lastResult, counter) + + for (i = 0; i < 256; i++) { + switch (i & 3) { + case 0: { + accumulator ^= accumulator << 13 + break + } + case 1: { + accumulator ^= accumulator >>> 6 + break + } + case 2: { + accumulator ^= accumulator << 2 + break + } + case 3: { + accumulator ^= accumulator >>> 16 + break + } + } + + accumulator = add32(memory[(i + 128) & 0xff], accumulator) + x = memory[i] + + memory[i] = add32(memory[(x >>> 2) & 0xff], add32(accumulator, lastResult)) + y = memory[i] + + result[i] = add32(memory[(y >>> 10) & 0xff], x) + lastResult = result[i] + } + } + } + + /** + * Returns a signed, random integer in the range [-2^31, 2^31). + */ + function nextInt32(): number { + if (!generation--) { + pnrg() + generation = 255 + } + return result[generation] + } + + function nextIntUnsafe(): number { + return Math.floor(nextDoubleUnsafe() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER + 1)) + + Number.MIN_SAFE_INTEGER + } + + /** + * Returns a 53-bit fraction in the range [0, 1). + */ + function nextDoubleUnsafe(): number { + const hi = (nextInt32() >>> 0) & 0x1FFFFF // take top 21 bits + const lo = nextInt32() >>> 0 // full 32 bits + + // 53-bit integer + const combined = hi * 4294967296 + lo + + return combined / 0x20000000000000 // [0, 1) + } + + return { nextIntUnsafe, nextDoubleUnsafe } +} + +/** + * 32-bit addition with overflow handling (JavaScript numbers are 53-bit). + * + * Example: add32(0xFFFFFFFF, 0x00000001) = 0x00000000 (wraps around) + */ +function add32(x: number, y: number): number { + // Add lower 16 bits separately to handle carry + // Example: x=0x12345678, y=0xABCDEF01 + // lsb = (0x5678 + 0xEF01) = 0x14579 + const lsb = (x & 0xffff) + (y & 0xffff) + + // Add upper 16 bits + carry from lower addition + // msb = (0x1234 + 0xABCD + (0x14579 >>> 16)) = (0x1234 + 0xABCD + 0x1) = 0xBE02 + const msb = (x >>> 16) + (y >>> 16) + (lsb >>> 16) + + // Combine: upper 16 bits | lower 16 bits (masked to prevent double carry) + // return (0xBE02 << 16) | (0x14579 & 0xffff) = 0xBE024579 + return (msb << 16) | (lsb & 0xffff) +} + +const seedEncoder = new TextEncoder() + +/** + * Convert a string to UTF-8 encoded 32-bit integers (little-endian). + */ +function toIntArray(seed: string): Array { + const bytes = seedEncoder.encode(seed) + const result: Array = [] + + for (let index = 0; index < bytes.length; index += 4) { + result.push( + ((bytes[index] ?? 0) << 0) | + ((bytes[index + 1] ?? 0) << 8) | + ((bytes[index + 2] ?? 0) << 16) | + ((bytes[index + 3] ?? 0) << 24) + ) + } + + return result +} diff --git a/repos/effect/packages/effect/src/RcMap.ts b/repos/effect/packages/effect/src/RcMap.ts new file mode 100644 index 0000000..1889248 --- /dev/null +++ b/repos/effect/packages/effect/src/RcMap.ts @@ -0,0 +1,612 @@ +/** + * Shares scoped resources by key and releases them when no one is using them. + * + * An `RcMap` runs a lookup effect the first time a key is requested, shares the + * in-progress or acquired resource with other callers for the same key, and + * tracks each caller through its current `Scope`. When the last scope for a key + * closes, the resource can be released, kept alive for an idle time, or removed + * by capacity limits or explicit invalidation. It is meant for resource + * lifecycles such as clients, sessions, and connections, not as a general + * mutable cache. + * + * @since 3.5.0 + */ +import * as Cause from "./Cause.ts" +import { Clock } from "./Clock.ts" +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import { constant, dual, flow } from "./Function.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import * as Scope from "./Scope.ts" + +const TypeId = "~effect/RcMap" + +/** + * An `RcMap` is a reference-counted map data structure that manages the lifecycle + * of resources indexed by keys. Resources are lazily acquired and automatically + * released when no longer in use. + * + * **When to use** + * + * Use to share scoped resources by key while automatically releasing them after + * their last active reference is gone. + * + * **Example** (Inspecting a reference-counted map) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * // Create an RcMap that manages database connections + * const dbConnectionMap = yield* RcMap.make({ + * lookup: (dbName: string) => + * Effect.acquireRelease( + * Effect.succeed(`Connection to ${dbName}`), + * (conn) => Effect.log(`Closing ${conn}`) + * ), + * capacity: 10, + * idleTimeToLive: "5 minutes" + * }) + * + * // The RcMap interface provides access to: + * // - lookup: Function to acquire resources + * // - capacity: Maximum number of resources + * // - idleTimeToLive: Time before idle resources are released + * // - state: Current state of the map + * + * console.log(`Capacity: ${dbConnectionMap.capacity}`) + * }).pipe(Effect.scoped) + * ``` + * + * @see {@link make} for creating an `RcMap` + * @see {@link get} for acquiring or retaining a resource by key + * + * @category models + * @since 3.5.0 + */ +export interface RcMap extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly lookup: (key: K) => Effect.Effect + readonly context: Context.Context + readonly scope: Scope.Scope + readonly idleTimeToLive: (key: K) => Duration.Duration + readonly capacity: number + state: State +} + +/** + * Represents the internal state of an RcMap, which can be either Open (active) + * or Closed (shutdown and no longer accepting operations). + * + * **When to use** + * + * Use when typing code that inspects an `RcMap`'s `state` field and narrows + * between open and closed lifecycle states. + * + * @see {@link RcMap} for the map value that exposes this state + * @see {@link State.Open} for the active state with entries + * @see {@link State.Closed} for the shutdown state + * + * @category models + * @since 4.0.0 + */ +export type State = State.Open | State.Closed + +/** + * Namespace containing the internal state types for RcMap. + * + * **When to use** + * + * Use when referring to the concrete open, closed, and entry state shapes used + * by `RcMap`. + * + * @since 4.0.0 + */ +export declare namespace State { + /** + * Represents the open/active state of an RcMap, containing the actual + * resource map that stores entries. + * + * **When to use** + * + * Use when handling an `RcMap` that can still accept operations and contains + * stored entries. + * + * @category models + * @since 4.0.0 + */ + export interface Open { + readonly _tag: "Open" + readonly map: MutableHashMap.MutableHashMap> + } + + /** + * Represents the closed state of an RcMap, indicating that the map has been + * shut down and will no longer accept new operations. + * + * **When to use** + * + * Use when handling an `RcMap` after its owning scope has closed. + * + * @category models + * @since 4.0.0 + */ + export interface Closed { + readonly _tag: "Closed" + } + + /** + * Represents an individual entry in the RcMap, containing the resource's + * metadata including reference count, expiration time, and lifecycle management. + * + * **When to use** + * + * Use when inspecting the stored resource, reference count, and idle lifecycle + * metadata for a single key. + * + * @category models + * @since 4.0.0 + */ + export interface Entry { + readonly deferred: Deferred.Deferred + readonly scope: Scope.Closeable + readonly finalizer: Effect.Effect + readonly idleTimeToLive: Duration.Duration + fiber: Fiber.Fiber | undefined + expiresAt: number + refCount: number + } +} + +const makeUnsafe = (options: { + readonly lookup: (key: K) => Effect.Effect + readonly context: Context.Context + readonly scope: Scope.Scope + readonly idleTimeToLive: (key: K) => Duration.Duration + readonly capacity: number +}): RcMap => ({ + [TypeId]: TypeId, + lookup: options.lookup, + context: options.context, + scope: options.scope, + idleTimeToLive: options.idleTimeToLive, + capacity: options.capacity, + state: { + _tag: "Open", + map: MutableHashMap.empty() + }, + pipe() { + return pipeArguments(this, arguments) + } +}) + +/** + * Creates an `RcMap` that can contain multiple reference counted resources that can be indexed + * by a key. The resources are lazily acquired on the first call to `get` and + * released when the last reference is released. + * + * **When to use** + * + * Use to create a scoped reference-counted map for resources that should be + * acquired once per key and shared while in use. + * + * **Details** + * + * Complex keys can extend `Equal` and `Hash` to allow lookups by value. + * + * - `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. + * + * **Example** (Creating a reference-counted map) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`acquired ${key}`), + * () => Effect.log(`releasing ${key}`) + * ) + * }) + * + * // Get "foo" from the map twice, which will only acquire it once. + * // It will then be released once the scope closes. + * yield* RcMap.get(map, "foo").pipe( + * Effect.andThen(RcMap.get(map, "foo")), + * Effect.scoped + * ) + * }) + * ``` + * + * @see {@link get} for acquiring or retaining a resource by key + * @see {@link invalidate} for removing a resource from the map + * + * @category models + * @since 3.5.0 + */ +export const make: { + (options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined + readonly capacity?: undefined + }): Effect.Effect, never, Scope.Scope | R> + (options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined + readonly capacity: number + }): Effect.Effect, never, Scope.Scope | R> +} = (options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined + readonly capacity?: number | undefined +}) => + Effect.withFiber, never, R | Scope.Scope>((fiber) => { + const context = fiber.context as Context.Context + const scope = Context.get(context, Scope.Scope) + const self = makeUnsafe({ + lookup: options.lookup as any, + context, + scope, + idleTimeToLive: typeof options.idleTimeToLive === "function" + ? flow(options.idleTimeToLive, Duration.fromInputUnsafe) + : constant(Duration.fromInputUnsafe(options.idleTimeToLive ?? Duration.zero)), + capacity: Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0) + }) + return Effect.as( + Scope.addFinalizerExit(scope, () => { + if (self.state._tag === "Closed") { + return Effect.void + } + const map = self.state.map + self.state = { _tag: "Closed" } + return Effect.forEach( + map, + ([, entry]) => Effect.exit(Scope.close(entry.scope, Exit.void)) + ).pipe( + Effect.tap(() => + Effect.sync(() => { + MutableHashMap.clear(map) + }) + ) + ) + }), + self + ) + }) + +/** + * Gets the resource for a key, acquiring it with the map's lookup function when + * the key is not already cached. + * + * **When to use** + * + * Use to acquire or retain the resource for a key within the current scope. + * + * **Details** + * + * The resource's reference count is incremented for the current `Scope`, and a + * release finalizer is added to that scope. When the current scope closes, the + * reference is released; the resource is closed when the last reference is + * released, subject to the map's idle time-to-live setting. + * + * **Example** (Acquiring a resource) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`Resource: ${key}`), + * () => Effect.log(`Released ${key}`) + * ) + * }) + * + * // Get a resource - it will be acquired on first access + * const resource = yield* RcMap.get(map, "database") + * console.log(resource) // "Resource: database" + * }).pipe(Effect.scoped) + * ``` + * + * @see {@link make} for creating the reference-counted map + * @see {@link invalidate} for removing a resource by key + * + * @category combinators + * @since 3.5.0 + */ +export const get: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = dual( + 2, + (self: RcMap, key: K): Effect.Effect => + Effect.uninterruptibleMask((restore) => { + if (self.state._tag === "Closed") { + return Effect.interrupt + } + const state = self.state + const parent = Fiber.getCurrent()! + const o = MutableHashMap.get(state.map, key) + let entry: State.Entry + if (o._tag === "Some") { + entry = o.value + entry.refCount++ + } else if (Number.isFinite(self.capacity) && MutableHashMap.size(self.state.map) >= self.capacity) { + return Effect.fail( + new Cause.ExceededCapacityError(`RcMap attempted to exceed capacity of ${self.capacity}`) + ) as Effect.Effect + } else { + entry = { + deferred: Deferred.makeUnsafe(), + scope: Scope.makeUnsafe(), + idleTimeToLive: self.idleTimeToLive(key), + finalizer: undefined as any, + fiber: undefined, + expiresAt: 0, + refCount: 1 + } + ;(entry as any).finalizer = release(self, key, entry) + MutableHashMap.set(state.map, key, entry) + const context = new Map(self.context.mapUnsafe) + parent.context.mapUnsafe.forEach((value, key) => { + context.set(key, value) + }) + context.set(Scope.Scope.key, entry.scope) + self.lookup(key).pipe( + Effect.runForkWith(Context.makeUnsafe(context)), + Fiber.runIn(entry.scope) + ).addObserver((exit) => Deferred.doneUnsafe(entry.deferred, exit)) + } + const scope = Context.getUnsafe(parent.context, Scope.Scope) + return Scope.addFinalizer(scope, entry.finalizer).pipe( + Effect.andThen(restore(Deferred.await(entry.deferred))) + ) + }) +) + +const release = (self: RcMap, key: K, entry: State.Entry) => + Effect.withFiber((fiber) => { + entry.refCount-- + if (entry.refCount > 0) { + return Effect.void + } else if ( + self.state._tag === "Closed" + || !MutableHashMap.has(self.state.map, key) + || Duration.isZero(entry.idleTimeToLive) + ) { + if (self.state._tag === "Open") { + MutableHashMap.remove(self.state.map, key) + } + return Scope.close(entry.scope, Exit.void) + } else if (!Duration.isFinite(entry.idleTimeToLive)) { + return Effect.void + } + + const clock = fiber.getRef(Clock) + entry.expiresAt = clock.currentTimeMillisUnsafe() + Duration.toMillis(entry.idleTimeToLive) + if (entry.fiber) return Effect.void + + entry.fiber = Effect.interruptibleMask(function loop(restore): Effect.Effect { + const now = clock.currentTimeMillisUnsafe() + const remaining = entry.expiresAt - now + if (remaining <= 0) { + if (self.state._tag === "Closed" || entry.refCount > 0) return Effect.void + MutableHashMap.remove(self.state.map, key) + return restore(Scope.close(entry.scope, Exit.void)) + } + return Effect.flatMap(clock.sleep(Duration.millis(remaining)), () => loop(restore)) + }).pipe( + Effect.ensuring(Effect.sync(() => { + entry.fiber = undefined + })), + Effect.runForkWith(fiber.context), + Fiber.runIn(self.scope) + ) + return Effect.void + }) + +/** + * Returns an iterable of all keys currently stored in the `RcMap`. + * + * **When to use** + * + * Use to inspect which keys currently have stored resources in an `RcMap`. + * + * **Details** + * + * If the `RcMap` has been closed, the effect is interrupted. + * + * **Example** (Listing keys) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => Effect.succeed(`value-${key}`) + * }) + * + * // Add some resources to the map + * yield* RcMap.get(map, "foo") + * yield* RcMap.get(map, "bar") + * yield* RcMap.get(map, "baz") + * + * // Get all keys currently in the map + * const allKeys = yield* RcMap.keys(map) + * console.log(allKeys) // ["foo", "bar", "baz"] + * }).pipe(Effect.scoped) + * ``` + * + * @see {@link has} for checking one key without enumerating all keys + * + * @category combinators + * @since 3.8.0 + */ +export const keys = (self: RcMap): Effect.Effect> => { + return Effect.suspend(() => + self.state._tag === "Closed" ? Effect.interrupt : Effect.succeed(MutableHashMap.keys(self.state.map)) + ) +} + +/** + * Invalidates and removes a specific key from the RcMap. If the resource is not + * currently in use (reference count is 0), it will be immediately released. + * + * **When to use** + * + * Use to remove a resource by key so the next access performs a fresh lookup. + * + * **Example** (Invalidating a resource) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`Resource: ${key}`), + * () => Effect.log(`Released ${key}`) + * ) + * }) + * + * // Get a resource + * yield* RcMap.get(map, "cache") + * + * // Invalidate the resource - it will be removed from the map + * // and released if no longer in use + * yield* RcMap.invalidate(map, "cache") + * + * // Next access will create a new resource + * yield* RcMap.get(map, "cache") + * }).pipe(Effect.scoped) + * ``` + * + * @see {@link get} for acquiring or retaining the resource for a key + * @see {@link touch} for extending the idle lifetime without removing the entry + * + * @category combinators + * @since 3.13.0 + */ +export const invalidate: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = dual( + 2, + Effect.fnUntraced(function*(self: RcMap, key: K) { + if (self.state._tag === "Closed") return + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None") return + const entry = o.value + MutableHashMap.remove(self.state.map, key) + if (entry.refCount > 0) return + if (entry.fiber) yield* Fiber.interrupt(entry.fiber) + yield* Scope.close(entry.scope, Exit.void) + }, Effect.uninterruptible) +) + +/** + * Returns whether the `RcMap` currently contains an entry for the specified + * key. + * + * **When to use** + * + * Use to check whether a key is already present in an `RcMap` without running + * the lookup function or acquiring a missing resource. + * + * **Details** + * + * This operation only checks the current map state. + * + * **Gotchas** + * + * Closed maps return `false`, so `false` does not distinguish a missing key + * from a closed map. + * + * @see {@link get} for acquiring or retaining the resource for a key + * @see {@link keys} for enumerating all currently stored keys + * + * @category combinators + * @since 3.17.7 + */ +export const has: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = dual( + 2, + (self: RcMap, key: K) => + Effect.sync(() => { + if (self.state._tag === "Closed") return false + return MutableHashMap.has(self.state.map, key) + }) +) + +/** + * Extends the idle time for a resource in the RcMap. If the RcMap has an + * `idleTimeToLive` configured, calling `touch` will reset the expiration + * timer for the specified key. + * + * **When to use** + * + * Use to keep an idle resource alive longer without acquiring a new reference. + * + * **Example** (Extending resource idle time) + * + * ```ts + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`Resource: ${key}`), + * () => Effect.log(`Released ${key}`) + * ), + * idleTimeToLive: "10 seconds" + * }) + * + * // Get a resource + * yield* RcMap.get(map, "session") + * + * // Touch the resource to extend its idle time + * // This resets the 10-second expiration timer + * yield* RcMap.touch(map, "session") + * + * // The resource will now live for another 10 seconds + * // from the time it was touched + * }).pipe(Effect.scoped) + * ``` + * + * @see {@link invalidate} for removing the resource instead of extending it + * + * @category combinators + * @since 3.13.0 + */ +export const touch: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = dual( + 2, + (self: RcMap, key: K) => + Effect.clockWith((clock) => { + if (self.state._tag === "Closed") { + return Effect.void + } + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None" || Duration.isZero(o.value.idleTimeToLive)) { + return Effect.void + } + const entry = o.value + entry.expiresAt = clock.currentTimeMillisUnsafe() + Duration.toMillis(entry.idleTimeToLive) + return Effect.void + }) +) diff --git a/repos/effect/packages/effect/src/RcRef.ts b/repos/effect/packages/effect/src/RcRef.ts new file mode 100644 index 0000000..fab9307 --- /dev/null +++ b/repos/effect/packages/effect/src/RcRef.ts @@ -0,0 +1,225 @@ +/** + * Reference-counted handles for sharing one scoped resource across many scoped + * users. An `RcRef` acquires the resource lazily the first time `get` + * needs it, reuses that value while it is borrowed or kept idle, and finalizes + * it when the final borrowing scope closes unless an idle timeout keeps it + * available. The module also provides `invalidate` for forcing the next `get` + * to acquire a fresh resource. + * + * @since 3.5.0 + */ +import type * as Duration from "./Duration.ts" +import type * as Effect from "./Effect.ts" +import * as internal from "./internal/rcRef.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Scope } from "./Scope.ts" +import type * as Types from "./Types.ts" + +const TypeId = "~effect/RcRef" + +/** + * A reference counted reference that manages resource lifecycle. + * + * **When to use** + * + * Use to share a scoped resource across active users with reference-counted + * acquisition and release. + * + * **Details** + * + * An RcRef wraps a resource that can be acquired and released multiple times. + * The resource is lazily acquired on the first call to `get` and automatically + * released when the last reference is released. + * + * **Example** (Sharing a lazily acquired resource) + * + * ```ts + * import { Effect, RcRef } from "effect" + * + * // Create an RcRef for a database connection + * const createConnectionRef = (connectionString: string) => + * RcRef.make({ + * acquire: Effect.acquireRelease( + * Effect.succeed(`Connected to ${connectionString}`), + * (connection) => Effect.log(`Closing connection: ${connection}`) + * ) + * }) + * + * // Use the RcRef in multiple operations + * const program = Effect.gen(function*() { + * const connectionRef = yield* createConnectionRef("postgres://localhost") + * + * // Multiple gets will share the same connection + * const connection1 = yield* RcRef.get(connectionRef) + * const connection2 = yield* RcRef.get(connectionRef) + * + * return [connection1, connection2] + * }) + * ``` + * + * @category models + * @since 3.5.0 + */ +export interface RcRef extends Pipeable { + readonly [TypeId]: RcRef.Variance +} + +/** + * Namespace containing type-level members associated with `RcRef`. + * + * **Example** (Referencing namespace types) + * + * ```ts + * import type { RcRef } from "effect" + * + * // Use RcRef namespace types + * type MyRcRef = RcRef.RcRef + * type MyVariance = RcRef.RcRef.Variance + * ``` + * + * @since 3.5.0 + */ +export declare namespace RcRef { + /** + * Type-level variance marker for `RcRef`. + * + * **When to use** + * + * Use to carry the value and error type parameters for `RcRef` in Effect's + * type machinery. + * + * **Details** + * + * This interface records the covariant value and error types carried by an + * `RcRef`. It is used by Effect's type machinery and is not normally + * referenced directly by users. + * + * @category models + * @since 3.5.0 + */ + export interface Variance { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * Creates an `RcRef` from an acquire effect. + * + * **When to use** + * + * Use to create a lazily acquired, reference-counted resource from an acquire + * effect. + * + * **Details** + * + * The resource is acquired lazily on the first `get` and shared by subsequent + * gets while it remains cached. Each `get` adds a reference to the current + * `Scope`. When the last reference is released, the resource is closed + * immediately by default, or after `idleTimeToLive` when that option is + * provided. + * + * **Example** (Creating a reference-counted resource) + * + * ```ts + * import { Effect, RcRef } from "effect" + * + * Effect.gen(function*() { + * const ref = yield* RcRef.make({ + * acquire: Effect.acquireRelease( + * Effect.succeed("foo"), + * () => Effect.log("release foo") + * ) + * }) + * + * // will only acquire the resource once, and release it + * // when the scope is closed + * yield* RcRef.get(ref).pipe( + * Effect.andThen(RcRef.get(ref)), + * Effect.scoped + * ) + * }) + * ``` + * + * @category constructors + * @since 3.5.0 + */ +export const make: ( + options: { + readonly acquire: Effect.Effect + /** + * When the reference count reaches zero, the resource will be released + * after this duration. + */ + readonly idleTimeToLive?: Duration.Input | undefined + } +) => Effect.Effect, never, R | Scope> = internal.make + +/** + * Gets the value from an `RcRef`, acquiring it first if needed. + * + * **When to use** + * + * Use to borrow the current resource within a `Scope`, acquiring it first if + * necessary. + * + * **Details** + * + * The reference count is incremented for the current `Scope`, and a release + * finalizer is added to that scope. When the current scope closes, the + * reference is released; the resource is closed when the final reference is + * released, subject to any configured idle time-to-live. + * + * **Example** (Sharing one acquired value) + * + * ```ts + * import { Effect, RcRef } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an RcRef with a resource + * const ref = yield* RcRef.make({ + * acquire: Effect.acquireRelease( + * Effect.succeed("shared resource"), + * (resource) => Effect.log(`Releasing ${resource}`) + * ) + * }) + * + * // Get the value from the RcRef + * const value1 = yield* RcRef.get(ref) + * const value2 = yield* RcRef.get(ref) + * + * // Both values are the same instance + * console.log(value1 === value2) // true + * + * return value1 + * }) + * ``` + * + * @category combinators + * @since 3.5.0 + */ +export const get: (self: RcRef) => Effect.Effect = internal.get + +/** + * Invalidates the currently cached resource, if one has been acquired. + * + * **When to use** + * + * Use to force future `RcRef.get` calls to acquire a fresh resource when the + * currently cached resource should no longer be reused. + * + * **Details** + * + * After invalidation, the next `get` acquires a fresh resource. + * + * **Gotchas** + * + * Invalidation does not revoke resources already borrowed by active scopes; + * those remain usable until their scopes close. + * + * @see {@link get} for acquiring the current cached resource or the fresh resource after invalidation + * + * @category combinators + * @since 3.19.6 + */ +export const invalidate: (self: RcRef) => Effect.Effect = internal.invalidate diff --git a/repos/effect/packages/effect/src/Record.ts b/repos/effect/packages/effect/src/Record.ts new file mode 100644 index 0000000..99d006c --- /dev/null +++ b/repos/effect/packages/effect/src/Record.ts @@ -0,0 +1,1563 @@ +/** + * Works with plain JavaScript records as immutable key/value dictionaries. + * + * A record is an object whose keys are strings or symbols. This module includes + * helpers for construction, lookup, updates, mapping, filtering, folding, + * set-like combination, and typed conversions between records and iterable + * entries. Helpers that change values return new records instead of mutating the + * input. + * + * @since 2.0.0 + */ + +import type * as Combiner from "./Combiner.ts" +import * as Equal from "./Equal.ts" +import type { Equivalence } from "./Equivalence.ts" +import { dual, identity } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import * as Option from "./Option.ts" +import * as Reducer from "./Reducer.ts" +import type { Result } from "./Result.ts" +import * as R from "./Result.ts" +import type { NoInfer } from "./Types.ts" + +/** + * Represents a readonly record with keys of type `K` and values of type `A`. + * This is the foundational type for immutable key-value mappings in Effect. + * + * **Example** (Defining a readonly record type) + * + * ```ts + * import type { Record } from "effect" + * + * // Creating a readonly record type + * type UserRecord = Record.ReadonlyRecord<"name" | "age", string | number> + * + * const user: UserRecord = { + * name: "John", + * age: 30 + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export type ReadonlyRecord = { + readonly [P in K]: A +} + +/** + * Namespace containing utility types for working with readonly records. + * These types help with type-level operations on record keys and values. + * + * **Example** (Using readonly record helper types) + * + * ```ts + * import type { Record } from "effect" + * + * // Using NonLiteralKey to convert literal keys to generic types + * type GenericKey = Record.ReadonlyRecord.NonLiteralKey<"foo" | "bar"> // string + * + * // Using IntersectKeys to find common keys between record types + * type CommonKeys = Record.ReadonlyRecord.IntersectKeys<"a" | "b", "b" | "c"> // "b" + * ``` + * + * @since 2.0.0 + */ +export declare namespace ReadonlyRecord { + type IsFiniteString = T extends "" ? true : + [T] extends [`${infer Head}${infer Rest}`] + ? string extends Head ? false : `${number}` extends Head ? false : Rest extends "" ? true : IsFiniteString + : false + + /** + * Represents a type that converts literal string keys to generic string type and symbol keys to generic symbol type. + * This is useful for maintaining type safety while allowing flexible key types in record operations. + * + * **Example** (Converting literal keys to non-literal keys) + * + * ```ts + * import type { Record } from "effect" + * + * // For literal string keys, this becomes 'string' + * type Example1 = Record.ReadonlyRecord.NonLiteralKey<"foo" | "bar"> // string + * + * // For symbol keys, this becomes 'symbol' + * type Example2 = Record.ReadonlyRecord.NonLiteralKey // symbol + * ``` + * + * @category models + * @since 2.0.0 + */ + export type NonLiteralKey = K extends string ? IsFiniteString extends true ? string : K + : symbol + + /** + * Represents the intersection of two key types, handling both literal and non-literal string keys. + * This type is used in record operations that need to compute overlapping keys. + * + * **Example** (Intersecting record keys) + * + * ```ts + * import type { Record } from "effect" + * + * // Intersection of literal keys + * type Example1 = Record.ReadonlyRecord.IntersectKeys<"a" | "b", "b" | "c"> // "b" + * + * // Intersection with generic string + * type Example2 = Record.ReadonlyRecord.IntersectKeys // string + * ``` + * + * @category models + * @since 2.0.0 + */ + export type IntersectKeys = [string] extends [K1 | K2] ? + NonLiteralKey & NonLiteralKey + : K1 & K2 +} + +/** + * Type lambda for readonly records, used in higher-kinded type operations. + * This enables records to work with generic type constructors and functors. + * + * **Example** (Applying a readonly record type lambda) + * + * ```ts + * import type { HKT, Record } from "effect" + * + * type Settings = HKT.Kind< + * Record.ReadonlyRecordTypeLambda<"port" | "retries">, + * never, + * never, + * never, + * number + * > + * + * const defaults: Settings = { + * port: 3000, + * retries: 3 + * } + * ``` + * + * @category type lambdas + * @since 2.0.0 + */ +export interface ReadonlyRecordTypeLambda extends TypeLambda { + readonly type: ReadonlyRecord +} + +/** + * Creates a new, empty record. + * + * **Example** (Creating an empty record) + * + * ```ts + * import { Record } from "effect" + * + * // Create an empty record + * const emptyRecord = Record.empty() + * console.log(emptyRecord) // {} + * + * // The type ensures type safety for future operations + * const withValue = Record.set(emptyRecord, "count", 42) + * console.log(withValue) // { count: 42 } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Record< + ReadonlyRecord.NonLiteralKey, + V +> => ({} as any) + +/** + * Determines if a mutable record is empty. + * + * **Example** (Checking for an empty record) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.isEmptyRecord({}), true) + * assert.deepStrictEqual(Record.isEmptyRecord({ a: 3 }), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyRecord = (self: Record): self is Record => + Object.keys(self).length === 0 + +/** + * Determines if a readonly record is empty. + * + * **Example** (Checking for an empty readonly record) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.isEmptyReadonlyRecord({}), true) + * assert.deepStrictEqual(Record.isEmptyReadonlyRecord({ a: 3 }), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isEmptyReadonlyRecord: ( + self: ReadonlyRecord +) => self is ReadonlyRecord = isEmptyRecord + +/** + * Takes an iterable and a projection function and returns a record. + * The projection function maps each value of the iterable to a tuple of a key and a value, which is then added to the resulting record. + * + * **Example** (Building a record from mapped iterable values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const input = [1, 2, 3, 4] + * + * assert.deepStrictEqual( + * Record.fromIterableWith(input, (a) => [String(a), a * 2]), + * { "1": 2, "2": 4, "3": 6, "4": 8 } + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterableWith: { + ( + f: (a: A) => readonly [K, B] + ): (self: Iterable) => Record, B> + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> +} = dual( + 2, + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> => { + const out: Record = empty() + for (const a of self) { + const [k, b] = f(a) + out[k] = b + } + return out + } +) + +/** + * Creates a new record from an iterable, utilizing the provided function to determine the key for each element. + * + * **Example** (Building a record keyed by iterable values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const users = [ + * { id: "2", name: "name2" }, + * { id: "1", name: "name1" } + * ] + * + * assert.deepStrictEqual( + * Record.fromIterableBy(users, (user) => user.id), + * { + * "2": { id: "2", name: "name2" }, + * "1": { id: "1", name: "name1" } + * } + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterableBy = ( + items: Iterable, + f: (a: A) => K +): Record, A> => fromIterableWith(items, (a) => [f(a), a]) + +/** + * Builds a record from an iterable of key-value pairs. + * + * **Details** + * + * If there are conflicting keys when using `fromEntries`, the last occurrence of the key/value pair will overwrite the + * previous ones. So the resulting record will only have the value of the last occurrence of each key. + * + * **Example** (Building a record from entries) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const input: Array<[string, number]> = [["a", 1], ["b", 2]] + * + * assert.deepStrictEqual(Record.fromEntries(input), { a: 1, b: 2 }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromEntries: ( + entries: Iterable +) => Record, Entry[1]> = Object.fromEntries + +/** + * Transforms the values of a record into an `Array` with a custom mapping function. + * + * **Example** (Collecting mapped record values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(Record.collect(x, (key, n) => [key, n]), [["a", 1], [ + * "b", + * 2 + * ], ["c", 3]]) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const collect: { + (f: (key: K, a: A) => B): (self: ReadonlyRecord) => Array + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array +} = dual( + 2, + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array => { + const out: Array = [] + for (const key of keys(self)) { + out.push(f(key, self[key])) + } + return out + } +) + +/** + * Takes a record and returns an array of tuples containing its keys and values. + * + * **Example** (Converting a record to entries) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3 } + * assert.deepStrictEqual(Record.toEntries(x), [["a", 1], ["b", 2], ["c", 3]]) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const toEntries: (self: ReadonlyRecord) => Array<[K, A]> = collect(( + key, + value +) => [key, value]) + +/** + * Returns the number of key/value pairs in a record. + * + * **Example** (Getting the record size) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.size({ a: "a", b: 1, c: true }), 3) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: ReadonlyRecord): number => keys(self).length + +/** + * Checks whether a given `key` exists in a record. + * + * **Example** (Checking key membership) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.has({ a: 1, b: 2 }, "a"), true) + * assert.deepStrictEqual(Record.has(Record.empty(), "c"), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const has: { + ( + key: NoInfer + ): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean +} = dual( + 2, + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean => Object.hasOwn(self, key) +) + +/** + * Retrieves a value at a particular key from a record safely, returning it wrapped in an `Option`. + * + * **Example** (Getting a value as an Option) + * + * ```ts + * import { Option, Record as R } from "effect" + * import * as assert from "node:assert" + * + * const person: Record = { name: "John Doe", age: 35 } + * + * assert.deepStrictEqual(R.get(person, "name"), Option.some("John Doe")) + * assert.deepStrictEqual(R.get(person, "email"), Option.none()) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const get: { + (key: NoInfer): (self: ReadonlyRecord) => Option.Option + (self: ReadonlyRecord, key: NoInfer): Option.Option +} = dual( + 2, + (self: ReadonlyRecord, key: NoInfer): Option.Option => + has(self, key) ? Option.some(self[key]) : Option.none() +) + +/** + * Applies a function to the element at the specified key safely, creating a new record, + * or return `Option.none()` if the key doesn't exist. + * + * **Example** (Modifying a value at a key) + * + * ```ts + * import { Record } from "effect" + * + * const f = (x: number) => x * 2 + * + * const input: Record = { a: 3 } + * + * Record.modify(input, "a", f) // Option.some({ a: 6 }) + * Record.modify(input, "b", f) // Option.none() + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const modify: { + ( + key: NoInfer, + f: (a: A) => B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> +} = dual( + 3, + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> => { + if (!has(self, key)) return Option.none() + return Option.some({ ...self, [key]: f(self[key]) }) + } +) + +/** + * Replaces the value at an existing key safely and returns the updated record in + * `Option.some`. + * + * **Details** + * + * If the key is not present, returns `Option.none()` and leaves the record + * unchanged. + * + * **Example** (Replacing a value at a key) + * + * ```ts + * import { Record } from "effect" + * + * Record.replace({ a: 1, b: 2, c: 3 }, "a", 10) // Option.some({ a: 10, b: 2, c: 3 }) + * Record.replace(Record.empty(), "a", 10) // Option.none() + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const replace: { + ( + key: NoInfer, + b: B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> +} = dual( + 3, + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> => modify(self, key, () => b) +) + +/** + * Removes a key from a record. + * + * **When to use** + * + * Use to create a shallow copy of a record without one property. + * + * **Details** + * + * If the key is not present, the result is still a shallow copy of the original + * record. + * + * **Example** (Removing a key) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.remove({ a: 1, b: 2 }, "a"), { b: 2 }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (key: X): (self: ReadonlyRecord) => Record, A> + (self: ReadonlyRecord, key: X): Record, A> +} = dual( + 2, + (self: ReadonlyRecord, key: X): Record, A> => { + if (!has(self, key)) { + return { ...self } + } + const out = { ...self } + delete out[key] + return out + } +) + +/** + * Retrieves the value of the property with the given `key` from a record safely and returns an `Option` + * of a tuple with the value and the record with the removed property. + * If the key is not present, returns `Option.none()`. + * + * **Example** (Popping a value and removing its key) + * + * ```ts + * import { Record } from "effect" + * + * const input: Record = { a: 1, b: 2 } + * + * Record.pop(input, "a") // Option.some([1, { b: 2 }]) + * Record.pop(input, "c") // Option.none() + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const pop: { + ( + key: X + ): (self: ReadonlyRecord) => Option.Option<[A, Record, A>]> + ( + self: ReadonlyRecord, + key: X + ): Option.Option<[A, Record, A>]> +} = dual(2, ( + self: ReadonlyRecord, + key: X +): Option.Option<[A, Record, A>]> => + has(self, key) ? Option.some([self[key], remove(self, key)]) : Option.none()) + +/** + * Maps a record into another record by applying a transformation function to each of its values. + * + * **Example** (Mapping record values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const f = (n: number) => `-${n}` + * + * assert.deepStrictEqual(Record.map({ a: 3, b: 5 }, f), { a: "-3", b: "-5" }) + * + * const g = (n: number, key: string) => `${key.toUpperCase()}-${n}` + * + * assert.deepStrictEqual(Record.map({ a: 3, b: 5 }, g), { a: "A-3", b: "B-5" }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A, key: NoInfer) => B): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record +} = dual( + 2, + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record => { + const out: Record = { ...self } as any + for (const key of keys(self)) { + out[key] = f(self[key], key) + } + return out + } +) + +/** + * Maps the keys of a `ReadonlyRecord` while preserving the corresponding values. + * + * **Example** (Mapping record keys) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.mapKeys({ a: 3, b: 5 }, (key) => key.toUpperCase()), + * { A: 3, B: 5 } + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapKeys: { + ( + f: (key: K, a: A) => K2 + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record => { + const out: Record = {} as any + for (const key of keys(self)) { + const a = self[key] + out[f(key, a)] = a + } + return out + } +) + +/** + * Maps entries of a `ReadonlyRecord` using the provided function, allowing modification of both keys and corresponding values. + * + * **Example** (Mapping record entries) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.mapEntries({ a: 3, b: 5 }, (a, key) => [key.toUpperCase(), a + 1]), + * { A: 4, B: 6 } + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapEntries: { + ( + f: (a: A, key: K) => readonly [K2, B] + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record => { + const out = {} as Record + for (const key of keys(self)) { + const [k, b] = f(self[key], key) + out[k] = b + } + return out + } +) + +/** + * Transforms a record by applying the function `f` to each key and value in the original record. + * If the function succeeds, the key-value pair is included in the output record. + * + * **Example** (Filtering and mapping with Result) + * + * ```ts + * import { Record, Result } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3 } + * const f = (a: number, key: string) => a > 2 ? Result.succeed(a * 2) : Result.failVoid + * assert.deepStrictEqual(Record.filterMap(x, f), { c: 6 }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + ( + f: (input: A, key: K) => Result + ): (self: ReadonlyRecord) => Record, B> + ( + self: ReadonlyRecord, + f: (input: A, key: K) => Result + ): Record, B> +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (input: A, key: K) => Result + ): Record, B> => { + const out: Record = empty() + for (const key of keys(self)) { + const result = f(self[key], key) + if (R.isSuccess(result)) { + out[key] = result.success + } + } + return out + } +) + +/** + * Selects properties from a record whose values match the given predicate. + * + * **Example** (Filtering record values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3, d: 4 } + * assert.deepStrictEqual(Record.filter(x, (n) => n > 2), { c: 3, d: 4 }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + ( + refinement: (a: NoInfer, key: K) => a is B + ): (self: ReadonlyRecord) => Record, B> + ( + predicate: (A: NoInfer, key: K) => boolean + ): (self: ReadonlyRecord) => Record, A> + ( + self: ReadonlyRecord, + refinement: (a: A, key: K) => a is B + ): Record, B> + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> +} = dual( + 2, + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> => { + const out: Record = empty() + for (const key of keys(self)) { + if (predicate(self[key], key)) { + out[key] = self[key] + } + } + return out + } +) + +/** + * Returns a new record containing only the `Some` values from a record of + * `Option` values, preserving the original keys. + * + * **Example** (Extracting Some values) + * + * ```ts + * import { Option, Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.getSomes({ a: Option.some(1), b: Option.none(), c: Option.some(2) }), + * { a: 1, c: 2 } + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const getSomes: ( + self: ReadonlyRecord> +) => Record, A> = ( + self: ReadonlyRecord> +): Record, A> => { + const out: Record = empty() + for (const key of keys(self)) { + const option = self[key] + if (Option.isSome(option)) { + out[key] = option.value + } + } + return out +} + +/** + * Returns a new record containing only the `Err` values from a record of + * `Result` values, preserving the original keys. + * + * **Example** (Extracting Result failures) + * + * ```ts + * import { Record, Result } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.getFailures({ + * a: Result.succeed(1), + * b: Result.fail("err"), + * c: Result.succeed(2) + * }), + * { b: "err" } + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const getFailures = ( + self: ReadonlyRecord> +): Record, E> => { + const out: Record = empty() + for (const key of keys(self)) { + const value = self[key] + if (R.isFailure(value)) { + out[key] = value.failure + } + } + + return out +} + +/** + * Returns a new record containing only the `Ok` values from a record of + * `Result` values, preserving the original keys. + * + * **Example** (Extracting Result successes) + * + * ```ts + * import { Record, Result } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.getSuccesses({ + * a: Result.succeed(1), + * b: Result.fail("err"), + * c: Result.succeed(2) + * }), + * { a: 1, c: 2 } + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const getSuccesses = ( + self: ReadonlyRecord> +): Record => { + const out: Record = empty() + for (const key of keys(self)) { + const value = self[key] + if (R.isSuccess(value)) { + out[key] = value.success + } + } + + return out +} + +/** + * Applies a function to each record entry and partitions the returned `Result` + * values into two records. + * + * **Details** + * + * Failure values are collected in the left record, and success values are + * collected in the right record, preserving the original keys. + * + * **Example** (Partitioning with Result) + * + * ```ts + * import { Record, Result } from "effect" + * import * as assert from "node:assert" + * + * const x = { a: 1, b: 2, c: 3 } + * const f = (n: number) => (n % 2 === 0 ? Result.succeed(n) : Result.fail(n)) + * assert.deepStrictEqual(Record.partition(x, f), [{ a: 1, c: 3 }, { b: 2 }]) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + ( + f: (input: A, key: K) => Result + ): ( + self: ReadonlyRecord + ) => [left: Record, B>, right: Record, C>] + ( + self: ReadonlyRecord, + f: (input: A, key: K) => Result + ): [left: Record, B>, right: Record, C>] +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (input: A, key: K) => Result + ): [left: Record, B>, right: Record, C>] => { + const left: Record = empty() + const right: Record = empty() + for (const key of keys(self)) { + const e = f(self[key], key) + if (R.isFailure(e)) { + left[key] = e.failure + } else { + right[key] = e.success + } + } + return [left, right] + } +) + +/** + * Partitions a record of `Result` values into two separate records, + * one with the `Err` values and one with the `Ok` values. + * + * **Example** (Separating Result values) + * + * ```ts + * import { Record, Result } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.separate({ a: Result.fail("e"), b: Result.succeed(1) }), + * [{ a: "e" }, { b: 1 }] + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const separate: ( + self: ReadonlyRecord> +) => [Record, A>, Record, B>] = partition(identity) + +/** + * Retrieves the keys of a given record as an array. + * + * **Example** (Getting record keys) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.keys({ a: 1, b: 2, c: 3 }), ["a", "b", "c"]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const keys = (self: ReadonlyRecord): Array => + Object.keys(self) as Array + +/** + * Retrieves the values of a given record as an array. + * + * **Example** (Getting record values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.values({ a: 1, b: 2, c: 3 }), [1, 2, 3]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const values = (self: ReadonlyRecord): Array => collect(self, (_, a) => a) + +/** + * Adds a new key-value pair or update an existing key's value in a record. + * + * **Example** (Setting a record value) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.set("a", 5)({ a: 1, b: 2 }), { a: 5, b: 2 }) + * assert.deepStrictEqual(Record.set("c", 5)({ a: 1, b: 2 }), { a: 1, b: 2, c: 5 }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const set: { + ( + key: K1, + value: B + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record +} = dual( + 3, + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record => { + return { ...self, [key]: value } as any + } +) + +/** + * Checks whether all the keys and values in one record are also found in another record. + * Uses the provided equivalence function to compare values. + * + * **Example** (Checking subrecords with a custom equivalence) + * + * ```ts + * import { Equivalence, Record } from "effect" + * + * const isSubrecord = Record.isSubrecordBy( + * Equivalence.make((self, that) => self.toLowerCase() === that.toLowerCase()) + * ) + * + * const required: Record.ReadonlyRecord = { role: "Admin" } + * const available: Record.ReadonlyRecord = { + * role: "admin", + * status: "active" + * } + * + * console.log( + * isSubrecord(required, available) + * ) // true + * console.log( + * isSubrecord({ role: "Admin", status: "inactive" }, available) + * ) // false + * console.log( + * isSubrecord(required, { role: "editor", status: "active" }) + * ) // false + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isSubrecordBy = (equivalence: Equivalence): { + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean +} => + dual(2, (self: ReadonlyRecord, that: ReadonlyRecord): boolean => { + for (const key of keys(self)) { + if (!has(that, key) || !equivalence(self[key], that[key])) { + return false + } + } + return true + }) + +/** + * Checks whether the first record is a subrecord of the second record. + * + * **Details** + * + * Returns `true` when every key and value in `self` is also present in `that`. + * Values are compared with Effect equality via `Equal.asEquivalence()`. + * + * **Example** (Checking subrecords) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.isSubrecord({ a: 1 } as Record, { a: 1, b: 2 }), + * true + * ) + * assert.deepStrictEqual( + * Record.isSubrecord({ a: 1, b: 2 }, { a: 1 } as Record), + * false + * ) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isSubrecord: { + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean +} = isSubrecordBy(Equal.asEquivalence()) + +/** + * Reduces a record to a single value by combining its entries with a specified function. + * + * **Example** (Reducing record values) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.reduce({ a: 1, b: 2, c: 3 }, 0, (acc, value, key) => acc + value), + * 6 + * ) + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + ( + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): (self: ReadonlyRecord) => Z + (self: ReadonlyRecord, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = dual( + 3, + ( + self: ReadonlyRecord, + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): Z => { + let out: Z = zero + for (const key of keys(self)) { + out = f(out, self[key], key) + } + return out + } +) + +/** + * Checks whether all entries in a record meet a specific condition. + * + * **Example** (Checking every record value) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.every({ a: 1, b: 2 }, (n) => n > 0), true) + * assert.deepStrictEqual(Record.every({ a: 1, b: -1 }, (n) => n > 0), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const every: { + ( + refinement: (value: A, key: K) => value is B + ): (self: ReadonlyRecord) => self is ReadonlyRecord + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + refinement: (value: A, key: K) => value is B + ): self is ReadonlyRecord + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, + ( + self: ReadonlyRecord, + refinement: (value: A, key: K) => value is B + ): self is ReadonlyRecord => { + for (const key of keys(self)) { + if (!refinement(self[key], key)) { + return false + } + } + return true + } +) + +/** + * Checks whether any entry in a record meets a specific condition. + * + * **Example** (Checking for any matching value) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.some({ a: 1, b: 2 }, (n) => n > 1), true) + * assert.deepStrictEqual(Record.some({ a: 1, b: 2 }, (n) => n > 2), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const some: { + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean => { + for (const key of keys(self)) { + if (predicate(self[key], key)) { + return true + } + } + return false + } +) + +/** + * Merges two records, preserving entries that exist in either of the records. + * For keys that exist in both records, the provided combine function is used to merge the values. + * + * **Example** (Merging records with union) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.union({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b), + * { a: 1, b: 5, c: 4 } + * ) + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const union: { + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record +} = dual( + 3, + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record => { + if (isEmptyRecord(self)) { + return { ...that } as any + } + if (isEmptyRecord(that)) { + return { ...self } as any + } + const out: Record = empty() + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) + } else { + out[key] = self[key] + } + } + for (const key of keys(that)) { + if (!has(out, key)) { + out[key] = that[key] + } + } + return out + } +) + +/** + * Merges two records, retaining only the entries that exist in both records. + * For intersecting keys, the provided combine function is used to merge the values. + * + * **Example** (Merging intersecting keys) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.intersection({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b), + * { b: 5 } + * ) + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const intersection: { + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record, C> + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> +} = dual( + 3, + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> => { + const out: Record = empty() + if (isEmptyRecord(self) || isEmptyRecord(that)) { + return out + } + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) + } + } + return out + } +) + +/** + * Merges two records, preserving only the entries that are unique to each record. + * Keys that exist in both records are excluded from the result. + * + * **Example** (Keeping keys unique to each record) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual( + * Record.difference({ a: 1, b: 2 }, { b: 3, c: 4 }), + * { a: 1, c: 4 } + * ) + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const difference: { + ( + that: ReadonlyRecord + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord + ): Record +} = dual(2, ( + self: ReadonlyRecord, + that: ReadonlyRecord +): Record => { + if (isEmptyRecord(self)) { + return { ...that } as any + } + if (isEmptyRecord(that)) { + return { ...self } as any + } + const out = {} as Record + for (const key of keys(self)) { + if (!has(that, key as any)) { + out[key] = self[key] + } + } + for (const key of keys(that)) { + if (!has(self, key as any)) { + out[key] = that[key] + } + } + return out +}) + +/** + * Create an `Equivalence` for records using the provided `Equivalence` for values. + * Two records are considered equivalent if they have the same keys and their corresponding values are equivalent. + * + * **Example** (Comparing records with a value equivalence) + * + * ```ts + * import { Equal, Record } from "effect" + * import * as assert from "node:assert" + * + * const recordEquivalence = Record.makeEquivalence(Equal.asEquivalence()) + * + * assert.deepStrictEqual(recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 2 }), true) + * assert.deepStrictEqual(recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 3 }), false) + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = ( + equivalence: Equivalence +): Equivalence> => { + const is = isSubrecordBy(equivalence) + return (self, that) => is(self, that) && is(that, self) +} + +/** + * Create a non-empty record from a single element. + * + * **Example** (Creating a singleton record) + * + * ```ts + * import { Record } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(Record.singleton("a", 1), { a: 1 }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const singleton = (key: K, value: A): Record => ({ + [key]: value +} as any) + +/** + * Creates a `Reducer` for combining `Record`s using union, with values for keys that exist in both records combined + * using the provided `Combiner`. + * + * **When to use** + * + * Use to build a reusable reducer for accumulating many records into one + * union-shaped record, preserving keys from every input and combining + * overlapping values with the supplied combiner. + * + * **Details** + * + * The returned reducer uses `Record.union` for combine and an empty record as + * `initialValue`, so the default `combineAll` folds from `{}` and accumulates + * keys from each input record. + * + * @see {@link union} for one-off record merging with the same union semantics + * @see {@link makeReducerIntersection} for a reducer that keeps only keys present on both sides + * + * @category combining + * @since 4.0.0 + */ +export function makeReducerUnion(combiner: Combiner.Combiner): Reducer.Reducer> { + return Reducer.make>( + (self, that) => union(self, that, combiner.combine), + {} as Record + ) +} + +/** + * Creates a `Reducer` whose `combine` operation intersects two records and + * combines values for keys present in both records. + * + * **When to use** + * + * Use to build a `Reducer` that combines records by retaining only keys shared + * by both inputs and combining matching values with a `Combiner`. + * + * **Gotchas** + * + * The reducer's `initialValue` is an empty record. Because intersection with + * an empty record is empty, the default `combineAll` folds from `{}` and + * therefore produces `{}` for ordinary non-empty inputs. + * + * @see {@link makeReducerUnion} for a reducer that preserves keys from either input record + * @see {@link intersection} for applying the shared-key merge to one pair of records + * + * @category combining + * @since 4.0.0 + */ +export function makeReducerIntersection( + combiner: Combiner.Combiner +): Reducer.Reducer> { + return Reducer.make( + (self, that) => intersection(self, that, combiner.combine) as any, + {} as Record + ) +} + +/** + * Returns the first entry that satisfies the specified + * predicate, or `None` if no such entry exists. + * + * **Example** (Finding the first matching entry) + * + * ```ts + * import { Record } from "effect" + * + * const record = { a: 1, b: 2, c: 3 } + * const result = Record.findFirst( + * record, + * (value, key) => value > 1 && key !== "b" + * ) + * console.log(result) // Option.Some(["c", 3]) + * ``` + * + * @category elements + * @since 3.14.0 + */ +export const findFirst: { + ( + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): (self: ReadonlyRecord) => Option.Option<[K, V2]> + ( + predicate: (value: NoInfer, key: NoInfer) => boolean + ): (self: ReadonlyRecord) => Option.Option<[K, V]> + ( + self: ReadonlyRecord, + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): Option.Option<[K, V2]> + ( + self: ReadonlyRecord, + predicate: (value: NoInfer, key: NoInfer) => boolean + ): Option.Option<[K, V]> +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (value: V, key: K) => boolean + ): Option.Option<[K, V]> => { + const k = keys(self) + for (let i = 0; i < k.length; i++) { + const key = k[i] + if (f(self[key], key)) { + return Option.some([key, self[key]]) + } + } + return Option.none() + } +) diff --git a/repos/effect/packages/effect/src/Redactable.ts b/repos/effect/packages/effect/src/Redactable.ts new file mode 100644 index 0000000..23aeb26 --- /dev/null +++ b/repos/effect/packages/effect/src/Redactable.ts @@ -0,0 +1,168 @@ +/** + * Context-aware redaction for sensitive values. + * + * The `Redactable` module provides a protocol for objects that need to present + * alternative representations of themselves depending on the runtime context. + * Typical use cases include masking secrets, tokens, or personal data in logs, traces, + * and serialized output. + * + * @since 4.0.0 + */ +import type * as Context from "./Context.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" + +/** + * Defines the symbol used to identify objects that implement the {@link Redactable} + * protocol. + * + * **When to use** + * + * Use as the property key when implementing the `Redactable` protocol. + * + * **Details** + * + * Add a method under this key to make an object redactable. The method receives + * the current `Context` and must return the replacement value. The symbol is + * registered globally via `Symbol.for("~effect/Redactable")`, so it is + * identical across multiple copies of the library at runtime. + * + * **Example** (Masking an API key) + * + * ```ts + * import { Context, Redactable } from "effect" + * + * class ApiKey { + * constructor(readonly raw: string) {} + * + * [Redactable.symbolRedactable](_ctx: Context.Context) { + * return this.raw.slice(0, 4) + "..." + * } + * } + * ``` + * + * @see {@link Redactable} for the interface this symbol belongs to + * @see {@link isRedactable} to check whether a value has this symbol + * @category symbols + * @since 3.10.0 + */ +export const symbolRedactable: unique symbol = Symbol.for("~effect/Redactable") + +/** + * Interface for objects that provide context-aware redacted representations. + * + * **When to use** + * + * Use to define classes or objects that hold sensitive data and should present + * a sanitized form when inspected or logged. + * + * **Details** + * + * The `[symbolRedactable]` method receives the current fiber's `Context`. If no + * fiber is active, an empty `Context` is provided. + * + * **Example** (Masking an API key) + * + * ```ts + * import { Context, Redactable } from "effect" + * + * class ApiKey { + * constructor(readonly raw: string) {} + * + * [Redactable.symbolRedactable](_ctx: Context.Context) { + * return this.raw.slice(0, 4) + "..." + * } + * } + * ``` + * + * @see {@link symbolRedactable} for the symbol key to implement + * @see {@link redact} to apply redaction to any value + * @see {@link isRedactable} for the type guard for this interface + * @category models + * @since 3.10.0 + */ +export interface Redactable { + readonly [symbolRedactable]: (context: Context.Context) => unknown +} + +/** + * Type guard that checks whether a value implements the {@link Redactable} + * interface. + * + * **When to use** + * + * Use to narrow an unknown value before calling redaction-specific helpers. + * + * @see {@link Redactable} for the interface being checked + * @see {@link redact} to apply redaction if the value is redactable + * @category guards + * @since 3.10.0 + */ +export const isRedactable = (u: unknown): u is Redactable => hasProperty(u, symbolRedactable) + +/** + * Returns a redacted value if it implements {@link Redactable}, otherwise returns it + * unchanged. + * + * **When to use** + * + * Use as the general-purpose entry point for redaction when the input may + * or may not implement the redaction protocol. + * + * **Details** + * + * This function calls {@link isRedactable} and, when it returns `true`, + * delegates to {@link getRedacted}. + * + * **Gotchas** + * + * Redaction is not recursive. Nested redactable values inside the returned + * object are not automatically redacted. + * + * @see {@link isRedactable} to check before redacting + * @see {@link getRedacted} for the lower-level variant for known redactables + * @category destructors + * @since 3.10.0 + */ +export function redact(u: unknown): unknown { + if (isRedactable(u)) return getRedacted(u) + return u +} + +/** + * Returns the result of calling `[symbolRedactable]` on a value that is + * already known to be {@link Redactable}. + * + * **When to use** + * + * Use when you need to read the redacted representation from a value already + * verified as `Redactable`. + * + * **Details** + * + * This function reads the current fiber's `Context` from the global fiber + * reference and passes it to the redaction method. + * + * **Gotchas** + * + * If no fiber is active, an empty `Context` is passed to the redaction method. + * + * @see {@link redact} for the higher-level variant that handles non-redactable values + * @see {@link isRedactable} for the type guard to verify before calling this + * @category destructors + * @since 4.0.0 + */ +export function getRedacted(redactable: Redactable): unknown { + return redactable[symbolRedactable]((globalThis as any)[currentFiberTypeId]?.context ?? emptyContext) +} + +/** @internal */ +export const currentFiberTypeId = "~effect/Fiber/currentFiber" + +const emptyContext: Context.Context = { + "~effect/Context": {} as any, + mapUnsafe: new Map(), + pipe() { + return pipeArguments(this, arguments) + } +} as any diff --git a/repos/effect/packages/effect/src/Redacted.ts b/repos/effect/packages/effect/src/Redacted.ts new file mode 100644 index 0000000..afb1eb6 --- /dev/null +++ b/repos/effect/packages/effect/src/Redacted.ts @@ -0,0 +1,314 @@ +/** + * Wraps sensitive values so normal output does not reveal them. + * + * A `Redacted` shows a redacted placeholder in string, JSON, and inspection + * output, while still storing the original value for trusted code that needs to + * recover it. This helps reduce accidental leaks in logs and diagnostics. This + * module includes constructors, runtime checks, value recovery, wiping of stored + * values, and comparison helpers that avoid exposing the wrapped value at the + * call site. + * + * @since 3.3.0 + */ +import * as Equal from "./Equal.ts" +import * as Equivalence from "./Equivalence.ts" +import * as Hash from "./Hash.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as redacted from "./internal/redacted.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty, isString } from "./Predicate.ts" +import type { Covariant } from "./Types.ts" + +const TypeId = "~effect/data/Redacted" + +/** + * A wrapper for sensitive values whose string, JSON, and inspection output is + * redacted. + * + * **When to use** + * + * Use to carry sensitive values while reducing accidental exposure in string, + * JSON, and inspection output. + * + * **Gotchas** + * + * The underlying value is still stored in memory and can be recovered with + * `Redacted.value` until the wrapper is wiped or becomes unreachable. Use + * `Redacted` to reduce accidental disclosure in logs and diagnostics, not as a + * cryptographic protection mechanism. + * + * **Example** (Creating redacted values) + * + * ```ts + * import { Redacted } from "effect" + * + * // Create a redacted value to protect sensitive information + * const apiKey = Redacted.make("secret-key") + * const userPassword = Redacted.make("user-password") + * + * // TypeScript will infer the types as Redacted + * ``` + * + * @category models + * @since 3.3.0 + */ +export interface Redacted extends Redacted.Variance, Equal.Equal, Pipeable { + readonly label: string | undefined +} + +/** + * Namespace containing type-level members associated with `Redacted` values. + * + * **When to use** + * + * Use to access type-level helpers associated with `Redacted`. + * + * **Example** (Using namespace utilities) + * + * ```ts + * import { Redacted } from "effect" + * + * // Use the Redacted namespace for type-level operations + * const secret = Redacted.make("my-secret") + * + * // The namespace contains utilities for working with Redacted values + * const isRedacted = Redacted.isRedacted(secret) // true + * ``` + * + * @since 3.3.0 + */ +export declare namespace Redacted { + /** + * Type-level variance marker for `Redacted`. + * + * **When to use** + * + * Use when defining internals that need to preserve the covariant value type + * carried by `Redacted`. + * + * **Details** + * + * This interface records the covariant value type carried by a `Redacted` + * value and is not normally referenced directly by users. + * + * @category models + * @since 3.3.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Covariant + } + } + + /** + * Extracts the underlying value type from a `Redacted` type. + * + * **When to use** + * + * Use to infer the sensitive value type from an existing `Redacted` type. + * + * **Example** (Extracting the redacted value type) + * + * ```ts + * import { Redacted } from "effect" + * + * type ApiKey = Redacted.Redacted<{ readonly token: string }> + * type ApiKeyValue = Redacted.Redacted.Value + * + * const rotate = (value: ApiKeyValue): ApiKeyValue => ({ + * token: `${value.token}:rotated` + * }) + * + * console.log(rotate({ token: "secret" })) // { token: "secret:rotated" } + * ``` + * + * @category utility types + * @since 3.3.0 + */ + export type Value> = [T] extends [Redacted] ? _A : never +} + +/** + * Returns `true` if a value is a `Redacted` wrapper. + * + * **When to use** + * + * Use to validate unknown input and narrow it to `Redacted`. + * + * **Details** + * + * When this function returns `true`, TypeScript narrows the value to + * `Redacted`. + * + * **Example** (Checking for redacted values) + * + * ```ts + * import { Redacted } from "effect" + * + * const secret = Redacted.make("my-secret") + * const plainString = "not-secret" + * + * console.log(Redacted.isRedacted(secret)) // true + * console.log(Redacted.isRedacted(plainString)) // false + * ``` + * + * @category refinements + * @since 3.3.0 + */ +export const isRedacted = (u: unknown): u is Redacted => hasProperty(u, TypeId) + +/** + * Creates a `Redacted` wrapper for a sensitive value. + * + * **When to use** + * + * Use to wrap a sensitive value so normal string, JSON, and inspection output + * is redacted. + * + * **Details** + * + * The wrapper redacts string, JSON, and inspection output to reduce accidental + * disclosure. The original value remains retrievable with `Redacted.value` + * until the wrapper is wiped or becomes unreachable. + * + * **Example** (Creating a redacted value) + * + * ```ts + * import { Redacted } from "effect" + * + * const API_KEY = Redacted.make("1234567890") + * ``` + * + * @category constructors + * @since 3.3.0 + */ +export const make = (value: T, options?: { + readonly label?: string | undefined +}): Redacted => { + const self = Object.create(Proto) + if (options?.label) { + self.label = options.label + } + redacted.redactedRegistry.set(self, value) + return self +} + +const Proto = { + [TypeId]: { + _A: (_: never) => _ + }, + label: undefined, + ...PipeInspectableProto, + toJSON() { + return this.toString() + }, + toString() { + return `` + }, + [Hash.symbol](this: Redacted): number { + return Hash.hash(redacted.redactedRegistry.get(this)) + }, + [Equal.symbol](this: Redacted, that: unknown): boolean { + return ( + isRedacted(that) && + Equal.equals( + redacted.redactedRegistry.get(this), + redacted.redactedRegistry.get(that) + ) + ) + } +} + +/** + * Retrieves the original value from a `Redacted` instance. Use this function + * with caution, as it exposes the sensitive data. + * + * **When to use** + * + * Use when you need the underlying sensitive value at a trusted boundary. + * + * **Example** (Retrieving a redacted value) + * + * ```ts + * import { Redacted } from "effect" + * import * as assert from "node:assert" + * + * const API_KEY = Redacted.make("1234567890") + * + * assert.equal(Redacted.value(API_KEY), "1234567890") + * ``` + * + * @category getters + * @since 3.3.0 + */ +export const value: (self: Redacted) => T = redacted.value + +/** + * Deletes the stored value for a `Redacted` wrapper, making future + * `Redacted.value` calls on that wrapper fail. + * + * **When to use** + * + * Use when a `Redacted` wrapper should no longer be able to reveal its stored + * value. + * + * **Gotchas** + * + * This unsafe operation does not zero memory and does not affect other + * references to the original value. It only removes the value from the + * internal redacted registry. + * + * **Example** (Wiping a redacted value) + * + * ```ts + * import { Redacted } from "effect" + * import * as assert from "node:assert" + * + * const API_KEY = Redacted.make("1234567890") + * + * assert.equal(Redacted.value(API_KEY), "1234567890") + * + * Redacted.wipeUnsafe(API_KEY) + * + * assert.throws( + * () => Redacted.value(API_KEY), + * new Error("Unable to get redacted value") + * ) + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const wipeUnsafe = (self: Redacted): boolean => redacted.redactedRegistry.delete(self) + +/** + * Generates an equivalence relation for `Redacted` values based on an + * equivalence relation for the underlying values `A`. This function is useful + * for comparing `Redacted` instances without exposing their contents. + * + * **When to use** + * + * Use when you need to compare wrapped secrets through an approved equality + * rule without exposing the underlying values at each comparison site. + * + * **Example** (Comparing redacted values) + * + * ```ts + * import { Equivalence, Redacted } from "effect" + * import * as assert from "node:assert" + * + * const API_KEY1 = Redacted.make("1234567890") + * const API_KEY2 = Redacted.make("1-34567890") + * const API_KEY3 = Redacted.make("1234567890") + * + * const equivalence = Redacted.makeEquivalence(Equivalence.strictEqual()) + * + * assert.equal(equivalence(API_KEY1, API_KEY2), false) + * assert.equal(equivalence(API_KEY1, API_KEY3), true) + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = (isEquivalent: Equivalence.Equivalence): Equivalence.Equivalence> => + Equivalence.make((x, y) => isEquivalent(value(x), value(y))) diff --git a/repos/effect/packages/effect/src/Reducer.ts b/repos/effect/packages/effect/src/Reducer.ts new file mode 100644 index 0000000..cff3de0 --- /dev/null +++ b/repos/effect/packages/effect/src/Reducer.ts @@ -0,0 +1,178 @@ +/** + * Reusable strategies for reducing many values into one value. A `Reducer` + * extends `Combiner.Combiner` with an `initialValue` for empty collections and + * a `combineAll` method for folding an entire iterable. This module provides a + * constructor for reducers and a helper for reversing the order in which values + * are combined. + * + * @since 4.0.0 + */ + +import type * as Combiner from "./Combiner.ts" + +/** + * Represents a strategy for reducing a collection of values of type `A` into + * a single result. + * + * **When to use** + * + * Use when you need to fold/reduce a collection into a single value. + * - You want a reusable reducing strategy that can be passed to library + * functions like `Struct.makeReducer`, `Option.makeReducer`, or + * `Record.makeReducerUnion`. + * - You need both the combining logic *and* a known starting value. + * + * **Details** + * + * Extends {@link Combiner.Combiner} with: + * + * - `initialValue` – the identity/neutral element for `combine`. + * - `combineAll` – folds an entire `Iterable` from `initialValue`. + * + * Many modules ship pre-built reducers: + * + * - `Number.ReducerSum`, `Number.ReducerMultiply` + * - `String.ReducerConcat` + * - `Boolean.ReducerAnd`, `Boolean.ReducerOr` + * + * **Example** (String concatenation reducer) + * + * ```ts + * import { Reducer } from "effect" + * + * const Concat = Reducer.make((a, b) => a + b, "") + * + * console.log(Concat.combineAll(["hello", " ", "world"])) + * // Output: "hello world" + * ``` + * + * @see {@link make} – create a `Reducer` from a function and initial value + * @see {@link Combiner.Combiner} – parent interface without `initialValue` + * + * @category models + * @since 4.0.0 + */ +export interface Reducer extends Combiner.Combiner { + /** + * Neutral starting value (combining with this changes nothing). + * + * **When to use** + * + * Use to seed a reduction and represent the result of reducing an empty collection. + */ + readonly initialValue: A + + /** + * Combines all values in the collection, starting from `initialValue`. + * + * **When to use** + * + * Use to reduce an iterable with this reducer's initial value and combining operation. + */ + readonly combineAll: (collection: Iterable) => A +} + +/** + * Creates a `Reducer` from a `combine` function and an `initialValue`. + * + * **When to use** + * + * Use when you have a custom reducing operation not covered by a pre-built reducer. + * - You want to provide an optimized `combineAll` (e.g. short-circuiting on + * a known absorbing element like `0` for multiplication). + * + * **Details** + * + * - If `combineAll` is omitted, a default left-to-right fold starting from + * `initialValue` is used. + * - If `combineAll` is provided, it completely replaces the default fold. + * + * **Example** (Multiplying with short-circuit) + * + * ```ts + * import { Reducer } from "effect" + * + * const Product = Reducer.make( + * (a, b) => a * b, + * 1, + * (collection) => { + * let acc = 1 + * for (const n of collection) { + * if (n === 0) return 0 + * acc *= n + * } + * return acc + * } + * ) + * + * console.log(Product.combineAll([2, 3, 4])) + * // Output: 24 + * + * console.log(Product.combineAll([2, 0, 4])) + * // Output: 0 + * ``` + * + * @see {@link Reducer} – the interface this creates + * @see {@link flip} – reverse the argument order + * + * @category constructors + * @since 4.0.0 + */ +export function make( + combine: (self: A, that: A) => A, + initialValue: A, + combineAll?: (collection: Iterable) => A +): Reducer { + return { + combine, + initialValue, + combineAll: combineAll ?? + ((collection) => { + let out = initialValue + for (const value of collection) { + out = combine(out, value) + } + return out + }) + } +} + +/** + * Reverses the argument order of a reducer's `combine` method. + * + * **When to use** + * + * Use when you want the right-hand value to act as the accumulator, or need to + * reverse a non-commutative reducer such as string concatenation. + * + * **Details** + * + * - Returns a new `Reducer` where `combine(self, that)` calls the original + * reducer as `combine(that, self)`. + * - The `initialValue` is preserved from the original reducer. + * - The `combineAll` is re-derived from the flipped `combine` (using the + * default left-to-right fold), not carried over from the original. + * + * **Example** (Reversing string concatenation) + * + * ```ts + * import { Reducer, String } from "effect" + * + * const Prepend = Reducer.flip(String.ReducerConcat) + * + * console.log(Prepend.combine("a", "b")) + * // Output: "ba" + * + * console.log(Prepend.combineAll(["a", "b", "c"])) + * // Output: "cba" + * ``` + * + * @see {@link make} + * @see {@link Combiner.flip} – the same operation on a plain `Combiner` + * + * @category combinators + * @since 4.0.0 + */ +export function flip(reducer: Reducer): Reducer { + return make((self, that) => reducer.combine(that, self), reducer.initialValue) +} diff --git a/repos/effect/packages/effect/src/Ref.ts b/repos/effect/packages/effect/src/Ref.ts new file mode 100644 index 0000000..d356901 --- /dev/null +++ b/repos/effect/packages/effect/src/Ref.ts @@ -0,0 +1,801 @@ +/** + * Stores fiber-safe mutable state inside Effect programs. + * + * A `Ref` holds one value and exposes reads, writes, and atomic + * transformations as effects, so state changes compose with Effect's + * concurrency model. This module includes constructors, safe and unsafe reads, + * set and get-and-set helpers, update and modify helpers, and conditional + * update variants that leave the value unchanged when an `Option.none` result + * is returned. + * + * @since 2.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual, identity } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as MutableRef from "./MutableRef.ts" +import type * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Invariant } from "./Types.ts" + +const TypeId = "~effect/Ref" + +/** + * A mutable reference that provides atomic read, write, and update operations. + * + * **When to use** + * + * Use to keep shared mutable state that is read and updated inside Effect + * programs. + * + * **Details** + * + * A `Ref` is a thread-safe mutable reference type for shared state. It supports + * simple read and write operations as well as atomic transformations. + * + * **Example** (Reading and updating a ref) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a ref with initial value + * const counter = yield* Ref.make(0) + * + * // Read the current value + * const value = yield* Ref.get(counter) + * console.log(value) // 0 + * + * // Update the value atomically + * yield* Ref.update(counter, (n) => n + 1) + * + * // Read the updated value + * const newValue = yield* Ref.get(counter) + * console.log(newValue) // 1 + * }) + * ``` + * + * @see {@link make} for creating a `Ref` + * @see {@link get} for reading the current value + * @see {@link set} for replacing the current value + * + * @category models + * @since 2.0.0 + */ +export interface Ref extends Ref.Variance, Pipeable { + readonly ref: MutableRef.MutableRef +} + +/** + * The Ref namespace containing type definitions and utilities. + * + * **When to use** + * + * Use when referring to type members nested under the `Ref` namespace. + * + * @since 2.0.0 + */ +export declare namespace Ref { + /** + * Variance interface for Ref types, defining the type parameter constraints. + * + * **When to use** + * + * Use when working with the type-level variance marker carried by `Ref`. + * + * **Example** (Using invariant refs) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * // This interface defines the invariant nature of Ref's type parameter + * // A Ref is both a producer and consumer of A + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make(42) + * + * // Ref is invariant - it can both produce and consume numbers + * const value = yield* Ref.get(ref) // produces number + * yield* Ref.set(ref, value + 1) // consumes number + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Invariant + } + } +} + +const RefProto = { + [TypeId]: { + _A: identity + }, + ...PipeInspectableProto, + toJSON(this: Ref) { + return { + _id: "Ref", + ref: this.ref + } + } +} + +/** + * Creates a new Ref with the specified initial value (unsafe version). + * + * **When to use** + * + * Use when you need immediate synchronous construction and can guarantee + * that creating the `Ref` outside of `Effect` is safe. + * + * **Gotchas** + * + * Prefer `Ref.make` for Effect-wrapped creation in Effect programs. + * + * **Example** (Creating a ref unsafely) + * + * ```ts + * import { Ref } from "effect" + * + * // Create a ref directly without Effect + * const counter = Ref.makeUnsafe(0) + * + * // Get the current value + * const value = Ref.getUnsafe(counter) + * console.log(value) // 0 + * + * // Note: This is unsafe and should be used carefully + * // Prefer Ref.make for Effect-wrapped creation + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (value: A): Ref => { + const self = Object.create(RefProto) + self.ref = MutableRef.make(value) + return self +} + +/** + * Creates a new Ref with the specified initial value. + * + * **When to use** + * + * Use to create a `Ref` for shared mutable state inside an Effect program. + * + * **Example** (Creating a ref) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make(42) + * const value = yield* Ref.get(ref) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link makeUnsafe} for synchronous construction outside Effect code + * + * @category constructors + * @since 2.0.0 + */ +export const make = (value: A): Effect.Effect> => Effect.sync(() => makeUnsafe(value)) + +/** + * Gets the current value of the Ref. + * + * **When to use** + * + * Use to read the current `Ref` value without changing it. + * + * **Example** (Getting the current value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make(42) + * const value = yield* Ref.get(ref) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link set} for replacing the current value + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: Ref) => Effect.sync(() => self.ref.current) + +/** + * Sets the value of the Ref to the specified value. + * + * **When to use** + * + * Use to replace the current `Ref` value with a known value. + * + * **Example** (Setting a value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make(0) + * yield* Ref.set(ref, 42) + * const value = yield* Ref.get(ref) + * console.log(value) // 42 + * }) + * + * // Using multiple operations + * const program2 = Effect.gen(function*() { + * const ref = yield* Ref.make(0) + * yield* Ref.set(ref, 100) + * const value = yield* Ref.get(ref) + * console.log(value) // 100 + * }) + * ``` + * + * @see {@link getAndSet} for setting while returning the previous value + * @see {@link setAndGet} for setting while returning the new value + * + * @category setters + * @since 2.0.0 + */ +export const set = dual< + (value: A) => (self: Ref) => Effect.Effect, + (self: Ref, value: A) => Effect.Effect +>(2, (self: Ref, value: A) => Effect.sync(() => MutableRef.set(self.ref, value))) + +/** + * Gets the current value of the Ref, sets it to the specified value, and returns the previous value atomically. + * + * **When to use** + * + * Use to replace a plain `Ref` value while returning the previous value. + * + * **Example** (Replacing a value atomically) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make("initial") + * + * // Get current value and set new value atomically + * const previous = yield* Ref.getAndSet(ref, "updated") + * console.log(previous) // "initial" + * + * const current = yield* Ref.get(ref) + * console.log(current) // "updated" + * }) + * ``` + * + * @see {@link set} for setting without returning the previous value + * @see {@link getAndUpdate} for deriving the new value from the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndSet = dual< + (value: A) => (self: Ref) => Effect.Effect, + (self: Ref, value: A) => Effect.Effect +>(2, (self: Ref, value: A) => + Effect.sync(() => { + const current = self.ref.current + self.ref.current = value + return current + })) + +/** + * Gets the current value of the Ref, updates it with the given function, and returns the previous value atomically. + * + * **When to use** + * + * Use to derive a new `Ref` value while returning the previous value. + * + * **Example** (Updating and returning the previous value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(10) + * + * // Get current value and update it atomically + * const previous = yield* Ref.getAndUpdate(counter, (n) => n * 2) + * console.log(previous) // 10 + * + * const current = yield* Ref.get(counter) + * console.log(current) // 20 + * }) + * ``` + * + * @see {@link update} for updating without returning the previous value + * @see {@link updateAndGet} for returning the new value instead + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdate = dual< + (f: (a: A) => A) => (self: Ref) => Effect.Effect, + (self: Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref, f: (a: A) => A) => + Effect.sync(() => { + const current = self.ref.current + self.ref.current = f(current) + return current + })) + +/** + * Gets the current value of the Ref and updates it atomically with the given partial function. + * + * **When to use** + * + * Use to return the previous `Ref` value while applying a conditional update. + * + * **Details** + * + * If the partial function returns `Option.some`, the Ref is updated with the + * new value. If it returns `Option.none`, the Ref is left unchanged. The effect + * always returns the value that was in the Ref before the attempted update. + * + * **Example** (Conditionally updating and returning the previous value) + * + * ```ts + * import { Effect, Option, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * + * // Only update if value is greater than 3 + * const previous1 = yield* Ref.getAndUpdateSome( + * counter, + * (n) => n > 3 ? Option.some(n * 2) : Option.none() + * ) + * console.log(previous1) // 5 + * + * const current1 = yield* Ref.get(counter) + * console.log(current1) // 10 + * + * // Try to update again (won't update since 10 > 3 is true but let's say condition is n < 3) + * const previous2 = yield* Ref.getAndUpdateSome( + * counter, + * (n) => n < 3 ? Option.some(n * 2) : Option.none() + * ) + * console.log(previous2) // 10 + * + * const current2 = yield* Ref.get(counter) + * console.log(current2) // 10 (unchanged) + * }) + * ``` + * + * @see {@link getAndUpdate} for always applying an update + * @see {@link updateSome} for conditional updates without returning the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdateSome = dual< + (pf: (a: A) => Option.Option) => (self: Ref) => Effect.Effect, + (self: Ref, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref, pf: (a: A) => Option.Option) => + Effect.sync(() => { + const current = self.ref.current + const option = pf(current) + if (option._tag === "Some") { + self.ref.current = option.value + } + return current + })) + +/** + * Sets the value of the Ref atomically to the specified value and returns the new value. + * + * **When to use** + * + * Use when you want to set a `Ref` value and immediately get it back in one + * atomic operation. + * + * **Example** (Setting and returning the new value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* Ref.make(10) + * + * // Set new value and get it back in one operation + * const newValue = yield* Ref.setAndGet(ref, 42) + * console.log(newValue) // 42 + * + * // Verify the ref contains the new value + * const current = yield* Ref.get(ref) + * console.log(current) // 42 + * }) + * + * // Useful for sequential operations + * const program2 = Effect.gen(function*() { + * const counter = yield* Ref.make(0) + * + * const newValue = yield* Ref.setAndGet(counter, 20) + * console.log(newValue) // 20 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const setAndGet = dual< + (value: A) => (self: Ref) => Effect.Effect, + (self: Ref, value: A) => Effect.Effect +>(2, (self: Ref, value: A) => Effect.sync(() => self.ref.current = value)) + +/** + * Modifies the value of the Ref atomically using the given function. + * + * **When to use** + * + * Use to compute both a separate return value and the next stored `Ref` value + * in one atomic update. + * + * **Details** + * + * The function receives the current value and returns a tuple of + * `[result, newValue]`. The Ref is updated with `newValue`, and `result` is + * returned by the effect. + * + * **Example** (Modifying a value atomically) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(10) + * + * // Modify the ref and return some computation result + * const result = yield* Ref.modify(counter, (n) => [ + * `Previous value was ${n}`, // Return value + * n * 2 // New ref value + * ]) + * + * console.log(result) // "Previous value was 10" + * + * const current = yield* Ref.get(counter) + * console.log(current) // 20 + * }) + * + * // Example with more complex computation + * const program2 = Effect.gen(function*() { + * const state = yield* Ref.make({ count: 0, total: 0 }) + * + * const incremented = yield* Ref.modify(state, (s) => [ + * s.count, // Return previous count + * { count: s.count + 1, total: s.total + s.count + 1 } // New state + * ]) + * + * console.log(incremented) // 0 + * }) + * ``` + * + * @see {@link updateAndGet} for returning the new stored value + * @see {@link modifySome} for optionally updating while returning a separate result + * + * @category setters + * @since 2.0.0 + */ +export const modify = dual< + (f: (a: A) => readonly [B, A]) => (self: Ref) => Effect.Effect, + (self: Ref, f: (a: A) => readonly [B, A]) => Effect.Effect +>(2, (self, f) => + Effect.sync(() => { + const [b, a] = f(self.ref.current) + self.ref.current = a + return b + })) + +/** + * Computes a result atomically and optionally updates the value of the `Ref`. + * + * **When to use** + * + * Use to compute a return value while optionally updating a plain `Ref`. + * + * **Details** + * + * The callback receives the current value and returns `[result, nextValue]`, + * where `nextValue` is an `Option`. If `nextValue` is `Option.some(value)`, + * the `Ref` is updated to `value`; if it is `Option.none()`, the `Ref` is left + * unchanged. The returned effect always succeeds with `result`. + * + * **Example** (Conditionally modifying a value) + * + * ```ts + * import { Effect, Option, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * + * // Only modify if value is greater than 3 + * const result1 = yield* Ref.modifySome( + * counter, + * (n) => + * n > 3 + * ? [`incremented ${n}`, Option.some(n + 10)] + * : ["no change", Option.none()] + * ) + * + * console.log(result1) // "incremented 5" + * + * const current1 = yield* Ref.get(counter) + * console.log(current1) // 15 + * + * // Try to modify with a condition that fails + * const result2 = yield* Ref.modifySome( + * counter, + * (n) => + * n < 10 + * ? [`decremented ${n}`, Option.some(n - 5)] + * : ["no change", Option.none()] + * ) + * + * console.log(result2) // "no change" + * + * const current2 = yield* Ref.get(counter) + * console.log(current2) // 15 (unchanged) + * }) + * ``` + * + * @see {@link modify} for always storing a new value + * @see {@link updateSome} for optional updates without a separate return value + * + * @category setters + * @since 2.0.0 + */ +export const modifySome: { + (pf: (a: A) => readonly [B, Option.Option]): (self: Ref) => Effect.Effect + (self: Ref, pf: (a: A) => readonly [B, Option.Option]): Effect.Effect +} = dual< + ( + pf: (a: A) => readonly [B, Option.Option] + ) => (self: Ref) => Effect.Effect, + ( + self: Ref, + pf: (a: A) => readonly [B, Option.Option] + ) => Effect.Effect +>(2, (self, pf) => + modify(self, (value) => { + const [b, option] = pf(value) + return [b, option._tag === "None" ? value : option.value] + })) + +/** + * Updates the value of the Ref atomically using the given function. + * + * **When to use** + * + * Use to apply a `Ref` state transition without returning a value. + * + * **Example** (Updating a value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * + * // Update the value + * yield* Ref.update(counter, (n) => n * 2) + * + * const value = yield* Ref.get(counter) + * console.log(value) // 10 + * }) + * + * // Using multiple operations + * const program2 = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * yield* Ref.update(counter, (n: number) => n + 10) + * const value = yield* Ref.get(counter) + * console.log(value) // 15 + * }) + * ``` + * + * @see {@link updateAndGet} for returning the new value + * @see {@link getAndUpdate} for returning the previous value + * + * @category setters + * @since 2.0.0 + */ +export const update = dual< + (f: (a: A) => A) => (self: Ref) => Effect.Effect, + (self: Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref, f: (a: A) => A) => + Effect.sync(() => { + self.ref.current = f(self.ref.current) + })) + +/** + * Updates the value of the Ref atomically using the given function and returns the new value. + * + * **When to use** + * + * Use to apply a `Ref` state transition and return the new stored value. + * + * **Example** (Updating and returning the new value) + * + * ```ts + * import { Effect, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * + * // Update and get the new value in one operation + * const newValue = yield* Ref.updateAndGet(counter, (n) => n * 3) + * console.log(newValue) // 15 + * + * // Verify the ref contains the new value + * const current = yield* Ref.get(counter) + * console.log(current) // 15 + * }) + * ``` + * + * @see {@link update} for updating without returning the new value + * @see {@link getAndUpdate} for returning the previous value instead + * + * @category mutations + * @since 2.0.0 + */ +export const updateAndGet = dual< + (f: (a: A) => A) => (self: Ref) => Effect.Effect, + (self: Ref, f: (a: A) => A) => Effect.Effect +>(2, (self: Ref, f: (a: A) => A) => Effect.sync(() => self.ref.current = f(self.ref.current))) + +/** + * Updates the value of the Ref atomically using the given partial function. + * + * **When to use** + * + * Use to apply a conditional `Ref` update without returning a value. + * + * **Details** + * + * If the partial function returns `Option.some`, the Ref is updated with the + * new value. If it returns `Option.none`, the Ref is left unchanged. + * + * **Example** (Conditionally updating a value) + * + * ```ts + * import { Effect, Option, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(5) + * + * // Only update if value is even + * yield* Ref.updateSome( + * counter, + * (n) => n % 2 === 0 ? Option.some(n * 2) : Option.none() + * ) + * + * let current = yield* Ref.get(counter) + * console.log(current) // 5 (unchanged because 5 is odd) + * + * // Set to even number and try again + * yield* Ref.set(counter, 6) + * yield* Ref.updateSome( + * counter, + * (n) => n % 2 === 0 ? Option.some(n * 2) : Option.none() + * ) + * + * current = yield* Ref.get(counter) + * console.log(current) // 12 (updated because 6 is even) + * }) + * ``` + * + * @see {@link update} for always applying an update + * @see {@link updateSomeAndGet} for returning the resulting current value + * + * @category setters + * @since 2.0.0 + */ +export const updateSome = dual< + (f: (a: A) => Option.Option) => (self: Ref) => Effect.Effect, + (self: Ref, f: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref, f: (a: A) => Option.Option) => + Effect.sync(() => { + const option = f(self.ref.current) + if (option._tag === "Some") { + self.ref.current = option.value + } + })) + +/** + * Updates the value of the Ref atomically using the given partial function and returns the current value. + * + * **When to use** + * + * Use to apply a conditional `Ref` update and return the resulting current + * value. + * + * **Details** + * + * If the partial function returns `Option.some`, the Ref is updated with the + * new value. If it returns `Option.none`, the Ref is left unchanged. The effect + * returns the current value of the Ref after the potential update. + * + * **Example** (Conditionally updating and returning the current value) + * + * ```ts + * import { Effect, Option, Ref } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* Ref.make(10) + * + * // Only update if value is greater than 5 + * const result1 = yield* Ref.updateSomeAndGet( + * counter, + * (n) => n > 5 ? Option.some(n / 2) : Option.none() + * ) + * console.log(result1) // 5 (updated and returned) + * + * // Try to update again with same condition + * const result2 = yield* Ref.updateSomeAndGet( + * counter, + * (n) => n > 5 ? Option.some(n / 2) : Option.none() + * ) + * console.log(result2) // 5 (unchanged because 5 is not > 5) + * }) + * ``` + * + * @see {@link updateSome} for conditional updates without returning a value + * @see {@link updateAndGet} for always updating and returning the new value + * + * @category mutations + * @since 2.0.0 + */ +export const updateSomeAndGet = dual< + (pf: (a: A) => Option.Option) => (self: Ref) => Effect.Effect, + (self: Ref, pf: (a: A) => Option.Option) => Effect.Effect +>(2, (self: Ref, pf: (a: A) => Option.Option) => + Effect.sync(() => { + const option = pf(self.ref.current) + if (option._tag === "Some") { + self.ref.current = option.value + } + return self.ref.current + })) + +/** + * Gets the current value of the Ref synchronously (unsafe version). + * + * **When to use** + * + * Use when you need immediate synchronous access and can guarantee that + * reading the `Ref` outside of `Effect` is safe. + * + * **Gotchas** + * + * Prefer `Ref.get` for Effect-wrapped access in Effect programs. + * + * **Example** (Reading a ref unsafely) + * + * ```ts + * import { Ref } from "effect" + * + * // Create a ref directly + * const counter = Ref.makeUnsafe(42) + * + * // Get the value synchronously + * const value = Ref.getUnsafe(counter) + * console.log(value) // 42 + * + * // Note: This is unsafe and should be used carefully + * // Prefer Ref.get for Effect-wrapped access + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const getUnsafe = (self: Ref): A => self.ref.current diff --git a/repos/effect/packages/effect/src/References.ts b/repos/effect/packages/effect/src/References.ts new file mode 100644 index 0000000..bc7926e --- /dev/null +++ b/repos/effect/packages/effect/src/References.ts @@ -0,0 +1,786 @@ +/** + * The `References` module exposes the built-in `Context.Reference` keys that + * the Effect runtime consults for execution settings and diagnostic metadata. + * These references cover concurrency, scheduling, logging, tracing, and + * low-level diagnostic state. + * + * A `Context.Reference` is a service key with a default value. Reading one + * of these references returns the value from the current fiber context, and + * providing a new value changes behavior for the provided effect and the fibers + * it starts. + * + * @since 4.0.0 + */ +import type * as Context from "./Context.ts" +import * as internalEffect from "./internal/effect.ts" +import * as references from "./internal/references.ts" +import type { Logger } from "./Logger.ts" +import type { LogLevel, Severity } from "./LogLevel.ts" +import type { ReadonlyRecord } from "./Record.ts" +import { MaxOpsBeforeYield, PreventSchedulerYield } from "./Scheduler.ts" +import { CurrentTraceLevel, DisablePropagation, MinimumTraceLevel, type SpanLink, Tracer } from "./Tracer.ts" + +export { + /** + * Context reference for the current trace level used for dynamic trace filtering. + * + * **When to use** + * + * Use to set the default trace level for spans created in a scope when span + * options do not provide `level`. + * + * @see {@link MinimumTraceLevel} for configuring the threshold that decides whether spans at a given level are sampled or exported + * + * @category references + * @since 4.0.0 + */ + CurrentTraceLevel, + /** + * Context reference for disabling trace propagation in the current context. + * + * **When to use** + * + * Use to mark tracing work as non-propagating while still allowing local span + * tracking. + * + * **Details** + * + * Annotated spans become non-propagating no-op spans, and parent selection + * skips spans marked with disabled propagation. + * + * @see {@link TracerEnabled} for disabling span registration instead of only propagation + * + * @category references + * @since 4.0.0 + */ + DisablePropagation, + /** + * Context reference for the maximum operation budget before a fiber yields to the scheduler. + * + * **When to use** + * + * Use to configure the runtime reference for the fiber operation budget that + * triggers a scheduler yield. + * + * **Details** + * + * The default value is `2048` operations. + * + * @see {@link PreventSchedulerYield} for bypassing scheduler yield checks instead of changing the operation budget + * + * @category references + * @since 4.0.0 + */ + MaxOpsBeforeYield, + /** + * Context reference for the minimum trace level threshold for span sampling. + * + * **When to use** + * + * Use to set the trace-level threshold that decides whether newly created + * spans are sampled and exported. + * + * @see {@link CurrentTraceLevel} for setting the level assigned to spans before this threshold is applied + * + * @category references + * @since 4.0.0 + */ + MinimumTraceLevel, + /** + * Context reference for whether the runtime bypasses scheduler yield checks. + * + * **When to use** + * + * Use to bypass automatic scheduler yield checks in a controlled runtime scope + * where throughput is preferred over scheduler fairness. + * + * **Details** + * + * When set to `true`, the fiber run loop skips `Scheduler.shouldYield`. The + * default value is `false`. + * + * **Gotchas** + * + * Disabling automatic yield checks can let long-running fibers monopolize the + * JavaScript thread. + * + * @see {@link MaxOpsBeforeYield} for tuning the operation budget while keeping scheduler yield checks enabled + * + * @category references + * @since 4.0.0 + */ + PreventSchedulerYield, + /** + * Context reference for the active tracer service used to create spans. + * + * **When to use** + * + * Use to access or override the active tracer service through the references + * module when working directly with Effect runtime references. + * + * @category references + * @since 4.0.0 + */ + Tracer +} + +/** + * Context reference for controlling the current concurrency limit. Can be set to "unbounded" + * for unlimited concurrency or a specific number to limit concurrent operations. + * + * **When to use** + * + * Use to configure the default concurrency limit for operations that read + * concurrency from the current context. + * + * **Example** (Setting current concurrency) + * + * ```ts + * import { Effect, References } from "effect" + * + * const limitConcurrency = Effect.gen(function*() { + * // Get current setting + * const current = yield* References.CurrentConcurrency + * console.log(current) // "unbounded" (default) + * + * // Run with limited concurrency + * yield* Effect.provideService( + * Effect.gen(function*() { + * const limited = yield* References.CurrentConcurrency + * console.log(limited) // 10 + * }), + * References.CurrentConcurrency, + * 10 + * ) + * + * // Run with unlimited concurrency + * yield* Effect.provideService( + * Effect.gen(function*() { + * const unlimited = yield* References.CurrentConcurrency + * console.log(unlimited) // "unbounded" + * }), + * References.CurrentConcurrency, + * "unbounded" + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentConcurrency: Context.Reference = references.CurrentConcurrency + +/** + * Context reference for managing log annotations that are automatically added to all log entries. + * These annotations provide contextual metadata that appears in every log message. + * + * **When to use** + * + * Use to attach shared contextual metadata to every log entry emitted in the + * current context. + * + * **Example** (Managing log annotations) + * + * ```ts + * import { Console, Effect, References } from "effect" + * + * const logAnnotationExample = Effect.gen(function*() { + * // Get current annotations (empty by default) + * const current = yield* References.CurrentLogAnnotations + * console.log(current) // {} + * + * // Run with custom log annotations + * yield* Effect.provideService( + * Effect.gen(function*() { + * const annotations = yield* References.CurrentLogAnnotations + * console.log(annotations) // { requestId: "req-123", userId: "user-456", version: "1.0.0" } + * + * // All log entries will include these annotations + * yield* Console.log("Starting operation") + * yield* Console.info("Processing data") + * }), + * References.CurrentLogAnnotations, + * { + * requestId: "req-123", + * userId: "user-456", + * version: "1.0.0" + * } + * ) + * + * // Run with extended annotations + * yield* Effect.provideService( + * Effect.gen(function*() { + * const extended = yield* References.CurrentLogAnnotations + * console.log(extended) // { requestId: "req-123", userId: "user-456", version: "1.0.0", operation: "data-sync", timestamp: 1234567890 } + * + * yield* Console.log("Operation completed with extended context") + * }), + * References.CurrentLogAnnotations, + * { + * requestId: "req-123", + * userId: "user-456", + * version: "1.0.0", + * operation: "data-sync", + * timestamp: 1234567890 + * } + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentLogAnnotations: Context.Reference> = + references.CurrentLogAnnotations + +/** + * Context reference for the current log severity used by `Effect.log` when no explicit + * level is provided. + * + * **When to use** + * + * Use to set the default severity for `Effect.log` entries that do not provide + * an explicit level. + * + * **Details** + * + * Use `MinimumLogLevel` to control which log entries are filtered out. + * + * **Example** (Changing the current log level) + * + * ```ts + * import { Console, Effect, References } from "effect" + * + * const dynamicLogging = Effect.gen(function*() { + * // Get current log level (default is "Info") + * const current = yield* References.CurrentLogLevel + * console.log(current) // "Info" + * + * // Set log level to Debug for detailed logging + * yield* Effect.provideService( + * Effect.gen(function*() { + * const level = yield* References.CurrentLogLevel + * console.log(level) // "Debug" + * yield* Console.debug("This debug message will be shown") + * }), + * References.CurrentLogLevel, + * "Debug" + * ) + * + * // Change to Error level to reduce noise + * yield* Effect.provideService( + * Effect.gen(function*() { + * const level = yield* References.CurrentLogLevel + * console.log(level) // "Error" + * yield* Console.info("This info message will be filtered out") + * yield* Console.error("This error message will be shown") + * }), + * References.CurrentLogLevel, + * "Error" + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentLogLevel: Context.Reference = references.CurrentLogLevel + +/** + * Context reference for managing log spans that track the duration and hierarchy of operations. + * Each span represents a labeled time period for performance analysis and debugging. + * + * **When to use** + * + * Use to carry the active log span stack that should be included with log + * entries in the current context. + * + * **Example** (Tracking log spans) + * + * ```ts + * import { Console, Effect, References } from "effect" + * + * const logSpanExample = Effect.gen(function*() { + * // Get current spans (empty by default) + * const current = yield* References.CurrentLogSpans + * console.log(current.length) // 0 + * + * // Add a log span manually + * const databaseConnectionStartedAt = 0 + * yield* Effect.provideService( + * Effect.gen(function*() { + * // Simulate some work + * yield* Effect.sleep("100 millis") + * yield* Console.log("Database operation in progress") + * + * const spans = yield* References.CurrentLogSpans + * console.log("Active spans:", spans.map(([label]) => label)) // ["database-connection"] + * }), + * References.CurrentLogSpans, + * [["database-connection", databaseConnectionStartedAt]] + * ) + * + * // Add another span + * const dataProcessingStartedAt = 100 + * yield* Effect.provideService( + * Effect.gen(function*() { + * const spans = yield* References.CurrentLogSpans + * console.log("Active spans:", spans.map(([label]) => label)) // ["database-connection", "data-processing"] + * + * yield* Console.log("Multiple operations in progress") + * }), + * References.CurrentLogSpans, + * [ + * ["database-connection", databaseConnectionStartedAt], + * ["data-processing", dataProcessingStartedAt] + * ] + * ) + * + * // Clear spans when operations complete + * yield* Effect.provideService( + * Effect.gen(function*() { + * const spans = yield* References.CurrentLogSpans + * console.log("Active spans:", spans.length) // 0 + * }), + * References.CurrentLogSpans, + * [] + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const CurrentLogSpans: Context.Reference> = + references.CurrentLogSpans + +/** + * Context reference for the current captured stack-frame chain for the running + * fiber. + * + * **When to use** + * + * Use when writing low-level tracing or diagnostic integrations that need direct + * access to the stack-frame chain carried by the current fiber. + * + * **Details** + * + * Effect and Layer tracing use this reference to attach stack-frame information + * to failures and interruption causes. It is normally managed by tracing APIs + * rather than provided directly by application code. + * + * @see {@link StackFrame} for the frame node stored in this reference + * + * @category references + * @since 4.0.0 + */ +export const CurrentStackFrame: Context.Reference = references.CurrentStackFrame + +/** + * Context reference for setting the minimum log level threshold. Log entries below this + * level will be filtered out completely. + * + * **When to use** + * + * Use to filter out log entries below a severity threshold. + * + * **Example** (Setting the minimum log level) + * + * ```ts + * import { Console, Effect, References } from "effect" + * + * const configureMinimumLogging = Effect.gen(function*() { + * // Get current minimum level (default is "Info") + * const current = yield* References.MinimumLogLevel + * console.log(current) // "Info" + * + * // Set minimum level to Warn - Debug and Info will be filtered + * yield* Effect.provideService( + * Effect.gen(function*() { + * const minLevel = yield* References.MinimumLogLevel + * console.log(minLevel) // "Warn" + * + * // These won't be processed at all + * yield* Console.debug("Debug message") // Filtered out + * yield* Console.info("Info message") // Filtered out + * + * // These will be processed + * yield* Console.warn("Warning message") // Shown + * yield* Console.error("Error message") // Shown + * }), + * References.MinimumLogLevel, + * "Warn" + * ) + * + * // Reset to default Info level + * yield* Effect.provideService( + * Effect.gen(function*() { + * const minLevel = yield* References.MinimumLogLevel + * console.log(minLevel) // "Info" + * + * // Now info messages will be processed + * yield* Console.info("Info message") // Shown + * }), + * References.MinimumLogLevel, + * "Info" + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const MinimumLogLevel: Context.Reference = references.MinimumLogLevel + +/** + * Context reference for controlling whether tracing is enabled globally. When set to false, + * spans will not be registered with the tracer and tracing overhead is minimized. + * + * **When to use** + * + * Use to disable or re-enable span registration in the current context. + * + * **Example** (Toggling tracing) + * + * ```ts + * import { Effect, References } from "effect" + * + * const tracingControl = Effect.gen(function*() { + * // Check if tracing is enabled (default is true) + * const current = yield* References.TracerEnabled + * console.log(current) // true + * + * // Disable tracing globally + * yield* Effect.provideService( + * Effect.gen(function*() { + * const isEnabled = yield* References.TracerEnabled + * console.log(isEnabled) // false + * + * // Spans will not be traced in this context + * yield* Effect.log("This will not be traced") + * }), + * References.TracerEnabled, + * false + * ) + * + * // Re-enable tracing + * yield* Effect.provideService( + * Effect.gen(function*() { + * const isEnabled = yield* References.TracerEnabled + * console.log(isEnabled) // true + * + * // All subsequent spans will be traced + * yield* Effect.log("This will be traced") + * }), + * References.TracerEnabled, + * true + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const TracerEnabled: Context.Reference = references.TracerEnabled + +/** + * Context reference for managing span annotations that are automatically added to all new spans. + * These annotations provide context and metadata that applies across multiple spans. + * + * **When to use** + * + * Use to attach shared metadata to every span created in the current context. + * + * **Example** (Managing span annotations) + * + * ```ts + * import { Effect, References } from "effect" + * + * const spanAnnotationExample = Effect.gen(function*() { + * // Get current annotations (empty by default) + * const current = yield* References.TracerSpanAnnotations + * console.log(current) // {} + * + * // Set global span annotations + * yield* Effect.provideService( + * Effect.gen(function*() { + * // Get current annotations + * const annotations = yield* References.TracerSpanAnnotations + * console.log(annotations) // { service: "user-service", version: "1.2.3", environment: "production" } + * + * // All spans created will include these annotations + * yield* Effect.gen(function*() { + * // Add more specific annotations for this span + * yield* Effect.annotateCurrentSpan("userId", "123") + * yield* Effect.log("Processing user") + * }) + * }), + * References.TracerSpanAnnotations, + * { + * service: "user-service", + * version: "1.2.3", + * environment: "production" + * } + * ) + * + * // Clear annotations + * yield* Effect.provideService( + * Effect.gen(function*() { + * const annotations = yield* References.TracerSpanAnnotations + * console.log(annotations) // {} + * }), + * References.TracerSpanAnnotations, + * {} + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const TracerSpanAnnotations: Context.Reference> = + references.TracerSpanAnnotations + +/** + * Context reference for managing span links that are automatically added to all new spans. + * Span links connect related spans that are not in a parent-child relationship. + * + * **When to use** + * + * Use to attach shared links to every span created in the current context. + * + * **Example** (Managing span links) + * + * ```ts + * import { Effect, References, Tracer } from "effect" + * + * const spanLinksExample = Effect.gen(function*() { + * // Get current links (empty by default) + * const current = yield* References.TracerSpanLinks + * console.log(current.length) // 0 + * + * // Create an external span for the example + * const externalSpan = Tracer.externalSpan({ + * spanId: "external-span-123", + * traceId: "trace-456" + * }) + * + * // Create span links + * const spanLink: Tracer.SpanLink = { + * span: externalSpan, + * attributes: { + * relationship: "follows-from", + * priority: "high" + * } + * } + * + * // Set global span links + * yield* Effect.provideService( + * Effect.gen(function*() { + * // Get current links + * const links = yield* References.TracerSpanLinks + * console.log(links.length) // 1 + * + * // All new spans will include these links + * yield* Effect.gen(function*() { + * yield* Effect.log("This span will have linked spans") + * return "operation complete" + * }) + * }), + * References.TracerSpanLinks, + * [spanLink] + * ) + * + * // Clear links + * yield* Effect.provideService( + * Effect.gen(function*() { + * const links = yield* References.TracerSpanLinks + * console.log(links.length) // 0 + * }), + * References.TracerSpanLinks, + * [] + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const TracerSpanLinks: Context.Reference> = references.TracerSpanLinks + +/** + * Context reference for controlling whether trace timing is enabled globally. When set + * to false, spans will not contain timing information (trace time will always + * be set to zero). + * + * **When to use** + * + * Use to disable or re-enable timing capture for spans in the current context. + * + * **Example** (Toggling trace timing) + * + * ```ts + * import { Effect, References } from "effect" + * + * const tracingControl = Effect.gen(function*() { + * // Check if trace timing is enabled (default is true) + * const current = yield* References.TracerTimingEnabled + * console.log(current) // true + * + * // Disable trace timing globally + * yield* Effect.provideService( + * Effect.gen(function*() { + * // Spans will not having timing information in this context + * const isEnabled = yield* References.TracerTimingEnabled + * console.log(isEnabled) // false + * }), + * References.TracerTimingEnabled, + * false + * ) + * + * // Re-enable trace timing + * yield* Effect.provideService( + * Effect.gen(function*() { + * // Spans will have timing information in this context + * const isEnabled = yield* References.TracerTimingEnabled + * console.log(isEnabled) // true + * }), + * References.TracerTimingEnabled, + * true + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ +export const TracerTimingEnabled: Context.Reference = references.TracerTimingEnabled + +/** + * Context reference for the log severity used when a pool finalizer reports an + * unhandled error. + * + * **When to use** + * + * Use to choose whether and at which severity pool finalizer failures are + * reported. + * + * **Details** + * + * The default level is `"Error"`. + * + * **Gotchas** + * + * Providing `undefined` suppresses this report; it does not fall back to + * `CurrentLogLevel`. + * + * @see {@link CurrentLogLevel} for the default severity used by ordinary `Effect.log` calls + * @see {@link MinimumLogLevel} for filtering emitted log entries by threshold + * + * @category references + * @since 4.0.0 + */ +export const UnhandledLogLevel: Context.Reference = references.UnhandledLogLevel + +/** + * A captured stack-frame node used to describe the traced execution path. + * + * **When to use** + * + * Use when reading or supplying the stack-frame chain that Effect tracing uses + * to attach diagnostic call-site information to failures and interruptions. + * + * **Details** + * + * Each frame has a span or operation `name`, a lazy `stack` supplier, and an + * optional `parent` frame that links it to the previous captured frame. + * + * @see {@link CurrentStackFrame} for the fiber reference carrying the active stack-frame chain + * + * @category references + * @since 4.0.0 + */ +export interface StackFrame { + readonly name: string + readonly stack: () => string | undefined + readonly parent: StackFrame | undefined +} + +/** + * Context reference for the set of loggers currently used by Effect logging + * operations. + * + * **When to use** + * + * Use to inspect or provide the complete set of loggers used by Effect logging + * in the current context. + * + * **Details** + * + * The default set contains the built-in default logger and tracer logger. + * Providing this reference changes which `Logger` instances receive log entries + * in the current context. + * + * @category references + * @since 4.0.0 + */ +export const CurrentLoggers: Context.Reference>> = internalEffect.CurrentLoggers + +/** + * Context reference for controlling whether built-in console loggers write to stderr. + * + * **When to use** + * + * Use to configure the runtime reference that controls whether built-in console + * loggers write to stderr. + * + * **Details** + * + * The default value is `false`. When set to `true`, the built-in default logger + * and TTY pretty console logger call `console.error` instead of `console.log`. + * + * @category references + * @since 4.0.0 + */ +export const LogToStderr: Context.Reference = internalEffect.LogToStderr + +export { + /** + * Context reference for the current scheduler implementation used by the Effect runtime. + * Controls how Effects are scheduled and executed. + * + * **When to use** + * + * Use to provide the scheduler implementation that fibers use in the current + * context. + * + * **Example** (Providing a custom scheduler) + * + * ```ts + * import { Effect, References, Scheduler } from "effect" + * + * const customScheduling = Effect.gen(function*() { + * // Get current scheduler (default is MixedScheduler) + * const current = yield* References.Scheduler + * console.log(current) // MixedScheduler instance + * + * // Use a custom scheduler + * yield* Effect.provideService( + * Effect.gen(function*() { + * const scheduler = yield* References.Scheduler + * console.log(scheduler) // Custom scheduler instance + * + * // Effects will use the custom scheduler in this context + * yield* Effect.log("Using custom scheduler") + * }), + * References.Scheduler, + * new Scheduler.MixedScheduler() + * ) + * }) + * ``` + * + * @category references + * @since 4.0.0 + */ + Scheduler +} from "./Scheduler.ts" diff --git a/repos/effect/packages/effect/src/RegExp.ts b/repos/effect/packages/effect/src/RegExp.ts new file mode 100644 index 0000000..0123914 --- /dev/null +++ b/repos/effect/packages/effect/src/RegExp.ts @@ -0,0 +1,82 @@ +/** + * Tools for working with JavaScript regular expressions from the Effect module + * namespace. The module exposes the native `RegExp` constructor, a guard for + * narrowing unknown values, and escaping for literal text that will be embedded + * in a pattern. + * + * Reach for `RegExp` when you need to build a regular expression from user or + * data-driven text, check whether an unknown value is already a `RegExp`, or + * access the native constructor without leaving the Effect namespace. + * + * @since 2.0.0 + */ +import * as predicate from "./Predicate.ts" + +/** + * Exposes the JavaScript regular expression constructor from `globalThis`. + * + * **When to use** + * + * Use to construct JavaScript regular expressions through the Effect module + * namespace. + * + * **Example** (Creating a regular expression) + * + * ```ts + * import { RegExp } from "effect" + * + * // Create a regular expression using Effect's RegExp constructor + * const pattern = new RegExp.RegExp("hello", "i") + * + * // Test the pattern + * console.log(pattern.test("Hello World")) // true + * console.log(pattern.test("goodbye")) // false + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const RegExp = globalThis.RegExp + +/** + * Checks whether a value is a `RegExp`. + * + * **When to use** + * + * Use to validate unknown input before treating it as a regular expression. + * + * **Example** (Checking for regular expressions) + * + * ```ts + * import { RegExp } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(RegExp.isRegExp(/a/), true) + * assert.deepStrictEqual(RegExp.isRegExp("a"), false) + * ``` + * + * @category guards + * @since 3.9.0 + */ +export const isRegExp: (input: unknown) => input is RegExp = predicate.isRegExp + +/** + * Escapes special characters in a regular expression pattern. + * + * **When to use** + * + * Use to turn literal text into a safe regular expression pattern fragment. + * + * **Example** (Escaping a pattern string) + * + * ```ts + * import { RegExp } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(RegExp.escape("a*b"), "a\\*b") + * ``` + * + * @category RegExp + * @since 2.0.0 + */ +export const escape = (string: string): string => string.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&") diff --git a/repos/effect/packages/effect/src/Request.ts b/repos/effect/packages/effect/src/Request.ts new file mode 100644 index 0000000..5eb39ff --- /dev/null +++ b/repos/effect/packages/effect/src/Request.ts @@ -0,0 +1,598 @@ +/** + * Typed request values for data loading with `Effect.request`. + * + * A request describes one logical piece of work without performing it. It + * records the success type, typed error, service requirements, and fields a + * resolver needs to complete the request. Requests are paired with + * `RequestResolver`, which performs backend-specific loading and completes each + * pending request entry with a success, failure, cause, exit, or effect. + * + * @since 2.0.0 + */ +import type * as Cause from "./Cause.ts" +import type * as Context from "./Context.ts" +import type * as Effect from "./Effect.ts" +import * as Equal from "./Equal.ts" +import type * as Exit from "./Exit.ts" +import { dual } from "./Function.ts" +import * as core from "./internal/core.ts" +import * as internalEffect from "./internal/effect.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Types from "./Types.ts" + +const TypeId = "~effect/Request" + +/** + * A `Request` is a request from a data source for a value of type `A` + * that may fail with an `E` and have requirements of type `R`. + * + * **Example** (Defining typed requests) + * + * ```ts + * import type { Request } from "effect" + * + * // Define a request that fetches a user by ID + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * + * // Define a request that fetches all users + * interface GetAllUsers extends Request.Request, Error> { + * readonly _tag: "GetAllUsers" + * } + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Request extends Variance {} + +/** + * Alias for any `Request`, regardless of its success, error, or service + * requirements. + * + * **When to use** + * + * Use as a generic constraint for APIs that accept any request while preserving + * each concrete request's success, error, and service types. + * + * @see {@link Request} for the request interface + * @see {@link Success} for extracting a request's success type + * @see {@link Error} for extracting a request's error type + * @see {@link Services} for extracting a request's service requirements + * @see {@link Result} for the exit type produced by completing a request + * + * @category models + * @since 4.0.0 + */ +export type Any = Request + +/** + * Variance marker carried by every `Request`. + * + * **Details** + * + * This marker preserves the success, error, and service requirement types for + * Effect's type-level machinery. Users normally get it by extending `Request`. + * + * @category models + * @since 2.0.0 + */ +export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + readonly _R: Types.Covariant + } +} + +/** + * The constructor type returned by `Request.of` and `Request.tagged`. + * + * **Details** + * + * The constructor accepts the request's data fields, excluding request variance + * fields and any fields already supplied by the constructor such as `_tag`, and + * returns a value of the request type. + * + * **Example** (Using generated request constructors) + * + * ```ts + * import { Request } from "effect" + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * + * // Constructor type is used internally by Request.of() and Request.tagged() + * const GetUser = Request.tagged("GetUser") + * const userRequest = GetUser({ id: 123 }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Constructor, T extends keyof R = never> { + (args: Types.VoidIfEmpty)>>>): R +} + +/** + * A utility type to extract the error type from a `Request`. + * + * **Example** (Extracting a request error type) + * + * ```ts + * import type { Request } from "effect" + * + * interface GetUser extends Request.Request { + * readonly id: number + * } + * + * // Extract the error type from a Request using the utility + * type UserError = Request.Error // Error + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export type Error> = [T] extends [Request] ? _E : never + +/** + * A utility type to extract the value type from a `Request`. + * + * **Example** (Extracting a request success type) + * + * ```ts + * import type { Request } from "effect" + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * + * // Extract the success type from a Request using the utility + * type UserSuccess = Request.Success // string + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export type Success> = [T] extends [Request] ? _A + : never + +/** + * A utility type to extract the requirements type from a `Request`. + * + * @category utility types + * @since 4.0.0 + */ +export type Services> = [T] extends [Request] ? _R + : never + +/** + * A utility type to extract the result type from a `Request`. + * + * **Example** (Extracting a request result type) + * + * ```ts + * import type { Request } from "effect" + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * + * // Extract the result type from a Request using the utility + * type UserResult = Request.Result // Exit.Exit + * ``` + * + * @category utility types + * @since 2.0.0 + */ +export type Result> = T extends Request ? Exit.Exit + : never + +const requestVariance = Equal.byReferenceUnsafe({ + /* c8 ignore next */ + _E: (_: never) => _, + /* c8 ignore next */ + _A: (_: never) => _, + /* c8 ignore next */ + _R: (_: never) => _ +}) + +/** + * Prototype used by Effect's request constructors. + * + * **Details** + * + * This low-level value provides the structural request marker for values + * created by `Request.of`, `Request.tagged`, `Request.Class`, and + * `Request.TaggedClass`. Most users should use those constructors instead of + * interacting with the prototype directly. + * + * @category prototypes + * @since 4.0.0 + */ +export const RequestPrototype: Request = { + ...core.StructuralProto, + [TypeId]: requestVariance +} + +/** + * Checks whether a value is a `Request`. + * + * **Example** (Checking request values) + * + * ```ts + * import { Request } from "effect" + * + * declare const User: unique symbol + * declare const UserNotFound: unique symbol + * type User = typeof User + * type UserNotFound = typeof UserNotFound + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: string + * } + * const GetUser = Request.tagged("GetUser") + * + * const request = GetUser({ id: "123" }) + * console.log(Request.isRequest(request)) // true + * console.log(Request.isRequest("not a request")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isRequest = (u: unknown): u is Request => hasProperty(u, TypeId) + +/** + * Creates a constructor function for a specific Request type. + * + * **Example** (Creating untagged request constructors) + * + * ```ts + * import { Request } from "effect" + * + * declare const UserProfile: unique symbol + * declare const ProfileError: unique symbol + * type UserProfile = typeof UserProfile + * type ProfileError = typeof ProfileError + * + * interface GetUserProfile extends Request.Request { + * readonly id: string + * readonly includeSettings: boolean + * } + * + * const GetUserProfile = Request.of() + * + * const request = GetUserProfile({ + * id: "user-123", + * includeSettings: true + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const of = >(): Constructor => (args) => + Object.assign(Object.create(RequestPrototype), args) + +/** + * Creates a constructor function for a tagged Request type. The tag is automatically + * added to the request, making it useful for discriminated unions. + * + * **Example** (Creating tagged request constructors) + * + * ```ts + * import { Request } from "effect" + * + * declare const User: unique symbol + * declare const UserNotFound: unique symbol + * declare const Post: unique symbol + * declare const PostNotFound: unique symbol + * type User = typeof User + * type UserNotFound = typeof UserNotFound + * type Post = typeof Post + * type PostNotFound = typeof PostNotFound + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: string + * } + * + * interface GetPost extends Request.Request { + * readonly _tag: "GetPost" + * readonly id: string + * } + * + * const GetUser = Request.tagged("GetUser") + * const GetPost = Request.tagged("GetPost") + * + * const userRequest = GetUser({ id: "user-123" }) + * const postRequest = GetPost({ id: "post-456" }) + * + * // _tag is automatically set + * console.log(userRequest._tag) // "GetUser" + * console.log(postRequest._tag) // "GetPost" + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const tagged = & { _tag: string }>( + tag: R["_tag"] +): Constructor => +(args) => { + const request = Object.create(RequestPrototype) + if (args) Object.assign(request, args) + request._tag = tag + return request +} + +/** + * Defines request types with TypeScript classes. + * + * **Details** + * + * Subclasses pass their data fields to `super`, and instances are marked as + * `Request` values while retaining the provided readonly fields. + * + * **Example** (Defining request classes) + * + * ```ts + * import { Request } from "effect" + * + * class GetUser extends Request.Class<{ id: number }, string, Error> { + * constructor(readonly id: number) { + * super({ id }) + * } + * } + * + * const getUserRequest = new GetUser(123) + * console.log(getUserRequest.id) // 123 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const Class: new, Success, Error = never, Context = never>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends keyof Request ? never : P]: A[P] } +) => Request & Readonly = (function() { + function Class(this: any, args: any) { + if (args) { + Object.assign(this, args) + } + } + Class.prototype = RequestPrototype + return Class as any +})() + +/** + * Creates a class constructor for requests with a fixed `_tag` field. + * + * **Details** + * + * Use this when defining class-based request types that should participate in + * tagged unions or tag-based request resolvers. + * + * **Example** (Defining tagged request classes) + * + * ```ts + * import { Request } from "effect" + * + * class GetUserById + * extends Request.TaggedClass("GetUserById")<{ id: number }, string, Error> + * {} + * + * const request = new GetUserById({ id: 123 }) + * console.log(request._tag) // "GetUserById" + * console.log(request.id) // 123 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const TaggedClass = ( + tag: Tag +): new, Success, Error = never, Services = never>( + args: Types.Equals>, {}> extends true ? void + : { readonly [P in keyof A as P extends "_tag" | keyof Request ? never : P]: A[P] } +) => Request & Readonly & { readonly _tag: Tag } => { + return class TaggedClass extends Class { + readonly _tag = tag + } as any +} + +/** + * Completes a request entry with the provided result. + * + * **When to use** + * + * Use when you need to finish a `Request.Entry` with a prebuilt final `Exit` + * result. + * + * @see {@link completeEffect} for completing an entry from an effect that may succeed or fail + * @see {@link succeed} for completing an entry with a successful value + * @see {@link fail} for completing an entry with a typed failure + * @see {@link failCause} for completing an entry with a failure `Cause` + * + * @category completion + * @since 2.0.0 + */ +export const complete: { + (result: Result): (self: Entry) => Effect.Effect + (self: Entry, result: Result): Effect.Effect +} = dual( + 2, + (self: Entry, result: Result): Effect.Effect => + internalEffect.sync(() => self.completeUnsafe(result)) +) + +/** + * Completes a request entry with the result of an effect. + * + * **When to use** + * + * Use to finish a `Request.Entry` by running an effect whose success or typed + * failure should become the request result. + * + * **Details** + * + * If the effect succeeds, the entry is completed successfully with its value. + * If the effect fails, the entry is completed with that failure. + * + * **Gotchas** + * + * The returned effect itself does not fail with the request error. + * + * @see {@link complete} for completing an entry with a prebuilt `Exit` + * @see {@link succeed} for completing an entry with a successful value + * @see {@link fail} for completing an entry with a typed failure + * @see {@link failCause} for completing an entry with a failure `Cause` + * + * @category completion + * @since 2.0.0 + */ +export const completeEffect: { + (effect: Effect.Effect, Error, R>): (self: Entry) => Effect.Effect + (self: Entry, effect: Effect.Effect, Error, R>): Effect.Effect +} = dual( + 2, + (self: Entry, effect: Effect.Effect, Error, R>): Effect.Effect => + internalEffect.matchEffect(effect, { + onFailure: (error) => complete(self, core.exitFail(error) as any), + onSuccess: (value) => complete(self, core.exitSucceed(value) as any) + }) +) + +/** + * Completes a request entry with a typed failure. + * + * **When to use** + * + * Use to report a request-specific typed error while implementing a + * `RequestResolver`. + * + * @see {@link failCause} for completing an entry with a full `Cause` + * @see {@link complete} for completing an entry with an existing `Exit` + * @see {@link completeEffect} for completing an entry from an effect result + * @see {@link succeed} for completing an entry successfully + * + * @category completion + * @since 2.0.0 + */ +export const fail: { + (error: Error): (self: Entry) => Effect.Effect + (self: Entry, error: Error): Effect.Effect +} = dual( + 2, + (self: Entry, error: Error): Effect.Effect => complete(self, core.exitFail(error) as any) +) + +/** + * Completes a request entry with a failure `Cause`. + * + * **When to use** + * + * Use when you need a `RequestResolver` to complete an entry with structured + * cause information rather than only the request's typed error value. + * + * @see {@link fail} for completing an entry with a typed error value + * @see {@link complete} for completing an entry with an existing `Exit` + * @see {@link completeEffect} for completing an entry from an effect result + * @see {@link succeed} for completing an entry successfully + * + * @category completion + * @since 2.0.0 + */ +export const failCause: { + (cause: Cause.Cause>): (self: Entry) => Effect.Effect + (self: Entry, cause: Cause.Cause>): Effect.Effect +} = dual( + 2, + (self: Entry, cause: Cause.Cause>): Effect.Effect => + complete(self, core.exitFailCause(cause) as any) +) + +/** + * Completes a request entry successfully with the supplied value. + * + * **When to use** + * + * Use when you need to finish a `Request.Entry` with a successful request + * value. + * + * @see {@link complete} for completing an entry with a prebuilt `Exit` + * @see {@link completeEffect} for completing an entry from an effect result + * @see {@link fail} for completing an entry with a typed failure + * @see {@link failCause} for completing an entry with a failure `Cause` + * + * @category completion + * @since 2.0.0 + */ +export const succeed: { + (value: Success): (self: Entry) => Effect.Effect + (self: Entry, value: Success): Effect.Effect +} = dual( + 2, + (self: Entry, value: Success): Effect.Effect => + complete(self, core.exitSucceed(value) as any) +) + +/** + * A pending request handed to a `RequestResolver`. + * + * **Details** + * + * An entry contains the original request, the fiber context needed to run it, + * an `uninterruptible` flag used by batching and caching internals, and the + * `completeUnsafe` callback used by resolvers to supply the final `Exit`. + * + * @category entry + * @since 2.0.0 + */ +export interface Entry { + readonly request: R + readonly context: Context.Context< + [R] extends [Request] ? _R : never + > + uninterruptible: boolean + completeUnsafe( + exit: Exit.Exit< + [R] extends [Request] ? _A : never, + [R] extends [Request] ? _E : never + > + ): void +} + +/** + * Creates a `Request.Entry` from its component fields. + * + * **Details** + * + * This is a low-level helper for request runtime and resolver infrastructure; + * most application code receives entries from a `RequestResolver` instead of + * constructing them directly. + * + * @category entry + * @since 2.0.0 + */ +export const makeEntry = (options: { + readonly request: R + readonly context: Context.Context< + [R] extends [Request] ? _R : never + > + readonly uninterruptible: boolean + readonly completeUnsafe: ( + exit: Exit.Exit< + [R] extends [Request] ? _A : never, + [R] extends [Request] ? _E : never + > + ) => void +}): Entry => options diff --git a/repos/effect/packages/effect/src/RequestResolver.ts b/repos/effect/packages/effect/src/RequestResolver.ts new file mode 100644 index 0000000..33cce71 --- /dev/null +++ b/repos/effect/packages/effect/src/RequestResolver.ts @@ -0,0 +1,1249 @@ +/** + * Resolves data requests made with `Effect.request`. + * + * A `Request` describes what a fiber needs, while a `RequestResolver` describes + * how to collect request entries, group them into batches, run backend work, + * and complete each waiting entry. This module includes constructors for common + * resolver shapes and tools for controlling batching, grouping, delays, + * tracing, caching, racing, hooks around resolver execution, and persistence. + * + * @since 2.0.0 + */ +import type { NonEmptyArray } from "./Array.ts" +import * as Arr from "./Array.ts" +import * as Cache from "./Cache.ts" +import * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import { constTrue, dual, identity } from "./Function.ts" +import { exitFail, exitSucceed } from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as internal from "./internal/request.ts" +import * as Iterable from "./Iterable.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Request from "./Request.ts" +import type * as Schema from "./Schema.ts" +import type { Scope } from "./Scope.ts" +import * as Tracer from "./Tracer.ts" +import type * as Types from "./Types.ts" +import type * as Persistable from "./unstable/persistence/Persistable.ts" +import * as Persistence from "./unstable/persistence/Persistence.ts" + +const TypeId = "~effect/RequestResolver" + +/** + * A resolver that executes and completes batched `Request` entries. + * + * **Details** + * + * A resolver controls how requests are grouped, delayed, optionally + * pre-checked, and finally run. Its `runAll` method receives a non-empty batch + * of `Request.Entry` values for a single batch key and must complete every + * received entry, usually by calling `completeUnsafe` or one of the `Request` + * completion helpers. + * + * **Gotchas** + * + * If a resolver finishes without completing an entry, the waiting request fails + * because the resolver did not supply a result. + * + * **Example** (Defining a request resolver) + * + * ```ts + * import { Effect, Exit, RequestResolver } from "effect" + * import type { Request } from "effect" + * + * interface GetUserRequest extends Request.Request { + * readonly _tag: "GetUserRequest" + * readonly id: number + * } + * + * // In practice, you would typically use RequestResolver.make() instead + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`)) + * } + * }) + * ) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface RequestResolver extends RequestResolver.Variance, Pipeable { + readonly delay: Effect.Effect + + /** + * Get a batch key for the given request. + */ + batchKey(entry: Request.Entry): unknown + + /** + * An optional pre-check function that can be used to filter requests before + * they are added to a batch. If the function returns `false`, the request + * will not be processed. + */ + readonly preCheck: ((entry: Request.Entry) => boolean) | undefined + + /** + * Should the resolver continue collecting requests? Otherwise, it will + * immediately execute the collected requests cutting the delay short. + */ + collectWhile(entries: ReadonlySet>): boolean + + /** + * Execute a collection of requests. + */ + runAll(entries: NonEmptyArray>, key: unknown): Effect.Effect> +} + +/** + * Namespace containing type-level helpers associated with `RequestResolver`. + * + * @since 2.0.0 + */ +export declare namespace RequestResolver { + /** + * Variance marker carried by every `RequestResolver`. + * + * **Details** + * + * This marker preserves the request type accepted by the resolver for + * Effect's type-level machinery. Users normally do not implement it directly. + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Types.Contravariant + } + } +} + +const RequestResolverProto = { + [TypeId]: { + _A: identity, + _R: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Returns `true` if the specified value is a `RequestResolver`, `false` otherwise. + * + * **When to use** + * + * Use to narrow unknown values before passing them to APIs that require a + * `RequestResolver`. + * + * @see {@link RequestResolver} for the type narrowed by this guard + * + * @category guards + * @since 2.0.0 + */ +export const isRequestResolver = (u: unknown): u is RequestResolver => hasProperty(u, TypeId) + +/** + * Creates a request resolver with fine-grained + * control over its behavior. + * + * **When to use** + * + * Use when you need to supply the resolver batching primitives directly, + * including the batch key, optional pre-check, delay effect, collection cutoff, + * and batch runner. + * + * **Details** + * + * `batchKey` groups request entries, `delay` schedules batch execution, + * `collectWhile` can end collection early, and `runAll` receives a non-empty + * batch for one key. + * + * **Gotchas** + * + * Accepted entries must be completed. If `runAll` succeeds with incomplete + * entries, waiting requests fail. If `preCheck` returns `false`, the entry is + * not batched, so it must be completed or linked to another completion path. + * + * @see {@link make} for constructing a resolver from a batch runner + * @see {@link makeGrouped} for constructing a resolver that groups requests by key + * + * @category constructors + * @since 4.0.0 + */ +export const makeWith = (options: { + readonly batchKey: (request: Request.Entry) => unknown + readonly preCheck?: ((entry: Request.Entry) => boolean) | undefined + readonly delay: Effect.Effect + readonly collectWhile: (requests: ReadonlySet>) => boolean + readonly runAll: (entries: NonEmptyArray>, key: unknown) => Effect.Effect> +}): RequestResolver => { + const self = Object.create(RequestResolverProto) + self.batchKey = options.batchKey + self.preCheck = options.preCheck + self.delay = options.delay + self.collectWhile = options.collectWhile + self.runAll = options.runAll + return self +} + +const defaultKeyObject = {} +const defaultKey = (_request: unknown): unknown => defaultKeyObject + +/** + * Constructs a request resolver with the specified method to run requests. + * + * **Example** (Creating a request resolver) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * // Define a request type + * interface GetUserRequest extends Request.Request { + * readonly _tag: "GetUserRequest" + * readonly id: number + * } + * const GetUserRequest = Request.tagged("GetUserRequest") + * + * // Create a resolver that handles the requests + * const UserResolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * // Complete each request with a result + * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`)) + * } + * }) + * ) + * + * // Use the resolver to handle requests + * const getUserEffect = Effect.request(GetUserRequest({ id: 123 }), UserResolver) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = ( + runAll: (entries: NonEmptyArray>, key: unknown) => Effect.Effect> +): RequestResolver => + makeWith({ + batchKey: defaultKey, + delay: Effect.yieldNow, + collectWhile: constTrue, + runAll + }) + +/** + * Constructs a request resolver with the requests grouped by a calculated key. + * + * **Details** + * + * The key can use the Equal trait to determine if two keys are equal. + * + * **Example** (Grouping requests by key) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetUserByRole extends Request.Request { + * readonly _tag: "GetUserByRole" + * readonly role: string + * readonly id: number + * } + * const GetUserByRole = Request.tagged("GetUserByRole") + * + * // Group requests by role for efficient batch processing + * const UserByRoleResolver = RequestResolver.makeGrouped({ + * key: ({ request }) => request.role, + * resolver: (entries, role) => + * Effect.sync(() => { + * console.log(`Processing ${entries.length} requests for role: ${role}`) + * for (const entry of entries) { + * entry.completeUnsafe( + * Exit.succeed(`User ${entry.request.id} with role ${role}`) + * ) + * } + * }) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeGrouped = (options: { + readonly key: (entry: Request.Entry) => K + readonly resolver: (entries: NonEmptyArray>, key: K) => Effect.Effect> +}): RequestResolver => + makeWith({ + batchKey: hashGroupKey(options.key), + delay: Effect.yieldNow, + collectWhile: constTrue, + runAll: options.resolver as any + }) + +const hashGroupKey = (get: (entry: Request.Entry) => K) => { + const groupKeys = MutableHashMap.empty() + return (entry: Request.Entry): unknown => { + const key = get(entry) + const okey = MutableHashMap.get(groupKeys, key) + if (okey._tag === "Some") { + return okey.value + } + MutableHashMap.set(groupKeys, key, key) + return key + } +} + +/** + * Constructs a request resolver from a pure function. + * + * **Example** (Creating a resolver from a pure function) + * + * ```ts + * import { Effect, Request, RequestResolver } from "effect" + * + * interface GetSquareRequest extends Request.Request { + * readonly _tag: "GetSquareRequest" + * readonly value: number + * } + * const GetSquareRequest = Request.tagged("GetSquareRequest") + * + * // Create a resolver from a pure function + * const SquareResolver = RequestResolver.fromFunction( + * (entry) => entry.request.value * entry.request.value + * ) + * + * // Usage + * const getSquareEffect = Effect.request( + * GetSquareRequest({ value: 5 }), + * SquareResolver + * ) + * // Will resolve to 25 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromFunction = ( + f: (entry: Request.Entry) => Request.Success +): RequestResolver => + make( + (entries) => + Effect.sync(() => { + for (let i = 0; i < entries.length; i++) { + const entry = entries[i] + entry.completeUnsafe(exitSucceed(f(entry))) + } + }) + ) + +/** + * Constructs a request resolver from a pure function that takes a list of requests + * and returns a list of results of the same size. Each item in the result + * list must correspond to the item at the same index in the request list. + * + * **Example** (Batching pure request handling) + * + * ```ts + * import { Effect, Request, RequestResolver } from "effect" + * + * interface GetDoubleRequest extends Request.Request { + * readonly _tag: "GetDoubleRequest" + * readonly value: number + * } + * const GetDoubleRequest = Request.tagged("GetDoubleRequest") + * + * // Create a resolver that processes multiple requests in a batch + * const DoubleResolver = RequestResolver.fromFunctionBatched( + * (entries) => entries.map((entry) => entry.request.value * 2) + * ) + * + * // Usage with multiple requests + * const effects = [1, 2, 3].map((value) => + * Effect.request(GetDoubleRequest({ value }), DoubleResolver) + * ) + * const batchedEffect = Effect.all(effects) // [2, 4, 6] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromFunctionBatched = ( + f: (entries: NonEmptyArray>) => Iterable> +): RequestResolver => + make( + (entries) => + Effect.sync(() => { + let i = 0 + for (const result of f(entries)) { + const entry = entries[i++] + entry.completeUnsafe(exitSucceed(result)) + } + }) + ) + +/** + * Constructs a request resolver from an effectual function. + * + * **Example** (Creating a resolver from an effectful function) + * + * ```ts + * import { Effect, Request, RequestResolver } from "effect" + * + * interface GetUserFromAPIRequest extends Request.Request { + * readonly _tag: "GetUserFromAPIRequest" + * readonly id: number + * } + * const GetUserFromAPIRequest = Request.tagged( + * "GetUserFromAPIRequest" + * ) + * + * // Create a resolver that uses effects (like HTTP calls) + * const UserAPIResolver = RequestResolver.fromEffect( + * (entry) => + * Effect.gen(function*() { + * // Simulate an API call + * yield* Effect.sleep("100 millis") + * // Just return the result without error handling for simplicity + * return `User ${entry.request.id} from API` + * }) + * ) + * + * // Usage + * const getUserEffect = Effect.request( + * GetUserFromAPIRequest({ id: 123 }), + * UserAPIResolver + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromEffect = ( + f: (entry: Request.Entry) => Effect.Effect, Request.Error> +): RequestResolver => { + effect.interruptChildrenPatch() // ensure middleware is registered + return make((entries) => + Effect.callback((resume) => { + const parent = effect.getCurrentFiber()! + let done = 0 + for (let i = 0; i < entries.length; i++) { + const entry = entries[i] + const fiber = effect.forkUnsafe(parent as any, f(entry), true) + fiber.addObserver((exit) => { + entry.completeUnsafe(exit) + done++ + if (done === entries.length) { + resume(effect.void) + } + }) + } + }) + ) +} + +/** + * Constructs a request resolver from a list of tags paired to functions, that takes + * a list of requests and returns a list of results of the same size. Each item + * in the result list must correspond to the item at the same index in the + * request list. + * + * **Example** (Handling tagged request batches) + * + * ```ts + * import { Effect, RequestResolver } from "effect" + * import type { Request } from "effect" + * + * interface GetUser extends Request.Request { + * readonly _tag: "GetUser" + * readonly id: number + * } + * + * interface GetPost extends Request.Request { + * readonly _tag: "GetPost" + * readonly id: number + * } + * + * type MyRequest = GetUser | GetPost + * + * // Create a resolver that handles different request types + * const MyResolver = RequestResolver.fromEffectTagged()({ + * GetUser: (requests) => + * Effect.succeed(requests.map((req) => `User ${req.request.id}`)), + * GetPost: (requests) => + * Effect.succeed(requests.map((req) => `Post ${req.request.id}`)) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromEffectTagged = () => +< + Fns extends { + readonly [Tag in A["_tag"]]: [Extract] extends [infer Req] + ? Req extends Request.Request ? + (requests: Array>) => Effect.Effect, ReqE> + : never + : never + } +>( + fns: Fns +): RequestResolver => + make( + (entries): Effect.Effect => { + const grouped = new Map>>() + for (let i = 0, len = entries.length; i < len; i++) { + const group = grouped.get(entries[i].request._tag) + if (group) { + group.push(entries[i]) + } else { + grouped.set(entries[i].request._tag, [entries[i]]) + } + } + return Effect.forEach( + grouped, + ([tag, requests]) => + Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { + onFailure: (cause) => { + for (let i = 0; i < requests.length; i++) { + const entry = requests[i] + entry.completeUnsafe(exitFail(cause) as any) + } + }, + onSuccess: (res) => { + for (let i = 0; i < res.length; i++) { + const entry = requests[i] + entry.completeUnsafe(exitSucceed(res[i]) as any) + } + } + }), + { concurrency: "unbounded", discard: true } + ) as Effect.Effect + } + ) as any + +/** + * Sets the batch delay effect for this request resolver. + * + * **Example** (Setting an effectful batch delay) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed("data")) + * } + * }) + * ) + * + * // Set a custom delay effect (e.g., with logging) + * const resolverWithCustomDelay = RequestResolver.setDelayEffect( + * resolver, + * Effect.gen(function*() { + * yield* Effect.log("Waiting before processing batch...") + * yield* Effect.sleep("50 millis") + * }) + * ) + * ``` + * + * @category delay + * @since 4.0.0 + */ +export const setDelayEffect: { + (delay: Effect.Effect): (self: RequestResolver) => RequestResolver + (self: RequestResolver, delay: Effect.Effect): RequestResolver +} = dual( + 2, + (self: RequestResolver, delay: Effect.Effect): RequestResolver => + makeWith({ + ...self, + delay + }) +) + +/** + * Sets the batch delay window for this request resolver to the specified duration. + * + * **Example** (Setting a batch delay) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed("data")) + * } + * }) + * ) + * + * // Add a 100ms delay to batch requests together + * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis") + * + * // Can also use number for milliseconds + * const delayedResolver2 = RequestResolver.setDelay(resolver, 100) + * ``` + * + * @category delay + * @since 4.0.0 + */ +export const setDelay: { + (duration: Duration.Input): (self: RequestResolver) => RequestResolver + (self: RequestResolver, duration: Duration.Input): RequestResolver +} = dual( + 2, + (self: RequestResolver, duration: Duration.Input): RequestResolver => + makeWith({ + ...self, + delay: Effect.sleep(duration) + }) +) + +/** + * Wraps request resolver execution between `before` and `after` effects. + * + * **Example** (Running effects around request resolution) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed("data")) + * } + * }) + * ) + * + * // Add setup and cleanup around request execution + * const resolverWithAround = RequestResolver.around( + * resolver, + * (entries) => + * Effect.gen(function*() { + * yield* Effect.log(`Starting batch of ${entries.length} requests`) + * return entries.length + * }), + * (entries, initialSize) => + * Effect.gen(function*() { + * yield* Effect.log( + * `Batch completed with ${entries.length} requests (started with ${initialSize})` + * ) + * }) + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const around: { + ( + before: (entries: NonEmptyArray>>) => Effect.Effect>, + after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> + ): (self: RequestResolver) => RequestResolver + ( + self: RequestResolver, + before: (entries: NonEmptyArray>>) => Effect.Effect>, + after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> + ): RequestResolver +} = dual(3, ( + self: RequestResolver, + before: (entries: NonEmptyArray>>) => Effect.Effect>, + after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> +): RequestResolver => + makeWith({ + ...self, + runAll: (entries, key) => + Effect.acquireUseRelease( + before(entries), + () => self.runAll(entries, key), + (a) => after(entries, a) + ) + })) + +/** + * Creates a request resolver that never executes requests. + * + * **When to use** + * + * Use as a resolver value for request types that are statically impossible and + * should never be issued. + * + * **Gotchas** + * + * If this resolver is used for an actual request, the request waits forever + * unless the fiber is interrupted. + * + * @see {@link make} for constructing a resolver that executes batches and completes request entries + * + * @category constructors + * @since 2.0.0 + */ +export const never: RequestResolver = make(() => Effect.never) + +/** + * Returns a request resolver that collects at most `n` requests into each + * batch. + * + * **Details** + * + * When more than `n` requests are waiting for the same resolver and batch key, + * the current batch is run and additional requests are collected into later + * batches. + * + * **Example** (Limiting parallel request batches) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * readonly id: number + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * console.log(`Processing batch of ${entries.length} requests`) + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) + * } + * }) + * ) + * + * // Limit batches to maximum 5 requests + * const limitedResolver = RequestResolver.batchN(resolver, 5) + * + * // When more than 5 requests are made, they'll be split into multiple batches + * const requests = Array.from( + * { length: 12 }, + * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver) + * ) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const batchN: { + (n: number): (self: RequestResolver) => RequestResolver + (self: RequestResolver, n: number): RequestResolver +} = dual(2, (self: RequestResolver, n: number): RequestResolver => + makeWith({ + ...self, + collectWhile: (requests) => requests.size < n + })) + +/** + * Transforms a request resolver by grouping requests using the specified key + * function. + * + * **Example** (Grouping resolver requests) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetUserRequest extends Request.Request { + * readonly _tag: "GetUserRequest" + * readonly userId: number + * readonly department: string + * } + * const GetUserRequest = Request.tagged("GetUserRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * console.log(`Processing ${entries.length} users`) + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`)) + * } + * }) + * ) + * + * // Group requests by department for more efficient processing + * const groupedResolver = RequestResolver.grouped( + * resolver, + * ({ request }) => request.department + * ) + * + * // Requests for the same department will be batched together + * const requests = [ + * Effect.request( + * GetUserRequest({ userId: 1, department: "Engineering" }), + * groupedResolver + * ), + * Effect.request( + * GetUserRequest({ userId: 2, department: "Engineering" }), + * groupedResolver + * ), + * Effect.request( + * GetUserRequest({ userId: 3, department: "Marketing" }), + * groupedResolver + * ) + * ] + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const grouped: { + (f: (entry: Request.Entry) => K): (self: RequestResolver) => RequestResolver + (self: RequestResolver, f: (entry: Request.Entry) => K): RequestResolver +} = dual( + 2, + (self: RequestResolver, f: (entry: Request.Entry) => K): RequestResolver => + makeWith({ + ...self, + batchKey: hashGroupKey(f) + }) +) + +/** + * Returns a request resolver that sends each batch to both resolvers and + * completes with the first resolver to finish. + * + * **Details** + * + * The losing resolver run is interrupted after the winning resolver completes + * the batch. + * + * **Example** (Racing request resolvers) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * readonly id: number + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * // Fast resolver (simulating cache) + * const fastResolver = RequestResolver.make((entries) => + * Effect.gen(function*() { + * yield* Effect.sleep("10 millis") + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`fast-${entry.request.id}`)) + * } + * }) + * ) + * + * // Slow resolver (simulating database) + * const slowResolver = RequestResolver.make((entries) => + * Effect.gen(function*() { + * yield* Effect.sleep("100 millis") + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`slow-${entry.request.id}`)) + * } + * }) + * ) + * + * // Race resolvers - will use whichever completes first + * const racingResolver = RequestResolver.race(fastResolver, slowResolver) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const race: { + ( + that: RequestResolver + ): (self: RequestResolver) => RequestResolver + ( + self: RequestResolver, + that: RequestResolver + ): RequestResolver +} = dual(2, ( + self: RequestResolver, + that: RequestResolver +): RequestResolver => + make( + (requests, key) => effect.race(self.runAll(requests, key), that.runAll(requests, key)) + )) + +/** + * Adds a tracing span to the request resolver, which will also add any span + * links from the request's. + * + * **Example** (Adding a tracing span) + * + * ```ts + * import { Effect, Exit, Request, RequestResolver } from "effect" + * + * interface GetDataRequest extends Request.Request { + * readonly _tag: "GetDataRequest" + * readonly id: number + * } + * const GetDataRequest = Request.tagged("GetDataRequest") + * + * const resolver = RequestResolver.make((entries) => + * Effect.sync(() => { + * for (const entry of entries) { + * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) + * } + * }) + * ) + * + * // Add tracing span with custom name and attributes + * const tracedResolver = RequestResolver.withSpan( + * resolver, + * "user-data-resolver", + * { + * attributes: { + * "resolver.type": "user-data", + * "resolver.version": "1.0" + * } + * } + * ) + * + * // Spans will automatically include batch size and request links + * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const withSpan: { + ( + name: string, + options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined + ): (self: RequestResolver) => RequestResolver + ( + self: RequestResolver, + name: string, + options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined + ): RequestResolver +} = dual((args) => isRequestResolver(args[0]), ( + self: RequestResolver, + name: string, + options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined +): RequestResolver => + makeWith({ + ...self, + runAll: (entries, key) => + Effect.suspend(() => { + const opts = typeof options === "function" ? options(entries) : options + const links = opts?.links ? opts.links.slice() : [] + const seen = new Set() + for (const entry of entries) { + const span = Context.getOption(entry.context, Tracer.ParentSpan) + if (span._tag === "None" || seen.has(span.value)) continue + seen.add(span.value) + links.push({ span: span.value, attributes: {} }) + } + return Effect.withSpan(self.runAll(entries, key), name, { + ...options, + links, + attributes: { + batchSize: entries.length, + ...opts?.attributes + } + }) + }) + })) + +/** + * Wraps a request resolver in a cache, allowing it to cache results up to a + * specified capacity and optional time-to-live. + * + * **When to use** + * + * Use to turn a request resolver into a first-class `Cache` when callers need + * cache lookup, refresh, invalidation, or inspection around request results. + * + * **Details** + * + * The request value is the cache key. Cache misses run the resolver via + * `Effect.request`, `timeToLive` receives the request `Exit` and the request, + * and `requireServicesAt` controls whether services are required at lookup time + * or construction time. + * + * **Gotchas** + * + * Cache hits depend on the request value's equality semantics. + * + * @see {@link withCache} for keeping caching behind a resolver used with `Effect.request` + * @see {@link persisted} for storing persistable request results outside process memory + * @see {@link Cache.Cache} for operations available on the returned cache + * + * @category caching + * @since 4.0.0 + */ +export const asCache: { + < + A extends Request.Any, + ServiceMode extends "lookup" | "construction" = never + >(options: { + readonly capacity: number + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly requireServicesAt?: ServiceMode | undefined + }): (self: RequestResolver) => Effect.Effect< + Cache.Cache< + A, + Request.Success, + Request.Error, + "construction" extends ServiceMode ? never : Request.Services + >, + never, + "construction" extends ServiceMode ? Request.Services : never + > + < + A extends Request.Any, + ServiceMode extends "lookup" | "construction" = never + >(self: RequestResolver, options: { + readonly capacity: number + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly requireServicesAt?: ServiceMode | undefined + }): Effect.Effect< + Cache.Cache< + A, + Request.Success, + Request.Error, + "construction" extends ServiceMode ? never : Request.Services + >, + never, + "construction" extends ServiceMode ? Request.Services : never + > +} = dual(2, < + A extends Request.Any, + ServiceMode extends "lookup" | "construction" = never +>(self: RequestResolver, options: { + readonly capacity: number + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly requireServicesAt?: ServiceMode | undefined +}): Effect.Effect< + Cache.Cache< + A, + Request.Success, + Request.Error, + "construction" extends ServiceMode ? never : Request.Services + >, + never, + "construction" extends ServiceMode ? Request.Services : never +> => + Cache.makeWith((req: A) => internal.request(req, self), { + capacity: options.capacity, + timeToLive: options.timeToLive as any, + requireServicesAt: options.requireServicesAt ?? "lookup" as ServiceMode + }) as any) + +/** + * Adds a bounded in-memory cache to a request resolver. + * + * **When to use** + * + * Use to reuse completed results for repeated equal request values while still + * passing a `RequestResolver` to `Effect.request`. + * + * **Details** + * + * Running the returned effect creates the cache and returns a wrapped resolver. + * The cache stores completed success or failure results by request equality up + * to `capacity`. The `strategy` option controls eviction order and defaults to + * `"lru"`; `"fifo"` keeps insertion order. + * + * **Gotchas** + * + * Entries do not expire by time, and completed failures are cached the same as + * successes. Request equality controls cache hits. + * + * @see {@link asCache} for exposing the resolver as a `Cache` with time-to-live and service lookup controls + * @see {@link persisted} for backing persistable requests with the configured persistence store + * + * @category caching + * @since 4.0.0 + */ +export const withCache: { + (options: { + readonly capacity: number + readonly strategy?: "lru" | "fifo" | undefined + }): (self: RequestResolver) => Effect.Effect> + (self: RequestResolver, options: { + readonly capacity: number + readonly strategy?: "lru" | "fifo" | undefined + }): Effect.Effect> +} = dual(2, (self: RequestResolver, options: { + readonly capacity: number + readonly strategy?: "lru" | "fifo" | undefined +}): Effect.Effect> => + Effect.sync(() => { + const strategy = options.strategy ?? "lru" + const cache = MutableHashMap.empty + exit: Request.Result | undefined + }>() + return makeWith({ + ...self, + runAll(entries, key) { + return Effect.onExit(self.runAll(entries, key), () => { + let toRemove = MutableHashMap.size(cache) - options.capacity + if (toRemove <= 0) return Effect.void + for (const k of MutableHashMap.keys(cache)) { + MutableHashMap.remove(cache, k) + toRemove-- + if (toRemove <= 0) break + } + return Effect.void + }) + }, + preCheck(entry) { + const ocached = MutableHashMap.get(cache, entry.request) + if (ocached._tag === "None") { + const cached = { entry, exit: undefined as Request.Result | undefined } + MutableHashMap.set(cache, entry.request, cached) + const prevComplete = entry.completeUnsafe + entry.completeUnsafe = function(exit) { + cached.exit = exit as any + prevComplete(exit) + } + return true + } + const cached = ocached.value + if (cached.exit) { + if (strategy === "lru") { + MutableHashMap.remove(cache, cached.entry.request) + MutableHashMap.set(cache, cached.entry.request, cached) + } + entry.completeUnsafe(cached.exit as any) + } else { + cached.entry.uninterruptible = true + const prevComplete = cached.entry.completeUnsafe + cached.entry.completeUnsafe = function(exit) { + prevComplete(exit) + entry.completeUnsafe(exit) + } + } + return false + } + }) + })) + +/** + * Wraps a request resolver with persistent storage for persistable requests. + * + * **When to use** + * + * Use to keep a `RequestResolver` interface while reusing completed + * `Persistable` request results through a `Persistence` store. + * + * **Details** + * + * Cached results are loaded from the configured persistence store before + * running the underlying resolver. Missing entries are resolved normally and + * written back to the store. Entries marked stale by `staleWhileRevalidate` + * receive the stored result and are also resolved again so the refreshed result + * can be written back to the store. Creating the persisted resolver requires + * `Persistence.Persistence` and `Scope`. + * + * @see {@link withCache} for in-memory resolver caching that does not require persistable request values or a persistence store + * @see {@link asCache} for exposing resolver results through a `Cache` instead of returning another resolver + * + * @category Persistence + * @since 4.0.0 + */ +export const persisted: { + & Persistable.Any>( + options: { + readonly storeId: string + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined + } + ): (self: RequestResolver) => Effect.Effect< + RequestResolver, + never, + Persistence.Persistence | Scope + > + < + A extends Request.Request & Persistable.Any + >( + self: RequestResolver, + options: { + readonly storeId: string + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined + } + ): Effect.Effect< + RequestResolver, + never, + Persistence.Persistence | Scope + > +} = dual( + 2, + Effect.fnUntraced(function*< + A extends Request.Request & Persistable.Any + >( + self: RequestResolver, + options: { + readonly storeId: string + readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined + readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined + } + ) { + const store = yield* (yield* Persistence.Persistence).make(options as any) + return makeWith({ + ...self, + runAll: Effect.fnUntraced(function*(entries, key) { + const results = yield* (store.getMany(Iterable.map(entries, (_) => _.request)).pipe( + Effect.provideContext(entries[0].context) + ) as Effect.Effect< + Array | undefined>, + Request.Error + >) + const leftover: Array> = [] + const toPersist = new Map>() + for (let i = 0; i < results.length; i++) { + const entry = entries[i] + const exit = results[i] + if ( + exit === undefined || + (options.staleWhileRevalidate && options.staleWhileRevalidate(exit as any, entry.request)) + ) { + const prevComplete = entry.completeUnsafe + entry.completeUnsafe = function(exit) { + toPersist.set(entry.request, exit as any) + prevComplete(exit) + } + leftover.push(entry) + if (exit === undefined) continue + } + entry.completeUnsafe(exit as any) + } + if (!Arr.isArrayNonEmpty(leftover)) { + return + } + yield* Effect.catchCause(self.runAll(leftover, key), (cause) => { + for (let i = 0; i < leftover.length; i++) { + const entry = leftover[i] + if (!toPersist.has(entry.request)) continue + entry.completeUnsafe(Exit.failCause(cause) as any) + } + return Effect.void + }) + yield* (store.setMany(toPersist).pipe( + Effect.provideContext(entries[0].context) + ) as Effect.Effect>) + }) + }) + }) +) diff --git a/repos/effect/packages/effect/src/Resource.ts b/repos/effect/packages/effect/src/Resource.ts new file mode 100644 index 0000000..7e6d538 --- /dev/null +++ b/repos/effect/packages/effect/src/Resource.ts @@ -0,0 +1,184 @@ +/** + * Stores refreshable scoped values. + * + * A `Resource` keeps the latest successful or failed acquisition result. + * It can be read repeatedly, refreshed manually, or refreshed automatically on a + * schedule. Resource acquisition runs in a scope, so replacements and final + * cleanup release the resources owned by previous values. + * + * @since 2.0.0 + */ +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import { identity } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Schedule from "./Schedule.ts" +import type * as Scope from "./Scope.ts" +import * as ScopedRef from "./ScopedRef.ts" + +const TypeId = "~effect/Resource" as const + +/** + * A `Resource` is a value loaded into memory that can be refreshed manually or + * automatically according to a schedule. + * + * **When to use** + * + * Use to model a scoped value whose latest acquisition result is kept available + * for repeated reads and can be refreshed manually or on a schedule. + * + * @see {@link manual} for creating a resource refreshed by the caller + * @see {@link auto} for creating a resource refreshed according to a schedule + * @see {@link get} for reading the currently stored acquisition result + * @see {@link refresh} for forcing a new acquisition + * + * @category models + * @since 2.0.0 + */ +export interface Resource extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly scopedRef: ScopedRef.ScopedRef> + readonly acquire: Effect.Effect +} + +/** + * Returns `true` if the specified value is a `Resource`. + * + * **When to use** + * + * Use to validate unknown values at runtime boundaries before treating them as + * `Resource` values. + * + * **Details** + * + * This predicate narrows the input to `Resource`. + * + * @category guards + * @since 4.0.0 + */ +export const isResource: (u: unknown) => u is Resource = ( + u: unknown +): u is Resource => hasProperty(u, TypeId) + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + toJSON() { + return { + _id: "Resource" + } + } +} + +const makeUnsafe = ( + scopedRef: ScopedRef.ScopedRef>, + acquire: Effect.Effect +): Resource => { + const self = Object.create(Proto) + self.scopedRef = scopedRef + self.acquire = acquire + return self +} + +/** + * Creates a `Resource` that must be refreshed manually. + * + * **When to use** + * + * Use when you need manual control over resource refresh timing rather than an + * automatic schedule. + * + * @see {@link auto} for schedule-driven automatic refreshes + * @see {@link refresh} to manually trigger a resource refresh + * @category constructors + * @since 2.0.0 + */ +export const manual = ( + acquire: Effect.Effect +): Effect.Effect, never, Scope.Scope | R> => + Effect.contextWith((context: Context.Context) => { + const providedAcquire = Effect.updateContext( + acquire, + (input: Context.Context) => Context.merge(context, input) + ) + return Effect.map( + ScopedRef.fromAcquire(Effect.exit(providedAcquire)), + (scopedRef) => makeUnsafe(scopedRef, providedAcquire) + ) + }) + +/** + * Creates a `Resource` that refreshes automatically according to the supplied + * schedule. + * + * **When to use** + * + * Use when a resource should refresh in the background according to a schedule + * for the lifetime of its scope. + * + * @see {@link manual} for caller-controlled refresh timing + * @see {@link refresh} to trigger a refresh explicitly + * + * @category constructors + * @since 2.0.0 + */ +export const auto = ( + acquire: Effect.Effect, + policy: Schedule.Schedule +): Effect.Effect, never, R | R2 | Scope.Scope> => + Effect.tap( + manual(acquire), + (self) => Effect.forkScoped(Effect.repeat(refresh(self), policy)) + ) + +/** + * Retrieves the current value stored in this resource. + * + * **When to use** + * + * Use to read the value currently cached by a `Resource`. + * + * **Gotchas** + * + * If the resource currently stores a failed acquisition result, the returned + * effect fails with the stored error. + * + * @see {@link refresh} to re-run acquisition and update the stored value before a later read + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: Resource): Effect.Effect => + Effect.flatMap(ScopedRef.get(self.scopedRef), identity) + +/** + * Re-runs this resource's acquisition effect and updates the current value. + * + * **When to use** + * + * Use to force an existing `Resource` to reacquire its value at a + * caller-controlled point. + * + * **Details** + * + * When acquisition succeeds, refreshing replaces the value stored in the + * resource's scoped reference and releases resources associated with the + * previous value. + * + * **Gotchas** + * + * If acquisition fails, the returned effect fails and the previously stored + * result is left as what `get` reads. + * + * @see {@link get} for reading the current stored value + * @see {@link manual} for resources refreshed only by caller action + * @see {@link auto} for schedule-driven automatic refreshes + * + * @category resource management + * @since 2.0.0 + */ +export const refresh = (self: Resource): Effect.Effect => + ScopedRef.set(self.scopedRef, Effect.map(self.acquire, Exit.succeed)) diff --git a/repos/effect/packages/effect/src/Result.ts b/repos/effect/packages/effect/src/Result.ts new file mode 100644 index 0000000..c1c1052 --- /dev/null +++ b/repos/effect/packages/effect/src/Result.ts @@ -0,0 +1,1939 @@ +/** + * Models a value that has already succeeded or failed. + * + * A `Result` is `Success` when a value is available and + * `Failure` when an error is available. It is plain data, so inspecting + * or transforming it does not run side effects. This module includes helpers + * for creating, checking, mapping, combining, and extracting results, plus + * conversions to and from `Option` and nullable values. + * + * @since 4.0.0 + */ + +import * as Equivalence from "./Equivalence.ts" +import type { LazyArg } from "./Function.ts" +import { constNull, constUndefined, dual, identity } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as doNotation from "./internal/doNotation.ts" +import * as option_ from "./internal/option.ts" +import * as result from "./internal/result.ts" +import type { Option } from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Predicate, Refinement } from "./Predicate.ts" +import { isFunction } from "./Predicate.ts" +import type { Covariant, NoInfer, NotFunction } from "./Types.ts" +import type * as Unify from "./Unify.ts" +import type * as Gen from "./Utils.ts" + +const TypeId = "~effect/data/Result" + +/** + * A value that is either `Success` or `Failure`. + * + * **When to use** + * + * Use when both success and failure should remain available as data and + * `Option` would lose failure information. + * + * **Details** + * + * - Use {@link succeed} / {@link fail} to construct + * - Use {@link match} to fold both branches + * - Use {@link isSuccess} / {@link isFailure} to narrow the type + * + * `E` defaults to `never`, so `Result` means a result that cannot fail. + * + * **Example** (Creating and matching a Result) + * + * ```ts + * import { Result } from "effect" + * + * const success = Result.succeed(42) + * const failure = Result.fail("something went wrong") + * + * const message = Result.match(success, { + * onSuccess: (value) => `Success: ${value}`, + * onFailure: (error) => `Error: ${error}` + * }) + * console.log(message) + * // Output: "Success: 42" + * ``` + * + * @see {@link succeed} / {@link fail} to create values + * @see {@link match} to fold both branches + * @see {@link isSuccess} / {@link isFailure} for type guards + * + * @category models + * @since 4.0.0 + */ +export type Result = Success | Failure + +/** + * The failure variant of {@link Result}. Wraps an error of type `E`. + * + * **Details** + * + * - Access the error via the `.failure` property + * - Use {@link isFailure} to narrow a `Result` to `Failure` + * - Create with {@link fail} + * + * **Example** (Accessing the failure value) + * + * ```ts + * import { Result } from "effect" + * + * const failure = Result.fail("Network error") + * + * if (Result.isFailure(failure)) { + * console.log(failure.failure) + * // Output: "Network error" + * } + * ``` + * + * @see {@link fail} to create a Failure + * @see {@link isFailure} to narrow the type + * @see {@link Success} for the other variant + * + * @category models + * @since 4.0.0 + */ +export interface Failure extends Pipeable, Inspectable { + readonly _tag: "Failure" + readonly _op: "Failure" + readonly failure: E + readonly [TypeId]: { + readonly _A: Covariant + readonly _E: Covariant + } + [Symbol.iterator](): ResultIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ResultUnify + [Unify.ignoreSymbol]?: ResultUnifyIgnore +} + +/** + * Iterator protocol used to yield a `Result` inside {@link gen}, returning the + * success value type back to the generator. + * + * **When to use** + * + * Use when defining or typing `[Symbol.iterator]()` for `Result` values so + * `yield*` can pass the success value type back into `Result.gen`. + * + * @see {@link gen} for writing generator-based `Result` code that consumes this iterator protocol + * + * @category generators + * @since 4.0.0 + */ +export interface ResultIterator> { + next( + ...args: ReadonlyArray + ): IteratorResult> +} + +/** + * The success variant of {@link Result}. Wraps a value of type `A`. + * + * **Details** + * + * - Access the value via the `.success` property + * - Use {@link isSuccess} to narrow a `Result` to `Success` + * - Create with {@link succeed} + * + * **Example** (Accessing the success value) + * + * ```ts + * import { Result } from "effect" + * + * const success = Result.succeed(42) + * + * if (Result.isSuccess(success)) { + * console.log(success.success) + * // Output: 42 + * } + * ``` + * + * @see {@link succeed} to create a Success + * @see {@link isSuccess} to narrow the type + * @see {@link Failure} for the other variant + * + * @category models + * @since 4.0.0 + */ +export interface Success extends Pipeable, Inspectable { + readonly _tag: "Success" + readonly _op: "Success" + readonly success: A + readonly [TypeId]: { + readonly _A: Covariant + readonly _E: Covariant + } + [Symbol.iterator](): ResultIterator> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: ResultUnify + [Unify.ignoreSymbol]?: ResultUnifyIgnore +} + +/** + * Type-level utility for unifying `Result` types in generic contexts. + * + * **Details** + * + * This is an internal interface used by the Effect type system. You typically + * do not need to reference it directly. + * + * @category models + * @since 4.0.0 + */ +export interface ResultUnify { + Result?: () => T[Unify.typeSymbol] extends Result | infer _ ? Result : never +} + +/** + * Marker interface for ignoring unification in `Result` types. + * + * **Details** + * + * This is an internal interface used by the Effect type system. You typically + * do not need to reference it directly. + * + * @category models + * @since 4.0.0 + */ +export interface ResultUnifyIgnore {} + +/** + * Higher-kinded type representation for `Result`. + * + * **Details** + * + * Used internally to integrate `Result` with generic type-class utilities + * (e.g., `map`, `flatMap` abstractions). You typically do not need to + * reference this directly. + * + * @category type lambdas + * @since 4.0.0 + */ +export interface ResultTypeLambda extends TypeLambda { + readonly type: Result +} + +/** + * Namespace containing type-level utilities for extracting the inner types + * of a `Result`. + * + * **Example** (Extracting inner types) + * + * ```ts + * import type { Result } from "effect" + * + * type R = Result.Result + * + * // number + * type A = Result.Result.Success + * + * // string + * type E = Result.Result.Failure + * ``` + * + * @since 4.0.0 + */ +export declare namespace Result { + /** + * Extracts the failure type `E` from `Result`. + * + * @category Type Level + * @since 4.0.0 + */ + export type Failure> = [T] extends [Result] ? _E : never + /** + * Extracts the success type `A` from `Result`. + * + * @category Type Level + * @since 4.0.0 + */ + export type Success> = [T] extends [Result] ? _A : never +} + +/** + * Creates a `Result` holding a `Success` value. + * + * **Details** + * + * - Use when you have a value and want to lift it into the `Result` type + * - The error type `E` defaults to `never` + * + * **Example** (Wrapping a value) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.succeed(42) + * + * console.log(Result.isSuccess(result)) + * // Output: true + * ``` + * + * @see {@link fail} to create a Failure + * @see {@link void_ void} for a pre-built `Success` + * + * @category constructors + * @since 4.0.0 + */ +export const succeed: (right: A) => Result = result.succeed + +/** + * Creates a `Result` holding a `Failure` value. + * + * **When to use** + * + * Use to represent a failed `Result` with a typed failure value. + * + * **Details** + * + * - The success type `A` defaults to `never` + * + * **Example** (Creating a failure) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.fail("Something went wrong") + * + * console.log(Result.isFailure(result)) + * // Output: true + * ``` + * + * @see {@link succeed} to create a Success + * @see {@link mapError} to transform the error + * + * @category constructors + * @since 4.0.0 + */ +export const fail: (left: E) => Result = result.fail + +const void_: Result = succeed(void 0) +export { + /** + * Provides a pre-built successful `Result` that carries `undefined`. + * + * **When to use** + * + * Use when you need a successful `Result` value that signals completion + * without carrying meaningful data. + * + * **Details** + * + * This is equivalent to `Result.succeed(undefined)`, but reuses a shared + * `Success` wrapper instead of allocating one each time. + * + * **Example** (Referencing void results) + * + * ```ts + * import { Result } from "effect" + * + * const result: Result.Result = Result.void + * + * console.log(Result.isSuccess(result)) + * // Output: true + * ``` + * + * @see {@link succeed} to create a Success with a specific value + * + * @category constructors + * @since 3.13.0 + */ + void_ as void +} + +/** + * Provides a pre-built failed `Result` whose failure value is `undefined`. + * + * **When to use** + * + * Use when you need a failed `Result` value that acts only as a control signal + * without failure data. + * + * **Details** + * + * This is equivalent to `Result.fail(undefined)` with type + * `Result`, but reuses a shared `Failure` wrapper instead of + * allocating one each time. + * + * **Example** (Failing without a payload) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.failVoid + * + * console.log(Result.isFailure(result)) + * // Output: true + * ``` + * + * @see {@link fail} to create a Failure with a specific value + * + * @category constructors + * @since 4.0.0 + */ +export const failVoid: Result = fail(void 0) + +/** + * Converts a possibly `null` or `undefined` value into a `Result`. + * + * **When to use** + * + * Use when you need `null` or `undefined` input to become a `Failure` while + * present values remain available as `Success`. + * + * **Details** + * + * - Non-nullish values become `Success>` + * - `null` or `undefined` becomes `Failure` using the provided function + * - Supports both data-first and data-last (piped) usage + * - The `onNullish` callback receives the original value + * + * **Example** (Handling nullable values) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.fromNullishOr(1, () => "fallback")) + * // Output: { _tag: "Success", success: 1, ... } + * + * console.log(Result.fromNullishOr(null, () => "fallback")) + * // Output: { _tag: "Failure", failure: "fallback", ... } + * ``` + * + * @see {@link fromOption} to convert from an Option + * @see {@link succeed} / {@link fail} for direct construction + * + * @category constructors + * @since 4.0.0 + */ +export const fromNullishOr: { + (onNullish: (a: A) => E): (self: A) => Result, E> + (self: A, onNullish: (a: A) => E): Result, E> +} = dual( + 2, + (self: A, onNullish: (a: A) => E): Result, E> => + self == null ? fail(onNullish(self)) : succeed(self) +) + +/** + * Converts an `Option` into a `Result`. + * + * **When to use** + * + * Use when an existing `Option` should become a `Result`, preserving `Some` as + * success and turning `None` into a caller-provided failure. + * + * **Details** + * + * - `Some` becomes `Success` + * - `None` becomes `Failure` using the provided function + * - Supports both data-first and data-last (piped) usage + * + * **Example** (Converting an Option to a Result) + * + * ```ts + * import { Option, Result } from "effect" + * + * const some = Result.fromOption(Option.some(1), () => "missing") + * console.log(some) + * // Output: { _tag: "Success", success: 1, ... } + * + * const none = Result.fromOption(Option.none(), () => "missing") + * console.log(none) + * // Output: { _tag: "Failure", failure: "missing", ... } + * ``` + * + * @see {@link getSuccess} to extract the success value as an Option + * @see {@link getFailure} to extract the failure value as an Option + * @see {@link fromNullishOr} to build a Result from nullable values + * + * @category constructors + * @since 2.0.0 + */ +export const fromOption: { + (onNone: () => E): (self: Option) => Result + (self: Option, onNone: () => E): Result +} = result.fromOption + +const try_: { + ( + options: { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } + ): Result + (evaluate: LazyArg): Result +} = ( + evaluate: LazyArg | { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } +) => { + if (isFunction(evaluate)) { + try { + return succeed(evaluate()) + } catch (e) { + return fail(e) + } + } else { + try { + return succeed(evaluate.try()) + } catch (e) { + return fail(evaluate.catch(e)) + } + } +} + +export { + /** + * Wraps a synchronous computation that may throw into a `Result` safely. + * + * **Details** + * + * - If the function returns normally, the result is `Success` + * - If the function throws, the exception is caught and becomes `Failure` + * - With a single function argument, the error type is `unknown` + * - With `{ try, catch }` options, the `catch` function maps the thrown value to `E` + * + * **Example** (Catching JSON parse errors) + * + * ```ts + * import { Result } from "effect" + * + * const ok = Result.try(() => JSON.parse('{"name": "Alice"}')) + * console.log(ok) + * // Output: { _tag: "Success", success: { name: "Alice" }, ... } + * + * const err = Result.try({ + * try: () => JSON.parse("not json"), + * catch: (e) => `Parse failed: ${e}` + * }) + * console.log(Result.isFailure(err)) + * // Output: true + * ``` + * + * @see {@link succeed} / {@link fail} for direct construction + * @see {@link fromNullishOr} for nullable values + * + * @category constructors + * @since 2.0.0 + */ + try_ as try +} + +/** + * Checks whether a value is a `Result` (either `Success` or `Failure`). + * + * **When to use** + * + * Use to validate unknown input before operating on it as a `Result`. + * + * **Details** + * + * - Returns `true` for both `Success` and `Failure` variants + * - Acts as a TypeScript type guard, narrowing to `Result` + * + * **Example** (Checking if a value is a Result) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.isResult(Result.succeed(1))) + * // Output: true + * + * console.log(Result.isResult({ value: 1 })) + * // Output: false + * ``` + * + * @see {@link isSuccess} / {@link isFailure} to narrow to a specific variant + * + * @category guards + * @since 4.0.0 + */ +export const isResult: (input: unknown) => input is Result = result.isResult + +/** + * Checks whether a `Result` is a `Failure`. + * + * **When to use** + * + * Use to narrow a known `Result` to the `Failure` variant. + * + * **Details** + * + * - Acts as a TypeScript type guard, narrowing to `Failure` + * - After narrowing, you can access `.failure` to read the error value + * + * **Example** (Narrowing to failure) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.fail("oops") + * + * if (Result.isFailure(result)) { + * console.log(result.failure) + * // Output: "oops" + * } + * ``` + * + * @see {@link isSuccess} for the opposite check + * @see {@link isResult} to check if a value is any Result + * + * @category guards + * @since 4.0.0 + */ +export const isFailure: (self: Result) => self is Failure = result.isFailure + +/** + * Checks whether a `Result` is a `Success`. + * + * **When to use** + * + * Use to narrow a known `Result` to the `Success` variant. + * + * **Details** + * + * - Acts as a TypeScript type guard, narrowing to `Success` + * - After narrowing, you can access `.success` to read the value + * + * **Example** (Narrowing to success) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.succeed(42) + * + * if (Result.isSuccess(result)) { + * console.log(result.success) + * // Output: 42 + * } + * ``` + * + * @see {@link isFailure} for the opposite check + * @see {@link isResult} to check if a value is any Result + * + * @category guards + * @since 4.0.0 + */ +export const isSuccess: (self: Result) => self is Success = result.isSuccess + +/** + * Extracts the success value as an `Option`, discarding the failure. + * + * **When to use** + * + * Use when you need to extract the success value from a `Result` as an + * `Option` and discard failure information. + * + * **Details** + * + * - `Success` becomes `Some` + * - `Failure` becomes `None` + * + * **Example** (Extracting the success as an Option) + * + * ```ts + * import { Option, Result } from "effect" + * + * console.log(Result.getSuccess(Result.succeed("ok"))) + * // Output: { _tag: "Some", value: "ok" } + * + * console.log(Result.getSuccess(Result.fail("err"))) + * // Output: { _tag: "None" } + * ``` + * + * @see {@link getFailure} to extract the error instead + * @see {@link fromOption} for the reverse conversion + * + * @category getters + * @since 4.0.0 + */ +export const getSuccess: (self: Result) => Option = result.getSuccess + +/** + * Extracts the failure value as an `Option`, discarding the success. + * + * **When to use** + * + * Use when you need to extract the failure value from a `Result` as an + * `Option` and discard successful values. + * + * **Details** + * + * - `Failure` becomes `Some` + * - `Success` becomes `None` + * + * **Example** (Extracting the failure as an Option) + * + * ```ts + * import { Option, Result } from "effect" + * + * console.log(Result.getFailure(Result.succeed("ok"))) + * // Output: { _tag: "None" } + * + * console.log(Result.getFailure(Result.fail("err"))) + * // Output: { _tag: "Some", value: "err" } + * ``` + * + * @see {@link getSuccess} to extract the success instead + * @see {@link fromOption} for the reverse conversion + * + * @category getters + * @since 4.0.0 + */ +export const getFailure: (self: Result) => Option = result.getFailure + +/** + * Creates an `Equivalence` for comparing two `Result` values. + * + * **Details** + * + * - Two `Success` values are equal when the `success` equivalence says so + * - Two `Failure` values are equal when the `failure` equivalence says so + * - A `Success` and a `Failure` are never equal + * + * **Example** (Comparing Results for equality) + * + * ```ts + * import { Equivalence, Result } from "effect" + * + * const eq = Result.makeEquivalence( + * Equivalence.strictEqual(), + * Equivalence.strictEqual() + * ) + * + * console.log(eq(Result.succeed(1), Result.succeed(1))) + * // Output: true + * + * console.log(eq(Result.succeed(1), Result.fail("x"))) + * // Output: false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = ( + success: Equivalence.Equivalence, + failure: Equivalence.Equivalence +): Equivalence.Equivalence> => + Equivalence.make((x, y) => + isFailure(x) ? + isFailure(y) && failure(x.failure, y.failure) : + isSuccess(y) && success(x.success, y.success) + ) + +/** + * Transforms both the success and failure channels of a `Result`. + * + * **When to use** + * + * Use to transform both success and failure values without changing whether the + * result succeeds or fails. + * + * **Details** + * + * - Applies `onSuccess` if the result is a `Success` + * - Applies `onFailure` if the result is a `Failure` + * + * **Example** (Mapping both channels) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(1), + * Result.mapBoth({ + * onSuccess: (n) => n + 1, + * onFailure: (e) => `Error: ${e}` + * }) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: 2, ... } + * ``` + * + * @see {@link map} to transform only the success value + * @see {@link mapError} to transform only the error value + * @see {@link match} to fold into a single value + * + * @category mapping + * @since 2.0.0 + */ +export const mapBoth: { + (options: { + readonly onFailure: (left: E) => E2 + readonly onSuccess: (right: A) => A2 + }): (self: Result) => Result + (self: Result, options: { + readonly onFailure: (left: E) => E2 + readonly onSuccess: (right: A) => A2 + }): Result +} = dual( + 2, + (self: Result, { onFailure, onSuccess }: { + readonly onFailure: (left: E) => E2 + readonly onSuccess: (right: A) => A2 + }): Result => isFailure(self) ? fail(onFailure(self.failure)) : succeed(onSuccess(self.success)) +) + +/** + * Transforms the failure channel of a `Result`, leaving the success channel unchanged. + * + * **When to use** + * + * Use to transform only the failure channel while preserving success values. + * + * **Details** + * + * - If the result is a `Failure`, applies `f` to the error and returns a new `Failure` + * - If the result is a `Success`, returns it as-is + * + * **Example** (Adding context to an error) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.fail("not found"), + * Result.mapError((e) => `Error: ${e}`) + * ) + * console.log(result) + * // Output: { _tag: "Failure", failure: "Error: not found", ... } + * ``` + * + * @see {@link map} to transform only the success value + * @see {@link mapBoth} to transform both channels + * + * @category mapping + * @since 4.0.0 + */ +export const mapError: { + (f: (err: E) => E2): (self: Result) => Result + (self: Result, f: (err: E) => E2): Result +} = dual( + 2, + (self: Result, f: (err: E) => E2): Result => + isFailure(self) ? fail(f(self.failure)) : succeed(self.success) +) + +/** + * Transforms the success channel of a `Result`, leaving the failure channel unchanged. + * + * **When to use** + * + * Use to apply a transformation to the success value of a `Result` while + * preserving any existing failure. + * + * **Details** + * + * - If the result is a `Success`, applies `f` to the value and returns a new `Success` + * - If the result is a `Failure`, returns it as-is + * - Use {@link flatMap} if `f` returns a `Result` (to avoid nested Results) + * + * **Example** (Doubling the success value) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(3), + * Result.map((n) => n * 2) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: 6, ... } + * ``` + * + * @see {@link mapError} to transform only the error value + * @see {@link mapBoth} to transform both channels + * @see {@link flatMap} when `f` returns a `Result` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (ok: A) => A2): (self: Result) => Result + (self: Result, f: (ok: A) => A2): Result +} = dual( + 2, + (self: Result, f: (ok: A) => A2): Result => + isSuccess(self) ? succeed(f(self.success)) : fail(self.failure) +) + +/** + * Folds a `Result` into a single value by applying one of two functions. + * + * **When to use** + * + * Use when a `Result`'s success and failure branches should be collapsed into + * one plain output type. + * + * **Details** + * + * - Applies `onSuccess` if the result is a `Success` + * - Applies `onFailure` if the result is a `Failure` + * - Both branches must return the same type (or a common supertype) + * + * **Example** (Folding to a string) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const format = Result.match({ + * onSuccess: (n: number) => `Got ${n}`, + * onFailure: (e: string) => `Err: ${e}` + * }) + * + * console.log(format(Result.succeed(42))) + * // Output: "Got 42" + * + * console.log(format(Result.fail("timeout"))) + * // Output: "Err: timeout" + * ``` + * + * @see {@link merge} to extract `A | E` without mapping + * @see {@link getOrElse} to unwrap only the success with a fallback + * + * @category pattern matching + * @since 2.0.0 + */ +export const match: { + (options: { + readonly onFailure: (error: E) => B + readonly onSuccess: (ok: A) => C + }): (self: Result) => B | C + (self: Result, options: { + readonly onFailure: (error: E) => B + readonly onSuccess: (ok: A) => C + }): B | C +} = dual( + 2, + (self: Result, { onFailure, onSuccess }: { + readonly onFailure: (error: E) => B + readonly onSuccess: (ok: A) => C + }): B | C => isFailure(self) ? onFailure(self.failure) : onSuccess(self.success) +) + +/** + * Lifts a value into a `Result` based on a predicate or refinement. + * + * **When to use** + * + * Use to construct a `Result` from a raw value guarded by a predicate or + * refinement. + * + * **Details** + * + * - If the predicate returns `true`, the value becomes `Success` + * - If the predicate returns `false`, `orFailWith` produces the error for `Failure` + * - Also accepts a `Refinement` to narrow the success type + * - Supports both data-first and data-last (piped) usage + * + * **Example** (Validating a number) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const ensurePositive = pipe( + * 5, + * Result.liftPredicate( + * (n: number) => n > 0, + * (n) => `${n} is not positive` + * ) + * ) + * console.log(ensurePositive) + * // Output: { _tag: "Success", success: 5, ... } + * ``` + * + * @see {@link filterOrFail} to validate a value that is already in a `Result` + * @see {@link fromNullishOr} for nullable-based construction + * + * @category constructors + * @since 3.4.0 + */ +export const liftPredicate: { + (refinement: Refinement, orFailWith: (a: A) => E): (a: A) => Result + ( + predicate: Predicate, + orFailWith: (a: A) => E + ): (a: B) => Result + ( + self: A, + refinement: Refinement, + orFailWith: (a: A) => E + ): Result + ( + self: B, + predicate: Predicate, + orFailWith: (a: A) => E + ): Result +} = dual( + 3, + (a: A, predicate: Predicate, orFailWith: (a: A) => E): Result => + predicate(a) ? succeed(a) : fail(orFailWith(a)) +) + +/** + * Validates the success value of a `Result` using a predicate, failing with a + * custom error if the predicate returns `false`. + * + * **When to use** + * + * Use to validate an already-successful `Result` value with a predicate or + * refinement. + * + * **Details** + * + * - If the result is already a `Failure`, it is returned as-is + * - If the predicate passes, the `Success` is returned unchanged + * - If the predicate fails, `orFailWith` produces the error for a new `Failure` + * - Also accepts a `Refinement` to narrow the success type + * - The error type of the output is the union of both error types + * + * **Example** (Filtering a success value) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(0), + * Result.filterOrFail( + * (n) => n > 0, + * (n) => `${n} is not positive` + * ) + * ) + * console.log(result) + * // Output: { _tag: "Failure", failure: "0 is not positive", ... } + * ``` + * + * @see {@link liftPredicate} to create a `Result` from a raw value with a predicate + * @see {@link flatMap} for general conditional chaining + * + * @category filtering + * @since 4.0.0 + */ +export const filterOrFail: { + ( + refinement: Refinement, B>, + orFailWith: (value: NoInfer) => E2 + ): (self: Result) => Result + ( + predicate: Predicate>, + orFailWith: (value: NoInfer) => E2 + ): (self: Result) => Result + ( + self: Result, + refinement: Refinement, + orFailWith: (value: A) => E2 + ): Result + (self: Result, predicate: Predicate, orFailWith: (value: A) => E2): Result +} = dual(3, ( + self: Result, + predicate: Predicate, + orFailWith: (value: A) => E2 +): Result => flatMap(self, (a) => predicate(a) ? succeed(a) : fail(orFailWith(a)))) + +/** + * Unwraps a `Result` into `A | E` by returning the inner value regardless + * of whether it is a success or failure. + * + * **Details** + * + * - `Success` returns `A` + * - `Failure` returns `E` + * - Useful when both channels share a compatible type + * + * **Example** (Extracting the inner value) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.merge(Result.succeed(42))) + * // Output: 42 + * + * console.log(Result.merge(Result.fail("error"))) + * // Output: "error" + * ``` + * + * @see {@link match} to map each branch to a common type + * @see {@link getOrElse} to provide a fallback for failures + * + * @category getters + * @since 2.0.0 + */ +export const merge: (self: Result) => E | A = match({ onFailure: identity, onSuccess: identity }) + +/** + * Extracts the success value, or computes a fallback from the error. + * + * **When to use** + * + * Use when you need the success value from a `Result`, with a fallback computed + * from the failure value. + * + * **Details** + * + * - `Success` returns the inner value + * - `Failure` applies `onFailure` to the error and returns the result + * - The return type is `A | A2` (union of both branches) + * + * **Example** (Providing a fallback) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.getOrElse(Result.succeed(1), () => 0)) + * // Output: 1 + * + * console.log(Result.getOrElse(Result.fail("err"), () => 0)) + * // Output: 0 + * ``` + * + * @see {@link getOrNull} / {@link getOrUndefined} for simpler fallbacks + * @see {@link getOrThrow} to throw on failure + * @see {@link match} to map both branches + * @see {@link orElse} to recover with another Result instead of unwrapping + * + * @category getters + * @since 2.0.0 + */ +export const getOrElse: { + (onFailure: (err: E) => A2): (self: Result) => A2 | A + (self: Result, onFailure: (err: E) => A2): A | A2 +} = dual( + 2, + (self: Result, onFailure: (err: E) => A2): A | A2 => + isFailure(self) ? onFailure(self.failure) : self.success +) + +/** + * Extracts the success value, or returns `null` on failure. + * + * **When to use** + * + * Use when you need to pass failed `Result` values to APIs that represent + * absence as `null`. + * + * **Details** + * + * - `Success` returns `A` + * - `Failure` returns `null` + * + * **Example** (Unwrapping to nullable) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.getOrNull(Result.succeed(1))) + * // Output: 1 + * + * console.log(Result.getOrNull(Result.fail("err"))) + * // Output: null + * ``` + * + * @see {@link getOrUndefined} to return `undefined` instead + * @see {@link getOrElse} for a custom fallback + * + * @category getters + * @since 2.0.0 + */ +export const getOrNull: (self: Result) => A | null = getOrElse(constNull) + +/** + * Extracts the success value, or returns `undefined` on failure. + * + * **When to use** + * + * Use when you need to pass failed `Result` values to APIs that represent + * absence as `undefined`. + * + * **Details** + * + * - `Success` returns `A` + * - `Failure` returns `undefined` + * + * **Example** (Unwrapping to optional) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.getOrUndefined(Result.succeed(1))) + * // Output: 1 + * + * console.log(Result.getOrUndefined(Result.fail("err"))) + * // Output: undefined + * ``` + * + * @see {@link getOrNull} to return `null` instead + * @see {@link getOrElse} for a custom fallback + * + * @category getters + * @since 2.0.0 + */ +export const getOrUndefined: (self: Result) => A | undefined = getOrElse(constUndefined) + +/** + * Extracts the success value or throws a custom error derived from the failure. + * + * **When to use** + * + * Use when converting a `Result` into a thrown exception with a custom error + * message or error type. + * + * **Details** + * + * - `Success` returns `A` + * - `Failure` throws the value returned by `onFailure(e)` + * + * **Example** (Throwing a custom error) + * + * ```ts + * import { Result } from "effect" + * + * console.log( + * Result.getOrThrowWith(Result.succeed(1), () => new Error("fail")) + * ) + * // Output: 1 + * + * // This would throw: new Error("Unexpected: oops") + * // Result.getOrThrowWith( + * // Result.fail("oops"), + * // (err) => new Error(`Unexpected: ${err}`) + * // ) + * ``` + * + * @see {@link getOrThrow} to throw the raw failure value + * @see {@link getOrElse} for a non-throwing alternative + * + * @category getters + * @since 2.0.0 + */ +export const getOrThrowWith: { + (onFailure: (err: E) => unknown): (self: Result) => A + (self: Result, onFailure: (err: E) => unknown): A +} = dual(2, (self: Result, onFailure: (err: E) => unknown): A => { + if (isSuccess(self)) { + return self.success + } + throw onFailure(self.failure) +}) + +/** + * Extracts the success value or throws the raw failure value `E`. + * + * **When to use** + * + * Use when unchecked boundaries should turn failures into thrown exceptions. + * + * **Details** + * + * - `Success` returns `A` + * - `Failure` throws `E` directly + * - Use {@link getOrThrowWith} for a custom error object + * + * **Example** (Unwrapping or throwing) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.getOrThrow(Result.succeed(1))) + * // Output: 1 + * + * // This would throw the string "error": + * // Result.getOrThrow(Result.fail("error")) + * ``` + * + * @see {@link getOrThrowWith} for custom error mapping + * @see {@link getOrElse} for a non-throwing alternative + * + * @category getters + * @since 2.0.0 + */ +export const getOrThrow: (self: Result) => A = getOrThrowWith(identity) + +/** + * Returns the original `Result` if it is a `Success`, otherwise applies + * `that` to the error and returns the resulting `Result`. + * + * **When to use** + * + * Use when a failure should recover into another `Result` while keeping + * successes unchanged. + * + * **Details** + * + * - `Success` is returned unchanged + * - `Failure` calls `that(e)` to produce a new `Result` + * + * **Example** (Recovering from a failure) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.fail("primary failed"), + * Result.orElse(() => Result.succeed(99)) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: 99, ... } + * ``` + * + * @see {@link getOrElse} to unwrap with a fallback value (not a Result) + * @see {@link mapError} to transform the error without recovering + * + * @category error handling + * @since 2.0.0 + */ +export const orElse: { + (that: (err: E) => Result): (self: Result) => Result + (self: Result, that: (err: E) => Result): Result +} = dual( + 2, + (self: Result, that: (err: E) => Result): Result => + isFailure(self) ? that(self.failure) : succeed(self.success) +) + +/** + * Chains a function that returns a `Result` onto a successful value. + * + * **When to use** + * + * Use to sequence `Result`-returning computations that should short-circuit on + * failure. + * + * **Details** + * + * - If `self` is a `Success`, applies `f` to the value and returns the resulting `Result` + * - If `self` is a `Failure`, short-circuits and returns it unchanged + * - The error types are merged into a union (`E | E2`) + * - This is the monadic `bind` / `>>=` for `Result` + * + * **Example** (Validating sequentially) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(5), + * Result.flatMap((n) => + * n > 0 ? Result.succeed(n * 2) : Result.fail("not positive") + * ) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: 10, ... } + * ``` + * + * @see {@link andThen} for a more flexible variant that also accepts plain values + * @see {@link map} when `f` does not return a `Result` + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + (f: (a: A) => Result): (self: Result) => Result + (self: Result, f: (a: A) => Result): Result +} = dual( + 2, + (self: Result, f: (a: A) => Result): Result => + isFailure(self) ? fail(self.failure) : f(self.success) +) + +/** + * Provides a flexible variant of {@link flatMap} that accepts multiple input shapes. + * + * **When to use** + * + * Use to sequence a next step that may be a `Result`, a function, or a plain + * value. + * + * **Details** + * + * The second argument can be: + * - A function `(a: A) => Result` (same as `flatMap`) + * - A function `(a: A) => A2` (auto-wrapped in `succeed`) + * - A `Result` value (ignores the success of `self`) + * - A plain value `A2` (auto-wrapped in `succeed`, ignores `self`) + * + * If `self` is a `Failure`, the second argument is never evaluated. + * + * **Example** (Chaining Result values with different argument types) + * + * ```ts + * import { pipe, Result } from "effect" + * + * // With a function returning a Result + * const a = pipe( + * Result.succeed(1), + * Result.andThen((n) => Result.succeed(n + 1)) + * ) + * + * // With a plain mapping function + * const b = pipe( + * Result.succeed(1), + * Result.andThen((n) => n + 1) + * ) + * + * // With a constant value + * const c = pipe(Result.succeed(1), Result.andThen("done")) + * + * console.log(a, b, c) + * ``` + * + * @see {@link flatMap} for the stricter variant (function returning Result only) + * @see {@link map} when you always return a plain value + * + * @category sequencing + * @since 2.0.0 + */ +export const andThen: { + (f: (a: A) => Result): (self: Result) => Result + (f: Result): (self: Result) => Result + (f: (a: A) => A2): (self: Result) => Result + (right: NotFunction): (self: Result) => Result + (self: Result, f: (a: A) => Result): Result + (self: Result, f: Result): Result + (self: Result, f: (a: A) => A2): Result + (self: Result, f: NotFunction): Result +} = dual( + 2, + ( + self: Result, + f: ((a: A) => Result | A2) | Result | A2 + ): Result => + flatMap(self, (a) => { + const out = isFunction(f) ? f(a) : f + return isResult(out) ? out : succeed(out) + }) +) + +/** + * Collects a structure of `Result`s into a single `Result` of collected values. + * + * **When to use** + * + * Use to collect independent `Result` values into one `Result` while preserving + * the original structure. + * + * **Details** + * + * Accepts: + * - A tuple/array: returns `Result` with a tuple/array of success values + * - A struct (record): returns `Result` with a struct of success values + * - An iterable: returns `Result` with an array of success values + * + * Short-circuits on the first `Failure` encountered; later elements are not inspected. + * + * **Example** (Collecting a tuple and a struct) + * + * ```ts + * import { Result } from "effect" + * + * // Tuple + * const tuple = Result.all([Result.succeed(1), Result.succeed("two")]) + * console.log(tuple) + * // Output: { _tag: "Success", success: [1, "two"], ... } + * + * // Struct + * const struct = Result.all({ x: Result.succeed(1), y: Result.fail("err") }) + * console.log(struct) + * // Output: { _tag: "Failure", failure: "err", ... } + * ``` + * + * @see {@link flatMap} for chaining two Results sequentially + * @see {@link gen} for generator-based composition of multiple Results + * + * @category sequencing + * @since 2.0.0 + */ +// @ts-expect-error +export const all: > | Record>>( + input: I +) => [I] extends [ReadonlyArray>] ? Result< + { -readonly [K in keyof I]: [I[K]] extends [Result] ? R : never }, + I[number] extends never ? never : [I[number]] extends [Result] ? L : never + > + : [I] extends [Iterable>] ? Result, L> + : Result< + { -readonly [K in keyof I]: [I[K]] extends [Result] ? R : never }, + I[keyof I] extends never ? never : [I[keyof I]] extends [Result] ? L : never + > = ( + input: Iterable> | Record> + ): Result => { + if (Symbol.iterator in input) { + const out: Array> = [] + for (const e of input) { + if (isFailure(e)) { + return e + } + out.push(e.success) + } + return succeed(out) + } + + const out: Record = {} + for (const key of Object.keys(input)) { + const e = input[key] + if (isFailure(e)) { + return e + } + out[key] = e.success + } + return succeed(out) + } + +/** + * Swaps the success and failure channels of a `Result`. + * + * **When to use** + * + * Use to swap channels when failure-focused operations are easier through + * success-oriented combinators. + * + * **Details** + * + * - `Success` becomes `Failure` (i.e., `Result`) + * - `Failure` becomes `Success` (i.e., `Result`) + * - Useful when you want to apply success-oriented operations (like `map`) + * to the error channel, then flip back + * + * **Example** (Swapping channels) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.flip(Result.succeed(42))) + * // Output: { _tag: "Failure", failure: 42, ... } + * + * console.log(Result.flip(Result.fail("error"))) + * // Output: { _tag: "Success", success: "error", ... } + * ``` + * + * @see {@link mapError} to transform the error without swapping + * + * @category transforming + * @since 2.0.0 + */ +export const flip = (self: Result): Result => + isFailure(self) ? succeed(self.failure) : fail(self.success) + +/** + * Provides generator-based syntax for composing `Result` values sequentially. + * + * **When to use** + * + * Use when you need generator syntax to compose sequential `Result` + * computations instead of nested `flatMap` calls. + * + * **Details** + * + * - Use `yield*` to unwrap a `Result` inside the generator; if any yielded + * `Result` is a `Failure`, the generator short-circuits and returns that failure + * - The return value of the generator is wrapped in `Success` + * - Evaluated eagerly and synchronously (unlike `Effect.gen`) + * + * **Example** (Composing multiple Results) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.gen(function*() { + * const a = yield* Result.succeed(1) + * const b = yield* Result.succeed(2) + * return a + b + * }) + * + * console.log(result) + * // Output: { _tag: "Success", success: 3, ... } + * ``` + * + * @see {@link flatMap} for point-free sequential composition + * @see {@link all} to collect multiple independent Results + * + * @category generators + * @since 2.0.0 + */ +export const gen: Gen.Gen = (...args) => { + const f = args.length === 1 ? args[0] : args[1].bind(args[0]) + const iterator = f() + let state: IteratorResult = iterator.next() + while (!state.done) { + const current = state.value + if (isFailure(current)) { + return current + } + state = iterator.next(current.success as never) + } + return succeed(state.value) as any +} + +// ------------------------------------------------------------------------------------- +// do notation +// ------------------------------------------------------------------------------------- + +/** + * Provides the starting point for the "do notation" simulation with `Result`. + * + * **When to use** + * + * Use to start a `Result` do-notation pipeline from an empty successful record + * before adding named fields from `Result`-producing computations and pure + * computed values. + * + * **Details** + * + * Creates a `Result<{}>` (success with an empty object). Use with + * {@link bind} to add `Result`-producing fields and {@link let_ let} + * to add pure computed fields. + * + * **Example** (Building an object step by step) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.Do, + * Result.bind("x", () => Result.succeed(2)), + * Result.bind("y", () => Result.succeed(3)), + * Result.let("sum", ({ x, y }) => x + y) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: { x: 2, y: 3, sum: 5 }, ... } + * ``` + * + * @see {@link bind} to add Result-producing fields + * @see {@link let_ let} to add pure computed fields + * @see {@link gen} for an alternative generator-based syntax + * @see {@link bindTo} for starting a do-notation chain from an existing Result + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Result<{}> = succeed({}) + +/** + * Adds a named field to the do-notation accumulator by running a `Result`-producing + * function that receives the current accumulated object. + * + * **When to use** + * + * Use when you need to add a `Result`-producing step to a `Result` + * do-notation pipeline and store its successful value under a named field in + * the accumulated object. + * + * **Details** + * + * - Short-circuits on the first `Failure` + * - The field name must not collide with existing keys + * - Use {@link let_ let} for pure (non-Result) computed fields + * + * **Example** (Binding Result values) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.Do, + * Result.bind("x", () => Result.succeed(2)), + * Result.bind("y", ({ x }) => Result.succeed(x + 3)) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: { x: 2, y: 5 }, ... } + * ``` + * + * @see {@link Do} to start the do-notation chain + * @see {@link let_ let} for pure computed fields + * @see {@link bindTo} to wrap an initial Result into a named field + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + name: Exclude, + f: (a: NoInfer) => Result + ): (self: Result) => Result<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, L1 | L2> + ( + self: Result, + name: Exclude, + f: (a: NoInfer) => Result + ): Result<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, L1 | L2> +} = doNotation.bind(map, flatMap) + +/** + * Wraps the success value of a `Result` into a named field, producing a + * `Result>`. + * + * **When to use** + * + * Use to name the success value of an existing `Result` before continuing a + * do-notation pipeline. + * + * **Details** + * + * This is typically used to start a do-notation chain from an existing + * `Result`. + * + * **Example** (Wrapping a value into a named field) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(42), + * Result.bindTo("answer") + * ) + * console.log(result) + * // Output: { _tag: "Success", success: { answer: 42 }, ... } + * ``` + * + * @see {@link Do} to start from an empty object + * @see {@link bind} to add more fields + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Result) => Result, L> + (self: Result, name: N): Result, L> +} = doNotation.bindTo(map) + +const let_: { + ( + name: Exclude, + f: (r: NoInfer) => B + ): (self: Result) => Result<{ [K in N | keyof R]: K extends keyof R ? R[K] : B }, L> + ( + self: Result, + name: Exclude, + f: (r: NoInfer) => B + ): Result<{ [K in N | keyof R]: K extends keyof R ? R[K] : B }, L> +} = doNotation.let_(map) + +export { + /** + * Adds a named field to the do-notation accumulator by computing a pure + * (non-Result) value from the current accumulated object. + * + * **When to use** + * + * Use when you need to add a derived field that cannot fail inside a + * do-notation pipeline. + * + * **Details** + * + * - Use {@link bind} when the computation returns a `Result` + * - The field name must not collide with existing keys + * + * **Example** (Adding a computed field) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.Do, + * Result.bind("x", () => Result.succeed(2)), + * Result.bind("y", () => Result.succeed(3)), + * Result.let("sum", ({ x, y }) => x + y) + * ) + * console.log(result) + * // Output: { _tag: "Success", success: { x: 2, y: 3, sum: 5 }, ... } + * ``` + * + * @see {@link Do} to start the do-notation chain + * @see {@link bind} for Result-producing fields + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Transforms `Option>` into `Result, E>`. + * + * **When to use** + * + * Use when optional absence should be treated as a successful `None`, while an + * inner `Result` failure should still fail the whole result. + * + * **Details** + * + * - `None` becomes `Success(None)` + * - `Some(Success(a))` becomes `Success(Some(a))` + * - `Some(Failure(e))` becomes `Failure(e)` + * + * **Example** (Transposing an Option of a Result) + * + * ```ts + * import { Option, Result } from "effect" + * + * const some = Option.some(Result.succeed(42)) + * console.log(Result.transposeOption(some)) + * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * + * const none = Option.none>() + * console.log(Result.transposeOption(none)) + * // Output: { _tag: "Success", success: { _tag: "None" }, ... } + * ``` + * + * @see {@link transposeMapOption} to map and transpose in one step + * + * @category Transposing + * @since 3.14.0 + */ +export const transposeOption = ( + self: Option> +): Result, E> => { + return option_.isNone(self) ? succeedNone : map(self.value, option_.some) +} + +/** + * Maps an `Option` value with a `Result`-producing function, then transposes + * the structure from `Option>` to `Result, E>`. + * + * **When to use** + * + * Use when an optional value should be validated only when present, preserving + * absence as a successful `None`. + * + * **Details** + * + * - `None` becomes `Success(None)` (the function is never called) + * - `Some(a)` where `f(a)` is `Success(b)` becomes `Success(Some(b))` + * - `Some(a)` where `f(a)` is `Failure(e)` becomes `Failure(e)` + * + * **Example** (Mapping and transposing in one step) + * + * ```ts + * import { Option, Result } from "effect" + * + * const parse = (s: string) => + * isNaN(Number(s)) + * ? Result.fail("not a number" as const) + * : Result.succeed(Number(s)) + * + * console.log(Result.transposeMapOption(Option.some("42"), parse)) + * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * + * console.log(Result.transposeMapOption(Option.none(), parse)) + * // Output: { _tag: "Success", success: { _tag: "None" }, ... } + * ``` + * + * @see {@link transposeOption} when the Option already contains a Result + * + * @category Transposing + * @since 3.15.0 + */ +export const transposeMapOption = dual< + ( + f: (self: A) => Result + ) => (self: Option) => Result, E>, + ( + self: Option, + f: (self: A) => Result + ) => Result, E> +>(2, (self, f) => option_.isNone(self) ? succeedNone : map(f(self.value), option_.some)) + +/** + * Provides a pre-built `Result>` that succeeds with `None`. + * + * **When to use** + * + * Use when an optional success should be absent, such as the `None` branch of + * `transposeOption` or `transposeMapOption`. + * + * **Details** + * + * This is equivalent to `Result.succeed(Option.none())`, but reuses a shared + * `Success` wrapper instead of allocating one each time. + * + * **Example** (Succeeding with None) + * + * ```ts + * import { Result } from "effect" + * + * console.log(Result.isSuccess(Result.succeedNone)) + * // Output: true + * ``` + * + * @see {@link succeedSome} for the `Some` counterpart + * @see {@link transposeOption} to transpose an Option that already contains a Result + * @see {@link transposeMapOption} to map and transpose an Option in one step + * + * @category constructors + * @since 4.0.0 + */ +export const succeedNone = succeed(option_.none) + +/** + * Creates a `Result>` that succeeds with `Some(a)`. + * + * **Details** + * + * - Equivalent to `Result.succeed(Option.some(a))` + * - Useful with {@link transposeOption} patterns + * + * **Example** (Wrapping a value in Some inside a Result) + * + * ```ts + * import { Result } from "effect" + * + * const result = Result.succeedSome(42) + * console.log(result) + * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * ``` + * + * @see {@link succeedNone} for the `None` counterpart + * + * @category constructors + * @since 4.0.0 + */ +export const succeedSome = (a: A): Result, E> => succeed(option_.some(a)) + +/** + * Runs a side-effect on the success value without altering the `Result`. + * + * **Details** + * + * - If the result is a `Success`, calls `f` with the value (return value is ignored) + * - If the result is a `Failure`, `f` is not called + * - Returns the original `Result` unchanged (same reference) + * - Useful for logging, debugging, or performing mutations outside the Result chain + * + * **Example** (Logging a success value) + * + * ```ts + * import { pipe, Result } from "effect" + * + * const result = pipe( + * Result.succeed(42), + * Result.tap((n) => console.log("Got:", n)) + * ) + * // Output: "Got: 42" + * + * console.log(Result.isSuccess(result)) + * // Output: true + * ``` + * + * @see {@link map} to transform the success value + * + * @category mapping + * @since 4.0.0 + */ +export const tap: { + (f: (a: A) => void): (self: Result) => Result + (self: Result, f: (a: A) => void): Result +} = dual( + 2, + (self: Result, f: (a: A) => void): Result => { + if (isSuccess(self)) { + f(self.success) + } + return self + } +) diff --git a/repos/effect/packages/effect/src/Runtime.ts b/repos/effect/packages/effect/src/Runtime.ts new file mode 100644 index 0000000..001c81a --- /dev/null +++ b/repos/effect/packages/effect/src/Runtime.ts @@ -0,0 +1,432 @@ +/** + * Helpers for turning an `Effect` program into a host application's main entry + * point. This module is the low-level layer used by platform adapters to run a + * main effect, observe its fiber, report unhandled failures, and translate the + * resulting `Exit` into an application or process exit code. It provides + * `makeRunMain`, the default teardown behavior, and error markers for custom + * exit codes and already-reported failures. Application code usually calls the + * platform-provided runner instead of using this module directly. + * + * @since 4.0.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { constVoid, dual } from "effect/Function" +import type * as Fiber from "./Fiber.ts" + +/** + * Represents a teardown function that handles program completion and determines the exit code. + * + * **When to use** + * + * Use when integrating {@link makeRunMain} with a host platform that needs to + * translate an Effect `Exit` into a process, worker, or application exit code. + * + * **Details** + * + * A teardown function is called when an Effect program completes, either + * successfully or with a failure. It determines the appropriate exit code and + * can perform cleanup before invoking the supplied `onExit` callback. + * + * **Example** (Customizing teardown behavior) + * + * ```ts + * import { Effect, Exit, Runtime } from "effect" + * + * // Custom teardown that logs completion status + * const customTeardown: Runtime.Teardown = (exit, onExit) => { + * if (Exit.isSuccess(exit)) { + * console.log("Program completed successfully with value:", exit.value) + * onExit(0) + * } else { + * console.log("Program failed with cause:", exit.cause) + * onExit(1) + * } + * } + * + * // Use with makeRunMain + * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { + * fiber.addObserver((exit) => { + * teardown(exit, (code) => { + * console.log(`Exiting with code: ${code}`) + * }) + * }) + * }) + * + * const program = Effect.succeed("Hello, World!") + * runMain(program, { teardown: customTeardown }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Teardown { + (exit: Exit.Exit, onExit: (code: number) => void): void +} + +/** + * The default teardown function that determines exit codes from an Effect exit. + * + * **When to use** + * + * Use as the standard teardown for main programs with conventional process + * exit codes and support for {@link errorExitCode}. + * + * **Details** + * + * This teardown follows these exit-code rules: + * + * - `0` for successful completion. + * - `130` for interruption-only failures. + * - The squashed error's {@link errorExitCode} value for other failures when + * present. + * - `1` for other failures. + * + * **Gotchas** + * + * The `130` code is used only when the Cause contains interruptions and no + * other failure reasons. Mixed causes use the squashed error path instead. + * + * **Example** (Referencing default teardown) + * + * ```ts + * import { Exit, Runtime } from "effect" + * + * const logExitCode = (exit: Exit.Exit) => { + * Runtime.defaultTeardown(exit, (code) => { + * console.log(`Exit code: ${code}`) + * }) + * } + * + * logExitCode(Exit.succeed(42)) + * // Output: Exit code: 0 + * + * logExitCode(Exit.fail("error")) + * // Output: Exit code: 1 + * + * logExitCode(Exit.interrupt(123)) + * // Output: Exit code: 130 + * ``` + * + * @see {@link errorExitCode} for customizing failure exit codes + * + * @category running + * @since 4.0.0 + */ +export const defaultTeardown: Teardown = ( + exit: Exit.Exit, + onExit: (code: number) => void +) => { + if (Exit.isSuccess(exit)) return onExit(0) + if (Cause.hasInterruptsOnly(exit.cause)) return onExit(130) + return onExit(getErrorExitCode(Cause.squash(exit.cause))) +} + +/** + * Creates a platform-specific main program runner that handles Effect execution lifecycle. + * + * **When to use** + * + * Use when building a runtime adapter for a host platform. + * + * **Details** + * + * The runner executes Effect programs as main entry points. The provided + * function receives a forked fiber and a teardown callback so it can install + * platform-specific signal handling, fiber observers, and final exit behavior. + * + * Most applications should use a platform-provided runner, such as + * `NodeRuntime.runMain`, rather than constructing one directly. + * + * `disableErrorReporting` disables the automatic log emitted for unreported + * non-interruption failures. It does not change exit-code calculation or the + * custom teardown callback. + * + * **Gotchas** + * + * The setup function is responsible for observing the fiber and eventually + * invoking teardown. `makeRunMain` also tries to keep the host process alive + * with a long interval while the main fiber is running; if the host blocks + * timers, the runner still starts but cannot use that keep-alive fallback. + * + * **Example** (Creating platform runners) + * + * ```ts + * import { Effect, Fiber, Runtime } from "effect" + * + * // Create a simple runner for a hypothetical platform + * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { + * // Set up signal handling + * const handleSignal = () => { + * Effect.runSync(Fiber.interrupt(fiber)) + * } + * + * // Add signal listeners (platform-specific) + * // process.on('SIGINT', handleSignal) + * // process.on('SIGTERM', handleSignal) + * + * // Handle fiber completion + * fiber.addObserver((exit) => { + * teardown(exit, (code) => { + * console.log(`Program finished with exit code: ${code}`) + * // process.exit(code) + * }) + * }) + * }) + * + * // Use the runner + * const program = Effect.gen(function*() { + * yield* Effect.log("Starting program") + * yield* Effect.sleep(1000) + * yield* Effect.log("Program completed") + * return "success" + * }) + * + * // Run with default options + * runMain(program) + * + * // Run with custom teardown + * runMain(program, { + * teardown: (exit, onExit) => { + * console.log("Custom teardown logic") + * Runtime.defaultTeardown(exit, onExit) + * } + * }) + * ``` + * + * @category running + * @since 4.0.0 + */ +export const makeRunMain = ( + f: ( + options: { + readonly fiber: Fiber.Fiber + readonly teardown: Teardown + } + ) => void +): { + ( + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly teardown?: Teardown | undefined + } + ): (effect: Effect.Effect) => void + ( + effect: Effect.Effect, + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly teardown?: Teardown | undefined + } + ): void +} => + dual((args) => Effect.isEffect(args[0]), (effect: Effect.Effect, options?: { + readonly disableErrorReporting?: boolean | undefined + readonly teardown?: Teardown | undefined + }) => { + const fiber = options?.disableErrorReporting === true + ? Effect.runFork(effect) + : Effect.runFork( + Effect.tapCause(effect, (cause) => { + if (Cause.hasInterruptsOnly(cause)) return Effect.void + const isReported = getErrorReported(Cause.squash(cause)) + return isReported ? Effect.logError(cause) : Effect.void + }) + ) + try { + const keepAlive = globalThis.setInterval(constVoid, 2_147_483_647) + fiber.addObserver(() => { + clearInterval(keepAlive) + }) + } catch {} + const teardown = options?.teardown ?? defaultTeardown + return f({ fiber, teardown }) + }) + +declare global { + interface Error { + readonly [errorExitCode]?: number + readonly [errorReported]?: boolean + } +} + +/** + * Type-level key for the `Runtime.errorExitCode` marker. + * + * **When to use** + * + * Use to type properties keyed by `Runtime.errorExitCode` on custom error + * values. + * + * @category symbols + * @since 4.0.0 + */ +export type errorExitCode = "~effect/Runtime/errorExitCode" + +/** + * Allows associating an exit code with an error for determining the process + * exit code on failure. + * + * **When to use** + * + * Use when error classes should map failures to a specific process exit code + * when handled by {@link defaultTeardown}. + * + * **Details** + * + * Attach this marker as a readonly property on an error object. When the main + * program fails, {@link defaultTeardown} squashes the Cause and reads the marker + * from the resulting error value. + * + * **Gotchas** + * + * The marker is read from the squashed failure value. If a Cause contains + * multiple failures, the selected squashed error determines the exit code. + * + * **Example** (Setting a process exit code) + * + * ```ts + * import { Data, Effect, Runtime } from "effect" + * import { NodeRuntime } from "@effect/platform-node" + * + * class MyError extends Data.TaggedError("MyError") { + * readonly [Runtime.errorExitCode] = 42 + * } + * + * // If the program fails with MyError, the process will exit with code 42 + * NodeRuntime.runMain(Effect.fail(new MyError())) + * ``` + * + * @see {@link errorReported} for controlling automatic error logging + * @see {@link defaultTeardown} for the default failure exit-code rules that read this marker + * @see {@link getErrorExitCode} for reading the marker from unknown error values + * + * @category symbols + * @since 4.0.0 + */ +export const errorExitCode: errorExitCode = "~effect/Runtime/errorExitCode" + +/** + * Reads the runtime exit-code marker from an unknown error value. + * + * **When to use** + * + * Use to read a custom failure exit code from an unknown error value, falling + * back to the default failure code. + * + * **Details** + * + * Returns the numeric `[Runtime.errorExitCode]` property when it is present on + * an object. Otherwise returns `1`, the default failure exit code used by + * `defaultTeardown`. + * + * **Gotchas** + * + * Non-object values, missing markers, and non-number marker values all return + * `1`. + * + * @see {@link errorExitCode} for the marker read by this function + * + * @category accessors + * @since 4.0.0 + */ +export const getErrorExitCode = (u: unknown): number => { + if (typeof u === "object" && u !== null && errorExitCode in u) { + const code = u[errorExitCode] + if (typeof code === "number") { + return code + } + } + return 1 +} + +/** + * Type-level key for the `Runtime.errorReported` marker. + * + * **When to use** + * + * Use to type properties keyed by `Runtime.errorReported` on custom error + * values. + * + * @category symbols + * @since 4.0.0 + */ +export type errorReported = "~effect/Runtime/errorReported" + +/** + * Defines the runtime marker that controls default `runMain` error logging for an error. + * + * **When to use** + * + * Use when you need error classes reported by application code to avoid being + * logged again by the default main runner. + * + * **Details** + * + * Set `[Runtime.errorReported]` to `false` on an error object to suppress the + * runtime log because the error has already been reported. Omitted or + * non-boolean values are treated as `true`, so failures are logged by default. + * + * **Gotchas** + * + * This marker controls only automatic error logging. It does not change the + * failure Cause or the process exit code. + * `makeRunMain` reads the marker from `Cause.squash(cause)`, so for causes + * with multiple failures, the squashed error determines whether default logging + * is suppressed. + * + * **Example** (Suppressing error reporting) + * + * ```ts + * import { Data, Effect, Runtime } from "effect" + * import { NodeRuntime } from "@effect/platform-node" + * + * class MyError extends Data.TaggedError("MyError") { + * readonly [Runtime.errorReported] = false + * } + * + * // If the program fails with MyError, the process will exit with code 1 but + * // no error will be logged. + * NodeRuntime.runMain(Effect.fail(new MyError())) + * ``` + * + * @see {@link errorExitCode} for controlling failure exit codes + * @see {@link getErrorReported} for reading the marker from unknown error values + * + * @category symbols + * @since 4.0.0 + */ +export const errorReported: errorReported = "~effect/Runtime/errorReported" + +/** + * Reads the runtime error-reporting marker from an unknown error value. + * + * **When to use** + * + * Use to read whether an unknown error value should be treated as already + * reported by the default main runner. + * + * **Details** + * + * Returns a boolean `[Runtime.errorReported]` property when it is present on an + * object. Otherwise returns `true`, so failures are logged by default. + * + * **Gotchas** + * + * Non-object values, missing markers, and non-boolean marker values all return + * `true`. + * + * @see {@link errorReported} for the marker read by this function + * + * @category accessors + * @since 4.0.0 + */ +export const getErrorReported = (u: unknown): boolean => { + if (typeof u === "object" && u !== null && errorReported in u) { + const isReported = u[errorReported] + if (typeof isReported === "boolean") { + return isReported + } + } + return true +} diff --git a/repos/effect/packages/effect/src/Schedule.ts b/repos/effect/packages/effect/src/Schedule.ts new file mode 100644 index 0000000..b311b48 --- /dev/null +++ b/repos/effect/packages/effect/src/Schedule.ts @@ -0,0 +1,2186 @@ +/** + * Describes policies for retrying, repeating, and pacing Effect programs. + * + * A `Schedule` is stepped with an input value. Each + * step either stops or produces an output together with the delay before the + * next step. Schedules are used by retry, repeat, stream, and channel APIs to + * decide when work should continue, how long to wait, and when to stop. + * + * @since 2.0.0 + */ +import type { NonEmptyReadonlyArray } from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Context from "./Context.ts" +import * as Cron from "./Cron.ts" +import type * as DateTime from "./DateTime.ts" +import * as Duration from "./Duration.ts" +import type { Effect } from "./Effect.ts" +import { constant, dual, identity } from "./Function.ts" +import { isEffect } from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as random from "./internal/random.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as Pull from "./Pull.ts" +import * as Result from "./Result.ts" +import type { Contravariant, Covariant, Mutable, UnionToIntersection } from "./Types.ts" + +const TypeId = "~effect/Schedule" + +const randomNext: Effect = random.Random.useSync((random) => random.nextDoubleUnsafe()) + +/** + * A Schedule defines a strategy for repeating or retrying effects based on some policy. + * + * **Example** (Defining retry and repeat schedules) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class NetworkError extends Data.TaggedError("NetworkError")<{ + * readonly attempt: number + * }> {} + * + * // Basic retry schedule - retry up to 3 times with exponential backoff + * const retrySchedule = Schedule.max([ + * Schedule.exponential("100 millis"), + * Schedule.recurs(3) + * ]) + * + * // Basic repeat schedule - repeat every 30 seconds forever + * const repeatSchedule: Schedule.Schedule = Schedule + * .spaced("30 seconds") + * + * const program = Effect.gen(function*() { + * let attempts = 0 + * + * const result1 = yield* Effect.retry( + * Effect.gen(function*() { + * attempts++ + * if (attempts < 3) { + * return yield* Effect.fail(new NetworkError({ attempt: attempts })) + * } + * return "Success" + * }), + * retrySchedule + * ) + * console.log(result1) // "Success" + * + * yield* Console.log("heartbeat").pipe( + * Effect.repeat(repeatSchedule.pipe(Schedule.upTo({ times: 5 }))) + * ) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Schedule + extends Schedule.Variance, Pipeable +{} + +/** + * Metadata provided to schedule functions containing timing and input information. + * + * @category metadata + * @since 4.0.0 + */ +export interface InputMetadata { + readonly input: Input + readonly attempt: number + readonly start: number + readonly now: number + readonly elapsed: number + readonly elapsedSincePrevious: number +} + +/** + * Extended metadata that includes both input metadata and the output value from the schedule. + * + * @category metadata + * @since 4.0.0 + */ +export interface Metadata extends InputMetadata { + readonly output: Output + readonly duration: Duration.Duration +} + +/** + * Context reference containing metadata for the currently running schedule step. + * + * **Details** + * + * Repeat, retry, stream, and channel scheduling operations provide this service + * to effects run between schedule steps. The default value contains undefined + * input and output values, zero duration, and zeroed timing fields before any + * schedule step has produced metadata. + * + * @category metadata + * @since 4.0.0 + */ +export const CurrentMetadata = Context.Reference("effect/Schedule/CurrentMetadata", { + defaultValue: constant({ + input: undefined, + output: undefined, + duration: Duration.zero, + attempt: 0, + start: 0, + now: 0, + elapsed: 0, + elapsedSincePrevious: 0 + }) +}) + +/** + * The Schedule namespace contains types and utilities for working with schedules. + * + * @since 2.0.0 + */ +export declare namespace Schedule { + /** + * Variance interface that defines the type parameter relationships for Schedule. + * + * **Example** (Understanding schedule variance) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * // Understanding Schedule variance: + * // - Output: covariant (can be a subtype) + * // - Input: contravariant (can accept supertypes) + * // - Error: covariant (can be a subtype) + * // - Env: covariant (can be a subtype) + * + * // Schedule that produces strings, accepts any input + * const stringSchedule = Schedule.spaced("1 second").pipe( + * Schedule.map(() => Effect.succeed("tick")) + * ) + * + * // Schedule that only accepts Error inputs + * const errorSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.upTo({ times: 5 }) + * ) + * + * // Schedule requiring a service environment + * const serviceSchedule = Schedule.spaced("5 seconds") + * ``` + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: VarianceStruct + } + + /** + * Type-level marker used by `Schedule.Variance` to record the variance of + * `Schedule` type parameters. + * + * **Details** + * + * This interface exists for TypeScript inference and assignability. Users + * normally do not construct or inspect it directly. + * + * @category models + * @since 4.0.0 + */ + export interface VarianceStruct { + readonly _Out: Covariant + readonly _In: Contravariant + readonly _Error: Covariant + readonly _Env: Covariant + } +} + +/** + * Extracts the output type from a `Schedule`. + * + * @category type extractors + * @since 4.0.0 + */ +export type Output = S extends Schedule ? Output : never + +/** + * Extracts the input type from a `Schedule`. + * + * @category type extractors + * @since 4.0.0 + */ +export type Input = S extends Schedule ? Input : never + +/** + * Extracts the error type from a `Schedule`. + * + * @category type extractors + * @since 4.0.0 + */ +export type Error = S extends Schedule ? Error : never + +/** + * Extracts the service requirements from a `Schedule`. + * + * @category type extractors + * @since 4.0.0 + */ +export type Env = S extends Schedule ? Env : never + +const ScheduleProto = { + [TypeId]: { + _Out: identity, + _In: identity, + _Env: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Type guard that checks if a value is a Schedule. + * + * **Example** (Checking for schedules) + * + * ```ts + * import { Schedule } from "effect" + * + * const schedule = Schedule.exponential("100 millis") + * const notSchedule = { foo: "bar" } + * + * console.log(Schedule.isSchedule(schedule)) // true + * console.log(Schedule.isSchedule(notSchedule)) // false + * console.log(Schedule.isSchedule(null)) // false + * console.log(Schedule.isSchedule(undefined)) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isSchedule = (u: unknown): u is Schedule => hasProperty(u, TypeId) + +/** + * Creates a Schedule from a step function that returns a Pull. + * + * **Example** (Creating a custom schedule from a step function) + * + * ```ts + * import { Cause, Duration, Effect, Schedule } from "effect" + * + * const schedule = Schedule.fromStep(Effect.sync(() => { + * let count = 0 + * + * return (_now: number, _input: string) => { + * if (count >= 3) { + * return Cause.done(count) + * } + * return Effect.succeed([count++, Duration.millis(100)] as [number, Duration.Duration]) + * } + * })) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromStep = ( + step: Effect< + (now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>, + Error, + Env + > +): Schedule, Env | EnvX> => { + const self = Object.create(ScheduleProto) + self.step = step + return self +} + +const metadataFn = () => { + let n = 0 + let previous: number | undefined + let start: number | undefined + return (now: number, input: In): InputMetadata => { + if (start === undefined) start = now + const elapsed = now - start + const elapsedSincePrevious = previous === undefined ? 0 : now - previous + previous = now + return { input, attempt: ++n, start, now, elapsed, elapsedSincePrevious } + } +} + +/** + * Creates a Schedule from a step function that receives metadata about the schedule's execution. + * + * **Example** (Creating a metadata-aware schedule) + * + * ```ts + * import { Cause, Duration, Effect, Schedule } from "effect" + * + * const firstThreeInputs = Schedule.fromStepWithMetadata(Effect.succeed((metadata: Schedule.InputMetadata) => { + * if (metadata.attempt > 3) { + * return Cause.done("finished") + * } + * + * return Effect.succeed([ + * `attempt ${metadata.attempt}: ${metadata.input}`, + * Duration.millis(250) + * ] as [string, Duration.Duration]) + * })) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromStepWithMetadata = ( + step: Effect< + (options: InputMetadata) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>, + Error, + Env + > +): Schedule, Env | EnvX> => + fromStep(effect.map(step, (f) => { + const meta = metadataFn() + return (now, input) => f(meta(now, input)) + })) + +/** + * Extracts the step function from a Schedule. + * + * **Example** (Extracting a schedule step function) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * // Extract step function from an existing schedule + * const schedule = Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) + * + * const program = Effect.gen(function*() { + * const stepFn = yield* Schedule.toStep(schedule) + * + * // Use the step function directly for custom logic. The timestamp is + * // supplied by the caller, so tests can pass a deterministic value. + * const now = 0 + * const result = yield* stepFn(now, "input") + * + * console.log(`Step result: ${result}`) + * }) + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toStep = ( + schedule: Schedule +): Effect< + (now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], Error, Output, Env>, + never, + Env +> => + effect.catchCause( + (schedule as any).step, + (cause) => effect.succeed(() => effect.failCause(cause) as any) + ) + +/** + * Extracts a step function from a `Schedule` that sleeps for each computed + * delay and returns metadata for the completed step. + * + * **When to use** + * + * Use to drive a schedule manually while preserving the computed output, + * delay, input, attempt, and elapsed timing metadata for each step. + * + * **Details** + * + * The returned step reads the current time from `Clock` when invoked, calls the + * schedule step with that timestamp and input, sleeps for the returned + * duration, and then yields `Metadata`. + * + * @see {@link toStep} for manually supplying the timestamp and handling the returned delay yourself + * @see {@link toStepWithSleep} for the same automatic sleeping behavior when only the schedule output is needed + * + * @category destructors + * @since 4.0.0 + */ +export const toStepWithMetadata = ( + schedule: Schedule +): Effect< + (input: Input) => Pull.Pull, Error, Output, Env>, + never, + Env +> => + effect.clockWith((clock) => + effect.map( + toStep(schedule), + (step) => { + const metaFn = metadataFn() + return (input) => + effect.suspend(() => { + const now = clock.currentTimeMillisUnsafe() + return effect.flatMap( + step(now, input), + ([output, duration]) => { + const meta = metaFn(now, input) as Mutable> + meta.output = output + meta.duration = duration + return effect.as(effect.sleep(duration), meta) + } + ) + }) + } + ) + ) + +/** + * Extracts a step function from a Schedule that automatically handles sleep delays. + * + * **Example** (Extracting a sleeping step function) + * + * ```ts + * import { Effect, Schedule } from "effect" + * + * // Convert schedule to step function with automatic sleeping + * const schedule = Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 3 })) + * + * const program = Effect.gen(function*() { + * const stepWithSleep = yield* Schedule.toStepWithSleep(schedule) + * + * // Each call will automatically sleep for the scheduled delay + * console.log("Starting...") + * const result1 = yield* stepWithSleep("first") + * console.log(`First result: ${result1}`) + * + * const result2 = yield* stepWithSleep("second") + * console.log(`Second result: ${result2}`) + * + * const result3 = yield* stepWithSleep("third") + * console.log(`Third result: ${result3}`) + * }) + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toStepWithSleep = ( + schedule: Schedule +): Effect< + (input: Input) => Pull.Pull, + never, + Env +> => + effect.map( + toStepWithMetadata(schedule), + (step) => (input) => effect.map(step(input), (meta) => meta.output) + ) + +/** + * Returns a new `Schedule` that adds the delay computed by the specified + * effectful function to the next recurrence of the schedule. + * + * **Example** (Adding extra delay to a schedule) + * + * ```ts + * import { Console, Data, Duration, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Add a deterministic extra delay based on the schedule metadata + * const delayedSchedule = Schedule.addDelay( + * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 5 })), + * ({ output }) => + * Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25)) + * ) + * + * const repeatProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.succeed("delayed task"), + * delayedSchedule.pipe( + * Schedule.tap(({ output: delay }) => + * Console.log(`Base delay: ${delay}`) + * ) + * ) + * ) + * }) + * + * // Add adaptive delay based on execution count + * const adaptiveSchedule = Schedule.addDelay( + * Schedule.recurs(6), + * ({ output: executionCount }) => + * // Increase delay as execution count grows + * Effect.succeed(Duration.millis(executionCount * 200)) + * ) + * + * const adaptiveProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Adaptive delay task") + * return "adaptive" + * }), + * adaptiveSchedule.pipe( + * Schedule.tap(({ output: count }) => + * Console.log(`Execution ${count + 1} with adaptive delay`) + * ) + * ) + * ) + * }) + * + * // Add effectful delay computation from deterministic service data + * const loadByExecution = [1, 3, 2, 4] as const + * + * const dynamicSchedule = Schedule.addDelay( + * Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 4 })), + * ({ output: executionNumber }) => { + * const load = loadByExecution[executionNumber] ?? 1 + * return Effect.succeed(Duration.millis(load * 100)) + * } + * ) + * + * const dynamicProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Dynamic delay task") + * return "dynamic" + * }), + * dynamicSchedule + * ) + * }) + * + * // Combine with retry for progressive backoff + * const progressiveRetrySchedule = Schedule.addDelay( + * Schedule.exponential("50 millis").pipe(Schedule.upTo({ times: 4 })), + * () => Effect.succeed(Duration.millis(100)) // Fixed additional delay + * ) + * + * const retryProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * if (attempt < 5) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * return `Success on attempt ${attempt}` + * }), + * progressiveRetrySchedule + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * ``` + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const addDelay: { + ( + f: (metadata: Metadata) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (metadata: Metadata) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (metadata: Metadata) => Effect +): Schedule => + modifyDelay( + self, + (metadata) => effect.map(f(metadata), (d) => Duration.sum(Duration.fromInputUnsafe(d), metadata.duration)) + )) + +/** + * Returns a schedule that runs `self` to completion, then runs `other`, and + * merges their outputs. + * + * **Example** (Sequencing quick and slow retries) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // First retry 3 times quickly, then switch to slower retries + * const quickRetries = Schedule.exponential("100 millis").pipe( + * Schedule.upTo({ times: 3 }) + * ) + * const slowRetries = Schedule.exponential("1 second").pipe( + * Schedule.upTo({ times: 2 }) + * ) + * + * const combinedRetries = Schedule.andThen(quickRetries, slowRetries) + * + * const program = Effect.gen(function*() { + * let attempt = 0 + * yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Attempt ${attempt}`) + * if (attempt < 6) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` })) + * } + * return `Success on attempt ${attempt}` + * }), + * combinedRetries + * ) + * }) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const andThen: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + other: Schedule + ): Schedule +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule => + map(andThenResult(self, other), ({ output }) => effect.succeed(Result.merge(output)))) + +/** + * Returns a schedule that runs `self` to completion, then runs `other`, and + * preserves which schedule produced each output. + * + * **Details** + * + * The resulting schedule emits a `Result` to indicate which phase produced + * each output: outputs from `self` are emitted as `Failure`, and outputs from + * `other` are emitted as `Success`. + * + * **Example** (Tracking sequential schedule phases) + * + * ```ts + * import { Console, Effect, Result, Schedule } from "effect" + * + * // Track which phase of the schedule we're in + * const phaseTracker = Schedule.andThenResult( + * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 2 })), + * Schedule.spaced("500 millis").pipe(Schedule.upTo({ times: 2 })) + * ) + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed") + * return "task-result" + * }), + * phaseTracker.pipe( + * Schedule.tap(({ output: result }) => + * Result.match(result, { + * onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`), + * onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`) + * }) + * ) + * ) + * ) + * }) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const andThenResult: { + ( + other: Schedule + ): ( + self: Schedule + ) => Schedule, Input & Input2, Error | Error2, Env | Env2> + ( + self: Schedule, + other: Schedule + ): Schedule, Input & Input2, Error | Error2, Env | Env2> +} = dual(2, ( + self: Schedule, + other: Schedule +): Schedule, Input & Input2, Error | Error2, Env | Env2> => + fromStep(effect.sync(() => { + let currentSide = 0 + let currentStep: + | undefined + | ((now: number, input: Input & Input2) => Pull.Pull< + [Result.Result, Duration.Duration], + Error | Error2, + Result.Result, + Env | Env2 + >) + const left = map(self, ({ output }) => Result.fail(output)) + const right = map(other, ({ output }) => Result.succeed(output)) + return function recur( + now, + input + ): Pull.Pull< + [Result.Result, Duration.Duration], + Error | Error2, + Result.Result, + Env | Env2 + > { + if (currentStep) return currentStep(now, input) + return toStep< + Result.Result, + Input & Input2, + Error | Error2, + Env | Env2 + >(currentSide === 0 ? left : right).pipe( + effect.flatMap((step) => { + currentSide++ + if (currentSide === 1) { + currentStep = (now, input) => + Pull.catchDone(step(now, input), (_) => { + currentStep = undefined + return recur(now, input) + }) + return currentStep(now, input) + } + currentStep = step + return currentStep(now, input) + }) + ) + } + }))) + +/** + * Combines schedules by recurring while all schedules want to recur, using the + * maximum delay between recurrences and outputting that maximum delay. + * + * **When to use** + * + * Use when a combined policy should continue only while every schedule still + * recurs, and should wait for the slowest schedule between recurrences. + * + * **Example** (Combining retry schedules by their maximum delay) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * const retrySchedule = Schedule.max([ + * Schedule.fixed("5 seconds"), + * Schedule.exponential("5 seconds"), + * Schedule.spaced("10 seconds") + * ]) + * + * const program = Effect.gen(function*() { + * let attempt = 0 + * + * yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Retry attempt ${attempt}`) + * if (attempt < 3) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * return "success" + * }), + * retrySchedule.pipe( + * Schedule.tap(({ output: duration }) => + * Console.log(`Waiting for the slowest schedule: ${duration}`) + * ) + * ) + * ) + * }) + * ``` + * + * @category combining + * @since 4.0.0 + */ +export const max = < + const Schedules extends NonEmptyReadonlyArray< + Schedule + > +>( + schedules: Schedules +): Schedule< + Duration.Duration, + UnionToIntersection< + Input + >, + Error, + Env +> => + fromStep(effect.map( + effect.all(schedules.map(toStep)), + (steps) => (now, input) => + effect.flatMap( + effect.forEach(steps, (step) => + Pull.matchEffect(step(now, input as never), { + onSuccess: (result) => effect.succeed(result[1]), + onDone: () => effect.undefined, + onFailure: effect.failCause + })), + (results) => { + const duration = maxDuration(results) + if (duration === undefined) { + return Cause.done(Duration.zero) + } + return effect.succeed([duration, duration] as [Duration.Duration, Duration.Duration]) + } + ) + )) + +const maxDuration = (results: ReadonlyArray): Duration.Duration | undefined => { + let max = results[0] + for (let i = 1; i < results.length; i++) { + max = results[i] && max && Duration.max(max, results[i]!) + if (max === undefined) break + } + + return max +} + +/** + * Returns a new `Schedule` that recurs on the specified `Cron` schedule and + * outputs the duration between recurrences. + * + * **Example** (Scheduling work with cron expressions) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {} + * + * // Run every minute + * const everyMinute = Schedule.cron("* * * * *") + * + * const minutelyProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Running minutely task") + * return "minute" + * }), + * everyMinute.pipe( + * Schedule.upTo({ times: 3 }), // Run only 3 times for demo + * Schedule.tap(({ output: duration }) => + * Console.log(`Next execution in: ${duration}`) + * ) + * ) + * ) + * }) + * + * // Run every day at 2:30 AM + * const dailyBackup = Schedule.cron("30 2 * * *") + * + * const backupProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Running daily backup...") + * // Simulate backup process + * yield* Effect.sleep("2 seconds") + * yield* Console.log("Backup completed") + * return "backup-done" + * }), + * dailyBackup.pipe( + * Schedule.upTo({ times: 2 }) // Run 2 times for demo + * ) + * ) + * }) + * + * // Run every Monday at 9:00 AM with timezone + * const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York") + * + * const reportProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Generating weekly report...") + * const report = { + * week: 42, + * status: "ready" as const + * } + * yield* Console.log(`Report generated: ${JSON.stringify(report)}`) + * return report + * }), + * weeklyReport.pipe(Schedule.upTo({ times: 1 })) + * ) + * }) + * + * // Run every 15 minutes during business hours (9 AM - 5 PM) + * const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5") + * + * const businessProgram = Effect.gen(function*() { + * const statuses = ["healthy", "healthy", "degraded", "healthy"] as const + * let index = 0 + * + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Business hours health check...") + * const status = statuses[index++] + * yield* Console.log(`System status: ${status}`) + * return status + * }), + * businessHoursCheck.pipe( + * Schedule.upTo({ times: 4 }) // Demo with 4 checks + * ) + * ) + * }) + * + * // Run on specific days of the month + * const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM + * + * const invoiceProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Processing monthly invoices...") + * const invoiceCount = 72 + * yield* Console.log(`Processed ${invoiceCount} invoices`) + * return { count: invoiceCount, batch: "2024-01-a" } + * }), + * monthlyInvoice.pipe(Schedule.upTo({ times: 1 })) + * ) + * }) + * + * // Complex cron with error handling + * const complexCron = Schedule.cron("0 2,4,6 * * *").pipe( + * Schedule.tap(({ output: duration }) => + * Console.log(`Scheduled to run again in ${duration}`) + * ) + * ) + * + * const robustProgram = Effect.gen(function*() { + * let attempt = 0 + * + * yield* Effect.repeat( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log("Complex scheduled task...") + * if (attempt === 1) { + * return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" })) + * } + * return "success" + * }), + * complexCron.pipe(Schedule.upTo({ times: 3 })) + * ).pipe( + * Effect.catch((error: unknown) => + * Console.log(`Cron task error: ${String(error)}`) + * ) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const cron: { + (expression: Cron.Cron): Schedule + (expression: string, tz?: string | DateTime.TimeZone): Schedule +} = (expression: string | Cron.Cron, tz?: string | DateTime.TimeZone) => { + const parsed = Cron.isCron(expression) ? Result.succeed(expression) : Cron.parse(expression, tz) + return fromStep(effect.map(effect.fromResult(parsed), (cron) => (now, _) => { + if (now === Number.POSITIVE_INFINITY) { + return Cause.done(Duration.zero) + } + return effect.sync(() => { + const next = Cron.next(cron, now).getTime() + const duration = Duration.millis(next - now) + return [duration, duration] + }) + })) +} + +/** + * Returns a schedule that recurs once after the specified duration. + * + * **When to use** + * + * Use when you need a schedule that recurs once after a fixed delay. + * + * **Details** + * + * The schedule outputs the configured duration for its first recurrence and + * then completes. + * + * **Example** (Recurring once after a duration) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * const program = Effect.repeat( + * Console.log("runs again after one second"), + * Schedule.duration("1 second") + * ) + * ``` + * + * @see {@link during} for recurring until a duration has elapsed + * + * @category constructors + * @since 2.0.0 + */ +export const duration = (durationInput: Duration.Input): Schedule => { + const duration = Duration.fromInputUnsafe(durationInput) + return fromStepWithMetadata(effect.succeed((meta) => + meta.attempt === 1 + ? effect.succeed([duration, duration]) + : Cause.done(Duration.zero) + )) +} + +/** + * Returns a new `Schedule` that will always recur, but only during the + * specified `duration` of time. + * + * **When to use** + * + * Use to bound a repeating or retrying schedule by elapsed time. + * + * **Example** (Repeating work during a duration) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Run a task for exactly 5 seconds, regardless of how many iterations + * const fiveSecondSchedule = Schedule.during("5 seconds") + * + * const timedProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Task executed inside the time window") + * yield* Effect.sleep("500 millis") // Each task takes 500ms + * return "task done" + * }), + * fiveSecondSchedule.pipe( + * Schedule.tap(({ output: elapsedDuration }) => + * Console.log(`Total elapsed: ${elapsedDuration}`) + * ) + * ) + * ) + * + * yield* Console.log("Time limit reached!") + * }) + * + * // Combine with other schedules for time-bounded execution + * const timeAndCountLimited = Schedule.max([ + * Schedule.spaced("1 second"), + * Schedule.during("10 seconds"), // Stop after 10 seconds OR + * Schedule.recurs(15) // 15 attempts, whichever comes first + * ]) + * + * // Burst execution within time window + * const burstWindow = Schedule.during("3 seconds") + * + * const burstProgram = Effect.gen(function*() { + * yield* Console.log("Starting burst execution...") + * + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Burst task") + * return "burst" + * }), + * burstWindow + * ) + * + * yield* Console.log("Burst window completed") + * }) + * + * // Timed retry window - retry for up to 30 seconds + * const timedRetry = Schedule.max([ + * Schedule.exponential("200 millis"), + * Schedule.during("30 seconds") + * ]) + * + * const retryProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Retry attempt ${attempt}`) + * + * if (attempt < 4) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * timedRetry + * ) + * + * yield* Console.log(`Result: ${result}`) + * }).pipe( + * Effect.catch((error: unknown) => Console.log(`Timed out: ${String(error)}`)) + * ) + * ``` + * + * @see {@link duration} for one delayed recurrence + * + * @category constructors + * @since 4.0.0 + */ +export const during = (duration: Duration.Input): Schedule => { + const durationMillis = Duration.toMillis(duration) + return fromStepWithMetadata( + effect.succeed((meta) => { + const elapsed = Duration.millis(meta.elapsed) + return meta.elapsed > durationMillis + ? effect.succeed([elapsed, Duration.zero]) + : Cause.done(elapsed) + }) + ) +} + +/** + * Combines schedules by recurring while at least one schedule wants to recur, + * using the minimum delay between recurrences and outputting that minimum delay. + * + * **When to use** + * + * Use when a combined policy should continue while any schedule still recurs, + * and should wait for the fastest schedule between recurrences. + * + * **Example** (Combining retry schedules by their minimum delay) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * const retrySchedule = Schedule.min([ + * Schedule.fixed("5 seconds"), + * Schedule.exponential("5 seconds"), + * Schedule.spaced("10 seconds") + * ]) + * + * const program = Effect.gen(function*() { + * let attempt = 0 + * + * yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Retry attempt ${attempt}`) + * if (attempt < 3) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * return "success" + * }), + * retrySchedule.pipe( + * Schedule.tap(({ output: duration }) => + * Console.log(`Waiting for the fastest schedule: ${duration}`) + * ) + * ) + * ) + * }) + * ``` + * + * @category combining + * @since 4.0.0 + */ +export const min = < + const Schedules extends NonEmptyReadonlyArray< + Schedule + > +>( + schedules: Schedules +): Schedule< + Duration.Duration, + UnionToIntersection< + Input + >, + Error, + Env +> => + fromStep(effect.map( + effect.all(schedules.map(toStep)), + (steps) => (now, input) => + effect.flatMap( + effect.forEach(steps, (step) => + Pull.matchEffect(step(now, input as never), { + onSuccess: (result) => effect.succeed(result[1]), + onDone: () => effect.undefined, + onFailure: effect.failCause + })), + (results) => { + const duration = minDuration(results) + if (duration === undefined) { + return Cause.done(Duration.zero) + } + return effect.succeed([duration, duration] as [Duration.Duration, Duration.Duration]) + } + ) + )) + +const minDuration = (results: ReadonlyArray): Duration.Duration | undefined => { + let min: Duration.Duration | undefined = undefined + for (let i = 0; i < results.length; i++) { + const duration = results[i] + if (duration !== undefined) { + min = min === undefined ? duration : Duration.min(min, duration) + } + } + + return min +} + +/** + * Schedule that always recurs, but will wait a certain amount between + * repetitions, given by `base * factor.pow(n)`, where `n` is the number of + * repetitions so far. Returns the current duration between recurrences. + * + * **Example** (Retrying with exponential backoff) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryFailure extends Data.TaggedError("RetryFailure")<{ readonly message: string }> {} + * + * // Basic exponential backoff with default factor of 2 + * const basicExponential = Schedule.exponential("100 millis") + * // Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ... + * + * // Custom exponential backoff with factor 1.5 + * const gentleExponential = Schedule.exponential("200 millis", 1.5) + * // Delays: 200ms, 300ms, 450ms, 675ms, 1012ms, ... + * + * // Retry with exponential backoff (limited to 5 attempts) + * const retryPolicy = Schedule.max([ + * Schedule.exponential("50 millis"), + * Schedule.recurs(5) + * ]) + * + * const program = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * if (attempt < 4) { + * yield* Console.log(`Attempt ${attempt} failed, retrying...`) + * return yield* Effect.fail(new RetryFailure({ message: `Failure ${attempt}` })) + * } + * return `Success on attempt ${attempt}` + * }), + * retryPolicy + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Will retry with delays: 50ms, 100ms, 200ms before success + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const exponential = ( + base: Duration.Input, + factor: number = 2 +): Schedule => { + const baseMillis = Duration.toMillis(Duration.fromInputUnsafe(base)) + return fromStepWithMetadata(effect.succeed((meta) => { + const duration = Duration.millis(baseMillis * Math.pow(factor, meta.attempt - 1)) + return effect.succeed([duration, duration]) + })) +} + +/** + * Schedule that always recurs, increasing delays by summing the preceding + * two delays (similar to the Fibonacci sequence). Returns the current + * duration between recurrences. + * + * **Example** (Retrying with Fibonacci backoff) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Basic Fibonacci schedule starting with 100ms + * const fibSchedule = Schedule.fibonacci("100 millis") + * // Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, ... + * + * // Retry with Fibonacci backoff for gradual increase + * const retryWithFib = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Attempt ${attempt}`) + * + * if (attempt < 5) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * Schedule.max([ + * Schedule.fibonacci("50 millis"), + * Schedule.recurs(6) // Maximum 6 retries + * ]).pipe( + * Schedule.tap(({ output: delay }) => Console.log(`Next retry in ${delay}`)) + * ) + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Heartbeat with Fibonacci intervals (starts fast, gets slower) + * const adaptiveHeartbeat = Effect.gen(function*() { + * yield* Console.log("Heartbeat") + * return "pulse" + * }).pipe( + * Effect.repeat( + * Schedule.fibonacci("200 millis").pipe( + * Schedule.upTo({ times: 8 }) // First 8 heartbeats + * ) + * ) + * ) + * + * // Fibonacci vs exponential comparison + * const compareSchedules = Effect.gen(function*() { + * yield* Console.log("=== Fibonacci Delays ===") + * // 100ms, 100ms, 200ms, 300ms, 500ms, 800ms + * + * yield* Console.log("=== Exponential Delays ===") + * // 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms + * + * // Fibonacci grows more slowly than exponential + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fibonacci = (one: Duration.Input): Schedule => { + const oneMillis = Duration.toMillis(Duration.fromInputUnsafe(one)) + return fromStep(effect.sync(() => { + let a = 0 + let b = oneMillis + return constant(effect.sync(() => { + const next = a + b + a = b + b = next + const duration = Duration.millis(next) + return [duration, duration] + })) + })) +} + +/** + * Returns a `Schedule` that recurs on the specified fixed `interval` and + * outputs the number of repetitions of the schedule so far. + * + * **When to use** + * + * Use when recurrences should stay aligned to a regular cadence. + * + * **Gotchas** + * + * If the action run between recurrences takes longer than the interval, the + * next recurrence happens immediately, but missed intervals are not replayed. + * + * ```text + * |-----interval-----|-----interval-----|-----interval-----| + * |---------action--------||action|-----|action|-----------| + * ``` + * + * **Example** (Repeating on fixed intervals) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Fixed interval schedule - recurs on a one-second cadence + * const everySecond = Schedule.fixed("1 second") + * + * // Health check that runs at fixed intervals + * const healthCheck = Effect.gen(function*() { + * yield* Console.log("Health check") + * yield* Effect.sleep("200 millis") // simulate health check work + * return "healthy" + * }).pipe( + * Effect.repeat(Schedule.fixed("2 seconds").pipe(Schedule.upTo({ times: 5 }))) + * ) + * + * // Difference between fixed and spaced: + * // - fixed: maintains constant rate regardless of action duration + * // - spaced: waits for the duration AFTER each action completes + * + * const longRunningTask = Effect.gen(function*() { + * yield* Console.log("Task started") + * yield* Effect.sleep("1.5 seconds") // Longer than interval + * yield* Console.log("Task completed") + * return "done" + * }) + * + * // Fixed schedule: if task takes 1.5s but interval is 1s, + * // next execution happens immediately (no pile-up) + * const fixedSchedule = longRunningTask.pipe( + * Effect.repeat(Schedule.fixed("1 second").pipe(Schedule.upTo({ times: 3 }))) + * ) + * + * // Comparing with spaced (waits 1s AFTER each task) + * const spacedSchedule = longRunningTask.pipe( + * Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 3 }))) + * ) + * + * const program = Effect.gen(function*() { + * yield* Console.log("=== Fixed Schedule Demo ===") + * yield* fixedSchedule + * + * yield* Console.log("=== Spaced Schedule Demo ===") + * yield* spacedSchedule + * }) + * ``` + * + * @see {@link spaced} for delaying after each action completes + * + * @category constructors + * @since 2.0.0 + */ +export const fixed = (interval: Duration.Input): Schedule => { + const window = Duration.toMillis(Duration.fromInputUnsafe(interval)) + return fromStepWithMetadata(effect.sync(() => { + let start = 0 + let lastRun = 0 + return (meta) => + effect.sync(() => { + if (window === 0) { + return [meta.attempt - 1, Duration.zero] as const + } + if (meta.attempt === 1) { + start = meta.now + lastRun = meta.now + window + return [0, Duration.millis(window)] as const + } + const runningBehind = meta.now > (lastRun + window) + const boundary = window - ((meta.now - start) % window) + const delay = runningBehind ? 0 : boundary === 0 ? window : boundary + lastRun = runningBehind ? meta.now : meta.now + delay + return [meta.attempt - 1, Duration.millis(delay)] as const + }) + })) +} + +/** + * Returns a new `Schedule` that maps each schedule decision to a new output + * using the full schedule metadata. + * + * **Details** + * + * The callback receives the schedule input, output, selected delay duration, + * current attempt, and elapsed timing information. Return either a plain value + * or an `Effect` that produces the new output. + * + * **Example** (Mapping schedule outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Transform schedule output from number to string + * const countSchedule = Schedule.recurs(5).pipe( + * Schedule.map(({ output: count }) => Effect.succeed(`Execution #${count + 1}`)) + * ) + * + * // Map schedule delays to human-readable format + * const readableDelays = Schedule.exponential("100 millis").pipe( + * Schedule.map(({ output: delay }) => Effect.succeed(`Next retry in ${delay}`)) + * ) + * + * // Transform numeric output to structured data + * const structuredSchedule = Schedule.spaced("1 second").pipe( + * Schedule.map(({ output: recurrence }) => Effect.succeed({ + * iteration: recurrence + 1, + * phase: recurrence < 5 ? "warmup" as const : "steady" as const + * })) + * ) + * + * const program = Effect.gen(function*() { + * const results = yield* Effect.repeat( + * Effect.succeed("task completed"), + * structuredSchedule.pipe( + * Schedule.upTo({ times: 8 }), + * Schedule.tap(({ output: info }) => + * Console.log( + * `${info.phase} phase - iteration ${info.iteration}` + * ) + * ) + * ) + * ) + * + * yield* Console.log(`Completed iterations`) + * }) + * + * // Map with effectful transformation + * const effectfulMap = Schedule.fixed("2 seconds").pipe( + * Schedule.map(({ output: count }) => + * Effect.gen(function*() { + * yield* Console.log(`Processing count: ${count}`) + * return count * 10 + * }) + * ) + * ) + * + * // Use timing metadata in the mapped output + * const complexSchedule = Schedule.fibonacci("100 millis").pipe( + * Schedule.map(({ output: delay, attempt }) => + * Effect.succeed(`Attempt ${attempt} delay: ${delay}`) + * ) + * ) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + ( + f: (metadata: Metadata) => Output2 | Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (metadata: Metadata) => Output2 | Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (metadata: Metadata) => Output2 | Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => { + const meta = metadataFn() + return (now, input) => + Pull.matchEffect(step(now, input), { + onSuccess: ([output, duration]) => { + const result = f({ ...meta(now, input), output, duration }) + if (!isEffect(result)) return effect.succeed([result, duration] as [Output2, Duration.Duration]) + return effect.map(result, (output) => [output, duration] as [Output2, Duration.Duration]) + }, + onFailure: effect.failCause, + onDone: (output) => { + const result = f({ ...meta(now, input), output, duration: Duration.zero }) + if (!isEffect(result)) return Cause.done(result as Output2) + return effect.flatMap(result, Cause.done) + } + }) + }))) + +/** + * Returns a new `Schedule` that modifies the delay of the next recurrence + * of the schedule using the specified effectful function. + * + * **Example** (Modifying delays from schedule metadata) + * + * ```ts + * import { Console, Duration, Effect, Schedule } from "effect" + * + * // Modify delays based on output - increase delay on high iteration counts + * const adaptiveDelay = Schedule.recurs(10).pipe( + * Schedule.modifyDelay(({ output, duration }) => { + * // Double the delay if we're seeing high iteration counts + * return Effect.succeed(output > 5 ? Duration.times(duration, 2) : duration) + * }) + * ) + * + * const program = Effect.gen(function*() { + * let counter = 0 + * yield* Effect.repeat( + * Effect.gen(function*() { + * counter++ + * yield* Console.log(`Attempt ${counter}`) + * return counter + * }), + * adaptiveDelay.pipe(Schedule.upTo({ times: 8 })) + * ) + * }) + * ``` + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const modifyDelay: { + ( + f: ( + metadata: Metadata + ) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: ( + metadata: Metadata + ) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: ( + metadata: Metadata + ) => Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => { + const meta = metadataFn() + return (now, input) => + effect.flatMap( + step(now, input), + ([output, duration]) => + effect.map(f({ ...meta(now, input), output, duration }), (replacement) => [ + output, + Duration.fromInputUnsafe(replacement) + ]) + ) + }))) + +/** + * Returns a new `Schedule` that randomly adjusts each recurrence delay. + * + * **When to use** + * + * Use to add random variation to an existing schedule's recurrence delays while + * preserving its output and completion behavior. + * + * **Details** + * + * Each recurrence delay is scaled by a random factor between `0.8` and `1.2`. + * + * @see {@link modifyDelay} for replacing recurrence delays with a custom effectful transformation + * + * @category delays & timeouts + * @since 2.0.0 + */ +export const jittered = ( + self: Schedule +): Schedule => + modifyDelay(self, ({ duration }) => + effect.map(randomNext, (random) => { + const millis = Duration.toMillis(duration) + return Duration.millis(millis * 0.8 * (1 - random) + millis * 1.2 * random) + })) + +/** + * Returns a new `Schedule` that outputs the inputs of the specified schedule. + * + * **Example** (Passing inputs through as outputs) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Create a schedule that outputs the inputs instead of original outputs + * const inputSchedule = Schedule.passthrough( + * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) + * ) + * + * const program = Effect.gen(function*() { + * let counter = 0 + * yield* Effect.repeat( + * Effect.gen(function*() { + * counter++ + * yield* Console.log(`Task ${counter} executed`) + * return `result-${counter}` + * }), + * inputSchedule + * ) + * }) + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const passthrough = ( + self: Schedule +): Schedule => + fromStep(effect.map(toStep(self), (step) => (now, input) => + Pull.matchEffect(step(now, input), { + onSuccess: (result) => effect.succeed([input, result[1]]), + onFailure: effect.failCause, + onDone: () => Cause.done(input) + }))) + +/** + * Returns a `Schedule` which can only be stepped the specified number of + * `times` before it terminates. + * + * **When to use** + * + * Use when you need a counter schedule with no additional delay. + * + * **Gotchas** + * + * `recurs(n)` counts schedule recurrences, not the first evaluation of the + * effect being repeated or retried. For retrying, this means one initial + * attempt plus at most `n` retries. + * + * **Example** (Limiting recurrences) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Basic recurs - retry at most 3 times + * const maxThreeAttempts = Schedule.recurs(3) + * + * // Retry a failing operation at most 5 times + * const program = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Attempt ${attempt}`) + * + * if (attempt < 4) { + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * Schedule.recurs(5) // Will retry up to 5 times + * ) + * + * yield* Console.log(`Final result: ${result}`) + * }) + * + * // Combining recurs with other schedules for sophisticated retry logic + * const complexRetry = Schedule.max([ + * Schedule.exponential("100 millis"), + * Schedule.recurs(3) // At most 3 retries + * ]) + * + * // Allow ten recurrences after the initial run + * const tenRecurrences = Effect.gen(function*() { + * yield* Console.log("Executing task...") + * return "completed" + * }).pipe( + * Effect.repeat(Schedule.recurs(10)) + * ) + * + * // The schedule outputs the current recurrence count (0-based) + * const countingSchedule = Schedule.recurs(3).pipe( + * Schedule.tap(({ output: count }) => Console.log(`Execution #${count + 1}`)) + * ) + * ``` + * + * @see {@link upTo} for limiting an existing schedule + * + * @category constructors + * @since 2.0.0 + */ +export const recurs = (times: number): Schedule => + while_(forever, ({ attempt }) => effect.succeed(attempt <= times)) + +/** + * Returns a schedule that recurs continuously, each repetition spaced the + * specified duration from the last run. + * + * **When to use** + * + * Use when each delay should start after the previous action completes. + * + * **Example** (Repeating with fixed spacing) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Basic spaced schedule - runs every 2 seconds + * const everyTwoSeconds = Schedule.spaced("2 seconds") + * + * // Heartbeat that runs indefinitely with fixed spacing + * const heartbeat = Effect.gen(function*() { + * yield* Console.log("Heartbeat") + * }).pipe( + * Effect.repeat(everyTwoSeconds) + * ) + * + * // Limited repeat - run only 5 times with 1-second spacing + * const limitedTask = Effect.gen(function*() { + * yield* Console.log("Executing scheduled task...") + * yield* Effect.sleep("500 millis") // simulate work + * return "Task completed" + * }).pipe( + * Effect.repeat( + * Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 5 })) + * ) + * ) + * + * // Simple spaced schedule with limited repetitions + * const limitedSpaced = Schedule.max([ + * Schedule.spaced("100 millis"), + * Schedule.recurs(5) // at most 5 times + * ]) + * + * const program = Effect.gen(function*() { + * yield* Console.log("Starting spaced execution...") + * + * yield* Effect.repeat( + * Effect.succeed("work item"), + * limitedSpaced + * ) + * + * yield* Console.log("Completed executions") + * }) + * ``` + * + * @see {@link fixed} for recurrence aligned to a regular cadence + * + * @category constructors + * @since 2.0.0 + */ +export const spaced = (duration: Duration.Input): Schedule => { + const decoded = Duration.fromInputUnsafe(duration) + return fromStepWithMetadata(effect.succeed((meta) => effect.succeed([meta.attempt - 1, decoded]))) +} + +/** + * Returns a new `Schedule` that allows execution of an effectful function for + * every decision of the schedule, but does not alter the inputs and outputs of + * the schedule. + * + * **Details** + * + * The callback receives the full schedule metadata, including the input, output, + * computed delay duration, current attempt, and elapsed timing information. + * + * **Example** (Tapping schedule metadata) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * const monitoredSchedule = Schedule.exponential("100 millis").pipe( + * Schedule.upTo({ times: 5 }), + * Schedule.tap((metadata) => + * Console.log( + * `Attempt ${metadata.attempt} produced ${metadata.output} ` + + * `after ${metadata.elapsed}ms; next delay is ${metadata.duration}` + * ) + * ) + * ) + * + * const program = Effect.retry( + * Effect.fail("transient error"), + * monitoredSchedule + * ) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const tap: { + ( + f: (metadata: Metadata) => Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + f: (metadata: Metadata) => Effect + ): Schedule +} = dual(2, ( + self: Schedule, + f: (metadata: Metadata) => Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => { + const meta = metadataFn() + return (now, input) => + effect.tap(step(now, input), ([output, duration]) => f({ ...meta(now, input), output, duration })) + }))) + +/** + * Returns a new `Schedule` that limits an existing schedule by elapsed + * duration, number of outputs, or both. + * + * **When to use** + * + * Use to bound an existing schedule while preserving its output and delay + * behavior. When both `duration` and `times` are specified, the schedule + * stops as soon as either limit is reached. + * + * **Gotchas** + * + * The `times` option limits schedule outputs. When used with repeat or retry, + * the effect is evaluated once before the schedule is stepped, so the total + * number of evaluations can be one greater than the configured number of + * outputs. + * + * The `duration` option is based on the elapsed time observed by the schedule + * step. Long-running effects can cause the duration limit to be detected on the + * following schedule step. + * + * **Example** (Limiting by duration and recurrence count) + * + * ```ts + * import { Console, Data, Effect, Schedule } from "effect" + * + * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} + * + * // Limit an infinite schedule to five recurrences + * const limitedHeartbeat = Schedule.spaced("1 second").pipe( + * Schedule.upTo({ times: 5 }) + * ) + * + * const heartbeatProgram = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Heartbeat") + * return "pulse" + * }), + * limitedHeartbeat + * ) + * + * yield* Console.log("Heartbeat sequence completed") + * }) + * + * // Limit retry attempts by both count and elapsed time + * const limitedRetry = Schedule.exponential("100 millis").pipe( + * Schedule.upTo({ + * duration: "5 seconds", + * times: 3 + * }) + * ) + * + * const retryProgram = Effect.gen(function*() { + * let attempt = 0 + * + * const result = yield* Effect.retry( + * Effect.gen(function*() { + * attempt++ + * yield* Console.log(`Attempt ${attempt}`) + * + * if (attempt < 5) { // Will fail more than 3 times + * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) + * } + * + * return `Success on attempt ${attempt}` + * }), + * limitedRetry + * ) + * + * yield* Console.log(`Result: ${result}`) + * }).pipe( + * Effect.catch((error: unknown) => + * Console.log(`Failed after limited retries: ${String(error)}`) + * ) + * ) + * + * // Empty options leave the schedule unchanged + * const unchanged = Schedule.fixed("500 millis").pipe( + * Schedule.upTo({}) + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const upTo: { + (options: { + readonly duration?: Duration.Input | undefined + readonly times?: number | undefined + }): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + options: { + readonly duration?: Duration.Input | undefined + readonly times?: number | undefined + } + ): Schedule +} = dual(2, ( + self: Schedule, + options: { + readonly duration?: Duration.Input | undefined + readonly times?: number | undefined + } +): Schedule => { + const duration = options.duration === undefined ? undefined : Duration.fromInputUnsafe(options.duration) + return while_(self, ({ attempt, elapsed }) => + effect.succeed( + (options.times === undefined || attempt <= options.times) && + (duration === undefined || Duration.isLessThanOrEqualTo(Duration.millis(elapsed), duration)) + )) +}) + +const while_: { + ( + predicate: ( + metadata: Metadata + ) => boolean | Effect + ): ( + self: Schedule + ) => Schedule + ( + self: Schedule, + predicate: ( + metadata: Metadata + ) => boolean | Effect + ): Schedule +} = dual(2, ( + self: Schedule, + predicate: ( + metadata: Metadata + ) => boolean | Effect +): Schedule => + fromStep(effect.map(toStep(self), (step) => { + const meta = metadataFn() + return (now, input) => + effect.flatMap(step(now, input), (result) => { + const [output, duration] = result + const eff = predicate({ ...meta(now, input), output, duration }) + return effect.flatMap( + isEffect(eff) ? eff : effect.succeed(eff), + (check) => (check ? effect.succeed(result) : Cause.done(output)) + ) + }) + }))) + +export { + /** + * Returns a new schedule that continues while the predicate returns `true`. + * + * **When to use** + * + * Use to stop an existing schedule based on its full metadata, such as the + * current input, output, attempt, delay, or elapsed time. + * + * **Details** + * + * The predicate receives `Metadata`, may return `boolean` or an + * `Effect`, preserves the output and delay when it returns + * `true`, and stops the schedule when it returns `false`. + * + * @see {@link upTo} for stopping after a fixed number of schedule outputs + * + * @category filtering + * @since 4.0.0 + */ + while_ as while +} + +/** + * Schedule that divides the timeline to `interval`-long windows, and sleeps + * until the nearest window boundary every time it recurs. + * + * **Details** + * + * For example, `Schedule.windowed("10 seconds")` would produce a schedule as + * follows: + * + * ```text + * 10s 10s 10s 10s + * |----------|----------|----------|----------| + * |action------|sleep---|act|-sleep|action----| + * ``` + * + * **Example** (Repeating on aligned windows) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // Execute tasks at regular intervals aligned to window boundaries + * const windowSchedule = Schedule.windowed("5 seconds") + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Window task executed") + * return "window-task" + * }), + * windowSchedule.pipe(Schedule.upTo({ times: 4 })) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const windowed = (interval: Duration.Input): Schedule => { + const window = Duration.toMillis(Duration.fromInputUnsafe(interval)) + return fromStepWithMetadata(effect.succeed((meta) => + effect.sync(() => [ + meta.attempt - 1, + window === 0 ? Duration.zero : Duration.millis(window - (meta.elapsed % window)) + ]) + )) +} + +/** + * Returns a new `Schedule` that will recur forever. + * + * **Details** + * + * The output of the schedule is the current count of its repetitions thus far + * (i.e. `0, 1, 2, ...`). + * + * **Example** (Repeating forever) + * + * ```ts + * import { Console, Effect, Schedule } from "effect" + * + * // A schedule that runs forever with no delay + * const infiniteSchedule = Schedule.forever + * + * const program = Effect.gen(function*() { + * yield* Effect.repeat( + * Effect.gen(function*() { + * yield* Console.log("Running forever...") + * return "continuous-task" + * }), + * infiniteSchedule.pipe(Schedule.upTo({ times: 5 })) // Limit for demo + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const forever: Schedule = spaced(Duration.zero) + +const constIdentity = fromStep( + effect.succeed((_now, input: unknown) => effect.succeed([input, Duration.zero] as [unknown, Duration.Duration])) +) + +const identity_ = (): Schedule => constIdentity as Schedule + +export { + /** + * Creates a schedule that always recurs, passing inputs directly as outputs. + * + * **When to use** + * + * Use when you need an infinite schedule that preserves input values as + * outputs. + * + * **Details** + * + * This schedule runs indefinitely, returning each input value as its output + * without modification. It effectively acts as a pass-through that simply + * echoes its input values at each step. + * + * @see {@link forever} for an infinite schedule that returns incrementing step counts + * @category constructors + * @since 2.0.0 + */ + identity_ as identity +} + +/** + * Sets the input type of the provided schedule without altering its behavior. + * + * **When to use** + * + * Use to adapt a schedule that does not depend on its input values. + * + * **Details** + * + * This helper is checked at compile time and does not change the schedule's + * runtime behavior. + * + * **Example** (Setting a schedule input type) + * + * ```ts + * import { Schedule } from "effect" + * + * const schedule = Schedule.recurs(3).pipe( + * Schedule.setInputType() + * ) + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export const setInputType = + () => (self: Schedule): Schedule => self diff --git a/repos/effect/packages/effect/src/Scheduler.ts b/repos/effect/packages/effect/src/Scheduler.ts new file mode 100644 index 0000000..4e076de --- /dev/null +++ b/repos/effect/packages/effect/src/Scheduler.ts @@ -0,0 +1,285 @@ +/** + * Controls how runnable Effect fiber tasks are dispatched. + * + * A scheduler decides how tasks are queued, when queued tasks run, and when a + * fiber should pause so other work can continue. This module includes the + * scheduler service reference, the default `MixedScheduler`, dispatcher types + * for queued tasks, and references for tuning or disabling automatic scheduler + * yields. + * + * @since 2.0.0 + */ +import * as Context from "./Context.ts" +import type * as Fiber from "./Fiber.ts" + +/** + * A scheduler manages the execution of Effect fibers by controlling when queued + * tasks run. + * + * **When to use** + * + * Use to define or provide custom runtime scheduling behavior for Effect fibers. + * + * **Details** + * + * A scheduler determines the execution mode, schedules tasks with different + * priorities, and decides when fibers should yield control after consuming + * their operation budget. + * + * @category models + * @since 2.0.0 + */ +export interface Scheduler { + readonly executionMode: "sync" | "async" + shouldYield(fiber: Fiber.Fiber): boolean + makeDispatcher(): SchedulerDispatcher +} + +/** + * A dispatcher created by a `Scheduler` for enqueuing tasks and forcing queued + * tasks to run. + * + * **When to use** + * + * Use when implementing or testing scheduler-created dispatchers that enqueue + * prioritized runtime tasks and flush queued work deterministically. + * + * **Details** + * + * `scheduleTask` queues a task with a priority. `flush` drains pending work + * synchronously, which is useful when callers need deterministic completion of + * already scheduled tasks. Lower priority numbers run first, and equal + * priorities run in FIFO order. + * + * @category models + * @since 4.0.0 + */ +export interface SchedulerDispatcher { + scheduleTask(task: () => void, priority: number): void + flush(): void +} + +/** + * Context reference for the scheduler used by the Effect runtime. + * + * **When to use** + * + * Use when you need to replace scheduling behavior globally in tests or runtime + * setup, such as forcing deterministic task dispatch. + * + * **Details** + * + * The default value creates a `MixedScheduler`. Provide this service to + * customize execution mode, task dispatching, or yield behavior. + * + * @category references + * @since 2.0.0 + */ +export const Scheduler: Context.Reference = Context.Reference("effect/Scheduler", { + defaultValue: () => new MixedScheduler() +}) + +const setImmediate = "setImmediate" in globalThis + ? (f: () => void) => { + // @ts-ignore + const timer = globalThis.setImmediate(f) + // @ts-ignore + return (): void => globalThis.clearImmediate(timer) + } + : (f: () => void) => { + const timer = setTimeout(f, 0) + return (): void => clearTimeout(timer) + } + +class PriorityBuckets { + buckets: Array<[priority: number, tasks: Array<() => void>]> = [] + + scheduleTask(task: () => void, priority: number): void { + const buckets = this.buckets + const len = buckets.length + let bucket: [number, Array<() => void>] | undefined + let index = 0 + for (; index < len; index++) { + if (buckets[index][0] > priority) break + bucket = buckets[index] + } + if (bucket && bucket[0] === priority) { + bucket[1].push(task) + } else if (index === len) { + buckets.push([priority, [task]]) + } else { + buckets.splice(index, 0, [priority, [task]]) + } + } + + drain() { + const buckets = this.buckets + this.buckets = [] + return buckets + } +} + +/** + * Provides a scheduler implementation that batches queued tasks and dispatches them by + * priority. + * + * **When to use** + * + * Use when you need the default runtime scheduler directly, including a + * scheduler that batches queued work by priority and preserves FIFO order within + * each priority. + * + * **Details** + * + * `MixedScheduler` supports synchronous and asynchronous execution modes, uses + * operation counts to decide when fibers should yield, and is the default + * scheduler implementation. + * + * @category schedulers + * @since 2.0.0 + */ +export class MixedScheduler implements Scheduler { + readonly executionMode: "sync" | "async" + readonly setImmediate: (f: () => void) => () => void + + constructor( + executionMode: "sync" | "async" = "async", + setImmediateFn: (f: () => void) => () => void = setImmediate + ) { + this.executionMode = executionMode + this.setImmediate = setImmediateFn + } + + /** + * Returns whether the fiber has reached its operation budget and should yield. + * + * **When to use** + * + * Use to decide whether a fiber should yield after consuming its current + * operation budget. + * + * @since 2.0.0 + */ + shouldYield(fiber: Fiber.Fiber) { + return fiber.currentOpCount >= fiber.maxOpsBeforeYield + } + + /** + * Creates a dispatcher that schedules work through this scheduler. + * + * **When to use** + * + * Use when you need a standalone dispatcher from a scheduler instance, for + * example in tests that enqueue tasks and then flush them deterministically. + * + * @since 4.0.0 + */ + makeDispatcher() { + return new MixedSchedulerDispatcher(this.setImmediate) + } +} + +class MixedSchedulerDispatcher implements SchedulerDispatcher { + private tasks = new PriorityBuckets() + private running: (() => void) | undefined = undefined + readonly setImmediate: (f: () => void) => () => void + + constructor( + setImmediateFn: (f: () => void) => () => void = setImmediate + ) { + this.setImmediate = setImmediateFn + } + + /** + * @since 2.0.0 + */ + scheduleTask(task: () => void, priority: number) { + this.tasks.scheduleTask(task, priority) + if (this.running === undefined) { + this.running = this.setImmediate(this.afterScheduled) + } + } + + /** + * @since 2.0.0 + */ + afterScheduled = () => { + this.running = undefined + this.runTasks() + } + + /** + * @since 2.0.0 + */ + runTasks() { + const buckets = this.tasks.drain() + for (let i = 0; i < buckets.length; i++) { + const toRun = buckets[i][1] + for (let j = 0; j < toRun.length; j++) { + toRun[j]() + } + } + } + + /** + * @since 2.0.0 + */ + flush() { + while (this.tasks.buckets.length > 0) { + if (this.running !== undefined) { + this.running() + this.running = undefined + } + this.runTasks() + } + } +} + +/** + * Context reference that controls the maximum number of operations a fiber + * can perform before yielding control back to the scheduler. + * + * **When to use** + * + * Use to tune scheduler fairness for CPU-bound fibers by changing the scheduler + * operation budget that triggers a yield. + * + * **Details** + * + * The default value is `2048` operations, which balances performance and + * fairness by helping prevent long-running fibers from monopolizing the + * execution thread. + * + * @see {@link PreventSchedulerYield} for bypassing scheduler yield checks entirely rather than tuning the operation budget + * + * @category references + * @since 4.0.0 + */ +export const MaxOpsBeforeYield = Context.Reference("effect/Scheduler/MaxOpsBeforeYield", { + defaultValue: () => 2048 +}) + +/** + * Context reference that controls whether the runtime should bypass scheduler + * yield checks. When set to `true`, the fiber run loop won't call + * `Scheduler.shouldYield`. + * + * **When to use** + * + * Use to bypass scheduler yield checks for controlled runtime workloads where + * cooperative yielding should be disabled. + * + * **Gotchas** + * + * Setting this reference to `true` can let long-running fibers monopolize the + * JavaScript thread. + * + * @see {@link MaxOpsBeforeYield} for tuning yield frequency without disabling yield checks + * @see {@link Scheduler} for providing custom scheduler yield behavior + * + * @category references + * @since 4.0.0 + */ +export const PreventSchedulerYield = Context.Reference("effect/Scheduler/PreventSchedulerYield", { + defaultValue: () => false +}) diff --git a/repos/effect/packages/effect/src/Schema.ts b/repos/effect/packages/effect/src/Schema.ts new file mode 100644 index 0000000..71b774d --- /dev/null +++ b/repos/effect/packages/effect/src/Schema.ts @@ -0,0 +1,15120 @@ +/** + * Describes data shapes and how unknown input becomes trusted values. + * + * A schema can validate input, decode it into an application type, and encode + * that value back to another representation. This module contains the main + * schema, codec, decoder, and encoder APIs, together with schemas for common + * JavaScript values and Effect data types. It also supports refinements, + * transformations, defaults, classes, JSON Schema generation, test data + * generation, formatting, equivalence, optics, and differs derived from schema + * definitions. + * + * @since 4.0.0 + */ + +/** @effect-diagnostics schemaStructWithTag:skip-file */ +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" +import * as Arr from "./Array.ts" +import * as BigDecimal_ from "./BigDecimal.ts" +import type * as Brand from "./Brand.ts" +import * as Cause_ from "./Cause.ts" +import * as Chunk_ from "./Chunk.ts" +import type * as Combiner from "./Combiner.ts" +import * as Data from "./Data.ts" +import * as DateTime from "./DateTime.ts" +import type { Differ } from "./Differ.ts" +import * as Duration_ from "./Duration.ts" +import * as Effect from "./Effect.ts" +import * as Encoding from "./Encoding.ts" +import * as Equal from "./Equal.ts" +import * as Equivalence from "./Equivalence.ts" +import * as Exit_ from "./Exit.ts" +import type { Formatter } from "./Formatter.ts" +import { format, formatPropertyKey } from "./Formatter.ts" +import { identity, memoize } from "./Function.ts" +import * as HashMap_ from "./HashMap.ts" +import * as HashSet_ from "./HashSet.ts" +import * as core from "./internal/core.ts" +import * as InternalAnnotations from "./internal/schema/annotations.ts" +import * as InternalArbitrary from "./internal/schema/arbitrary.ts" +import * as InternalEquivalence from "./internal/schema/equivalence.ts" +import * as InternalStandard from "./internal/schema/representation.ts" +import * as InternalSchema from "./internal/schema/schema.ts" +import * as JsonPatch from "./JsonPatch.ts" +import * as JsonSchema from "./JsonSchema.ts" +import { remainder } from "./Number.ts" +import * as Optic_ from "./Optic.ts" +import * as Option_ from "./Option.ts" +import * as Order from "./Order.ts" +import * as Pipeable from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as Record_ from "./Record.ts" +import * as Redacted_ from "./Redacted.ts" +import * as Result_ from "./Result.ts" +import * as Scheduler from "./Scheduler.ts" +import * as SchemaAST from "./SchemaAST.ts" +import { isSchemaError, SchemaError } from "./SchemaError.ts" +import * as SchemaGetter from "./SchemaGetter.ts" +import * as SchemaIssue from "./SchemaIssue.ts" +import * as SchemaParser from "./SchemaParser.ts" +import type * as SchemaRepresentation from "./SchemaRepresentation.ts" +import * as SchemaTransformation from "./SchemaTransformation.ts" +import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts" +import * as Struct_ from "./Struct.ts" +import * as FastCheck from "./testing/FastCheck.ts" +import type { RequiredKeys, UnionToIntersection } from "./Types.ts" +import type { Unify } from "./Unify.ts" + +const TypeId = InternalSchema.TypeId + +/** + * Whether a schema field is required or optional within a struct. + * + * @see {@link optionalKey} — mark a struct field as optional + * @see {@link optional} — mark a struct field as optional with `| undefined` + * + * @category models + * @since 4.0.0 + */ +export type Optionality = "required" | "optional" + +/** + * Whether a schema field is readonly or mutable within a struct. + * + * @see {@link mutableKey} — mark a struct field as mutable + * + * @category models + * @since 4.0.0 + */ +export type Mutability = "readonly" | "mutable" + +/** + * Whether a schema field has a constructor default value. + * + * @see {@link withConstructorDefault} — add a default to a schema field + * @see {@link tag} — creates a literal field with a constructor default + * + * @category models + * @since 4.0.0 + */ +export type ConstructorDefault = "no-default" | "with-default" + +/** + * Options for `makeEffect`, `make`, and Class constructors. + * + * **When to use** + * + * Use when passing `disableChecks: true` to skip validation when you trust the data. + * - Pass `parseOptions` to control error reporting behavior. + * + * @see {@link Bottom.makeEffect} + * @see {@link Bottom.make} + * + * @category options + * @since 3.13.4 + */ +export interface MakeOptions { + /** + * The parse options to use for the schema. + */ + readonly parseOptions?: SchemaAST.ParseOptions | undefined + /** + * Whether to disable validation for the schema. + */ + readonly disableChecks?: boolean | undefined + + /** @internal */ + readonly "~payload"?: { + readonly token: unknown + readonly value: unknown + } +} + +/** + * The fully-parameterized base interface for all schemas. Exposes all 14 type + * parameters controlling type inference, mutability, optionality, services, and + * transformation behavior. + * + * **When to use** + * + * Use when you are writing advanced generic schema utilities or performing + * schema introspection. + * + * @category models + * @since 4.0.0 + */ +export interface Bottom< + out T, + out E, + out RD, + out RE, + out Ast extends SchemaAST.AST, + out Rebuild extends Top, + out TypeMakeIn = T, + out Iso = T, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMake = TypeMakeIn, + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends Pipeable.Pipeable { + readonly [TypeId]: typeof TypeId + + readonly "ast": Ast + readonly "Rebuild": Rebuild + readonly "~type.parameters": TypeParameters + + readonly "Type": T + readonly "Encoded": E + readonly "DecodingServices": RD + readonly "EncodingServices": RE + + readonly "~type.make.in": TypeMakeIn + readonly "~type.make": TypeMake // useful to type the `refine` interface + readonly "~type.constructor.default": TypeConstructorDefault + readonly "Iso": Iso + + readonly "~type.mutability": TypeMutability + readonly "~type.optionality": TypeOptionality + readonly "~encoded.mutability": EncodedMutability + readonly "~encoded.optionality": EncodedOptionality + + annotate(annotations: Annotations.Bottom): this["Rebuild"] + annotateKey(annotations: Annotations.Key): this["Rebuild"] + check(...checks: readonly [SchemaAST.Check, ...Array>]): this["Rebuild"] + rebuild(ast: this["ast"]): this["Rebuild"] + /** + * Constructs a value from the make input representation synchronously. + * + * **When to use** + * + * Use when constructor input is trusted or when validation failure + * should abort with a thrown `Error`. + * + * **Details** + * + * Applies constructor defaults and type-side validation according to + * `MakeOptions`. + * + * **Gotchas** + * + * Throws an `Error` with the schema issue in its `cause` when validation + * fails. + * Causes that contain defects, interruptions, or other non-schema reasons + * throw with the underlying `Cause` attached instead. + * + * @see {@link Bottom.makeOption} — construct synchronously and discard validation details + * @see {@link Bottom.makeEffect} — construct through `Effect` when validation failure should stay in the error channel + */ + make(input: this["~type.make.in"], options?: MakeOptions): this["Type"] + /** + * Constructs a value from the make input representation, returning `Option.none` + * when validation fails. + * + * **When to use** + * + * Use when you only need to know whether construction succeeds + * and do not need validation details. + * + * **Details** + * + * Applies constructor defaults and type-side validation according to + * `MakeOptions`. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `None`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @see {@link Bottom.make} — construct synchronously when validation failure should throw + * @see {@link Bottom.makeEffect} — construct through `Effect` when validation details should stay in the error channel + */ + makeOption(input: this["~type.make.in"], options?: MakeOptions): Option_.Option + /** + * Constructs a value from the make input representation, returning validation + * failures in the `Effect` error channel. + * + * **When to use** + * + * Use when constructor input may fail validation and you want to + * compose that failure with other `Effect` operations instead of throwing. + * + * @see {@link Bottom.make} — construct synchronously when validation failure should throw + * @see {@link Bottom.makeOption} — construct synchronously and discard validation details + */ + makeEffect(input: this["~type.make.in"], options?: MakeOptions): Effect.Effect +} + +/** + * Lazy `Bottom` variant for schema implementations that compute their public + * views on demand. + * + * **When to use** + * + * Use as an implementation base for schema interfaces that must expose + * `Bottom` behavior without forcing TypeScript to eagerly evaluate expensive + * `Type`, `Encoded`, or service views. + * + * **Details** + * + * The laziness is purely type-level; runtime behavior is unchanged. + * `BottomLazy` keeps the structural operations inherited from `Bottom`, but + * erases the expensive schema views to `unknown`. Concrete schema interfaces can + * then redeclare the precise views they expose. This keeps wide schemas such as + * `Struct` and `Union` cheaper when generic code reads a single view, while + * preserving their exact public types. + * + * @see {@link Bottom} for the fully parameterized schema interface when every + * view must be supplied directly. + * + * @category utility types + * @since 4.0.0 + */ +export interface BottomLazy< + out Ast extends SchemaAST.AST, + out Rebuild extends Top, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends + Bottom< + unknown, + unknown, + unknown, + unknown, + Ast, + Rebuild, + unknown, + unknown, + TypeParameters, + unknown, + TypeMutability, + TypeOptionality, + TypeConstructorDefault, + EncodedMutability, + EncodedOptionality + > +{} + +/** + * Type-level representation returned by {@link declareConstructor}. + * + * @category constructors + * @since 4.0.0 + */ +export interface declareConstructor, Iso = T> extends + Bottom< + T, + E, + TypeParameters[number]["DecodingServices"], + TypeParameters[number]["EncodingServices"], + SchemaAST.Declaration, + declareConstructor, + T, + Iso, + TypeParameters + > +{} + +/** + * Creates a schema for a **parametric** type (a generic container such as + * `Array`, `Option`, etc.) by accepting a list of type-parameter schemas + * and a decoder factory. + * + * **When to use** + * + * Use when you are defining a schema for a generic container whose validation + * depends on one or more type-parameter schemas. + * + * **Details** + * + * The outer call `declareConstructor()` fixes the decoded type `T`, + * the encoded type `E`, and the optional iso type. The inner call receives: + * - `typeParameters` — the concrete schemas for each type variable + * - `run` — a factory that, given resolved codecs for each type parameter, + * returns a parsing function `(u, ast, options) => Effect` + * - `annotations` — optional metadata + * + * @see {@link declare} for creating schemas for non-parametric types. + * + * **Example** (Schema for a parametric `Box` type) + * + * ```ts + * import { Effect, Option, Schema, SchemaIssue as Issue, SchemaParser } from "effect" + * + * interface Box { + * readonly value: A + * } + * + * const isBox = (u: unknown): u is Box => + * typeof u === "object" && u !== null && "value" in u + * + * const Box = (item: A) => + * Schema.declareConstructor, Box>()( + * [item], + * ([itemCodec]) => + * (u, ast, options) => { + * if (!isBox(u)) { + * return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(u))) + * } + * return Effect.map( + * SchemaParser.decodeUnknownEffect(itemCodec)(u.value, options), + * (value) => ({ value }) + * ) + * } + * ) + * + * const schema = Box(Schema.Number) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function declareConstructor() { + return >( + typeParameters: TypeParameters, + run: ( + typeParameters: { + readonly [K in keyof TypeParameters]: Codec + } + ) => ( + u: unknown, + self: SchemaAST.Declaration, + options: SchemaAST.ParseOptions + ) => Effect.Effect, + annotations?: Annotations.Declaration + ): declareConstructor => { + return make( + new SchemaAST.Declaration( + typeParameters.map(SchemaAST.getAST), + (typeParameters) => run(typeParameters.map((ast) => make(ast)) as any), + annotations + ) + ) + } +} + +/** + * Type-level representation returned by {@link declare}. + * + * @category constructors + * @since 3.13.3 + */ +export interface declare extends declareConstructor { + readonly "Rebuild": declare +} + +/** + * Creates a schema for a **non-parametric** opaque type using a type-guard + * function. The schema accepts any unknown value and succeeds when `is` returns + * `true`, failing with an `InvalidType` issue otherwise. + * + * **When to use** + * + * Use when you are defining a schema for an opaque type with no type parameters + * and validation can be expressed as a type guard. + * + * **Example** (Defining a schema for a custom `UserId` branded type) + * + * ```ts + * import { Schema } from "effect" + * + * type UserId = string & { readonly _tag: "UserId" } + * + * const isUserId = (u: unknown): u is UserId => + * typeof u === "string" && u.startsWith("user_") + * + * const UserId = Schema.declare(isUserId, { + * title: "UserId", + * description: "A user identifier starting with 'user_'" + * }) + * ``` + * + * @see {@link declareConstructor} for creating schemas for parametric types. + * + * @category constructors + * @since 3.10.0 + */ +export function declare( + is: (u: unknown) => u is T, + annotations?: Annotations.Declaration | undefined +): declare { + return declareConstructor()( + [], + () => (input, ast) => + is(input) ? + Effect.succeed(input) : + Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))), + annotations + ) +} + +/** + * Returns a schema widened to the fully-parameterized {@link Bottom} interface, + * making all 14 type parameters visible to TypeScript. + * + * **Details** + * + * Normally, concrete schema interfaces (e.g. `Schema`) hide most type + * parameters. `revealBottom` is useful when writing generic utilities that need + * to inspect or propagate the complete set of type parameters. + * + * **Example** (Inspecting all type parameters of a schema) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.String + * + * // Widen to Bottom to access all 14 type parameters + * const bottom = Schema.revealBottom(schema) + * + * // `bottom` now exposes Type, Encoded, DecodingServices, EncodingServices, + * // ast, Rebuild, ~type.make.in, Iso, ~type.parameters, etc. + * type T = typeof bottom["Type"] // string + * type E = typeof bottom["Encoded"] // string + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export function revealBottom( + bottom: S +): Bottom< + S["Type"], + S["Encoded"], + S["DecodingServices"], + S["EncodingServices"], + S["ast"], + S["Rebuild"], + S["~type.make.in"], + S["Iso"], + S["~type.parameters"], + S["~type.make"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] +> { + return bottom +} + +/** + * Adds metadata annotations to a schema without changing its runtime behavior. + * This is the pipeable (curried) counterpart of the `.annotate` method. + * + * **Details** + * + * Annotations provide extra context used by documentation generators, JSON + * Schema converters, error formatters, and other tooling. Common keys include + * `title`, `description`, `examples`, `message`, and `identifier`. + * + * **Example** (Adding a title and description) + * + * ```ts + * import { Schema } from "effect" + * + * const Age = Schema.Number.pipe( + * Schema.annotate({ + * title: "Age", + * description: "A non-negative integer representing age in years" + * }) + * ) + * ``` + * + * @see {@link annotateEncoded} to annotate the encoded side instead. + * + * @category annotations + * @since 4.0.0 + */ +export function annotate(annotations: Annotations.Bottom) { + return (self: S) => self.annotate(annotations) +} + +/** + * Adds metadata annotations to the **encoded** side of a schema without + * changing its runtime behavior. This is the encoded-side counterpart of + * `annotate`, which targets the decoded (Type) side. + * + * **Details** + * + * Internally the schema is flipped so that `Encoded` becomes `Type`, + * annotated, and then flipped back. + * + * **Example** (Adding a title to the encoded representation) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.NumberFromString.pipe( + * Schema.annotateEncoded({ + * title: "my title" + * }) + * ) + * + * console.log(Schema.toEncoded(schema).ast.annotations?.title) + * // "my title" + * ``` + * + * @see {@link annotate} to annotate the type side instead. + * + * @category annotations + * @since 4.0.0 + */ +export function annotateEncoded(annotations: Annotations.Bottom) { + return (self: S): S["Rebuild"] => flip(flip(self).annotate(annotations)) +} + +/** + * Adds key-level annotations to a schema field. This is the pipeable + * (curried) counterpart of the `.annotateKey` method. + * + * **Details** + * + * Key annotations apply to a field's position inside a `Struct` or `Tuple` + * rather than to the field's value type. They can carry a + * `messageMissingKey` to customise the error shown when the field is absent, + * as well as standard documentation fields such as `title`, `description`, + * and `examples`. + * + * **Example** (Customizing the missing-key message for a required field) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ + * username: Schema.String.pipe( + * Schema.annotateKey({ + * description: "The username used to log in", + * messageMissingKey: "Username is required" + * }) + * ) + * }) + * ``` + * + * @category annotations + * @since 4.0.0 + */ +export function annotateKey(annotations: Annotations.Key) { + return (self: S): S["Rebuild"] => { + return self.rebuild(SchemaAST.annotateKey(self.ast, annotations)) + } +} + +/** + * The existential "any schema" type — all type parameters are erased to `unknown`. + * + * **Details** + * + * Use `Top` as a constraint when writing generic utilities that must accept *any* + * schema regardless of its `Type`, `Encoded`, or service requirements. It is the + * widest possible schema type and therefore gives you the least static information. + * + * In user code prefer the narrower interfaces: + * - {@link Schema}`` — when you only care about the decoded type + * - {@link Codec}`` — when you need the encoded type and service requirements + * - {@link ConstraintDecoder}`` — for decode-only APIs + * - {@link ConstraintEncoder}`` — for encode-only APIs + * + * @category models + * @since 4.0.0 + */ +export interface Top extends + Bottom< + unknown, + unknown, + unknown, + unknown, + SchemaAST.AST, + Top, + unknown, + unknown, + any, // this is because TypeParameters is invariant + unknown, + Mutability, + Optionality, + ConstructorDefault, + Mutability, + Optionality + > +{} + +/** + * Lightweight structural constraint for APIs that accept schema values but only + * read their data and type-level views. + * + * **When to use** + * + * Use when you need to constrain a generic value to be a schema, but the API + * only reads properties such as `ast`, `Type`, `Encoded`, service + * requirements, constructor input views, or modifier flags. + * + * **Details** + * + * `Constraint` keeps the schema type identifier and the property surface needed + * by schema constructors, while avoiding the full `Bottom` protocol. Use + * {@link Top} when an API calls schema methods such as `annotate`, `check`, + * `rebuild`, `make`, or `makeEffect`. + * + * @see {@link Top} for the complete schema protocol. + * + * @category models + * @since 4.0.0 + */ +export interface Constraint { + readonly [TypeId]: typeof TypeId + readonly "ast": SchemaAST.AST + + readonly "Type": unknown + readonly "Encoded": unknown + readonly "DecodingServices": unknown + readonly "EncodingServices": unknown + + readonly "~type.parameters": any + readonly "~type.make.in": unknown + readonly "~type.make": unknown + readonly "Iso": unknown + + readonly "~type.optionality": Optionality + readonly "~type.mutability": Mutability + readonly "~type.constructor.default": ConstructorDefault + readonly "~encoded.optionality": Optionality + readonly "~encoded.mutability": Mutability +} + +/** + * Lightweight structural constraint for APIs that need codec type views but do + * not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve decoded type, encoded type, and service + * requirements for a schema value, but the API does not call schema methods + * such as `annotate`, `check`, `rebuild`, `make`, or `makeEffect`. + * + * @see {@link Constraint} for the generic lightweight schema constraint. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintCodec extends Constraint { + readonly "Type": T + readonly "Encoded": E + readonly "DecodingServices": RD + readonly "EncodingServices": RE +} + +/** + * Lightweight structural constraint for APIs that need decoder type views but + * do not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve a schema's decoded type and decoding services, + * but the API does not constrain the encoded type, encoding services, or call + * schema methods such as `annotate`, `check`, `rebuild`, `make`, or + * `makeEffect`. + * + * @see {@link ConstraintCodec} for APIs that need both decoded and encoded codec views. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintDecoder extends ConstraintCodec {} + +/** + * Lightweight structural constraint for APIs that need encoder type views but + * do not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve a schema's encoded type and encoding services, + * but the API does not constrain the decoded type, decoding services, or call + * schema methods such as `annotate`, `check`, `rebuild`, `make`, or + * `makeEffect`. + * + * @see {@link ConstraintCodec} for APIs that need both decoded and encoded codec views. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintEncoder extends ConstraintCodec {} + +/** + * Lightweight structural constraint for APIs that need schema views and the + * rebuilt schema type, but do not call the full schema protocol. + * + * **When to use** + * + * Use when an API needs to read `Rebuild` in addition to the schema views + * exposed by {@link Constraint}, but does not call methods such as `annotate`, + * `check`, `rebuild`, `make`, or `makeEffect`. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintRebuildable extends Constraint { + readonly "Rebuild": Constraint +} + +/** + * Namespace of type-level helpers for {@link Schema}. + * + * @since 3.10.0 + */ +export declare namespace Schema { + /** + * Extracts the decoded `Type` from a schema. + * + * **Example** (Extracting the decoded type) + * + * ```ts + * import { Schema } from "effect" + * + * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) + * type Person = Schema.Schema.Type + * // { readonly name: string; readonly age: number } + * ``` + * + * @category utility types + * @since 3.10.0 + */ + export type Type = S extends { readonly "Type": infer T } ? T : never +} + +/** + * A typed view of a schema that tracks only the decoded (output) type `T`. + * + * **Details** + * + * Use `Schema` as a constraint when you want to accept "any schema that + * decodes to `T`" and do not need to know or constrain the encoded + * representation, required services, or any other type parameters. + * + * This is a structural interface — concrete schema values are produced by the + * constructors in this module (e.g. {@link Struct}, {@link String}, {@link Number}). + * When you also need the encoded type or service requirements, use {@link Codec}. + * + * **Example** (Accepting any schema decoding to `string`) + * + * ```ts + * import { Schema } from "effect" + * + * declare function print(schema: Schema.Schema): void + * + * print(Schema.String) // ok + * print(Schema.NonEmptyString) // ok + * ``` + * + * @see {@link Codec} — also tracks Encoded, DecodingServices, EncodingServices + * @see {@link Schema.Type} — extract the decoded type at the type level + * + * @category models + * @since 3.10.0 + */ +export interface Schema extends Top { + readonly "Type": T + readonly "Rebuild": Schema +} + +/** + * Namespace of type-level helpers for {@link Codec}. + * + * @since 4.0.0 + */ +export declare namespace Codec { + /** + * Extracts the encoded (`Encoded`) type from a schema. + * + * **Example** (Extracting the encoded type) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.NumberFromString + * type Enc = Schema.Codec.Encoded + * // string + * ``` + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = S extends { readonly "Encoded": infer E } ? E : never + + /** + * Extracts the Effect services required during *decoding* from a schema. + * + * **Example** (Checking decoding service requirements) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.String + * type RD = Schema.Codec.DecodingServices + * // never + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type DecodingServices = S extends { readonly "DecodingServices": infer R } ? R : never + + /** + * Extracts the Effect services required during *encoding* from a schema. + * + * **Example** (Checking encoding service requirements) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.String + * type RE = Schema.Codec.EncodingServices + * // never + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type EncodingServices = S extends { readonly "EncodingServices": infer R } ? R : never +} + +/** + * A schema that tracks the decoded type `T`, the encoded type `E`, and the + * Effect services required during decoding (`RD`) and encoding (`RE`). + * + * **Details** + * + * Use `Codec` when you need to preserve full type information + * about a schema — both what it decodes to and what it serializes from/to. + * Most concrete schemas produced by this module implement `Codec`. + * + * For APIs that only need one direction, prefer the narrower views: + * - {@link Decoder}`` — decode-only + * - {@link Encoder}`` — encode-only + * - {@link Schema}`` — type-only (no encoded representation) + * + * **Example** (Accepting a codec that decodes to `number` from `string`) + * + * ```ts + * import { Schema } from "effect" + * + * declare function serialize(codec: Schema.Codec): string + * + * serialize(Schema.NumberFromString) // ok — decodes number, encoded as string + * ``` + * + * @see {@link Codec.Encoded} — extract the encoded type + * @see {@link Codec.DecodingServices} — extract required decoding services + * @see {@link Codec.EncodingServices} — extract required encoding services + * @see {@link revealCodec} — helper to make TypeScript infer the full Codec type + * + * @category models + * @since 4.0.0 + */ +export interface Codec extends Schema { + readonly "Encoded": E + readonly "DecodingServices": RD + readonly "EncodingServices": RE + readonly "Rebuild": Codec +} + +/** + * A schema that tracks the decoded type `T` and the Effect services required + * during decoding (`RD`). + * + * **When to use** + * + * Use when you need to preserve a schema's decoded type and decoding service + * requirements, but do not need to constrain its encoded representation or + * encoding services. + * + * @see {@link Codec} for preserving both decoded and encoded type information. + * @see {@link Encoder} for the encode-only view. + * + * @category models + * @since 4.0.0 + */ +export interface Decoder extends Schema { + readonly "Encoded": unknown + readonly "DecodingServices": RD + readonly "EncodingServices": unknown + readonly "Rebuild": Decoder +} + +/** + * A schema that tracks the encoded type `E` and the Effect services required + * during encoding (`RE`). + * + * **When to use** + * + * Use when you need to preserve a schema's encoded type and encoding service + * requirements, but do not need to constrain its decoded representation or + * decoding services. + * + * @see {@link Codec} for preserving both decoded and encoded type information. + * @see {@link Decoder} for the decode-only view. + * + * @category models + * @since 4.0.0 + */ +export interface Encoder extends Schema { + readonly "Encoded": E + readonly "DecodingServices": unknown + readonly "EncodingServices": RE + readonly "Rebuild": Encoder +} + +/** + * Returns a codec widened to the full {@link Codec} interface, prompting + * TypeScript to infer all four type parameters (`T`, `E`, `RD`, `RE`). + * + * **Details** + * + * When a schema is stored in a variable typed as `Schema` or `Top`, the + * encoded type and service requirements are erased. Passing the value through + * `revealCodec` recovers those parameters without any runtime cost. + * + * **Example** (Recovering encoded type from a schema variable) + * + * ```ts + * import { Schema } from "effect" + * + * const schema: Schema.Schema = Schema.NumberFromString + * + * // Without revealCodec, Encoded is unknown + * const codec = Schema.revealCodec(schema) + * type Enc = typeof codec["Encoded"] // string + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export function revealCodec(codec: Codec) { + return codec +} + +/** + * A schema that additionally supports optic (lens/prism) operations. + * + * **Details** + * + * `Optic` extends {@link Schema}`` with an `Iso` type that + * describes the isomorphic counterpart used by the optic layer. Crucially, + * decoding and encoding require *no* Effect services (`DecodingServices` and + * `EncodingServices` are both `never`), which means the optic can operate + * purely without an Effect runtime. + * + * Most primitive schemas (e.g. `Schema.String`, `Schema.Number`) implement + * `Optic` automatically. You normally interact with this interface through + * {@link Optic_} utilities rather than constructing it directly. + * + * @category models + * @since 4.0.0 + */ +export interface Optic extends Schema { + readonly "Iso": Iso + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "Rebuild": Optic +} + +export { + /** + * Returns `true` if `u` is a {@link SchemaError}. + * + * **Example** (Narrowing Schema errors in a catch block) + * + * ```ts + * import { Schema } from "effect" + * + * try { + * Schema.decodeUnknownSync(Schema.Number)("oops") + * } catch (err) { + * if (Schema.isSchemaError(err)) { + * console.log(err._tag) // "SchemaError" + * } + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ + isSchemaError, + /** + * Error thrown (or returned as the error channel value) when schema decoding + * or encoding fails. + * + * **Details** + * + * The `issue` field contains a structured {@link SchemaIssue.Issue} tree describing + * every validation failure, including the path to the problematic value, + * expected types, and actual values received. `message` renders the issue tree + * as a human-readable string. + * + * Use {@link isSchemaError} to narrow an unknown value to `SchemaError`. + * + * **Example** (Catching a SchemaError) + * + * ```ts + * import { Schema } from "effect" + * + * try { + * Schema.decodeUnknownSync(Schema.Number)("not a number") + * } catch (err) { + * if (Schema.isSchemaError(err)) { + * console.log(err.message) + * // Expected number, actual "not a number" + * } + * } + * ``` + * + * @category errors + * @since 4.0.0 + */ + SchemaError +} + +function makeStandardResult(exit: Exit_.Exit>): StandardSchemaV1.Result { + return Exit_.isSuccess(exit) ? exit.value : { + issues: [{ message: Cause_.pretty(exit.cause) }] + } +} + +/** + * Returns a "Standard Schema" object conforming to the [Standard Schema + * v1](https://standardschema.dev/) specification. + * + * **Details** + * + * This function creates a schema whose `validate` method attempts to decode and + * validate the provided input synchronously. If the underlying `Schema` + * includes any asynchronous components (e.g., asynchronous message resolutions + * or checks), then validation will necessarily return a `Promise` instead. + * + * **Example** (Creating a standard schema from a regular schema) + * + * ```ts + * import { Schema } from "effect" + * + * // Define custom hook functions for error formatting + * const leafHook = (issue: any) => { + * switch (issue._tag) { + * case "InvalidType": + * return "Expected different type" + * case "InvalidValue": + * return "Invalid value provided" + * case "MissingKey": + * return "Required property missing" + * case "UnexpectedKey": + * return "Unexpected property found" + * case "Forbidden": + * return "Operation not allowed" + * case "OneOf": + * return "Multiple valid options available" + * default: + * return "Validation error" + * } + * } + * + * // Create a standard schema from a regular schema + * const PersonSchema = Schema.Struct({ + * name: Schema.NonEmptyString, + * age: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 })) + * }) + * + * const standardSchema = Schema.toStandardSchemaV1(PersonSchema, { + * leafHook + * }) + * + * // The standard schema can be used with any Standard Schema v1 compatible library + * const validResult = standardSchema["~standard"].validate({ + * name: "Alice", + * age: 30 + * }) + * console.log(validResult) // { value: { name: "Alice", age: 30 } } + * + * const invalidResult = standardSchema["~standard"].validate({ + * name: "", + * age: 200 + * }) + * console.log(invalidResult) // { issues: [{ path: ["name"], message: "..." }, { path: ["age"], message: "..." }] } + * ``` + * + * @category Standard Schema + * @since 4.0.0 + */ +export function toStandardSchemaV1>( + self: S, + options?: { + readonly leafHook?: SchemaIssue.LeafHook | undefined + readonly checkHook?: SchemaIssue.CheckHook | undefined + readonly parseOptions?: SchemaAST.ParseOptions | undefined + } +): StandardSchemaV1 & S { + const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(self) as ( + input: unknown, + options?: SchemaAST.ParseOptions + ) => Effect.Effect + const parseOptions: SchemaAST.ParseOptions = { errors: "all", ...options?.parseOptions } + const formatter = SchemaIssue.makeFormatterStandardSchemaV1(options) + const validate: StandardSchemaV1["~standard"]["validate"] = (value: unknown) => { + const scheduler = new Scheduler.MixedScheduler() + const fiber = Effect.runFork( + Effect.match(decodeUnknownEffect(value, parseOptions), { + onFailure: formatter, + onSuccess: (value): StandardSchemaV1.Result => ({ value }) + }), + { scheduler } + ) + fiber.currentDispatcher?.flush() + const exit = fiber.pollUnsafe() + if (exit) { + return makeStandardResult(exit) + } + return new Promise((resolve) => { + fiber.addObserver((exit) => { + resolve(makeStandardResult(exit)) + }) + }) + } + if ("~standard" in self) { + const out = self as any + if ("validate" in out["~standard"]) return out + Object.assign(out["~standard"], { validate }) + return out + } else { + return Object.assign(self, { + "~standard": { + version: 1, + vendor: "effect", + validate + } as const + }) + } +} + +function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchemaV1.Target): JsonSchema.JsonSchema { + const doc2020_12 = toJsonSchemaDocument(self) + if (target === "draft-2020-12") { + const schema = doc2020_12.schema + if (Object.keys(doc2020_12.definitions).length > 0) { + schema.$defs = doc2020_12.definitions + } + return schema + } else if (target === "draft-07") { + const doc07 = JsonSchema.toDocumentDraft07(doc2020_12) + const schema = doc07.schema + if (Object.keys(doc07.definitions).length > 0) { + schema.definitions = doc07.definitions + } + return schema + } + throw new globalThis.Error(`Unsupported target: ${target}`) +} + +/** + * Converts a schema to an experimental Standard JSON Schema V1 representation. + * + * **Details** + * + * https://github.com/standard-schema/standard-schema/pull/134 + * + * @category Standard Schema + * @since 4.0.0 + */ +export function toStandardJSONSchemaV1( + self: S +): StandardJSONSchemaV1 & S { + const jsonSchema: StandardJSONSchemaV1.Props["jsonSchema"] = { + input(options) { + return toBaseStandardJSONSchemaV1(self, options.target) + }, + output(options) { + return toBaseStandardJSONSchemaV1(toType(self), options.target) + } + } + if ("~standard" in self) { + const out = self as any + if ("jsonSchema" in out["~standard"]) return out + Object.assign(out["~standard"], { jsonSchema }) + return out + } else { + return Object.assign(self, { + "~standard": { + version: 1, + vendor: "effect", + jsonSchema + } as const + }) + } +} + +/** + * Creates a type guard function that checks if a value conforms to a given + * schema. + * + * **Details** + * + * This function returns a predicate that performs a type-safe check, narrowing + * the type of the input value if the check passes. The predicate returns `false` + * for schema mismatches. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `false`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * **Example** (Defining a basic type guard) + * + * ```ts + * import { Schema } from "effect" + * + * const isString = Schema.is(Schema.String) + * + * console.log(isString("hello")) // true + * console.log(isString(42)) // false + * + * // Type narrowing in action + * const value: unknown = "hello" + * if (isString(value)) { + * // value is now typed as string + * console.log(value.toUpperCase()) // "HELLO" + * } + * ``` + * + * @category guards + * @since 3.10.0 + */ +export const is = SchemaParser.is + +/** + * Creates an assertion function that throws an error if the input does not match + * the schema. + * + * **When to use** + * + * Use to validate unknown input at runtime while narrowing the value with a + * TypeScript assertion signature. + * + * **Details** + * + * The input is narrowed if the assertion succeeds. If schema validation fails, + * the assertion throws an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or other non-schema reasons throw + * with the underlying `Cause` attached instead of being converted to schema + * validation errors. + * + * **Example** (Asserting and narrowing an input) + * + * ```ts + * import { Schema } from "effect" + * + * const input: unknown = "hello" + * + * // This will pass silently (no return value) and narrow input to string + * Schema.asserts(Schema.String, input) + * console.log(input.toUpperCase()) + * + * // This will throw an error + * try { + * const invalid: unknown = 123 + * Schema.asserts(Schema.String, invalid) + * } catch (error) { + * console.log("Non-string assertion failed as expected") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = + SchemaParser.asserts + +/** + * Decodes an `unknown` input against a schema, returning an `Effect` that + * succeeds with the decoded value or fails with a {@link SchemaError}. + * + * **When to use** + * + * Use when you need to decode unknown input in an `Effect` whose failure + * channel is `SchemaError`. + * + * **Details** + * + * Prefer {@link decodeEffect} when the input is already typed as the schema's + * `Encoded` type. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @see {@link SchemaParser.decodeUnknownEffect} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.decodeUnknownEffect(schema, options) + return ( + input: unknown, + options?: SchemaAST.ParseOptions + ): Effect.Effect => { + return InternalSchema.fromIssueEffect(parser(input, options)) + } +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema, + * returning an `Effect` that succeeds with the decoded value or fails with a + * {@link SchemaError}. + * + * **When to use** + * + * Use when you need to decode input already typed as the schema's `Encoded` + * type in an `Effect` whose failure channel is `SchemaError`. + * + * **Details** + * + * For `unknown` input use {@link decodeUnknownEffect}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * @see {@link SchemaParser.decodeEffect} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export const decodeEffect: ( + schema: S, + options?: SchemaAST.ParseOptions +) => ( + input: S["Encoded"], + options?: SchemaAST.ParseOptions +) => Effect.Effect = decodeUnknownEffect + +function getSchemaErrorOrThrow( + cause: Cause_.Cause, + message: string +): SchemaError { + let schemaError: SchemaError | undefined + for (const reason of cause.reasons) { + if (!Cause_.isFailReason(reason) || !isSchemaError(reason.error)) { + throw new globalThis.Error(message, { cause }) + } + schemaError ??= reason.error + } + if (schemaError === undefined) { + throw new globalThis.Error(message, { cause }) + } + return schemaError +} + +function runSchemaErrorPromise( + self: Effect.Effect +): Promise { + return Effect.runPromiseExit(self).then((exit) => { + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Promise adapter can only reject schema errors") + }) +} + +function runSchemaErrorSync( + self: Effect.Effect +): A { + const exit = Effect.runSyncExit(self) + if (Exit_.isSuccess(exit)) { + return exit.value + } + throw getSchemaErrorOrThrow(exit.cause, "Sync adapter can only throw schema errors") +} + +/** + * Decodes an `unknown` input against a schema synchronously, returning an + * `Exit` that is either a `Success` with the decoded value or a `Failure`. + * + * **When to use** + * + * Use when you need to decode unknown input into an `Exit` and capture schema + * mismatches as `SchemaError`. + * + * **Details** + * + * Only usable with schemas that have no `DecodingServices` requirement. Prefer + * {@link decodeExit} when the input is already typed as the schema's `Encoded` + * type. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * Schema mismatches are represented by a `Failure` cause containing + * `SchemaError`. + * + * **Gotchas** + * + * Schema issue fail reasons are wrapped as `SchemaError`. Defects, + * interruptions, and other non-schema reasons remain in the returned `Cause`, + * including when they are mixed with schema issues. + * + * @see {@link SchemaParser.decodeUnknownExit} for the adapter whose failure contains `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.decodeUnknownExit(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Exit_.Exit => { + return fromIssueExit(parser(input, options)) + } +} + +function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { + return Exit_.isSuccess(exit) + ? Exit_.succeed(exit.value) + : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema + * synchronously, returning an `Exit` that is either a `Success` with the decoded + * value or a `Failure`. + * + * **When to use** + * + * Use when you need to decode already typed `Encoded` input into an `Exit` and + * capture schema mismatches as `SchemaError`. + * + * **Details** + * + * Only usable with schemas that have no `DecodingServices` requirement. For + * `unknown` input use {@link decodeUnknownExit}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * Schema mismatches are represented by a `Failure` cause containing + * `SchemaError`. + * + * **Gotchas** + * + * Schema issue fail reasons are wrapped as `SchemaError`. Defects, + * interruptions, and other non-schema reasons remain in the returned `Cause`, + * including when they are mixed with schema issues. + * + * @see {@link SchemaParser.decodeExit} for the adapter whose failure contains `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export const decodeExit: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit_.Exit = decodeUnknownExit + +/** + * Decodes an `unknown` input against a schema, returning an `Option` that is + * `Some` with the decoded value on success or `None` for schema mismatches. + * + * **When to use** + * + * Use when you do not know the input type statically and only need to know + * whether decoding succeeded. + * + * **Details** + * + * Prefer this over {@link decodeUnknownExit} or {@link decodeUnknownEffect} + * when you don't need error details. For input already typed as the schema's + * `Encoded` type use {@link decodeOption}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `None`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @category decoding + * @since 3.10.0 + */ +export const decodeUnknownOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeUnknownOption + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema, + * returning an `Option` that is `Some` with the decoded value on success or + * `None` for schema mismatches. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and + * only need to know whether decoding succeeded. + * + * **Details** + * + * For `unknown` input use {@link decodeUnknownOption}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `None`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @category decoding + * @since 3.10.0 + */ +export const decodeOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeOption + +/** + * Decodes an `unknown` input against a schema, returning a `Result` that + * succeeds with the decoded value or fails with a {@link SchemaError} for schema + * mismatches. + * + * **When to use** + * + * Use when you do not know the input type statically and want schema mismatches + * returned as `Result.fail` with `SchemaError`. + * + * **Details** + * + * For input already typed as the schema's `Encoded` type use + * {@link decodeResult}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * Schema mismatches are returned as `Result.fail` with `SchemaError`. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are returned as `Result.fail`. + * Causes that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @see {@link SchemaParser.decodeUnknownResult} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.decodeUnknownResult(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Result_.Result => { + return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) + } +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema, + * returning a `Result` that succeeds with the decoded value or fails with a + * {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and want + * schema mismatches returned as `Result.fail` with `SchemaError`. + * + * **Details** + * + * For `unknown` input use {@link decodeUnknownResult}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * Schema mismatches are returned as `Result.fail` with `SchemaError`. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are returned as `Result.fail`. + * Causes that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @see {@link SchemaParser.decodeResult} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category decoding + * @since 4.0.0 + */ +export const decodeResult: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result_.Result = + decodeUnknownResult + +/** + * Decodes an `unknown` input against a schema, returning a `Promise` that + * resolves with the decoded value or rejects with a {@link SchemaError} for + * schema mismatches. + * + * **When to use** + * + * Use when you need decoding of unknown input to return a JavaScript `Promise` + * that rejects with `SchemaError` for schema mismatches. + * + * **Details** + * + * For input already typed as the schema's `Encoded` type use + * {@link decodePromise}. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may reject with a runtime failure instead of + * `SchemaError`. + * + * @see {@link SchemaParser.decodeUnknownPromise} for the adapter that rejects with an `Error` whose cause is `SchemaIssue.Issue` + * + * @category decoding + * @since 3.10.0 + */ +export function decodeUnknownPromise>( + schema: S, + options?: SchemaAST.ParseOptions +) { + const parser = decodeUnknownEffect(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Promise => { + return runSchemaErrorPromise(parser(input, options)) + } +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema, + * returning a `Promise` that resolves with the decoded value or rejects with a + * {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and + * need decoding to return a JavaScript `Promise` that rejects with + * `SchemaError` for schema mismatches. + * + * **Details** + * + * For `unknown` input use `decodeUnknownPromise`. + * Options may be provided either when creating the decoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may reject with a runtime failure instead of + * `SchemaError`. + * + * @see {@link SchemaParser.decodePromise} for the adapter that rejects with an `Error` whose cause is `SchemaIssue.Issue` + * + * @category decoding + * @since 3.10.0 + */ +export const decodePromise: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise = decodeUnknownPromise + +/** + * Decodes an `unknown` input against a schema synchronously, returning the + * decoded value or throwing a {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you need to validate unknown data at a synchronous boundary and want + * schema mismatches to throw `SchemaError`. + * + * **Details** + * + * For input already typed as the schema's `Encoded` type use `decodeSync`. + * Only service-free schemas can be decoded synchronously. For alternatives that + * do not throw on schema mismatches, see `decodeUnknownOption`, + * `decodeUnknownExit`, or `decodeUnknownEffect`. Options may be provided either + * when creating the decoder or when applying it; application options override + * creation options. + * + * **Gotchas** + * + * Non-schema failures may throw a runtime failure instead of `SchemaError`. + * + * **Example** (Decoding with a transformation schema) + * + * ```ts + * import { Schema } from "effect" + * + * const NumberFromString = Schema.NumberFromString + * + * console.log(Schema.decodeUnknownSync(NumberFromString)("42")) + * // Output: 42 + * + * Schema.decodeUnknownSync(NumberFromString)("not a number") + * // throws SchemaError: NumberFromString + * // └─ Encoded side transformation failure + * // └─ NumberFromString + * // └─ Expected a numeric string, actual "not a number" + * ``` + * + * @see {@link SchemaParser.decodeUnknownSync} for the adapter that throws an `Error` whose cause is `SchemaIssue.Issue` + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = decodeUnknownEffect(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): S["Type"] => { + return runSchemaErrorSync(parser(input, options)) + } +} + +/** + * Decodes a typed input (the schema's `Encoded` type) against a schema + * synchronously, returning the decoded value or throwing a {@link SchemaError} + * for schema mismatches. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and + * want schema mismatches to throw `SchemaError` synchronously. + * + * **Details** + * + * For `unknown` input use `decodeUnknownSync`. + * Only service-free schemas can be decoded synchronously. Options may be + * provided either when creating the decoder or when applying it; application + * options override creation options. + * + * **Gotchas** + * + * Non-schema failures may throw a runtime failure instead of `SchemaError`. + * + * @see {@link SchemaParser.decodeSync} for the adapter that throws an `Error` whose cause is `SchemaIssue.Issue` + * + * @category decoding + * @since 4.0.0 + */ +export const decodeSync: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync + +/** + * Encodes an `unknown` input against a schema, returning an `Effect` that + * succeeds with the encoded value or fails with a {@link SchemaError}. + * + * **When to use** + * + * Use when you need to encode unknown input in an `Effect` whose failure + * channel is `SchemaError`. + * + * **Details** + * + * Prefer {@link encodeEffect} when the value is already typed as the schema's + * `Type`. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Example** (Encoding a value to a string) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const NumberFromString = Schema.NumberFromString + * + * Effect.runPromise(Schema.encodeUnknownEffect(NumberFromString)(42)).then(console.log) + * // Output: "42" + * ``` + * + * @see {@link SchemaParser.encodeUnknownEffect} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.encodeUnknownEffect(schema, options) + return ( + input: unknown, + options?: SchemaAST.ParseOptions + ): Effect.Effect => { + return InternalSchema.fromIssueEffect(parser(input, options)) + } +} + +/** + * Encodes a typed input (the schema's `Type`) against a schema, returning an + * `Effect` that succeeds with the encoded value or fails with a + * {@link SchemaError}. + * + * **When to use** + * + * Use when you need to encode input already typed as the schema's `Type` in + * an `Effect` whose failure channel is `SchemaError`. + * + * **Details** + * + * For `unknown` input use {@link encodeUnknownEffect}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * @see {@link SchemaParser.encodeEffect} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export const encodeEffect: ( + schema: S, + options?: SchemaAST.ParseOptions +) => ( + input: S["Type"], + options?: SchemaAST.ParseOptions +) => Effect.Effect = encodeUnknownEffect + +/** + * Encodes an `unknown` input against a schema synchronously, returning an + * `Exit` that is either a `Success` with the encoded value or a `Failure`. + * + * **When to use** + * + * Use when you need to encode unknown input into an `Exit` and capture schema + * mismatches as `SchemaError`. + * + * **Details** + * + * Only usable with schemas that have no `EncodingServices` requirement. Prefer + * {@link encodeExit} when the value is already typed as the schema's `Type`. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * Schema mismatches are represented by a `Failure` cause containing + * `SchemaError`. + * + * **Gotchas** + * + * Schema issue fail reasons are wrapped as `SchemaError`. Defects, + * interruptions, and other non-schema reasons remain in the returned `Cause`, + * including when they are mixed with schema issues. + * + * @see {@link SchemaParser.encodeUnknownExit} for the adapter whose failure contains `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.encodeUnknownExit(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Exit_.Exit => { + return fromIssueExit(parser(input, options)) + } +} + +/** + * Encodes a typed input (the schema's `Type`) against a schema synchronously, + * returning an `Exit` that is either a `Success` with the encoded value or a + * `Failure`. + * + * **When to use** + * + * Use when you need to encode already typed schema values into an `Exit` and + * capture schema mismatches as `SchemaError`. + * + * **Details** + * + * Only usable with schemas that have no `EncodingServices` requirement. For + * `unknown` input use {@link encodeUnknownExit}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * Schema mismatches are represented by a `Failure` cause containing + * `SchemaError`. + * + * **Gotchas** + * + * Schema issue fail reasons are wrapped as `SchemaError`. Defects, + * interruptions, and other non-schema reasons remain in the returned `Cause`, + * including when they are mixed with schema issues. + * + * @see {@link SchemaParser.encodeExit} for the adapter whose failure contains `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export const encodeExit: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit_.Exit = encodeUnknownExit + +/** + * Encodes an `unknown` input against a schema, returning an `Option` that is + * `Some` with the encoded value on success or `None` for schema mismatches. + * + * **When to use** + * + * Use when you do not know the input type statically and only need to know + * whether encoding succeeded. + * + * **Details** + * + * Prefer this over {@link encodeUnknownExit} or {@link encodeUnknownEffect} + * when you don't need error details. For values already typed as the schema's + * `Type` use {@link encodeOption}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `None`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = + SchemaParser.encodeUnknownOption + +/** + * Encodes a typed input (the schema's `Type`) against a schema, returning an + * `Option` that is `Some` with the encoded value on success or `None` for schema + * mismatches. + * + * **When to use** + * + * Use when you already have a value typed as the schema's `Type` and only need + * to know whether encoding succeeded. + * + * **Details** + * + * For `unknown` input use {@link encodeUnknownOption}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `None`. Causes + * that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @category encoding + * @since 3.10.0 + */ +export const encodeOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeOption + +/** + * Encodes an `unknown` input against a schema, returning a `Result` that + * succeeds with the encoded value or fails with a {@link SchemaError} for schema + * mismatches. + * + * **When to use** + * + * Use when you do not know the input type statically and want schema mismatches + * returned as `Result.fail` with `SchemaError`. + * + * **Details** + * + * For values already typed as the schema's `Type` use {@link encodeResult}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * Schema mismatches are returned as `Result.fail` with `SchemaError`. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are returned as `Result.fail`. + * Causes that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @see {@link SchemaParser.encodeUnknownResult} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = SchemaParser.encodeUnknownResult(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Result_.Result => { + return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) + } +} + +/** + * Encodes a typed input (the schema's `Type`) against a schema, returning a + * `Result` that succeeds with the encoded value or fails with a + * {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you already have a value typed as the schema's `Type` and want schema + * mismatches returned as `Result.fail` with `SchemaError`. + * + * **Details** + * + * For `unknown` input use {@link encodeUnknownResult}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * Schema mismatches are returned as `Result.fail` with `SchemaError`. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are returned as `Result.fail`. + * Causes that contain defects, interruptions, or other non-schema reasons throw + * instead. + * + * @see {@link SchemaParser.encodeResult} for the adapter that fails with `SchemaIssue.Issue` directly + * + * @category encoding + * @since 4.0.0 + */ +export const encodeResult: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result_.Result = + encodeUnknownResult + +/** + * Encodes an `unknown` input against a schema, returning a `Promise` that + * resolves with the encoded value or rejects with a {@link SchemaError} for + * schema mismatches. + * + * **When to use** + * + * Use when you need encoding of unknown input to return a JavaScript `Promise` + * that rejects with `SchemaError` for schema mismatches. + * + * **Details** + * + * For values already typed as the schema's `Type` use {@link encodePromise}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may reject with a runtime failure instead of + * `SchemaError`. + * + * @see {@link SchemaParser.encodeUnknownPromise} for the adapter that rejects with an `Error` whose cause is `SchemaIssue.Issue` + * + * @category encoding + * @since 3.10.0 + */ +export function encodeUnknownPromise>( + schema: S, + options?: SchemaAST.ParseOptions +) { + const parser = encodeUnknownEffect(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): Promise => { + return runSchemaErrorPromise(parser(input, options)) + } +} + +/** + * Encodes a typed input (the schema's `Type`) against a schema, returning a + * `Promise` that resolves with the encoded value or rejects with a + * {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you already have a value typed as the schema's `Type` and need + * encoding to return a JavaScript `Promise` that rejects with `SchemaError` for + * schema mismatches. + * + * **Details** + * + * For `unknown` input use {@link encodeUnknownPromise}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may reject with a runtime failure instead of + * `SchemaError`. + * + * @see {@link SchemaParser.encodePromise} for the adapter that rejects with an `Error` whose cause is `SchemaIssue.Issue` + * + * @category encoding + * @since 3.10.0 + */ +export const encodePromise: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise + +/** + * Encodes an `unknown` input against a schema synchronously, throwing a + * {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you need to serialize unknown data at a synchronous boundary and + * want schema mismatches to throw `SchemaError`. + * + * **Details** + * + * For alternatives that do not throw on schema mismatches, see + * {@link encodeUnknownOption}, {@link encodeUnknownExit}, or + * {@link encodeUnknownEffect}. For values already typed as the schema's `Type` + * use {@link encodeSync}. Options may be provided either when creating the + * encoder or when applying it; application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may throw a runtime failure instead of `SchemaError`. + * + * @see {@link SchemaParser.encodeUnknownSync} for the adapter that throws an `Error` whose cause is `SchemaIssue.Issue` + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { + const parser = encodeUnknownEffect(schema, options) + return (input: unknown, options?: SchemaAST.ParseOptions): S["Encoded"] => { + return runSchemaErrorSync(parser(input, options) as Effect.Effect) + } +} + +/** + * Encodes a typed input (the schema's `Type`) against a schema synchronously, + * throwing a {@link SchemaError} for schema mismatches. + * + * **When to use** + * + * Use when you already have a value typed as the schema's `Type` and want + * schema mismatches to throw `SchemaError` synchronously. + * + * **Details** + * + * For `unknown` input use {@link encodeUnknownSync}. + * Options may be provided either when creating the encoder or when applying it; + * application options override creation options. + * + * **Gotchas** + * + * Non-schema failures may throw a runtime failure instead of `SchemaError`. + * + * @see {@link SchemaParser.encodeSync} for the adapter that throws an `Error` whose cause is `SchemaIssue.Issue` + * + * @category encoding + * @since 4.0.0 + */ +export const encodeSync: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync + +/** + * Creates a schema from an AST (Abstract Syntax Tree) node. + * + * **Details** + * + * This is the fundamental constructor for all schemas in the Effect Schema + * library. It takes an AST node and wraps it in a fully-typed schema that + * preserves all type information and provides the complete schema API. + * + * The `make` function is used internally to create all primitive schemas like + * `String`, `Number`, `Boolean`, etc., as well as more complex schemas. It's + * the bridge between the untyped AST representation and the strongly-typed + * schema. + * + * @category constructors + * @since 3.10.0 + */ +export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make + +/** + * Transforms a schema into a class that can be extended with `extends`. The + * resulting class inherits the full schema API (e.g. `annotate`) and can define + * static methods that reference `this`. + * + * **Example** (Wrapping a primitive schema) + * + * ```ts + * import { Schema } from "effect" + * + * class MyString extends Schema.asClass(Schema.String) { + * static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) + * } + * + * console.log(MyString.decodeUnknownSync("a")) + * // "a" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function asClass(schema: S): S & { new(_: never): {} } { + // oxlint-disable-next-line @typescript-eslint/no-extraneous-class + class Class {} + return Object.setPrototypeOf(Class, schema) +} + +/** + * Checks whether a value is a `Schema`. + * + * @category guards + * @since 3.10.0 + */ +export function isSchema(u: unknown): u is Top { + return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId +} + +/** + * Type-level representation returned by {@link optionalKey}. + * + * @category models + * @since 4.0.0 + */ +export interface optionalKey extends + BottomLazy< + S["ast"], + optionalKey, + S["~type.parameters"], + S["~type.mutability"], + "optional", + S["~type.constructor.default"], + S["~encoded.mutability"], + "optional" + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +interface optionalKeyLambda extends Lambda { + (self: S): optionalKey + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optionalKey : never +} + +/** + * Creates an exact optional key schema for struct fields. Unlike `optional`, + * this creates exact optional properties (not `| undefined`) that can be + * completely omitted from the object. + * + * **Example** (Creating a struct with optional key) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ + * name: Schema.String, + * age: Schema.optionalKey(Schema.Number) + * }) + * + * // Type: { readonly name: string; readonly age?: number } + * type Person = typeof schema["Type"] + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const optionalKey = Struct_.lambda((schema) => + make(SchemaAST.optionalKey(schema.ast), { schema }) +) + +interface requiredKeyLambda extends Lambda { + (self: optionalKey): S + readonly "~lambda.out": this["~lambda.in"] extends optionalKey ? this["~lambda.in"]["schema"] + : "Error: schema not eligible for requiredKey" +} + +/** + * Reverses `optionalKey` and returns the inner required schema. + * + * **When to use** + * + * Use to remove optional-key wrapping from a schema field that was previously + * wrapped with {@link optionalKey}. + * + * @category combinators + * @since 4.0.0 + */ +export const requiredKey = Struct_.lambda((self) => self.schema) + +/** + * Type-level representation returned by {@link optional}. + * + * @category models + * @since 3.10.0 + */ +export interface optional extends optionalKey> { + readonly "Rebuild": optional +} + +interface optionalLambda extends Lambda { + (self: S): optional + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optional : never +} + +/** + * Marks a struct field as optional, allowing the key to be absent or + * `undefined`. + * + * **Details** + * + * The resulting property may be absent or explicitly set to `undefined`. + * Equivalent to `optionalKey(UndefinedOr(S))`. + * + * Use {@link optionalKey} instead if you want exact optional semantics (absent + * only, not `undefined`). + * + * **Example** (Defining an optional field accepting undefined) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ + * name: Schema.String, + * age: Schema.optional(Schema.Number) + * }) + * + * // { readonly name: string; readonly age?: number | undefined } + * type Person = typeof schema.Type + * ``` + * + * @category combinators + * @since 3.10.0 + */ +export const optional = Struct_.lambda((self) => optionalKey(UndefinedOr(self))) + +interface requiredLambda extends Lambda { + (self: optional): S + readonly "~lambda.out": this["~lambda.in"] extends optional ? this["~lambda.in"]["schema"]["members"][0] + : "Error: schema not eligible for required" +} + +/** + * Reverses `optional` and returns the inner schema. + * + * **When to use** + * + * Use to remove optional wrapping from a schema field that was previously + * wrapped with {@link optional}. + * + * **Details** + * + * This also unwraps the `UndefinedOr` member added by `optional`. + * + * @category combinators + * @since 3.10.0 + */ +export const required = Struct_.lambda((self) => self.schema.members[0]) + +/** + * Type-level representation returned by {@link mutableKey}. + * + * @category models + * @since 4.0.0 + */ +export interface mutableKey extends + BottomLazy< + S["ast"], + mutableKey, + S["~type.parameters"], + "mutable", + S["~type.optionality"], + S["~type.constructor.default"], + "mutable", + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +interface mutableKeyLambda extends Lambda { + (self: S): mutableKey + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? mutableKey : never +} + +/** + * Makes a struct field mutable (removes the `readonly` modifier on the property). + * Use {@link readonlyKey} to reverse. + * + * @category combinators + * @since 4.0.0 + */ +export const mutableKey = Struct_.lambda((schema) => + make(SchemaAST.mutableKey(schema.ast), { schema }) +) + +interface readonlyKeyLambda extends Lambda { + (self: mutableKey): S + readonly "~lambda.out": this["~lambda.in"] extends mutableKey ? this["~lambda.in"]["schema"] + : "Error: schema not eligible for readonlyKey" +} + +/** + * Reverses `mutableKey` and returns the inner readonly schema. + * + * **When to use** + * + * Use to remove mutable-key wrapping from a schema field that was previously + * wrapped with {@link mutableKey}. + * + * @category combinators + * @since 4.0.0 + */ +export const readonlyKey = Struct_.lambda((self) => self.schema) + +/** + * Type-level representation returned by {@link toType}. + * + * @category transforming + * @since 4.0.0 + */ +export interface toType extends + BottomLazy< + S["ast"], + toType, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Type"] + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +interface toTypeLambda extends Lambda { + (self: S): toType + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toType : never +} + +/** + * Extracts the type-side schema: sets `Encoded` to equal the decoded `Type`, + * discarding the encoding transformation path. + * + * @category transforming + * @since 4.0.0 + */ +export const toType = Struct_.lambda((schema) => make(SchemaAST.toType(schema.ast), { schema })) + +/** + * Type-level representation returned by {@link toEncoded}. + * + * @category transforming + * @since 4.0.0 + */ +export interface toEncoded extends + BottomLazy< + SchemaAST.AST, + toEncoded, + ReadonlyArray, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Encoded"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": S["Encoded"] + readonly "~type.make": S["Encoded"] + readonly "Iso": S["Encoded"] + readonly schema: S +} + +interface toEncodedLambda extends Lambda { + (self: S): toEncoded + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toEncoded : never +} + +/** + * Extracts the encoded-side schema: sets `Type` to equal the `Encoded`, + * discarding the decoding transformation path. + * + * @category transforming + * @since 4.0.0 + */ +export const toEncoded = Struct_.lambda((schema) => make(SchemaAST.toEncoded(schema.ast), { schema })) + +const FlipTypeId = "~effect/Schema/flip" + +/** + * Type-level representation returned by {@link flip}. + * + * @category transforming + * @since 4.0.0 + */ +export interface flip extends + BottomLazy< + SchemaAST.AST, + flip, + ReadonlyArray, + S["~encoded.mutability"], + S["~encoded.optionality"], + ConstructorDefault, + S["~type.mutability"], + S["~type.optionality"] + > +{ + readonly "Type": S["Encoded"] + readonly "Encoded": S["Type"] + readonly "DecodingServices": S["EncodingServices"] + readonly "EncodingServices": S["DecodingServices"] + readonly "~type.make.in": S["Encoded"] + readonly "~type.make": S["Encoded"] + readonly "Iso": S["Encoded"] + readonly [FlipTypeId]: typeof FlipTypeId + readonly schema: S +} + +function isFlip$(schema: Top): schema is flip { + return Predicate.hasProperty(schema, FlipTypeId) && schema[FlipTypeId] === FlipTypeId +} + +/** + * Swaps the decoded and encoded sides of a schema. + * + * **When to use** + * + * Use to invert a schema transformation direction. + * + * **Details** + * + * Calling `flip` twice returns the original schema. + * + * **Example** (Flipping a number-from-string schema) + * + * ```ts + * import { Schema } from "effect" + * + * // NumberFromString: decodes string → number + * const flipped = Schema.flip(Schema.NumberFromString) + * // flipped: decodes number → string + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export function flip(schema: S): S extends flip ? F["Rebuild"] : flip +export function flip(schema: S): flip { + if (isFlip$(schema)) { + return schema.schema.rebuild(SchemaAST.flip(schema.ast)) + } + return make(SchemaAST.flip(schema.ast), { [FlipTypeId]: FlipTypeId, schema }) +} + +/** + * Type-level representation returned by {@link Literal}. + * + * @category models + * @since 3.10.0 + */ +export interface Literal + extends Bottom> +{ + readonly literal: L + transform(to: L2): decodeTo, Literal> +} + +/** + * Creates a schema for a single literal value (string, number, bigint, boolean, or null). + * + * **Example** (Defining a string literal) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Literal("hello") + * // Type: Schema.Literal<"hello"> + * ``` + * + * @see {@link Literals} for a schema that represents a union of literals. + * @see {@link tag} for a schema that represents a literal value that can be + * used as a discriminator field in tagged unions and has a constructor default. + * @category constructors + * @since 3.10.0 + */ +export function Literal(literal: L): Literal { + const out = make>(new SchemaAST.Literal(literal), { + literal, + transform(to: L2): decodeTo, Literal> { + return out.pipe(decodeTo(Literal(to), { + decode: SchemaGetter.transform(() => to), + encode: SchemaGetter.transform(() => literal) + })) + } + }) + return out +} + +/** + * Namespace for {@link TemplateLiteral} helper types. + * + * @since 3.10.0 + */ +export declare namespace TemplateLiteral { + /** + * Constraint for schema parts that can appear inside a `TemplateLiteral`. + * + * **Details** + * + * The schema's encoded value must be a `string`, `number`, or `bigint` so it can + * be converted into a template literal string segment. + * + * @category utility types + * @since 4.0.0 + */ + export interface SchemaPart extends Constraint { + readonly Encoded: string | number | bigint + } + + /** + * Literal value that can be used directly as a part of a `TemplateLiteral`. + * + * @category utility types + * @since 4.0.0 + */ + export type LiteralPart = string | number | bigint + + /** + * A single part of a `TemplateLiteral`, either an interpolated schema part or a + * literal `string`, `number`, or `bigint`. + * + * @category utility types + * @since 4.0.0 + */ + export type Part = SchemaPart | LiteralPart + + /** + * Ordered list of parts used to construct a `TemplateLiteral` schema. + * + * @category utility types + * @since 4.0.0 + */ + export type Parts = ReadonlyArray + + type AppendType< + Template extends string, + Next + > = Next extends LiteralPart ? `${Template}${Next}` + : Next extends { readonly Encoded: infer E extends LiteralPart } ? `${Template}${E}` + : never + + /** + * Computes the encoded string literal type produced by concatenating the encoded + * forms of all template literal parts. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = Parts extends readonly [...infer Init, infer Last] ? AppendType, Last> + : `` +} + +/** + * Type-level representation returned by {@link TemplateLiteral}. + * + * @category models + * @since 3.10.0 + */ +export interface TemplateLiteral extends + Bottom< + TemplateLiteral.Encoded, + TemplateLiteral.Encoded, + never, + never, + SchemaAST.TemplateLiteral, + TemplateLiteral + > +{ + readonly parts: Parts +} + +function templateLiteralFromParts(parts: Parts) { + return new SchemaAST.TemplateLiteral( + parts.map((part) => isSchema(part) ? part.ast : new SchemaAST.Literal(part as TemplateLiteral.LiteralPart)) + ) +} + +/** + * Creates a schema that validates strings by matching ordered template literal + * parts. + * + * **When to use** + * + * Use when the decoded value should remain the matched string and you do not + * need the individual template parts parsed into a tuple. + * + * **Details** + * + * Each part can be a literal `string`, `number`, or `bigint`, or a schema whose + * encoded type is `string`, `number`, or `bigint`. Checks on string, number, + * and bigint schema parts are applied while matching each segment. + * + * **Example** (Defining a URL path pattern) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.TemplateLiteral(["/user/", Schema.Number]) + * // matches strings like "/user/123", "/user/42", etc. + * ``` + * + * @see {@link TemplateLiteralParser} for a schema that also parses matched parts into a tuple. + * @category constructors + * @since 3.10.0 + */ +export function TemplateLiteral(parts: Parts): TemplateLiteral { + return make(templateLiteralFromParts(parts), { parts }) +} + +/** + * Namespace for {@link TemplateLiteralParser} helper types. + * + * @since 3.10.0 + */ +export declare namespace TemplateLiteralParser { + /** + * Computes the decoded tuple type produced by `TemplateLiteralParser`. + * + * **Details** + * + * Literal parts contribute their literal value to the tuple. Schema parts + * contribute their decoded `Type`. + * + * @category utility types + * @since 3.10.0 + */ + export type Type = Parts extends readonly [infer Head, ...infer Tail] ? readonly [ + Head extends TemplateLiteral.LiteralPart ? Head : + Head extends ConstraintDecoder ? T + : never, + ...Type + ] + : [] +} + +/** + * Type-level representation returned by {@link TemplateLiteralParser}. + * + * @category models + * @since 3.10.0 + */ +export interface TemplateLiteralParser extends + BottomLazy< + SchemaAST.Arrays, + TemplateLiteralParser + > +{ + readonly "Type": TemplateLiteralParser.Type + readonly "Encoded": TemplateLiteral.Encoded + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": TemplateLiteralParser.Type + readonly "~type.make": TemplateLiteralParser.Type + readonly "Iso": TemplateLiteralParser.Type + readonly parts: Parts +} + +/** + * Schema for parsing matched template literal strings into typed tuple parts. + * + * **When to use** + * + * Use to validate a template literal string and decode the matched parts into + * typed values. + * + * **Details** + * + * Unlike {@link TemplateLiteral}, this schema decodes the matched string into a + * readonly tuple with one element per schema part. Checks on string, number, + * and bigint schema parts are applied while matching each segment. + * + * **Example** (Parsing path parameters) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.TemplateLiteralParser(["/user/", Schema.NumberFromString]) + * // decodes "/user/42" => readonly ["/user/", 42] + * ``` + * + * @see {@link TemplateLiteral} for a validation-only version that keeps the string encoded. + * @category constructors + * @since 3.10.0 + */ +export function TemplateLiteralParser( + parts: Parts +): TemplateLiteralParser { + return make(templateLiteralFromParts(parts).asTemplateLiteralParser(), { parts }) +} + +/** + * Type-level representation returned by {@link Enum}. + * + * @category models + * @since 4.0.0 + */ +export interface Enum + extends Bottom> +{ + readonly enums: A +} + +/** + * Creates a schema from a TypeScript enum object. Validates that the input is one of the enum's values. + * + * **Example** (Defining a direction enum) + * + * ```ts + * import { Schema } from "effect" + * + * enum Direction { + * Up = "Up", + * Down = "Down" + * } + * + * const schema = Schema.Enum(Direction) + * // accepts "Up" or "Down" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function Enum(enums: A): Enum { + return make( + new SchemaAST.Enum( + Object.keys(enums).filter( + (key) => typeof enums[enums[key]] !== "number" + ).map((key) => [key, enums[key]]) + ), + { enums } + ) +} + +/** + * Type-level representation of {@link Never}. + * + * @category models + * @since 3.10.0 + */ +export interface Never extends Bottom {} + +/** + * Schema for the `never` type. Always fails validation — no value satisfies it. + * + * @category schemas + * @since 3.10.0 + */ +export const Never: Never = make(SchemaAST.never) + +/** + * Type-level representation of {@link Any}. + * + * @category models + * @since 3.10.0 + */ +export interface Any extends Bottom {} + +/** + * Schema for the `any` type. Accepts any value without validation. + * + * @see {@link Unknown} for a safer alternative that uses `unknown`. + * @category schemas + * @since 3.10.0 + */ +export const Any: Any = make(SchemaAST.any) + +/** + * Type-level representation of {@link Unknown}. + * + * @category models + * @since 3.10.0 + */ +export interface Unknown extends Bottom {} + +/** + * Schema for the `unknown` type. Accepts any value without validation. + * + * **When to use** + * + * Use as a top schema when you need to accept any input while preserving + * TypeScript's `unknown` safety at use sites. + * + * @see {@link Any} for the `any` variant. + * @category schemas + * @since 3.10.0 + */ +export const Unknown: Unknown = make(SchemaAST.unknown) + +/** + * Type-level representation of {@link Null}. + * + * @category models + * @since 3.10.0 + */ +export interface Null extends Bottom {} + +/** + * Schema for the `null` literal. Validates that the input is strictly `null`. + * + * @see {@link NullOr} for a union with another schema. + * @category schemas + * @since 3.10.0 + */ +export const Null: Null = make(SchemaAST.null) + +/** + * Type-level representation of {@link Undefined}. + * + * @category models + * @since 3.10.0 + */ +export interface Undefined extends Bottom {} + +/** + * Schema for the `undefined` literal. Validates that the input is strictly `undefined`. + * + * @see {@link UndefinedOr} for a union with another schema. + * @category schemas + * @since 3.10.0 + */ +export const Undefined: Undefined = make(SchemaAST.undefined) + +/** + * Type-level representation of {@link String}. + * + * @category models + * @since 4.0.0 + */ +export interface String extends Bottom {} + +/** + * Schema for `string` values. Validates that the input is `typeof` `"string"`. + * + * @category schemas + * @since 4.0.0 + */ +export const String: String = make(SchemaAST.string) + +/** + * Type-level representation of {@link Number}. + * + * @category models + * @since 4.0.0 + */ +export interface Number extends Bottom {} + +/** + * Schema for `number` values, including `NaN`, `Infinity`, and `-Infinity`. + * + * **Details** + * + * Default JSON serializer: + * + * - Finite numbers are serialized as numbers. + * - Non-finite values are serialized as strings (`"NaN"`, `"Infinity"`, `"-Infinity"`). + * + * @see {@link Finite} for a schema that excludes non-finite values. + * @category schemas + * @since 4.0.0 + */ +export const Number: Number = make(SchemaAST.number) + +/** + * Type-level representation of {@link Boolean}. + * + * @category models + * @since 4.0.0 + */ +export interface Boolean extends Bottom {} + +/** + * Schema for `boolean` values. Validates that the input is `typeof` `"boolean"`. + * + * **When to use** + * + * Use to validate values that are already JavaScript booleans. + * + * @see {@link BooleanFromBit} for a schema that decodes bit literals `0` or `1` into a boolean + * + * @category boolean + * @since 4.0.0 + */ +export const Boolean: Boolean = make(SchemaAST.boolean) + +/** + * Type-level representation of {@link Symbol}. + * + * @category models + * @since 4.0.0 + */ +export interface Symbol extends Bottom {} + +/** + * Schema for `symbol` values. Validates that the input is `typeof` `"symbol"`. + * + * @see {@link UniqueSymbol} for a schema that matches a specific symbol. + * @category schemas + * @since 4.0.0 + */ +export const Symbol: Symbol = make(SchemaAST.symbol) + +/** + * Type-level representation of {@link BigInt}. + * + * @category models + * @since 4.0.0 + */ +export interface BigInt extends Bottom {} + +/** + * Schema for `bigint` values. Validates that the input is `typeof` `"bigint"`. + * + * **When to use** + * + * Use when the input is already a bigint and the schema should validate and + * preserve bigint values without parsing from another representation. + * + * @see {@link BigIntFromString} for parsing string input into a bigint + * + * @category schemas + * @since 4.0.0 + */ +export const BigInt: BigInt = make(SchemaAST.bigInt) + +/** + * Type-level representation of {@link Void}. + * + * @category models + * @since 3.10.0 + */ +export interface Void extends Bottom {} + +/** + * Schema for a TypeScript `void` return value. + * + * **When to use** + * + * Use when you need to model the return value of a function, RPC, or endpoint + * whose result is intentionally ignored. + * + * **Details** + * + * Runtime parsing accepts any present value and discards it, producing + * `undefined`. The public decoded and encoded TypeScript representation remains + * `void`, so typed construction, decoding, and encoding APIs are still modeled + * as `void`. + * + * @see {@link Undefined} for a schema that matches only the exact `undefined` value. + * @category schemas + * @since 3.10.0 + */ +export const Void: Void = make(SchemaAST.void) + +/** + * Type-level representation of {@link ObjectKeyword}. + * + * @category models + * @since 4.0.0 + */ +export interface ObjectKeyword extends Bottom {} + +/** + * Schema for the `object` type. Validates that the input is a non-null object or function + * (i.e. `typeof value === "object" && value !== null || typeof value === "function"`). + * + * @category schemas + * @since 4.0.0 + */ +export const ObjectKeyword: ObjectKeyword = make(SchemaAST.objectKeyword) + +/** + * Type-level representation returned by {@link UniqueSymbol}. + * + * @category models + * @since 4.0.0 + */ +export interface UniqueSymbol + extends Bottom> +{} + +/** + * Creates a schema for a specific symbol. Only that exact symbol satisfies the schema. + * + * **Example** (Defining a specific symbol) + * + * ```ts + * import { Schema } from "effect" + * + * const mySymbol = Symbol.for("mySymbol") + * const schema = Schema.UniqueSymbol(mySymbol) + * ``` + * + * @see {@link Symbol} for a schema that accepts any symbol. + * @category constructors + * @since 4.0.0 + */ +export function UniqueSymbol(symbol: sym): UniqueSymbol { + return make(new SchemaAST.UniqueSymbol(symbol)) +} + +/** + * Namespace for struct field type utilities. + * + * **Details** + * + * These types compute the decoded `Type`, encoded `Encoded`, and constructor + * input `MakeIn` of a {@link Struct} from its field map, handling optional, + * mutable, and other field modifiers automatically. + * + * - `Struct.Fields` — constraint for the field map object + * - `Struct.Type` — decoded type of the struct + * - `Struct.Encoded` — encoded type of the struct + * - `Struct.MakeIn` — constructor input (optional/defaulted fields may be omitted) + * - `Struct.DecodingServices` / `Struct.EncodingServices` — required services + * + * @since 3.10.0 + */ +export declare namespace Struct { + /** + * Constraint for a struct field map: an object whose values are schemas. + * + * @category utility types + * @since 3.10.0 + */ + export type Fields = { readonly [x: PropertyKey]: Constraint } + + type TypeOptionalKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~type.optionality": "optional" } ? K + : never + }[keyof Fields] + + type TypeMutableKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~type.mutability": "mutable" } ? K + : never + }[keyof Fields] + + type SetOptional = Omit & Partial> + + type Mutable = { -readonly [K in keyof A]: A[K] } + + type SetMutable = Omit & Mutable> + + type Side = "Type" | "Iso" | "Encoded" + + type EncodedOptionalKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~encoded.optionality": "optional" } ? K + : never + }[keyof Fields] + + type EncodedMutableKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~encoded.mutability": "mutable" } ? K + : never + }[keyof Fields] + + type SideOptionalKeys = S extends "Encoded" ? EncodedOptionalKeys + : TypeOptionalKeys + + type SideMutableKeys = S extends "Encoded" ? EncodedMutableKeys + : TypeMutableKeys + + type ReadonlySide = { readonly [K in keyof F]: F[K][S] } + + type View< + F extends Fields, + S extends Side, + O extends keyof F = SideOptionalKeys, + M extends keyof F = SideMutableKeys + > = [O | M] extends [never] ? ReadonlySide + : [M] extends [never] ? Simplify, O>> + : [O] extends [never] ? Simplify, M>> + : Simplify< + SetMutable< + SetOptional, O>, + Extract, O>, M> + > + > + + /** + * Computes the decoded object type for a struct field map. + * + * **Details** + * + * Field schemas contribute their decoded `Type`. `optionalKey` and `optional` + * produce optional properties, while `mutableKey` produces writable properties. + * + * @category utility types + * @since 3.10.0 + */ + export type Type = View + + /** + * Computes the iso object type for a struct field map from each field schema's + * `Iso` type. + * + * **Details** + * + * The resulting property optionality and mutability follow the same field + * modifiers used by `Struct.Type`. + * + * @category utility types + * @since 4.0.0 + */ + export type Iso = View + + /** + * Computes the encoded object type for a struct field map. + * + * **Details** + * + * Field schemas contribute their `Encoded` type. Encoded-side optionality and + * mutability modifiers determine whether properties are optional or writable in + * the encoded shape. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = View + + /** + * Union of all decoding service requirements needed by the schemas in a struct + * field map. + * + * @category utility types + * @since 4.0.0 + */ + export type DecodingServices = { readonly [K in keyof F]: F[K]["DecodingServices"] }[keyof F] + + /** + * Union of all encoding service requirements needed by the schemas in a struct + * field map. + * + * @category utility types + * @since 4.0.0 + */ + export type EncodingServices = { readonly [K in keyof F]: F[K]["EncodingServices"] }[keyof F] + + type TypeConstructorDefaultedKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~type.constructor.default": "with-default" } ? K + : never + }[keyof Fields] + + type ReadonlyMakeIn = { readonly [K in keyof F]: F[K]["~type.make"] } + + type MakeInView< + F extends Fields, + O extends keyof F = TypeOptionalKeys | TypeConstructorDefaultedKeys + > = [O] extends [never] ? ReadonlyMakeIn : Simplify, O>> + + /** + * Computes the input object type accepted when constructing a struct value. + * + * **Details** + * + * Required fields use each field schema's `~type.make` input. Fields marked + * optional or with a constructor default may be omitted. + * + * @category utility types + * @since 4.0.0 + */ + export type MakeIn = MakeInView +} + +/** + * Type-level representation returned by {@link Struct}. + * + * @category models + * @since 3.10.0 + */ +export interface Struct extends BottomLazy> { + readonly "Type": Struct.Type + readonly "Encoded": Struct.Encoded + readonly "DecodingServices": Struct.DecodingServices + readonly "EncodingServices": Struct.EncodingServices + readonly "~type.make.in": Struct.MakeIn + readonly "~type.make": Struct.MakeIn + readonly "Iso": Struct.Iso + /** + * The field definitions of this struct. Spread them into a new struct to + * reuse fields across schemas. + * + * **Example** (Reusing fields across structs) + * + * ```ts + * import { Schema } from "effect" + * + * const Timestamped = Schema.Struct({ + * createdAt: Schema.Date, + * updatedAt: Schema.Date + * }) + * + * const User = Schema.Struct({ + * ...Timestamped.fields, + * name: Schema.String, + * email: Schema.String + * }) + * ``` + */ + readonly fields: Fields + /** + * Returns a new struct with the fields modified by the provided function. + * + * **Details** + * + * Options: + * + * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints + * that were attached to the original union. Defaults to `false`. + * + * **Warning**: This is an unsafe operation. Since `mapFields` + * transformations change the schema type, the original refinement functions + * may no longer be valid or safe to apply to the transformed schema. Only + * use this option if you have verified that your refinements remain correct + * after the transformation. + */ + mapFields( + f: (fields: Fields) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Struct>> +} + +function makeStruct(ast: SchemaAST.Objects, fields: Fields): Struct { + return make(ast, { + fields, + mapFields( + this: Struct, + f: (fields: Fields) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Struct { + const fields = f(this.fields) + return makeStruct(SchemaAST.struct(fields, options?.unsafePreserveChecks ? this.ast.checks : undefined), fields) + } + }) +} + +/** + * Defines a struct schema from a map of field schemas. + * + * **Details** + * + * Each field value is a schema. Use {@link optionalKey} or {@link optional} to + * mark fields as optional, and {@link mutableKey} to mark them as mutable. + * + * The resulting schema's `Type` is a readonly object type with the fields' + * decoded types. The `Encoded` form mirrors the field schemas' encoded types. + * + * **Example** (Defining a basic struct) + * + * ```ts + * import { Schema } from "effect" + * + * const Person = Schema.Struct({ + * name: Schema.String, + * age: Schema.Number, + * email: Schema.optionalKey(Schema.String) + * }) + * + * // { readonly name: string; readonly age: number; readonly email?: string } + * type Person = typeof Person.Type + * + * const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) + * console.log(alice) + * // { name: 'Alice', age: 30 } + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function Struct(fields: Fields): Struct { + return makeStruct(SchemaAST.struct(fields, undefined), fields) +} + +interface fieldsAssign extends Lambda { + ( + struct: Struct + ): Struct>> + readonly "~lambda.out": this["~lambda.in"] extends Struct + ? Struct>> + : "Error: schema not eligible for fieldsAssign" +} + +/** + * Adds fields to a struct schema through a struct-mapping lambda. + * + * **When to use** + * + * Use to add the same fields to an existing struct or every struct member of a + * union. + * + * **Details** + * + * This is a shortcut for `MyStruct.mapFields(Struct.assign(fields))`. + * + * **Example** (Adding fields to a union of structs) + * + * ```ts + * import { Schema, Tuple } from "effect" + * + * // Add a new field to all members of a union of structs + * const schema = Schema.Union([ + * Schema.Struct({ a: Schema.String }), + * Schema.Struct({ b: Schema.Number }) + * ]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number }))) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export function fieldsAssign(fields: NewFields) { + return Struct_.lambda>((struct) => struct.mapFields(Struct_.assign(fields))) +} + +/** + * Type-level representation returned by {@link encodeKeys}. + * + * @category transforming + * @since 4.0.0 + */ +export interface encodeKeys< + S extends Constraint & { readonly fields: Struct.Fields }, + M extends { readonly [K in keyof S["fields"]]?: PropertyKey } +> extends + decodeTo< + S, + Struct< + { + [ + K in keyof S["fields"] as K extends keyof M ? M[K] extends PropertyKey ? M[K] : K : K + ]: toEncoded + } + > + > +{} + +const canonicalPropertyKey = (key: PropertyKey): string | symbol => + typeof key === "symbol" ? key : globalThis.String(key) + +/** + * Renames struct keys in the encoded form without changing the decoded type. + * + * **Details** + * + * Takes a partial mapping `{ decodedKey: encodedKey }` and produces a + * transformation schema that decodes from the renamed keys and encodes back to + * the renamed keys. Keys not present in the mapping are left unchanged. + * If two existing fields would produce the same encoded key, construction + * fails. + * + * **Example** (Renaming `name` to `full_name` in the encoded form) + * + * ```ts + * import { Schema } from "effect" + * + * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) + * const Encoded = Person.pipe(Schema.encodeKeys({ name: "full_name" })) + * + * // Decodes { full_name: "Alice", age: 30 } → { name: "Alice", age: 30 } + * const alice = Schema.decodeUnknownSync(Encoded)({ full_name: "Alice", age: 30 }) + * console.log(alice) + * // { name: 'Alice', age: 30 } + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export function encodeKeys< + S extends Constraint & { readonly fields: Struct.Fields }, + const M extends { readonly [K in keyof S["fields"]]?: PropertyKey } +>(mapping: M) { + return function(self: S): encodeKeys { + const fields: any = {} + const appliedMapping: any = {} + const reverseMapping: any = {} + const seenEncodedKeys = new Set() + for (const k of Reflect.ownKeys(self.fields)) { + const encoded = toEncoded(self.fields[k]) + const hasMapping = Object.hasOwn(mapping, k) + const encodedKey = hasMapping ? (mapping as any)[k] as PropertyKey : k + const canonical = canonicalPropertyKey(encodedKey) + if (seenEncodedKeys.has(canonical)) { + throw new globalThis.Error(`Duplicate encoded keys: ${formatPropertyKey(encodedKey)}`) + } + seenEncodedKeys.add(canonical) + fields[encodedKey] = encoded + if (hasMapping) { + appliedMapping[k] = encodedKey + reverseMapping[encodedKey] = k + } + } + return Struct(fields).pipe(decodeTo( + self, + SchemaTransformation.transform({ + decode: Struct_.renameKeys(reverseMapping), + encode: Struct_.renameKeys(appliedMapping) + }) + )) as any + } +} + +/** + * Adds derived fields to a struct schema during decoding. + * + * **Details** + * + * Each new field is derived from the decoded struct value via a function that + * returns `Option`. On encoding the derived fields are stripped. This allows + * computed or enriched fields to live in the decoded type without appearing in + * the encoded form. + * + * **Example** (Adding a computed `fullName` field) + * + * ```ts + * import { Option, Schema } from "effect" + * + * const Person = Schema.Struct({ first: Schema.String, last: Schema.String }) + * const Extended = Person.pipe( + * Schema.extendTo( + * { fullName: Schema.String }, + * { fullName: (p) => Option.some(`${p.first} ${p.last}`) } + * ) + * ) + * + * const alice = Schema.decodeUnknownSync(Extended)({ first: "Alice", last: "Smith" }) + * console.log(alice.fullName) + * // Alice Smith + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export function extendTo, const Fields extends Struct.Fields>( + /** The new fields to add */ + fields: Fields, + /** A function per field to derive its value from the original input */ + derive: { readonly [K in keyof Fields]: (s: S["Type"]) => Option_.Option } +) { + return ( + self: S + ): decodeTo } & Fields>>, S> => { + const f = Record_.map(self.fields, toType) + const to = Struct({ ...f, ...fields }) + return self.pipe(decodeTo( + to, + SchemaTransformation.transform({ + decode: (input) => { + const out: any = { ...input } + for (const k in fields) { + const f = derive[k] + const o = f(input) + if (Option_.isSome(o)) { + out[k] = o.value + } + } + return out + }, + encode: (input) => { + const out = { ...input } + for (const k in fields) { + delete out[k] + } + return out + } + }) + )) as any + } +} + +/** + * Namespace for `Record` type utilities. + * + * **Details** + * + * - `Record.Key` — constraint for the key schema (must encode to `PropertyKey`) + * - `Record.Type` — decoded type of the record + * - `Record.Encoded` — encoded type of the record + * + * @since 3.10.0 + */ +export declare namespace Record { + /** + * Constraint for schemas that can be used as record keys. + * + * **Details** + * + * The key schema must decode and encode property keys (`string`, `number`, or + * `symbol`) so it can describe object property names. + * + * @category utility types + * @since 4.0.0 + */ + export interface Key extends Codec { + readonly "~type.make": PropertyKey + readonly "Iso": PropertyKey + } + + /** + * Computes the decoded object type for a record schema from its key and value + * schemas. + * + * **Details** + * + * The key schema supplies the property keys and the value schema supplies each + * property's decoded `Type`. Optional and mutable value schemas affect the + * resulting property optionality and writability. + * + * @category utility types + * @since 3.10.0 + */ + export type Type = Value extends + { readonly "~type.optionality": "optional" } ? + Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Type"]]?: Value["Type"] } + : { readonly [P in Key["Type"]]?: Value["Type"] } + : Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Type"]]: Value["Type"] } + : { readonly [P in Key["Type"]]: Value["Type"] } + + /** + * Computes the iso object type for a record schema from the key schema's `Iso` + * keys and the value schema's `Iso` values. + * + * @category utility types + * @since 4.0.0 + */ + export type Iso = Value extends + { readonly "~type.optionality": "optional" } ? + Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Iso"]]?: Value["Iso"] } + : { readonly [P in Key["Iso"]]?: Value["Iso"] } + : Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Iso"]]: Value["Iso"] } + : { readonly [P in Key["Iso"]]: Value["Iso"] } + + /** + * Computes the encoded object type for a record schema from the key and value + * schemas' encoded types. + * + * **Details** + * + * Encoded-side optionality and mutability on the value schema determine whether + * the encoded record properties are optional or writable. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = Value extends + { readonly "~encoded.optionality": "optional" } ? + Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["Encoded"]]?: Value["Encoded"] } + : { readonly [P in Key["Encoded"]]?: Value["Encoded"] } + : Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["Encoded"]]: Value["Encoded"] } + : { readonly [P in Key["Encoded"]]: Value["Encoded"] } + + /** + * Union of the decoding service requirements of a record's key schema and value + * schema. + * + * @category utility types + * @since 4.0.0 + */ + export type DecodingServices = + | Key["DecodingServices"] + | Value["DecodingServices"] + + /** + * Union of the encoding service requirements of a record's key schema and value + * schema. + * + * @category utility types + * @since 4.0.0 + */ + export type EncodingServices = + | Key["EncodingServices"] + | Value["EncodingServices"] + + /** + * Computes the input object type accepted when constructing a record value. + * + * **Details** + * + * Keys use the key schema's `~type.make` type and values use the value schema's + * `~type.make` type. Value optionality and mutability determine whether + * properties are optional or writable. + * + * @category utility types + * @since 4.0.0 + */ + export type MakeIn = Value extends + { readonly "~encoded.optionality": "optional" } ? + Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["~type.make"]]?: Value["~type.make"] } + : { readonly [P in Key["~type.make"]]?: Value["~type.make"] } + : Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["~type.make"]]: Value["~type.make"] } + : { readonly [P in Key["~type.make"]]: Value["~type.make"] } +} + +/** + * Type-level representation returned by {@link Record}. + * + * @category models + * @since 4.0.0 + */ +export interface $Record extends + BottomLazy< + SchemaAST.Objects, + $Record + > +{ + readonly "Type": Record.Type + readonly "Encoded": Record.Encoded + readonly "DecodingServices": Record.DecodingServices + readonly "EncodingServices": Record.EncodingServices + readonly "~type.make.in": Simplify> + readonly "~type.make": Simplify> + readonly "Iso": Record.Iso + readonly key: Key + readonly value: Value +} + +/** + * Defines a record schema whose dynamic properties are selected by a key schema + * and decoded with a value schema. + * + * **Details** + * + * For dynamic keys, the key schema selects matching own properties and the + * value schema decodes or encodes only those selected properties. Checks on + * string, number, symbol, and template literal key schemas narrow which + * properties are selected. + * + * For transformed key schemas, property selection is based on encoded property + * names before the selected key is decoded. + * + * **Example** (Defining a string-keyed record of numbers) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Record(Schema.String, Schema.Number) + * + * // { readonly [x: string]: number } + * type R = typeof schema.Type + * + * const result = Schema.decodeUnknownSync(schema)({ a: 1, b: 2 }) + * console.log(result) + * // { a: 1, b: 2 } + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function Record( + key: Key, + value: Value, + options?: { + readonly keyValueCombiner: { + readonly decode?: Combiner.Combiner | undefined + readonly encode?: Combiner.Combiner | undefined + } + } +): $Record { + const keyValueCombiner = options?.keyValueCombiner?.decode || options?.keyValueCombiner?.encode + ? new SchemaAST.KeyValueCombiner(options.keyValueCombiner.decode, options.keyValueCombiner.encode) + : undefined + return make(SchemaAST.record(key.ast, value.ast, keyValueCombiner), { key, value }) +} + +/** + * Namespace for `StructWithRest` type utilities. + * + * **Details** + * + * - `StructWithRest.Type` — decoded type (struct type intersected with record types) + * - `StructWithRest.Encoded` — encoded type + * + * @since 4.0.0 + */ +export declare namespace StructWithRest { + /** + * Constraint for object-like schemas that can be used as the fixed portion of a + * `StructWithRest` schema. + * + * @category utility types + * @since 4.0.0 + */ + export type Objects = Constraint & { readonly ast: SchemaAST.Objects } + + /** + * Readonly list of record schemas that provide the additional index signatures + * for a `StructWithRest` schema. + * + * @category utility types + * @since 3.10.0 + */ + export type Records = ReadonlyArray<$Record> + + type MergeTuple> = T extends readonly [infer Head, ...infer Tail] ? + Head & MergeTuple + : {} + + type Intersect< + S extends Objects, + Records extends StructWithRest.Records, + Side extends "Type" | "Iso" | "Encoded" | "~type.make" + > = + & S[Side] + & MergeTuple<{ readonly [K in keyof Records]: Records[K][Side] }> + + /** + * Computes the decoded type for `StructWithRest` by intersecting the base object + * schema's decoded `Type` with the decoded types of all rest record schemas. + * + * @category utility types + * @since 3.10.0 + */ + export type Type = Intersect + + /** + * Computes the iso type for `StructWithRest` by intersecting the base object + * schema's `Iso` type with the `Iso` types of all rest record schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type Iso = Intersect + + /** + * Computes the encoded type for `StructWithRest` by intersecting the base object + * schema's encoded type with the encoded types of all rest record schemas. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = Intersect + + /** + * Computes the input type accepted when constructing a `StructWithRest` value by + * intersecting the base object's make input with the make inputs of all rest + * record schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type MakeIn = Intersect + + type Services< + S extends Objects, + Records extends StructWithRest.Records, + Side extends "DecodingServices" | "EncodingServices" + > = + | S[Side] + | { [K in keyof Records]: Records[K][Side] }[number] + + /** + * Union of the decoding service requirements of the base object schema and all + * rest record schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type DecodingServices = Services< + S, + Records, + "DecodingServices" + > + + /** + * Union of the encoding service requirements of the base object schema and all + * rest record schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type EncodingServices = Services< + S, + Records, + "EncodingServices" + > + + type IncompatibleKeys> = { + [K in OK]: Required>[K] extends B[K] ? never : K + }[OK] + + type IncompatibleSideKeys< + S extends Objects, + Records extends StructWithRest.Records, + Side extends "Type" | "Encoded" | "Iso" | "~type.make" + > = { + [I in keyof Records]: Records[I][Side] extends object ? IncompatibleKeys : never + }[number] + + type IncompatibleRecords = + | IncompatibleSideKeys + | IncompatibleSideKeys + | IncompatibleSideKeys + | IncompatibleSideKeys + + /** + * Checks whether fixed fields are compatible with the rest record schemas. + * + * **Details** + * + * Returns `true` when all fixed fields can also satisfy the matching rest + * index signatures. Returns a diagnostic object when TypeScript would make + * the resulting intersection too narrow for one or more fixed keys. + * + * **Example** (Checking record compatibility) + * + * ```ts + * import { Schema } from "effect" + * + * const user = Schema.Struct({ id: Schema.String }) + * const stringExtras = [Schema.Record(Schema.String, Schema.String)] as const + * + * type UserCheck = Schema.StructWithRest.ValidateRecords + * + * const userCheck: UserCheck = true + * void userCheck + * + * const counter = Schema.Struct({ count: Schema.NumberFromString }) + * + * type CounterCheck = Schema.StructWithRest.ValidateRecords + * // ^? { "incompatible index signatures": "count" } + * + * const counterCheck = null as unknown as CounterCheck + * void counterCheck + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type ValidateRecords< + S extends Objects, + Records extends StructWithRest.Records + > = [IncompatibleRecords] extends [never] ? true + : { + "incompatible index signatures": IncompatibleRecords + } +} + +/** + * Type-level representation returned by {@link StructWithRest}. + * + * @category models + * @since 4.0.0 + */ +export interface StructWithRest< + S extends StructWithRest.Objects, + Records extends StructWithRest.Records +> extends + BottomLazy< + SchemaAST.Objects, + StructWithRest + > +{ + readonly "Type": Simplify> + readonly "Encoded": Simplify> + readonly "DecodingServices": StructWithRest.DecodingServices + readonly "EncodingServices": StructWithRest.EncodingServices + readonly "~type.make.in": Simplify> + readonly "~type.make": Simplify> + readonly "Iso": Simplify> + readonly schema: S + readonly records: Records +} + +/** + * Extends a struct schema with one or more record (index-signature) schemas, + * producing a schema whose decoded type intersects the struct and all records. + * + * **Gotchas** + * + * TypeScript index signatures also apply to fixed keys. `StructWithRest` does + * not reject incompatible fixed fields at the call site; use + * `StructWithRest.ValidateRecords` when you want an explicit type-level + * compatibility check. + * + * **Example** (Defining structs with string-indexed extra keys) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.StructWithRest( + * Schema.Struct({ id: Schema.Number }), + * [Schema.Record(Schema.String, Schema.Number)] + * ) + * + * // { readonly id: number, readonly [x: string]: number } + * type T = typeof schema.Type + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function StructWithRest< + const S extends StructWithRest.Objects, + const Records extends StructWithRest.Records +>( + schema: S, + records: Records +): StructWithRest { + return make(SchemaAST.structWithRest(schema.ast, records.map(SchemaAST.getAST)), { schema, records }) +} + +/** + * Namespace for `Tuple` type utilities. + * + * **Details** + * + * - `Tuple.Elements` — constraint for the element schema array + * - `Tuple.Type` — decoded tuple type + * - `Tuple.Encoded` — encoded tuple type + * - `Tuple.MakeIn` — constructor input tuple + * + * @since 3.10.0 + */ +export declare namespace Tuple { + /** + * Constraint for the readonly array of element schemas used to define a + * fixed-length `Tuple` schema. + * + * @category utility types + * @since 3.10.0 + */ + export type Elements = ReadonlyArray + + type Type_< + Elements, + Out extends ReadonlyArray = readonly [] + > = Elements extends readonly [infer Head, ...infer Tail] ? + Head extends { readonly "Type": infer T } ? + Head extends { readonly "~type.optionality": "optional" } ? Type_ + : Type_ + : Out + : Out + + /** + * Computes the decoded tuple type for a tuple element schema array. + * + * **Details** + * + * Each element contributes its decoded `Type`; optional element schemas produce + * optional tuple positions. + * + * @category utility types + * @since 3.10.0 + */ + export type Type = Type_ + + type Iso_< + Elements, + Out extends ReadonlyArray = readonly [] + > = Elements extends readonly [infer Head, ...infer Tail] ? + Head extends { readonly "Iso": infer T } ? + Head extends { readonly "~type.optionality": "optional" } ? Iso_ + : Iso_ + : Out + : Out + + /** + * Computes the iso tuple type for a tuple element schema array from each + * element schema's `Iso` type. + * + * @category utility types + * @since 4.0.0 + */ + export type Iso = Iso_ + + type Encoded_< + Elements, + Out extends ReadonlyArray = readonly [] + > = Elements extends readonly [infer Head, ...infer Tail] ? + Head extends { readonly "Encoded": infer T } ? + Head extends { readonly "~encoded.optionality": "optional" } ? Encoded_ + : Encoded_ + : Out + : Out + + /** + * Computes the encoded tuple type for a tuple element schema array. + * + * **Details** + * + * Each element contributes its `Encoded` type; encoded-side optional element + * schemas produce optional tuple positions. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded = Encoded_ + + /** + * Union of all decoding service requirements needed by the tuple element + * schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type DecodingServices = E[number]["DecodingServices"] + + /** + * Union of all encoding service requirements needed by the tuple element + * schemas. + * + * @category utility types + * @since 4.0.0 + */ + export type EncodingServices = E[number]["EncodingServices"] + + type MakeIn_< + E, + Out extends ReadonlyArray = readonly [] + > = E extends readonly [infer Head, ...infer Tail] ? + Head extends { "~type.make": infer T } ? + Head extends + { readonly "~type.optionality": "optional" } | { readonly "~type.constructor.default": "with-default" } ? + MakeIn_ : + MakeIn_ + : Out : + Out + + /** + * Computes the input tuple type accepted when constructing a tuple value. + * + * **Details** + * + * Each element uses its `~type.make` input type. Optional elements and elements + * with constructor defaults produce optional tuple positions. + * + * @category utility types + * @since 4.0.0 + */ + export type MakeIn = MakeIn_ +} + +/** + * Type-level representation returned by {@link Tuple}. + * + * @category models + * @since 3.10.0 + */ +export interface Tuple extends + BottomLazy< + SchemaAST.Arrays, + Tuple + > +{ + readonly "Type": Tuple.Type + readonly "Encoded": Tuple.Encoded + readonly "DecodingServices": Tuple.DecodingServices + readonly "EncodingServices": Tuple.EncodingServices + readonly "~type.make.in": Tuple.MakeIn + readonly "~type.make": Tuple.MakeIn + readonly "Iso": Tuple.Iso + readonly elements: Elements + /** + * Returns a new tuple with the elements modified by the provided function. + * + * **Details** + * + * Options: + * + * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints + * that were attached to the original union. Defaults to `false`. + * + * **Warning**: This is an unsafe operation. Since `mapFields` + * transformations change the schema type, the original refinement functions + * may no longer be valid or safe to apply to the transformed schema. Only + * use this option if you have verified that your refinements remain correct + * after the transformation. + */ + mapElements( + f: (elements: Elements) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Tuple>> +} + +function makeTuple(ast: SchemaAST.Arrays, elements: Elements): Tuple { + return make(ast, { + elements, + mapElements( + this: Tuple, + f: (elements: Elements) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Tuple>> { + const elements = f(this.elements) + return makeTuple(SchemaAST.tuple(elements, options?.unsafePreserveChecks ? this.ast.checks : undefined), elements) + } + }) +} + +/** + * Defines a fixed-length tuple schema from an array of element schemas. + * + * **Example** (Defining a pair of string and number) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Tuple([Schema.String, Schema.Number]) + * + * const pair = Schema.decodeUnknownSync(schema)(["hello", 42]) + * console.log(pair) + * // [ 'hello', 42 ] + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function Tuple>(elements: Elements): Tuple { + return makeTuple(SchemaAST.tuple(elements), elements) +} + +/** + * Namespace for `TupleWithRest` type utilities. + * + * **Details** + * + * - `TupleWithRest.TupleType` — constraint for the leading tuple schema + * - `TupleWithRest.Rest` — the rest element schema(s) + * - `TupleWithRest.Type` — decoded type (fixed elements + rest) + * - `TupleWithRest.Encoded` — encoded type + * + * @since 4.0.0 + */ +export declare namespace TupleWithRest { + /** + * Constraint for tuple-like schemas that can be used as the fixed leading + * portion of a `TupleWithRest` schema. + * + * @category utility types + * @since 3.10.0 + */ + export type TupleType = Constraint & { + readonly Type: ReadonlyArray + readonly Encoded: ReadonlyArray + readonly ast: SchemaAST.Arrays + readonly "~type.make": ReadonlyArray + readonly "Iso": ReadonlyArray + } + + /** + * Non-empty list of schemas used for the rest portion of a `TupleWithRest`. + * + * **Details** + * + * The first schema describes the repeated rest element. Additional schemas, when + * present, describe trailing tuple elements after the repeated rest segment. + * + * @category utility types + * @since 3.10.0 + */ + export type Rest = readonly [Constraint, ...Array] + + /** + * Computes the decoded tuple type for a `TupleWithRest`. + * + * **Details** + * + * The output starts with the fixed tuple elements, continues with zero or more + * values decoded by the first rest schema, and includes any trailing rest schemas + * as fixed tuple positions. + * + * @category utility types + * @since 3.10.0 + */ + export type Type, Rest extends TupleWithRest.Rest> = Rest extends + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? Readonly<[ + ...T, + ...Array, + ...{ readonly [K in keyof Tail]: Tail[K]["Type"] } + ]> : + T + + /** + * Computes the iso tuple type for a `TupleWithRest`. + * + * **Details** + * + * The output starts with the fixed tuple's `Iso` elements, continues with zero + * or more values using the first rest schema's `Iso`, and includes any trailing + * rest schemas as fixed tuple positions. + * + * @category utility types + * @since 4.0.0 + */ + export type Iso, Rest extends TupleWithRest.Rest> = Rest extends + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? Readonly<[ + ...T, + ...Array, + ...{ readonly [K in keyof Tail]: Tail[K]["Iso"] } + ]> : + T + + /** + * Computes the encoded tuple type for `TupleWithRest`. + * + * **Details** + * + * The leading tuple's encoded elements are kept first. The encoded type of the + * first rest schema may repeat zero or more times, and the encoded types of any + * additional rest schemas become required trailing tuple elements. + * + * @category utility types + * @since 3.10.0 + */ + export type Encoded, Rest extends TupleWithRest.Rest> = Rest extends + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? readonly [ + ...E, + ...Array, + ...{ readonly [K in keyof Tail]: Tail[K]["Encoded"] } + ] : + E + + /** + * Computes the constructor input tuple type for `TupleWithRest`. + * + * **Details** + * + * The leading tuple's make input elements are kept first. The make input type of + * the first rest schema may repeat zero or more times, and the make input types + * of any additional rest schemas become required trailing tuple elements. + * + * @category utility types + * @since 4.0.0 + */ + export type MakeIn, Rest extends TupleWithRest.Rest> = Rest extends + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? readonly [ + ...M, + ...Array, + ...{ readonly [K in keyof Tail]: Tail[K]["~type.make"] } + ] : + M +} + +/** + * Type-level representation returned by {@link TupleWithRest}. + * + * @category models + * @since 4.0.0 + */ +export interface TupleWithRest< + S extends TupleWithRest.TupleType, + Rest extends TupleWithRest.Rest +> extends + BottomLazy< + SchemaAST.Arrays, + TupleWithRest + > +{ + readonly "Type": TupleWithRest.Type + readonly "Encoded": TupleWithRest.Encoded + readonly "DecodingServices": S["DecodingServices"] | Rest[number]["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] | Rest[number]["EncodingServices"] + readonly "~type.make.in": TupleWithRest.MakeIn + readonly "~type.make": TupleWithRest.MakeIn + readonly "Iso": TupleWithRest.Iso + readonly schema: S + readonly rest: Rest +} + +/** + * Extends a fixed-length tuple schema with a variadic rest segment. + * + * **Details** + * + * The resulting tuple starts with the fixed elements from `schema`. The first + * schema in `rest` is the repeatable element schema, and any additional schemas + * in `rest` are required trailing tuple elements after the variadic segment. For + * example, `[Schema.Boolean, Schema.String]` represents zero or more booleans + * followed by a final string. + * + * **Example** (Defining tuples with rest elements) + * + * ```ts + * import { Schema } from "effect" + * + * // [string, number, ...boolean[]] + * const schema = Schema.TupleWithRest( + * Schema.Tuple([Schema.String, Schema.Number]), + * [Schema.Boolean] + * ) + * + * const result = Schema.decodeUnknownSync(schema)(["hello", 1, true, false]) + * console.log(result) + * // [ 'hello', 1, true, false ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function TupleWithRest, const Rest extends TupleWithRest.Rest>( + schema: S, + rest: Rest +): TupleWithRest { + return make(SchemaAST.tupleWithRest(schema.ast, rest.map(SchemaAST.getAST)), { schema, rest }) +} + +/** + * Type-level representation returned by {@link Array}. + * + * @category models + * @since 4.0.0 + */ +export interface $Array extends + BottomLazy< + SchemaAST.Arrays, + $Array + > +{ + readonly "Type": ReadonlyArray + readonly "Encoded": ReadonlyArray + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": ReadonlyArray + readonly "~type.make": ReadonlyArray + readonly "Iso": ReadonlyArray + readonly value: S +} + +interface ArrayLambda extends Lambda { + (self: S): $Array + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? $Array : never +} + +/** + * @category constructors + * @since 4.0.0 + */ +const ArraySchema = Struct_.lambda((schema) => + make(new SchemaAST.Arrays(false, [], [schema.ast]), { value: schema }) +) + +export { + /** + * Defines a `ReadonlyArray` schema for a given element schema. + * + * **Example** (Defining an array of strings) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Array(Schema.String) + * + * const result = Schema.decodeUnknownSync(schema)(["a", "b", "c"]) + * console.log(result) + * // [ 'a', 'b', 'c' ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ + ArraySchema as Array +} + +/** + * Type-level representation returned by {@link NonEmptyArray}. + * + * @category models + * @since 3.10.0 + */ +export interface NonEmptyArray extends + BottomLazy< + SchemaAST.Arrays, + NonEmptyArray + > +{ + readonly "Type": readonly [S["Type"], ...Array] + readonly "Encoded": readonly [S["Encoded"], ...Array] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": readonly [S["~type.make"], ...Array] + readonly "~type.make": readonly [S["~type.make"], ...Array] + readonly "Iso": readonly [S["Iso"], ...Array] + readonly value: S +} + +interface NonEmptyArrayLambda extends Lambda { + (self: S): NonEmptyArray + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NonEmptyArray : never +} + +/** + * Defines a non-empty `ReadonlyArray` schema — at least one element required. + * Type is `readonly [T, ...T[]]`. + * + * **Example** (Defining a non-empty array of numbers) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.NonEmptyArray(Schema.Number) + * + * Schema.decodeUnknownSync(schema)([1, 2, 3]) // ok + * Schema.decodeUnknownSync(schema)([]) // throws + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export const NonEmptyArray = Struct_.lambda((schema) => + make(new SchemaAST.Arrays(false, [schema.ast], [schema.ast]), { value: schema }) +) + +/** + * Type-level representation returned by {@link ArrayEnsure}. + * + * @category constructors + * @since 3.10.0 + */ +export interface ArrayEnsure extends decodeTo<$Array>, Union]>> { + readonly "Rebuild": ArrayEnsure +} + +/** + * Creates a schema that accepts either a value decoded by `schema` or an array + * decoded by `Schema.Array(schema)`, then returns an array. + * + * **When to use** + * + * Use to accept input that may be provided either as one item or as an array, + * while normalizing decoded values to a readonly array. + * + * **Details** + * + * During encoding, one-element arrays are encoded as the single element. Empty + * arrays and arrays with two or more elements are encoded as arrays. + * + * **Gotchas** + * + * The single-value branch is tried before the array branch. If `schema` itself + * accepts arrays, an array input can be treated as one value and wrapped in a + * one-element array. + * + * @see {@link Array} for accepting only array input + * @see {@link NonEmptyArray} for requiring at least one decoded element + * + * @category constructors + * @since 3.10.0 + */ +export function ArrayEnsure(schema: S): ArrayEnsure { + return Union([schema, ArraySchema(schema)]).pipe(decodeTo( + ArraySchema(toType(schema)), + SchemaTransformation.transform({ + decode: Arr.ensure, + encode: (array) => array.length === 1 ? array[0] : array + }) + )) +} + +/** + * Type-level representation returned by {@link UniqueArray}. + * + * @category models + * @since 4.0.0 + */ +export interface UniqueArray extends $Array { + readonly "Rebuild": UniqueArray +} + +/** + * Returns a new array schema that ensures all elements are unique. + * + * **Details** + * + * The equivalence used to determine uniqueness is the one provided by + * `Schema.toEquivalence(item)`. + * + * @category constructors + * @since 4.0.0 + */ +export function UniqueArray(item: S): UniqueArray { + return ArraySchema(item).check(isUnique()) +} + +/** + * Type-level representation returned by {@link mutable}. + * + * @category transforming + * @since 3.10.0 + */ +export interface mutable extends + BottomLazy< + S["ast"], + mutable, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": Mutable + readonly "Encoded": Mutable + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + // "~type.make" and "~type.make.in" as they are because they are contravariant + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +interface mutableLambda extends Lambda { + (self: S): mutable + readonly "~lambda.out": this["~lambda.in"] extends Constraint & { readonly "ast": SchemaAST.Arrays } ? + mutable + : "Error: schema not eligible for mutable" +} + +/** + * Makes an array or tuple schema mutable, removing the `readonly` modifier. + * + * **Example** (Defining mutable arrays) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.mutable(Schema.Array(Schema.Number)) + * + * // number[] (mutable) + * type T = typeof schema.Type + * ``` + * + * @category transforming + * @since 3.10.0 + */ +export const mutable = Struct_.lambda((schema) => { + return make(new SchemaAST.Arrays(true, schema.ast.elements, schema.ast.rest), { schema }) +}) + +/** + * Type-level representation returned by {@link Union}. + * + * @category models + * @since 3.10.0 + */ +export interface Union> extends + BottomLazy< + SchemaAST.Union<{ [K in keyof Members]: Members[K]["ast"] }[number]>, + Union + > +{ + readonly "Type": { [K in keyof Members]: Members[K]["Type"] }[number] + readonly "Encoded": { [K in keyof Members]: Members[K]["Encoded"] }[number] + readonly "DecodingServices": { [K in keyof Members]: Members[K]["DecodingServices"] }[number] + readonly "EncodingServices": { [K in keyof Members]: Members[K]["EncodingServices"] }[number] + readonly "~type.make.in": { [K in keyof Members]: Members[K]["~type.make"] }[number] + readonly "~type.make": { [K in keyof Members]: Members[K]["~type.make"] }[number] + readonly "Iso": { [K in keyof Members]: Members[K]["Iso"] }[number] + readonly members: Members + /** + * Returns a new union with the members modified by the provided function. + * + * **Details** + * + * Options: + * + * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints + * that were attached to the original union. Defaults to `false`. + * + * **Warning**: This is an unsafe operation. Since `mapFields` + * transformations change the schema type, the original refinement functions + * may no longer be valid or safe to apply to the transformed schema. Only + * use this option if you have verified that your refinements remain correct + * after the transformation. + */ + mapMembers>( + f: (members: Members) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Union>> +} + +function makeUnion>( + ast: SchemaAST.Union, + members: Members +): Union { + return make(ast, { + members, + mapMembers>( + this: Union, + f: (members: Members) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Union>> { + const members = f(this.members) + return makeUnion( + SchemaAST.union(members, this.ast.mode, options?.unsafePreserveChecks ? this.ast.checks : undefined), + members + ) + } + }) +} + +/** + * Creates a union schema from an array of member schemas. Members are tested in + * order; the first match is returned. + * + * **Details** + * + * Optionally, specify `mode`: + * - `"anyOf"` (default) — matches if any member matches. + * - `"oneOf"` — matches if exactly one member matches. + * + * **Example** (Defining a string or number union) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Union([Schema.String, Schema.Number]) + * + * Schema.decodeUnknownSync(schema)("hello") // "hello" + * Schema.decodeUnknownSync(schema)(42) // 42 + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function Union>( + members: Members, + options?: { mode?: "anyOf" | "oneOf" } +): Union { + return makeUnion(SchemaAST.union(members, options?.mode ?? "anyOf", undefined), members) +} + +/** + * Type-level representation returned by {@link Literals}. + * + * @category models + * @since 4.0.0 + */ +export interface Literals> + extends Bottom, Literals> +{ + readonly literals: L + readonly members: { readonly [K in keyof L]: Literal } + /** + * Map over the members of the union. + */ + mapMembers>(f: (members: this["members"]) => To): Union>> + + pick>(literals: L2): Literals + + transform( + to: L2 + ): Union<{ [I in keyof L]: decodeTo, Literal> }> +} + +/** + * Creates a union schema from an array of literal values. + * + * **Example** (Defining status codes) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Literals(["active", "inactive", "pending"]) + * // accepts "active", "inactive", or "pending" + * ``` + * + * @see {@link Literal} for a schema that represents a single literal. + * @category constructors + * @since 4.0.0 + */ +export function Literals>(literals: L): Literals { + const members = literals.map(Literal) as { readonly [K in keyof L]: Literal } + return make(SchemaAST.union(members, "anyOf", undefined), { + literals, + members, + mapMembers>( + this: Literals, + f: (members: Literals["members"]) => To + ): Union>> { + return Union(f(this.members)) + }, + pick>(literals: L2): Literals { + return Literals(literals) + }, + transform( + to: L2 + ): Union<{ [I in keyof L]: decodeTo, Literal> }> { + return Union(members.map((member, index) => member.transform(to[index]))) as any + } + }) +} + +/** + * Type-level representation returned by {@link NullOr}. + * + * @category models + * @since 3.10.0 + */ +export interface NullOr extends Union { + readonly "Rebuild": NullOr +} + +interface NullOrLambda extends Lambda { + (self: S): NullOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NullOr : never +} + +/** + * Creates a union schema of `S | null`. + * + * @category constructors + * @since 3.10.0 + */ +export const NullOr = Struct_.lambda((self) => Union([self, Null])) + +/** + * Type-level representation returned by {@link UndefinedOr}. + * + * @category models + * @since 3.10.0 + */ +export interface UndefinedOr extends Union { + readonly "Rebuild": UndefinedOr +} + +interface UndefinedOrLambda extends Lambda { + (self: S): UndefinedOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? UndefinedOr : never +} + +/** + * Creates a union schema of `S | undefined`. + * + * @category constructors + * @since 3.10.0 + */ +export const UndefinedOr = Struct_.lambda((self) => Union([self, Undefined])) + +/** + * Type-level representation returned by {@link NullishOr}. + * + * @category models + * @since 3.10.0 + */ +export interface NullishOr extends Union { + readonly "Rebuild": NullishOr +} + +interface NullishOrLambda extends Lambda { + (self: S): NullishOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NullishOr : never +} + +/** + * Creates a union schema of `S | null | undefined`. + * + * @category constructors + * @since 3.10.0 + */ +export const NullishOr = Struct_.lambda((self) => Union([self, Null, Undefined])) + +/** + * Type-level representation returned by {@link suspend}. + * + * @category models + * @since 3.10.0 + */ +export interface suspend extends + BottomLazy< + SchemaAST.Suspend, + suspend, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] +} + +/** + * Creates a suspended schema that defers evaluation until needed. This is + * essential for creating recursive schemas where a schema references itself, + * preventing infinite recursion during schema definition. + * + * **Example** (Defining recursive tree schemas) + * + * ```ts + * import { Schema } from "effect" + * + * interface Tree { + * readonly value: number + * readonly children: ReadonlyArray + * } + * + * const Tree = Schema.Struct({ + * value: Schema.Number, + * children: Schema.Array(Schema.suspend((): Schema.Codec => Tree)) + * }) + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function suspend(f: () => S): suspend { + return make(new SchemaAST.Suspend(() => f().ast)) +} + +/** + * Attaches one or more filter checks to a schema without changing the + * TypeScript type. + * + * **Example** (Adding checks to a schema) + * + * ```ts + * import { Schema } from "effect" + * + * const AgeSchema = Schema.Number.pipe( + * Schema.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(120)) + * ) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export function check( + ...checks: readonly [SchemaAST.Check, ...Array>] +) { + return (self: S): S["Rebuild"] => self.check(...checks) +} + +/** + * Type-level representation returned by {@link refine}. + * + * @category filtering + * @since 3.10.0 + */ +export interface refine extends + BottomLazy< + S["ast"], + refine, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": T + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": T + readonly "Iso": T + readonly schema: S +} + +/** + * Narrows the TypeScript type of a schema's output via a type guard predicate, + * attaching the guard as a runtime filter check. + * + * **Details** + * + * The `annotations` parameter annotates the filter created by the refinement. + * With the default formatter, failed refinements use `message` first, + * `expected` second, and `` when neither is provided. `identifier` + * names type-level failures before the refinement runs; it does not name the + * failed refinement itself. + * + * @category filtering + * @since 3.10.0 + */ +export function refine( + refinement: (value: S["Type"]) => value is T, + annotations?: Annotations.Filter +) { + return (schema: S): refine => + make(SchemaAST.appendChecks(schema.ast, [SchemaAST.makeFilterByGuard(refinement, annotations)]), { schema }) +} + +type DistributeBrands = UnionToIntersection : never> + +/** + * Type-level representation returned by {@link brand}. + * + * @category branding + * @since 3.10.0 + */ +export interface brand extends + BottomLazy< + S["ast"], + brand, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] & DistributeBrands + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["Type"] & DistributeBrands + readonly "Iso": S["Type"] & DistributeBrands + readonly schema: S + readonly identifier: string +} + +/** + * Adds a nominal brand to a schema, intersecting the output type with + * `Brand.Brand` to prevent accidental mixing of structurally identical types. + * + * **When to use** + * + * Use to make values decoded by an existing schema nominally distinct when the + * schema already carries the runtime validation you need. + * + * **Gotchas** + * + * `brand` adds brand metadata and narrows the TypeScript output type, but it + * does not add runtime checks. + * + * @see {@link fromBrand} for applying a Brand constructor's checks along with the brand tag + * + * @category branding + * @since 3.10.0 + */ +export function brand(identifier: B) { + return (schema: S): brand => + make(SchemaAST.brand(schema.ast, identifier), { schema, identifier }) +} + +/** + * Creates a branded schema from a {@link Brand.Constructor}, applying the + * constructor's checks and brand tag to the underlying schema. + * + * @category branding + * @since 3.10.0 + */ +export function fromBrand>(identifier: string, ctor: Brand.Constructor) { + return }>( + self: S + ): brand> => { + return (ctor.checks ? self.check(...ctor.checks) : self).pipe(brand(identifier)) + } +} + +/** + * Type-level representation returned by {@link middlewareDecoding}. + * + * @category decoding + * @since 4.0.0 + */ +export interface middlewareDecoding extends + BottomLazy< + S["ast"], + middlewareDecoding, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": RD + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +/** + * Intercepts the decoding pipeline of a schema. + * + * **Details** + * + * The provided function receives the current decoding `Effect` and `ParseOptions`, + * and returns a new `Effect` — potentially adding service requirements (`RD`), + * recovering from errors, or augmenting the result. + * + * **Example** (Logging decode failures) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const Logged = Schema.String.pipe( + * Schema.middlewareDecoding((effect) => + * Effect.tapError(effect, (issue) => Effect.log("decode failed", issue)) + * ) + * ) + * ``` + * + * @see {@link catchDecoding} for a simpler error-recovery variant + * @category decoding + * @since 4.0.0 + */ +export function middlewareDecoding( + decode: ( + effect: Effect.Effect, SchemaIssue.Issue, S["DecodingServices"]>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, RD> +) { + return (schema: S): middlewareDecoding => + make( + SchemaAST.middlewareDecoding(schema.ast, new SchemaTransformation.Middleware(decode, identity)), + { schema } + ) +} + +/** + * Type-level representation returned by {@link middlewareEncoding}. + * + * @category encoding + * @since 4.0.0 + */ +export interface middlewareEncoding extends + BottomLazy< + S["ast"], + middlewareEncoding, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": RE + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +/** + * Intercepts the encoding pipeline of a schema. + * + * **Details** + * + * The provided function receives the current encoding `Effect` and `ParseOptions`, + * and returns a new `Effect` — potentially adding service requirements (`RE`), + * recovering from errors, or augmenting the result. + * + * **Example** (Logging encode failures) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const Logged = Schema.String.pipe( + * Schema.middlewareEncoding((effect) => + * Effect.tapError(effect, (issue) => Effect.log("encode failed", issue)) + * ) + * ) + * ``` + * + * @see {@link catchEncoding} for a simpler error-recovery variant + * @category encoding + * @since 4.0.0 + */ +export function middlewareEncoding( + encode: ( + effect: Effect.Effect, SchemaIssue.Issue, S["EncodingServices"]>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, RE> +) { + return (schema: S): middlewareEncoding => + make( + SchemaAST.middlewareEncoding(schema.ast, new SchemaTransformation.Middleware(identity, encode)), + { schema } + ) +} + +/** + * Recovers from a decoding error by providing a fallback value. + * + * **Details** + * + * The handler receives the `Issue` and returns an `Effect` that either + * succeeds with a fallback value or re-fails with a (possibly different) issue. + * + * **Example** (Returning a default on decode failure) + * + * ```ts + * import { Effect, Option, Schema } from "effect" + * + * const schema = Schema.Number.pipe( + * Schema.catchDecoding((_issue) => Effect.succeed(Option.some(0))) + * ) + * ``` + * + * @see {@link catchDecodingWithContext} to add service requirements to the handler + * @category error handling + * @since 4.0.0 + */ +export function catchDecoding( + f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue> +): (self: S) => middlewareDecoding { + return catchDecodingWithContext(f) +} + +/** + * Recovers from a decoding error with a handler that may require Effect services. + * + * **When to use** + * + * Use when you need decoding fallback logic to require services from the Effect + * context. + * + * **Details** + * + * The handler receives the `Issue` and returns an `Effect` that either succeeds + * with a fallback value or re-fails with a (possibly different) issue. The + * handler's services are added to the schema's decoding services. + * + * @see {@link catchDecoding} for recovery handlers that do not require services + * @see {@link middlewareDecoding} for intercepting or replacing the full decoding pipeline + * + * @category error handling + * @since 4.0.0 + */ +export function catchDecodingWithContext( + f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue, R> +) { + return (self: S): middlewareDecoding => + middlewareDecoding(Effect.catchEager(f))(self) +} + +/** + * Recovers from an encoding error by providing a fallback value. + * + * **Details** + * + * The handler receives the `Issue` and returns an `Effect` that either + * succeeds with a fallback value or re-fails with a (possibly different) issue. + * + * @see {@link catchEncodingWithContext} to add service requirements to the handler + * @category error handling + * @since 4.0.0 + */ +export function catchEncoding( + f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue> +): (self: S) => middlewareEncoding { + return catchEncodingWithContext(f) +} + +/** + * Recovers from an encoding error with a handler that may require Effect services. + * + * **When to use** + * + * Use when you need encoding fallback logic to require services from the Effect + * context. + * + * **Details** + * + * The handler receives the `Issue` and returns an `Effect` that either succeeds + * with a fallback encoded value or re-fails with a (possibly different) issue. + * The handler's services are added to the schema's encoding services. + * + * @see {@link catchEncoding} for recovery handlers that do not require services + * @see {@link middlewareEncoding} for intercepting or replacing the full encoding pipeline + * + * @category error handling + * @since 4.0.0 + */ +export function catchEncodingWithContext( + f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue, R> +) { + return (self: S): middlewareEncoding => + middlewareEncoding(Effect.catchEager(f))(self) +} + +/** + * Type-level representation returned by {@link decodeTo}. + * + * @category transforming + * @since 4.0.0 + */ +export interface decodeTo extends + BottomLazy< + To["ast"], + decodeTo, + To["~type.parameters"], + To["~type.mutability"], + To["~type.optionality"], + To["~type.constructor.default"], + From["~encoded.mutability"], + From["~encoded.optionality"] + > +{ + readonly "Type": To["Type"] + readonly "Encoded": From["Encoded"] + readonly "DecodingServices": To["DecodingServices"] | From["DecodingServices"] | RD + readonly "EncodingServices": To["EncodingServices"] | From["EncodingServices"] | RE + readonly "~type.make.in": To["~type.make.in"] + readonly "~type.make": To["~type.make"] + readonly "Iso": To["Iso"] + readonly from: From + readonly to: To +} + +/** + * Type-level representation returned by {@link decodeTo} without a custom transformation. + * + * @category transforming + * @since 3.10.0 + */ +export interface compose extends decodeTo {} + +/** + * Creates a schema that transforms from a source schema to a target schema. + * + * **When to use** + * + * Use when decoding should change the schema's decoded type or encoded shape, + * with an optional custom bidirectional transformation. + * + * **Details** + * + * Call it with the target schema `to` and then pipe the source schema `from` + * into the returned function. The resulting schema decodes from + * `From["Encoded"]` to `To["Type"]` and encodes from `To["Type"]` back to + * `From["Encoded"]`. + * + * When no transformation is provided, `SchemaTransformation.passthrough()` is + * used, so `From["Type"]` must already be compatible with `To["Encoded"]`. + * The resulting schema combines decoding and encoding services from both + * schemas and any custom transformation. + * + * **Gotchas** + * + * In a custom transformation, `decode` maps `From["Type"]` to `To["Encoded"]` + * and is used on the encoding path, while `encode` maps `To["Encoded"]` to + * `From["Type"]` and is used on the decoding path. + * + * **Example** (Transforming strings to numbers with a schema transformation) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * const NumberFromString = Schema.String.pipe( + * Schema.decodeTo( + * Schema.Number, + * { + * decode: SchemaGetter.transform((s) => Number(s)), + * encode: SchemaGetter.transform((n) => String(n)) + * } + * ) + * ) + * + * const result = Schema.decodeUnknownSync(NumberFromString)("123") + * // result: 123 + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export function decodeTo(to: To): (from: From) => compose +export function decodeTo( + to: To, + transformation: { + readonly decode: SchemaGetter.Getter, NoInfer, RD> + readonly encode: SchemaGetter.Getter, NoInfer, RE> + } +): (from: From) => decodeTo +export function decodeTo( + to: To, + transformation?: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter + } | undefined +) { + return (from: From) => { + return make( + SchemaAST.decodeTo( + from.ast, + to.ast, + transformation ? SchemaTransformation.make(transformation) : SchemaTransformation.passthrough() + ), + { + from, + to + } + ) + } +} + +/** + * Applies a transformation to a schema, creating a new schema with the same type but transformed encoding/decoding. + * + * **When to use** + * + * Use when the decoded type stays the same and the transformation only + * normalizes values during encoding and decoding. + * + * **Details** + * + * Call it with a transformation object and then pipe a schema into the returned + * function. The resulting schema keeps the same `Type` and `Encoded` types as + * the source schema, while applying the transformation during both decoding and + * encoding. + * + * Internally this uses `toType(self)` as the target schema and combines service + * requirements from the source schema and the transformation. + * + * **Gotchas** + * + * Use {@link decodeTo} instead when the transformation should change the + * decoded type. For this helper, both transformation getters operate on + * `S["Type"]` values. + * + * **Example** (Trimming string values during encoding/decoding) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * const Trimmed = Schema.String.pipe( + * Schema.decode({ + * decode: SchemaGetter.transform((s) => s.trim()), + * encode: SchemaGetter.transform((s) => s.trim()) + * }) + * ) + * + * const result = Schema.decodeUnknownSync(Trimmed)(" hello ") + * // result: "hello" + * ``` + * + * @category transforming + * @since 3.10.0 + */ +export function decode(transformation: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter +}) { + return (self: S): decodeTo, S, RD, RE> => { + return decodeTo, S, RD, RE>(toType(self), transformation)(self) + } +} + +/** + * Reverses a schema transformation so the encoded schema is supplied first. + * + * **When to use** + * + * Use to define a transformation by naming the encoded schema before the + * decoded schema. + * + * **Details** + * + * `encodeTo(to)(from)` is equivalent to `to.pipe(decodeTo(from))`. The `from` + * schema acts as the target decoded schema and `to` acts as the encoded source. + * + * **Example** (Encoding a number back to a string) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * const NumberFromString = Schema.Number.pipe( + * Schema.encodeTo(Schema.String, { + * decode: SchemaGetter.transform((s: string) => Number(s)), + * encode: SchemaGetter.transform((n: number) => String(n)) + * }) + * ) + * ``` + * + * @category transforming + * @since 4.0.0 + */ +export function encodeTo( + to: To +): (from: From) => decodeTo +export function encodeTo( + to: To, + transformation: { + readonly decode: SchemaGetter.Getter, NoInfer, RD> + readonly encode: SchemaGetter.Getter, NoInfer, RE> + } +): (from: From) => decodeTo +export function encodeTo( + to: To, + transformation?: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter + } +) { + return (from: From): decodeTo => { + return transformation ? + decodeTo(from, transformation)(to) : + decodeTo(from)(to) + } +} + +/** + * Applies a transformation to a schema's encoded type, creating a new schema where encoding/decoding + * operate on `S["Encoded"]` rather than `S["Type"]`. + * + * **Details** + * + * The `decode` getter maps `S["Encoded"]` → `S["Encoded"]` (applied during decoding), + * and the `encode` getter maps `S["Encoded"]` → `S["Encoded"]` (applied during encoding). + * + * **Example** (Upper-casing encoded strings) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * const UpperFromLower = Schema.String.pipe( + * Schema.encode({ + * decode: SchemaGetter.transform((s: string) => s.toLowerCase()), + * encode: SchemaGetter.transform((s: string) => s.toUpperCase()) + * }) + * ) + * ``` + * + * @category transforming + * @since 3.10.0 + */ +export function encode(transformation: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter +}) { + return (self: S): decodeTo, RD, RE> => { + return decodeTo, RD, RE>(self, transformation)(toEncoded(self)) + } +} + +/** + * Constraint used to ensure a schema field does not already have a constructor default. + * + * **Details** + * + * Only schemas that satisfy this constraint can be passed to {@link withConstructorDefault}. + * + * @category models + * @since 4.0.0 + */ +export interface WithoutConstructorDefault { + readonly "~type.constructor.default": "no-default" +} + +/** + * Type-level representation returned by {@link withConstructorDefault}. + * + * @category constructors + * @since 3.10.0 + */ +export interface withConstructorDefault extends + BottomLazy< + S["ast"], + withConstructorDefault, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + "with-default", + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +/** + * Attaches a constructor default value to a schema field. + * + * **Details** + * + * Constructor defaults are applied only during `make*`, not during decoding or + * encoding. + * + * **Example** (Defining an optional field with a static default) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const MySchema = Schema.Struct({ + * name: Schema.String.pipe( + * Schema.optionalKey, + * Schema.withConstructorDefault(Effect.succeed("anonymous")) + * ) + * }) + * + * const value = MySchema.make({}) + * // value: { name: "anonymous" } + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function withConstructorDefault( + // `S["~type.make.in"]` instead of `S["Type"]` is intentional here because + // it makes easier to define the default value if there are nested defaults + defaultValue: Effect.Effect +) { + return (schema: S): withConstructorDefault => + make(SchemaAST.withConstructorDefault(schema.ast, toIssueEffect(defaultValue)), { schema }) +} + +function toIssueEffect( + self: Effect.Effect +): Effect.Effect { + return Effect.catchCause(self, (cause) => Effect.failCauseSync(() => Cause_.map(cause, (error) => error.issue))) +} + +/** + * Type-level representation returned by {@link withDecodingDefaultKey}. + * + * @category decoding + * @since 4.0.0 + */ +export interface withDecodingDefaultKey + extends decodeTo>, R> +{ + readonly "Rebuild": withDecodingDefaultKey +} + +/** + * Options for {@link withDecodingDefaultKey} and {@link withDecodingDefault}. + * + * **Details** + * + * - `encodingStrategy`: + * - `"passthrough"` (default): pass the value through during encoding + * - `"omit"`: omit the key from the encoded output + * + * @category options + * @since 4.0.0 + */ +export type DecodingDefaultOptions = { + readonly encodingStrategy?: "omit" | "passthrough" | undefined +} + +/** + * Makes a struct key optional on the `Encoded` side and provides a default + * `Encoded` value when the key is missing during decoding. + * + * **Details** + * + * The key uses `optionalKey` on the encoded side, so it may be absent from the + * input object but **not** `undefined`. The default value is specified in terms + * of the `Encoded` type (before any decoding transformations). + * + * Options: + * + * - `encodingStrategy`: + * - `"passthrough"` (default): include the value in the encoded output. + * - `"omit"`: omit the key from the encoded output. + * + * **Example** (Providing a default for a missing struct key) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const MySchema = Schema.Struct({ + * name: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("anonymous"))) + * }) + * + * const result = Schema.decodeUnknownSync(MySchema)({}) + * // result: { name: "anonymous" } + * ``` + * + * @see {@link withDecodingDefault} for the value-level variant (key absent **or** `undefined`) + * @see {@link withDecodingDefaultTypeKey} for the variant where the default is a `Type` value + * @category decoding + * @since 4.0.0 + */ +export function withDecodingDefaultKey( + defaultValue: Effect.Effect, + options?: DecodingDefaultOptions +) { + const encode = options?.encodingStrategy === "omit" ? SchemaGetter.omit() : SchemaGetter.passthrough() + return (self: S): withDecodingDefaultKey => { + return optionalKey(toEncoded(self)).pipe(decodeTo(self, { + decode: SchemaGetter.withDefault(toIssueEffect(defaultValue)), + encode + })) + } +} + +/** + * Type-level representation returned by {@link withDecodingDefaultTypeKey}. + * + * @category decoding + * @since 4.0.0 + */ +export interface withDecodingDefaultTypeKey + extends decodeTo, R>, optionalKey> +{ + readonly "Rebuild": withDecodingDefaultTypeKey +} + +/** + * Makes a struct key optional on the `Encoded` side (`optionalKey`, so the + * key may be absent but **not** `undefined`) and provides a default `Type` + * value when the key is missing during decoding. + * + * **Details** + * + * Unlike {@link withDecodingDefaultKey}, the default value is specified in + * terms of the `Type` (decoded) representation, so it does not need to go + * through the decoding transformation. + * + * Options: + * + * - `encodingStrategy`: + * - `"passthrough"` (default): include the value in the encoded output. + * - `"omit"`: omit the key from the encoded output. + * + * @see {@link withDecodingDefaultKey} for the variant where the default is an `Encoded` value + * @see {@link withDecodingDefaultType} for the value-level variant + * @category decoding + * @since 4.0.0 + */ +export function withDecodingDefaultTypeKey( + defaultValue: Effect.Effect, + options?: DecodingDefaultOptions +) { + return (self: S): withDecodingDefaultTypeKey => { + return toType(self).pipe( + withDecodingDefaultKey, R>(defaultValue, options), + encodeTo(optionalKey(self)) + ) + } +} + +/** + * Type-level representation returned by {@link withDecodingDefault}. + * + * @category decoding + * @since 3.10.0 + */ +export interface withDecodingDefault extends decodeTo>, R> { + readonly "Rebuild": withDecodingDefault +} + +/** + * Wraps the `Encoded` side with `optional` (key absent **or** `undefined`) + * and provides a default `Encoded` value when the field is missing or + * `undefined` during decoding. + * + * **When to use** + * + * Use when the default is expressed in the encoded representation, before the + * field's decoding transformation runs. + * + * **Details** + * + * The default value is specified in terms of the `Encoded` type (before any + * decoding transformations). + * + * Options: + * + * - `encodingStrategy`: + * - `"passthrough"` (default): include the value in the encoded output. + * - `"omit"`: omit the key from the encoded output. + * + * **Example** (Providing a default for an optional field value) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * const MySchema = Schema.Struct({ + * name: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("anonymous"))) + * }) + * + * const result = Schema.decodeUnknownSync(MySchema)({ name: undefined }) + * // result: { name: "anonymous" } + * ``` + * + * @see {@link withDecodingDefaultKey} for the key-level variant (key absent only, not `undefined`) + * @see {@link withDecodingDefaultType} for the variant where the default is a `Type` value + * @category decoding + * @since 3.10.0 + */ +export function withDecodingDefault( + defaultValue: Effect.Effect, + options?: DecodingDefaultOptions +) { + const encode = options?.encodingStrategy === "omit" ? SchemaGetter.omit() : SchemaGetter.passthrough() + return (self: S): withDecodingDefault => { + return optional(toEncoded(self)).pipe(decodeTo(self, { + decode: SchemaGetter.withDefault(toIssueEffect(defaultValue)), + encode + })) + } +} + +/** + * Type-level representation returned by {@link withDecodingDefaultType}. + * + * @category decoding + * @since 4.0.0 + */ +export interface withDecodingDefaultType + extends decodeTo, R>, optional> +{ + readonly "Rebuild": withDecodingDefaultType +} + +/** + * Wraps the `Encoded` side with `optional` (key absent **or** `undefined`) + * and provides a default `Type` value when the field is missing or + * `undefined` during decoding. + * + * **When to use** + * + * Use when the default is already in the decoded representation and should not + * pass through the field's decoding transformation. + * + * **Details** + * + * Unlike {@link withDecodingDefault}, the default value is specified in terms + * of the `Type` (decoded) representation, so it does not need to go through + * the decoding transformation. + * + * Options: + * + * - `encodingStrategy`: + * - `"passthrough"` (default): include the value in the encoded output. + * - `"omit"`: omit the key from the encoded output. + * + * @see {@link withDecodingDefault} for the variant where the default is an `Encoded` value + * @see {@link withDecodingDefaultTypeKey} for the key-level variant + * @category decoding + * @since 4.0.0 + */ +export function withDecodingDefaultType( + defaultValue: Effect.Effect, + options?: DecodingDefaultOptions +) { + return (self: S): withDecodingDefaultType => { + return toType(self).pipe( + withDecodingDefault, R>(defaultValue, options), + encodeTo(optional(self)) + ) + } +} + +/** + * Type-level representation returned by {@link tag}. + * + * @category constructors + * @since 3.10.0 + */ +export interface tag extends withConstructorDefault> {} + +/** + * Combines a {@link Literal} schema with {@link withConstructorDefault}, making it ideal + * for discriminator fields in tagged unions. When constructing via `make`, the + * `_tag` field can be omitted and will be filled automatically. + * + * **Example** (Defining a discriminated union tag) + * + * ```ts + * import { Schema } from "effect" + * + * const A = Schema.Struct({ _tag: Schema.tag("A"), value: Schema.Number }) + * + * // _tag is optional in make, auto-filled to "A" + * const a = A.make({ value: 42 }) + * // a: { _tag: "A", value: 42 } + * ``` + * + * @see {@link tagDefaultOmit} to also omit the tag during encoding + * @see {@link TaggedStruct} for a shorthand that adds `_tag` automatically + * @category constructors + * @since 3.10.0 + */ +export function tag(literal: Tag): tag { + return Literal(literal).pipe(withConstructorDefault(Effect.succeed(literal))) +} + +/** + * Creates a literal `_tag` schema that is omitted from encoded output. + * + * **When to use** + * + * Use to decode data that omits the discriminator field while still constructing + * values with a `_tag` for tagged union matching. + * + * **Details** + * + * The tag is filled during decoding and construction, like {@link tag}, but is + * omitted when encoding. + * + * **Example** (Omitting tags during encoding) + * + * ```ts + * import { Schema } from "effect" + * + * const A = Schema.Struct({ + * _tag: Schema.tagDefaultOmit("A"), + * value: Schema.Number + * }) + * + * // Encode strips the _tag field + * const encoded = Schema.encodeUnknownSync(A)({ _tag: "A", value: 1 }) + * // encoded: { value: 1 } + * ``` + * + * @see {@link tag} for the variant that keeps the tag during encoding + * @category constructors + * @since 4.0.0 + */ +export function tagDefaultOmit(literal: Tag) { + return tag(literal).pipe(withDecodingDefaultKey(Effect.succeed(literal), { encodingStrategy: "omit" })) +} + +/** + * Type-level representation returned by {@link TaggedStruct}. + * + * @category models + * @since 3.10.0 + */ +export type TaggedStruct = Struct< + Simplify<{ readonly _tag: tag } & Fields> +> + +/** + * Creates a struct schema with an automatically populated `_tag` field. + * + * **When to use** + * + * Use to define a tagged union case from a literal tag and a set of fields. + * + * **Details** + * + * When using the `make` method, the `_tag` field is optional and will be + * added automatically. However, when decoding or encoding, the `_tag` field + * must be present in the input. + * + * **Example** (Defining a tagged struct shorthand) + * + * ```ts + * import { Schema } from "effect" + * + * // Defines a struct with a fixed `_tag` field + * const tagged = Schema.TaggedStruct("A", { + * a: Schema.String + * }) + * + * // This is the same as writing: + * const equivalent = Schema.Struct({ + * _tag: Schema.tag("A"), + * a: Schema.String + * }) + * ``` + * + * **Example** (Accessing the literal value of the tag) + * + * ```ts + * import { Schema } from "effect" + * + * const tagged = Schema.TaggedStruct("A", { + * a: Schema.String + * }) + * + * // literal: "A" + * const literal = tagged.fields._tag.schema.literal + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function TaggedStruct( + value: Tag, + fields: Fields +): TaggedStruct { + return Struct({ _tag: tag(value), ...fields }) +} + +/** + * Recursively flatten any nested Schema.Union members into a single tuple of leaf schemas. + */ +type Flatten = Schemas extends readonly [infer Head, ...infer Tail] + ? Head extends Union ? [...Flatten, ...Flatten] + : [Head, ...Flatten] + : [] + +type TaggedUnionUtils< + Tag extends PropertyKey, + Members extends ReadonlyArray, + Flattened extends ReadonlyArray = Flatten< + Members + > +> = { + readonly cases: Simplify<{ [M in Flattened[number] as M["Type"][Tag]]: M }> + readonly isAnyOf: ( + keys: ReadonlyArray + ) => (value: Members[number]["Type"]) => value is Extract + readonly guards: { [M in Flattened[number] as M["Type"][Tag]]: (u: unknown) => u is M["Type"] } + readonly match: { + < + Cases extends { [M in Flattened[number] as M["Type"][Tag]]: (value: M["Type"]) => any } + >( + value: Members[number]["Type"], + cases: Cases + ): Cases[keyof Cases] extends (value: any) => infer R ? Unify + : never + < + Cases extends { [M in Flattened[number] as M["Type"][Tag]]: (value: M["Type"]) => any } + >( + cases: Cases + ): (value: Members[number]["Type"]) => Cases[keyof Cases] extends (value: any) => infer R ? Unify + : never + } +} + +/** + * Type-level representation returned by {@link toTaggedUnion}. + * + * @category combinators + * @since 4.0.0 + */ +export type toTaggedUnion< + Tag extends PropertyKey, + Members extends ReadonlyArray +> = Union & TaggedUnionUtils + +/** + * Augments an existing {@link Union} of tagged structs with utility methods keyed by the discriminant field. + * + * **Example** (Adding tagged-union utilities to an existing union) + * + * ```ts + * import { Schema } from "effect" + * + * const A = Schema.TaggedStruct("A", { value: Schema.Number }) + * const B = Schema.TaggedStruct("B", { name: Schema.String }) + * + * const MyUnion = Schema.Union([A, B]).pipe(Schema.toTaggedUnion("_tag")) + * + * // Pattern-match on the union + * const result = MyUnion.match({ _tag: "A", value: 1 }, { + * A: (a) => `number: ${a.value}`, + * B: (b) => `name: ${b.name}` + * }) + * ``` + * + * @see {@link TaggedUnion} for a shorthand that builds the union from scratch + * @category combinators + * @since 4.0.0 + */ +export function toTaggedUnion(tag: Tag) { + return >( + self: Union + ): toTaggedUnion => { + const cases: Record = {} + const guards: Record boolean> = {} + const isAnyOf = (keys: ReadonlyArray) => (value: Members[number]["Type"]) => keys.includes(value[tag]) + + walk(self) + + return Object.assign(self, { cases, isAnyOf, guards, match }) as any + + function walk(schema: Constraint) { + const ast = schema.ast + + if ( + SchemaAST.isUnion(ast) && "members" in schema && globalThis.Array.isArray(schema.members) && + schema.members.every(isSchema) + ) { + return schema.members.forEach(walk) + } + + const sentinels = SchemaAST.collectSentinels(ast) + if (sentinels.length > 0) { + const literal = sentinels.find((s) => s.key === tag)?.literal + if (Predicate.isPropertyKey(literal)) { + cases[literal] = schema + guards[literal] = is(toType(schema)) + return + } + } + + throw new globalThis.Error("No literal or unique symbol found") + } + + function match() { + if (arguments.length === 1) { + const cases = arguments[0] + return function(value: any) { + return cases[value[tag]](value) + } + } + const value = arguments[0] + const cases = arguments[1] + return cases[value[tag]](value) + } + } +} + +/** + * Type-level representation returned by {@link TaggedUnion}. + * + * @category models + * @since 4.0.0 + */ +export interface TaggedUnion> extends + BottomLazy< + SchemaAST.Union, + TaggedUnion + > +{ + readonly "Type": { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases] + readonly "Encoded": { [K in keyof Cases]: Cases[K]["Encoded"] }[keyof Cases] + readonly "DecodingServices": { [K in keyof Cases]: Cases[K]["DecodingServices"] }[keyof Cases] + readonly "EncodingServices": { [K in keyof Cases]: Cases[K]["EncodingServices"] }[keyof Cases] + readonly "~type.make.in": { [K in keyof Cases]: Cases[K]["~type.make"] }[keyof Cases] + readonly "~type.make": { [K in keyof Cases]: Cases[K]["~type.make"] }[keyof Cases] + readonly "Iso": { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases] + readonly cases: Cases + readonly isAnyOf: ( + keys: ReadonlyArray + ) => (value: Cases[keyof Cases]["Type"]) => value is Extract + readonly guards: { [K in keyof Cases]: (u: unknown) => u is Cases[K]["Type"] } + readonly match: { + ( + cases: { [K in keyof Cases]: (value: Cases[K]["Type"]) => Output } + ): (value: Cases[keyof Cases]["Type"]) => Output + ( + value: Cases[keyof Cases]["Type"], + cases: { [K in keyof Cases]: (value: Cases[K]["Type"]) => Output } + ): Output + } +} + +/** + * Builds a discriminated union from a record of field sets, one per variant. + * Each key becomes the `_tag` literal and the value is passed to {@link TaggedStruct}. + * The result includes `cases`, `guards`, `isAnyOf`, and `match` utilities. + * + * **Example** (Pattern matching a discriminated union) + * + * ```ts + * import { Schema } from "effect" + * + * const Shape = Schema.TaggedUnion({ + * Circle: { radius: Schema.Number }, + * Rectangle: { width: Schema.Number, height: Schema.Number } + * }) + * + * // Pattern-match on a decoded value + * const area = Shape.match({ _tag: "Circle", radius: 5 }, { + * Circle: (c) => Math.PI * c.radius ** 2, + * Rectangle: (r) => r.width * r.height + * }) + * ``` + * + * @see {@link toTaggedUnion} to augment an existing union instead + * @category constructors + * @since 4.0.0 + */ +export function TaggedUnion>( + casesByTag: CasesByTag +): TaggedUnion<{ readonly [K in keyof CasesByTag & string]: TaggedStruct }> { + const cases: any = {} + const members: any = [] + for (const key of Object.keys(casesByTag)) { + members.push(cases[key] = TaggedStruct(key, casesByTag[key])) + } + const union = Union(members) + const { guards, isAnyOf, match } = toTaggedUnion("_tag")(union) + return make(union.ast, { cases, isAnyOf, guards, match }) +} + +/** + * Type-level representation returned by {@link Opaque}. + * + * @category models + * @since 4.0.0 + */ +export interface Opaque extends + BottomLazy< + S["ast"], + S["Rebuild"], + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": Self + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + new(_: never): S["Type"] & Brand +} + +/** + * Wraps a struct schema so that its decoded `Type` becomes a nominally distinct type `Self`. + * Useful for creating opaque types that are structurally identical to a base struct + * but type-incompatible with it. + * + * **Example** (Defining opaque structs) + * + * ```ts + * import { Schema } from "effect" + * + * class Person extends Schema.Opaque()( + * Schema.Struct({ + * name: Schema.String + * }) + * ) {} + * + * // Decoded value is Person, not { name: string } + * const person = Schema.decodeUnknownSync(Person)({ name: "Alice" }) + * // person: Person + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function Opaque() { + return (schema: S): Opaque & Omit => { + // oxlint-disable-next-line @typescript-eslint/no-extraneous-class + class Opaque {} + return Object.setPrototypeOf(Opaque, schema) + } +} + +/** + * Type-level representation returned by {@link instanceOf}. + * + * @category models + * @since 3.10.0 + */ +export interface instanceOf extends declare { + readonly "Rebuild": instanceOf +} + +/** + * Creates a schema that validates values using `instanceof`. + * Decoding and encoding pass the value through unchanged. + * + * **Example** (Defining a schema for a built-in class) + * + * ```ts + * import { Schema } from "effect" + * + * const DateSchema = Schema.instanceOf(Date) + * + * const decoded = Schema.decodeUnknownSync(DateSchema)(new Date("2024-01-01")) + * // decoded: Date + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export function instanceOf any, Iso = InstanceType>( + constructor: C, + annotations?: Annotations.Declaration> | undefined +): instanceOf, Iso> { + return declare((u): u is InstanceType => u instanceof constructor, annotations) +} + +/** + * Constructs an `SchemaAST.Link` that describes how a value of type `T` encodes to and decodes from a `To` schema. + * Used when building low-level AST transformations that bridge two schema types. + * + * @category transforming + * @since 4.0.0 + */ +export function link() { + return ( + encodeTo: To, + transformation: { + readonly decode: SchemaGetter.Getter> + readonly encode: SchemaGetter.Getter, T> + } + ): SchemaAST.Link => { + return new SchemaAST.Link(encodeTo.ast, SchemaTransformation.make(transformation)) + } +} + +// ----------------------------------------------------------------------------- +// Checks +// ----------------------------------------------------------------------------- + +/** + * Creates a custom validation filter from a predicate function. + * + * **Details** + * + * The predicate receives the decoded input value, the schema AST, and parse + * options, and returns a `FilterOutput`. Non-success outputs are normalized into + * schema issues. The `annotations` parameter annotates the filter itself; with + * the default formatter, failures use `message` first, `expected` second, and + * `` when neither is provided. + * + * When `abort` is `true`, parsing stops after this filter fails instead of + * collecting later check failures. + * + * **Example** (Reporting failure at a nested path) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check( + * Schema.makeFilter((o) => + * o.password === o.confirmPassword + * ? undefined + * : { path: ["password"], issue: "password and confirmPassword must match" } + * ) + * ) + * + * console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" }))) + * // Failure(Cause([Fail(SchemaError: password and confirmPassword must match + * // at ["password"])])) + * ``` + * + * **Example** (Reporting multiple failures at once) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check( + * Schema.makeFilter((o) => { + * const issues: Array = [] + * if (o.a > 0) { + * if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" }) + * if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" }) + * } + * return issues + * }) + * ) + * + * console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 }))) + * // Failure(Cause([Fail(SchemaError: b must be greater than 0 + * // at ["b"] + * // c must be greater than 0 + * // at ["c"])])) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeFilter: ( + filter: (input: T, ast: SchemaAST.AST, options: SchemaAST.ParseOptions) => FilterOutput, + annotations?: Annotations.Filter | undefined, + abort?: boolean +) => SchemaAST.Filter = SchemaAST.makeFilter + +/** + * A single failure reported by a filter predicate. Used as the element type + * of the array arm of {@link FilterOutput}, and also accepted on its own. + * + * **Details** + * + * - `string`: failure with that string as the message. Produces an + * {@link SchemaIssue.InvalidValue} wrapping the input, with the string used as + * the issue's `message` annotation. + * - {@link SchemaIssue.Issue}: a fully-formed issue, returned as-is. + * - `{ path, issue }`: failure attached to a nested path. `issue` is either + * a `string` (wrapped in an {@link SchemaIssue.InvalidValue}) or a full + * {@link SchemaIssue.Issue}; the result is wrapped in an {@link SchemaIssue.Pointer} + * at the given `path`. + * + * @category models + * @since 3.10.0 + */ +export type FilterIssue = string | SchemaIssue.Issue | { + readonly path: ReadonlyArray + readonly issue: string | SchemaIssue.Issue +} + +/** + * The value a filter predicate (see {@link makeFilter}) may return. + * + * **Details** + * + * Each shape is normalized into an {@link SchemaIssue.Issue} (or `undefined` for + * success) before being attached to the parse result: + * + * - `undefined`: success. The input satisfies the filter. + * - `true`: success. Equivalent to `undefined`, useful when the predicate is + * a plain boolean expression. + * - `false`: generic failure. Produces an {@link SchemaIssue.InvalidValue} wrapping + * the input, with no custom message. + * - {@link FilterIssue}: a single failure. See {@link FilterIssue} for the + * shapes (`string`, {@link SchemaIssue.Issue}, or `{ path, issue }`). + * - `ReadonlyArray`: several failures reported together. An + * empty array is treated as success; a single-element array is equivalent + * to returning that element directly; otherwise the entries are grouped + * into an {@link SchemaIssue.Composite}. + * + * @category models + * @since 3.10.0 + */ +export type FilterOutput = + | undefined + | boolean + | FilterIssue + | ReadonlyArray + +/** + * Groups multiple checks into a single {@link SchemaAST.FilterGroup}, applying + * optional shared annotations to the group as a whole. + * + * @category constructors + * @since 4.0.0 + */ +export function makeFilterGroup( + checks: readonly [SchemaAST.Check, ...Array>], + annotations: Annotations.Filter | undefined = undefined +): SchemaAST.FilterGroup { + return new SchemaAST.FilterGroup(checks, annotations) +} + +const TRIMMED_PATTERN = "^\\S[\\s\\S]*\\S$|^\\S$|^$" + +/** + * Validates that a string has no leading or trailing whitespace. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that + * matches strings without leading or trailing whitespace. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the trimmed pattern. + * + * @category String checks + * @since 4.0.0 + */ +export function isTrimmed(annotations?: Annotations.Filter) { + return makeFilter( + (s: string) => s.trim() === s, + { + expected: "a string with no leading or trailing whitespace", + meta: { + _tag: "isTrimmed", + regExp: new globalThis.RegExp(TRIMMED_PATTERN) + }, + arbitrary: { + constraint: { + patterns: [TRIMMED_PATTERN] + } + }, + ...annotations + } + ) +} + +/** + * Validates that a string matches the specified regular expression pattern. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `pattern` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the specified RegExp pattern. + * + * @category String checks + * @since 4.0.0 + */ +export const isPattern: (regExp: globalThis.RegExp, annotations?: Annotations.Filter) => SchemaAST.Filter = + SchemaAST.isPattern + +/** + * Validates that a string represents a finite number. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that matches + * strings representing finite numbers. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the number string pattern. + * + * @category String checks + * @since 4.0.0 + */ +export const isStringFinite: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringFinite + +/** + * Validates that a string is a signed base-10 integer literal for Effect's + * BigInt string encoding. + * + * **Details** + * + * The check uses the pattern `^-?\d+$`. It does not accept leading `+`, decimal + * points, exponent notation, separators, or non-decimal inputs such as + * hexadecimal strings. + * + * JSON Schema: + * This check corresponds to a `pattern` constraint with the same signed + * base-10 integer pattern. + * + * @category String checks + * @since 4.0.0 + */ +export const isStringBigInt: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringBigInt + +/** + * Validates that a string has the `Symbol(description)` format used by Effect's + * symbol string encoding. + * + * **Details** + * + * The check uses the pattern `^Symbol\((.*)\)$`. It is not a general test for + * whether a string can be passed to JavaScript's `Symbol()` function. + * + * @category String checks + * @since 4.0.0 + */ +export const isStringSymbol: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringSymbol + +/** + * Returns a RegExp for validating an RFC 9562 / RFC 4122 UUID. + * + * Optionally specify a version 1-8. If no version is specified (`undefined`), all versions are supported. + */ +const getUUIDRegExp = (version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8): globalThis.RegExp => { + if (version) { + return new globalThis.RegExp( + `^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$` + ) + } + return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/ +} + +/** + * Validates that a string is a strict Universally Unique Identifier (UUID). + * + * **When to use** + * + * Use when you need UUID semantics, including version and RFC variant bits, + * rather than only the dashed hexadecimal shape. + * + * **Details** + * + * Without a version argument, this accepts UUID versions 1 through 8, the nil + * UUID (`00000000-0000-0000-0000-000000000000`), and the max UUID + * (`ffffffff-ffff-ffff-ffff-ffffffffffff`). With a version argument, this + * accepts only UUIDs with that version and RFC variant bits; nil and max UUIDs + * are not versioned UUIDs and do not match version-specific checks. + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that matches + * UUID format, and includes a `format: "uuid"` annotation. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the UUID pattern. + * + * @see {@link isGUID} for shape-only GUID validation. + * @category String checks + * @since 4.0.0 + */ +export function isUUID(version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, annotations?: Annotations.Filter) { + const regExp = getUUIDRegExp(version) + return isPattern( + regExp, + { + expected: version ? `a UUID v${version}` : "a UUID", + meta: { + _tag: "isUUID", + regExp, + version + }, + ...annotations + } + ) +} + +const GUID_REGEXP = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/ + +/** + * Validates that a string has the GUID / UUID textual shape. + * + * **When to use** + * + * Use when you need to accept dashed hexadecimal identifiers without enforcing + * UUID version or variant bits. + * + * **Details** + * + * This check accepts strings in the `8-4-4-4-12` hexadecimal form. JSON Schema + * output includes the corresponding `pattern` constraint and intentionally does + * not include `format: "uuid"` because GUID validation is looser than UUID + * validation. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the GUID pattern. + * + * @see {@link isUUID} for strict UUID validation. + * @category String checks + * @since 4.0.0 + */ +export function isGUID(annotations?: Annotations.Filter) { + return isPattern( + GUID_REGEXP, + { + expected: "a GUID", + meta: { + _tag: "isGUID", + regExp: GUID_REGEXP + }, + ...annotations + } + ) +} + +/** + * Validates that a string is a valid ULID (Universally Unique Lexicographically + * Sortable Identifier). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that matches + * the ULID format. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the ULID pattern. + * + * @category String checks + * @since 4.0.0 + */ +export function isULID(annotations?: Annotations.Filter) { + const regExp = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/ + return isPattern( + regExp, + { + meta: { + _tag: "isULID", + regExp + }, + ...annotations + } + ) +} + +/** + * Validates that a string is valid Base64 encoded data. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that matches + * Base64 format. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the Base64 pattern. + * + * @category String checks + * @since 4.0.0 + */ +export function isBase64(annotations?: Annotations.Filter) { + const regExp = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/ + return isPattern( + regExp, + { + expected: "a base64 encoded string", + meta: { + _tag: "isBase64", + regExp + }, + ...annotations + } + ) +} + +/** + * Validates that a string is valid Base64URL encoded data (Base64 with URL-safe + * characters). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to a `pattern` constraint in JSON Schema that matches + * Base64URL format. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `patterns` + * constraint to ensure generated strings match the Base64URL pattern. + * + * @category String checks + * @since 4.0.0 + */ +export function isBase64Url(annotations?: Annotations.Filter) { + const regExp = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/ + return isPattern( + regExp, + { + expected: "a base64url encoded string", + meta: { + _tag: "isBase64Url", + regExp + }, + ...annotations + } + ) +} + +/** + * Validates at runtime that a string starts with the specified literal prefix. + * + * **Details** + * + * Notes: + * The JSON Schema and arbitrary metadata are built from `^${startsWith}` without + * escaping regexp metacharacters. If the prefix contains regexp syntax, generated + * patterns may not be equivalent to the runtime `startsWith` check. + * + * @category String checks + * @since 4.0.0 + */ +export function isStartsWith(startsWith: string, annotations?: Annotations.Filter) { + const formatted = JSON.stringify(startsWith) + return makeFilter( + (s: string) => s.startsWith(startsWith), + { + expected: `a string starting with ${formatted}`, + meta: { + _tag: "isStartsWith", + startsWith, + regExp: new globalThis.RegExp(`^${startsWith}`) + }, + arbitrary: { + constraint: { + patterns: [`^${startsWith}`] + } + }, + ...annotations + } + ) +} + +/** + * Validates at runtime that a string ends with the specified literal suffix. + * + * **Details** + * + * Notes: + * The JSON Schema and arbitrary metadata are built from `${endsWith}$` without + * escaping regexp metacharacters. If the suffix contains regexp syntax, generated + * patterns may not be equivalent to the runtime `endsWith` check. + * + * @category String checks + * @since 4.0.0 + */ +export function isEndsWith(endsWith: string, annotations?: Annotations.Filter) { + const formatted = JSON.stringify(endsWith) + return makeFilter( + (s: string) => s.endsWith(endsWith), + { + expected: `a string ending with ${formatted}`, + meta: { + _tag: "isEndsWith", + endsWith, + regExp: new globalThis.RegExp(`${endsWith}$`) + }, + arbitrary: { + constraint: { + patterns: [`${endsWith}$`] + } + }, + ...annotations + } + ) +} + +/** + * Validates at runtime that a string contains the specified literal substring. + * + * **Details** + * + * Notes: + * The JSON Schema and arbitrary metadata use the substring as a raw regexp + * pattern. If the substring contains regexp syntax, generated patterns may not be + * equivalent to the runtime `includes` check. + * + * @category String checks + * @since 4.0.0 + */ +export function isIncludes(includes: string, annotations?: Annotations.Filter) { + const formatted = JSON.stringify(includes) + return makeFilter( + (s: string) => s.includes(includes), + { + expected: `a string including ${formatted}`, + meta: { + _tag: "isIncludes", + includes, + regExp: new globalThis.RegExp(includes) + }, + arbitrary: { + constraint: { + patterns: [includes] + } + }, + ...annotations + } + ) +} + +const UPPERCASED_PATTERN = "^[^a-z]*$" + +/** + * Validates that a string is unchanged by JavaScript's `toUpperCase()`. + * + * **Details** + * + * This accepts empty strings and characters that do not have lowercase forms, + * such as digits, punctuation, and whitespace. It rejects strings that would + * change when uppercased. + * + * @category String checks + * @since 4.0.0 + */ +export function isUppercased(annotations?: Annotations.Filter) { + return makeFilter( + (s: string) => s.toUpperCase() === s, + { + expected: "a string with all characters in uppercase", + meta: { + _tag: "isUppercased", + regExp: new globalThis.RegExp(UPPERCASED_PATTERN) + }, + arbitrary: { + constraint: { + patterns: [UPPERCASED_PATTERN] + } + }, + ...annotations + } + ) +} + +const LOWERCASED_PATTERN = "^[^A-Z]*$" + +/** + * Validates that a string is unchanged by JavaScript's `toLowerCase()`. + * + * **Details** + * + * This accepts empty strings and characters that do not have uppercase forms, + * such as digits, punctuation, and whitespace. It rejects strings that would + * change when lowercased. + * + * @category String checks + * @since 4.0.0 + */ +export function isLowercased(annotations?: Annotations.Filter) { + return makeFilter( + (s: string) => s.toLowerCase() === s, + { + expected: "a string with all characters in lowercase", + meta: { + _tag: "isLowercased", + regExp: new globalThis.RegExp(LOWERCASED_PATTERN) + }, + arbitrary: { + constraint: { + patterns: [LOWERCASED_PATTERN] + } + }, + ...annotations + } + ) +} + +const CAPITALIZED_PATTERN = "^[^a-z]?.*$" + +/** + * Validates that the first character of a string is unchanged by + * `toUpperCase()`. + * + * **Details** + * + * Empty strings pass. Strings whose first character has no lowercase form, such + * as a digit, punctuation mark, or whitespace, also pass. + * + * @category String checks + * @since 4.0.0 + */ +export function isCapitalized(annotations?: Annotations.Filter) { + return makeFilter( + (s: string) => s.charAt(0).toUpperCase() === s.charAt(0), + { + expected: "a string with the first character in uppercase", + meta: { + _tag: "isCapitalized", + regExp: new globalThis.RegExp(CAPITALIZED_PATTERN) + }, + arbitrary: { + constraint: { + patterns: [CAPITALIZED_PATTERN] + } + }, + ...annotations + } + ) +} + +const UNCAPITALIZED_PATTERN = "^[^A-Z]?.*$" + +/** + * Validates that the first character of a string is unchanged by + * `toLowerCase()`. + * + * **Details** + * + * Empty strings pass. Strings whose first character has no uppercase form, such + * as a digit, punctuation mark, or whitespace, also pass. + * + * @category String checks + * @since 4.0.0 + */ +export function isUncapitalized(annotations?: Annotations.Filter) { + return makeFilter( + (s: string) => s.charAt(0).toLowerCase() === s.charAt(0), + { + expected: "a string with the first character in lowercase", + meta: { + _tag: "isUncapitalized", + regExp: new globalThis.RegExp(UNCAPITALIZED_PATTERN) + }, + arbitrary: { + constraint: { + patterns: [UNCAPITALIZED_PATTERN] + } + }, + ...annotations + } + ) +} + +/** + * Validates that a number is finite (not `Infinity`, `-Infinity`, or `NaN`). + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, but ensures the + * number is valid and finite. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies `noNaN: true` and + * `noInfinity: true` constraints to ensure generated numbers are finite. + * + * @category Number checks + * @since 4.0.0 + */ +export function isFinite(annotations?: Annotations.Filter) { + return makeFilter( + (n: number) => globalThis.Number.isFinite(n), + { + expected: "a finite number", + meta: { + _tag: "isFinite" + }, + arbitrary: { + constraint: { + noInfinity: true, + noNaN: true + } + }, + ...annotations + } + ) +} + +/** + * Creates a greater-than (`>`) check for any ordered type from an + * `Order.Order` instance. + * + * @category Order checks + * @since 4.0.0 + */ +export function makeIsGreaterThan(options: { + readonly order: Order.Order + readonly annotate?: ((exclusiveMinimum: T) => Annotations.Filter) | undefined + readonly formatter?: Formatter | undefined +}) { + const gt = Order.isGreaterThan(options.order) + const formatter = options.formatter ?? format + return (exclusiveMinimum: T, annotations?: Annotations.Filter) => { + return makeFilter( + (input) => gt(input, exclusiveMinimum), + { + expected: `a value greater than ${formatter(exclusiveMinimum)}`, + arbitrary: { + constraint: { + ordered: { + order: options.order, + minimum: exclusiveMinimum, + exclusiveMinimum: true + } + } + }, + ...options.annotate?.(exclusiveMinimum), + ...annotations + } + ) + } +} + +/** + * Creates a greater-than-or-equal-to (`>=`) check for any ordered type from an + * `Order.Order` instance. + * + * @category Order checks + * @since 4.0.0 + */ +export function makeIsGreaterThanOrEqualTo(options: { + readonly order: Order.Order + readonly annotate?: ((exclusiveMinimum: T) => Annotations.Filter) | undefined + readonly formatter?: Formatter | undefined +}) { + const gte = Order.isGreaterThanOrEqualTo(options.order) + const formatter = options.formatter ?? format + return (minimum: T, annotations?: Annotations.Filter) => { + return makeFilter( + (input) => gte(input, minimum), + { + expected: `a value greater than or equal to ${formatter(minimum)}`, + arbitrary: { + constraint: { + ordered: { + order: options.order, + minimum + } + } + }, + ...options.annotate?.(minimum), + ...annotations + } + ) + } +} + +/** + * Creates a less-than (`<`) check for any ordered type from an `Order.Order` + * instance. + * + * @category Order checks + * @since 4.0.0 + */ +export function makeIsLessThan(options: { + readonly order: Order.Order + readonly annotate?: ((exclusiveMaximum: T) => Annotations.Filter) | undefined + readonly formatter?: Formatter | undefined +}) { + const lt = Order.isLessThan(options.order) + const formatter = options.formatter ?? format + return (exclusiveMaximum: T, annotations?: Annotations.Filter) => { + return makeFilter( + (input) => lt(input, exclusiveMaximum), + { + expected: `a value less than ${formatter(exclusiveMaximum)}`, + arbitrary: { + constraint: { + ordered: { + order: options.order, + maximum: exclusiveMaximum, + exclusiveMaximum: true + } + } + }, + ...options.annotate?.(exclusiveMaximum), + ...annotations + } + ) + } +} + +/** + * Creates a less-than-or-equal-to (`<=`) check for any ordered type from an + * `Order.Order` instance. + * + * @category Order checks + * @since 4.0.0 + */ +export function makeIsLessThanOrEqualTo(options: { + readonly order: Order.Order + readonly annotate?: ((exclusiveMaximum: T) => Annotations.Filter) | undefined + readonly formatter?: Formatter | undefined +}) { + const lte = Order.isLessThanOrEqualTo(options.order) + const formatter = options.formatter ?? format + return (maximum: T, annotations?: Annotations.Filter) => { + return makeFilter( + (input) => lte(input, maximum), + { + expected: `a value less than or equal to ${formatter(maximum)}`, + arbitrary: { + constraint: { + ordered: { + order: options.order, + maximum + } + } + }, + ...options.annotate?.(maximum), + ...annotations + } + ) + } +} + +/** + * Creates an inclusive or exclusive range check for any ordered type from an + * `Order.Order` instance. + * + * @category Order checks + * @since 4.0.0 + */ +export function makeIsBetween(deriveOptions: { + readonly order: Order.Order + readonly annotate?: + | ((options: { + readonly minimum: T + readonly maximum: T + readonly exclusiveMinimum?: boolean | undefined + readonly exclusiveMaximum?: boolean | undefined + }) => Annotations.Filter) + | undefined + readonly formatter?: Formatter | undefined +}) { + const greaterThanOrEqualTo = Order.isGreaterThanOrEqualTo(deriveOptions.order) + const greaterThan = Order.isGreaterThan(deriveOptions.order) + const lessThanOrEqualTo = Order.isLessThanOrEqualTo(deriveOptions.order) + const lessThan = Order.isLessThan(deriveOptions.order) + const formatter = deriveOptions.formatter ?? format + return (options: { + readonly minimum: T + readonly maximum: T + readonly exclusiveMinimum?: boolean | undefined + readonly exclusiveMaximum?: boolean | undefined + }, annotations?: Annotations.Filter) => { + const gte = options.exclusiveMinimum ? greaterThan : greaterThanOrEqualTo + const lte = options.exclusiveMaximum ? lessThan : lessThanOrEqualTo + return makeFilter( + (input) => gte(input, options.minimum) && lte(input, options.maximum), + { + expected: `a value between ${formatter(options.minimum)}${options.exclusiveMinimum ? " (excluded)" : ""} and ${ + formatter(options.maximum) + }${options.exclusiveMaximum ? " (excluded)" : ""}`, + arbitrary: { + constraint: { + ordered: { + order: deriveOptions.order, + minimum: options.minimum, + maximum: options.maximum, + ...(options.exclusiveMinimum && { exclusiveMinimum: true }), + ...(options.exclusiveMaximum && { exclusiveMaximum: true }) + } + } + }, + ...deriveOptions.annotate?.(options), + ...annotations + } + ) + } +} + +/** + * Creates a divisibility check for any numeric type from a remainder function + * and a zero value. + * + * @category Numeric checks + * @since 4.0.0 + */ +export function makeIsMultipleOf(options: { + readonly remainder: (input: T, divisor: T) => T + readonly zero: NoInfer + readonly annotate?: ((divisor: T) => Annotations.Filter) | undefined + readonly formatter?: Formatter | undefined +}) { + return (divisor: T, annotations?: Annotations.Filter) => { + const formatter = options.formatter ?? format + return makeFilter( + (input) => options.remainder(input, divisor) === options.zero, + { + expected: `a value that is a multiple of ${formatter(divisor)}`, + ...options.annotate?.(divisor), + ...annotations + } + ) + } +} + +/** + * Validates that a number is greater than the specified value (exclusive). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `exclusiveMinimum` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies an + * `exclusiveMinimum` constraint to ensure generated numbers are greater than + * the specified value. + * + * @category Number checks + * @since 4.0.0 + */ +export const isGreaterThan = makeIsGreaterThan({ + order: Order.Number, + annotate: (exclusiveMinimum) => ({ + meta: { + _tag: "isGreaterThan", + exclusiveMinimum + } + }) +}) + +/** + * Validates that a number is greater than or equal to the specified value + * (inclusive). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `minimum` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `minimum` constraint + * to ensure generated numbers are greater than or equal to the specified value. + * + * @category Number checks + * @since 4.0.0 + */ +export const isGreaterThanOrEqualTo = makeIsGreaterThanOrEqualTo({ + order: Order.Number, + annotate: (minimum) => ({ + meta: { + _tag: "isGreaterThanOrEqualTo", + minimum + } + }) +}) + +/** + * Validates that a number is less than the specified value (exclusive). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `exclusiveMaximum` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies an + * `exclusiveMaximum` constraint to ensure generated numbers are less than the + * specified value. + * + * @category Number checks + * @since 4.0.0 + */ +export const isLessThan = makeIsLessThan({ + order: Order.Number, + annotate: (exclusiveMaximum) => ({ + meta: { + _tag: "isLessThan", + exclusiveMaximum + } + }) +}) + +/** + * Validates that a number is less than or equal to the specified value + * (inclusive). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `maximum` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `maximum` constraint + * to ensure generated numbers are less than or equal to the specified value. + * + * @category Number checks + * @since 4.0.0 + */ +export const isLessThanOrEqualTo = makeIsLessThanOrEqualTo({ + order: Order.Number, + annotate: (maximum) => ({ + meta: { + _tag: "isLessThanOrEqualTo", + maximum + } + }) +}) + +/** + * Validates that a number is within a specified range. The range boundaries can + * be inclusive or exclusive based on the provided options. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to `minimum`/`maximum` or `exclusiveMinimum`/`exclusiveMaximum` + * constraints in JSON Schema, depending on the options provided. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies `minimum` and + * `maximum` constraints with optional `exclusiveMinimum` and + * `exclusiveMaximum` flags to ensure generated numbers fall within the + * specified range. + * + * @category Number checks + * @since 4.0.0 + */ +export const isBetween = makeIsBetween({ + order: Order.Number, + annotate: (options) => { + return { + meta: { + _tag: "isBetween", + ...options + } + } + } +}) + +/** + * Validates that a number is a multiple of the specified divisor. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `multipleOf` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies constraints to ensure + * generated numbers are multiples of the specified divisor. + * + * @category Number checks + * @since 4.0.0 + */ +export const isMultipleOf = makeIsMultipleOf({ + remainder, + zero: 0, + annotate: (divisor) => ({ + expected: `a value that is a multiple of ${divisor}`, + meta: { + _tag: "isMultipleOf", + divisor + } + }) +}) + +/** + * Validates that a number is a safe integer (within the safe integer range + * that can be exactly represented in JavaScript). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `type: "integer"` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies an `integer: true` + * constraint to ensure generated numbers are integers. + * + * @category Integer checks + * @since 4.0.0 + */ +export function isInt(annotations?: Annotations.Filter) { + return makeFilter( + (n: number) => globalThis.Number.isSafeInteger(n), + { + expected: "an integer", + meta: { + _tag: "isInt" + }, + arbitrary: { + constraint: { + integer: true + } + }, + ...annotations + } + ) +} + +/** + * Validates that a number is a 32-bit signed integer (range: -2,147,483,648 to + * 2,147,483,647). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `format: "int32"` constraint in OpenAPI 3.1, + * or `minimum`/`maximum` constraints in other JSON Schema targets. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies integer and range + * constraints to ensure generated numbers are 32-bit signed integers. + * + * @category Integer checks + * @since 4.0.0 + */ +export function isInt32(annotations?: Annotations.Filter) { + return new SchemaAST.FilterGroup( + [ + isInt(), + isBetween({ minimum: -2147483648, maximum: 2147483647 }) + ], + { + expected: "a 32-bit integer", + ...annotations + } + ) +} + +/** + * Validates that a number is a 32-bit unsigned integer (range: 0 to + * 4,294,967,295). + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `format: "uint32"` constraint in OpenAPI 3.1, + * or `minimum`/`maximum` constraints in other JSON Schema targets. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies integer and range + * constraints to ensure generated numbers are 32-bit unsigned integers. + * + * @category Integer checks + * @since 4.0.0 + */ +export function isUint32(annotations?: Annotations.Filter) { + return new SchemaAST.FilterGroup( + [ + isInt(), + isBetween({ minimum: 0, maximum: 4294967295 }) + ], + { + expected: "a 32-bit unsigned integer", + ...annotations + } + ) +} + +/** + * Validates that a Date object represents a valid date (not an invalid date + * like `new Date("invalid")`). + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as JSON Schema + * validates date strings, not Date objects. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `valid: true` + * constraint to ensure generated Date objects are valid. + * + * @category Date checks + * @since 4.0.0 + */ +export function isDateValid(annotations?: Annotations.Filter) { + return makeFilter( + (date) => !isNaN(date.getTime()), + { + expected: "a valid date", + meta: { + _tag: "isDateValid" + }, + arbitrary: { + constraint: { + valid: true + } + }, + ...annotations + } + ) +} + +/** + * Validates that a Date is greater than the specified value (exclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `min` constraint of + * one millisecond after the specified value to ensure generated Date objects are + * greater than it. + * + * @category Date checks + * @since 4.0.0 + */ +export const isGreaterThanDate = makeIsGreaterThan({ + order: Order.Date, + annotate: (exclusiveMinimum) => ({ + meta: { + _tag: "isGreaterThanDate", + exclusiveMinimum + } + }) +}) + +/** + * Validates that a Date is greater than or equal to the specified date + * (inclusive). + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as JSON Schema + * validates date strings, not Date objects. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `min` constraint + * to ensure generated Date objects are greater than or equal to the specified + * date. + * + * @category Date checks + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToDate = makeIsGreaterThanOrEqualTo({ + order: Order.Date, + annotate: (minimum) => ({ + meta: { + _tag: "isGreaterThanOrEqualToDate", + minimum + } + }) +}) + +/** + * Validates that a Date is less than the specified value (exclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `max` constraint of + * one millisecond before the specified value to ensure generated Date objects + * are less than it. + * + * @category Date checks + * @since 4.0.0 + */ +export const isLessThanDate = makeIsLessThan({ + order: Order.Date, + annotate: (exclusiveMaximum) => ({ + meta: { + _tag: "isLessThanDate", + exclusiveMaximum + } + }) +}) + +/** + * Validates that a Date is less than or equal to the specified date + * (inclusive). + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as JSON Schema + * validates date strings, not Date objects. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `max` constraint + * to ensure generated Date objects are less than or equal to the specified + * date. + * + * @category Date checks + * @since 4.0.0 + */ +export const isLessThanOrEqualToDate = makeIsLessThanOrEqualTo({ + order: Order.Date, + annotate: (maximum) => ({ + meta: { + _tag: "isLessThanOrEqualToDate", + maximum + } + }) +}) + +/** + * Validates that a Date is within a specified range. The range boundaries can + * be inclusive or exclusive based on the provided options. + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as JSON Schema + * validates date strings, not Date objects. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies `min` and `max` + * constraints to ensure generated Date objects fall within the specified range, + * shifting exclusive bounds by one millisecond. + * + * @category Date checks + * @since 4.0.0 + */ +export const isBetweenDate = makeIsBetween({ + order: Order.Date, + annotate: (options) => ({ + meta: { + _tag: "isBetweenDate", + ...options + } + }) +}) + +/** + * Validates that a BigInt is greater than the specified value (exclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `min` constraint of + * `exclusiveMinimum + 1n` to ensure generated BigInts are greater than the + * specified value. + * + * @category BigInt checks + * @since 4.0.0 + */ +export const isGreaterThanBigInt = makeIsGreaterThan({ + order: Order.BigInt, + annotate: (exclusiveMinimum) => ({ + meta: { + _tag: "isGreaterThanBigInt", + exclusiveMinimum + } + }) +}) + +/** + * Validates that a BigInt is greater than or equal to the specified value + * (inclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `min` constraint + * to ensure generated BigInt values are greater than or equal to the specified + * value. + * + * @category BigInt checks + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToBigInt = makeIsGreaterThanOrEqualTo({ + order: Order.BigInt, + annotate: (minimum) => ({ + meta: { + _tag: "isGreaterThanOrEqualToBigInt", + minimum + } + }) +}) + +/** + * Validates that a BigInt is less than the specified value (exclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `max` constraint of + * `exclusiveMaximum - 1n` to ensure generated BigInts are less than the + * specified value. + * + * @category BigInt checks + * @since 4.0.0 + */ +export const isLessThanBigInt = makeIsLessThan({ + order: Order.BigInt, + annotate: (exclusiveMaximum) => ({ + meta: { + _tag: "isLessThanBigInt", + exclusiveMaximum + } + }) +}) + +/** + * Validates that a BigInt is less than or equal to the specified value + * (inclusive). + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `max` constraint + * to ensure generated BigInt values are less than or equal to the specified + * value. + * + * @category BigInt checks + * @since 4.0.0 + */ +export const isLessThanOrEqualToBigInt = makeIsLessThanOrEqualTo({ + order: Order.BigInt, + annotate: (maximum) => ({ + meta: { + _tag: "isLessThanOrEqualToBigInt", + maximum + } + }) +}) + +/** + * Validates that a BigInt is within a specified range. The range boundaries can + * be inclusive or exclusive based on the provided options. + * + * **Details** + * + * Arbitrary: + * + * When generating test data with fast-check, this applies `min` and `max` + * constraints to ensure generated BigInt values fall within the specified + * range. + * + * @category BigInt checks + * @since 4.0.0 + */ +export const isBetweenBigInt = makeIsBetween({ + order: Order.BigInt, + annotate: (options) => ({ + meta: { + _tag: "isBetweenBigInt", + ...options + } + }) +}) + +/** + * Validates that a BigDecimal is greater than the specified value (exclusive). + * + * @category BigDecimal checks + * @since 4.0.0 + */ +export const isGreaterThanBigDecimal = makeIsGreaterThan({ + order: BigDecimal_.Order, + formatter: (bd) => BigDecimal_.format(bd) +}) + +/** + * Validates that a BigDecimal is greater than or equal to the specified value + * (inclusive). + * + * @category BigDecimal checks + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToBigDecimal = makeIsGreaterThanOrEqualTo({ + order: BigDecimal_.Order, + formatter: (bd) => BigDecimal_.format(bd) +}) + +/** + * Validates that a BigDecimal is less than the specified value (exclusive). + * + * @category BigDecimal checks + * @since 4.0.0 + */ +export const isLessThanBigDecimal = makeIsLessThan({ + order: BigDecimal_.Order, + formatter: (bd) => BigDecimal_.format(bd) +}) + +/** + * Validates that a BigDecimal is less than or equal to the specified value + * (inclusive). + * + * @category BigDecimal checks + * @since 4.0.0 + */ +export const isLessThanOrEqualToBigDecimal = makeIsLessThanOrEqualTo({ + order: BigDecimal_.Order, + formatter: (bd) => BigDecimal_.format(bd) +}) + +/** + * Validates that a `BigDecimal` is within a specified range. + * + * **Details** + * + * The minimum and maximum boundaries are inclusive by default. Pass + * `exclusiveMinimum` or `exclusiveMaximum` to exclude either boundary. + * + * @category BigDecimal checks + * @since 4.0.0 + */ +export const isBetweenBigDecimal = makeIsBetween({ + order: BigDecimal_.Order, + formatter: (bd) => BigDecimal_.format(bd) +}) + +/** + * Validates that a value has at least the specified length. Works with strings + * and arrays. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `minLength` constraint for strings or the + * `minItems` constraint for arrays in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `minLength` + * constraint to ensure generated strings or arrays have at least the required + * length. + * + * **Example** (Checking minimum length) + * + * ```ts + * import { Schema } from "effect" + * + * const NonEmptyStringSchema = Schema.String.check(Schema.isMinLength(1)) + * const NonEmptyArraySchema = Schema.Array(Schema.Number).check(Schema.isMinLength(1)) + * ``` + * + * @category Length checks + * @since 4.0.0 + */ +export function isMinLength(minLength: number, annotations?: Annotations.Filter) { + minLength = Math.max(0, Math.floor(minLength)) + return makeFilter<{ readonly length: number }>( + (input) => input.length >= minLength, + { + expected: `a value with a length of at least ${minLength}`, + meta: { + _tag: "isMinLength", + minLength + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength + } + }, + ...annotations + } + ) +} + +/** + * Validates that a value has at least one element. Works with strings and arrays. + * This is equivalent to `isMinLength(1)`. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `minLength: 1` constraint for strings or the + * `minItems: 1` constraint for arrays in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `minLength: 1` + * constraint to ensure generated strings or arrays are non-empty. + * + * @category Length checks + * @since 4.0.0 + */ +export function isNonEmpty(annotations?: Annotations.Filter) { + return isMinLength(1, annotations) +} + +/** + * Validates that a value has at most the specified length. Works with strings + * and arrays. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `maxLength` constraint for strings or the + * `maxItems` constraint for arrays in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a `maxLength` + * constraint to ensure generated strings or arrays have at most the required + * length. + * + * @category Length checks + * @since 4.0.0 + */ +export function isMaxLength(maxLength: number, annotations?: Annotations.Filter) { + maxLength = Math.max(0, Math.floor(maxLength)) + return makeFilter<{ readonly length: number }>( + (input) => input.length <= maxLength, + { + expected: `a value with a length of at most ${maxLength}`, + meta: { + _tag: "isMaxLength", + maxLength + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + maxLength + } + }, + ...annotations + } + ) +} + +/** + * Validates that a value's length is within the specified range. Works with + * strings and arrays. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to `minLength`/`maxLength` constraints for strings + * or `minItems`/`maxItems` constraints for arrays in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies `minLength` and + * `maxLength` constraints to ensure generated strings or arrays have a length + * within the specified range. + * + * @category Length checks + * @since 4.0.0 + */ +export function isLengthBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { + minimum = Math.max(0, Math.floor(minimum)) + maximum = Math.max(0, Math.floor(maximum)) + return makeFilter<{ readonly length: number }>( + (input) => input.length >= minimum && input.length <= maximum, + { + expected: minimum === maximum + ? `a value with a length of ${minimum}` + : `a value with a length between ${minimum} and ${maximum}`, + meta: { + _tag: "isLengthBetween", + minimum, + maximum + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength: minimum, + maxLength: maximum + } + }, + ...annotations + } + ) +} + +/** + * Validates that a value has at least the specified size. Works with values + * that have a `size` property, such as `Set` or `Map`. + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as it applies to + * values with a `size` property rather than standard JSON Schema types. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a node-local + * `minLength` constraint. Generators for values with a final `.size`, such as + * sets and maps, interpret it as final cardinality. + * + * @category Size checks + * @since 4.0.0 + */ +export function isMinSize(minSize: number, annotations?: Annotations.Filter) { + minSize = Math.max(0, Math.floor(minSize)) + return makeFilter<{ readonly size: number }>( + (input) => input.size >= minSize, + { + expected: `a value with a size of at least ${minSize}`, + meta: { + _tag: "isMinSize", + minSize + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength: minSize + } + }, + ...annotations + } + ) +} + +/** + * Validates that a value has at most the specified size. Works with values + * that have a `size` property, such as `Set` or `Map`. + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as it applies to + * values with a `size` property rather than standard JSON Schema types. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a node-local + * `maxLength` constraint. Generators for values with a final `.size`, such as + * sets and maps, interpret it as final cardinality. + * + * @category Size checks + * @since 4.0.0 + */ +export function isMaxSize(maxSize: number, annotations?: Annotations.Filter) { + maxSize = Math.max(0, Math.floor(maxSize)) + return makeFilter<{ readonly size: number }>( + (input) => input.size <= maxSize, + { + expected: `a value with a size of at most ${maxSize}`, + meta: { + _tag: "isMaxSize", + maxSize + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + maxLength: maxSize + } + }, + ...annotations + } + ) +} + +/** + * Validates that a value's size is within the specified range. Works with + * values that have a `size` property, such as `Set` or `Map`. + * + * **Details** + * + * JSON Schema: + * + * This check does not have a direct JSON Schema equivalent, as it applies to + * values with a `size` property rather than standard JSON Schema types. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies node-local + * `minLength` and `maxLength` constraints. Generators for values with a final + * `.size`, such as sets and maps, interpret them as final cardinality. + * + * @category Size checks + * @since 4.0.0 + */ +export function isSizeBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { + minimum = Math.max(0, Math.floor(minimum)) + maximum = Math.max(0, Math.floor(maximum)) + return makeFilter<{ readonly size: number }>( + (input) => input.size >= minimum && input.size <= maximum, + { + expected: minimum === maximum + ? `a value with a size of ${minimum}` + : `a value with a size between ${minimum} and ${maximum}`, + meta: { + _tag: "isSizeBetween", + minimum, + maximum + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength: minimum, + maxLength: maximum + } + }, + ...annotations + } + ) +} + +/** + * Validates that an object contains at least the specified number of + * properties. This includes both string and symbol keys when counting + * properties. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `minProperties` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a node-local + * `minLength` constraint. Object generators interpret it as the final number + * of own properties. + * + * @category Object checks + * @since 4.0.0 + */ +export function isMinProperties(minProperties: number, annotations?: Annotations.Filter) { + minProperties = Math.max(0, Math.floor(minProperties)) + return makeFilter( + (input) => Reflect.ownKeys(input).length >= minProperties, + { + expected: `a value with at least ${minProperties === 1 ? "1 entry" : `${minProperties} entries`}`, + meta: { + _tag: "isMinProperties", + minProperties + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength: minProperties + } + }, + ...annotations + } + ) +} + +/** + * Validates that an object contains at most the specified number of properties. + * This includes both string and symbol keys when counting properties. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to the `maxProperties` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies a node-local + * `maxLength` constraint. Object generators interpret it as the final number + * of own properties. + * + * @category Object checks + * @since 4.0.0 + */ +export function isMaxProperties(maxProperties: number, annotations?: Annotations.Filter) { + maxProperties = Math.max(0, Math.floor(maxProperties)) + return makeFilter( + (input) => Reflect.ownKeys(input).length <= maxProperties, + { + expected: `a value with at most ${maxProperties === 1 ? "1 entry" : `${maxProperties} entries`}`, + meta: { + _tag: "isMaxProperties", + maxProperties + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + maxLength: maxProperties + } + }, + ...annotations + } + ) +} + +/** + * Validates that an object contains between `minimum` and `maximum` properties (inclusive). + * This includes both string and symbol keys when counting properties. + * + * **Details** + * + * JSON Schema: + * + * This check corresponds to `minProperties` and `maxProperties` + * constraints in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies node-local + * `minLength` and `maxLength` constraints. Object generators interpret them as + * the final number of own properties. + * + * @category Object checks + * @since 4.0.0 + */ +export function isPropertiesLengthBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { + minimum = Math.max(0, Math.floor(minimum)) + maximum = Math.max(0, Math.floor(maximum)) + return makeFilter( + (input) => Reflect.ownKeys(input).length >= minimum && Reflect.ownKeys(input).length <= maximum, + { + expected: minimum === maximum + ? `a value with exactly ${minimum === 1 ? "1 entry" : `${minimum} entries`}` + : `a value with between ${minimum} and ${maximum} entries`, + meta: { + _tag: "isPropertiesLengthBetween", + minimum, + maximum + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + arbitrary: { + constraint: { + minLength: minimum, + maxLength: maximum + } + }, + ...annotations + } + ) +} + +/** + * Validates that every own property key of an object satisfies the encoded side + * of the provided key schema. + * + * **Details** + * + * This check uses `Reflect.ownKeys`, so symbol keys are validated in addition to + * string property names. + * + * JSON Schema: + * For string property names, this corresponds to the `propertyNames` constraint + * in JSON Schema. + * + * @category Object checks + * @since 4.0.0 + */ +export function isPropertyNames(keySchema: Constraint, annotations?: Annotations.Filter) { + const propertyNames = toEncoded(keySchema) + const parser = SchemaParser._issue(propertyNames.ast) + return makeFilter( + (input, ast, options) => { + const keys = Reflect.ownKeys(input) + const issues: Array = [] + for (const key of keys) { + const issue = parser(key, options) + if (issue !== undefined) { + issues.push(new SchemaIssue.Pointer([key], issue)) + if (options.errors === "first") break + } + } + if (Arr.isArrayNonEmpty(issues)) { + return new SchemaIssue.Composite(ast, Option_.some(input), issues) + } + return true + }, + { + expected: "an object with property names matching the schema", + meta: { + _tag: "isPropertyNames", + propertyNames: propertyNames.ast + }, + [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + ...annotations + } + ) +} + +/** + * Validates that all items in an array are unique according to Effect equality. + * + * **Details** + * + * JSON Schema: + * This check corresponds to the `uniqueItems: true` constraint in JSON Schema. + * + * Arbitrary: + * When generating test data with fast-check, this applies a node-local + * `unique: true` constraint. Array generators translate it to `fast-check` + * `uniqueArray` using Effect equality. + * + * @category Array checks + * @since 4.0.0 + */ +export function isUnique(annotations?: Annotations.Filter) { + const equivalence = Equal.asEquivalence() + return makeFilter>( + (input) => Arr.dedupeWith(input, equivalence).length === input.length, + { + expected: "an array with unique items", + meta: { + _tag: "isUnique" + }, + arbitrary: { + constraint: { + unique: true + } + }, + ...annotations + } + ) +} + +// ----------------------------------------------------------------------------- +// Built-in Schemas +// ----------------------------------------------------------------------------- + +/** + * Type-level representation of {@link NonEmptyString}. + * + * @category string + * @since 3.10.0 + */ +export interface NonEmptyString extends String { + readonly "Rebuild": NonEmptyString +} + +/** + * Schema for non-empty strings. Validates that a string has at least one + * character. + * + * @category string + * @since 3.10.0 + */ +export const NonEmptyString: NonEmptyString = String.check(isNonEmpty()) + +/** + * Type-level representation of {@link Char}. + * + * @category string + * @since 3.10.0 + */ +export interface Char extends String { + readonly "Rebuild": Char +} + +/** + * Schema for strings whose JavaScript `length` is exactly `1`. + * + * **When to use** + * + * Use to validate string values that must have `length === 1`. + * + * **Gotchas** + * + * This schema uses JavaScript `String.length`, so visible characters made from + * multiple UTF-16 code units do not satisfy `length === 1`. + * + * @see {@link String} for unconstrained string values + * @see {@link NonEmptyString} for strings with length greater than zero + * @see {@link isLengthBetween} for the underlying length check + * + * @category string + * @since 3.10.0 + */ +export const Char: Char = String.check(isLengthBetween(1, 1)) + +/** + * Type-level representation returned by {@link Option}. + * + * @category Option + * @since 3.10.0 + */ +export interface Option extends + declareConstructor< + Option_.Option, + Option_.Option, + readonly [A], + OptionIso + > +{ + readonly "Rebuild": Option + readonly value: A +} + +/** + * Iso representation used for `Option` schemas. + * + * **Details** + * + * `None` is represented as `{ _tag: "None" }`, while `Some` is represented as + * `{ _tag: "Some", value }` using the wrapped schema's `Iso` type. + * + * @category Option + * @since 4.0.0 + */ +export type OptionIso = + | { readonly _tag: "None" } + | { readonly _tag: "Some"; readonly value: A["Iso"] } + +/** + * Schema for `Option` values. + * + * @category Option + * @since 3.10.0 + */ +export function Option(value: A): Option { + const schema = declareConstructor< + Option_.Option, + Option_.Option, + OptionIso + >()( + [value], + ([value]) => (input, ast, options) => { + if (Option_.isOption(input)) { + if (Option_.isNone(input)) { + return Effect.succeedNone + } + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(value)(input.value, options), + { + onSuccess: Option_.some, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["value"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + }, + { + typeConstructor: { + _tag: "effect/Option" + }, + generation: { + runtime: `Schema.Option(?)`, + Type: `Option.Option`, + importDeclaration: `import * as Option from "effect/Option"` + }, + expected: "Option", + toCodec: ([value]) => + link>()( + Union([ + Struct({ _tag: Literal("Some"), value }), + Struct({ _tag: Literal("None") }) + ]), + SchemaTransformation.transform({ + decode: (e) => e._tag === "None" ? Option_.none() : Option_.some(e.value), + encode: (o) => (Option_.isSome(o) ? { _tag: "Some", value: o.value } as const : { _tag: "None" } as const) + }) + ), + toArbitrary: ([value]) => (fc, ctx) => { + const terminal = fc.constant(Option_.none()) + const arbitrary = fc.oneof( + terminal, + value.arbitrary.map(Option_.some) + ) + return withRecursion(fc, ctx, terminal, arbitrary) + }, + toEquivalence: ([value]) => Option_.makeEquivalence(value), + toFormatter: ([value]) => + Option_.match({ + onNone: () => "none()", + onSome: (t) => `some(${value(t)})` + }) + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation returned by {@link OptionFromNullOr}. + * + * @category Option + * @since 3.10.0 + */ +export interface OptionFromNullOr extends decodeTo>, NullOr> { + readonly "Rebuild": OptionFromNullOr +} + +/** + * Decodes a nullable, required value `T` to a required `Option` value. + * + * **Details** + * + * Decoding maps `null` to `None` and all other values to `Some`. Encoding maps + * `None` to `null` and maps `Some` to its value. + * + * @category Option + * @since 3.10.0 + */ +export function OptionFromNullOr(schema: S): OptionFromNullOr { + return NullOr(schema).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.optionFromNullOr() + )) +} + +/** + * Type-level representation returned by {@link OptionFromUndefinedOr}. + * + * @category Option + * @since 3.10.0 + */ +export interface OptionFromUndefinedOr extends decodeTo>, UndefinedOr> { + readonly "Rebuild": OptionFromUndefinedOr +} + +/** + * Decodes a required value that may be `undefined` to a required `Option` + * value. + * + * **Details** + * + * Decoding maps `undefined` to `None` and all other values to `Some`. Encoding + * maps `None` to `undefined` and maps `Some` to its value. + * + * @category Option + * @since 3.10.0 + */ +export function OptionFromUndefinedOr(schema: S): OptionFromUndefinedOr { + return UndefinedOr(schema).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.optionFromUndefinedOr() + )) +} + +/** + * Type-level representation returned by {@link OptionFromNullishOr}. + * + * @category Option + * @since 3.10.0 + */ +export interface OptionFromNullishOr extends decodeTo>, NullishOr> { + readonly "Rebuild": OptionFromNullishOr +} + +/** + * Decodes a nullish value `T` to a required `Option` value. + * + * **Details** + * + * Decoding maps `null` and `undefined` to `None` and all other values to + * `Some`. Encoding maps `None` to `null` or `undefined` depending on + * `options.onNoneEncoding`, which defaults to `undefined`, and maps `Some` to + * its value. + * + * @category Option + * @since 3.10.0 + */ +export function OptionFromNullishOr( + schema: S, + options?: { + onNoneEncoding: null | undefined + } +): OptionFromNullishOr { + return NullishOr(schema).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.optionFromNullishOr(options) + )) +} + +/** + * Type-level representation returned by {@link OptionFromOptionalKey}. + * + * @category Option + * @since 4.0.0 + */ +export interface OptionFromOptionalKey extends decodeTo>, optionalKey> { + readonly "Rebuild": OptionFromOptionalKey +} + +/** + * Decodes an optional value `A` to a required `Option` value. + * + * **Details** + * + * Decoding maps a missing key to `None` and a present value to `Some`. + * Encoding maps `None` to a missing key and maps `Some` to its value. + * + * @category Option + * @since 4.0.0 + */ +export function OptionFromOptionalKey(schema: S): OptionFromOptionalKey { + return optionalKey(schema).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.optionFromOptionalKey() + )) +} + +/** + * Type-level representation returned by {@link OptionFromOptional}. + * + * @category Option + * @since 4.0.0 + */ +export interface OptionFromOptional extends decodeTo>, optional> { + readonly "Rebuild": OptionFromOptional +} + +/** + * Decodes an optional or `undefined` value `A` to a required `Option` + * value. + * + * **Details** + * + * Decoding maps a missing key or a present `undefined` value to `None`, and + * maps all other values to `Some`. Encoding maps `None` to a missing key and + * maps `Some` to its value. + * + * @category Option + * @since 4.0.0 + */ +export function OptionFromOptional(schema: S): OptionFromOptional { + return optional(schema).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.optionFromOptional() + )) +} + +/** + * Type-level representation returned by {@link OptionFromOptionalNullOr}. + * + * @category Option + * @since 4.0.0 + */ +export interface OptionFromOptionalNullOr + extends decodeTo>, optional>> +{ + readonly "Rebuild": OptionFromOptionalNullOr +} + +/** + * Decodes an optional or `null` or `undefined` value `A` to a required `Option` + * value. + * + * **Details** + * + * Decoding maps a missing key, `undefined`, or `null` to `None`, and maps all + * other values to `Some`. Encoding maps `Some` to its value. `None` is encoded + * according to `options.onNoneEncoding`: `"omit"` encodes a missing key, + * `null` encodes `null`, and `undefined` encodes `undefined`. + * + * @category Option + * @since 4.0.0 + */ +export function OptionFromOptionalNullOr( + schema: S, + options?: { + readonly onNoneEncoding: "omit" | null | undefined + } +): OptionFromOptionalNullOr { + const onNoneEncoding = options === undefined ? "omit" : options.onNoneEncoding + const noneValue = onNoneEncoding === null + ? null as S["Type"] | null | undefined + : undefined as S["Type"] | null | undefined + return optional(NullOr(schema)).pipe(decodeTo( + Option(toType(schema)), + SchemaTransformation.transformOptional, S["Type"] | null | undefined>({ + decode: (oe) => oe.pipe(Option_.filter(Predicate.isNotNullish), Option_.some), + encode: onNoneEncoding === "omit" + ? Option_.flatten + : (ot) => Option_.some(Option_.getOrElse(Option_.flatten(ot), () => noneValue)) + }) + )) +} + +/** + * Type-level representation returned by {@link Result}. + * + * @category schemas + * @since 4.0.0 + */ +export interface Result extends + declareConstructor< + Result_.Result, + Result_.Result, + readonly [A, E], + ResultIso + > +{ + readonly "Rebuild": Result + readonly success: A + readonly failure: E +} + +/** + * Iso representation used for `Result` schemas. + * + * **Details** + * + * Successful results are represented as `{ _tag: "Success", success }`, while + * failed results are represented as `{ _tag: "Failure", failure }`. + * + * @category schemas + * @since 4.0.0 + */ +export type ResultIso = + | { readonly _tag: "Success"; readonly success: A["Iso"] } + | { readonly _tag: "Failure"; readonly failure: E["Iso"] } + +/** + * Schema for `Result` values. + * + * @category schemas + * @since 4.0.0 + */ +export function Result( + success: A, + failure: E +): Result { + const schema = declareConstructor< + Result_.Result, + Result_.Result, + ResultIso + >()( + [success, failure], + ([success, failure]) => (input, ast, options) => { + if (!Result_.isResult(input)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Success": + return Effect.mapBothEager(SchemaParser.decodeEffect(success)(input.success, options), { + onSuccess: Result_.succeed, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["success"], issue)]) + }) + case "Failure": + return Effect.mapBothEager(SchemaParser.decodeEffect(failure)(input.failure, options), { + onSuccess: Result_.fail, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["failure"], issue)]) + }) + } + }, + { + typeConstructor: { + _tag: "effect/Result" + }, + generation: { + runtime: `Schema.Result(?, ?)`, + Type: `Result.Result`, + importDeclaration: `import * as Result from "effect/Result"` + }, + expected: "Result", + toCodec: ([success, failure]) => + link>()( + Union([ + Struct({ _tag: Literal("Success"), success }), + Struct({ _tag: Literal("Failure"), failure }) + ]), + SchemaTransformation.transform({ + decode: (e): Result_.Result => + e._tag === "Success" ? Result_.succeed(e.success) : Result_.fail(e.failure), + encode: (r) => + Result_.isSuccess(r) + ? { _tag: "Success", success: r.success } as const + : { _tag: "Failure", failure: r.failure } as const + }) + ), + toArbitrary: ([success, failure]) => (fc, ctx) => { + const terminal = oneOfArbitraries( + fc, + success.terminal?.map((a): Result_.Result => Result_.succeed(a)), + failure.terminal?.map((e): Result_.Result => Result_.fail(e)) + ) + const arbitrary = fc.oneof( + success.arbitrary.map((a): Result_.Result => Result_.succeed(a)), + failure.arbitrary.map((e): Result_.Result => Result_.fail(e)) + ) + return withRecursion(fc, ctx, terminal, arbitrary) + }, + toEquivalence: ([success, failure]) => Result_.makeEquivalence(success, failure), + toFormatter: ([success, failure]) => + Result_.match({ + onSuccess: (t) => `success(${success(t)})`, + onFailure: (t) => `failure(${failure(t)})` + }) + } + ) + return make(schema.ast, { success, failure }) +} + +/** + * Type-level representation returned by {@link Redacted}. + * + * @category Redacted + * @since 3.10.0 + */ +export interface Redacted extends + declareConstructor< + Redacted_.Redacted, + Redacted_.Redacted, + readonly [S] + > +{ + readonly "Rebuild": Redacted + readonly value: S +} + +/** + * Schema for values that hide sensitive information from error output and + * inspection. + * + * **Details** + * + * If the wrapped schema fails, the issue will be redacted to prevent both + * the actual value and the schema details from being exposed. + * + * Options: + * + * - `label`: When provided, the schema will behave as follows: + * - Values will be validated against the label in addition to the wrapped schema + * - The default JSON serializer will deserialize into a `Redacted` instance with the label + * - The arbitrary generator will produce a `Redacted` instance with the label + * - The formatter will return the label + * - `disallowJsonEncode`: When set to `true`, when attempting to encode a `Redacted` instance + * into JSON, it will fail with an error. This is useful when the wrapped schema is + * sensitive and should not be exposed in JSON. + * + * @see {@link RedactedFromValue} for decoding raw values and wrapping them in `Redacted`. + * @category Redacted + * @since 3.10.0 + */ +export function Redacted(value: S, options?: { + readonly label?: string | undefined + readonly disallowJsonEncode?: boolean | undefined +}): Redacted { + const decodeLabel = typeof options?.label === "string" + ? SchemaParser.decodeUnknownEffect(Literal(options.label)) + : undefined + const schema = declareConstructor, Redacted_.Redacted>()( + [value], + ([value]) => (input, ast, poptions) => { + if (Redacted_.isRedacted(input)) { + const label: Effect.Effect = decodeLabel !== undefined + ? Effect.mapErrorEager( + decodeLabel(input.label, poptions), + (issue) => new SchemaIssue.Pointer(["label"], issue) + ) + : Effect.void + return Effect.flatMapEager( + label, + () => + Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(value)(Redacted_.value(input), poptions), + { + onSuccess: () => input, + onFailure: (/** ignore the actual issue because of security reasons */) => { + const oinput = Option_.some(input) + return new SchemaIssue.Composite(ast, oinput, [ + new SchemaIssue.Pointer(["value"], new SchemaIssue.InvalidValue(oinput)) + ]) + } + } + ) + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + }, + { + typeConstructor: { + _tag: "effect/Redacted", + options + }, + generation: { + runtime: options !== undefined ? `Schema.Redacted(?, ${format(options)})` : `Schema.Redacted(?)`, + Type: `Redacted.Redacted`, + importDeclaration: `import * as Redacted from "effect/Redacted"` + }, + expected: "Redacted", + toCodecJson: ([value]) => + link>()( + redact(value), + { + decode: SchemaGetter.transform((e) => Redacted_.make(e, { label: options?.label })), + encode: options?.disallowJsonEncode ? + SchemaGetter.forbidden((oe) => + "Cannot serialize Redacted" + + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") + ) : + SchemaGetter.transform(Redacted_.value) + } + ), + toArbitrary: ([value]) => () => ({ + arbitrary: value.arbitrary.map((a) => Redacted_.make(a, { label: options?.label })), + terminal: value.terminal?.map((a) => Redacted_.make(a, { label: options?.label })) + }), + toFormatter: () => globalThis.String, + toEquivalence: ([value]) => Redacted_.makeEquivalence(value) + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation returned by {@link RedactedFromValue}. + * + * @category Redacted + * @since 4.0.0 + */ +export interface RedactedFromValue + extends decodeTo>, middlewareDecoding> +{ + readonly "Rebuild": RedactedFromValue +} + +/** + * Middleware that wraps decoded errors in `Redacted`, preventing sensitive + * schema details from leaking in error messages. + * + * @category Redacted + * @since 4.0.0 + */ +export function redact(schema: S): middlewareDecoding { + return middlewareDecoding(Effect.mapErrorEager(SchemaIssue.redact))(schema) +} + +/** + * Decodes a value and wraps it in `Redacted`. Unlike {@link Redacted} which + * expects the input to already be a `Redacted` instance, this schema decodes + * the raw value and wraps it. + * + * @see {@link Redacted} for schemas whose input is already a `Redacted` value. + * @category Redacted + * @since 4.0.0 + */ +export function RedactedFromValue(value: S, options?: { + readonly label?: string | undefined + readonly disallowEncode?: boolean | undefined +}): RedactedFromValue { + return redact(value).pipe( + decodeTo( + Redacted(toType(value), { + label: options?.label, + disallowJsonEncode: options?.disallowEncode + }), + { + decode: SchemaGetter.transform((t) => Redacted_.make(t, { label: options?.label })), + encode: options?.disallowEncode ? + SchemaGetter.forbidden((oe) => + "Cannot encode Redacted" + + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") + ) : + SchemaGetter.transform(Redacted_.value) + } + ) + ) +} + +/** + * Type-level representation returned by {@link CauseReason}. + * + * @category CauseReason + * @since 4.0.0 + */ +export interface CauseReason extends + declareConstructor< + Cause_.Reason, + Cause_.Reason, + readonly [E, D], + CauseReasonIso + > +{ + readonly "Rebuild": CauseReason + readonly error: E + readonly defect: D +} + +/** + * Iso representation used for `CauseReason` schemas. + * + * **Details** + * + * Failures are represented with a `Fail` tag and encoded error, defects with a + * `Die` tag and encoded defect, and interrupts with an optional `fiberId`. + * + * @category CauseReason + * @since 4.0.0 + */ +export type CauseReasonIso = { + readonly _tag: "Fail" + readonly error: E["Iso"] +} | { + readonly _tag: "Die" + readonly error: D["Iso"] +} | { + readonly _tag: "Interrupt" + readonly fiberId: number | undefined +} + +/** + * Creates a schema for `Cause.Reason` values using separate schemas for typed + * failures and unexpected defects. + * + * **When to use** + * + * Use when serializing or decoding individual cause reasons separately from a + * full failure cause, with distinct schemas for typed errors and defects. + * + * **Details** + * + * `Fail` reasons use the `error` schema, `Die` reasons use the `defect` schema, + * and `Interrupt` reasons carry only an optional fiber id. + * + * @see {@link Cause} for constructing schemas for full Cause values + * @see {@link CauseReasonIso} for the ISO shape of each cause reason + * + * @category CauseReason + * @since 4.0.0 + */ +export function CauseReason(error: E, defect: D): CauseReason { + const schema = declareConstructor, Cause_.Reason, CauseReasonIso>()( + [error, defect], + ([error, defect]) => (input, ast, options) => { + if (!Cause_.isReason(input)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Fail": + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(error)(input.error, options), + { + onSuccess: Cause_.makeFailReason, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["error"], issue)]) + } + ) + case "Die": + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(defect)(input.defect, options), + { + onSuccess: Cause_.makeDieReason, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["defect"], issue)]) + } + ) + case "Interrupt": + return Effect.succeed(input) + } + }, + { + typeConstructor: { + _tag: "effect/Cause/Failure" + }, + generation: { + runtime: `Schema.CauseReason(?, ?)`, + Type: `Cause.Failure`, + importDeclaration: `import * as Cause from "effect/Cause"` + }, + expected: "Cause.Failure", + toCodec: ([error, defect]) => + link>()( + Union([ + Struct({ _tag: Literal("Fail"), error }), + Struct({ _tag: Literal("Die"), defect }), + Struct({ _tag: Literal("Interrupt"), fiberId: UndefinedOr(Finite) }) + ]), + SchemaTransformation.transform({ + decode: (e) => { + switch (e._tag) { + case "Fail": + return Cause_.makeFailReason(e.error) + case "Die": + return Cause_.makeDieReason(e.defect) + case "Interrupt": + return Cause_.makeInterruptReason(e.fiberId) + } + }, + encode: identity + }) + ), + toArbitrary: ([error, defect]) => causeReasonToArbitrary(error, defect), + toEquivalence: ([error, defect]) => causeReasonToEquivalence(error, defect), + toFormatter: ([error, defect]) => causeReasonToFormatter(error, defect) + } + ) + return make(schema.ast, { error, defect }) +} + +function causeReasonToArbitrary( + error: Annotations.ToArbitrary.TypeParameter, + defect: Annotations.ToArbitrary.TypeParameter +) { + return (fc: typeof FastCheck, ctx: Annotations.ToArbitrary.Context) => { + const terminal = fc.constant(Cause_.makeInterruptReason()) + const arbitrary = fc.oneof( + terminal, + fc.integer({ min: 1 }).map(Cause_.makeInterruptReason), + error.arbitrary.map((e) => Cause_.makeFailReason(e)), + defect.arbitrary.map((d) => Cause_.makeDieReason(d)) + ) + return withRecursion(fc, ctx, terminal, arbitrary) + } +} + +function causeReasonToEquivalence(error: Equivalence.Equivalence, defect: Equivalence.Equivalence) { + return (a: Cause_.Reason, b: Cause_.Reason) => { + if (a._tag !== b._tag) return false + switch (a._tag) { + case "Fail": + return error(a.error, (b as Cause_.Fail).error) + case "Die": + return defect(a.defect, (b as Cause_.Die).defect) + case "Interrupt": + return a.fiberId === (b as Cause_.Interrupt).fiberId + } + } +} + +function causeReasonToFormatter(error: Formatter, defect: Formatter) { + return (t: Cause_.Reason) => { + switch (t._tag) { + case "Fail": + return `Fail(${error(t.error)})` + case "Die": + return `Die(${defect(t.defect)})` + case "Interrupt": + return "Interrupt" + } + } +} + +/** + * Type-level representation returned by {@link Cause}. + * + * @category Cause + * @since 3.10.0 + */ +export interface Cause extends + declareConstructor< + Cause_.Cause, + Cause_.Cause, + readonly [E, D], + CauseIso + > +{ + readonly "Rebuild": Cause + readonly error: E + readonly defect: D +} + +/** + * Iso representation used for `Cause` schemas: an ordered array of + * `CauseReasonIso` values. + * + * **When to use** + * + * Use when working with the ISO shape of a `Cause` schema, such as `toIso` + * optics or codecs that expose a cause as its ordered array of encoded reasons. + * + * @see {@link Cause} for constructing schemas for full Cause values + * @see {@link CauseReasonIso} for the ISO shape of each array element + * + * @category Cause + * @since 4.0.0 + */ +export type CauseIso = ReadonlyArray> + +/** + * Creates a schema for `Cause` values using separate schemas for typed failures + * and unexpected defects. + * + * **When to use** + * + * Use to validate, transform, or serialize Effect failure causes when typed + * failures and unexpected defects need separate schemas. + * + * **Details** + * + * The `error` schema is applied to `Fail` reasons and the `defect` schema is + * applied to `Die` reasons. Interrupt reasons do not use either schema and + * carry only an optional fiber id. + * + * @see {@link CauseReason} for the schema used by each individual cause reason + * @see {@link CauseIso} for the ordered array representation used by the schema ISO + * + * @category Cause + * @since 3.10.0 + */ +export function Cause(error: E, defect: D): Cause { + const schema = declareConstructor, Cause_.Cause, CauseIso>()( + [error, defect], + ([error, defect]) => { + const failures = ArraySchema(CauseReason(error, defect)) + return (input, ast, options) => { + if (!Cause_.isCause(input)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + return Effect.mapBothEager(SchemaParser.decodeUnknownEffect(failures)(input.reasons, options), { + onSuccess: Cause_.fromReasons, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["failures"], issue)]) + }) + } + }, + { + typeConstructor: { + _tag: "effect/Cause" + }, + generation: { + runtime: `Schema.Cause(?, ?)`, + Type: `Cause.Cause`, + importDeclaration: `import * as Cause from "effect/Cause"` + }, + expected: "Cause", + toCodec: ([error, defect]) => + link>()( + ArraySchema(CauseReason(error, defect)), + SchemaTransformation.transform({ + decode: Cause_.fromReasons, + encode: ({ reasons: failures }) => failures + }) + ), + toArbitrary: ([error, defect]) => causeToArbitrary(error, defect), + toEquivalence: ([error, defect]) => causeToEquivalence(error, defect), + toFormatter: ([error, defect]) => causeToFormatter(error, defect) + } + ) + return make(schema.ast, { error, defect }) +} + +function causeToArbitrary( + error: Annotations.ToArbitrary.TypeParameter, + defect: Annotations.ToArbitrary.TypeParameter +) { + return (fc: typeof FastCheck, ctx: Annotations.ToArbitrary.Context) => { + const reason = causeReasonToArbitrary(error, defect)(fc, ctx) + const terminal = fc.constant(Cause_.empty) + const arbitrary = fc.array(reason.arbitrary).map(Cause_.fromReasons) + return withRecursion(fc, ctx, terminal, arbitrary) + } +} + +function causeToEquivalence(error: Equivalence.Equivalence, defect: Equivalence.Equivalence) { + const failures = Equivalence.Array(causeReasonToEquivalence(error, defect)) + return (a: Cause_.Cause, b: Cause_.Cause) => failures(a.reasons, b.reasons) +} + +function causeToFormatter(error: Formatter, defect: Formatter) { + const causeReason = causeReasonToFormatter(error, defect) + return (t: Cause_.Cause) => `Cause([${t.reasons.map(causeReason).join(", ")}])` +} + +/** + * Type-level representation of {@link Error}. + * + * @category Error + * @since 4.0.0 + */ +export interface Error extends instanceOf { + readonly "Rebuild": Error +} + +/** + * Options for {@link Error} and {@link Defect}. + * + * @category options + * @since 4.0.0 + */ +export interface ErrorOptions { + /** + * Includes string stack traces in encoded `Error` values when set to `true`. + * + * @default false + */ + readonly includeStack?: boolean | undefined + /** + * Excludes `Error.cause` values from encoded `Error` values when set to + * `true`. + * + * @default false + */ + readonly excludeCause?: boolean | undefined +} + +type ErrorOptionsKey = 0 | 1 | 2 | 3 + +const getErrorOptionsKey = (options?: ErrorOptions): ErrorOptionsKey => + ((options?.includeStack === true ? 1 : 0) | + (options?.excludeCause === true ? 2 : 0)) as ErrorOptionsKey + +const getErrorOptions = (key: ErrorOptionsKey): ErrorOptions | undefined => { + switch (key) { + case 0: + return undefined + case 1: + return { includeStack: true } + case 2: + return { excludeCause: true } + case 3: + return { includeStack: true, excludeCause: true } + } +} + +const errorSchemaCache: Array = [] + +/** + * Schema for JavaScript `Error` objects. + * + * **Details** + * + * Default JSON serializer: + * + * Encodes an `Error` as an object with `message`, optional `name`, and optional + * `cause` properties, and decodes that object back into an `Error`. Stack + * traces are omitted by default for security. Pass `{ includeStack: true }` to + * include stack traces, or `{ excludeCause: true }` to omit causes. + * + * @category constructors + * @since 4.0.0 + */ +export function Error(options?: ErrorOptions): Error { + const key = getErrorOptionsKey(options) + const cached = errorSchemaCache[key] + if (cached !== undefined) { + return cached + } + const normalizedOptions = getErrorOptions(key) + const schema = instanceOf(globalThis.Error, { + typeConstructor: { + _tag: "Error", + ...(normalizedOptions === undefined ? {} : { options: normalizedOptions }) + }, + generation: { + runtime: normalizedOptions !== undefined ? `Schema.Error(${format(normalizedOptions)})` : `Schema.Error()`, + Type: `globalThis.Error` + }, + expected: "Error", + toCodecJson: () => link()(JsonError, SchemaTransformation.errorFromJsonError(normalizedOptions)), + toArbitrary: () => (fc) => fc.string().map((message) => new globalThis.Error(message)) + }) + errorSchemaCache[key] = schema + return schema +} + +/** + * Type-level representation of {@link Defect}. + * + * @category Defect + * @since 3.10.0 + */ +export interface Defect extends decodeTo { + readonly "Rebuild": Defect +} + +const defectSchemaCache: Array = [] + +/** + * Schema for unexpected defect values represented as `unknown` with a JSON + * encoded form. + * + * **When to use** + * + * Use when you need a schema for `Cause` defects or other unexpected failures + * whose runtime value may be any value. + * + * **Details** + * + * The encoded side is {@link Json}. During decoding, JSON objects with a string + * `message` property are decoded into JavaScript `Error` values, preserving a + * non-default `name` and any string `stack`. Other JSON values decode + * unchanged. + * + * During encoding, JavaScript `Error` values encode to JSON objects with + * `name`, `message`, and optional `cause` properties. Pass + * `{ includeStack: true }` to include string stack traces in encoded `Error` + * defects, or `{ excludeCause: true }` to omit causes. Other values are + * serialized through Effect's JSON formatter and then parsed back into JSON + * when possible. + * + * **Gotchas** + * + * This schema is for carrying defects across JSON boundaries, not for + * preserving every JavaScript value exactly. Some values cannot round-trip + * unchanged: + * + * - A non-`Error` object such as `{ message: "boom" }` encodes as an + * error-shaped JSON object and decodes back as an `Error`. + * - JSON serialization normalizes unsupported values. For example, + * `undefined` array elements encode as `null`, unsupported object properties + * are omitted, and circular references are dropped. + * - Values that cannot be represented as JSON fall back to Effect's formatted + * string representation. + * + * @see {@link Error} for a schema that only accepts JavaScript `Error` values. + * @category constructors + * @since 4.0.0 + */ +export function Defect(options?: ErrorOptions): Defect { + const key = getErrorOptionsKey(options) + const cached = defectSchemaCache[key] + if (cached !== undefined) { + return cached + } + const schema = Json.pipe(decodeTo(Unknown, SchemaTransformation.defectFromJson(getErrorOptions(key)))) + defectSchemaCache[key] = schema + return schema +} + +/** + * Type-level representation returned by {@link Exit}. + * + * @category Exit + * @since 3.10.0 + */ +export interface Exit extends + declareConstructor< + Exit_.Exit, + Exit_.Exit, + readonly [A, E, D], + ExitIso + > +{ + readonly "Rebuild": Exit + readonly value: A + readonly error: E + readonly defect: D +} + +/** + * Iso representation used for `Exit` schemas. + * + * **Details** + * + * Successful exits are represented as `{ _tag: "Success", value }`, while failed + * exits are represented as `{ _tag: "Failure", cause }`. + * + * @category Exit + * @since 4.0.0 + */ +export type ExitIso = { + readonly _tag: "Success" + readonly value: A["Iso"] +} | { + readonly _tag: "Failure" + readonly cause: CauseIso +} + +/** + * Creates a schema for `Exit` values using schemas for the success value, typed + * failure, and unexpected defect channels. + * + * **When to use** + * + * Use when serializing or validating an effect outcome where success, typed + * failure, and defects each need their own schema. + * + * @category Exit + * @since 3.10.0 + */ +export function Exit( + value: A, + error: E, + defect: D +): Exit { + const schema = declareConstructor< + Exit_.Exit, + Exit_.Exit, + ExitIso + >()( + [value, error, defect], + ([value, error, defect]) => { + const cause = Cause(error, defect) + return (input, ast, options) => { + if (!Exit_.isExit(input)) { + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + switch (input._tag) { + case "Success": + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(value)(input.value, options), + { + onSuccess: Exit_.succeed, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["value"], issue)]) + } + ) + case "Failure": + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(cause)(input.cause, options), + { + onSuccess: Exit_.failCause, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["cause"], issue)]) + } + ) + } + } + }, + { + typeConstructor: { + _tag: "effect/Exit" + }, + generation: { + runtime: `Schema.Exit(?, ?, ?)`, + Type: `Exit.Exit`, + importDeclaration: `import * as Exit from "effect/Exit"` + }, + expected: "Exit", + toCodec: ([value, error, defect]) => + link>()( + Union([ + Struct({ _tag: Literal("Success"), value }), + Struct({ _tag: Literal("Failure"), cause: Cause(error, defect) }) + ]), + SchemaTransformation.transform({ + decode: (e): Exit_.Exit => + e._tag === "Success" ? Exit_.succeed(e.value) : Exit_.failCause(e.cause), + encode: (exit) => + Exit_.isSuccess(exit) + ? { _tag: "Success", value: exit.value } as const + : { _tag: "Failure", cause: exit.cause } as const + }) + ), + toArbitrary: ([value, error, defect]) => (fc, ctx) => { + const cause = causeToArbitrary(error, defect)(fc, ctx) + const terminal = oneOfArbitraries( + fc, + value.terminal?.map((v): Exit_.Exit => Exit_.succeed(v)), + cause.terminal?.map((cause): Exit_.Exit => Exit_.failCause(cause)) + ) + const arbitrary = fc.oneof( + value.arbitrary.map((v): Exit_.Exit => Exit_.succeed(v)), + cause.arbitrary.map((cause): Exit_.Exit => Exit_.failCause(cause)) + ) + return withRecursion(fc, ctx, terminal, arbitrary) + }, + toEquivalence: ([value, error, defect]) => { + const cause = causeToEquivalence(error, defect) + return (a, b) => { + if (a._tag !== b._tag) return false + switch (a._tag) { + case "Success": + return value(a.value, (b as Exit_.Success).value) + case "Failure": + return cause(a.cause, (b as Exit_.Failure).cause) + } + } + }, + toFormatter: ([value, error, defect]) => { + const cause = causeToFormatter(error, defect) + return (t) => { + switch (t._tag) { + case "Success": + return `Exit.Success(${value(t.value)})` + case "Failure": + return `Exit.Failure(${cause(t.cause)})` + } + } + } + } + ) + return make(schema.ast, { value, error, defect }) +} + +/** + * Type-level representation returned by {@link ReadonlyMap}. + * + * @category ReadonlyMap + * @since 4.0.0 + */ +export interface $ReadonlyMap extends + declareConstructor< + globalThis.ReadonlyMap, + globalThis.ReadonlyMap, + readonly [Key, Value], + ReadonlyMapIso + > +{ + readonly "Rebuild": $ReadonlyMap + readonly key: Key + readonly value: Value +} + +/** + * Iso representation used for `ReadonlyMap` schemas: an array of readonly + * `[key, value]` tuples using each entry schema's `Iso` type. + * + * @category ReadonlyMap + * @since 4.0.0 + */ +export type ReadonlyMapIso = ReadonlyArray< + readonly [Key["Iso"], Value["Iso"]] +> + +function oneOfArbitraries( + fc: typeof FastCheck, + a: FastCheck.Arbitrary | undefined, + b: FastCheck.Arbitrary | undefined +) { + return a === undefined ? b : b === undefined ? a : fc.oneof(a, b) +} + +function withRecursion( + fc: typeof FastCheck, + ctx: Annotations.ToArbitrary.Context, + terminal: FastCheck.Arbitrary | undefined, + arbitrary: FastCheck.Arbitrary +) { + return { + arbitrary: terminal === undefined || ctx.recursion === undefined + ? arbitrary + : fc.oneof(ctx.recursion, terminal, arbitrary), + terminal + } +} + +function arrayFromItems( + fc: typeof FastCheck, + item: FastCheck.Arbitrary, + constraints: FastCheck.ArrayConstraints | undefined, + comparator?: ((a: T, b: T) => boolean) | undefined +) { + return comparator === undefined + ? fc.array(item, constraints) + : fc.uniqueArray(item, { ...constraints, comparator }) +} + +function collectionArbitrary( + fc: typeof FastCheck, + ctx: Annotations.ToArbitrary.Context, + item: FastCheck.Arbitrary, + terminalItem: FastCheck.Arbitrary | undefined, + fromIterable: (items: Array) => Out, + comparator?: ((a: T, b: T) => boolean) | undefined +) { + const constraint = ctx.constraint + const constraints = constraint === undefined || + (constraint.minLength === undefined && constraint.maxLength === undefined) + ? undefined + : { + ...(constraint.minLength !== undefined ? { minLength: constraint.minLength } : {}), + ...(constraint.maxLength !== undefined ? { maxLength: constraint.maxLength } : {}) + } + if ( + constraints?.minLength !== undefined && constraints.maxLength !== undefined && + constraints.minLength > constraints.maxLength + ) { + throw new globalThis.Error("Unable to derive an arbitrary for size constraints") + } + const minLength = constraints?.minLength ?? 0 + const terminal = minLength === 0 + ? fc.constant>([]) + : terminalItem === undefined + ? undefined + : arrayFromItems(fc, terminalItem, { ...constraints, maxLength: minLength }, comparator) + const arrays = withRecursion( + fc, + ctx, + terminal, + arrayFromItems(fc, item, constraints, comparator) + ) + return { + arbitrary: arrays.arbitrary.map(fromIterable), + terminal: arrays.terminal?.map(fromIterable) + } +} + +function entriesArbitrary( + fc: typeof FastCheck, + ctx: Annotations.ToArbitrary.Context, + key: Annotations.ToArbitrary.TypeParameter, + value: Annotations.ToArbitrary.TypeParameter, + fromIterable: (items: Array<[K, V]>) => Out +) { + return collectionArbitrary( + fc, + ctx, + fc.tuple(key.arbitrary, value.arbitrary), + key.terminal === undefined || value.terminal === undefined ? undefined : fc.tuple(key.terminal, value.terminal), + fromIterable, + ([a], [b]) => Equal.equals(a, b) + ) +} + +/** + * Schema for readonly maps whose keys and values conform to the provided + * schemas. + * + * @category ReadonlyMap + * @since 3.10.0 + */ +export function ReadonlyMap( + key: Key, + value: Value +): $ReadonlyMap { + const schema = declareConstructor< + globalThis.ReadonlyMap, + globalThis.ReadonlyMap, + ReadonlyMapIso + >()( + [key, value], + ([key, value]) => { + const array = ArraySchema(Tuple([key, value])) + return (input, ast, options) => { + if (input instanceof globalThis.Map) { + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(array)([...input], options), + { + onSuccess: (array: ReadonlyArray) => new globalThis.Map(array), + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["entries"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "ReadonlyMap" + }, + generation: { + runtime: `Schema.ReadonlyMap(?, ?)`, + Type: `globalThis.ReadonlyMap` + }, + expected: "ReadonlyMap", + toCodec: ([key, value]) => + link>()( + ArraySchema(Tuple([key, value])), + SchemaTransformation.transform({ + decode: (e) => new globalThis.Map(e), + encode: (map) => [...map.entries()] + }) + ), + toArbitrary: ([key, value]) => (fc, ctx) => entriesArbitrary(fc, ctx, key, value, (as) => new globalThis.Map(as)), + toEquivalence: ([key, value]) => Equal.makeCompareMap(key, value), + toFormatter: ([key, value]) => (t) => { + const size = t.size + if (size === 0) { + return "ReadonlyMap(0) {}" + } + const entries = globalThis.Array.from(t.entries()).sort().map(([k, v]) => `${key(k)} => ${value(v)}`) + return `ReadonlyMap(${size}) { ${entries.join(", ")} }` + } + } + ) + return make(schema.ast, { key, value }) +} + +/** + * Type-level representation returned by {@link HashMap}. + * + * @category HashMap + * @since 3.10.0 + */ +export interface HashMap extends + declareConstructor< + HashMap_.HashMap, + HashMap_.HashMap, + readonly [Key, Value], + HashMapIso + > +{ + readonly "Rebuild": HashMap + readonly key: Key + readonly value: Value +} + +/** + * Iso representation used for `HashMap` schemas: an array of readonly + * `[key, value]` tuples using each entry schema's `Iso` type. + * + * @category HashMap + * @since 4.0.0 + */ +export type HashMapIso = ReadonlyArray< + readonly [Key["Iso"], Value["Iso"]] +> + +/** + * Schema for hash maps whose keys and values conform to the provided schemas. + * + * @category HashMap + * @since 3.10.0 + */ +export function HashMap(key: Key, value: Value): HashMap { + const schema = declareConstructor< + HashMap_.HashMap, + HashMap_.HashMap, + HashMapIso + >()( + [key, value], + ([key, value]) => { + const entries = ArraySchema(Tuple([key, value])) + return (input, ast, options) => { + if (HashMap_.isHashMap(input)) { + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(entries)(HashMap_.toEntries(input), options), + { + onSuccess: HashMap_.fromIterable, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["entries"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "effect/HashMap" + }, + generation: { + runtime: `Schema.HashMap(?, ?)`, + Type: `HashMap.HashMap`, + importDeclaration: `import * as HashMap from "effect/HashMap"` + }, + expected: "HashMap", + toCodec: ([key, value]) => + link>()( + ArraySchema(Tuple([key, value])), + SchemaTransformation.transform({ + decode: HashMap_.fromIterable, + encode: HashMap_.toEntries + }) + ), + toArbitrary: ([key, value]) => (fc, ctx) => entriesArbitrary(fc, ctx, key, value, HashMap_.fromIterable), + toEquivalence: ([key, value]) => Equal.makeCompareMap(key, value), + toFormatter: ([key, value]) => (t) => { + const size = HashMap_.size(t) + if (size === 0) { + return "HashMap(0) {}" + } + const entries = HashMap_.toEntries(t).sort().map(([k, v]) => `${key(k)} => ${value(v)}`) + return `HashMap(${size}) { ${entries.join(", ")} }` + } + } + ) + return make(schema.ast, { key, value }) +} + +/** + * Type-level representation returned by {@link ReadonlySet}. + * + * @category ReadonlySet + * @since 4.0.0 + */ +export interface $ReadonlySet extends + declareConstructor< + globalThis.ReadonlySet, + globalThis.ReadonlySet, + readonly [Value], + ReadonlySetIso + > +{ + readonly "Rebuild": $ReadonlySet + readonly value: Value +} + +/** + * Iso representation used for `ReadonlySet` schemas: an array of element values + * using the element schema's `Iso` type. + * + * @category ReadonlySet + * @since 4.0.0 + */ +export type ReadonlySetIso = ReadonlyArray + +/** + * Schema for readonly sets whose values conform to the provided element schema. + * + * @category ReadonlySet + * @since 3.10.0 + */ +export function ReadonlySet(value: Value): $ReadonlySet { + const schema = declareConstructor< + globalThis.ReadonlySet, + globalThis.ReadonlySet, + ReadonlySetIso + >()( + [value], + ([value]) => { + const array = ArraySchema(value) + return (input, ast, options) => { + if (input instanceof globalThis.Set) { + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(array)([...input], options), + { + onSuccess: (array: ReadonlyArray) => new globalThis.Set(array), + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "ReadonlySet" + }, + generation: { + runtime: `Schema.ReadonlySet(?)`, + Type: `globalThis.ReadonlySet` + }, + expected: "ReadonlySet", + toCodec: ([value]) => + link>()( + ArraySchema(value), + SchemaTransformation.transform({ + decode: (e) => new globalThis.Set(e), + encode: (set) => [...set.values()] + }) + ), + toArbitrary: ([value]) => (fc, ctx) => + collectionArbitrary(fc, ctx, value.arbitrary, value.terminal, (as) => new globalThis.Set(as), Equal.equals), + toEquivalence: ([value]) => Equal.makeCompareSet(value), + toFormatter: ([value]) => (t) => { + const size = t.size + if (size === 0) { + return "ReadonlySet(0) {}" + } + const values = globalThis.Array.from(t.values()).sort().map((v) => `${value(v)}`) + return `ReadonlySet(${size}) { ${values.join(", ")} }` + } + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation returned by {@link HashSet}. + * + * @category HashSet + * @since 3.10.0 + */ +export interface HashSet extends + declareConstructor< + HashSet_.HashSet, + HashSet_.HashSet, + readonly [Value], + HashSetIso + > +{ + readonly "Rebuild": HashSet + readonly value: Value +} + +/** + * Iso representation used for `HashSet` schemas: an array of element values + * using the element schema's `Iso` type. + * + * @category HashSet + * @since 4.0.0 + */ +export type HashSetIso = ReadonlyArray + +/** + * Schema for hash sets whose values conform to the provided element schema. + * + * @category HashSet + * @since 3.10.0 + */ +export function HashSet(value: Value): HashSet { + const schema = declareConstructor< + HashSet_.HashSet, + HashSet_.HashSet, + HashSetIso + >()( + [value], + ([value]) => { + const values = ArraySchema(value) + return (input, ast, options) => { + if (HashSet_.isHashSet(input)) { + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), + { + onSuccess: HashSet_.fromIterable, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "effect/HashSet" + }, + generation: { + runtime: `Schema.HashSet(?)`, + Type: `HashSet.HashSet` + }, + expected: "HashSet", + toCodec: ([value]) => + link>()( + ArraySchema(value), + SchemaTransformation.transform({ + decode: HashSet_.fromIterable, + encode: Arr.fromIterable + }) + ), + toArbitrary: ([value]) => (fc, ctx) => + collectionArbitrary(fc, ctx, value.arbitrary, value.terminal, HashSet_.fromIterable, Equal.equals), + toEquivalence: ([value]) => Equal.makeCompareSet(value), + toFormatter: ([value]) => (t) => { + const size = HashSet_.size(t) + if (size === 0) { + return "HashSet(0) {}" + } + const values = globalThis.Array.from(t).sort().map((v) => `${value(v)}`) + return `HashSet(${size}) { ${values.join(", ")} }` + } + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation returned by {@link Chunk}. + * + * @category Chunk + * @since 3.10.0 + */ +export interface Chunk extends + declareConstructor< + Chunk_.Chunk, + Chunk_.Chunk, + readonly [Value], + ChunkIso + > +{ + readonly "Rebuild": Chunk + readonly value: Value +} + +/** + * Iso representation used for `Chunk` schemas: an array of element values using + * the element schema's `Iso` type. + * + * **When to use** + * + * Use when annotating type-level helpers that work with the readonly-array ISO + * shape of a `Chunk` schema. + * + * @see {@link Chunk} for the schema interface and constructor that use this ISO representation + * + * @category Chunk + * @since 4.0.0 + */ +export type ChunkIso = ReadonlyArray + +/** + * Schema for chunks whose values conform to the provided element schema. + * + * @category Chunk + * @since 3.10.0 + */ +export function Chunk(value: Value): Chunk { + const schema = declareConstructor< + Chunk_.Chunk, + Chunk_.Chunk, + ChunkIso + >()( + [value], + ([value]) => { + const values = ArraySchema(value) + return (input, ast, options) => { + if (Chunk_.isChunk(input)) { + return Effect.mapBothEager( + SchemaParser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), + { + onSuccess: Chunk_.fromIterable, + onFailure: (issue) => + new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + } + ) + } + return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + } + }, + { + typeConstructor: { + _tag: "effect/Chunk" + }, + generation: { + runtime: `Schema.Chunk(?)`, + Type: `Chunk.Chunk` + }, + expected: "Chunk", + toCodec: ([value]) => + link>()( + ArraySchema(value), + SchemaTransformation.transform({ + decode: Chunk_.fromIterable, + encode: Arr.fromIterable + }) + ), + toArbitrary: ([value]) => (fc, ctx) => + collectionArbitrary(fc, ctx, value.arbitrary, value.terminal, Chunk_.fromIterable), + toEquivalence: ([value]) => Chunk_.makeEquivalence(value), + toFormatter: ([value]) => (t) => { + const size = Chunk_.size(t) + if (size === 0) { + return "Chunk(0) {}" + } + const values = globalThis.Array.from(t).sort().map((v) => `${value(v)}`) + return `Chunk(${size}) { ${values.join(", ")} }` + } + } + ) + return make(schema.ast, { value }) +} + +/** + * Type-level representation of {@link RegExp}. + * + * @category RegExp + * @since 4.0.0 + */ +export interface RegExp extends instanceOf { + readonly "Rebuild": RegExp +} + +/** + * Schema for JavaScript `RegExp` objects. + * + * **Details** + * + * The default JSON serializer encodes a `RegExp` as `{ source, flags }`. + * + * @category RegExp + * @since 4.0.0 + */ +export const RegExp: RegExp = instanceOf( + globalThis.RegExp, + { + typeConstructor: { + _tag: "RegExp" + }, + generation: { + runtime: `Schema.RegExp`, + Type: `globalThis.RegExp` + }, + expected: "RegExp", + toCodecJson: () => + link()( + Struct({ + source: String, + flags: String + }), + SchemaTransformation.transformOrFail({ + decode: (e) => + Effect.try({ + try: () => new globalThis.RegExp(e.source, e.flags), + catch: (e) => new SchemaIssue.InvalidValue(Option_.some(e), { message: globalThis.String(e) }) + }), + encode: (regExp) => + Effect.succeed({ + source: regExp.source, + flags: regExp.flags + }) + }) + ), + toArbitrary: () => (fc) => + fc + .tuple( + fc.constantFrom( + ".", + ".*", + "\\d+", + "\\w+", + "[a-z]+", + "[A-Z]+", + "[0-9]+", + "^[a-zA-Z0-9]+$", + "^\\d{4}-\\d{2}-\\d{2}$" // date pattern + ), + fc + .uniqueArray(fc.constantFrom("g", "i", "m", "s", "u", "y"), { + minLength: 0, + maxLength: 6 + }) + .map((flags) => flags.join("")) + ) + .map(([source, flags]) => new globalThis.RegExp(source, flags)), + toEquivalence: () => (a, b) => a.source === b.source && a.flags === b.flags + } +) + +/** + * Type-level representation of {@link URL}. + * + * @category URL + * @since 4.0.0 + */ +export interface URL extends instanceOf { + readonly "Rebuild": URL +} + +const URLString = String.annotate({ expected: "a string that will be decoded as a URL" }) + +/** + * Schema for JavaScript `URL` objects. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes `URL` as a `string` + * + * @category URL + * @since 4.0.0 + */ +export const URL: URL = instanceOf( + globalThis.URL, + { + typeConstructor: { + _tag: "URL" + }, + generation: { + runtime: `Schema.URL`, + Type: `globalThis.URL` + }, + expected: "URL", + toCodecJson: () => + link()( + URLString, + SchemaTransformation.urlFromString + ), + toArbitrary: () => (fc) => fc.webUrl().map((s) => new globalThis.URL(s)), + toEquivalence: () => (a, b) => a.toString() === b.toString() + } +) + +/** + * Type-level representation of {@link URLFromString}. + * + * @category URL + * @since 4.0.0 + */ +export interface URLFromString extends decodeTo { + readonly "Rebuild": URLFromString +} + +/** + * Schema that decodes a `string` into a `URL`. + * + * **Details** + * + * Decoding: + * - A **valid** URL `string` is decoded as a `URL` + * + * Encoding: + * - A `URL` is encoded as a `string` + * + * @category URL + * @since 4.0.0 + */ +export const URLFromString: URLFromString = URLString.pipe(decodeTo(URL, SchemaTransformation.urlFromString)) + +/** + * Type-level representation of {@link Date}. + * + * @category Date + * @since 4.0.0 + */ +export interface Date extends instanceOf { + readonly "Rebuild": Date +} + +type DateArbitraryConstraints = FastCheck.DateConstraints & { + readonly valid?: boolean | undefined +} + +function dateArbitraryConstraints( + constraint: Annotations.ToArbitrary.GenerationConstraint | undefined, + ordered: Annotations.ToArbitrary.OrderedConstraint | undefined, + base?: DateArbitraryConstraints | undefined, + toDate?: (value: T) => globalThis.Date +): FastCheck.DateConstraints { + const out: FastCheck.DateConstraints = { ...base } + delete (out as any).valid + if (base?.valid || constraint?.valid) { + out.noInvalidDate = true + } + if (ordered?.minimum !== undefined) { + const minimum = toDate === undefined ? ordered.minimum as globalThis.Date : toDate(ordered.minimum) + const nextMin = ordered.exclusiveMinimum ? new globalThis.Date(minimum.getTime() + 1) : minimum + if (out.min === undefined || nextMin.getTime() > out.min.getTime()) { + out.min = nextMin + } + } + if (ordered?.maximum !== undefined) { + const maximum = toDate === undefined ? ordered.maximum as globalThis.Date : toDate(ordered.maximum) + const nextMax = ordered.exclusiveMaximum ? new globalThis.Date(maximum.getTime() - 1) : maximum + if (out.max === undefined || nextMax.getTime() < out.max.getTime()) { + out.max = nextMax + } + } + return out +} + +const DateString = String.annotate({ expected: "a string in ISO 8601 format that will be decoded as a Date" }) + +/** + * Schema for JavaScript `Date` objects. + * + * **When to use** + * + * Use to validate in-memory values that must already be JavaScript date + * objects. + * + * **Details** + * + * This schema accepts any `Date` instance, including invalid dates. The default + * JSON serializer encodes valid dates as ISO 8601 strings; invalid dates encode + * as `"Invalid Date"`. + * + * **Example** (Defining a Date schema) + * + * ```ts + * import { Schema } from "effect" + * + * Schema.decodeUnknownSync(Schema.Date)(new Date("2024-01-01")) + * // => Date { 2024-01-01T00:00:00.000Z } + * ``` + * + * @see {@link DateValid} for accepting only valid Date instances + * @see {@link DateFromString} for decoding strings into Date instances + * @see {@link DateFromMillis} for decoding epoch milliseconds into Date instances + * + * @category Date + * @since 4.0.0 + */ +export const Date: Date = instanceOf( + globalThis.Date, + { + typeConstructor: { + _tag: "Date" + }, + generation: { + runtime: `Schema.Date`, + Type: `globalThis.Date` + }, + expected: "Date", + toCodecJson: () => + link()( + DateString, + SchemaTransformation.dateFromString + ), + toArbitrary: () => (fc, ctx) => + fc.date(dateArbitraryConstraints( + ctx?.constraint, + ctx?.constraint?.ordered?.order === Order.Date ? ctx.constraint.ordered : undefined + )) + } +) + +/** + * Type-level representation of {@link DateFromString}. + * + * @category Date + * @since 3.10.0 + */ +export interface DateFromString extends decodeTo { + readonly "Rebuild": DateFromString +} + +/** + * Schema that decodes a string into a JavaScript `Date`. + * + * **When to use** + * + * Use to model string-encoded dates that decode to JavaScript `Date` objects + * and encode back to strings. + * + * **Details** + * + * Decoding: + * The string is passed to JavaScript `Date` construction. + * + * Encoding: + * A valid `Date` is encoded as an ISO string; an invalid `Date` is encoded as + * `"Invalid Date"`. + * + * **Gotchas** + * + * Invalid date strings can decode to invalid `Date` instances. + * + * @see {@link DateFromMillis} for decoding epoch milliseconds into Date instances + * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values + * @see {@link Date} for accepting Date instances directly + * @see {@link DateValid} for rejecting invalid Date instances + * + * @category Date + * @since 3.10.0 + */ +export const DateFromString: DateFromString = DateString.pipe(decodeTo(Date, SchemaTransformation.dateFromString)) + +/** + * Type-level representation of {@link DateFromMillis}. + * + * @category Date + * @since 4.0.0 + */ +export interface DateFromMillis extends decodeTo { + readonly "Rebuild": DateFromMillis +} + +/** + * Schema that decodes epoch milliseconds into a JavaScript `Date`. + * + * **When to use** + * + * Use to model numeric millisecond timestamps that decode to JavaScript `Date` + * objects and encode back to numbers. + * + * **Details** + * + * Decoding: + * A number of milliseconds since the Unix epoch is decoded as a `Date`. + * + * Encoding: + * A `Date` is encoded as its millisecond timestamp. + * + * **Gotchas** + * + * This schema accepts any number, including `NaN`, `Infinity`, and `-Infinity`. + * Those values decode to invalid `Date` instances. + * + * @see {@link DateFromString} for decoding string-encoded dates + * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values + * + * @category Date + * @since 4.0.0 + */ +export const DateFromMillis: DateFromMillis = Number.pipe( + decodeTo(Date, SchemaTransformation.dateFromMillis) +) + +/** + * Type-level representation of {@link DateValid}. + * + * @category Date + * @since 4.0.0 + */ +export interface DateValid extends Date { + readonly "Rebuild": DateValid +} + +/** + * Schema for **valid** JavaScript `Date` objects. + * + * **Details** + * + * This schema accepts `Date` instances but rejects invalid dates (such as `new + * Date("invalid")`). + * + * @category Date + * @since 4.0.0 + */ +export const DateValid: DateValid = Date.check(isDateValid()) + +/** + * Type-level representation of {@link Duration}. + * + * @category Duration + * @since 3.10.0 + */ +export interface Duration extends declare { + readonly "Rebuild": Duration +} + +/** + * Schema for `Duration` values. + * + * **Details** + * + * The default JSON serializer encodes `Duration` as a tagged object with the + * duration type and value. + * + * **Example** (Defining a Duration schema) + * + * ```ts + * import { Duration, Schema } from "effect" + * + * Schema.decodeUnknownSync(Schema.Duration)(Duration.seconds(5)) + * // => Duration(5s) + * ``` + * + * @category Duration + * + * @since 3.10.0 + */ +export const Duration: Duration = declare( + Duration_.isDuration, + { + typeConstructor: { + _tag: "effect/Duration" + }, + generation: { + runtime: `Schema.Duration`, + Type: `Duration.Duration`, + importDeclaration: `import * as Duration from "effect/Duration"` + }, + expected: "Duration", + toCodecJson: () => + link()( + Union([ + Struct({ _tag: Literal("Infinity") }), + Struct({ _tag: Literal("NegativeInfinity") }), + Struct({ _tag: Literal("Nanos"), value: BigInt }), + Struct({ _tag: Literal("Millis"), value: Int }) + ]), + SchemaTransformation.transform({ + decode: (e) => { + switch (e._tag) { + case "Infinity": + return Duration_.infinity + case "NegativeInfinity": + return Duration_.negativeInfinity + case "Nanos": + return Duration_.nanos(e.value) + case "Millis": + return Duration_.millis(e.value) + } + }, + encode: (duration) => { + switch (duration.value._tag) { + case "Infinity": + return { _tag: "Infinity" } as const + case "NegativeInfinity": + return { _tag: "NegativeInfinity" } as const + case "Nanos": + return { _tag: "Nanos", value: duration.value.nanos } as const + case "Millis": + return { _tag: "Millis", value: duration.value.millis } as const + } + } + }) + ), + toArbitrary: () => (fc) => + fc.oneof( + fc.constant(Duration_.infinity), + fc.constant(Duration_.negativeInfinity), + fc.bigInt().map(Duration_.nanos), + fc.maxSafeInteger().map(Duration_.millis) + ), + toFormatter: () => globalThis.String, + toEquivalence: () => Duration_.Equivalence + } +) + +const DurationString = String.annotate({ expected: "a string that will be decoded as a Duration" }) + +/** + * Type-level representation of {@link DurationFromString}. + * + * @category Duration + * @since 4.0.0 + */ +export interface DurationFromString extends decodeTo { + readonly "Rebuild": DurationFromString +} + +/** + * Schema that parses a string into a `Duration`. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as a `Duration`, accepting any format that + * `Duration.fromInput` can parse. + * + * Encoding: + * - A `Duration` is encoded as a parseable `string`. + * + * @category Duration + * @since 4.0.0 + */ +export const DurationFromString: DurationFromString = DurationString.pipe( + decodeTo(Duration, SchemaTransformation.durationFromString) +) + +/** + * Type-level representation of {@link DurationFromNanos}. + * + * @category Duration + * @since 3.10.0 + */ +export interface DurationFromNanos extends decodeTo { + readonly "Rebuild": DurationFromNanos +} + +const bigint0 = globalThis.BigInt(0) + +/** + * Schema that decodes a non-negative `bigint` into a + * `Duration`, treating the bigint as nanoseconds. + * + * **Details** + * + * Decoding: + * A non-negative `bigint` representing nanoseconds is decoded as a `Duration`. + * + * Encoding: + * Finite durations are encoded as a non-negative `bigint` number of nanoseconds. + * Encoding fails when the duration cannot be represented as nanoseconds, such as + * `Duration.infinity`. + * + * @category Duration + * @since 3.10.0 + */ +export const DurationFromNanos: DurationFromNanos = BigInt.check(isGreaterThanOrEqualToBigInt(bigint0)).pipe( + decodeTo(Duration, SchemaTransformation.durationFromNanos) +) + +/** + * Type-level representation of {@link DurationFromMillis}. + * + * @category Duration + * @since 3.10.0 + */ +export interface DurationFromMillis extends decodeTo { + readonly "Rebuild": DurationFromMillis +} + +/** + * Schema that decodes a non-negative (possibly infinite) + * integer into a `Duration`, treating the integer value as the duration in + * milliseconds. + * + * **Details** + * + * Decoding: + * - A non-negative (possibly infinite) integer representing milliseconds is + * decoded as a `Duration` + * + * Encoding: + * - A `Duration` is encoded to a non-negative (possibly infinite) integer + * representing milliseconds + * + * @category Duration + * @since 3.10.0 + */ +export const DurationFromMillis: DurationFromMillis = Number.check(isGreaterThanOrEqualTo(0)).pipe( + decodeTo(Duration, SchemaTransformation.durationFromMillis) +) + +/** + * Type-level representation of {@link BigDecimal}. + * + * @category BigDecimal + * @since 3.10.0 + */ +export interface BigDecimal extends declare { + readonly "Rebuild": BigDecimal +} + +const BigDecimalString = String.annotate({ expected: "a string that will be decoded as a BigDecimal" }) + +const bigDecimalDefaultMaxScale = 20 +const bigDecimalInvalidOrderedConstraintsError = "Unable to derive an arbitrary for the ordered BigDecimal constraints" + +function bigDecimalScaleValueAtScale(bd: BigDecimal_.BigDecimal, scale: number): bigint { + return BigDecimal_.scale(bd, scale).value +} + +function bigDecimalMinValueAtScale( + minimum: BigDecimal_.BigDecimal, + scale: number, + excluded: boolean +): bigint { + return excluded + ? bigDecimalScaleValueAtScale(BigDecimal_.floor(minimum, scale), scale) + globalThis.BigInt(1) + : bigDecimalScaleValueAtScale(BigDecimal_.ceil(minimum, scale), scale) +} + +function bigDecimalMaxValueAtScale( + maximum: BigDecimal_.BigDecimal, + scale: number, + excluded: boolean +): bigint { + return excluded + ? bigDecimalScaleValueAtScale(BigDecimal_.ceil(maximum, scale), scale) - globalThis.BigInt(1) + : bigDecimalScaleValueAtScale(BigDecimal_.floor(maximum, scale), scale) +} + +function bigDecimalMaxScale(ordered: Annotations.ToArbitrary.OrderedConstraint): number { + return Math.max( + bigDecimalDefaultMaxScale, + ordered.minimum?.scale ?? 0, + ordered.maximum?.scale ?? 0, + ordered.exclusiveMinimum && ordered.minimum !== undefined ? ordered.minimum.scale + 1 : 0, + ordered.exclusiveMaximum && ordered.maximum !== undefined ? ordered.maximum.scale + 1 : 0 + ) +} + +function bigDecimalValueConstraintsAtScale( + ordered: Annotations.ToArbitrary.OrderedConstraint, + scale: number +): FastCheck.BigIntConstraints | undefined { + const constraints: FastCheck.BigIntConstraints = {} + if (ordered.minimum !== undefined) { + constraints.min = bigDecimalMinValueAtScale(ordered.minimum, scale, ordered.exclusiveMinimum === true) + } + if (ordered.maximum !== undefined) { + constraints.max = bigDecimalMaxValueAtScale(ordered.maximum, scale, ordered.exclusiveMaximum === true) + } + if (constraints.min !== undefined && constraints.max !== undefined && constraints.min > constraints.max) { + return undefined + } + return constraints +} + +function bigDecimalScaleConstraints( + ordered: Annotations.ToArbitrary.OrderedConstraint +): FastCheck.IntegerConstraints { + const max = bigDecimalMaxScale(ordered) + if (bigDecimalValueConstraintsAtScale(ordered, max) === undefined) { + throw new globalThis.Error(bigDecimalInvalidOrderedConstraintsError) + } + + let min = 0 + let high = max + while (min < high) { + const scale = min + Math.floor((high - min) / 2) + if (bigDecimalValueConstraintsAtScale(ordered, scale) === undefined) { + min = scale + 1 + } else { + high = scale + } + } + return { min, max } +} + +/** + * Schema for `BigDecimal` values. + * + * **When to use** + * + * Use when you already have Effect decimal instances and need schema + * validation, formatting, equivalence, and JSON string serialization. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes `BigDecimal` as a `string` + * + * @see {@link BigDecimalFromString} for parsing string input into a BigDecimal + * + * @category BigDecimal + * @since 3.10.0 + */ +export const BigDecimal: BigDecimal = declare( + BigDecimal_.isBigDecimal, + { + typeConstructor: { + _tag: "effect/BigDecimal" + }, + generation: { + runtime: `Schema.BigDecimal`, + Type: `BigDecimal.BigDecimal`, + importDeclaration: `import * as BigDecimal from "effect/BigDecimal"` + }, + expected: "BigDecimal", + toCodecJson: () => + link()( + BigDecimalString, + SchemaTransformation.bigDecimalFromString + ), + toArbitrary: () => (fc, ctx) => { + const ordered = ctx.constraint?.ordered?.order === BigDecimal_.Order + ? ctx.constraint.ordered as Annotations.ToArbitrary.OrderedConstraint + : undefined + if (ordered === undefined) { + return fc.tuple(fc.bigInt(), fc.integer({ min: 0, max: bigDecimalDefaultMaxScale })) + .map(([value, scale]) => BigDecimal_.make(value, scale)) + } + + return fc.integer(bigDecimalScaleConstraints(ordered)).chain((scale) => { + const constraints = bigDecimalValueConstraintsAtScale(ordered, scale) + if (constraints === undefined) { + throw new globalThis.Error(bigDecimalInvalidOrderedConstraintsError) + } + return fc.bigInt(constraints).map((value) => BigDecimal_.make(value, scale)) + }) + }, + toFormatter: () => (bd) => BigDecimal_.format(bd), + toEquivalence: () => BigDecimal_.Equivalence + } +) + +/** + * Type-level representation of {@link BigDecimalFromString}. + * + * @category BigDecimal + * @since 4.0.0 + */ +export interface BigDecimalFromString extends decodeTo { + readonly "Rebuild": BigDecimalFromString +} + +/** + * Schema that parses a string into a `BigDecimal`. + * + * **When to use** + * + * Use to parse decimal or exponent-notation strings into arbitrary-precision + * BigDecimal values while encoding them back to strings. + * + * **Details** + * + * Decoding: + * - A `string` is decoded with `BigDecimal.fromString`. + * + * Encoding: + * - A `BigDecimal` is encoded with `BigDecimal.format`. + * + * **Gotchas** + * + * An empty string decodes as zero. + * + * @see {@link BigDecimal} for validating values that are already BigDecimal values + * @see {@link BigIntFromString} for parsing base-10 integer strings into bigint values + * @see {@link NumberFromString} for parsing JavaScript number strings + * + * @category BigDecimal + * @since 4.0.0 + */ +export const BigDecimalFromString: BigDecimalFromString = BigDecimalString.pipe( + decodeTo(BigDecimal, SchemaTransformation.bigDecimalFromString) +) + +/** + * Type-level representation of {@link UnknownFromJsonString}. + * + * @category models + * @since 4.0.0 + */ +export interface UnknownFromJsonString extends fromJsonString { + readonly "Rebuild": UnknownFromJsonString +} + +/** + * Schema that decodes a JSON-encoded string into an `unknown` value. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as an `unknown` value. + * - If the string is not valid JSON, decoding fails. + * + * Encoding: + * - Any value is encoded as a JSON string using `JSON.stringify`. + * - If the value is not a valid JSON value, encoding fails. + * + * **Example** (Decoding unknown JSON strings) + * + * ```ts + * import { Schema } from "effect" + * + * Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(`{"a":1,"b":2}`) + * // => { a: 1, b: 2 } + * ``` + * + * @category schemas + * @since 4.0.0 + */ +export const UnknownFromJsonString: UnknownFromJsonString = fromJsonString(Unknown) + +/** + * Type-level representation returned by {@link fromJsonString}. + * + * @category models + * @since 4.0.0 + */ +export interface fromJsonString extends decodeTo { + readonly "Rebuild": fromJsonString +} + +/** + * Returns a schema that decodes a JSON string and then decodes the parsed value + * using the given schema. + * + * **Details** + * + * This is useful when working with JSON-encoded strings where the actual + * structure of the value is known and described by an existing schema. + * + * The resulting schema first parses the input string as JSON, and then runs the + * provided schema on the parsed result. + * + * JSON Schema generation: + * + * When using `fromJsonString` with `draft-2020-12` or `openApi3.1`, the + * resulting schema will be a JSON Schema with a `contentSchema` property that + * contains the JSON Schema for the given schema. + * + * **Example** (Decoding JSON strings with a schema) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.Struct({ a: Schema.Number }) + * const schemaFromJsonString = Schema.fromJsonString(schema) + * + * Schema.decodeUnknownSync(schemaFromJsonString)(`{"a":1,"b":2}`) + * // => { a: 1 } + * ``` + * + * **Example** (Emitting JSON Schema for a JSON string decoder) + * + * ```ts + * import { Schema } from "effect" + * + * const original = Schema.Struct({ a: Schema.String }) + * const schema = Schema.fromJsonString(original) + * + * const document = Schema.toJsonSchemaDocument(schema) + * + * console.log(JSON.stringify(document, null, 2)) + * // { + * // "source": "draft-2020-12", + * // "schema": { + * // "type": "string", + * // "contentMediaType": "application/json", + * // "contentSchema": { + * // "type": "object", + * // "properties": { + * // "a": { + * // "type": "string" + * // } + * // }, + * // "required": [ + * // "a" + * // ], + * // "additionalProperties": false + * // } + * // }, + * // "definitions": {} + * // } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function fromJsonString(schema: S): fromJsonString { + const identifier = SchemaAST.resolveIdentifier(schema.ast) + return String.annotate({ + // Give the transport wrapper its own name so the decoded payload keeps its identifier. + identifier: identifier === undefined ? undefined : `${identifier}JsonString`, + expected: "a string that will be decoded as JSON", + contentMediaType: "application/json", + contentSchema: SchemaAST.toEncoded(schema.ast) + }).pipe(decodeTo(schema, SchemaTransformation.fromJsonString)) +} + +/** + * Type-level representation of {@link File}. + * + * @category file + * @since 4.0.0 + */ +export interface File extends instanceOf { + readonly "Rebuild": File +} + +/** + * Schema for JavaScript `File` objects. + * + * **Details** + * + * The default JSON serializer encodes a `File` as `{ data, type, name, lastModified }` + * where `data` is base64-encoded. + * + * @category file + * @since 4.0.0 + */ +export const File: File = instanceOf(globalThis.File, { + typeConstructor: { + _tag: "File" + }, + generation: { + runtime: `Schema.File`, + Type: `globalThis.File` + }, + expected: "File", + toCodecJson: () => + link()( + Struct({ + data: String.check(isBase64()), + type: String, + name: String, + lastModified: Number + }), + SchemaTransformation.transformOrFail({ + decode: (e) => + Result_.match(Encoding.decodeBase64(e.data), { + onFailure: (error) => + Effect.fail( + new SchemaIssue.InvalidValue(Option_.some(e.data), { + message: error.message + }) + ), + onSuccess: (bytes) => { + const buffer = new globalThis.Uint8Array(bytes) + return Effect.succeed( + new globalThis.File([buffer], e.name, { type: e.type, lastModified: e.lastModified }) + ) + } + }), + encode: (file) => + Effect.tryPromise({ + try: async () => { + const bytes = new globalThis.Uint8Array(await file.arrayBuffer()) + return { + data: Encoding.encodeBase64(bytes), + type: file.type, + name: file.name, + lastModified: file.lastModified + } + }, + catch: (e) => + new SchemaIssue.InvalidValue(Option_.some(file), { + message: globalThis.String(e) + }) + }) + }) + ) +}) + +/** + * Type-level representation of {@link FormData}. + * + * @category FormData + * @since 4.0.0 + */ +export interface FormData extends instanceOf { + readonly "Rebuild": FormData +} + +/** + * Schema for JavaScript `FormData` objects. + * + * **Details** + * + * The default JSON serializer encodes a `FormData` as an array of `[key, entry]` + * pairs where each entry is tagged as `"String"` or `"File"`. + * + * @category FormData + * @since 4.0.0 + */ +export const FormData: FormData = instanceOf(globalThis.FormData, { + typeConstructor: { + _tag: "FormData" + }, + generation: { + runtime: `Schema.FormData`, + Type: `globalThis.FormData` + }, + expected: "FormData", + toCodecJson: () => + link()( + ArraySchema( + Tuple([ + String, + Union([ + Struct({ _tag: tag("String"), value: String }), + Struct({ _tag: tag("File"), value: File }) + ]) + ]) + ), + SchemaTransformation.transformOrFail({ + decode: (e) => { + const out = new globalThis.FormData() + for (const [key, entry] of e) { + out.append(key, entry.value) + } + return Effect.succeed(out) + }, + encode: (formData) => { + return Effect.succeed( + globalThis.Array.from(formData.entries()).map(([key, value]) => { + if (typeof value === "string") { + return [key, { _tag: "String", value }] as const + } else { + return [key, { _tag: "File", value }] as const + } + }) + ) + } + }) + ) +}) + +/** + * Type-level representation returned by {@link fromFormData}. + * + * @category FormData + * @since 4.0.0 + */ +export interface fromFormData extends decodeTo { + readonly "Rebuild": fromFormData +} + +/** + * Schema for decoding `FormData` through a bracket-notation tree. + * + * **When to use** + * + * Use to decode browser or multipart form data into a structured schema value. + * + * **Details** + * + * The decoding process has two steps: + * + * 1. Parse `FormData` into a nested tree record. + * 2. Decode the parsed value with the given schema. + * + * You can express nested values using bracket notation. + * + * If you want to decode string fields into non-string primitive values, use + * `Schema.toCodecStringTree`. + * + * **Example** (Decoding a flat structure) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromFormData( + * Schema.Struct({ + * a: Schema.String + * }) + * ) + * + * const formData = new FormData() + * formData.append("a", "1") + * formData.append("b", "2") + * + * console.log(String(Schema.decodeUnknownExit(schema)(formData))) + * // Success({"a":"1"}) + * ``` + * + * **Example** (Decoding nested fields) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromFormData( + * Schema.Struct({ + * a: Schema.String, + * b: Schema.Struct({ + * c: Schema.String, + * d: Schema.String + * }) + * }) + * ) + * + * const formData = new FormData() + * formData.append("a", "1") + * formData.append("b[c]", "2") + * formData.append("b[d]", "3") + * + * console.log(String(Schema.decodeUnknownExit(schema)(formData))) + * // Success({"a":"1","b":{"c":"2","d":"3"}}) + * ``` + * + * **Example** (Parsing non-string values) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromFormData( + * Schema.toCodecStringTree( + * Schema.Struct({ + * a: Schema.Int + * }) + * ) + * ) + * + * const formData = new FormData() + * formData.append("a", "1") + * + * console.log(String(Schema.decodeUnknownExit(schema)(formData))) + * // Success({"a":1}) // Note: the value is a number + * ``` + * + * @category decoding + * @since 4.0.0 + */ +export function fromFormData(schema: S): fromFormData { + return FormData.pipe(decodeTo(schema, SchemaTransformation.fromFormData)) +} + +/** + * Type-level representation of {@link URLSearchParams}. + * + * @category search params + * @since 4.0.0 + */ +export interface URLSearchParams extends instanceOf { + readonly "Rebuild": URLSearchParams +} + +/** + * Schema for JavaScript `URLSearchParams` objects. + * + * **Details** + * + * The default JSON serializer encodes a `URLSearchParams` as a query string. + * + * @category search params + * @since 4.0.0 + */ +export const URLSearchParams: URLSearchParams = instanceOf(globalThis.URLSearchParams, { + typeConstructor: { + _tag: "URLSearchParams" + }, + generation: { + runtime: `Schema.URLSearchParams`, + Type: `globalThis.URLSearchParams` + }, + expected: "URLSearchParams", + toCodecJson: () => + link()( + String.annotate({ expected: "a query string that will be decoded as URLSearchParams" }), + SchemaTransformation.transform({ + decode: (e) => new globalThis.URLSearchParams(e), + encode: (params) => params.toString() + }) + ) +}) + +/** + * Type-level representation returned by {@link fromURLSearchParams}. + * + * @category search params + * @since 4.0.0 + */ +export interface fromURLSearchParams extends decodeTo { + readonly "Rebuild": fromURLSearchParams +} + +/** + * Schema for decoding `URLSearchParams` through a bracket-notation tree. + * + * **When to use** + * + * Use to decode query parameters into a structured schema value. + * + * **Details** + * + * The decoding process has two steps: + * + * 1. Parse `URLSearchParams` into a nested tree record. + * 2. Decode the parsed value with the given schema. + * + * You can express nested values using bracket notation. + * + * If you want to decode values that are not strings, use + * `Schema.toCodecStringTree`. This serializer preserves values such as + * numbers when compatible with the schema. + * + * **Example** (Decoding a flat structure) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromURLSearchParams( + * Schema.Struct({ + * a: Schema.String + * }) + * ) + * + * const urlSearchParams = new URLSearchParams("a=1&b=2") + * + * console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) + * // Success({"a":"1"}) + * ``` + * + * **Example** (Decoding nested fields) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromURLSearchParams( + * Schema.Struct({ + * a: Schema.String, + * b: Schema.Struct({ + * c: Schema.String, + * d: Schema.String + * }) + * }) + * ) + * + * const urlSearchParams = new URLSearchParams("a=1&b[c]=2&b[d]=3") + * + * console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) + * // Success({"a":"1","b":{"c":"2","d":"3"}}) + * ``` + * + * **Example** (Parsing non-string values) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.fromURLSearchParams( + * Schema.toCodecStringTree( + * Schema.Struct({ + * a: Schema.Int + * }) + * ) + * ) + * + * const urlSearchParams = new URLSearchParams("a=1&b=2") + * + * console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) + * // Success({"a":1}) // Note: the value is a number + * ``` + * + * @category decoding + * @since 4.0.0 + */ +export function fromURLSearchParams(schema: S): fromURLSearchParams { + return URLSearchParams.pipe(decodeTo(schema, SchemaTransformation.fromURLSearchParams)) +} + +/** + * Type-level representation of {@link Finite}. + * + * @category Number + * @since 3.10.0 + */ +export interface Finite extends Number { + readonly "Rebuild": Finite +} + +/** + * Schema for finite numbers, rejecting `NaN`, `Infinity`, and `-Infinity`. + * + * @category Number + * @since 3.10.0 + */ +export const Finite: Finite = Number.check(isFinite()) + +/** + * Type-level representation of {@link Int}. + * + * @category Number + * @since 3.10.0 + */ +export interface Int extends Number { + readonly "Rebuild": Int +} + +/** + * Schema for integers, rejecting `NaN`, `Infinity`, and `-Infinity`. + * + * @category Number + * @since 3.10.0 + */ +export const Int: Int = Number.check(isInt()) + +/** + * Type-level representation of {@link NumberFromString}. + * + * @category Number + * @since 3.10.0 + */ +export interface NumberFromString extends decodeTo { + readonly "Rebuild": NumberFromString +} + +/** + * Schema that parses a string into a `number` using JavaScript + * number coercion. + * + * **Details** + * + * Decoding: + * A `string` is decoded as a number, including possible non-finite values such as + * `NaN`, `Infinity`, and `-Infinity`. Use `FiniteFromString` to reject non-finite + * numbers. + * + * Encoding: + * A number is encoded as a `string`. + * + * @category Number + * @since 3.10.0 + */ +export const NumberFromString: NumberFromString = String.annotate({ + expected: "a string that will be decoded as a number" +}).pipe(decodeTo(Number, SchemaTransformation.numberFromString)) + +/** + * Type-level representation of {@link FiniteFromString}. + * + * @category Number + * @since 4.0.0 + */ +export interface FiniteFromString extends decodeTo { + readonly "Rebuild": FiniteFromString +} + +/** + * Schema that parses a string into a finite number. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as a finite number, rejecting `NaN`, `Infinity`, and + * `-Infinity` values. + * + * Encoding: + * - A finite number is encoded as a `string`. + * + * @category Number + * @since 4.0.0 + */ +export const FiniteFromString: FiniteFromString = String.annotate({ + expected: "a string that will be decoded as a finite number" +}).pipe(decodeTo(Finite, SchemaTransformation.numberFromString)) + +/** + * Type-level representation of {@link BigIntFromString}. + * + * @category BigInt + * @since 4.0.0 + */ +export interface BigIntFromString extends decodeTo { + readonly "Rebuild": BigIntFromString +} + +/** + * Schema that parses a string into a `bigint`. + * + * **When to use** + * + * Use to parse signed base-10 integer strings into bigint values while encoding + * bigint values back to decimal strings. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as a `bigint`. + * + * Encoding: + * - A `bigint` is encoded as a `string`. + * + * **Gotchas** + * + * Decoding accepts only strings matching `^-?\d+$`. + * + * @see {@link isStringBigInt} for the string predicate used by this schema + * @see {@link BigInt} for validating values that are already bigint values + * @see {@link NumberFromString} for parsing JavaScript number strings, including non-finite values + * @see {@link BigDecimalFromString} for parsing decimal number strings + * + * @category BigInt + * @since 4.0.0 + */ +export const BigIntFromString: BigIntFromString = make(SchemaAST.bigIntString).pipe( + decodeTo(BigInt, SchemaTransformation.bigintFromString) +) + +/** + * Type-level representation of {@link Trimmed}. + * + * @category string + * @since 3.10.0 + */ +export interface Trimmed extends String { + readonly "Rebuild": Trimmed +} + +/** + * Schema for strings that contains no leading or trailing whitespaces. + * + * @category string + * @since 3.10.0 + */ +export const Trimmed: Trimmed = String.check(isTrimmed()) + +/** + * Type-level representation of {@link Trim}. + * + * @category string + * @since 3.10.0 + */ +export interface Trim extends decodeTo { + readonly "Rebuild": Trim +} + +/** + * Schema that trims whitespace from a string. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as a string with no leading or trailing whitespaces. + * + * Encoding: + * - The trimmed string is encoded as is. + * + * @category string + * @since 3.10.0 + */ +export const Trim: Trim = String.annotate({ + expected: "a string that will be decoded as a trimmed string" +}).pipe(decodeTo(Trimmed, SchemaTransformation.trim())) + +/** + * Type-level representation of {@link StringFromBase64}. + * + * @category string + * @since 3.10.0 + */ +export interface StringFromBase64 extends decodeTo { + readonly "Rebuild": StringFromBase64 +} + +/** + * Decodes a base64 (RFC4648) encoded string into a UTF-8 string. + * + * **Details** + * + * Decoding: + * - A **valid** base64 encoded string is decoded as a UTF-8 `string`. + * + * Encoding: + * - A `string` is encoded as a base64-encoded string. + * + * @category string + * @since 3.10.0 + */ +export const StringFromBase64: StringFromBase64 = String.annotate({ + expected: "a base64 encoded string that will be decoded as a UTF-8 string" +}).pipe( + decodeTo(String, SchemaTransformation.stringFromBase64String) +) + +/** + * Type-level representation of {@link StringFromBase64Url}. + * + * @category string + * @since 3.10.0 + */ +export interface StringFromBase64Url extends decodeTo { + readonly "Rebuild": StringFromBase64Url +} + +/** + * Decodes a base64 (URL) encoded string into a UTF-8 string. + * + * **Details** + * + * Decoding: + * - A **valid** base64 (URL) encoded string is decoded as a UTF-8 `string`. + * + * Encoding: + * - A `string` is encoded as a base64 (URL) encoded string. + * + * @category string + * @since 3.10.0 + */ +export const StringFromBase64Url: StringFromBase64Url = String.annotate({ + expected: "a base64 (URL) encoded string that will be decoded as a UTF-8 string" +}).pipe( + decodeTo(String, SchemaTransformation.stringFromBase64UrlString) +) + +/** + * Type-level representation of {@link StringFromHex}. + * + * @category string + * @since 3.10.0 + */ +export interface StringFromHex extends decodeTo { + readonly "Rebuild": StringFromHex +} + +/** + * Decodes a hex encoded string into a UTF-8 string. + * + * **Details** + * + * Decoding: + * - A **valid** hex encoded string is decoded as a UTF-8 `string`. + * + * Encoding: + * - A `string` is encoded as a hex string. + * + * @category string + * @since 3.10.0 + */ +export const StringFromHex: StringFromHex = String.annotate({ + expected: "a hex encoded string that will be decoded as a UTF-8 string" +}).pipe( + decodeTo(String, SchemaTransformation.stringFromHexString) +) + +/** + * Type-level representation of {@link StringFromUriComponent}. + * + * @category string + * @since 3.12.0 + */ +export interface StringFromUriComponent extends decodeTo { + readonly "Rebuild": StringFromUriComponent +} + +/** + * Decodes a URI component encoded string into a UTF-8 string. + * Can be used to store data in a URL. + * + * **Details** + * + * Decoding: + * - A **valid** URI component encoded string is decoded as a UTF-8 `string`. + * + * Encoding: + * - A `string` is encoded as a URI component encoded string. + * + * **Example** (Decoding URI component strings) + * + * ```ts + * import { Schema } from "effect" + * + * const PaginationSchema = Schema.Struct({ + * maxItemPerPage: Schema.Number, + * page: Schema.Number + * }) + * + * const UrlSchema = Schema.StringFromUriComponent.pipe( + * Schema.decodeTo(Schema.fromJsonString(PaginationSchema)) + * ) + * + * console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 })) + * // %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D + * ``` + * + * @category string + * @since 3.12.0 + */ +export const StringFromUriComponent: StringFromUriComponent = String.annotate({ + expected: "a URI component encoded string that will be decoded as a UTF-8 string" +}).pipe( + decodeTo(String, SchemaTransformation.stringFromUriComponent) +) + +/** + * Schema for property keys accepted by Effect schemas: finite `number`, + * `symbol`, or `string`. + * + * @category PropertyKey + * @since 4.0.0 + */ +export const PropertyKey = Union([Finite, Symbol, String]) + +/** + * Schema for a Standard Schema v1 failure result. + * + * **Details** + * + * The result contains an `issues` array where each issue has a message and an + * optional path made of property keys or keyed path segments. + * + * @category Standard Schema + * @since 4.0.0 + */ +export const StandardSchemaV1FailureResult = Struct({ + issues: ArraySchema(Struct({ + message: String, + path: optional(ArraySchema(Union([PropertyKey, Struct({ key: PropertyKey })]))) + })) +}) + +/** + * Type-level representation of {@link BooleanFromBit}. + * + * @category boolean + * @since 4.0.0 + */ +export interface BooleanFromBit extends decodeTo> { + readonly "Rebuild": BooleanFromBit +} + +/** + * Schema for a boolean parsed from 0 or 1. + * + * **When to use** + * + * Use when decoding data sources that represent booleans as `0 | 1` while + * keeping boolean values in the decoded model. + * + * **Details** + * + * Decoding accepts only `0 | 1`, maps `1` to `true`, and maps `0` to `false`. + * Encoding maps `true` to `1` and `false` to `0`. + * + * @see {@link Boolean} for validating values that are already booleans + * @see {@link Literals} for keeping bit literals instead of decoding them + * + * @category boolean + * @since 4.0.0 + */ +export const BooleanFromBit: BooleanFromBit = Literals([0, 1]).pipe( + decodeTo( + Boolean, + SchemaTransformation.transform({ + decode: (bit) => bit === 1, + encode: (bool) => bool ? 1 : 0 + }) + ) +) + +/** + * Type-level representation of {@link Uint8Array}. + * + * @category Uint8Array + * @since 4.0.0 + */ +export interface Uint8Array extends instanceOf> { + readonly "Rebuild": Uint8Array +} + +const Base64String = String.annotate({ + expected: "a base64 encoded string that will be decoded as Uint8Array", + format: "byte", + contentEncoding: "base64" +}) + +/** + * Schema for JavaScript `Uint8Array` objects. + * + * **Details** + * + * Default JSON serializer: + * + * The default JSON serializer encodes Uint8Array as a Base64 encoded string. + * + * @category Uint8Array + * @since 4.0.0 + */ +export const Uint8Array: Uint8Array = instanceOf(globalThis.Uint8Array, { + typeConstructor: { + _tag: "Uint8Array" + }, + generation: { + runtime: `Schema.Uint8Array`, + Type: `globalThis.Uint8Array` + }, + expected: "Uint8Array", + toCodecJson: () => + link>()( + Base64String, + SchemaTransformation.uint8ArrayFromBase64String + ), + toArbitrary: () => (fc) => fc.uint8Array() +}) + +/** + * Type-level representation of {@link Uint8ArrayFromBase64}. + * + * @category Uint8Array + * @since 3.10.0 + */ +export interface Uint8ArrayFromBase64 extends decodeTo { + readonly "Rebuild": Uint8ArrayFromBase64 +} + +/** + * Schema that decodes a base64 encoded string into a + * `Uint8Array`. + * + * **Details** + * + * Decoding: + * - A **valid** base64 encoded string is decoded as a `Uint8Array`. + * + * Encoding: + * - A `Uint8Array` is encoded as a base64-encoded string. + * + * @category Uint8Array + * @since 3.10.0 + */ +export const Uint8ArrayFromBase64: Uint8ArrayFromBase64 = Base64String.pipe( + decodeTo(Uint8Array, SchemaTransformation.uint8ArrayFromBase64String) +) + +/** + * Type-level representation of {@link Uint8ArrayFromBase64Url}. + * + * @category Uint8Array + * @since 3.10.0 + */ +export interface Uint8ArrayFromBase64Url extends decodeTo { + readonly "Rebuild": Uint8ArrayFromBase64Url +} + +/** + * Schema that decodes a base64 (URL) encoded string into a + * `Uint8Array`. + * + * **Details** + * + * Decoding: + * - A **valid** base64 (URL) encoded string is decoded as a `Uint8Array`. + * + * Encoding: + * - A `Uint8Array` is encoded as a base64 (URL) encoded string. + * + * @category Uint8Array + * @since 3.10.0 + */ +export const Uint8ArrayFromBase64Url: Uint8ArrayFromBase64Url = String.annotate({ + expected: "a base64 (URL) encoded string that will be decoded as a Uint8Array" +}).pipe( + decodeTo(Uint8Array, { + decode: SchemaGetter.decodeBase64Url(), + encode: SchemaGetter.encodeBase64Url() + }) +) + +/** + * Type-level representation of {@link Uint8ArrayFromHex}. + * + * @category Uint8Array + * @since 3.10.0 + */ +export interface Uint8ArrayFromHex extends decodeTo { + readonly "Rebuild": Uint8ArrayFromHex +} + +/** + * Schema that decodes a hex encoded string into a + * `Uint8Array`. + * + * **Details** + * + * Decoding: + * - A **valid** hex encoded string is decoded as a `Uint8Array`. + * + * Encoding: + * - A `Uint8Array` is encoded as a hex encoded string. + * + * @category Uint8Array + * @since 3.10.0 + */ +export const Uint8ArrayFromHex: Uint8ArrayFromHex = String.annotate({ + expected: "a hex encoded string that will be decoded as a Uint8Array" +}).pipe( + decodeTo(Uint8Array, { + decode: SchemaGetter.decodeHex(), + encode: SchemaGetter.encodeHex() + }) +) + +/** + * Type-level representation of {@link DateTimeUtc}. + * + * @category DateTime + * @since 3.10.0 + */ +export interface DateTimeUtc extends declare { + readonly "Rebuild": DateTimeUtc +} + +/** + * Schema for `DateTime.Utc` values. + * + * **When to use** + * + * Use to validate existing `DateTime.Utc` schema values and use the default JSON + * codec that represents them as UTC ISO strings. + * + * **Details** + * + * The default JSON codec decodes UTC ISO strings into `DateTime.Utc` values and + * encodes `DateTime.Utc` values as UTC ISO strings. + * + * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values + * @see {@link DateTimeUtcFromDate} for decoding JavaScript Date values into UTC values + * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values + * @see {@link DateTimeZoned} for preserving zoned DateTime values + * + * @category DateTime + * @since 3.10.0 + */ +export const DateTimeUtc: DateTimeUtc = declare( + (u) => DateTime.isDateTime(u) && DateTime.isUtc(u), + { + typeConstructor: { + _tag: "effect/DateTime.Utc" + }, + generation: { + runtime: `Schema.DateTimeUtc`, + Type: `DateTime.Utc`, + importDeclaration: `import * as DateTime from "effect/DateTime"` + }, + expected: "DateTime.Utc", + toCodecJson: () => + link()( + String, + SchemaTransformation.dateTimeUtcFromString + ), + toArbitrary: () => (fc, ctx) => + fc.date(dateArbitraryConstraints( + ctx?.constraint, + ctx?.constraint?.ordered?.order === DateTime.Order ? ctx.constraint.ordered : undefined, + { valid: true }, + DateTime.toDateUtc + )) + .map((date) => DateTime.fromDateUnsafe(date)), + toFormatter: () => (utc) => utc.toString(), + toEquivalence: () => DateTime.Equivalence + } +) + +/** + * Type-level representation of {@link DateTimeUtcFromDate}. + * + * @category DateTime + * @since 3.12.0 + */ +export interface DateTimeUtcFromDate extends decodeTo { + readonly "Rebuild": DateTimeUtcFromDate +} + +/** + * Schema that decodes a `Date` into a `DateTime.Utc`. + * + * **When to use** + * + * Use when you need to decode valid JavaScript `Date` objects into + * `DateTime.Utc` values. + * + * **Details** + * + * Decoding: + * - A **valid** `Date` is decoded as a `DateTime.Utc` + * + * Encoding: + * - A `DateTime.Utc` is encoded as a `Date` + * + * @see {@link DateTimeUtc} for validating values that are already `DateTime.Utc` + * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values + * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values + * @see {@link DateValid} for validating Date instances without converting them + * + * @category DateTime + * @since 3.12.0 + */ +export const DateTimeUtcFromDate: DateTimeUtcFromDate = DateValid.pipe( + decodeTo(DateTimeUtc, { + decode: SchemaGetter.dateTimeUtcFromInput(), + encode: SchemaGetter.transform(DateTime.toDateUtc) + }) +) + +/** + * Type-level representation of {@link DateTimeUtcFromString}. + * + * @category DateTime + * @since 4.0.0 + */ +export interface DateTimeUtcFromString extends decodeTo { + readonly "Rebuild": DateTimeUtcFromString +} + +/** + * Schema that decodes a date-time string into a `DateTime.Utc`. + * + * **Details** + * + * Decoding: + * + * - A string accepted by `DateTime.make` is parsed and normalized to UTC. Strings + * without an explicit zone are interpreted as UTC. + * + * Encoding: + * + * - A `DateTime.Utc` is encoded as a UTC ISO 8601 string. + * + * @see {@link DateTimeUtcFromDate} for decoding JavaScript Date values into UTC values + * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values + * @see {@link DateFromString} for decoding strings into JavaScript Date instances + * + * @category DateTime + * @since 4.0.0 + */ +export const DateTimeUtcFromString: DateTimeUtcFromString = String.annotate({ + expected: "a string that will be decoded as a DateTime.Utc" +}).pipe( + decodeTo( + DateTimeUtc, + SchemaTransformation.dateTimeUtcFromString + ) +) + +/** + * Type-level representation of {@link DateTimeUtcFromMillis}. + * + * @category DateTime + * @since 4.0.0 + */ +export interface DateTimeUtcFromMillis extends decodeTo, Number> { + readonly "Rebuild": DateTimeUtcFromMillis +} + +/** + * Schema that decodes a number into a `DateTime.Utc`. + * + * **Details** + * + * Decoding: + * - A number of milliseconds since the Unix epoch is decoded as a `DateTime.Utc` + * + * Encoding: + * - A `DateTime.Utc` is encoded as a number of milliseconds since the Unix epoch. + * + * @see {@link DateTimeUtcFromDate} for decoding JavaScript Date values into UTC values + * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values + * @see {@link DateFromMillis} for decoding epoch milliseconds into JavaScript Date instances + * + * @category DateTime + * @since 4.0.0 + */ +export const DateTimeUtcFromMillis: DateTimeUtcFromMillis = Number.pipe( + decodeTo(DateTimeUtc, { + decode: SchemaGetter.dateTimeUtcFromInput(), + encode: SchemaGetter.transform(DateTime.toEpochMillis) + }) +) + +/** + * Type-level representation of {@link TimeZoneOffset}. + * + * @category DateTime + * @since 3.10.0 + */ +export interface TimeZoneOffset extends declare { + readonly "Rebuild": TimeZoneOffset +} + +/** + * Schema for `DateTime.TimeZone.Offset` values. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes `DateTime.TimeZone.Offset` as a number (offset in milliseconds) + * + * @category DateTime + * @since 3.10.0 + */ +export const TimeZoneOffset: TimeZoneOffset = declare( + DateTime.isTimeZoneOffset, + { + typeConstructor: { + _tag: "effect/DateTime.TimeZone.Offset" + }, + generation: { + runtime: `Schema.TimeZoneOffset`, + Type: `DateTime.TimeZone.Offset`, + importDeclaration: `import * as DateTime from "effect/DateTime"` + }, + expected: "DateTime.TimeZone.Offset", + toCodecJson: () => + link()( + Number, + SchemaTransformation.timeZoneOffsetFromNumber + ), + toArbitrary: () => (fc) => + fc.integer({ min: -12 * 60 * 60 * 1000, max: 14 * 60 * 60 * 1000 }).map((n) => DateTime.zoneMakeOffset(n)), + toFormatter: () => (tz) => DateTime.zoneToString(tz), + toEquivalence: () => (a, b) => a.offset === b.offset + } +) + +/** + * Type-level representation of {@link TimeZoneNamed}. + * + * @category DateTime + * @since 3.10.0 + */ +export interface TimeZoneNamed extends declare { + readonly "Rebuild": TimeZoneNamed +} + +const TimeZoneNamedString = String.annotate({ expected: "an IANA time zone identifier" }) + +/** + * Schema for `DateTime.TimeZone.Named` values. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes `DateTime.TimeZone.Named` as a string (IANA time zone identifier) + * + * @category DateTime + * @since 3.10.0 + */ +export const TimeZoneNamed: TimeZoneNamed = declare( + DateTime.isTimeZoneNamed, + { + typeConstructor: { + _tag: "effect/DateTime.TimeZone.Named" + }, + generation: { + runtime: `Schema.TimeZoneNamed`, + Type: `DateTime.TimeZone.Named`, + importDeclaration: `import * as DateTime from "effect/DateTime"` + }, + expected: "DateTime.TimeZone.Named", + toCodecJson: () => + link()( + TimeZoneNamedString, + SchemaTransformation.timeZoneNamedFromString + ), + toArbitrary: () => (fc) => + fc.constantFrom( + ...["UTC", "Europe/London", "America/New_York", "Asia/Tokyo", "Australia/Sydney"].map( + DateTime.zoneMakeNamedUnsafe + ) + ), + toFormatter: () => (tz) => DateTime.zoneToString(tz), + toEquivalence: () => (a, b) => a.id === b.id + } +) + +/** + * Type-level representation of {@link TimeZoneNamedFromString}. + * + * @category DateTime + * @since 4.0.0 + */ +export interface TimeZoneNamedFromString extends decodeTo { + readonly "Rebuild": TimeZoneNamedFromString +} + +/** + * Schema that parses an IANA time zone identifier string into a `DateTime.TimeZone.Named`. + * + * **Details** + * + * Decoding: + * - A `string` is decoded as a `DateTime.TimeZone.Named`. + * + * Encoding: + * - A `DateTime.TimeZone.Named` is encoded as a `string`. + * + * @category DateTime + * @since 4.0.0 + */ +export const TimeZoneNamedFromString: TimeZoneNamedFromString = TimeZoneNamedString.pipe( + decodeTo(TimeZoneNamed, SchemaTransformation.timeZoneNamedFromString) +) + +/** + * Type-level representation of {@link TimeZone}. + * + * @category DateTime + * @since 3.10.0 + */ +export interface TimeZone extends declare { + readonly "Rebuild": TimeZone +} + +const TimeZoneString = String.annotate({ + expected: "a time zone string (IANA identifier or offset like +03:00)" +}) + +/** + * Schema for `DateTime.TimeZone` values. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes `DateTime.TimeZone` as a string (IANA identifier or offset like + * `+03:00`) + * + * @category DateTime + * @since 3.10.0 + */ +export const TimeZone: TimeZone = declare( + DateTime.isTimeZone, + { + typeConstructor: { + _tag: "effect/DateTime.TimeZone" + }, + generation: { + runtime: `Schema.TimeZone`, + Type: `DateTime.TimeZone`, + importDeclaration: `import * as DateTime from "effect/DateTime"` + }, + expected: "DateTime.TimeZone", + toCodecJson: () => + link()( + TimeZoneString, + SchemaTransformation.timeZoneFromString + ), + toArbitrary: () => (fc) => + fc.oneof( + fc.integer({ min: -12 * 60 * 60 * 1000, max: 14 * 60 * 60 * 1000 }).map((n) => DateTime.zoneMakeOffset(n)), + fc.constantFrom( + ...["UTC", "Europe/London", "America/New_York", "Asia/Tokyo", "Australia/Sydney"].map( + DateTime.zoneMakeNamedUnsafe + ) + ) + ), + toFormatter: () => (tz) => DateTime.zoneToString(tz), + toEquivalence: () => (a, b) => DateTime.zoneToString(a) === DateTime.zoneToString(b) + } +) + +/** + * Type-level representation of {@link TimeZoneFromString}. + * + * @category DateTime + * @since 4.0.0 + */ +export interface TimeZoneFromString extends decodeTo { + readonly "Rebuild": TimeZoneFromString +} + +/** + * Schema that parses a time zone string into a `DateTime.TimeZone`. + * + * **Details** + * + * Decoding: + * - A `string` (IANA identifier or offset like `+03:00`) is decoded as a `DateTime.TimeZone`. + * + * Encoding: + * - A `DateTime.TimeZone` is encoded as a `string`. + * + * @category DateTime + * @since 4.0.0 + */ +export const TimeZoneFromString: TimeZoneFromString = TimeZoneString.pipe( + decodeTo(TimeZone, SchemaTransformation.timeZoneFromString) +) + +/** + * Type-level representation of {@link DateTimeZoned}. + * + * @category DateTime + * @since 3.10.0 + */ +export interface DateTimeZoned extends declare { + readonly "Rebuild": DateTimeZoned +} + +const DateTimeZonedString = String.annotate({ + expected: "a zoned DateTime string (e.g. 2024-01-01T00:00:00.000+00:00[Europe/London])" +}) + +/** + * Schema for `DateTime.Zoned` values. + * + * **Details** + * + * Default JSON serializer: + * + * - encodes offset zones as an ISO date-time with a numeric offset, such as + * `YYYY-MM-DDTHH:mm:ss.sss+HH:MM` + * - encodes named zones by appending the IANA identifier in brackets, such as + * `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]` + * + * @category DateTime + * @since 3.10.0 + */ +export const DateTimeZoned: DateTimeZoned = declare( + (u) => DateTime.isDateTime(u) && DateTime.isZoned(u), + { + typeConstructor: { + _tag: "effect/DateTime.Zoned" + }, + generation: { + runtime: `Schema.DateTimeZoned`, + Type: `DateTime.Zoned`, + importDeclaration: `import * as DateTime from "effect/DateTime"` + }, + expected: "DateTime.Zoned", + toCodecJson: () => + link()( + DateTimeZonedString, + SchemaTransformation.dateTimeZonedFromString + ), + toArbitrary: () => (fc, ctx) => + fc.tuple( + fc.date(dateArbitraryConstraints( + ctx?.constraint, + ctx?.constraint?.ordered?.order === DateTime.Order ? ctx.constraint.ordered : undefined, + { + max: new globalThis.Date(8640000000000000 - 14 * 60 * 60 * 1000), + min: new globalThis.Date(-8640000000000000 + 14 * 60 * 60 * 1000), + valid: true + }, + DateTime.toDateUtc + )), + fc.constantFrom("UTC", "Europe/London", "America/New_York", "Asia/Tokyo", "Australia/Sydney") + ).map(([date, zone]) => DateTime.makeZonedUnsafe(date, { timeZone: zone })), + toFormatter: () => (zoned) => DateTime.formatIsoZoned(zoned), + toEquivalence: () => DateTime.Equivalence + } +) + +/** + * Type-level representation of {@link DateTimeZonedFromString}. + * + * @category DateTime + * @since 4.0.0 + */ +export interface DateTimeZonedFromString extends decodeTo { + readonly "Rebuild": DateTimeZonedFromString +} + +/** + * Schema that parses a zoned DateTime string into a `DateTime.Zoned`. + * + * **Details** + * + * Decoding: + * - A `string` (e.g. `2024-01-01T00:00:00.000+00:00[Europe/London]`) is decoded as a `DateTime.Zoned`. + * + * Encoding: + * - A `DateTime.Zoned` is encoded as a `string`. + * + * @category DateTime + * @since 4.0.0 + */ +export const DateTimeZonedFromString: DateTimeZonedFromString = DateTimeZonedString.pipe( + decodeTo(DateTimeZoned, SchemaTransformation.dateTimeZonedFromString) +) + +// ----------------------------------------------------------------------------- +// Class +// ----------------------------------------------------------------------------- + +/** + * Type-level representation returned by {@link Class}. + * + * @category models + * @since 3.10.0 + */ +export interface Class extends + BottomLazy< + SchemaAST.Declaration, + decodeTo, S>, + readonly [S], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": Self + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": RequiredKeys extends never ? void | S["~type.make.in"] + : S["~type.make.in"] + readonly "~type.make": Self + readonly "Iso": S["Iso"] + new( + ...args: {} extends S["~type.make.in"] ? [props?: S["~type.make.in"], options?: MakeOptions] + : [props: S["~type.make.in"], options?: MakeOptions] + ): S["Type"] & Inherited + readonly identifier: string + readonly fields: S["fields"] + + /** + * Returns a new struct with the fields modified by the provided function. + * + * **Details** + * + * Options: + * + * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints + * that were attached to the original struct. Defaults to `false`. + * + * **Warning**: This is an unsafe operation. Since `mapFields` + * transformations change the schema type, the original refinement functions + * may no longer be valid or safe to apply to the transformed schema. Only + * use this option if you have verified that your refinements remain correct + * after the transformation. + */ + mapFields( + f: (fields: S["fields"]) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Struct>> + + /** + * Returns a function that creates a schema-backed subclass with this class's + * fields plus additional fields. + * + * **When to use** + * + * Use when you need a subclass whose constructor validates both inherited + * fields and newly added fields. + * + * **Details** + * + * The returned function accepts either a field map or a `Struct`. When you + * pass a `Struct`, checks attached to that extension schema are preserved and + * combined with checks from the base class schema. + * + * **Gotchas** + * + * Checks from a `Struct` argument are evaluated against the full subclass + * value after inherited and extension fields are merged. Object-wide checks + * such as `isMaxProperties` count inherited fields too. + */ + extend( + identifier: string + ): { + ( + fields: NewFields, + annotations?: Annotations.Declaration>>]> + ): [Extended] extends [never] ? MissingSelfGeneric<"Base.extend"> : InheritStaticMembers< + Class>>, Self & Brand>, + Static + > + >( + schema: Extension, + annotations?: Annotations.Declaration< + Extended, + readonly [Struct>>] + > + ): [Extended] extends [never] ? MissingSelfGeneric<"Base.extend"> : InheritStaticMembers< + Class>>, Self & Brand>, + Static + > + } +} + +// Merges custom static members from a parent class onto the extended class, +// giving priority to the extended class's own members (e.g. schema-generated statics). +type InheritStaticMembers = C & Pick> + +const immerable: unique symbol = globalThis.Symbol.for("immer-draftable") as any + +const payloadToken = {} + +function makeClass< + Self, + S extends Struct, + Inherited extends new(...args: ReadonlyArray) => any +>( + Inherited: Inherited, + identifier: string, + struct: S, + annotations: Annotations.Declaration | undefined, + proto: ((identifier: string) => object) | undefined +): any { + const getClassSchema = getClassSchemaFactory(struct, identifier, annotations) + const ClassTypeId = getClassTypeId(identifier) // HMR support + + const out = class extends Inherited { + constructor(...[input, options]: ReadonlyArray) { + const internalOptions = options as MakeOptions | undefined + const payload = internalOptions?.["~payload"] + const value = payload?.token === payloadToken + ? payload.value + : struct.make(input ?? {}, options) + super(value, { ...options, disableChecks: true, "~payload": { token: payloadToken, value } }) + } + + static readonly [TypeId] = TypeId + + get [ClassTypeId]() { + return ClassTypeId + } + + static readonly [immerable] = true + + static readonly identifier = identifier + static readonly fields = struct.fields + + static get ast(): SchemaAST.Declaration { + return getClassSchema(this).ast + } + static pipe() { + return Pipeable.pipeArguments(this, arguments) + } + static rebuild(ast: SchemaAST.Declaration) { + return getClassSchema(this).rebuild(ast) + } + static make(input: S["~type.make.in"], options?: MakeOptions): Self { + return new this(input, options) + } + static makeOption(input: S["~type.make.in"], options?: MakeOptions): Option_.Option { + return SchemaParser.makeOption(getClassSchema(this) as any)(input ?? {}, options) as any + } + static makeEffect(input: S["~type.make.in"], options?: MakeOptions): Effect.Effect { + return (getClassSchema(this) as any).makeEffect(input ?? {}, options) + } + static annotate(annotations: Annotations.Declaration) { + return this.rebuild(SchemaAST.annotate(this.ast, annotations)) + } + static annotateKey(annotations: Annotations.Key) { + return this.rebuild(SchemaAST.annotateKey(this.ast, annotations)) + } + static check(...checks: readonly [SchemaAST.Check, ...Array>]) { + return this.rebuild(SchemaAST.appendChecks(this.ast, checks)) + } + static extend( + identifier: string + ) { + return ( + schema: Struct.Fields | Struct, + annotations?: Annotations.Declaration + ) => { + const extension = isStruct(schema) ? schema : Struct(schema) + const fields = { ...struct.fields, ...extension.fields } + const ast = SchemaAST.struct(fields, struct.ast.checks, { identifier }) + return makeClass( + this, + identifier, + makeStruct(SchemaAST.appendChecks(ast, extension.ast.checks), fields), + annotations, + proto + ) + } + } + static mapFields( + f: (fields: S["fields"]) => To, + options?: { + readonly unsafePreserveChecks?: boolean | undefined + } | undefined + ): Struct>> { + return struct.mapFields(f, options) + } + } + + if (proto !== undefined) { + Object.assign(out.prototype, proto(identifier)) + } + + return out +} + +function getClassTransformation(self: new(...args: ReadonlyArray) => any) { + return new SchemaTransformation.Transformation( + SchemaGetter.transform((input) => new self(input)), + SchemaGetter.passthrough() + ) +} + +function getClassTypeId(identifier: string) { + return `~effect/Schema/Class/${identifier}` +} + +function getClassSchemaFactory( + from: S, + identifier: string, + annotations: Annotations.Declaration | undefined +) { + let memo: decodeTo, S> | undefined + return ) => any) & { readonly identifier: string }>( + self: Self + ): decodeTo, S> => { + if (memo !== undefined) { + return memo + } + const transformation = getClassTransformation(self) + const to = make>( + new SchemaAST.Declaration( + [from.ast], + () => (input, ast) => { + return input instanceof self || + Predicate.hasProperty(input, getClassTypeId(identifier)) ? + Effect.succeed(input) : + Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + }, + { + identifier, + [SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation), + toCodec: ([from]: readonly [ConstraintCodec]) => + new SchemaAST.Link(from.ast, transformation), + toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter]) => () => ({ + arbitrary: from.arbitrary.map((args: S["Type"]) => new self(args)), + terminal: from.terminal?.map((args: S["Type"]) => new self(args)) + }), + toFormatter: ([from]: readonly [Formatter]) => (t: Self) => `${self.identifier}(${from(t)})`, + "~sentinels": SchemaAST.collectSentinels(from.ast), + ...annotations + } + ) + ) + return memo = decodeTo, S>(to, transformation)(from) + } +} + +function isStruct(schema: Struct.Fields | Struct): schema is Struct { + return isSchema(schema) +} + +type MissingSelfGeneric = + `Missing \`Self\` generic - use \`class Self extends ${Usage}(...)\`` + +/** + * Creates a schema-backed class whose constructor validates input against a + * {@link Struct} schema. Construction throws a {@link SchemaError} on invalid + * input. + * + * **When to use** + * + * Use when you need a schema-backed data class with validated construction, + * schema-derived decoding/encoding, and class-style methods or inheritance. + * + * **Details** + * + * Pass the desired class type as the first type parameter. The second optional + * type parameter can be used to add nominal brands. + * + * **Gotchas** + * + * Passing `disableChecks` in the options skips constructor validation. + * + * **Example** (Defining a basic class) + * + * ```ts + * import { Schema } from "effect" + * + * class Person extends Schema.Class("Person")({ + * name: Schema.String, + * age: Schema.Number + * }) {} + * + * const alice = new Person({ name: "Alice", age: 30 }) + * console.log(alice.name) // "Alice" + * console.log(`${alice}`) // "Person({ name: Alice, age: 30 })" + * ``` + * + * **Example** (Extending a class) + * + * ```ts + * import { Schema } from "effect" + * + * class Animal extends Schema.Class("Animal")({ + * name: Schema.String + * }) {} + * + * class Dog extends Animal.extend("Dog")({ + * breed: Schema.String + * }) {} + * + * const dog = new Dog({ name: "Rex", breed: "Labrador" }) + * console.log(dog.name) // "Rex" + * console.log(dog.breed) // "Labrador" + * ``` + * + * @see {@link TaggedClass} for adding a `_tag` literal field to the class schema + * @see {@link ErrorClass} for defining schema-backed error classes + * @see {@link TaggedErrorClass} for defining tagged schema-backed error classes + * + * @category constructors + * @since 3.10.0 + */ +export const Class: { + (identifier: string): { + ( + fields: Fields, + annotations?: Annotations.Declaration]> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class, Brand> + >( + schema: S, + annotations?: Annotations.Declaration + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class + } +} = (identifier: string) => +( + schema: Struct.Fields | Struct, + annotations?: Annotations.Declaration]> +): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class, Brand> => { + const struct = isStruct(schema) ? schema : Struct(schema) + return makeClass( + Data.Class, + identifier, + struct, + annotations, + (identifier) => ({ + toString() { + return `${identifier}(${format({ ...this })})` + } + }) + ) +} + +/** + * Defines a schema-backed class with an automatically populated `_tag` field. + * + * **When to use** + * + * Use to define class instances that are validated by a schema and participate + * in tagged union matching. + * + * **Details** + * + * The optional `identifier` parameter overrides the schema identifier; + * it defaults to the `tag` value. + * + * **Example** (Defining a tagged class) + * + * ```ts + * import { Schema } from "effect" + * + * class Circle extends Schema.TaggedClass()("Circle", { + * radius: Schema.Number + * }) {} + * + * const c = new Circle({ radius: 5 }) + * console.log(c._tag) // "Circle" + * console.log(c.radius) // 5 + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export const TaggedClass: { + (identifier?: string): { + ( + tag: Tag, + fields: Fields, + annotations?: Annotations.Declaration]> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedClass"> : Class, Brand> + >( + tag: Tag, + schema: S, + annotations?: Annotations.Declaration< + Self, + readonly [Struct } & S["fields"]>>] + > + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedClass"> + : Class } & S["fields"]>>, Brand> + } +} = (identifier?: string) => { + return ( + tagValue: string, + schema: Struct.Fields | Struct, + annotations?: Annotations.Declaration]> + ): any => { + const struct = isStruct(schema) ? + schema.mapFields((fields) => ({ _tag: tag(tagValue), ...fields }), { + unsafePreserveChecks: true + }) : + TaggedStruct(tagValue, schema) + return Class(identifier ?? tagValue)( + struct, + annotations as Annotations.Declaration + ) + } +} + +/** + * Creates a schema-backed error class that can be used as a typed, + * yieldable error in Effect programs. Combines {@link Class} validation with + * the `YieldableError` interface so instances can be yielded directly inside + * `Effect.gen`. + * + * **Example** (Schema-backed error) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * class NotFound extends Schema.ErrorClass("NotFound")({ + * id: Schema.Number + * }) {} + * + * const program = Effect.gen(function*() { + * yield* new NotFound({ id: 1 }) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const ErrorClass: { + (identifier: string): { + ( + fields: Fields, + annotations?: Annotations.Declaration]> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> + : Class, Cause_.YieldableError & Brand> + >( + schema: S, + annotations?: Annotations.Declaration + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> : Class + } +} = (identifier: string) => +( + schema: Struct.Fields | Struct, + annotations?: Annotations.Declaration]> +): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> + : Class, Cause_.YieldableError & Brand> => +{ + const struct = isStruct(schema) ? schema : Struct(schema) + const self = makeClass( + core.Error, + identifier, + struct, + annotations, + (identifier) => ({ + name: identifier + }) + ) + return self +} + +/** + * Defines a schema-backed yieldable error class with an automatically populated + * `_tag` field. + * + * **When to use** + * + * Use to define typed errors that are schema validated, yielded in `Effect.gen`, + * and matched as tagged union members. + * + * **Example** (Defining a tagged error class) + * + * ```ts + * import { Effect, Schema } from "effect" + * + * class NotFound extends Schema.TaggedErrorClass()("NotFound", { + * id: Schema.Number + * }) {} + * + * const program = Effect.gen(function*() { + * yield* new NotFound({ id: 42 }) + * }) + * ``` + * + * @category constructors + * @since 3.10.0 + */ +export const TaggedErrorClass: { + (identifier?: string): { + ( + tag: Tag, + fields: Fields, + annotations?: Annotations.Declaration]> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedErrorClass"> + : Class, Cause_.YieldableError & Brand> + >( + tag: Tag, + schema: S, + annotations?: Annotations.Declaration< + Self, + readonly [Struct } & S["fields"]>>] + > + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedErrorClass"> + : Class } & S["fields"]>>, Cause_.YieldableError & Brand> + } +} = (identifier?: string) => { + return ( + tagValue: string, + schema: Struct.Fields | Struct, + annotations?: Annotations.Declaration]> + ): any => { + const struct = isStruct(schema) ? + schema.mapFields((fields) => ({ _tag: tag(tagValue), ...fields }), { + unsafePreserveChecks: true + }) : + TaggedStruct(tagValue, schema) + return ErrorClass(identifier ?? tagValue)( + struct, + annotations as Annotations.Declaration + ) + } +} + +// ----------------------------------------------------------------------------- +// Arbitrary +// ----------------------------------------------------------------------------- + +/** + * A thunk that, given the `fast-check` module, returns an `Arbitrary`. + * Use this type when you need to defer instantiation of the arbitrary, for + * example to support recursive schemas. + * + * @category Arbitrary + * @since 4.0.0 + */ +export type LazyArbitrary = (fc: typeof FastCheck) => FastCheck.Arbitrary + +/** + * Derives a {@link LazyArbitrary} from a schema. The result is memoized so + * repeated calls with the same schema are cheap. + * + * **Details** + * + * Prefer {@link toArbitrary} when you need the arbitrary directly, or when you + * want derivation diagnostics via `{ report: true }`. Unsupported schema + * nodes, impossible constraints, invalid candidates, and recursive schemas + * without a finite terminal path fail immediately. + * + * @category Arbitrary + * @since 4.0.0 + */ +export function toArbitraryLazy(schema: S): LazyArbitrary { + const lawc = InternalArbitrary.memoized(schema.ast) + return (fc) => lawc(fc, {}) +} + +/** + * Derives a `fast-check` `Arbitrary` from a schema for property-based + * testing. The derived arbitrary generates values that satisfy the schema. + * + * **Details** + * + * Constraints refine base generators; candidates add weighted sources while + * filters still validate every value. `{ report: true }` returns warnings such + * as `OpaqueFilter`, while derivation errors remain fail-fast. Recursive + * schemas use terminal branches and fail when no finite terminal path exists. + * + * **Example** (Generating arbitrary values) + * + * ```ts + * import { Schema } from "effect" + * import * as FastCheck from "fast-check" + * + * const PersonArb = Schema.toArbitrary( + * Schema.Struct({ name: Schema.String, age: Schema.Number }) + * ) + * + * // Sample a random value + * const sample = FastCheck.sample(PersonArb, 1)[0] + * console.log(typeof sample.name) // "string" + * ``` + * + * @category Arbitrary + * @since 4.0.0 + */ +export function toArbitrary(schema: S): FastCheck.Arbitrary +export function toArbitrary( + schema: S, + options: { readonly report: true } +): Annotations.ToArbitrary.WithReport> +export function toArbitrary( + schema: S, + options?: { readonly report?: boolean } +): FastCheck.Arbitrary | Annotations.ToArbitrary.WithReport> { + if (options?.report === true) { + const lawc = InternalArbitrary.memoized(schema.ast) + const report = InternalArbitrary.makeReport() + InternalArbitrary.collectReport(schema.ast, report) + return { + value: lawc(FastCheck, {}), + report: InternalArbitrary.toReport(report) + } + } + return toArbitraryLazy(schema)(FastCheck) +} + +// ----------------------------------------------------------------------------- +// Formatter +// ----------------------------------------------------------------------------- + +/** + * Attaches a custom formatter used by `toFormatter`. + * + * **Details** + * + * Use this when the formatter derived from the schema structure is not suitable. + * The annotation is applied through this helper because adding it directly to + * `Annotations.Bottom` would make schemas invariant. + * + * @category Formatter + * @since 4.0.0 + */ +export function overrideToFormatter(toFormatter: () => Formatter) { + return (self: S): S["Rebuild"] => { + return self.annotate({ toFormatter }) + } +} + +/** + * Derives a string formatter function from a schema. The formatter converts + * a value to its human-readable string representation, recursing into structs, + * arrays, and unions. + * + * **Details** + * + * The optional `onBefore` hook lets you intercept specific AST nodes before + * the default formatting logic runs. + * + * @category Formatter + * @since 4.0.0 + */ +export function toFormatter(schema: S, options?: { + readonly onBefore?: + | ((ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Formatter) => Formatter | undefined) + | undefined +}): Formatter { + return recur(schema.ast) + + function recur(ast: SchemaAST.AST): Formatter { + // --------------------------------------------- + // handle annotation + // --------------------------------------------- + const annotation = InternalAnnotations.resolve(ast)?.["toFormatter"] + if (typeof annotation === "function") { + return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map(recur) : []) + } + // --------------------------------------------- + // handle onBefore + // --------------------------------------------- + if (options?.onBefore) { + const onBefore = options.onBefore(ast, recur) + if (onBefore !== undefined) { + return onBefore + } + } + // --------------------------------------------- + // handle base case + // --------------------------------------------- + return on(ast) + } + + function on(ast: SchemaAST.AST): Formatter { + switch (ast._tag) { + default: + return format + case "Never": + return () => "never" + case "Void": + return () => "void" + case "Arrays": { + const elements = ast.elements.map(recur) + const rest = ast.rest.map(recur) + return (t) => { + const out: Array = [] + let i = 0 + // --------------------------------------------- + // handle elements + // --------------------------------------------- + for (; i < elements.length; i++) { + if (t.length < i + 1) { + if (SchemaAST.isOptional(ast.elements[i])) { + continue + } + } else { + out.push(elements[i](t[i])) + } + } + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (rest.length > 0) { + const [head, ...tail] = rest + for (; i < t.length - tail.length; i++) { + out.push(head(t[i])) + } + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + for (let j = 0; j < tail.length; j++) { + out.push(tail[j](t[i + j])) + } + } + + return "[" + out.join(", ") + "]" + } + } + case "Objects": { + const propertySignatures = ast.propertySignatures.map((ps) => recur(ps.type)) + const indexSignatures = ast.indexSignatures.map((is) => recur(is.type)) + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return format + } + return (t) => { + const out: Array = [] + const visited = new Set() + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + for (let i = 0; i < propertySignatures.length; i++) { + const ps = ast.propertySignatures[i] + const name = ps.name + visited.add(name) + if (SchemaAST.isOptional(ps.type) && !Object.hasOwn(t, name)) { + continue + } + out.push(`${formatPropertyKey(name)}: ${propertySignatures[i](t[name])}`) + } + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + for (let i = 0; i < indexSignatures.length; i++) { + const keys = SchemaAST.getIndexSignatureKeys(t, ast.indexSignatures[i].parameter) + for (const key of keys) { + if (visited.has(key)) { + continue + } + visited.add(key) + out.push(`${formatPropertyKey(key)}: ${indexSignatures[i](t[key])}`) + } + } + + return out.length > 0 ? "{ " + out.join(", ") + " }" : "{}" + } + } + case "Union": { + const getCandidates = (t: any) => SchemaAST.getCandidates(t, ast.types) + return (t) => { + const candidates = getCandidates(t) + const refinements = candidates.map(SchemaParser._is) + for (let i = 0; i < candidates.length; i++) { + const is = refinements[i] + if (is(t)) { + return recur(candidates[i])(t) + } + } + return format(t) + } + } + case "Suspend": { + const get = SchemaAST.memoizeThunk(() => recur(ast.thunk())) + return (t) => get()(t) + } + } + } +} + +// ----------------------------------------------------------------------------- +// Equivalence +// ----------------------------------------------------------------------------- + +/** + * Overrides the equivalence derivation for a schema by supplying a custom + * `Equivalence`. + * + * **When to use** + * + * Use when you need a custom equivalence instead of the default structural + * equivalence derived by {@link toEquivalence}. + * + * @category instances + * @since 4.0.0 + */ +export function overrideToEquivalence(toEquivalence: () => Equivalence.Equivalence) { + return (self: S): S["Rebuild"] => self.annotate({ toEquivalence }) +} + +/** + * Derives an `Equivalence` from a schema. Two values are considered equal when + * every field (and nested field) compares equal according to the schema + * structure. + * + * **Example** (Comparing structs) + * + * ```ts + * import { Schema } from "effect" + * + * const eq = Schema.toEquivalence(Schema.Struct({ id: Schema.Number, name: Schema.String })) + * + * console.log(eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" })) // true + * console.log(eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" })) // false + * ``` + * + * @category instances + * @since 4.0.0 + */ +export function toEquivalence(schema: Schema): Equivalence.Equivalence { + return InternalEquivalence.toEquivalence(schema.ast) +} + +// ----------------------------------------------------------------------------- +// Representation +// ----------------------------------------------------------------------------- + +/** + * Derives an intermediate `SchemaRepresentation.Document` from a schema. This + * document is used internally by {@link toJsonSchemaDocument} and related + * functions to produce JSON Schema output. + * + * @category Representation + * @since 4.0.0 + */ +export function toRepresentation(schema: Constraint): SchemaRepresentation.Document { + return InternalStandard.fromAST(schema.ast) +} + +// ----------------------------------------------------------------------------- +// JsonSchema +// ----------------------------------------------------------------------------- + +/** + * Options for {@link toJsonSchemaDocument}. + * + * @category options + * @since 4.0.0 + */ +export interface ToJsonSchemaOptions { + /** + * Controls how additional properties are handled while resolving the JSON + * schema. + * + * **Details** + * + * Possible values include: + * - `false`: Disallow additional properties (default) + * - `true`: Allow additional properties + * - `JsonSchema`: Use the provided JSON Schema for additional properties + */ + readonly additionalProperties?: boolean | JsonSchema.JsonSchema | undefined + /** + * Controls whether to generate descriptions for checks (if the user has not + * provided them) based on the `expected` annotation of the check. + */ + readonly generateDescriptions?: boolean | undefined + /** + * A predicate that controls which additional annotation keys (beyond the + * standard JSON Schema keys) are included in the generated output. + * + * **When to use** + * + * Use when you need to include non-standard annotation keys in the generated + * JSON Schema, such as Monaco Editor properties (`markdownDescription`, + * `defaultSnippets`) or vendor extensions (`x-*`). + * + * **Details** + * + * Standard JSON Schema keys (`title`, `description`, `default`, `examples`, + * `readOnly`, `writeOnly`, `format`, `contentEncoding`, `contentMediaType`, + * `contentSchema`) are always included. This predicate is checked for any + * *other* annotation key. + * + * **Gotchas** + * + * Prefer whitelisting the custom annotation keys you want to emit instead of + * using a broad predicate such as `() => true`, because broad predicates can + * include Effect-specific annotations that are preserved for internal schema + * generation. + * + * **Example** (Including custom annotations) + * + * ```ts + * import { Schema } from "effect" + * + * const schema = Schema.String.annotate({ + * description: "A name", + * markdownDescription: "The **name** field" + * }) + * + * const doc = Schema.toJsonSchemaDocument(schema, { + * includeAnnotationKey: (key) => + * key === "markdownDescription" || key.startsWith("x-") + * }) + * + * console.log(doc.schema) + * // { + * // type: "string", + * // description: "A name", + * // markdownDescription: "The **name** field" + * // } + * ``` + */ + readonly includeAnnotationKey?: ((key: string) => boolean) | undefined +} + +/** + * Returns a JSON Schema document using draft 2020-12. + * + * **Details** + * + * The `options` parameter controls generation details such as additional + * properties and synthesized check descriptions; it does not change the draft + * target. + * + * **Gotchas** + * + * JSON Schema generation is best-effort. Some Effect schema semantics cannot + * be represented exactly in JSON Schema, and importing an emitted JSON Schema + * may produce an equivalent approximation rather than the original schema + * shape. + * + * @category converting + * @since 4.0.0 + */ +export function toJsonSchemaDocument( + schema: Constraint, + options?: ToJsonSchemaOptions +): JsonSchema.Document<"draft-2020-12"> { + const sd = toRepresentation(schema) + const jd = InternalStandard.toJsonSchemaDocument(sd, options) + return { + dialect: "draft-2020-12", + schema: jd.schema, + definitions: jd.definitions + } +} + +// ----------------------------------------------------------------------------- +// Canonical Codecs +// ----------------------------------------------------------------------------- + +/** + * Type-level representation returned by {@link toCodecJson}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecJson extends + BottomLazy< + S["ast"], + toCodecJson, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": Json + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +/** + * Derives a canonical JSON codec from a schema. The encoded form is `Json`, and + * decoding produces the schema's `Type`. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecJson(schema: S): toCodecJson { + return make(toCodecJsonTop(schema.ast), { schema }) +} + +const toCodecJsonTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + const out = toCodecJsonBase(ast, toCodecJsonTop) + return out !== ast && SchemaAST.isOptional(ast) ? SchemaAST.optionalKeyLastLink(out) : out +}) + +function toCodecJsonBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec + if (Predicate.isFunction(getLink)) { + const tps = SchemaAST.isDeclaration(ast) + ? ast.typeParameters.map((tp) => InternalSchema.make(SchemaAST.toEncoded(tp))) + : [] + const link = getLink(tps) + const to = recur(link.to) + return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + } + return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToNull]) + } + case "Unknown": + case "ObjectKeyword": + return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToJson]) + case "Undefined": + case "Void": + case "Literal": + case "Number": + return ast.toCodecJson() + case "UniqueSymbol": + case "Symbol": + case "BigInt": + return ast.toCodecStringTree() + case "Objects": { + if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) { + throw new globalThis.Error("Objects property names must be strings", { cause: ast }) + } + return ast.recur(recur, SchemaAST.parameterFromString) + } + case "Union": { + const sortedTypes = InternalSchema.jsonReorder(ast.types) + if (sortedTypes !== ast.types) { + return new SchemaAST.Union( + sortedTypes, + ast.mode, + ast.annotations, + ast.checks, + ast.encoding, + ast.context, + ast.encodingChecks + ).recur(recur) + } + return ast.recur(recur) + } + case "Arrays": + case "Suspend": + return ast.recur(recur) + } + // `Schema.Any` is used as an escape hatch + return ast +} + +/** + * Derives an isomorphism codec from a schema. The encoded form is the + * schema's `Iso` type — the intermediate representation used for round-tripping. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecIso(schema: S): Codec { + return make(toCodecIsoTop(SchemaAST.toType(schema.ast))) +} + +const toCodecIsoTop = memoize((ast: SchemaAST.AST): SchemaAST.AST => { + const out = toCodecIsoBase(ast, toCodecIsoTop) + return out !== ast && SchemaAST.isOptional(ast) ? SchemaAST.optionalKeyLastLink(out) : out +}) + +function toCodecIsoBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.toCodecIso ?? ast.annotations?.toCodec + if (Predicate.isFunction(getLink)) { + const link = getLink(ast.typeParameters.map((tp) => InternalSchema.make(tp))) + const to = recur(link.to) + return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + } + return ast + } + case "Arrays": + case "Objects": + case "Union": + case "Suspend": + return ast.recur(recur) + } + return ast +} + +/** + * A {@link Tree} of `string | undefined` nodes. Leaf values are either a + * string representation or `undefined` for opaque/declaration types. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export type StringTree = Tree + +/** + * Type-level representation returned by {@link toCodecStringTree}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecStringTree extends + BottomLazy< + S["ast"], + toCodecStringTree, + ReadonlyArray, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": StringTree + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + +/** + * Converts a schema to the StringTree canonical codec, where every leaf value + * becomes a string while preserving the original structure. + * + * **Details** + * + * Declarations are converted to `undefined` (unless they have a + * `toCodecJson` or `toCodec` annotation). + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecStringTree(schema: S): toCodecStringTree { + return make(serializerStringTree(schema.ast), { schema }) +} + +/** + * Type-level representation returned by {@link toCodecArrayFromSingle}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecArrayFromSingle extends + BottomLazy< + S["ast"], + toCodecArrayFromSingle, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] +} + +/** + * Allows array schemas to decode from either an array input or a single value + * input. + * + * **When to use** + * + * Use when you need to accept transport formats that may represent a + * single-item array as a bare value, such as query-string or form-data adapters. + * + * **Gotchas** + * + * This combinator is intentionally not part of `toCodecStringTree`; it adds a + * decoding convenience rather than a canonical StringTree representation. It + * does not parse comma-separated strings. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecArrayFromSingle(schema: S): toCodecArrayFromSingle { + return make(toCodecArrayFromSingleTop(schema.ast)) +} + +type XmlEncoderOptions = { + /** Root element name for the returned XML string. Default: "root" */ + readonly rootName?: string | undefined + /** When an array doesn't have a natural item name, use this. Default: "item" */ + readonly arrayItemName?: string | undefined + /** Pretty-print output. Default: true */ + readonly pretty?: boolean | undefined + /** Indentation used when pretty-printing. Default: " " (two spaces) */ + readonly indent?: string | undefined + /** Sort object keys for stable output. Default: true */ + readonly sortKeys?: boolean | undefined +} + +/** + * Derives an XML encoder from a codec. + * + * **Details** + * + * The returned function encodes a value through `toCodecStringTree` and returns + * an `Effect` that succeeds with the XML string or fails with `SchemaError` if + * codec encoding fails. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toEncoderXml( + codec: ConstraintCodec, + options?: XmlEncoderOptions +) { + const rootName = InternalAnnotations.resolveIdentifier(codec.ast) ?? InternalAnnotations.resolveTitle(codec.ast) + const serialize = encodeEffect(toCodecStringTree(codec)) + return (t: T): Effect.Effect => + serialize(t).pipe(Effect.map((stringTree) => stringTreeToXml(stringTree, { rootName, ...options }))) +} + +function stringTreeToXml(value: StringTree, options: XmlEncoderOptions): string { + const rootName = options.rootName ?? "root" + const arrayItemName = options.arrayItemName ?? "item" + const pretty = options.pretty ?? true + const indent = options.indent ?? " " + const sortKeys = options.sortKeys ?? true + + const seen = new Set() + const lines: Array = [] + + recur(rootName, value, 0) + return lines.join(pretty ? "\n" : "") + + function push(depth: number, text: string): void { + lines.push(pretty ? indent.repeat(depth) + text : text) + } + + function recur(tagName: string, node: StringTree, depth: number, originalNameForMeta?: string): void { + const { attrs, safe } = xml.tagInfo(tagName, originalNameForMeta) + + if (node === undefined) { + push(depth, `<${safe}${attrs}/>`) + } else if (typeof node === "string") { + push(depth, `<${safe}${attrs}>${xml.escapeText(node)}`) + } else if (typeof node !== "object" || node === null) { + push(depth, `<${safe}${attrs}>${xml.escapeText(format(node))}`) + } else { + if (seen.has(node)) throw new globalThis.Error("Cycle detected while serializing to XML.", { cause: node }) + seen.add(node) + try { + if (globalThis.globalThis.Array.isArray(node)) { + if (node.length === 0) { + push(depth, `<${safe}${attrs}/>`) + return + } + push(depth, `<${safe}${attrs}>`) + for (const item of node) recur(arrayItemName, item, depth + 1) + push(depth, ``) + return + } + + const obj = node as Record + const keys = Object.keys(obj) + if (sortKeys) keys.sort() + + if (keys.length === 0) { + push(depth, `<${safe}${attrs}/>`) + return + } + + push(depth, `<${safe}${attrs}>`) + for (const k of keys) { + recur(xml.parseTagName(k).safe, obj[k], depth + 1, k) + } + push(depth, ``) + } finally { + seen.delete(node) + } + } + } +} + +const xml = { + escapeText(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">") + }, + escapeAttribute(s: string): string { + return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">") + }, + parseTagName(name: string): { safe: string; changed: boolean } { + const original = name + let safe = name + if (!/^[A-Za-z_]/.test(safe)) safe = "_" + safe + safe = safe.replace(/[^A-Za-z0-9._-]/g, "_") + if (/^xml/i.test(safe)) safe = "_" + safe + return { safe, changed: safe !== original } + }, + tagInfo(name: string, original?: string): { safe: string; attrs: string } { + const { changed, safe } = xml.parseTagName(name) + const needsMeta = changed || (original && original !== name) + const attrs = needsMeta ? ` data-name="${xml.escapeAttribute(original ?? name)}"` : "" + return { safe, attrs } + } +} + +function getStringTreePriority(ast: SchemaAST.AST): number { + switch (ast._tag) { + case "Null": + case "Boolean": + case "Number": + case "BigInt": + case "Symbol": + case "UniqueSymbol": + return 0 + default: + return 1 + } +} + +const treeReorder = InternalSchema.makeReorder(getStringTreePriority) + +function serializerTree( + ast: SchemaAST.AST, + recur: (ast: SchemaAST.AST) => SchemaAST.AST, + onMissingAnnotation: (ast: SchemaAST.AST) => SchemaAST.AST +): SchemaAST.AST { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec + if (Predicate.isFunction(getLink)) { + const tps = SchemaAST.isDeclaration(ast) + ? ast.typeParameters.map((tp) => make(recur(SchemaAST.toEncoded(tp)))) + : [] + const link = getLink(tps) + const to = recur(link.to) + return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + } + return onMissingAnnotation(ast) + } + case "Null": + return SchemaAST.replaceEncoding(ast, [nullToString]) + case "Boolean": + return SchemaAST.replaceEncoding(ast, [booleanToString]) + case "Unknown": + case "ObjectKeyword": + return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToStringTree]) + case "Enum": + case "Number": + case "Literal": + case "UniqueSymbol": + case "Symbol": + case "BigInt": + return ast.toCodecStringTree() + case "Objects": { + if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) { + throw new globalThis.Error("Objects property names must be strings", { cause: ast }) + } + return ast.recur(recur, SchemaAST.parameterFromString) + } + case "Union": { + const sortedTypes = treeReorder(ast.types) + if (sortedTypes !== ast.types) { + return new SchemaAST.Union( + sortedTypes, + ast.mode, + ast.annotations, + ast.checks, + ast.encoding, + ast.context, + ast.encodingChecks + ).recur(recur) + } + return ast.recur(recur) + } + case "Arrays": + case "Suspend": + return ast.recur(recur) + } + // `Schema.Any` is used as an escape hatch + return ast +} + +const nullToString = new SchemaAST.Link( + new SchemaAST.Literal("null"), + new SchemaTransformation.Transformation( + SchemaGetter.transform(() => null), + SchemaGetter.transform(() => "null") + ) +) + +const booleanToString = new SchemaAST.Link( + new SchemaAST.Union([new SchemaAST.Literal("true"), new SchemaAST.Literal("false")], "anyOf"), + new SchemaTransformation.Transformation( + SchemaGetter.transform((s) => s === "true"), + SchemaGetter.String() + ) +) + +const SERIALIZER_ENSURE_ARRAY = "~effect/Schema/SERIALIZER_ENSURE_ARRAY" + +const isSerializerArrayFromSingle = (ast: SchemaAST.AST): boolean => + SchemaAST.isUnion(ast) && ast.annotations?.[SERIALIZER_ENSURE_ARRAY] === true + +const serializerStringTree = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + if (isSerializerArrayFromSingle(ast)) { + return ast + } + const out = serializerTree(ast, serializerStringTree, (ast) => SchemaAST.replaceEncoding(ast, [unknownToUndefined])) + if (out !== ast && SchemaAST.isOptional(ast)) { + return SchemaAST.optionalKeyLastLink(out) + } + return out +}) + +const unknownToUndefined = new SchemaAST.Link( + SchemaAST.undefined, + new SchemaTransformation.Transformation( + SchemaGetter.passthrough(), + SchemaGetter.transform(() => undefined) + ) +) + +const toArrayFromSingleInputElement = (ast: SchemaAST.AST): SchemaAST.AST => + SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(SchemaAST.unknown) : SchemaAST.unknown + +const arrayFromSingleTransformation = new SchemaTransformation.Transformation( + SchemaGetter.transform((input: ReadonlyArray | string) => typeof input === "string" ? [input] : input), + SchemaGetter.passthrough() +) + +const toCodecArrayFromSingleTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + if (isSerializerArrayFromSingle(ast)) { + return ast + } + const out = onSerializerArrayFromSingle(ast) + if (SchemaAST.isArrays(out)) { + const ensure = SchemaAST.decodeTo( + new SchemaAST.Union( + [ + new SchemaAST.Arrays( + out.isMutable, + out.elements.map(toArrayFromSingleInputElement), + out.rest.map(toArrayFromSingleInputElement) + ), + SchemaAST.string + ], + "anyOf", + { [SERIALIZER_ENSURE_ARRAY]: true } + ), + out, + arrayFromSingleTransformation + ) + return SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(ensure) : ensure + } + return out +}) + +function onSerializerArrayFromSingle(ast: SchemaAST.AST): SchemaAST.AST { + return ast._tag === "Declaration" || ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union" || + ast._tag === "Suspend" + ? ast.recur(toCodecArrayFromSingleTop) + : ast +} + +// ----------------------------------------------------------------------------- +// Optic APIs +// ----------------------------------------------------------------------------- + +/** + * Derives an `Iso` optic from a schema that isomorphically converts between + * the schema's `Type` and its `Iso` (intermediate / serialized form). + * + * @category Optic + * @since 4.0.0 + */ +export function toIso(schema: S): Optic_.Iso { + const serializer = toCodecIso(schema) + return Optic_.makeIso(SchemaParser.encodeSync(serializer), SchemaParser.decodeSync(serializer)) +} + +/** + * Returns an identity `Iso` over the schema's source (`Type`) side. + * + * @category Optic + * @since 4.0.0 + */ +export function toIsoSource(_: S): Optic_.Iso { + return Optic_.id() +} + +/** + * Returns an identity `Iso` over the schema's focus (`Iso`) side. + * + * @category Optic + * @since 4.0.0 + */ +export function toIsoFocus(_: S): Optic_.Iso { + return Optic_.id() +} + +/** + * Type-level representation returned by {@link overrideToCodecIso}. + * + * @category Optic + * @since 4.0.0 + */ +export interface overrideToCodecIso extends + BottomLazy< + S["ast"], + overrideToCodecIso, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": Iso + readonly schema: S +} + +/** + * Overrides a schema's derived ISO codec with an explicit target codec. + * + * **When to use** + * + * Use to provide a custom ISO transformation when the default derivation is not + * appropriate. + * + * **Details** + * + * The resulting schema carries a custom `Iso` type parameter and uses the + * provided `decode` and `encode` getters to transform between the schema type + * and the target codec. + * + * @category Optic + * @since 4.0.0 + */ +export function overrideToCodecIso( + to: ConstraintCodec, + transformation: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter + } +) { + return (schema: S): overrideToCodecIso => { + return make( + SchemaAST.annotate(schema.ast, { + toCodecIso: () => new SchemaAST.Link(to.ast, SchemaTransformation.make(transformation)) + }), + { schema } + ) + } +} + +// ----------------------------------------------------------------------------- +// Differ APIs +// ----------------------------------------------------------------------------- + +/** + * Derives a JSON Patch differ from a codec. Serializes values to JSON (via + * {@link toCodecJson}), computes RFC 6902 JSON Patch operations between old + * and new values, and can apply patches back to the typed value. + * + * @category converting + * @since 4.0.0 + */ +export function toDifferJsonPatch(schema: ConstraintCodec): Differ { + const serializer = toCodecJson(schema) + const get = SchemaParser.encodeSync(serializer) + const set = SchemaParser.decodeSync(serializer) + return { + empty: [], + diff: (oldValue, newValue) => JsonPatch.get(get(oldValue), get(newValue)), + combine: (first, second) => [...first, ...second], + patch: (oldValue, patch) => { + const value = get(oldValue) + const patched = JsonPatch.apply(patch, value) + return Object.is(patched, value) ? oldValue : set(patched) + } + } +} + +/** + * Recursive tree type whose leaves are `Node` values and whose branches are + * readonly arrays or string-keyed records of child trees. + * + * @category Tree + * @since 4.0.0 + */ +export type Tree = Node | TreeRecord | ReadonlyArray> + +/** + * A record node in a {@link Tree}: an object mapping string keys to child + * `Tree` nodes. + * + * @category Tree + * @since 4.0.0 + */ +export interface TreeRecord { + readonly [x: string]: Tree +} + +/** + * Creates a recursive schema for a {@link Tree} of values described by `node`. + * The resulting schema accepts a single node value, an array of trees, or an + * object whose values are trees. + * + * @category Tree + * @since 4.0.0 + */ +export function Tree(node: S) { + const Tree$ref = suspend((): Codec< + Tree, + Tree, + S["DecodingServices"], + S["EncodingServices"] + > => Tree) + const Tree = Union([ + node, + ArraySchema(Tree$ref), + Record(String, Tree$ref) + ]) + return Tree +} + +/** + * Recursive TypeScript type for any valid immutable JSON value: `null`, + * `number`, `boolean`, `string`, a readonly array of `Json` values, or a + * readonly record of `string → Json`. For the corresponding schema, see the + * {@link Json} const. + * + * @category models + * @since 4.0.0 + */ +export type Json = null | number | boolean | string | JsonArray | JsonObject + +/** + * A readonly array of {@link Json} values. + * + * @category models + * @since 4.0.0 + */ +export interface JsonArray extends ReadonlyArray {} + +/** + * A readonly record whose values are {@link Json} values. + * + * @category models + * @since 4.0.0 + */ +export interface JsonObject { + readonly [x: string]: Json +} + +/** + * Schema that accepts and validates any immutable JSON-compatible value. + * + * **Example** (Validating a JSON value) + * + * ```ts + * import { Schema } from "effect" + * + * const result = Schema.decodeUnknownOption(Schema.Json)({ key: [1, true, null] }) + * console.log(result._tag) // "Some" + * ``` + * + * @category schemas + * @since 4.0.0 + */ +export const Json: Codec = make(SchemaAST.Json) + +const JsonError = Struct({ + message: String, + name: optionalKey(String), + stack: optionalKey(String), + cause: optionalKey(Json) +}) + +/** + * Recursive TypeScript type for mutable JSON values: `null`, `number`, + * `boolean`, `string`, mutable arrays, or mutable string-keyed records. + * + * @category models + * @since 4.0.0 + */ +export type MutableJson = null | number | boolean | string | MutableJsonArray | MutableJsonObject + +/** + * A mutable array of {@link MutableJson} values. + * + * @category models + * @since 4.0.0 + */ +export interface MutableJsonArray extends Array {} + +/** + * A mutable record whose values are {@link MutableJson} values. + * + * @category models + * @since 4.0.0 + */ +export interface MutableJsonObject { + [x: string]: MutableJson +} + +/** + * Schema that accepts any mutable JSON-compatible value. See {@link Json} for + * the immutable variant. + * + * @category schemas + * @since 4.0.0 + */ +export const MutableJson: Codec = make(SchemaAST.MutableJson) + +// ----------------------------------------------------------------------------- +// Annotations +// ----------------------------------------------------------------------------- + +/** + * Resolves the typed annotations from a schema. The term "resolve" (rather + * than "get") reflects the lookup strategy: if the schema has checks, the + * annotations are taken from the last check; otherwise they are taken from + * the base schema instance. + * + * @category Schema Resolvers + * @since 4.0.0 + */ +export function resolveAnnotations( + schema: S +): Annotations.Bottom | undefined { + return InternalAnnotations.resolve(schema.ast) +} + +/** + * Resolves the context (key-level) annotations from a schema. Context + * annotations are those attached via `annotateKey` and live on the AST's + * `context` rather than on the schema node itself. + * + * @category Schema Resolvers + * @since 4.0.0 + */ +export function resolveAnnotationsKey(schema: S): Annotations.Key | undefined { + return schema.ast.context?.annotations +} + +/** + * The `Annotations` namespace groups all annotation interfaces used to attach + * metadata to schemas. Annotations control documentation, validation messages, + * JSON Schema generation, equivalence, arbitrary generation, and more. + * + * **Details** + * + * Use {@link resolveAnnotations} to read the annotations attached to a schema at + * runtime. + * + * @since 4.0.0 + */ +export declare namespace Annotations { + /** + * This interface is used to define the annotations that can be attached to a + * schema. You can extend this interface to define your own annotations. + * + * **Details** + * + * Note that both a missing key or `undefined` is used to indicate that the + * annotation is not present. + * + * This means that can remove any annotation by setting it to `undefined`. + * + * **Example** (Defining your own annotations) + * + * ```ts + * import { Schema } from "effect" + * + * // Extend the Annotations interface with a custom `version` annotation + * declare module "effect/Schema" { + * namespace Annotations { + * interface Annotations { + * readonly version?: + * | readonly [major: number, minor: number, patch: number] + * | undefined + * } + * } + * } + * + * // The `version` annotation is now recognized by the TypeScript compiler + * const schema = Schema.String.annotate({ version: [1, 2, 0] }) + * + * // const version: readonly [major: number, minor: number, patch: number] | undefined + * const version = Schema.resolveAnnotations(schema)?.["version"] + * + * if (version) { + * // Access individual parts of the version + * console.log(version[1]) + * // Output: 2 + * } + * ``` + * + * @category models + * @since 4.0.0 + */ + export interface Annotations { + readonly [x: string]: unknown + } + + /** + * Annotations shared by all schema nodes. These map to common JSON Schema / + * OpenAPI fields: `title`, `description`, `format`, etc. + * + * @category models + * @since 4.0.0 + */ + export interface Augment extends Annotations { + /** + * Human-readable description of what a value is expected to satisfy. + * + * **Details** + * + * For filter and refinement failures, the default formatter uses + * `message` first, then `expected`, and finally falls back to ``. + * + * Use this to name a failed filter in the default message: + * `Expected , got `. + */ + readonly expected?: string | undefined + readonly title?: string | undefined + readonly description?: string | undefined + readonly documentation?: string | undefined + readonly readOnly?: boolean | undefined + readonly writeOnly?: boolean | undefined + readonly format?: string | undefined + readonly contentEncoding?: string | undefined + readonly contentMediaType?: string | undefined + } + + /** + * Extends {@link Augment} with type-parametric `default` and `examples` fields. + * + * @category models + * @since 4.0.0 + */ + export interface Documentation extends Augment { + readonly default?: T | undefined + readonly examples?: ReadonlyArray | undefined + } + + /** + * Annotations for struct property schemas. Extends {@link Documentation} + * with an optional `messageMissingKey` to override the error message when + * the property key is absent during decoding. + * + * @category models + * @since 4.0.0 + */ + export interface Key extends Documentation { + /** + * The message to use when a key is missing. + */ + readonly messageMissingKey?: string | undefined + } + + /** + * Base annotations shared by all composite schema nodes. Extends + * {@link Documentation} with error messages, branding, parse options, and + * arbitrary generation hooks. {@link Declaration} and other annotation + * interfaces build on top of this. + * + * @category models + * @since 4.0.0 + */ + export interface Bottom> extends Documentation { + /** + * Complete message to use when this schema node reports an issue. + * + * **Details** + * + * This replaces the default message for matching issue types instead of + * only changing the expected label. For a filter or refinement failure, + * annotate the filter with `message` to replace the whole filter failure + * message, or `expected` to keep the default + * `Expected , got ` shape. + */ + readonly message?: string | undefined + /** + * The message to use when a key is unexpected. + */ + readonly messageUnexpectedKey?: string | undefined + /** + * Stable identifier for this schema node. + * + * **Details** + * + * Identifiers are used by schema tooling, including JSON Schema + * generation, to name references. The default formatter also uses + * `identifier` as the expected label for type-level failures, such as + * `Expected UserId, got null`. + * + * `identifier` does not name a failed filter or refinement. If the base + * type matches and a filter fails, put `expected` or `message` on the + * filter/refinement instead. + */ + readonly identifier?: string | undefined + readonly parseOptions?: SchemaAST.ParseOptions | undefined + /** + * Optional metadata used to identify or extend the filter with custom data. + */ + readonly meta?: Meta | undefined + /** + * Accumulated brands when multiple brands are added with `Schema.brand`. + */ + readonly brands?: ReadonlyArray | undefined + readonly toArbitrary?: + | ToArbitrary.Declaration + | undefined + } + + /** + * Helpers for projecting declaration type-parameter schemas into decoded or + * encoded codec arrays used by annotation hooks. + * + * @since 4.0.0 + */ + export namespace TypeParameters { + /** + * Maps declaration type-parameter schemas to codecs for their decoded `Type` + * values. + * + * @category utility types + * @since 4.0.0 + */ + export type Type> = { + readonly [K in keyof TypeParameters]: Codec + } + /** + * Maps declaration type-parameter schemas to codecs for their `Encoded` values. + * + * @category utility types + * @since 4.0.0 + */ + export type Encoded> = { + readonly [K in keyof TypeParameters]: Codec + } + } + + /** + * Full annotation set for `Declaration` schema nodes — used when defining + * custom, opaque schema types via `Schema.declare`. Extends {@link Bottom} + * with optional codec, arbitrary, equivalence, and formatter hooks so that + * derived capabilities (JSON encoding, property testing, etc.) can be + * provided for the custom type. + * + * @category models + * @since 4.0.0 + */ + export interface Declaration = readonly []> + extends Bottom + { + readonly toCodec?: + | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link) + | undefined + readonly toCodecJson?: + | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link) + | undefined + readonly toCodecIso?: + | ((typeParameters: TypeParameters.Type) => SchemaAST.Link) + | undefined + readonly toArbitrary?: ToArbitrary.Declaration | undefined + readonly toEquivalence?: ToEquivalence.Declaration | undefined + readonly toFormatter?: ToFormatter.Declaration | undefined + readonly typeConstructor?: { + readonly _tag: string + readonly [key: string]: unknown + } | undefined + readonly generation?: { + readonly runtime: string + readonly Type: string + readonly Encoded?: string | undefined + readonly importDeclaration?: string | undefined + } | undefined + /** + * Used to collect sentinels from a Declaration SchemaAST. + * + * @internal + */ + readonly "~sentinels"?: ReadonlyArray | undefined + } + + /** + * Annotations for filter schema nodes (created via `Schema.filter`). Extends + * {@link Augment} with an optional error message, identifier, and metadata. + * Filters are intentionally non-parametric to keep them covariant. + * + * @category models + * @since 4.0.0 + */ + export interface Filter extends Augment { + /** + * Complete message to use when this filter or refinement fails. + * + * **Details** + * + * The default formatter checks filter annotations in this order: + * `message`, then `expected`, then ``. + */ + readonly message?: string | undefined + /** + * Stable identifier for the schema after this filter is attached. + * + * **Details** + * + * This can affect schema tooling such as JSON Schema generation and + * type-level failures before the filter runs, but it does not name the + * failed filter itself. For filter failure messages, use `expected` or + * `message`. + */ + readonly identifier?: string | undefined + /** + * Optional metadata used to identify or extend the filter with custom data. + */ + readonly meta?: Meta | undefined + /** + * Optional hints used by arbitrary derivation for this filter. + * + * **Details** + * + * The same annotation can be attached to a single filter or a + * `FilterGroup`. Group hints apply to the same schema node while child + * filters are still collected and checked normally. + */ + readonly arbitrary?: + | ToArbitrary.Filter + | undefined + /** + * Marks the filter as *structural*, meaning it applies to the shape or + * structure of the container (e.g., array length, object keys) rather than + * the contents. + * + * **Details** + * + * Example: `minLength` on an array is a structural filter. + */ + readonly "~structural"?: boolean | undefined + } + + /** + * Types used by arbitrary-derivation annotations to configure `toArbitrary` + * hooks, filter hints, candidate sources, diagnostics, and merged generation + * constraints. + * + * @since 4.0.0 + */ + export namespace ToArbitrary { + /** + * Arbitrary-generation hints attached to a filter or filter group. + * + * **Details** + * + * `constraint` refines the schema node's base generator. `candidate` adds a + * weighted source before all filters run. If neither hint is provided, the + * filter does not guide generation; generated values are still checked by + * the filter predicate. With `{ report: true }`, this is reported as + * `OpaqueFilter`. + * + * @category models + * @since 4.0.0 + */ + export interface Filter { + readonly constraint?: GenerationConstraint | undefined + readonly candidate?: Candidate | undefined + } + + /** + * Additional arbitrary source used before final filter checks run. + * + * **Details** + * + * The base generator keeps weight `1`; candidates default to weight `1` + * and must use a positive integer weight. `make` receives the merged + * constraint for the current node and may return `undefined` to opt out, + * including for recursive terminal branches. Candidate values are still + * checked by every schema filter, so invalid candidates affect efficiency but + * not validity. + * + * @category models + * @since 4.0.0 + */ + export interface Candidate { + readonly weight?: number | undefined + readonly make: ( + fc: typeof FastCheck, + context: Context + ) => FastCheck.Arbitrary | undefined + } + + /** + * Ordered constraint accumulated from range checks. + * + * **Details** + * + * Generators consume these constraints only when they recognize `order`, + * such as `Order.Number`, `Order.BigInt`, DateTime, or BigDecimal. Merging + * constraints with different `Order` instances fails fast. + * + * @category models + * @since 4.0.0 + */ + export interface OrderedConstraint { + readonly order: Order.Order + readonly minimum?: T | undefined + readonly exclusiveMinimum?: boolean | undefined + readonly maximum?: T | undefined + readonly exclusiveMaximum?: boolean | undefined + } + + /** + * Node-local arbitrary-generation constraint accumulated from schema checks. + * + * **Details** + * + * `GenerationConstraint` is a generation hint for the current schema AST + * node, not a self-describing validation contract. Each generator consumes + * the fields it understands for the current node and ignores the rest; + * final schema filters still validate every generated value. + * + * `minLength` and `maxLength` represent node-local cardinality: string + * length for strings, array length for arrays, final own-property count for + * objects, and final size/cardinality for sets, maps, hash collections, and + * chunks. `patterns` are concatenated and used by string generators. + * `integer`, `noNaN`, `noInfinity`, `valid`, and `unique` are true when any + * contributing filter sets them. Range bounds live in `ordered` so ordered + * values can share the same representation. + * + * @category models + * @since 4.0.0 + */ + export interface GenerationConstraint { + readonly minLength?: number | undefined + readonly maxLength?: number | undefined + readonly patterns?: readonly [string, ...Array] + readonly integer?: boolean | undefined + readonly noInfinity?: boolean | undefined + readonly noNaN?: boolean | undefined + readonly valid?: boolean | undefined + readonly unique?: boolean | undefined + readonly ordered?: OrderedConstraint | undefined + } + + /** + * Recursion budget passed to arbitrary-derivation hooks. + * + * **Details** + * + * Pass this object to `fc.oneof` when combining terminal and recursive + * branches. Put the terminal branch first because fast-check uses only the + * first branch once `maxDepth` is reached for `depthIdentifier`. + * + * @category models + * @since 4.0.0 + */ + export interface Recursion { + readonly maxDepth: number + readonly depthIdentifier: FastCheck.DepthIdentifier | string + } + + /** + * Context passed to arbitrary-derivation hooks and candidate factories. + * + * **Details** + * + * `constraint` contains the merged constraint for the current schema + * node. `recursion` is present while deriving through a suspended schema; + * hooks that build recursive alternatives should pass it to `fc.oneof` with + * the finite branch first. + * + * @category models + * @since 4.0.0 + */ + export interface Context { + readonly constraint?: ToArbitrary.GenerationConstraint | undefined + readonly recursion?: ToArbitrary.Recursion | undefined + } + + /** + * Arbitrary generators derived for a declaration type parameter. + * + * **Details** + * + * `arbitrary` is the normal generator. `terminal` is the finite generator + * used while building recursive terminal branches and is `undefined` when + * no finite path is known. Optional containers can ignore it; non-empty + * containers need it for their terminal branch. + * + * @category models + * @since 4.0.0 + */ + export interface TypeParameter { + readonly arbitrary: FastCheck.Arbitrary + readonly terminal: FastCheck.Arbitrary | undefined + } + + /** + * Arbitrary derivation returned by declaration hooks. + * + * **Details** + * + * `arbitrary` is the normal generator. `terminal` is an optional finite + * branch for recursive schemas. If omitted, it defaults to `arbitrary` only + * for declarations without type parameters. + * + * @category models + * @since 4.0.0 + */ + export interface Derivation { + readonly arbitrary: FastCheck.Arbitrary + readonly terminal?: FastCheck.Arbitrary | undefined + } + + /** + * Output accepted from declaration arbitrary hooks. + * + * **Details** + * + * A bare fast-check arbitrary is shorthand for `{ arbitrary }`, useful for + * atomic declarations such as URLs. Generic declarations that need precise + * recursive behavior should return a {@link Derivation} with `terminal`. + * + * @category models + * @since 4.0.0 + */ + export type Output = FastCheck.Arbitrary | Derivation + + /** + * Hook signature for declaration schema arbitrary annotations. + * + * **Details** + * + * Type parameters expose normal and terminal generators. A declaration with + * no type parameters can return a bare arbitrary; a generic declaration + * must return `terminal` explicitly when it has a finite branch depending on + * parameters. + * + * @category models + * @since 4.0.0 + */ + export interface Declaration> { + ( + /* Arbitrary derivations for any type parameters of the schema (if present) */ + typeParameters: { readonly [K in keyof TypeParameters]: TypeParameter } + ): (fc: typeof FastCheck, context: Context) => Output + } + + /** + * Wraps a derived value together with arbitrary-derivation diagnostics. + * + * @category models + * @since 4.0.0 + */ + export interface WithReport { + readonly value: A + readonly report: Report + } + + /** + * Diagnostics collected while deriving an arbitrary. + * + * **Details** + * + * Reports contain warnings only. Unsupported schema nodes, impossible + * constraints, invalid candidate weights, and throwing candidate factories + * fail immediately. + * + * @category models + * @since 4.0.0 + */ + export interface Report { + readonly warnings: ReadonlyArray + } + + /** + * Non-fatal arbitrary-derivation warning. + * + * @category models + * @since 4.0.0 + */ + export type Warning = OpaqueFilterWarning + + /** + * Warning emitted when a filter is handled only by the final `.filter`. + * + * **Details** + * + * The filter is still enforced. The warning means it did not contribute + * a constraint or candidate, so generation may rely on fast-check discards. + * + * @category models + * @since 4.0.0 + */ + export interface OpaqueFilterWarning { + readonly _tag: "OpaqueFilter" + readonly path: ReadonlyArray + readonly description?: string | undefined + } + } + + /** + * Types used by formatter annotations to customize formatter derivation for + * declaration schemas. + * + * @since 4.0.0 + */ + export namespace ToFormatter { + /** + * Hook signature for declaration schema formatter annotations. + * + * **Details** + * + * Given formatters for any type parameters, returns a formatter for `T`. + * + * @category models + * @since 4.0.0 + */ + export interface Declaration> { + ( + /* Formatters for any type parameters of the schema (if present) */ + typeParameters: { readonly [K in keyof TypeParameters]: Formatter } + ): Formatter + } + } + + /** + * Types used by equivalence annotations to customize equivalence derivation for + * declaration schemas. + * + * @since 4.0.0 + */ + export namespace ToEquivalence { + /** + * Hook signature for declaration schema equivalence annotations. + * + * **Details** + * + * Given equivalences for any type parameters, returns an `Equivalence` for `T`. + * + * @category models + * @since 4.0.0 + */ + export interface Declaration> { + ( + /* Equivalences for any type parameters of the schema (if present) */ + typeParameters: { readonly [K in keyof TypeParameters]: Equivalence.Equivalence } + ): Equivalence.Equivalence + } + } + + /** + * Annotations that can be attached to schema issues. + * + * **Details** + * + * The optional `message` field overrides the default issue message. + * + * @category models + * @since 4.0.0 + */ + export interface Issue extends Annotations { + readonly message?: string | undefined + } + + /** + * Registry of metadata payloads emitted by built-in schema filters and checks. + * + * **Details** + * + * Do not augment this interface with custom metadata; extend `MetaDefinitions` + * instead. + * + * @category models + * @since 4.0.0 + */ + export interface BuiltInMetaDefinitions { + // String Meta + readonly isStringFinite: { + readonly _tag: "isStringFinite" + readonly regExp: globalThis.RegExp + } + readonly isStringBigInt: { + readonly _tag: "isStringBigInt" + readonly regExp: globalThis.RegExp + } + readonly isStringSymbol: { + readonly _tag: "isStringSymbol" + readonly regExp: globalThis.RegExp + } + readonly isMinLength: { + readonly _tag: "isMinLength" + readonly minLength: number + } + readonly isMaxLength: { + readonly _tag: "isMaxLength" + readonly maxLength: number + } + readonly isLengthBetween: { + readonly _tag: "isLengthBetween" + readonly minimum: number + readonly maximum: number + } + readonly isPattern: { + readonly _tag: "isPattern" + readonly regExp: globalThis.RegExp + } + readonly isTrimmed: { + readonly _tag: "isTrimmed" + readonly regExp: globalThis.RegExp + } + readonly isUUID: { + readonly _tag: "isUUID" + readonly regExp: globalThis.RegExp + readonly version: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | undefined + } + readonly isGUID: { + readonly _tag: "isGUID" + readonly regExp: globalThis.RegExp + } + readonly isULID: { + readonly _tag: "isULID" + readonly regExp: globalThis.RegExp + } + readonly isBase64: { + readonly _tag: "isBase64" + readonly regExp: globalThis.RegExp + } + readonly isBase64Url: { + readonly _tag: "isBase64Url" + readonly regExp: globalThis.RegExp + } + readonly isStartsWith: { + readonly _tag: "isStartsWith" + readonly startsWith: string + readonly regExp: globalThis.RegExp + } + readonly isEndsWith: { + readonly _tag: "isEndsWith" + readonly endsWith: string + readonly regExp: globalThis.RegExp + } + readonly isIncludes: { + readonly _tag: "isIncludes" + readonly includes: string + readonly regExp: globalThis.RegExp + } + readonly isUppercased: { + readonly _tag: "isUppercased" + readonly regExp: globalThis.RegExp + } + readonly isLowercased: { + readonly _tag: "isLowercased" + readonly regExp: globalThis.RegExp + } + readonly isCapitalized: { + readonly _tag: "isCapitalized" + readonly regExp: globalThis.RegExp + } + readonly isUncapitalized: { + readonly _tag: "isUncapitalized" + readonly regExp: globalThis.RegExp + } + // Number Meta + readonly isFinite: { + readonly _tag: "isFinite" + } + readonly isInt: { + readonly _tag: "isInt" + } + readonly isMultipleOf: { + readonly _tag: "isMultipleOf" + readonly divisor: number + } + readonly isGreaterThan: { + readonly _tag: "isGreaterThan" + readonly exclusiveMinimum: number + } + readonly isGreaterThanOrEqualTo: { + readonly _tag: "isGreaterThanOrEqualTo" + readonly minimum: number + } + readonly isLessThan: { + readonly _tag: "isLessThan" + readonly exclusiveMaximum: number + } + readonly isLessThanOrEqualTo: { + readonly _tag: "isLessThanOrEqualTo" + readonly maximum: number + } + readonly isBetween: { + readonly _tag: "isBetween" + readonly minimum: number + readonly maximum: number + readonly exclusiveMinimum?: boolean | undefined + readonly exclusiveMaximum?: boolean | undefined + } + // BigInt Meta + readonly isGreaterThanBigInt: { + readonly _tag: "isGreaterThanBigInt" + readonly exclusiveMinimum: bigint + } + readonly isGreaterThanOrEqualToBigInt: { + readonly _tag: "isGreaterThanOrEqualToBigInt" + readonly minimum: bigint + } + readonly isLessThanBigInt: { + readonly _tag: "isLessThanBigInt" + readonly exclusiveMaximum: bigint + } + readonly isLessThanOrEqualToBigInt: { + readonly _tag: "isLessThanOrEqualToBigInt" + readonly maximum: bigint + } + readonly isBetweenBigInt: { + readonly _tag: "isBetweenBigInt" + readonly minimum: bigint + readonly maximum: bigint + readonly exclusiveMinimum?: boolean | undefined + readonly exclusiveMaximum?: boolean | undefined + } + // Date Meta + readonly isDateValid: { + readonly _tag: "isDateValid" + } + readonly isGreaterThanDate: { + readonly _tag: "isGreaterThanDate" + readonly exclusiveMinimum: globalThis.Date + } + readonly isGreaterThanOrEqualToDate: { + readonly _tag: "isGreaterThanOrEqualToDate" + readonly minimum: globalThis.Date + } + readonly isLessThanDate: { + readonly _tag: "isLessThanDate" + readonly exclusiveMaximum: globalThis.Date + } + readonly isLessThanOrEqualToDate: { + readonly _tag: "isLessThanOrEqualToDate" + readonly maximum: globalThis.Date + } + readonly isBetweenDate: { + readonly _tag: "isBetweenDate" + readonly minimum: globalThis.Date + readonly maximum: globalThis.Date + readonly exclusiveMinimum?: boolean | undefined + readonly exclusiveMaximum?: boolean | undefined + } + // Objects Meta + readonly isMinProperties: { + readonly _tag: "isMinProperties" + readonly minProperties: number + } + readonly isMaxProperties: { + readonly _tag: "isMaxProperties" + readonly maxProperties: number + } + readonly isPropertiesLengthBetween: { + readonly _tag: "isPropertiesLengthBetween" + readonly minimum: number + readonly maximum: number + } + readonly isPropertyNames: { + readonly _tag: "isPropertyNames" + readonly propertyNames: SchemaAST.AST + } + // Arrays Meta + readonly isUnique: { + readonly _tag: "isUnique" + } + // Declaration Meta + readonly isMinSize: { + readonly _tag: "isMinSize" + readonly minSize: number + } + readonly isMaxSize: { + readonly _tag: "isMaxSize" + readonly maxSize: number + } + readonly isSizeBetween: { + readonly _tag: "isSizeBetween" + readonly minimum: number + readonly maximum: number + } + } + + /** + * Union of all metadata payloads defined by `BuiltInMetaDefinitions`. + * + * @category utility types + * @since 4.0.0 + */ + export type BuiltInMeta = BuiltInMetaDefinitions[keyof BuiltInMetaDefinitions] + + /** + * Augmentable registry of schema filter metadata payloads. + * + * **Details** + * + * Extend this interface to add custom values accepted by annotation `meta` + * fields. + * + * @category models + * @since 4.0.0 + */ + export interface MetaDefinitions extends BuiltInMetaDefinitions {} + + /** + * Union of built-in and user-augmented schema filter metadata payloads. + * + * @category utility types + * @since 4.0.0 + */ + export type Meta = MetaDefinitions[keyof MetaDefinitions] +} diff --git a/repos/effect/packages/effect/src/SchemaAST.ts b/repos/effect/packages/effect/src/SchemaAST.ts new file mode 100644 index 0000000..9e1b127 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaAST.ts @@ -0,0 +1,4001 @@ +/** + * Represents Effect schemas as runtime trees. + * + * Every `Schema` has an AST made from nodes for declarations, primitives, + * literals, arrays, objects, unions, suspended schemas, checks, annotations, + * encoding links, and parsing context. Most users work with the higher-level + * `Schema` module. Use `SchemaAST` when you need to inspect schema nodes, build + * ASTs programmatically, change encoded or decoded views, collect issues, or + * run low-level schema checks. + * + * @since 4.0.0 + */ + +import * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import type * as Combiner from "./Combiner.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import { format, formatPropertyKey } from "./Formatter.ts" +import { memoize } from "./Function.ts" +import { effectIsExit, iterateEager } from "./internal/effect.ts" +import * as internalRecord from "./internal/record.ts" +import * as InternalAnnotations from "./internal/schema/annotations.ts" +import * as InternalSchemaCause from "./internal/schema/cause.ts" +import * as Option from "./Option.ts" +import * as Pipeable from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import * as SchemaGetter from "./SchemaGetter.ts" +import * as SchemaIssue from "./SchemaIssue.ts" +import type * as SchemaParser from "./SchemaParser.ts" +import * as SchemaTransformation from "./SchemaTransformation.ts" +import type * as FastCheck from "./testing/FastCheck.ts" + +/** + * Discriminated union of all AST node types. + * + * **Details** + * + * Every `Schema` has an `.ast` property of this type. Use the guard functions + * ({@link isString}, {@link isObjects}, etc.) to narrow to a specific variant, + * then access variant-specific fields. + * + * - All variants share the {@link Base} fields: `annotations`, `checks`, + * `encoding`, `context`. + * - Discriminate on the `_tag` field (e.g. `"String"`, `"Objects"`, `"Union"`). + * + * @see {@link Base} + * @see {@link isAST} + * @category models + * @since 3.10.0 + */ +export type AST = + | Declaration + | Null + | Undefined + | Void + | Never + | Unknown + | Any + | String + | Number + | Boolean + | BigInt + | Symbol + | Literal + | UniqueSymbol + | ObjectKeyword + | Enum + | TemplateLiteral + | Arrays + | Objects + | Union + | Suspend + +function makeGuard(tag: T) { + return (ast: AST): ast is Extract => ast._tag === tag +} + +/** + * Returns `true` if the value is an {@link AST} node (any variant). + * + * **Details** + * + * Uses the internal `TypeId` brand to distinguish AST nodes from arbitrary + * objects. + * + * @see {@link AST} + * @category guards + * @since 4.0.0 + */ +export function isAST(u: unknown): u is AST { + return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId +} + +/** + * Narrows an {@link AST} to {@link Declaration}. + * + * **When to use** + * + * Use to recognize declaration AST nodes before running declaration-specific + * handling. + * + * @see {@link Declaration} for the AST node type narrowed by this guard + * + * @category guards + * @since 3.10.0 + */ +export const isDeclaration = makeGuard("Declaration") + +/** + * Narrows an {@link AST} to {@link Null}. + * + * **When to use** + * + * Use to recognize an AST node that represents exactly the `null` literal when + * inspecting, traversing, or transforming schema ASTs. + * + * @see {@link Null} for the AST node type narrowed by this guard + * @see {@link null_ null} for the singleton `Null` AST instance + * @see {@link isLiteral} for exact primitive literal AST nodes + * + * @category guards + * @since 4.0.0 + */ +export const isNull = makeGuard("Null") + +/** + * Narrows an {@link AST} to {@link Undefined}. + * + * **When to use** + * + * Use to identify AST nodes that represent exactly the JavaScript `undefined` + * value. + * + * @see {@link isVoid} for narrowing AST nodes that represent TypeScript `void` instead of exact `undefined` + * + * @category guards + * @since 4.0.0 + */ +export const isUndefined = makeGuard("Undefined") + +/** + * Narrows an {@link AST} to {@link Void}. + * + * **When to use** + * + * Use to identify AST nodes that represent the TypeScript `void` type before + * handling `Void`-specific schema behavior. + * + * @see {@link isUndefined} for narrowing AST nodes that represent the literal `undefined` value instead of TypeScript `void` + * + * @category guards + * @since 4.0.0 + */ +export const isVoid = makeGuard("Void") + +/** + * Narrows an {@link AST} to {@link Never}. + * + * **When to use** + * + * Use to detect the AST node for a schema that can never match before handling + * other schema variants. + * + * @see {@link Never} for the AST node type narrowed by this guard + * @see {@link never} for the singleton `Never` AST instance + * + * @category guards + * @since 4.0.0 + */ +export const isNever = makeGuard("Never") + +/** + * Narrows an {@link AST} to {@link Unknown}. + * + * **When to use** + * + * Use when you need to inspect a schema AST and handle the `Unknown` node + * variant specifically. + * + * @see {@link isAny} for the guard for the `Any` node, whose parsed result is typed as `any` rather than `unknown` + * + * @category guards + * @since 4.0.0 + */ +export const isUnknown = makeGuard("Unknown") + +/** + * Narrows an {@link AST} to {@link Any}. + * + * **When to use** + * + * Use when you need to inspect a schema AST and handle the `Any` node + * variant specifically. + * + * @see {@link isUnknown} for the guard for the `Unknown` node, whose parsed result is typed as `unknown` rather than `any` + * + * @category guards + * @since 4.0.0 + */ +export const isAny = makeGuard("Any") + +/** + * Narrows an {@link AST} to {@link String}. + * + * **When to use** + * + * Use to detect schema AST nodes that match any string value while inspecting + * or transforming a Schema AST. + * + * @see {@link String} for the AST node class narrowed by this guard + * @see {@link string} for the singleton `String` AST instance + * @see {@link isLiteral} for exact primitive literal AST nodes, including exact string literals + * + * @category guards + * @since 4.0.0 + */ +export const isString = makeGuard("String") + +/** + * Narrows an {@link AST} to {@link Number}. + * + * **When to use** + * + * Use to detect `Number` AST nodes while inspecting, traversing, or transforming + * schema ASTs. + * + * @category guards + * @since 4.0.0 + */ +export const isNumber = makeGuard("Number") + +/** + * Narrows an {@link AST} to {@link Boolean}. + * + * **When to use** + * + * Use to identify the `Boolean` AST variant while inspecting, traversing, or + * transforming schema definitions. + * + * @see {@link Boolean} for the AST node type matched by this guard + * @see {@link boolean} for the singleton instance to use when constructing a boolean AST directly + * + * @category guards + * @since 4.0.0 + */ +export const isBoolean = makeGuard("Boolean") + +/** + * Narrows an {@link AST} to {@link BigInt}. + * + * **When to use** + * + * Use to identify bigint AST nodes while inspecting or transforming schema ASTs. + * + * @see {@link BigInt} for the AST node matched by this guard + * @see {@link bigInt} for the singleton instance; use `isBigInt` when narrowing an existing `AST` value + * + * @category guards + * @since 4.0.0 + */ +export const isBigInt = makeGuard("BigInt") + +/** + * Narrows an {@link AST} to {@link Symbol}. + * + * **When to use** + * + * Use to narrow an `AST` node before handling the `Symbol` variant for schemas + * that accept any JavaScript symbol value. + * + * @see {@link isUniqueSymbol} for the sibling guard that narrows the `UniqueSymbol` variant for one exact symbol value + * + * @category guards + * @since 4.0.0 + */ +export const isSymbol = makeGuard("Symbol") + +/** + * Narrows an {@link AST} to {@link Literal}. + * + * **When to use** + * + * Use to recognize exact string, number, boolean, or bigint literal AST nodes. + * + * @see {@link Literal} for the AST node type narrowed by this guard + * @see {@link LiteralValue} for the values stored by literal nodes + * + * @category guards + * @since 3.10.0 + */ +export const isLiteral = makeGuard("Literal") + +/** + * Narrows an {@link AST} to {@link UniqueSymbol}. + * + * @category guards + * @since 3.10.0 + */ +export const isUniqueSymbol = makeGuard("UniqueSymbol") + +/** + * Narrows an {@link AST} to {@link ObjectKeyword}. + * + * **When to use** + * + * Use to identify the AST node for the TypeScript `object` keyword when + * inspecting or transforming a Schema AST. + * + * @see {@link ObjectKeyword} for the AST node matched by this guard + * @see {@link objectKeyword} for the singleton `ObjectKeyword` AST instance + * @see {@link isObjects} for struct and record AST nodes + * + * @category guards + * @since 3.10.0 + */ +export const isObjectKeyword = makeGuard("ObjectKeyword") + +/** + * Narrows an {@link AST} to {@link Enum}. + * + * **When to use** + * + * Use to recognize enum AST nodes before reading enum cases or running + * enum-specific handling. + * + * @see {@link Enum} for the AST node type narrowed by this guard + * + * @category guards + * @since 4.0.0 + */ +export const isEnum = makeGuard("Enum") + +/** + * Narrows an {@link AST} to {@link TemplateLiteral}. + * + * @category guards + * @since 3.10.0 + */ +export const isTemplateLiteral = makeGuard("TemplateLiteral") + +/** + * Narrows an {@link AST} to {@link Arrays}. + * + * **When to use** + * + * Use to recognize array-like AST nodes before reading their element, rest, or + * mutability metadata. + * + * @see {@link Arrays} for the AST node type narrowed by this guard + * + * @category guards + * @since 4.0.0 + */ +export const isArrays = makeGuard("Arrays") + +/** + * Narrows an {@link AST} to {@link Objects}. + * + * @category guards + * @since 4.0.0 + */ +export const isObjects = makeGuard("Objects") + +/** + * Narrows an {@link AST} to {@link Union}. + * + * @category guards + * @since 3.10.0 + */ +export const isUnion = makeGuard("Union") + +/** + * Narrows an {@link AST} to {@link Suspend}. + * + * @category guards + * @since 3.10.0 + */ +export const isSuspend = makeGuard("Suspend") + +/** + * Represents a single step in an {@link Encoding} chain. + * + * **Details** + * + * A link pairs a target {@link AST} with a `Transformation` or `Middleware` + * that converts values between the current node and the target. + * + * - `to` — the AST node on the other side of this transformation step. + * - `transformation` — the bidirectional conversion logic (decode/encode). + * + * Links are composed into a non-empty array ({@link Encoding}) attached to + * AST nodes that have a different encoded representation. + * + * @see {@link Encoding} + * @see {@link decodeTo} + * @category models + * @since 4.0.0 + */ +export class Link { + readonly to: AST + readonly transformation: + | SchemaTransformation.Transformation + | SchemaTransformation.Middleware + + constructor( + to: AST, + transformation: + | SchemaTransformation.Transformation + | SchemaTransformation.Middleware + ) { + this.to = to + this.transformation = transformation + } +} + +/** + * A non-empty chain of {@link Link} values representing the transformation + * steps between a schema's decoded (type) form and its encoded (wire) form. + * + * **Details** + * + * Stored on {@link Base.encoding}. When `undefined`, the node has no + * encoding transformation (type and encoded forms are identical). + * + * @see {@link Link} + * @see {@link toEncoded} + * @category models + * @since 4.0.0 + */ +export type Encoding = readonly [Link, ...Array] + +/** + * Options that control schema parsing, validation, transformation, and output behavior. + * + * **Details** + * + * Pass to `Schema.decodeUnknown`, `Schema.encode`, and related APIs to customize + * error reporting, excess property handling, output key ordering, check + * execution, and asynchronous parser concurrency. + * + * - `errors` — `"first"` (default) stops at the first error; `"all"` collects + * every error. + * - `onExcessProperty` — `"ignore"` (default) strips unknown object keys; + * `"error"` fails; `"preserve"` keeps them. + * - `propertyOrder` — `"none"` (default) lets the system choose key order; + * `"original"` preserves input key order. + * - `disableChecks` — skips validation checks while still applying defaults and + * transformations. + * - `concurrency` — maximum number of async parse effects to run concurrently; + * defaults to `1`, or use `"unbounded"`. + * + * @category options + * @since 3.10.0 + */ +export interface ParseOptions { + /** + * Controls how many parsing errors are reported. + * + * **Details** + * + * The default, `"first"`, stops at the first error. Set the option to `"all"` + * to collect every parsing error, which can help with debugging or with + * presenting more complete error messages to a user. + * + * @default "first" + */ + readonly errors?: "first" | "all" | undefined + + /** + * Controls how object parsing handles keys that are not declared by the schema. + * + * **Details** + * + * The default, `"ignore"`, strips unspecified properties from the output. Use + * `"error"` to fail when an excess property is present, or `"preserve"` to + * keep excess properties in the output. + * + * @default "ignore" + */ + readonly onExcessProperty?: "ignore" | "error" | "preserve" | undefined + + /** + * The `propertyOrder` option provides control over the order of object fields + * in the output. This feature is useful when the sequence of keys is + * important for the consuming processes or when maintaining the input order + * enhances readability and usability. + * + * **Details** + * + * By default, the `propertyOrder` option is set to `"none"`. This means that + * the internal system decides the order of keys to optimize parsing speed. + * + * Setting `propertyOrder` to `"original"` ensures that the keys are ordered + * as they appear in the input during the decoding/encoding process. + * + * **Gotchas** + * + * The key order for `"none"` should not be considered stable and may change + * in future updates without notice. + * + * @default "none" + */ + readonly propertyOrder?: "none" | "original" | undefined + + /** + * Whether to disable checks while still applying defaults and + * transformations. + */ + readonly disableChecks?: boolean | undefined + + /** + * The maximum number of async effects to run concurrently. + * + * @default 1 + */ + readonly concurrency?: number | "unbounded" | undefined +} + +/** @internal */ +export const defaultParseOptions: ParseOptions = {} + +/** + * Represents per-property metadata attached to AST nodes via {@link Base.context}. + * + * **Details** + * + * Tracks whether a property key is optional, mutable, has a constructor + * default, or carries key-level annotations. Typically set by helpers like + * {@link optionalKey} and `Schema.mutableKey`. + * + * - `isOptional` — the property key may be absent from the input. + * - `isMutable` — the property is `readonly` when `false`. + * - `defaultValue` — an {@link Encoding} applied during construction to + * supply missing values. + * - `annotations` — key-level annotations (e.g. description of the key + * itself). + * + * @see {@link optionalKey} + * @see {@link isOptional} + * @category models + * @since 4.0.0 + */ +export class Context { + readonly isOptional: boolean + readonly isMutable: boolean + /** Used for constructor default values (e.g. `withConstructorDefault` API) */ + readonly defaultValue: Encoding | undefined + readonly annotations: Schema.Annotations.Key | undefined + + constructor( + isOptional: boolean, + isMutable: boolean, + /** Used for constructor default values (e.g. `withConstructorDefault` API) */ + defaultValue: Encoding | undefined = undefined, + annotations: Schema.Annotations.Key | undefined = undefined + ) { + this.isOptional = isOptional + this.isMutable = isMutable + this.defaultValue = defaultValue + this.annotations = annotations + } +} + +/** + * Non-empty array of validation {@link Check} values attached to an AST node + * via {@link Base.checks}. + * + * **Details** + * + * Checks are run after basic type matching succeeds. They represent + * refinements like `minLength`, `pattern`, `int`, etc. + * + * @see {@link Check} + * @see {@link Filter} + * @see {@link FilterGroup} + * @category models + * @since 4.0.0 + */ +export type Checks = readonly [Check, ...Array>] + +const TypeId = "~effect/Schema" + +/** + * Represents the abstract base class for all {@link AST} node variants. + * + * **Details** + * + * Every AST node extends `Base` and inherits these fields: + * + * - `annotations` — user-supplied metadata (identifier, title, description, + * arbitrary keys). + * - `checks` — optional {@link Checks} for post-type-match validation. + * - `encoding` — optional {@link Encoding} chain for type ↔ wire + * transformations. + * - `context` — optional {@link Context} for per-property metadata. + * + * Subclasses add a `_tag` discriminant and variant-specific data. + * + * @see {@link AST} + * @category models + * @since 4.0.0 + */ +export abstract class Base { + readonly [TypeId] = TypeId + abstract readonly _tag: string + readonly annotations: Schema.Annotations.Annotations | undefined + readonly checks: Checks | undefined + readonly encoding: Encoding | undefined + readonly context: Context | undefined + + constructor( + annotations: Schema.Annotations.Annotations | undefined = undefined, + checks: Checks | undefined = undefined, + encoding: Encoding | undefined = undefined, + context: Context | undefined = undefined + ) { + this.annotations = annotations + this.checks = checks + this.encoding = encoding + this.context = context + } + toString() { + return `<${this._tag}>` + } +} + +/** + * AST node for user-defined opaque types with custom parsing logic. + * + * **When to use** + * + * Use when you need a custom schema AST node because none of the built-in + * nodes fit. + * + * **Details** + * + * - `typeParameters` — inner schemas this declaration is parameterized over + * (e.g. the element type for a custom collection). + * - `run` — factory that receives `typeParameters` and returns a parser that + * validates or transforms raw input. + * + * @see {@link isDeclaration} + * @category models + * @since 3.10.0 + */ +export class Declaration extends Base { + readonly _tag = "Declaration" + readonly typeParameters: ReadonlyArray + readonly run: ( + typeParameters: ReadonlyArray + ) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect + readonly encodingChecks: Checks | undefined + + constructor( + typeParameters: ReadonlyArray, + run: ( + typeParameters: ReadonlyArray + ) => (input: unknown, self: Declaration, options: ParseOptions) => Effect.Effect, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context, + encodingChecks?: Checks + ) { + super(annotations, checks, encoding, context) + this.typeParameters = typeParameters + this.run = run + this.encodingChecks = encodingChecks + } + /** @internal */ + getParser(): SchemaParser.Parser { + const run = this.run(this.typeParameters) + return (oinput, options) => { + if (Option.isNone(oinput)) return Effect.succeedNone + return Effect.mapEager(run(oinput.value, this, options), Option.some) + } + } + private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { + const tps = mapOrSame(this.typeParameters, recur) + return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks ? + this : + new Declaration(tps, this.run, this.annotations, checks, undefined, this.context, encodingChecks) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.checks, this.encodingChecks) + } + /** @internal */ + flip(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.encodingChecks, this.checks) + } + /** @internal */ + getExpected(): string { + const expected = this.annotations?.expected + if (typeof expected === "string") return expected + return "" + } +} + +/** + * AST node matching the `null` literal value. + * + * **Details** + * + * Parsing succeeds only when the input is exactly `null`. + * + * @see {@link null_ null} + * @see {@link isNull} + * @category models + * @since 4.0.0 + */ +export class Null extends Base { + readonly _tag = "Null" + /** @internal */ + getParser() { + return fromConst(this, null) + } + /** @internal */ + getExpected(): string { + return "null" + } +} + +const null_ = new Null() +export { + /** + * Provides the singleton {@link Null} AST instance. + * + * **When to use** + * + * Use when you need the shared AST node for exact null values while inspecting + * or constructing schema ASTs. + * + * @category constants + * @since 4.0.0 + */ + null_ as null +} + +/** + * AST node matching the `undefined` value. + * + * **Details** + * + * Parsing succeeds only when the input is exactly `undefined`. + * + * @see {@link undefined} + * @see {@link isUndefined} + * @category models + * @since 4.0.0 + */ +export class Undefined extends Base { + readonly _tag = "Undefined" + /** @internal */ + getParser() { + return fromConst(this, undefined) + } + /** @internal */ + toCodecJson(): AST { + return replaceEncoding(this, [undefinedToNull]) + } + /** @internal */ + getExpected(): string { + return "undefined" + } +} + +const undefinedToNull = new Link( + null_, + new SchemaTransformation.Transformation( + SchemaGetter.transform(() => undefined), + SchemaGetter.transform(() => null) + ) +) + +const undefined_ = new Undefined() +export { + /** + * Provides the singleton {@link Undefined} AST instance. + * + * **When to use** + * + * Use when you need the shared AST node for exact undefined values while + * inspecting or constructing schema ASTs. + * + * @category constants + * @since 4.0.0 + */ + undefined_ as undefined +} + +/** + * AST node matching TypeScript `void` return-value semantics. + * + * **When to use** + * + * Use when you need an AST node for a value whose result is intentionally + * ignored. + * + * **Details** + * + * Parsers built from this node accept any present runtime input and map it to + * `undefined`. Public schemas built from it may still expose `void` as their + * typed decoded and encoded representation. + * + * @see {@link undefined} for the AST singleton that matches only exact `undefined` + * @see {@link void_ void} + * @see {@link isVoid} + * @category models + * @since 4.0.0 + */ +export class Void extends Base { + readonly _tag = "Void" + /** @internal */ + getParser() { + return fromAnyToConst(undefined) + } + /** @internal */ + toCodecJson(): AST { + return replaceEncoding(this, [undefinedToNull]) + } + /** @internal */ + getExpected(): string { + return "void" + } +} + +const void_ = new Void() +export { + /** + * Provides the singleton {@link Void} AST instance. + * + * **When to use** + * + * Use when constructing or comparing AST nodes for TypeScript `void` return + * values whose result is intentionally ignored. + * + * **Details** + * + * The node parses any present runtime value as `undefined`; schemas may still + * expose `void` on their typed decoded and encoded sides. + * + * @see {@link Void} for the AST node class + * @see {@link undefined} for the sibling AST singleton that matches exactly `undefined` + * @see {@link isVoid} for narrowing an AST to a `Void` node + * + * @category constructors + * @since 4.0.0 + */ + void_ as void +} + +/** + * AST node representing the `never` type — no value matches. + * + * **Details** + * + * Parsing always fails. Useful as a placeholder in unions or as the result + * of narrowing that eliminates all options. + * + * @see {@link never} + * @see {@link isNever} + * @category models + * @since 4.0.0 + */ +export class Never extends Base { + readonly _tag = "Never" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isNever) + } + /** @internal */ + getExpected(): string { + return "never" + } +} + +/** + * Provides the singleton {@link Never} AST instance. + * + * **When to use** + * + * Use to reuse the canonical bottom-type AST node when constructing, + * comparing, or returning ASTs. + * + * @see {@link Never} for the AST node class + * @see {@link isNever} for narrowing an AST to a `Never` node + * + * @category constructors + * @since 4.0.0 + */ +export const never = new Never() + +/** + * AST node representing the `any` type — every value matches. + * + * @see {@link any} + * @see {@link isAny} + * + * @category models + * @since 4.0.0 + */ +export class Any extends Base { + readonly _tag = "Any" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isUnknown) + } + /** @internal */ + getExpected(): string { + return "any" + } +} + +/** + * Provides the singleton {@link Any} AST instance. + * + * **When to use** + * + * Use when you need the singleton AST node for the TypeScript `any` type and + * intentionally want parsing to accept every input value. + * + * @see {@link unknown} for the sibling AST singleton that also accepts every value while preserving the safer `unknown` type + * + * @category constructors + * @since 4.0.0 + */ +export const any = new Any() + +/** + * AST node representing the `unknown` type — every value matches. + * + * **Details** + * + * Unlike {@link Any}, this is type-safe: the parsed result is typed as + * `unknown` rather than `any`. + * + * @see {@link unknown} + * @see {@link isUnknown} + * @category models + * @since 4.0.0 + */ +export class Unknown extends Base { + readonly _tag = "Unknown" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isUnknown) + } + /** @internal */ + getExpected(): string { + return "unknown" + } +} + +/** + * Provides the singleton {@link Unknown} AST instance. + * + * **When to use** + * + * Use when you need the reusable AST singleton for a schema node that accepts + * every value while keeping parsed values opaque. + * + * @see {@link any} for the singleton that accepts every value as `any` + * + * @category constructors + * @since 4.0.0 + */ +export const unknown = new Unknown() + +/** + * AST node matching the TypeScript `object` type — accepts objects, arrays, + * and functions (anything non-primitive and non-null). + * + * @see {@link objectKeyword} + * @see {@link isObjectKeyword} + * + * @category models + * @since 3.10.0 + */ +export class ObjectKeyword extends Base { + readonly _tag = "ObjectKeyword" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isObjectKeyword) + } + /** @internal */ + getExpected(): string { + return "object | array | function" + } +} + +/** + * Provides the singleton {@link ObjectKeyword} AST instance. + * + * **When to use** + * + * Use to reuse the canonical AST node for the TypeScript `object` keyword when + * building or comparing `SchemaAST` values directly. + * + * @see {@link ObjectKeyword} for the AST node class + * @see {@link isObjectKeyword} for narrowing an AST to an `ObjectKeyword` node + * + * @category constructors + * @since 3.10.0 + */ +export const objectKeyword = new ObjectKeyword() + +/** + * AST node representing a TypeScript `enum`. + * + * **Details** + * + * Holds `enums` as an array of `[name, value]` pairs where values are + * `string | number`. Parsing succeeds when the input matches any enum value. + * + * @see {@link isEnum} + * @category models + * @since 4.0.0 + */ +export class Enum extends Base { + readonly _tag = "Enum" + readonly enums: ReadonlyArray + + constructor( + enums: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.enums = enums + } + /** @internal */ + getParser() { + const values = new Set(this.enums.map(([, v]) => v)) + return fromRefinement( + this, + (input): input is typeof this.enums[number][1] => values.has(input) + ) + } + /** @internal */ + toCodecStringTree(): AST { + if (this.enums.some(([_, v]) => typeof v === "number")) { + const coercions = Object.fromEntries(this.enums.map(([_, v]) => [globalThis.String(v), v])) + return replaceEncoding(this, [ + new Link( + new Union(Object.keys(coercions).map((k) => new Literal(k)), "anyOf"), + new SchemaTransformation.Transformation( + SchemaGetter.transform((s) => coercions[s]), + SchemaGetter.String() + ) + ) + ]) + } + return this + } + /** @internal */ + getExpected(): string { + return this.enums.map(([_, value]) => JSON.stringify(value)).join(" | ") + } +} + +type TemplateLiteralPart = + | String + | Number + | BigInt + | Literal + | TemplateLiteral + | Union + +function isTemplateLiteralPart(ast: AST): ast is TemplateLiteralPart { + switch (ast._tag) { + case "String": + case "Number": + case "BigInt": + return true + case "Literal": + case "TemplateLiteral": + return ast.checks === undefined + case "Union": + return ast.checks === undefined && ast.types.every(isTemplateLiteralPart) + default: + return false + } +} + +/** + * AST node representing a TypeScript template literal type + * (e.g. `` `user_${string}` ``). + * + * **Details** + * + * `parts` is an array of AST nodes; each part contributes to matching + * strings at runtime. + * + * @see {@link isTemplateLiteral} + * @category models + * @since 3.10.0 + */ +export class TemplateLiteral extends Base { + readonly _tag = "TemplateLiteral" + readonly parts: ReadonlyArray + /** @internal */ + readonly encodedParts: ReadonlyArray + + constructor( + parts: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + const encodedParts: Array = [] + for (const part of parts) { + const encoded = toEncoded(part) + if (isTemplateLiteralPart(encoded)) { + encodedParts.push(encoded) + } else { + throw new Error(`Invalid TemplateLiteral part ${encoded._tag}`) + } + } + this.parts = parts + this.encodedParts = encodedParts + } + /** @internal */ + getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + const parser = recur(this.asTemplateLiteralParser()) + return (oinput: Option.Option, options: ParseOptions) => + Effect.mapBothEager(parser(oinput, options), { + onSuccess: () => oinput, + onFailure: (issue) => new SchemaIssue.Composite(this, oinput, [issue]) + }) + } + /** @internal */ + getExpected(): string { + return "string" + } + /** @internal */ + matchPart(s: string, options: ParseOptions): string | undefined { + return segmentTemplateLiteralParts(this.encodedParts, s, options) === undefined ? undefined : s + } + /** @internal */ + asTemplateLiteralParser(): Arrays { + const tuple = new Arrays(false, this.parts.map(partFromString), []) + return decodeTo( + string, + tuple, + new SchemaTransformation.Transformation( + SchemaGetter.transformOrFail((s: string, options) => { + const segments = segmentTemplateLiteralParts(this.encodedParts, s, options) + if (segments !== undefined) return Effect.succeed(segments) + return Effect.fail( + new SchemaIssue.InvalidValue(Option.some(s), { + message: `Expected a string matching template literal parts, got ${format(s)}` + }) + ) + }), + SchemaGetter.transform((parts) => parts.join("")) + ) + ) + } +} + +/** + * AST node matching a specific `unique symbol` value. + * + * **Details** + * + * Parsing succeeds only when the input is reference-equal to the stored + * `symbol`. + * + * @see {@link isUniqueSymbol} + * @category models + * @since 3.10.0 + */ +export class UniqueSymbol extends Base { + readonly _tag = "UniqueSymbol" + readonly symbol: symbol + + constructor( + symbol: symbol, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + this.symbol = symbol + } + /** @internal */ + getParser() { + return fromConst(this, this.symbol) + } + /** @internal */ + toCodecStringTree(): AST { + return replaceEncoding(this, [symbolToString]) + } + /** @internal */ + getExpected(): string { + return globalThis.String(this.symbol) + } +} + +/** + * The set of primitive types that can appear as a {@link Literal} value. + * + * @see {@link Literal} + * + * @category models + * @since 3.10.0 + */ +export type LiteralValue = string | number | boolean | bigint + +/** + * AST node matching an exact primitive value (string, number, boolean, or + * bigint). + * + * **Details** + * + * Parsing succeeds only when the input is strictly equal (`===`) to the + * stored `literal`. Numeric literals must be finite — `Infinity`, `-Infinity`, + * and `NaN` are rejected at construction time. + * + * **Example** (Creating a literal AST) + * + * ```ts + * import { SchemaAST } from "effect" + * + * const ast = new SchemaAST.Literal("active") + * console.log(ast.literal) // "active" + * ``` + * + * @see {@link LiteralValue} + * @see {@link isLiteral} + * @category models + * @since 3.10.0 + */ +export class Literal extends Base { + readonly _tag = "Literal" + readonly literal: LiteralValue + + constructor( + literal: LiteralValue, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + super(annotations, checks, encoding, context) + if (typeof literal === "number" && !globalThis.Number.isFinite(literal)) { + throw new Error(`A numeric literal must be finite, got ${format(literal)}`) + } + this.literal = literal + } + /** @internal */ + getParser() { + return fromConst(this, this.literal) + } + /** @internal */ + matchPart(s: string, _options: ParseOptions): LiteralValue | undefined { + return s === globalThis.String(this.literal) ? this.literal : undefined + } + /** @internal */ + toCodecJson(): AST { + return typeof this.literal === "bigint" ? literalToString(this) : this + } + /** @internal */ + toCodecStringTree(): AST { + return typeof this.literal === "string" ? this : literalToString(this) + } + /** @internal */ + getExpected(): string { + return typeof this.literal === "string" ? JSON.stringify(this.literal) : globalThis.String(this.literal) + } +} + +function literalToString(ast: Literal): Literal { + const literalAsString = globalThis.String(ast.literal) + return replaceEncoding(ast, [ + new Link( + new Literal(literalAsString), + new SchemaTransformation.Transformation( + SchemaGetter.transform(() => ast.literal), + SchemaGetter.transform(() => literalAsString) + ) + ) + ]) +} + +/** + * AST node matching any `string` value. + * + * @see {@link string} + * @see {@link isString} + * + * @category models + * @since 4.0.0 + */ +export class String extends Base { + readonly _tag = "String" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isString) + } + /** @internal */ + matchPart(s: string, options: ParseOptions): string | undefined { + return applyTemplateLiteralPartChecks(this, s, options) + } + /** @internal */ + getExpected(): string { + return "string" + } +} + +/** + * Provides the singleton {@link String} AST instance. + * + * **When to use** + * + * Use as the shared `SchemaAST` node for unconstrained JavaScript strings. + * + * @see {@link String} for the AST node class + * @see {@link isString} for narrowing an AST to a string node + * + * @category constructors + * @since 4.0.0 + */ +export const string = new String() + +/** + * AST node matching any `number` value (including `NaN`, `Infinity`, + * `-Infinity`). + * + * **Details** + * + * Default JSON serialization: + * + * - Finite numbers are serialized as JSON numbers. + * - `Infinity`, `-Infinity`, and `NaN` are serialized as JSON strings. + * + * If the node has an `isFinite` or `isInt` check, the string fallback is + * skipped since non-finite values cannot occur. + * + * @see {@link number} + * @see {@link isNumber} + * @category models + * @since 4.0.0 + */ +export class Number extends Base { + readonly _tag = "Number" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isNumber) + } + /** @internal */ + matchKey(s: string, options: ParseOptions): number | undefined { + return this._match(isStringNumberRegExp, s, options) + } + /** @internal */ + matchPart(s: string, options: ParseOptions): number | undefined { + return this._match(isStringFiniteRegExp, s, options) + } + private _match(regexp: RegExp, s: string, options: ParseOptions): number | undefined { + return regexp.test(s) + ? applyTemplateLiteralPartChecks(this, globalThis.Number(s), options) + : undefined + } + /** @internal */ + toCodecJson(): AST { + if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) { + return this + } + return replaceEncoding(this, [numberToJson]) + } + /** @internal */ + toCodecStringTree(): AST { + if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) { + return replaceEncoding(this, [finiteToString]) + } + return replaceEncoding(this, [numberToString]) + } + /** @internal */ + getExpected(): string { + return "number" + } +} + +// oxlint-disable-next-line only-used-in-recursion - @gcanti what's this? :-) +function hasCheck(checks: ReadonlyArray>, tag: string): boolean { + return checks.some((c) => { + switch (c._tag) { + case "Filter": + return c.annotations?.meta?._tag === tag + case "FilterGroup": + return hasCheck(c.checks, tag) + } + }) +} + +/** + * Provides the singleton {@link Number} AST instance. + * + * **When to use** + * + * Use when you need the canonical `SchemaAST` node for schemas that accept any + * JavaScript number value. + * + * @see {@link Number} for the AST node class and serialization behavior + * @see {@link Literal} for exact finite numeric literal AST nodes + * + * @category constructors + * @since 4.0.0 + */ +export const number = new Number() + +/** + * AST node matching any `boolean` value (`true` or `false`). + * + * @see {@link boolean} + * @see {@link isBoolean} + * + * @category models + * @since 4.0.0 + */ +export class Boolean extends Base { + readonly _tag = "Boolean" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isBoolean) + } + /** @internal */ + getExpected(): string { + return "boolean" + } +} + +/** + * Provides the singleton {@link Boolean} AST instance. + * + * **When to use** + * + * Use to reuse the standard AST node that accepts either `true` or `false` when + * constructing schema ASTs directly. + * + * @see {@link Boolean} for the AST node class + * @see {@link Literal} for exact boolean literal AST nodes + * + * @category constructors + * @since 4.0.0 + */ +export const boolean = new Boolean() + +/** + * AST node matching any `symbol` value. + * + * **When to use** + * + * Use when you need the AST node class for schemas that match any JavaScript + * symbol value. + * + * **Details** + * + * When serialized to a string-based codec, symbols are converted via + * `Symbol.keyFor` and must be registered with `Symbol.for`. + * + * @see {@link symbol} + * @see {@link isSymbol} + * @category models + * @since 4.0.0 + */ +export class Symbol extends Base { + readonly _tag = "Symbol" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isSymbol) + } + /** @internal */ + matchKey(s: symbol, options: ParseOptions): symbol | undefined { + return applyTemplateLiteralPartChecks(this, s, options) + } + /** @internal */ + toCodecStringTree(): AST { + return replaceEncoding(this, [symbolToString]) + } + /** @internal */ + getExpected(): string { + return "symbol" + } +} + +/** + * Provides the singleton {@link Symbol} AST instance. + * + * **When to use** + * + * Use to reuse the singleton AST node for schemas that match any JavaScript + * symbol value. + * + * **Gotchas** + * + * String-based codecs can encode only symbols registered with `Symbol.for`, + * because the implementation uses `Symbol.keyFor`. + * + * @see {@link UniqueSymbol} for an AST node that matches one specific symbol + * + * @category constructors + * @since 4.0.0 + */ +export const symbol = new Symbol() + +/** + * AST node matching any `bigint` value. + * + * **Details** + * + * When serialized to a string-based codec, bigints are converted to/from + * their decimal string representation. + * + * @see {@link bigInt} + * @see {@link isBigInt} + * @category models + * @since 4.0.0 + */ +export class BigInt extends Base { + readonly _tag = "BigInt" + /** @internal */ + getParser() { + return fromRefinement(this, Predicate.isBigInt) + } + /** @internal */ + matchPart(s: string, options: ParseOptions): bigint | undefined { + return isStringBigIntRegExp.test(s) + ? applyTemplateLiteralPartChecks(this, globalThis.BigInt(s), options) + : undefined + } + /** @internal */ + toCodecStringTree(): AST { + return replaceEncoding(this, [bigIntToString]) + } + /** @internal */ + getExpected(): string { + return "bigint" + } +} + +/** + * Provides the singleton {@link BigInt} AST instance. + * + * **When to use** + * + * Use to reuse the canonical `BigInt` AST node when constructing, inspecting, + * or transforming schemas at the AST level. + * + * @see {@link BigInt} for the AST node class and string-codec behavior + * @see {@link isBigInt} for narrowing an AST to a `BigInt` node + * + * @category constructors + * @since 4.0.0 + */ +export const bigInt = new BigInt() + +/** + * AST node for array-like types — both tuples and arrays. + * + * **When to use** + * + * Use when constructing or inspecting AST nodes for tuple or array-like schemas, + * including rest elements. + * + * **Details** + * + * - `elements` — positional element types (tuple elements). An element is + * optional if its {@link Context.isOptional} is `true`. + * - `rest` — the rest/variadic element types. When non-empty, the first + * entry is the "spread" type (e.g. `...Array`), and subsequent + * entries are trailing positional elements after the spread. + * - `isMutable` — whether the resulting array is `readonly` (`false`) or + * mutable (`true`). + * + * **Gotchas** + * + * Construction enforces TypeScript ordering rules: a required element + * cannot follow an optional one, and an optional element cannot follow a + * rest element. + * + * **Example** (Inspecting a tuple AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.Tuple([Schema.String, Schema.Number]) + * const ast = schema.ast + * + * if (SchemaAST.isArrays(ast)) { + * console.log(ast.elements.length) // 2 + * console.log(ast.rest.length) // 0 + * } + * ``` + * + * @see {@link isArrays} + * @see {@link Objects} + * @category models + * @since 4.0.0 + */ +export class Arrays extends Base { + readonly _tag = "Arrays" + readonly isMutable: boolean + readonly elements: ReadonlyArray + readonly rest: ReadonlyArray + readonly encodingChecks: Checks | undefined + + constructor( + isMutable: boolean, + elements: ReadonlyArray, + rest: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context, + encodingChecks?: Checks + ) { + super(annotations, checks, encoding, context) + this.isMutable = isMutable + this.elements = elements + this.rest = rest + this.encodingChecks = encodingChecks + + // A required element cannot follow an optional element. ts(1257) + const i = elements.findIndex(isOptional) + if (i !== -1 && (elements.slice(i + 1).some((e) => !isOptional(e)) || rest.length > 1)) { + throw new Error("A required element cannot follow an optional element. ts(1257)") + } + + // An optional element cannot follow a rest element.ts(1266) + if (rest.length > 1 && rest.slice(1).some(isOptional)) { + throw new Error("An optional element cannot follow a rest element. ts(1266)") + } + } + /** @internal */ + getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const ast = this + const elements = ast.elements.map((ast) => ({ ast, parser: recur(ast) })) + const rest = ast.rest.map((ast) => ({ ast, parser: recur(ast) })) + const elementLen = elements.length + + const [head, ...tail] = rest + const tailLen = tail.length + + function getParser( + tailThreshold: number, + index: number + ): { readonly ast: AST; readonly parser: SchemaParser.Parser } { + if (index < elementLen) { + return elements[index] + } else if (index >= tailThreshold) { + return tail[index - tailThreshold] + } + return head + } + + return Effect.fnUntracedEager(function*(oinput, options) { + if (oinput._tag === "None") { + return oinput + } + const input = oinput.value + + // If the input is not an array, return early with an error + if (!Array.isArray(input)) { + return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + } + + const len = input.length + const state = { + ast, + getParser, + oinput, + len, + tailThreshold: resolveTailThreshold(len, elementLen, tailLen), + output: new globalThis.Array(len), + issues: undefined as Arr.NonEmptyArray | undefined, + options + } + const concurrency = resolveConcurrency(options?.concurrency) + const eff = parseArray(state, input, { + concurrency: concurrency?.concurrency, + end: ast.rest.length === 0 ? elementLen : Math.max(len, elementLen + tailLen) + }) + if (eff) yield* eff + + // --------------------------------------------- + // handle excess indexes + // --------------------------------------------- + if (ast.rest.length === 0 && len > elementLen) { + for (let i = elementLen; i <= len - 1; i++) { + const issue = new SchemaIssue.Pointer([i], new SchemaIssue.UnexpectedKey(ast, input[i])) + if (options.errors === "all") { + if (state.issues) state.issues.push(issue) + else state.issues = [issue] + } else { + return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue])) + } + } + } + if (state.issues) { + return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues)) + } + return Option.some(state.output) + }) + } + private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { + const elements = mapOrSame(this.elements, recur) + const rest = mapOrSame(this.rest, recur) + return elements === this.elements && rest === this.rest && checks === this.checks && + encodingChecks === this.encodingChecks ? + this : + new Arrays( + this.isMutable, + elements, + rest, + this.annotations, + checks, + undefined, + this.context, + encodingChecks + ) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.checks, this.encodingChecks) + } + /** @internal */ + flip(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.encodingChecks, this.checks) + } + /** @internal */ + getExpected(): string { + return "array" + } +} +const parseArray = iterateEager<{ + readonly ast: AST + readonly oinput: Option.Option + readonly len: number + readonly getParser: ( + tailThreshold: number, + index: number + ) => { readonly ast: AST; readonly parser: SchemaParser.Parser } + readonly tailThreshold: number + readonly options: ParseOptions + readonly output: Array + issues: Array | undefined +}, unknown>()({ + onItem(s, item, i) { + const value = i < s.len ? Option.some(item) : Option.none() + return s.getParser(s.tailThreshold, i).parser(value, s.options) + }, + step(s, _, exit, i) { + if (exit._tag === "Failure") { + return wrapPropertyKeyIssue(s, s.ast, i, exit) + } else if (exit.value._tag === "Some") { + s.output[i] = exit.value.value + } else { + const p = s.getParser(s.tailThreshold, i) + if (isOptional(p.ast)) return + const issue = new SchemaIssue.Pointer([i], new SchemaIssue.MissingKey(p.ast.context?.annotations)) + if (s.options.errors === "all") { + if (s.issues) s.issues.push(issue) + else s.issues = [issue] + } else { + return Exit.fail(new SchemaIssue.Composite(s.ast, s.oinput, [issue])) + } + } + } +}) + +function resolveTailThreshold( + inputLen: number, + elementLen: number, + tailLen: number +) { + return Math.max(elementLen, inputLen - tailLen) +} + +const resolveConcurrency = (value: number | "unbounded" | undefined) => { + value = value === "unbounded" ? Infinity : value ?? 1 + return value > 1 ? { concurrency: value } : undefined +} + +const wrapPropertyKeyIssue = ( + s: { + readonly oinput: Option.Option + readonly options: ParseOptions + issues: Array | undefined + }, + ast: AST, + key: PropertyKey, + exit: Exit.Failure +) => { + if (exit.cause.reasons.length === 0) { + return exit + } + const issue = InternalSchemaCause.getSchemaIssue(exit.cause) + if (issue === undefined) { + return Exit.failCause( + Cause.map( + exit.cause, + (issue) => new SchemaIssue.Composite(ast, s.oinput, [new SchemaIssue.Pointer([key], issue)]) + ) + ) + } + const pointer = new SchemaIssue.Pointer([key], issue) + if (s.options.errors === "all") { + if (s.issues) s.issues.push(pointer) + else s.issues = [pointer] + } else { + return Exit.fail(new SchemaIssue.Composite(ast, s.oinput, [pointer])) + } +} + +/** + * floating point or integer, with optional exponent + * @internal + */ +export const FINITE_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?" + +/** + * Returns the object keys that match the index signature parameter schema. + * @internal + */ +export function getIndexSignatureKeys( + input: { readonly [x: PropertyKey]: unknown }, + parameter: IndexSignatureParameter, + options: ParseOptions = defaultParseOptions +): ReadonlyArray { + let stringKeys: ReadonlyArray | undefined + let symbolKeys: ReadonlyArray | undefined + + function go(parameter: AST): ReadonlyArray { + switch (parameter._tag) { + case "String": + case "TemplateLiteral": + return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchPart(k, options) !== undefined) + case "Number": + return (stringKeys ??= Object.keys(input)).filter((k) => parameter.matchKey(k, options) !== undefined) + case "Symbol": + return (symbolKeys ??= Object.getOwnPropertySymbols(input)).filter((k) => + parameter.matchKey(k, options) !== undefined + ) + case "Union": + return [...new Set(parameter.types.flatMap(go))] + default: + return [] + } + } + return go(parameterFromPropertyKey(toEncoded(parameter))) +} + +/** + * Represents a named property within an {@link Objects} node. + * + * **Details** + * + * Pairs a `name` (any `PropertyKey`) with a `type` ({@link AST}). The + * property's optionality and mutability are determined by the `type`'s + * {@link Context}. + * + * @see {@link Objects} + * @category models + * @since 3.10.0 + */ +export class PropertySignature { + readonly name: PropertyKey + readonly type: AST + + constructor( + name: PropertyKey, + type: AST + ) { + this.name = name + this.type = type + } +} + +/** + * Represents a bidirectional merge strategy for index signature key-value pairs. + * + * **Details** + * + * Used by {@link IndexSignature} when the same key appears multiple times + * (e.g. from `Schema.extend` or overlapping records). Provides separate + * `decode` and `encode` combiners that determine how duplicate entries are + * merged. + * + * @see {@link IndexSignature} + * @category models + * @since 4.0.0 + */ +export class KeyValueCombiner { + readonly decode: Combiner.Combiner | undefined + readonly encode: Combiner.Combiner | undefined + + constructor( + decode: Combiner.Combiner | undefined, + encode: Combiner.Combiner | undefined + ) { + this.decode = decode + this.encode = encode + } + /** @internal */ + flip(): KeyValueCombiner { + return new KeyValueCombiner(this.encode, this.decode) + } +} + +type IndexSignatureParameter = + | String + | Number + | Symbol + | TemplateLiteral + | Union + +function isIndexSignatureParameterSide(ast: AST): ast is IndexSignatureParameter { + switch (ast._tag) { + case "String": + case "Number": + case "Symbol": + case "TemplateLiteral": + return true + case "Union": + return ast.types.every(isIndexSignatureParameterSide) + default: + return false + } +} + +function isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter { + return isIndexSignatureParameterSide(ast) && isIndexSignatureParameterSide(toEncoded(ast)) +} + +/** + * Represents an index signature entry within an {@link Objects} node. + * + * **When to use** + * + * Use when constructing or inspecting object AST entries for record-like keys + * and values. + * + * **Details** + * + * - `parameter` — the key type AST (e.g. {@link String} for `string` keys, + * {@link TemplateLiteral} for patterned keys). + * - `type` — the value type SchemaAST. + * - `merge` — optional {@link KeyValueCombiner} for handling duplicate keys. + * + * **Gotchas** + * + * Using `Schema.optionalKey` on the value type is not allowed for index + * signatures (throws at construction); use `Schema.optional` instead. + * + * @see {@link Objects} + * @see {@link PropertySignature} + * @category models + * @since 3.10.0 + */ +export class IndexSignature { + readonly parameter: IndexSignatureParameter + readonly type: AST + readonly merge: KeyValueCombiner | undefined + + constructor( + parameter: AST, + type: AST, + merge: KeyValueCombiner | undefined + ) { + if (!isIndexSignatureParameter(parameter)) { + throw new Error(`Invalid index signature parameter ${parameter._tag}`) + } + this.parameter = parameter + this.type = type + this.merge = merge + if (isOptional(type) && !containsUndefined(type)) { + throw new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.") + } + } +} + +/** + * AST node for object-like schemas, including structs and records. + * + * **When to use** + * + * Use when constructing or inspecting AST nodes for structs or records rather + * than array-like schemas. + * + * **Details** + * + * - `propertySignatures` — named properties with their types (struct fields). + * - `indexSignatures` — index signature entries (record patterns), each with + * a `parameter` AST for matching keys and a `type` AST for values. + * + * An `Objects` node with no properties and no index signatures performs only a + * non-nullish check: it accepts any value except `null` and `undefined`, + * including primitive values. + * + * **Gotchas** + * + * Duplicate property names throw at construction time. + * + * **Example** (Inspecting a struct AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.Struct({ name: Schema.String }) + * const ast = schema.ast + * + * if (SchemaAST.isObjects(ast)) { + * for (const ps of ast.propertySignatures) { + * console.log(ps.name, ps.type._tag) + * } + * // "name" "String" + * } + * ``` + * + * @see {@link isObjects} + * @see {@link PropertySignature} + * @see {@link IndexSignature} + * @see {@link Arrays} + * @category models + * @since 4.0.0 + */ +export class Objects extends Base { + readonly _tag = "Objects" + readonly propertySignatures: ReadonlyArray + readonly indexSignatures: ReadonlyArray + readonly encodingChecks: Checks | undefined + + constructor( + propertySignatures: ReadonlyArray, + indexSignatures: ReadonlyArray, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context, + encodingChecks?: Checks + ) { + super(annotations, checks, encoding, context) + this.propertySignatures = propertySignatures + this.indexSignatures = indexSignatures + this.encodingChecks = encodingChecks + + // Duplicate property signatures + const duplicates = propertySignatures.map((ps) => ps.name).filter((name, i, arr) => arr.indexOf(name) !== i) + if (duplicates.length > 0) { + throw new Error(`Duplicate identifiers: ${JSON.stringify(duplicates)}. ts(2300)`) + } + } + /** @internal */ + getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const ast = this + const expectedKeys: Array = [] + const expectedKeysSet = new Set() + const properties: Array<{ + readonly ps: PropertySignature | IndexSignature + readonly parser: SchemaParser.Parser + readonly name: PropertyKey + readonly type: AST + }> = [] + for (const ps of ast.propertySignatures) { + expectedKeys.push(ps.name) + expectedKeysSet.add(ps.name) + properties.push({ + ps, + parser: recur(ps.type), + name: ps.name, + type: ps.type + }) + } + const indexCount = ast.indexSignatures.length + // --------------------------------------------- + // handle empty struct + // --------------------------------------------- + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return fromRefinement(ast, Predicate.isNotNullish) + } + + const parseIndexes = indexCount > 0 ? + iterateEager<{ + readonly oinput: Option.Option + readonly input: Record + readonly options: ParseOptions + readonly out: Record + issues: Array | undefined + }, [key: PropertyKey, is: IndexSignature]>()({ + onItem: Effect.fnUntracedEager(function*( + s, + [key, is] + ) { + const parserKey = recur(parameterFromPropertyKey(is.parameter)) + const effKey = parserKey(Option.some(key), s.options) + const exitKey = (effectIsExit(effKey) ? effKey : yield* Effect.exit(effKey)) as Exit.Exit< + Option.Option, + SchemaIssue.Issue + > + if (exitKey._tag === "Failure") { + const eff = wrapPropertyKeyIssue(s, ast, key, exitKey) + if (eff) yield* eff + return + } + + const value: Option.Option = Option.some(s.input[key]) + const parserValue = recur(is.type) + const effValue = parserValue(value, s.options) + const exitValue = effectIsExit(effValue) ? effValue : yield* Effect.exit(effValue) + if (exitValue._tag === "Failure") { + const eff = wrapPropertyKeyIssue(s, ast, key, exitValue) + if (eff) yield* eff + return + } else if (exitKey.value._tag === "Some" && exitValue.value._tag === "Some") { + const k2 = exitKey.value.value + if (expectedKeysSet.has(key) || expectedKeysSet.has(k2)) { + return + } + const v2 = exitValue.value.value + if (is.merge && is.merge.decode && Object.hasOwn(s.out, k2)) { + const [k, v] = is.merge.decode.combine([k2, s.out[k2]], [k2, v2]) + internalRecord.set(s.out, k, v) + } else { + internalRecord.set(s.out, k2, v2) + } + } + }), + step: (_s, _, exit: Exit.Exit) => exit._tag === "Failure" ? exit : undefined + }) : + undefined + + return Effect.fnUntracedEager(function*(oinput, options) { + if (oinput._tag === "None") { + return oinput + } + const input = oinput.value as Record + + // If the input is not a record, return early with an error + if (!(typeof input === "object" && input !== null && !Array.isArray(input))) { + return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + } + + const out: Record = {} + const state = { + ast, + oinput, + input, + out, + issues: undefined as Arr.NonEmptyArray | undefined, + options + } + const errorsAllOption = options.errors === "all" + const onExcessPropertyError = options.onExcessProperty === "error" + const onExcessPropertyPreserve = options.onExcessProperty === "preserve" + + // --------------------------------------------- + // handle excess properties + // --------------------------------------------- + let inputKeys: Array | undefined + if (ast.indexSignatures.length === 0 && (onExcessPropertyError || onExcessPropertyPreserve)) { + inputKeys = Reflect.ownKeys(input) + for (let i = 0; i < inputKeys.length; i++) { + const key = inputKeys[i] + if (!expectedKeysSet.has(key)) { + // key is unexpected + if (onExcessPropertyError) { + const issue = new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(ast, input[key])) + if (errorsAllOption) { + if (state.issues) { + state.issues.push(issue) + } else { + state.issues = [issue] + } + continue + } else { + return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue])) + } + } else { + // preserve key + internalRecord.set(out, key, input[key]) + } + } + } + } + + const concurrency = resolveConcurrency(options?.concurrency) + + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + const eff = parseProperties(state, properties, concurrency) + if (eff) yield* eff + + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + if (parseIndexes) { + const keyPairs = Arr.empty<[PropertyKey, IndexSignature]>() + for (let i = 0; i < indexCount; i++) { + const is = ast.indexSignatures[i] + const keys = getIndexSignatureKeys(input, is.parameter, options) + for (let j = 0; j < keys.length; j++) { + const key = keys[j] + keyPairs.push([key, is]) + } + } + const eff = parseIndexes(state, keyPairs, concurrency) + if (eff) yield* eff + } + + if (state.issues) { + return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues)) + } + if (options.propertyOrder === "original") { + // preserve input keys order + const keys = (inputKeys ?? Reflect.ownKeys(input)).concat(expectedKeys) + const preserved: Record = {} + for (const key of keys) { + if (Object.hasOwn(out, key)) { + internalRecord.set(preserved, key, out[key]) + } + } + return Option.some(preserved) + } + return Option.some(out) + }) + } + private _rebuild( + recur: (ast: AST) => AST, + recurParameter: (ast: AST) => AST, + flipMerge: boolean, + checks: Checks | undefined, + encodingChecks: Checks | undefined + ): Objects { + const props = mapOrSame(this.propertySignatures, (ps) => { + const t = recur(ps.type) + return t === ps.type ? ps : new PropertySignature(ps.name, t) + }) + + const indexes = mapOrSame(this.indexSignatures, (is) => { + const p = recurParameter(is.parameter) + const t = recur(is.type) + const merge = flipMerge ? is.merge?.flip() : is.merge + return p === is.parameter && t === is.type && merge === is.merge + ? is + : new IndexSignature(p, t, merge) + }) + + return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks && + encodingChecks === this.encodingChecks + ? this + : new Objects( + props, + indexes, + this.annotations, + checks, + undefined, + this.context, + encodingChecks + ) + } + /** @internal */ + flip(recur: (ast: AST) => AST): AST { + return this._rebuild(recur, recur, true, this.encodingChecks, this.checks) + } + /** @internal */ + recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST { + return this._rebuild(recur, recurParameter, false, this.checks, this.encodingChecks) + } + /** @internal */ + getExpected(): string { + if (this.propertySignatures.length === 0 && this.indexSignatures.length === 0) return "object | array" + return "object" + } +} + +type ParsedProperty = { + readonly ps: PropertySignature | IndexSignature + readonly parser: SchemaParser.Parser + readonly name: PropertyKey + readonly type: AST +} + +const parseProperties = iterateEager<{ + readonly ast: AST + readonly oinput: Option.Option + readonly input: Record + readonly options: ParseOptions + readonly out: Record + issues: Array | undefined +}, ParsedProperty>()({ + onItem( + s: { + readonly oinput: Option.Option + readonly input: Record + readonly options: ParseOptions + readonly out: Record + issues: Array | undefined + }, + p + ) { + const value: Option.Option = Object.hasOwn(s.input, p.name) + ? Option.some(s.input[p.name]) + : Option.none() + return p.parser(value, s.options) + }, + step(s, p, exit) { + if (exit._tag === "Failure") { + return wrapPropertyKeyIssue(s, s.ast, p.name, exit) + } else if (exit.value._tag === "Some") { + internalRecord.set(s.out, p.name, exit.value.value) + } else if (!isOptional(p.type)) { + const issue = new SchemaIssue.Pointer([p.name], new SchemaIssue.MissingKey(p.type.context?.annotations)) + if (s.options.errors === "all") { + if (s.issues) s.issues.push(issue) + else s.issues = [issue] + return + } else { + return Exit.fail( + new SchemaIssue.Composite(s.ast, s.oinput, [issue]) + ) + } + } + } +}) + +function combineChecks(a: Checks | undefined, b: Checks | undefined): Checks | undefined { + if (!a) return b + if (!b) return a + return [...a, ...b] +} + +/** @internal */ +export function struct( + fields: Fields, + checks: Checks | undefined, + annotations?: Schema.Annotations.Annotations +): Objects { + return new Objects( + Reflect.ownKeys(fields).map((key) => { + return new PropertySignature(key, fields[key].ast) + }), + [], + annotations, + checks + ) +} + +/** @internal */ +export function getAST(self: S): S["ast"] { + return self.ast +} + +/** @internal */ +export function tuple( + elements: Elements, + checks: Checks | undefined = undefined +): Arrays { + return new Arrays(false, elements.map((e) => e.ast), [], undefined, checks) +} + +/** @internal */ +export function union>( + members: Members, + mode: "anyOf" | "oneOf", + checks: Checks | undefined +): Union { + return new Union(members.map(getAST), mode, undefined, checks) +} + +/** @internal */ +export function structWithRest(ast: Objects, records: ReadonlyArray): Objects { + if (ast.encoding || records.some((r) => r.encoding)) { + throw new Error("StructWithRest does not support encodings") + } + let propertySignatures = ast.propertySignatures + let indexSignatures = ast.indexSignatures + let checks = ast.checks + for (const record of records) { + propertySignatures = propertySignatures.concat(record.propertySignatures) + indexSignatures = indexSignatures.concat(record.indexSignatures) + checks = combineChecks(checks, record.checks) + } + return new Objects(propertySignatures, indexSignatures, undefined, checks) +} + +/** @internal */ +export function tupleWithRest(ast: Arrays, rest: ReadonlyArray): Arrays { + if (ast.encoding) { + throw new Error("TupleWithRest does not support encodings") + } + return new Arrays(ast.isMutable, ast.elements, rest, undefined, ast.checks) +} + +type Type = + | "null" + | "array" + | "object" + | "string" + | "number" + | "boolean" + | "symbol" + | "undefined" + | "bigint" + | "function" + +/** @internal */ +export type Sentinel = { + readonly key: PropertyKey + readonly literal: LiteralValue | symbol +} + +function getCandidateTypes(ast: AST): ReadonlyArray { + switch (ast._tag) { + case "Null": + return ["null"] + case "Undefined": + return ["undefined"] + case "String": + case "TemplateLiteral": + return ["string"] + case "Number": + return ["number"] + case "Boolean": + return ["boolean"] + case "Symbol": + case "UniqueSymbol": + return ["symbol"] + case "BigInt": + return ["bigint"] + case "Arrays": + return ["array"] + case "ObjectKeyword": + return ["object", "array", "function"] + case "Objects": + return ast.propertySignatures.length || ast.indexSignatures.length + ? ["object"] + : ["object", "array"] + case "Enum": + return Array.from(new Set(ast.enums.map(([, v]) => typeof v))) + case "Literal": + return [typeof ast.literal] + case "Union": + return Array.from(new Set(ast.types.flatMap(getCandidateTypes))) + default: + return [ + "null", + "undefined", + "string", + "number", + "boolean", + "symbol", + "bigint", + "object", + "array", + "function" + ] + } +} + +/** @internal */ +export function collectSentinels(ast: AST): Array { + switch (ast._tag) { + default: + return [] + case "Declaration": { + const s = ast.annotations?.["~sentinels"] + return Array.isArray(s) ? s : [] + } + case "Objects": + return ast.propertySignatures.flatMap((ps): Array => { + const type = ps.type + if (!isOptional(type)) { + if (isLiteral(type)) { + return [{ key: ps.name, literal: type.literal }] + } + if (isUniqueSymbol(type)) { + return [{ key: ps.name, literal: type.symbol }] + } + } + return [] + }) + case "Arrays": + return ast.elements.flatMap((e, i) => { + return isLiteral(e) && !isOptional(e) + ? [{ key: i, literal: e.literal }] + : [] + }) + case "Suspend": + return collectSentinels(ast.thunk()) + } +} + +type CandidateIndex = { + byType?: { [K in Type]?: Array } + bySentinel?: Map>> + otherwise?: { [K in Type]?: Array } +} + +const candidateIndexCache = new WeakMap, CandidateIndex>() + +function getIndex(types: ReadonlyArray): CandidateIndex { + let idx = candidateIndexCache.get(types) + if (idx) return idx + + idx = {} + for (let i = 0; i < types.length; i++) { + const a = types[i] + const encoded = toEncoded(a) + if (isNever(encoded)) continue + + const candidateTypes = getCandidateTypes(encoded) + const sentinels = collectSentinels(encoded) + + // by-type (always filled – cheap primary filter) + idx.byType ??= {} + for (const t of candidateTypes) (idx.byType[t] ??= []).push(i) + + if (sentinels.length > 0) { // discriminated variants + idx.bySentinel ??= new Map() + for (const { key, literal } of sentinels) { + let m = idx.bySentinel.get(key) + if (!m) idx.bySentinel.set(key, m = new Map()) + let arr = m.get(literal) + if (!arr) m.set(literal, arr = []) + arr.push(i) + } + } else { // non-discriminated + idx.otherwise ??= {} + for (const t of candidateTypes) (idx.otherwise[t] ??= []).push(i) + } + } + + candidateIndexCache.set(types, idx) + return idx +} + +function filterLiterals(input: any) { + return (ast: AST) => { + const encoded = toEncoded(ast) + return encoded._tag === "Literal" ? + encoded.literal === input + : encoded._tag === "UniqueSymbol" ? + encoded.symbol === input + : true + } +} + +/** + * The goal is to reduce the number of a union members that will be checked. + * This is useful to reduce the number of issues that will be returned. + * + * @internal + */ +export function getCandidates(input: any, types: ReadonlyArray): ReadonlyArray { + const idx = getIndex(types) + const runtimeType: Type = input === null ? "null" : Array.isArray(input) ? "array" : typeof input + + // 1. Try sentinel-based dispatch (most selective) + if (idx.bySentinel) { + const base = idx.otherwise?.[runtimeType] ?? [] + if (runtimeType === "object" || runtimeType === "array") { + const selected = new Set(base) + for (const [k, m] of idx.bySentinel) { + if (Object.hasOwn(input, k)) { + const match = m.get((input as any)[k]) + if (match) { + for (const candidate of match) selected.add(candidate) + } + } + } + return Array.from(selected).sort((a, b) => a - b).map((i) => types[i]).filter(filterLiterals(input)) + } + return base.map((i) => types[i]) + } + + // 2. Fallback: runtime-type dispatch only + return (idx.byType?.[runtimeType] ?? []).map((i) => types[i]).filter(filterLiterals(input)) +} + +/** + * AST node representing a union of schemas. + * + * **Details** + * + * - `types` — the member AST nodes. + * - `mode` — `"anyOf"` succeeds on the first match (like TypeScript unions); + * `"oneOf"` requires exactly one member to match (fails if multiple do). + * + * During parsing, members are tried in order. An internal candidate index + * narrows which members to try based on the runtime type of the input and + * discriminant ("sentinel") fields, making large unions efficient. + * + * **Example** (Inspecting a union AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.Union([Schema.String, Schema.Number]) + * const ast = schema.ast + * + * if (SchemaAST.isUnion(ast)) { + * console.log(ast.types.length) // 2 + * console.log(ast.mode) // "anyOf" + * } + * ``` + * + * @see {@link isUnion} + * @category models + * @since 3.10.0 + */ +export class Union extends Base { + readonly _tag = "Union" + readonly types: ReadonlyArray + readonly mode: "anyOf" | "oneOf" + readonly encodingChecks: Checks | undefined + + constructor( + types: ReadonlyArray, + mode: "anyOf" | "oneOf", + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context, + encodingChecks?: Checks + ) { + super(annotations, checks, encoding, context) + this.types = types + this.mode = mode + this.encodingChecks = encodingChecks + } + /** @internal */ + getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const ast = this + + return (oinput, options) => { + if (oinput._tag === "None") { + return Effect.succeed(oinput) + } + const input = oinput.value + const candidates = getCandidates(input, ast.types) + + const state = { + ast, + recur, + oinput, + input, + out: undefined, + successes: [], + issues: undefined as Arr.NonEmptyArray | undefined, + options + } + const concurrency = resolveConcurrency(options?.concurrency) + const eff = parseUnion(state, candidates, concurrency ? { ...concurrency, orderedStep: true } : undefined) + if (!eff) { + return state.out + ? Effect.succeed(state.out) + : Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? [])) + } + return Effect.flatMap(eff, (_) => { + return state.out + ? Effect.succeed(state.out) + : Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? [])) + }) + } + } + private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { + const types = mapOrSame(this.types, recur) + return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ? + this : + new Union(types, this.mode, this.annotations, checks, undefined, this.context, encodingChecks) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.checks, this.encodingChecks) + } + /** @internal */ + flip(recur: (ast: AST) => AST) { + return this._rebuild(recur, this.encodingChecks, this.checks) + } + /** @internal */ + matchPart(s: string, options: ParseOptions): LiteralValue | undefined { + for (const type of this.types) { + const out = (type as TemplateLiteralPart).matchPart(s, options) + if (out !== undefined) return out + } + return undefined + } + /** @internal */ + getExpected(getExpected: (ast: AST) => string): string { + const expected = this.annotations?.expected + if (typeof expected === "string") return expected + + if (this.types.length === 0) return "never" + + const types = this.types.map((type) => { + const encoded = toEncoded(type) + switch (encoded._tag) { + case "Arrays": { + const literals = encoded.elements.filter(isLiteral) + if (literals.length > 0) { + return `${formatIsMutable(encoded.isMutable)}[ ${ + literals.map((e) => getExpected(e) + formatIsOptional(e.context?.isOptional)).join(", ") + }, ... ]` + } + break + } + case "Objects": { + const literals = encoded.propertySignatures.filter((ps) => isLiteral(ps.type)) + if (literals.length > 0) { + return `{ ${ + literals.map((ps) => + `${formatIsMutable(ps.type.context?.isMutable)}${formatPropertyKey(ps.name)}${ + formatIsOptional(ps.type.context?.isOptional) + }: ${getExpected(ps.type)}` + ).join(", ") + }, ... }` + } + break + } + } + return getExpected(encoded) + }) + return Array.from(new Set(types)).join(" | ") + } +} + +const parseUnion = iterateEager<{ + readonly recur: (ast: AST) => SchemaParser.Parser + readonly ast: Union + readonly oinput: Option.Option + readonly input: unknown + readonly options: ParseOptions + out: Option.Option | undefined + successes: Array + issues: Array | undefined +}, AST>()({ + onItem(s, ast) { + const parser = s.recur(ast) + return parser(s.oinput, s.options) + }, + step(s, candidate, exit) { + if (exit._tag === "Failure") { + const issue = InternalSchemaCause.getSchemaIssue(exit.cause) + if (issue === undefined) { + return exit + } + if (s.issues) s.issues.push(issue) + else s.issues = [issue] + } else { + if (s.out && s.ast.mode === "oneOf") { + s.successes.push(candidate) + return Exit.fail(new SchemaIssue.OneOf(s.ast, s.input, s.successes)) + } + s.out = exit.value + s.successes.push(candidate) + if (s.ast.mode === "anyOf") { + return Exit.void + } + } + } +}) + +const nonFiniteLiterals = new Union([ + new Literal("Infinity"), + new Literal("-Infinity"), + new Literal("NaN") +], "anyOf") + +const numberToJson = new Link( + new Union([number, nonFiniteLiterals], "anyOf"), + new SchemaTransformation.Transformation( + SchemaGetter.Number(), + SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n)) + ) +) + +function formatIsMutable(isMutable: boolean | undefined): string { + return isMutable ? "" : "readonly " +} + +function formatIsOptional(isOptional: boolean | undefined): string { + return isOptional ? "?" : "" +} + +/** @internal */ +export function memoizeThunk(f: () => A): () => A { + let done = false + let a: A + return () => { + if (done) { + return a + } + a = f() + done = true + return a + } +} + +/** + * AST node for lazy/recursive schemas. + * + * **Details** + * + * Wraps a thunk (`() => AST`) that is memoized on first call. Use this to + * define recursive or mutually recursive schemas without infinite loops at + * construction time. + * + * **Example** (Defining recursive schema ASTs) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * interface Category { + * readonly name: string + * readonly children: ReadonlyArray + * } + * + * const Category = Schema.Struct({ + * name: Schema.String, + * children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + * }) + * + * // The recursive branch is a Suspend node + * ``` + * + * @see {@link isSuspend} + * @category models + * @since 3.10.0 + */ +export class Suspend extends Base { + readonly _tag = "Suspend" + readonly thunk: () => AST + + constructor( + thunk: () => AST, + annotations?: Schema.Annotations.Annotations, + checks?: Checks, + encoding?: Encoding, + context?: Context + ) { + if (checks !== undefined) { + throw new Error("Cannot add checks to Suspend") + } + super(annotations, undefined, encoding, context) + this.thunk = memoizeThunk(thunk) + } + /** @internal */ + getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + return recur(this.thunk()) + } + /** @internal */ + recur(recur: (ast: AST) => AST) { + return new Suspend( + () => recur(this.thunk()), + this.annotations, + undefined, + undefined, + this.context + ) + } + /** @internal */ + getExpected(getExpected: (ast: AST) => string): string { + return getExpected(this.thunk()) + } +} + +// ----------------------------------------------------------------------------- +// Checks +// ----------------------------------------------------------------------------- + +/** + * Represents a single validation check attached to an AST node. + * + * **Details** + * + * - `run` — the validation function. Returns `undefined` on success, or an + * `Issue` on failure. + * - `annotations` — optional filter-level metadata (expected message, meta + * tags, arbitrary constraint hints). + * - `aborted` — when `true`, parsing stops immediately after this filter + * fails (no further checks run). + * + * Use `.annotate()` to add metadata and `.abort()` to mark as aborting. + * Combine with another check via `.and()` to form a {@link FilterGroup}. + * + * @see {@link FilterGroup} + * @see {@link Check} + * @see {@link isPattern} + * @category models + * @since 4.0.0 + */ +export class Filter extends Pipeable.Class { + readonly _tag = "Filter" + readonly run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined + readonly annotations: Schema.Annotations.Filter | undefined + /** + * Whether the parsing process should be aborted after this check has failed. + */ + readonly aborted: boolean + + constructor( + run: (input: E, self: AST, options: ParseOptions) => SchemaIssue.Issue | undefined, + annotations: Schema.Annotations.Filter | undefined = undefined, + /** + * Whether the parsing process should be aborted after this check has failed. + */ + aborted: boolean = false + ) { + super() + this.run = run + this.annotations = annotations + this.aborted = aborted + } + annotate(annotations: Schema.Annotations.Filter): Filter { + return new Filter(this.run, { ...this.annotations, ...annotations }, this.aborted) + } + abort(): Filter { + return new Filter(this.run, this.annotations, true) + } + and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup + and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup { + return new FilterGroup([this, other], annotations) + } +} + +/** + * Represents a composite validation check grouping multiple {@link Check} values. + * + * **Details** + * + * Created by calling `.and()` on a {@link Filter} or another `FilterGroup`. + * All inner checks are run; failures from aborted filters still stop + * evaluation. + * + * @see {@link Filter} + * @see {@link Check} + * @category models + * @since 4.0.0 + */ +export class FilterGroup extends Pipeable.Class { + readonly _tag = "FilterGroup" + readonly checks: readonly [Check, ...Array>] + readonly annotations: Schema.Annotations.Filter | undefined + + constructor( + checks: readonly [Check, ...Array>], + annotations: Schema.Annotations.Filter | undefined = undefined + ) { + super() + this.checks = checks + this.annotations = annotations + } + annotate(annotations: Schema.Annotations.Filter): FilterGroup { + return new FilterGroup(this.checks, { ...this.annotations, ...annotations }) + } + and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup + and(other: Check, annotations?: Schema.Annotations.Filter): FilterGroup { + return new FilterGroup([this, other], annotations) + } +} + +/** + * A validation check — either a single {@link Filter} or a composite + * {@link FilterGroup}. + * + * **Details** + * + * Stored in the {@link Checks} array on {@link Base.checks}. + * + * @see {@link Filter} + * @see {@link FilterGroup} + * @category models + * @since 4.0.0 + */ +export type Check = Filter | FilterGroup + +/** @internal */ +export function makeFilter( + filter: (input: T, ast: AST, options: ParseOptions) => Schema.FilterOutput, + annotations?: Schema.Annotations.Filter | undefined, + aborted: boolean = false +): Filter { + return new Filter( + (input, ast, options) => SchemaIssue.make(input, ast, filter(input, ast, options)), + annotations, + aborted + ) +} + +/** @internal */ +export function makeFilterByGuard( + is: (value: E) => value is T, + annotations?: Schema.Annotations.Filter +): Filter { + return new Filter( + (input: E) => is(input) ? undefined : new SchemaIssue.InvalidValue(Option.some(input)), + annotations, + true // after a guard, we always want to abort + ) +} + +/** + * Creates a {@link Filter} that validates strings by running `RegExp.test`. + * + * **When to use** + * + * Use when string validation should be represented as a schema `Filter` backed + * by a regular expression. + * + * **Details** + * + * The filter can be used with `Schema.filter` or attached directly to a + * `String` AST node through checks. The regular expression source is stored in + * annotations for serialization and arbitrary generation. + * + * **Gotchas** + * + * Use a non-global, non-sticky regular expression, or reset `lastIndex` + * yourself, because `RegExp.test` is stateful for expressions with the `g` or + * `y` flag. + * + * **Example** (Validating an email pattern) + * + * ```ts + * import { SchemaAST } from "effect" + * + * const emailFilter = SchemaAST.isPattern(/^[^@]+@[^@]+$/) + * ``` + * + * @see {@link Filter} + * @category constructors + * @since 4.0.0 + */ +export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annotations.Filter) { + const source = regExp.source + return makeFilter( + (s: string) => regExp.test(s), + { + expected: `a string matching the RegExp ${source}`, + meta: { + _tag: "isPattern", + regExp + }, + arbitrary: { + constraint: { + patterns: [regExp.source] + } + }, + ...annotations + } + ) +} + +function modifyOwnPropertyDescriptors( + ast: A, + f: ( + d: { [P in keyof A]: TypedPropertyDescriptor } + ) => void +): A { + const d = Object.getOwnPropertyDescriptors(ast) + f(d) + return Object.create(Object.getPrototypeOf(ast), d) +} + +/** @internal */ +export function replaceEncoding(ast: A, encoding: Encoding | undefined): A { + if (ast.encoding === encoding) { + return ast + } + return modifyOwnPropertyDescriptors(ast, (d) => { + d.encoding.value = encoding + }) +} + +/** @internal */ +export function replaceContext(ast: A, context: Context | undefined): A { + if (ast.context === context) { + return ast + } + return modifyOwnPropertyDescriptors(ast, (d) => { + d.context.value = context + }) +} + +/** @internal */ +export function getLastEncoding(ast: AST): AST { + return ast.encoding ? getLastEncoding(ast.encoding[ast.encoding.length - 1].to) : ast +} + +/** @internal */ +export function annotate(ast: A, annotations: Schema.Annotations.Annotations): A { + if (ast.checks) { + const last = ast.checks[ast.checks.length - 1] + return replaceChecks(ast, Arr.append(ast.checks.slice(0, -1), last.annotate(annotations))) + } + return modifyOwnPropertyDescriptors(ast, (d) => { + d.annotations.value = { ...d.annotations.value, ...annotations } + }) +} + +/** @internal */ +export function replaceChecks(ast: A, checks: Checks | undefined): A { + if (ast._tag === "Suspend" && checks !== undefined) { + throw new Error("Cannot add checks to Suspend") + } + if (ast.checks === checks) { + return ast + } + return modifyOwnPropertyDescriptors(ast, (d) => { + d.checks.value = checks + }) +} + +/** @internal */ +export function appendChecks(ast: A, checks: Checks | undefined): A { + return replaceChecks(ast, combineChecks(ast.checks, checks)) +} + +function updateLastLink(encoding: Encoding, f: (ast: AST) => AST): Encoding { + const links = encoding + const last = links[links.length - 1] + const to = f(last.to) + if (to !== last.to) { + return Arr.append(encoding.slice(0, encoding.length - 1), new Link(to, last.transformation)) + } + return encoding +} + +/** @internal */ +export function applyToLastLink(f: (ast: AST) => AST) { + return (ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : ast +} + +/** @internal */ +export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) { + function out(ast: AST): AST { + return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) + } + return memoize(out) +} + +/** @internal */ +export function middlewareDecoding( + ast: AST, + middleware: SchemaTransformation.Middleware +): AST { + return appendTransformation(ast, middleware, toType(ast)) +} + +/** @internal */ +export function middlewareEncoding( + ast: AST, + middleware: SchemaTransformation.Middleware +): AST { + return appendTransformation(toEncoded(ast), middleware, ast) +} + +function appendTransformation( + from: AST, + transformation: + | SchemaTransformation.Transformation + | SchemaTransformation.Middleware, + to: A +): A { + const link = new Link(from, transformation) + return replaceEncoding(to, to.encoding ? [...to.encoding, link] : [link]) +} + +/** @internal */ +export function brand(ast: AST, brand: string): AST { + const existing = InternalAnnotations.resolveBrands(ast) + const brands = existing ? [...existing, brand] : [brand] + return annotate(ast, { brands }) +} + +/** + * Maps over the array but will return the original array if no changes occur. + * @internal + */ +export function mapOrSame(as: Arr.NonEmptyReadonlyArray, f: (a: A) => A): Arr.NonEmptyReadonlyArray +export function mapOrSame(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray +export function mapOrSame(as: ReadonlyArray, f: (a: A) => A): ReadonlyArray { + let changed = false + const out: Array = new Array(as.length) + for (let i = 0; i < as.length; i++) { + const a = as[i] + const fa = f(a) + if (fa !== a) { + changed = true + } + out[i] = fa + } + return changed ? out : as +} + +/** @internal */ +export function annotateKey(ast: A, annotations: Schema.Annotations.Key): A { + const context = ast.context ? + new Context( + ast.context.isOptional, + ast.context.isMutable, + ast.context.defaultValue, + { ...ast.context.annotations, ...annotations } + ) : + new Context(false, false, undefined, annotations) + return replaceContext(ast, context) +} + +/** @internal */ +export const optionalKeyLastLink = applyToLastLink(optionalKey) + +/** + * Marks an AST node's property key as optional by setting + * {@link Context.isOptional} to `true`. + * + * **Details** + * + * Also propagates the optional flag through the last link of the encoding + * chain if present. + * + * @see {@link isOptional} + * @see {@link Context} + * @category transforming + * @since 4.0.0 + */ +export function optionalKey(ast: A): A { + const context = ast.context ? + ast.context.isOptional === false ? + new Context(true, ast.context.isMutable, ast.context.defaultValue, ast.context.annotations) : + ast.context : + new Context(true, false) + return optionalKeyLastLink(replaceContext(ast, context)) +} + +const mutableKeyLastLink = applyToLastLink(mutableKey) + +/** @internal */ +export function mutableKey(ast: A): A { + const context = ast.context ? + ast.context.isMutable === false ? + new Context(ast.context.isOptional, true, ast.context.defaultValue, ast.context.annotations) : + ast.context : + new Context(false, true) + return mutableKeyLastLink(replaceContext(ast, context)) +} + +/** @internal */ +export function withConstructorDefault( + ast: A, + defaultValue: Effect.Effect +): A { + const transformation = new SchemaTransformation.Transformation( + SchemaGetter.withDefault(defaultValue), + SchemaGetter.passthrough() + ) + const encoding: Encoding = [new Link(unknown, transformation)] + const context = ast.context ? + new Context(ast.context.isOptional, ast.context.isMutable, encoding, ast.context.annotations) : + new Context(false, false, encoding) + return replaceContext(ast, context) +} + +/** + * Attaches a `Transformation` to the `to` AST, making it decode from the + * `from` AST and encode back to it. + * + * **Details** + * + * This is the low-level primitive behind `Schema.transform` and + * `Schema.transformOrFail`. It appends a {@link Link} to the `to` node's + * encoding chain. + * + * - Returns a new AST with the same type as `to`. + * + * @see {@link Link} + * @see {@link Encoding} + * @see {@link flip} + * @category transforming + * @since 4.0.0 + */ +export function decodeTo( + from: AST, + to: A, + transformation: SchemaTransformation.Transformation +): A { + return appendTransformation(from, transformation, to) +} + +function parseParameter(ast: AST): { + literals: ReadonlyArray + parameters: ReadonlyArray +} { + const literals: Array = [] + const parameters: Array = [] + function go(ast: AST) { + switch (ast._tag) { + case "Literal": + if (Predicate.isPropertyKey(ast.literal)) { + literals.push(ast.literal) + } + return + case "UniqueSymbol": + literals.push(ast.symbol) + return + case "Never": + return + case "Union": + for (let i = 0; i < ast.types.length; i++) { + go(ast.types[i]) + } + return + default: + parameters.push(ast) + } + } + go(ast) + return { literals, parameters } +} + +/** @internal */ +export function record(key: AST, value: AST, keyValueCombiner: KeyValueCombiner | undefined): Objects { + const { literals, parameters: indexSignatures } = parseParameter(key) + return new Objects( + literals.map((literal) => new PropertySignature(literal, value)), + indexSignatures.map((parameter) => new IndexSignature(parameter, value, keyValueCombiner)) + ) +} + +// ------------------------------------------------------------------------------------- +// Public APIs +// ------------------------------------------------------------------------------------- + +/** + * Returns `true` if the AST node represents an optional property. + * + * **Details** + * + * Checks `ast.context?.isOptional`. Defaults to `false` when no + * {@link Context} is set. + * + * @see {@link optionalKey} + * @see {@link Context} + * @category predicates + * @since 4.0.0 + */ +export function isOptional(ast: AST): boolean { + return ast.context?.isOptional ?? false +} + +/** @internal */ +export function isMutable(ast: AST): boolean { + return ast.context?.isMutable ?? false +} + +/** + * Strips all encoding transformations from an AST, returning the decoded + * (type-level) representation. + * + * **Details** + * + * - Memoized: same input reference → same output reference. + * - Recursively walks into composite nodes ({@link Arrays}, {@link Objects}, + * {@link Union}, {@link Suspend}). + * + * **Example** (Getting the type AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.NumberFromString + * const typeAst = SchemaAST.toType(schema.ast) + * console.log(typeAst._tag) // "Number" + * ``` + * + * @see {@link toEncoded} + * @see {@link flip} + * @category transforming + * @since 4.0.0 + */ +export const toType = memoize((ast: A): A => { + if (ast.encoding) { + return toType(replaceEncoding(ast, undefined)) + } + const out: any = ast + const type = out.recur?.(toType) ?? out + const encodingChecks = type.encodingChecks + if (encodingChecks) { + return modifyOwnPropertyDescriptors(type, (d) => { + d.encodingChecks.value = undefined + if (type === ast) { + d.checks.value = combineChecks(type.checks, encodingChecks) + } + }) + } + return type +}) + +/** + * Returns the encoded (wire-format) AST by flipping and then stripping + * encodings. + * + * **Details** + * + * Equivalent to `toType(flip(ast))`. This gives you the AST that describes + * the shape of the serialized/encoded data. + * + * - Memoized: same input reference → same output reference. + * + * **Example** (Getting the encoded AST) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.NumberFromString + * const encodedAst = SchemaAST.toEncoded(schema.ast) + * console.log(encodedAst._tag) // "String" + * ``` + * + * @see {@link toType} + * @see {@link flip} + * @category transforming + * @since 4.0.0 + */ +export const toEncoded = memoize((ast: AST): AST => { + return toType(flip(ast)) +}) + +function flipEncoding(ast: AST, encoding: Encoding): AST { + const links = encoding + const len = links.length + const last = links[len - 1] + const ls: Arr.NonEmptyArray = [ + new Link(flip(replaceEncoding(ast, undefined)), links[0].transformation.flip()) + ] + for (let i = 1; i < len; i++) { + ls.unshift(new Link(flip(links[i - 1].to), links[i].transformation.flip())) + } + const to = flip(last.to) + if (to.encoding) { + return replaceEncoding(to, [...to.encoding, ...ls]) + } else { + return replaceEncoding(to, ls) + } +} + +/** + * Swaps the decode and encode directions of an AST's {@link Encoding} chain. + * + * **Details** + * + * After flipping, what was decoding becomes encoding and vice versa. This is + * the core operation behind `Schema.encode` — encoding a value is decoding + * with a flipped SchemaAST. + * + * - Memoized: same input reference → same output reference. + * - Recursively walks composite nodes. + * + * @see {@link toType} + * @see {@link toEncoded} + * @category transforming + * @since 4.0.0 + */ +export const flip = memoize((ast: AST): AST => { + if (ast.encoding) { + return flipEncoding(ast, ast.encoding) + } + const out: any = ast + return out.flip?.(flip) ?? out.recur?.(flip) ?? out +}) + +/** @internal */ +export function containsUndefined(ast: AST): boolean { + switch (ast._tag) { + case "Undefined": + return true + case "Union": + return ast.types.some(containsUndefined) + default: + return false + } +} + +function fromConst( + ast: AST, + value: T +): SchemaParser.Parser { + const succeed = Effect.succeedSome(value) + return (oinput) => { + if (oinput._tag === "None") { + return Effect.succeedNone + } + return oinput.value === value + ? succeed + : Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + } +} + +function fromAnyToConst(value: T): SchemaParser.Parser { + const succeed = Effect.succeedSome(value) + return (oinput) => oinput._tag === "None" ? Effect.succeedNone : succeed +} + +function fromRefinement( + ast: AST, + refinement: (input: unknown) => input is T +): SchemaParser.Parser { + return (oinput) => { + if (oinput._tag === "None") { + return Effect.succeedNone + } + return refinement(oinput.value) + ? Effect.succeed(oinput) + : Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + } +} + +function applyTemplateLiteralPartChecks(ast: AST, value: A, options: ParseOptions): A | undefined { + if (options?.disableChecks || ast.checks === undefined) return value + const issues: Array = [] + collectIssues(ast.checks, value, issues, ast, options) + return issues.length === 0 ? value : undefined +} + +function segmentTemplateLiteralParts( + parts: ReadonlyArray, + input: string, + options: ParseOptions +): Array | undefined { + const out = new Array(parts.length) + const failures = new Set() + function go(i: number, pos: number): boolean { + if (i === parts.length) return pos === input.length + const key = `${i}/${pos}` + if (failures.has(key)) return false + const part = parts[i] + if (i === parts.length - 1) { + const s = input.slice(pos) + if (part.matchPart(s, options) !== undefined) { + out[i] = s + return true + } + } else if (part._tag === "Literal") { + const s = globalThis.String(part.literal) + if (input.startsWith(s, pos) && go(i + 1, pos + s.length)) { + out[i] = s + return true + } + } else { + for (let end = input.length; end >= pos; end--) { + const s = input.slice(pos, end) + if (part.matchPart(s, options) !== undefined && go(i + 1, end)) { + out[i] = s + return true + } + } + } + failures.add(key) + return false + } + return go(0, 0) ? out : undefined +} + +/** @internal */ +export const enumsToLiterals = memoize((ast: Enum): Union => { + return new Union( + ast.enums.map((e) => new Literal(e[1], { title: e[0] })), + "anyOf" + ) +}) + +const parameterFromPropertyKey = applyToSelfOrLastLinkEncoding((ast) => { + switch (ast._tag) { + default: + return ast + case "Number": + return ast.toCodecStringTree() + case "Union": + return ast.recur(parameterFromPropertyKey) + } +}) + +/** @internal */ +export const parameterFromString = applyToSelfOrLastLinkEncoding((ast) => { + switch (ast._tag) { + default: + return ast + case "Symbol": + case "UniqueSymbol": + return ast.toCodecStringTree() + case "Union": + return ast.recur(parameterFromString) + } +}) + +const partFromString = applyToSelfOrLastLinkEncoding((ast) => { + switch (ast._tag) { + default: + return ast + case "Number": + case "Literal": + case "BigInt": + return ast.toCodecStringTree() + case "Union": + return ast.recur(partFromString) + } +}) + +/** + * any string, including newlines + * @internal + */ +export const STRING_PATTERN = "[\\s\\S]*?" + +const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`) + +const isStringNumberRegExp = new globalThis.RegExp(`(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)`) + +/** @internal */ +export function isStringFinite(annotations?: Schema.Annotations.Filter) { + return isPattern( + isStringFiniteRegExp, + { + expected: "a string representing a finite number", + meta: { + _tag: "isStringFinite", + regExp: isStringFiniteRegExp + }, + ...annotations + } + ) +} + +const finiteString = appendChecks(string, [isStringFinite()]) + +const finiteToString = new Link( + finiteString, + SchemaTransformation.numberFromString +) + +const numberToString = new Link( + new Union([finiteString, nonFiniteLiterals], "anyOf"), + SchemaTransformation.numberFromString +) + +/** + * signed integer only (no leading "+" because TypeScript doesn't support it) + */ +const BIGINT_PATTERN = "-?\\d+" + +const isStringBigIntRegExp = new globalThis.RegExp(`^${BIGINT_PATTERN}$`) + +/** @internal */ +export function isStringBigInt(annotations?: Schema.Annotations.Filter) { + return isPattern( + isStringBigIntRegExp, + { + expected: "a string representing a bigint", + meta: { + _tag: "isStringBigInt", + regExp: isStringBigIntRegExp + }, + ...annotations + } + ) +} + +/** @internal */ +export const bigIntString = appendChecks(string, [isStringBigInt({ + expected: "a string representing a bigint" +})]) + +const bigIntToString = new Link( + bigIntString, + SchemaTransformation.bigintFromString +) + +const REGEXP_PATTERN = "Symbol\\((.*)\\)" + +const isStringSymbolRegExp = new globalThis.RegExp(`^${REGEXP_PATTERN}$`) + +/** @internal */ +export const symbolString = appendChecks(string, [isStringSymbol()]) + +/** + * to distinguish between Symbol and String, we need to add a check to the string keyword + */ +const symbolToString = new Link( + symbolString, + new SchemaTransformation.Transformation( + SchemaGetter.transform((description) => globalThis.Symbol.for(isStringSymbolRegExp.exec(description)![1])), + SchemaGetter.transformOrFail((sym: symbol) => { + const key = globalThis.Symbol.keyFor(sym) + if (key !== undefined) { + return Effect.succeed(globalThis.String(sym)) + } + return Effect.fail( + new SchemaIssue.Forbidden(Option.some(sym), { message: "cannot serialize to string, Symbol is not registered" }) + ) + }) + ) +) + +/** @internal */ +export function isStringSymbol(annotations?: Schema.Annotations.Filter) { + return isPattern( + isStringSymbolRegExp, + { + expected: "a string representing a symbol", + meta: { + _tag: "isStringSymbol", + regExp: isStringSymbolRegExp + }, + ...annotations + } + ) +} + +/** @internal */ +export function collectIssues( + checks: ReadonlyArray>, + value: T, + issues: Array, + ast: AST, + options: ParseOptions +) { + for (let i = 0; i < checks.length; i++) { + const check = checks[i] + if (check._tag === "FilterGroup") { + collectIssues(check.checks, value, issues, ast, options) + } else { + const issue = check.run(value, ast, options) + if (issue) { + issues.push(new SchemaIssue.Filter(value, check, issue)) + if (check.aborted || options?.errors !== "all") { + return + } + } + } + } +} + +/** @internal */ +export function runChecks( + checks: readonly [Check, ...Array>], + s: T +): Result.Result { + const issues: Array = [] + collectIssues(checks, s, issues, unknown, { errors: "all" }) + if (Arr.isArrayNonEmpty(issues)) { + const issue = new SchemaIssue.Composite(unknown, Option.some(s), issues) + return Result.fail(issue) + } + return Result.succeed(s) +} + +/** @internal */ +export const ClassTypeId = "~effect/Schema/Class" + +/** @internal */ +export const STRUCTURAL_ANNOTATION_KEY = "~structural" + +/** + * Returns all annotations from the AST node. + * + * **Details** + * + * If the node has {@link Checks}, returns annotations from the last check + * (which is where user-supplied annotations end up after `.pipe(Schema.annotations(...))`). + * Otherwise returns `Base.annotations` directly. + * + * **Example** (Reading annotations) + * + * ```ts + * import { Schema, SchemaAST } from "effect" + * + * const schema = Schema.String.annotate({ title: "Name" }) + * const annotations = SchemaAST.resolve(schema.ast) + * console.log(annotations?.title) // "Name" + * ``` + * + * @see {@link resolveAt} + * @see {@link resolveIdentifier} + * @see {@link resolveTitle} + * @see {@link resolveDescription} + * @category annotations + * @since 4.0.0 + */ +export const resolve: (ast: AST) => Schema.Annotations.Annotations | undefined = InternalAnnotations.resolve + +/** + * Returns a single annotation value by key from the AST node. + * + * **Details** + * + * Like {@link resolve}, reads from the last check's annotations when checks + * are present. Returns `undefined` if the key is not found. + * + * @see {@link resolve} + * @category annotations + * @since 4.0.0 + */ +export const resolveAt: (key: string) => (ast: AST) => A | undefined = InternalAnnotations.resolveAt + +/** + * Returns the `identifier` annotation from the AST node, if set. + * + * **Details** + * + * The identifier is typically set by `Schema.annotations({ identifier: "..." })` + * and is used for error messages and schema identification. + * + * @see {@link resolve} + * @see {@link resolveTitle} + * @category annotations + * @since 4.0.0 + */ +export const resolveIdentifier: (ast: AST) => string | undefined = InternalAnnotations.resolveIdentifier + +/** + * Returns the `title` annotation from the AST node, if set. + * + * @see {@link resolve} + * @see {@link resolveIdentifier} + * @see {@link resolveDescription} + * + * @category annotations + * @since 4.0.0 + */ +export const resolveTitle: (ast: AST) => string | undefined = InternalAnnotations.resolveTitle + +/** + * Returns the `description` annotation from the AST node, if set. + * + * @see {@link resolve} + * @see {@link resolveTitle} + * @see {@link resolveIdentifier} + * + * @category annotations + * @since 4.0.0 + */ +export const resolveDescription: (ast: AST) => string | undefined = InternalAnnotations.resolveDescription + +/** + * Returns true if the value is a JSON value. + * + * When a cyclic reference is detected, returns false. + * + * @internal + */ +export function isJson(u: unknown): u is Schema.Json { + // `onPath` is the current recursion stack: nodes between the root and the + // one being visited. A hit here means we looped back to an ancestor — a + // real cycle, not a DAG — so the value is not JSON. + const onPath = new Set() + // `validated` memoizes subtrees we've already fully checked. Without it, a + // diamond-shaped DAG (same node reached through multiple parents) would be + // re-traversed once per parent, which is exponential in the nesting depth. + const validated = new Set() + return recur(u) + + function recur(u: unknown): boolean { + if (u === null || typeof u === "string" || typeof u === "boolean") { + return true + } + if (typeof u === "number") { + return globalThis.Number.isFinite(u) + } + if (typeof u !== "object" || u === undefined) { + return false + } + if (onPath.has(u)) { + return false + } + if (validated.has(u)) { + return true + } + const isArray = Array.isArray(u) + if (!isArray) { + const prototype = Object.getPrototypeOf(u) + if (prototype !== null && Object.getPrototypeOf(prototype) !== null) { + return false + } + } + onPath.add(u) + const ok = isArray + ? u.every(recur) + : Object.keys(u).every((key) => recur((u as Record)[key])) + // Pop on exit so siblings reaching the same node via a different path + // don't see it as an ancestor (that would reject valid DAGs). + onPath.delete(u) + if (ok) { + validated.add(u) + } + return ok + } +} + +/** @internal */ +export const Json = new Declaration( + [], + () => (input, ast) => + isJson(input) ? + Effect.succeed(input) : + Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), + { + typeConstructor: { + _tag: "effect/Json" + }, + generation: { + runtime: `Schema.Json`, + Type: `Schema.Json` + }, + expected: "JSON value", + toCodecJson: () => new Link(unknown, SchemaTransformation.passthrough()), + toArbitrary: () => (fc: typeof FastCheck) => fc.jsonValue() + } +) + +/** @internal */ +export const MutableJson = annotate(Json, { + typeConstructor: { + _tag: "effect/MutableJson" + }, + generation: { + runtime: `Schema.MutableJson`, + Type: `Schema.MutableJson` + } +}) + +/** @internal */ +export const unknownToNull = new Link( + null_, + new SchemaTransformation.Transformation( + SchemaGetter.passthrough(), + SchemaGetter.transform(() => null) + ) +) + +/** @internal */ +export const unknownToJson = new Link( + Json, + SchemaTransformation.passthrough() +) + +/** + * Returns true if the value is a StringTree value. + * + * When a cyclic reference is detected, returns false. + * + * @internal + */ +export function isStringTree(u: unknown): u is Schema.StringTree { + const seen = new Set() + return recur(u) + + function recur(u: unknown): boolean { + if (u === undefined || typeof u === "string") { + return true + } + if (typeof u !== "object" || u === null) { + return false + } + if (seen.has(u)) { + return false + } + seen.add(u) + if (Array.isArray(u)) { + return u.every(recur) + } + return Object.keys(u).every((key) => recur((u as Record)[key])) + } +} + +const StringTree = new Declaration( + [], + () => (input, ast) => + isStringTree(input) ? + Effect.succeed(input) : + Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), + { expected: "StringTree" } +) + +/** @internal */ +export const unknownToStringTree = new Link( + StringTree, + SchemaTransformation.passthrough() +) diff --git a/repos/effect/packages/effect/src/SchemaError.ts b/repos/effect/packages/effect/src/SchemaError.ts new file mode 100644 index 0000000..168a853 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaError.ts @@ -0,0 +1,64 @@ +/** + * @since 4.0.0 + */ +import * as Data from "./Data.ts" +import * as Predicate from "./Predicate.ts" +import type { Issue } from "./SchemaIssue.ts" + +const TypeId = "~effect/SchemaError/SchemaError" + +/** + * Error thrown (or returned as the error channel value) when schema decoding + * or encoding fails. + * + * **Details** + * + * The `issue` field contains a structured {@link Issue} tree describing + * every validation failure, including the path to the problematic value, + * expected types, and actual values received. `message` renders the issue tree + * as a human-readable string. + * + * Use {@link isSchemaError} to narrow an unknown value to `SchemaError`. + * + * **Example** (Catching a SchemaError) + * + * ```ts + * import { Schema } from "effect" + * + * try { + * Schema.decodeUnknownSync(Schema.Number)("not a number") + * } catch (err) { + * if (Schema.isSchemaError(err)) { + * console.log(err.message) + * // Expected number, actual "not a number" + * } + * } + * ``` + * + * @category errors + * @since 4.0.0 + */ +export class SchemaError extends Data.TaggedError("SchemaError")<{ + readonly issue: Issue +}> { + readonly [TypeId]: typeof TypeId = TypeId + constructor(issue: Issue) { + super({ issue }) + } + override get message() { + return this.issue.toString() + } + override toString() { + return `SchemaError(${this.message})` + } +} + +/** + * Returns `true` if `u` is a {@link SchemaError}. + * + * @category guards + * @since 4.0.0 + */ +export function isSchemaError(u: unknown): u is SchemaError { + return Predicate.hasProperty(u, TypeId) +} diff --git a/repos/effect/packages/effect/src/SchemaGetter.ts b/repos/effect/packages/effect/src/SchemaGetter.ts new file mode 100644 index 0000000..cae60ca --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaGetter.ts @@ -0,0 +1,1876 @@ +/** + * Builds one-way conversions used by schemas. + * + * A `Getter` receives an optional encoded value and returns an + * optional decoded value. It can also report a schema issue or require Effect + * services. Schema transformations use getters to describe one direction of a + * conversion, for example decoding a field from input data. This module + * includes basic getters, validation helpers, pure and effectful conversions, + * and ready-made conversions for common string, number, binary, date, form, and + * URL-related values. + * + * @since 4.0.0 + */ +import * as DateTime from "./DateTime.ts" +import * as Effect from "./Effect.ts" +import * as Encoding from "./Encoding.ts" +import * as internalRecord from "./internal/record.ts" +import * as Option from "./Option.ts" +import * as Pipeable from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import type * as SchemaAST from "./SchemaAST.ts" +import * as SchemaIssue from "./SchemaIssue.ts" +import * as Str from "./String.ts" + +/** + * Represents a composable transformation from an encoded type `E` to a decoded type `T`. + * + * **When to use** + * + * Use when you need a schema getter to build and compose custom transformations + * for `Schema.decodeTo` or `Schema.decode`. + * + * **Details** + * + * A getter wraps a function `Option -> Effect, Issue, R>`. It + * receives `Option.None` when the encoded key is absent, such as a missing + * struct field, and returns `Option.None` to omit the value from the decoded + * output. It fails with `Issue` on invalid input and may require Effect + * services via `R`. `.map(f)` applies `f` to the decoded value inside `Some` + * while leaving `None` unchanged. `.compose(other)` chains two getters by + * feeding the output of `this` into `other`; passthrough getters on either side + * are optimized away. + * + * **Example** (Creating and composing getters) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const parseNumber = SchemaGetter.transform((s) => Number(s)) + * const double = SchemaGetter.transform((n) => n * 2) + * const composed = parseNumber.compose(double) + * // composed: Getter — parses then doubles + * ``` + * + * @see {@link transform} to create a getter from a pure function + * @see {@link passthrough} for the identity getter + * @see {@link transformOrFail} for fallible transformation + * + * @category models + * @since 4.0.0 + */ +export class Getter extends Pipeable.Class { + readonly run: ( + input: Option.Option, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, R> + + constructor( + run: ( + input: Option.Option, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, R> + ) { + super() + this.run = run + } + map(f: (t: T) => T2): Getter { + return new Getter((oe, options) => this.run(oe, options).pipe(Effect.mapEager(Option.map(f)))) + } + compose(other: Getter): Getter { + if (isPassthrough(this)) { + return other as any + } + if (isPassthrough(other)) { + return this as any + } + return new Getter((oe, options) => this.run(oe, options).pipe(Effect.flatMapEager((ot) => other.run(ot, options)))) + } +} + +/** + * Creates a getter that always produces the given constant value, ignoring the input. + * + * **When to use** + * + * Use when you need a schema getter that always decodes a field to a fixed + * value. + * + * **Details** + * + * The getter is pure and always returns `Option.some(t)` regardless of whether + * the input is `Some` or `None`. + * + * **Example** (Returning a constant getter) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const alwaysZero = SchemaGetter.succeed(0) + * // alwaysZero: Getter<0, unknown> — always produces 0 + * ``` + * + * @see {@link transform} when you need to use the input value + * @see {@link passthrough} when you want to keep the input as-is + * + * @category constructors + * @since 4.0.0 + */ +export function succeed(t: T): Getter { + return new Getter(() => Effect.succeedSome(t)) +} + +/** + * Creates a getter that always fails with the given issue. + * + * **When to use** + * + * Use when you need a schema getter that unconditionally rejects input. + * - Building custom validation getters that produce specific error types. + * + * **Details** + * + * - Always fails with the `Issue` returned by `f`. + * - The failure function receives the original `Option` input for error context. + * + * **Example** (Defining an always-failing getter) + * + * ```ts + * import { Option, SchemaGetter, SchemaIssue } from "effect" + * + * const rejectAll = SchemaGetter.fail( + * (oe) => new SchemaIssue.InvalidValue(oe, { message: "not allowed" }) + * ) + * ``` + * + * @see {@link forbidden} for a convenience helper for `Forbidden` issues + * @see {@link checkEffect} to fail conditionally based on input value + * + * @category constructors + * @since 4.0.0 + */ +export function fail(f: (oe: Option.Option) => SchemaIssue.Issue): Getter { + return new Getter((oe) => Effect.fail(f(oe))) +} + +/** + * Creates a getter that always fails with a `Forbidden` issue. + * + * **When to use** + * + * Use when you need a schema getter to disallow a field or direction + * (encode/decode) entirely. + * - You want a clear "forbidden" error message in schema validation output. + * + * **Details** + * + * - Always fails with `SchemaIssue.Forbidden`. + * - The message function receives the `Option` input for context. + * + * **Example** (Forbidding a decode direction) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const noEncode = SchemaGetter.forbidden( + * () => "encoding is not supported" + * ) + * ``` + * + * @see {@link fail} to fail with a custom issue type + * + * @category constructors + * @since 4.0.0 + */ +export function forbidden(message: (oe: Option.Option) => string): Getter { + return fail((oe) => new SchemaIssue.Forbidden(oe, { message: message(oe) })) +} + +const passthrough_ = new Getter(Effect.succeed) + +function isPassthrough(getter: Getter): getter is typeof passthrough_ { + return getter.run === passthrough_.run +} + +/** + * Returns the identity getter — passes the value through unchanged. + * + * **When to use** + * + * Use when you need a schema getter for one side of a `decodeTo` pair, either + * encode or decode, to pass values through unchanged. + * + * **Details** + * + * - Pure, no allocation (singleton instance). + * - Optimized away during `.compose()` — composing with a passthrough is free. + * - The default overload requires `T === E`. Pass `{ strict: false }` to opt + * out of the type constraint. + * + * **Example** (Passing through identity transformations) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * // No transformation needed — types already match + * const StringToString = Schema.String.pipe( + * Schema.decodeTo(Schema.String, { + * decode: SchemaGetter.passthrough(), + * encode: SchemaGetter.passthrough() + * }) + * ) + * ``` + * + * @see {@link passthroughSupertype} when `T extends E` + * @see {@link passthroughSubtype} when `E extends T` + * @see {@link transform} when you need to change the value + * + * @category constructors + * @since 4.0.0 + */ +export function passthrough(options: { readonly strict: false }): Getter +export function passthrough(): Getter +export function passthrough(): Getter { + return passthrough_ +} + +/** + * Returns the identity getter typed for the relationship `T extends E`. + * + * **When to use** + * + * Use when you need a schema getter that passes values through when the + * decoded/output type is narrower than the encoded/input type. + * + * **Details** + * + * - Same singleton as {@link passthrough} — no allocation, optimized in composition. + * + * **Example** (Passing through supertypes) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * // string extends string, so this is valid + * const g = SchemaGetter.passthroughSupertype() + * ``` + * + * @see {@link passthrough} when types are identical + * @see {@link passthroughSubtype} when `E extends T` + * + * @category constructors + * @since 4.0.0 + */ +export function passthroughSupertype(): Getter +export function passthroughSupertype(): Getter { + return passthrough_ +} + +/** + * Returns the identity getter, typed for when the encoded type `E` is a subtype of `T`. + * + * **When to use** + * + * Use when you need a schema getter that passes values through without + * `{ strict: false }` for an encoded type that narrows the decoded type. + * + * **Details** + * + * - Same singleton as {@link passthrough} — no allocation, optimized in composition. + * + * **Example** (Passing through subtypes) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * // "hello" extends string, so E extends T + * const g = SchemaGetter.passthroughSubtype() + * ``` + * + * @see {@link passthrough} when types are identical + * @see {@link passthroughSupertype} when `T extends E` + * + * @category constructors + * @since 4.0.0 + */ +export function passthroughSubtype(): Getter +export function passthroughSubtype(): Getter { + return passthrough_ +} + +/** + * Creates a getter that handles the case when the input is absent (`Option.None`). + * + * **When to use** + * + * Use when you need a schema getter to provide a fallback or computed value for + * missing struct keys. + * - Building custom "default value" logic more complex than {@link withDefault}. + * + * **Details** + * + * - When input is `None`, calls `f` to produce the result. + * - When input is `Some`, passes it through unchanged. + * - `f` receives the parse options and may return `None` to keep the value absent. + * + * **Example** (Providing a default timestamp for a missing field) + * + * ```ts + * import { Effect, Option, SchemaGetter } from "effect" + * + * const withTimestamp = SchemaGetter.onNone(() => + * Effect.succeed(Option.some(Date.now())) + * ) + * ``` + * + * @see {@link required} when absent input should fail + * @see {@link withDefault} for a simpler default value for undefined inputs + * @see {@link onSome} to handle only present values + * + * @category constructors + * @since 4.0.0 + */ +export function onNone( + f: (options: SchemaAST.ParseOptions) => Effect.Effect, SchemaIssue.Issue, R> +): Getter { + return new Getter((ot, options) => Option.isNone(ot) ? f(options) : Effect.succeed(ot)) +} + +/** + * Creates a getter that fails with `MissingKey` if the input is absent (`Option.None`). + * + * **When to use** + * + * Use when you need a schema getter to require a struct field in the encoded + * input and report a missing key error when it is absent. + * + * **Details** + * + * - When input is `None`, fails with `SchemaIssue.MissingKey`. + * - When input is `Some`, passes it through unchanged. + * - Optional `annotations` customize the error message for the missing key. + * + * **Example** (Defining a required struct field) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const mustExist = SchemaGetter.required() + * ``` + * + * @see {@link onNone} to provide a fallback instead of failing + * @see {@link withDefault} to substitute a default for undefined values + * + * @category constructors + * @since 4.0.0 + */ +export function required(annotations?: Schema.Annotations.Key): Getter { + return onNone(() => Effect.fail(new SchemaIssue.MissingKey(annotations))) +} + +/** + * Creates a getter that handles present values (`Option.Some`), passing `None` through. + * + * **When to use** + * + * Use when you need a schema getter to transform or validate only when a field + * value is present. + * - Missing keys should remain absent in the output. + * + * **Details** + * + * - When input is `None`, returns `None` (no-op). + * - When input is `Some(e)`, calls `f(e, options)` to produce the result. + * - `f` may return `None` to omit the value, or fail with an `Issue`. + * + * **Example** (Transforming only present values) + * + * ```ts + * import { Effect, Option, SchemaGetter } from "effect" + * + * const parseIfPresent = SchemaGetter.onSome( + * (s) => Effect.succeed(Option.some(Number(s))) + * ) + * ``` + * + * @see {@link onNone} to handle only absent values + * @see {@link transform} for a simpler pure transformation of present values + * @see {@link transformOrFail} for fallible transformation of present values + * + * @category constructors + * @since 4.0.0 + */ +export function onSome( + f: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect, SchemaIssue.Issue, R> +): Getter { + return new Getter((oe, options) => Option.isNone(oe) ? Effect.succeedNone : f(oe.value, options)) +} + +/** + * Creates a getter that validates a value using an effectful check function. + * + * **When to use** + * + * Use when you need a schema getter to validate a decoded value (e.g. check a + * constraint or call an external service). + * - The validation may be asynchronous or require Effect services. + * + * **Details** + * + * - Only runs when input is `Some` — `None` passes through. + * - The check function returns a validation result: + * - `undefined` or `true` — value is valid, passes through. + * - `false` or a `string` — value is invalid, fails with an `Issue`. + * - An `Issue` object — fails with that issue directly. + * - `{ path, issue }` — fails with a nested path issue (`issue` may be a + * message string or a full {@link SchemaIssue.Issue}). + * - Does not transform the value — input and output types are the same. + * + * **Example** (Validating effectfully) + * + * ```ts + * import { Effect, SchemaGetter } from "effect" + * + * const nonNegative = SchemaGetter.checkEffect((n) => + * Effect.succeed(n >= 0 ? undefined : "must be non-negative") + * ) + * ``` + * + * @see {@link transform} when you need to change the value, not just validate + * @see {@link fail} for unconditional failure + * + * @category constructors + * @since 4.0.0 + */ +export function checkEffect( + f: (input: T, options: SchemaAST.ParseOptions) => Effect.Effect< + undefined | boolean | Schema.FilterIssue, + never, + R + > +): Getter { + return onSome((t, options) => { + return f(t, options).pipe(Effect.flatMapEager((out) => { + const issue = SchemaIssue.makeSingle(t, out) + return issue ? + Effect.fail(issue) : + Effect.succeed(Option.some(t)) + })) + }) +} + +/** + * Creates a getter that applies a pure function to present values. + * + * **When to use** + * + * Use when you need a schema getter for a pure, infallible transformation + * between types. + * - Building encode/decode pairs for `Schema.decodeTo`. + * + * **Details** + * + * - This is the most commonly used constructor. + * - Transforms `Some(e)` to `Some(f(e))` and leaves `None` unchanged. + * - Skips `None` inputs — only called when a value is present. + * - Never fails. + * + * **Example** (Transforming strings to numbers) + * + * ```ts + * import { Schema, SchemaGetter } from "effect" + * + * const NumberFromString = Schema.String.pipe( + * Schema.decodeTo(Schema.Number, { + * decode: SchemaGetter.transform((s) => Number(s)), + * encode: SchemaGetter.transform((n) => String(n)) + * }) + * ) + * ``` + * + * @see {@link transformOrFail} when the transformation can fail + * @see {@link transformOptional} when you need to handle `None` inputs + * @see {@link passthrough} when no transformation is needed + * + * @category constructors + * @since 4.0.0 + */ +export function transform(f: (e: E) => T): Getter { + return transformOptional(Option.map(f)) +} + +/** + * Creates a getter that applies a fallible, effectful transformation to present values. + * + * **When to use** + * + * Use when you need a schema getter for a transformation that may fail, require + * Effect services, or run asynchronously. + * + * **Details** + * + * - Skips `None` inputs — only called when a value is present. + * - On success, wraps the result in `Some`. + * - On failure, propagates the `Issue`. + * + * **Example** (Parsing with failure) + * + * ```ts + * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect" + * + * const safeParseInt = SchemaGetter.transformOrFail( + * (s) => { + * const n = parseInt(s, 10) + * return isNaN(n) + * ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "not an integer" })) + * : Effect.succeed(n) + * } + * ) + * ``` + * + * @see {@link transform} when transformation cannot fail + * @see {@link onSome} when you need full `Option` control over the output + * + * @category constructors + * @since 4.0.0 + */ +export function transformOrFail( + f: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect +): Getter { + return onSome((e, options) => f(e, options).pipe(Effect.mapEager(Option.some))) +} + +/** + * Creates a getter that transforms the full `Option` — both present and absent values. + * + * **When to use** + * + * Use when you need a schema getter to handle both `Some` and `None` cases. + * + * **Details** + * + * The getter is pure and never fails. It receives the full `Option` and + * must return `Option`, so it can turn a present value into absent or an + * absent value into present. + * + * **Example** (Filtering out empty strings) + * + * ```ts + * import { Option, SchemaGetter } from "effect" + * + * const skipEmpty = SchemaGetter.transformOptional((o) => + * Option.filter(o, (s) => s.length > 0) + * ) + * ``` + * + * @see {@link transform} when you only need to transform present values + * @see {@link omit} when you always want `None` + * + * @category constructors + * @since 4.0.0 + */ +export function transformOptional(f: (oe: Option.Option) => Option.Option): Getter { + return new Getter((oe) => Effect.succeed(f(oe))) +} + +/** + * Creates a getter that always returns `None`, effectively omitting the value from output. + * + * **When to use** + * + * Use when you need a schema getter to exclude a field during decoding or + * encoding. + * + * **Details** + * + * - Always returns `Option.None` regardless of input. + * - Never fails. + * + * **Example** (Omitting a field during encoding) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const omitField = SchemaGetter.omit() + * ``` + * + * @see {@link transformOptional} when you want conditional omission + * @see {@link forbidden} when you want to fail instead of silently omit + * + * @category constructors + * @since 4.0.0 + */ +export function omit(): Getter { + return new Getter(() => Effect.succeedNone) +} + +/** + * Creates a getter that replaces `undefined` values with a default. + * + * **When to use** + * + * Use when you need a schema getter to provide a fallback for a field that may + * be `undefined` in the encoded input. + * + * **Details** + * + * - If the input is `Some(undefined)` or `None`, produces `Some(T)`. + * - If the input is `Some(value)` where value is not `undefined`, passes it through. + * - `defaultValue` is an `Effect` that will be executed each time a default is needed. + * + * **Example** (Providing a default value for an optional field) + * + * ```ts + * import { Effect, SchemaGetter } from "effect" + * + * const withZero = SchemaGetter.withDefault(Effect.succeed(0)) + * // Getter + * ``` + * + * @see {@link onNone} to handle only absent keys (not `undefined` values) + * @see {@link required} when absent input should fail instead of using a default + * + * @category constructors + * @since 4.0.0 + */ +export function withDefault( + defaultValue: Effect.Effect +): Getter { + return new Getter((o) => { + const filtered = Option.filter(o, Predicate.isNotUndefined) + return Option.isSome(filtered) ? Effect.succeed(filtered) : Effect.mapEager(defaultValue, Option.some) + }) +} + +/** + * Coerces any value to a `string` using the global `String()` constructor. + * + * **When to use** + * + * Use when you need a schema getter to coerce a present encoded value to a + * string with `String()`. + * + * **Details** + * + * The getter is pure, never fails, and delegates to `globalThis.String`. + * + * **Example** (Coercing to a string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toString = SchemaGetter.String() + * // Getter + * ``` + * + * @see {@link transform} for custom string conversions + * + * @category Coercions + * @since 4.0.0 + */ +export function String(): Getter { + return transform(globalThis.String) +} + +/** + * Coerces any value to a `number` using the global `Number()` constructor. + * + * **When to use** + * + * Use when you need a schema getter to coerce a present encoded value to a + * number with `Number()`. + * + * **Details** + * + * The getter is pure, never fails, and delegates to `globalThis.Number`. It may + * produce `NaN` for non-numeric inputs. + * + * **Example** (Coercing to a number) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toNumber = SchemaGetter.Number() + * // Getter + * ``` + * + * @see {@link transformOrFail} for validated number parsing + * + * @category Coercions + * @since 4.0.0 + */ +export function Number(): Getter { + return transform(globalThis.Number) +} + +/** + * Coerces any value to a `boolean` using the global `Boolean()` constructor. + * + * **When to use** + * + * Use when you need a schema getter to coerce a present encoded value to a + * boolean with `Boolean()`. + * + * **Details** + * + * The getter is pure, never fails, and delegates to `globalThis.Boolean`. + * + * **Example** (Coercing to a boolean) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toBool = SchemaGetter.Boolean() + * // Getter + * ``` + * + * @category Coercions + * @since 4.0.0 + */ +export function Boolean(): Getter { + return transform(globalThis.Boolean) +} + +/** + * Coerces a value to `bigint` using the global `BigInt()` constructor. + * + * **When to use** + * + * Use when you need a schema getter to convert a present string, number, or + * boolean value to `bigint`. + * + * **Details** + * + * - Delegates to `globalThis.BigInt`. + * - Throws at runtime if the input cannot be converted (e.g. non-numeric string). + * + * **Example** (Coercing to a bigint) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toBigInt = SchemaGetter.BigInt() + * // Getter + * ``` + * + * @category Coercions + * @since 4.0.0 + */ +export function BigInt(): Getter { + return transform(globalThis.BigInt) +} + +/** + * Coerces a value to a `Date` using `new Date(input)`. + * + * **When to use** + * + * Use when you need a schema getter to coerce a present string, number, or + * existing date object into a new date object. + * + * **Details** + * + * - Delegates to `new globalThis.Date(input)`. + * - Does not validate the result — may produce an invalid Date. + * + * **Example** (Coercing to a Date) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toDate = SchemaGetter.Date() + * // Getter + * ``` + * + * @see {@link dateTimeUtcFromInput} for validated DateTime parsing + * + * @category Coercions + * @since 4.0.0 + */ +export function Date(): Getter { + return transform((u) => new globalThis.Date(u)) +} + +/** + * Strips whitespace from both ends of a string. + * + * **Details** + * + * - Pure, delegates to `String.trim`. + * + * **Example** (Trimming whitespace) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const trimmed = SchemaGetter.trim() + * ``` + * + * @category string + * @since 4.0.0 + */ +export function trim(): Getter { + return transform(Str.trim) +} + +/** + * Capitalizes the first character of a string. + * + * **Details** + * + * - Pure, delegates to `String.capitalize`. + * + * **Example** (Capitalizing a string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const cap = SchemaGetter.capitalize() + * ``` + * + * @category string + * @since 4.0.0 + */ +export function capitalize(): Getter { + return transform(Str.capitalize) +} + +/** + * Uncapitalizes the first character of a string. + * + * **Details** + * + * - Pure, delegates to `String.uncapitalize`. + * + * **Example** (Uncapitalizing a string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const uncap = SchemaGetter.uncapitalize() + * ``` + * + * @category string + * @since 4.0.0 + */ +export function uncapitalize(): Getter { + return transform(Str.uncapitalize) +} + +/** + * Converts a `snake_case` string to `camelCase`. + * + * **Details** + * + * - Pure, delegates to `String.snakeToCamel`. + * + * **Example** (Converting snake case to camel case) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toCamel = SchemaGetter.snakeToCamel() + * ``` + * + * @see {@link camelToSnake} for the inverse operation + * + * @category string + * @since 4.0.0 + */ +export function snakeToCamel(): Getter { + return transform(Str.snakeToCamel) +} + +/** + * Converts a `camelCase` string to `snake_case`. + * + * **Details** + * + * - Pure, delegates to `String.camelToSnake`. + * + * **Example** (Converting camel case to snake case) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const toSnake = SchemaGetter.camelToSnake() + * ``` + * + * @see {@link snakeToCamel} for the inverse operation + * + * @category string + * @since 4.0.0 + */ +export function camelToSnake(): Getter { + return transform(Str.camelToSnake) +} + +/** + * Converts a string to lowercase. + * + * **Details** + * + * - Pure, delegates to `String.toLowerCase`. + * + * **Example** (Converting to lowercase) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const lower = SchemaGetter.toLowerCase() + * ``` + * + * @see {@link toUpperCase} for the inverse operation + * + * @category string + * @since 4.0.0 + */ +export function toLowerCase(): Getter { + return transform(Str.toLowerCase) +} + +/** + * Converts a string to uppercase. + * + * **Details** + * + * - Pure, delegates to `String.toUpperCase`. + * + * **Example** (Converting to uppercase) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const upper = SchemaGetter.toUpperCase() + * ``` + * + * @see {@link toLowerCase} for the inverse operation + * + * @category string + * @since 4.0.0 + */ +export function toUpperCase(): Getter { + return transform(Str.toUpperCase) +} + +type ParseJsonOptions = { + readonly reviver?: Parameters[1] +} + +/** + * Parses a JSON string into a value. + * + * **When to use** + * + * Use when you need a schema getter to parse a present encoded JSON string + * during decoding. + * + * **Details** + * + * - Skips `None` inputs. + * - Without `reviver`: returns `Schema.MutableJson` (typed JSON). + * - With `reviver`: returns `unknown` (reviver may produce arbitrary values). + * - On parse failure, fails with `SchemaIssue.InvalidValue` containing the error message. + * + * **Example** (Parsing JSON) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const parse = SchemaGetter.parseJson() + * // Getter + * ``` + * + * @see {@link stringifyJson} for the inverse operation + * + * @category JSON getters + * @since 4.0.0 + */ +export function parseJson(): Getter +export function parseJson(options: ParseJsonOptions): Getter +export function parseJson(options?: ParseJsonOptions | undefined): Getter { + return onSome((input) => + Effect.try({ + try: () => Option.some(JSON.parse(input, options?.reviver)), + catch: (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) + }) + ) +} + +type StringifyJsonOptions = { + readonly replacer?: Parameters[1] + readonly space?: Parameters[2] +} + +/** + * Stringifies a present value using `JSON.stringify`. + * + * **When to use** + * + * Use when you need a schema getter to serialize a present decoded value to + * JSON text during encoding. + * + * **Details** + * + * - Skips `None` inputs. + * - On thrown stringify failures, such as circular references, fails with + * `SchemaIssue.InvalidValue`. + * - Supports optional `replacer` and `space` options, matching + * `JSON.stringify`. + * - If `JSON.stringify` returns `undefined`, such as for `undefined`, + * functions, symbols, or a replacer that removes the root value, that + * `undefined` result is returned rather than converted into an `Issue`. + * + * **Example** (Stringifying JSON) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const stringify = SchemaGetter.stringifyJson() + * // Getter + * ``` + * + * @see {@link parseJson} for the inverse operation + * + * @category JSON getters + * @since 4.0.0 + */ +export function stringifyJson(options?: StringifyJsonOptions): Getter { + return onSome((input) => + Effect.try({ + try: () => Option.some(JSON.stringify(input, options?.replacer, options?.space)), + catch: (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) + }) + ) +} + +/** + * Parses a string into a record of key-value pairs. + * + * **When to use** + * + * Use when you need a schema getter to parse a present encoded string that + * contains delimited key-value pairs (e.g. `"a=1,b=2"`). + * + * **Details** + * + * The getter is pure and never fails. It splits the string by `separator` + * (default `,`) and then each pair by `keyValueSeparator` (default `=`). Pairs + * missing a key or value are silently skipped. + * + * **Example** (Parsing a key-value string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const parse = SchemaGetter.splitKeyValue() + * // "a=1,b=2" -> { a: "1", b: "2" } + * ``` + * + * @see {@link joinKeyValue} for the inverse operation + * @see {@link split} to split into an array of strings + * + * @category string + * @since 4.0.0 + */ +export function splitKeyValue(options?: { + readonly separator?: string | undefined + readonly keyValueSeparator?: string | undefined +}): Getter, E> { + const separator = options?.separator ?? "," + const keyValueSeparator = options?.keyValueSeparator ?? "=" + return transform((input) => + input.split(separator).reduce((acc, pair) => { + const [key, value] = pair.split(keyValueSeparator) + if (key && value) { + acc[key] = value + } + return acc + }, {} as Record) + ) +} + +/** + * Joins a record of key-value pairs into a delimited string. + * + * **When to use** + * + * Use when you need a schema getter to serialize a present decoded record as a + * delimited key-value string. + * + * **Details** + * + * The getter is pure and never fails. It joins entries with `separator` + * (default `,`) and joins each key and value with `keyValueSeparator` (default + * `=`). + * + * **Example** (Joining key-value records) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const join = SchemaGetter.joinKeyValue() + * // { a: "1", b: "2" } -> "a=1,b=2" + * ``` + * + * @see {@link splitKeyValue} for the inverse operation + * + * @category string + * @since 4.0.0 + */ +export function joinKeyValue>(options?: { + readonly separator?: string | undefined + readonly keyValueSeparator?: string | undefined +}): Getter { + const separator = options?.separator ?? "," + const keyValueSeparator = options?.keyValueSeparator ?? "=" + return transform((input) => + Object.entries(input).map(([key, value]) => `${key}${keyValueSeparator}${value}`).join(separator) + ) +} + +/** + * Splits a string into an array of strings by a separator. + * + * **When to use** + * + * Use when you need a schema getter to split a present encoded string + * containing a delimited list, such as CSV values. + * + * **Details** + * + * The getter is pure and never fails. It splits by `separator` (default `,`). + * An empty string produces an empty array, not `[""]`. + * + * **Example** (Splitting a comma-separated string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const splitComma = SchemaGetter.split() + * // "a,b,c" -> ["a", "b", "c"] + * // "" -> [] + * ``` + * + * @see {@link splitKeyValue} when values are key-value pairs + * + * @category string + * @since 4.0.0 + */ +export function split(options?: { + readonly separator?: string | undefined +}): Getter, E> { + const separator = options?.separator ?? "," + return transform((input) => input === "" ? [] : input.split(separator)) +} + +/** + * Encodes a `Uint8Array` or string to a Base64 string. + * + * **Details** + * + * The getter is pure and never fails. + * + * **Example** (Encoding to Base64) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeBase64() + * ``` + * + * @see {@link decodeBase64} for the inverse operation to `Uint8Array` + * @see {@link decodeBase64String} for the inverse operation to `string` + * @see {@link encodeBase64Url} for the URL-safe variant + * + * @category Base64 getters + * @since 4.0.0 + */ +export function encodeBase64(): Getter { + return transform(Encoding.encodeBase64) +} + +/** + * Encodes a `Uint8Array` or string to a URL-safe Base64 string. + * + * **Details** + * + * The getter is pure and never fails. + * + * **Example** (Encoding to Base64Url) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeBase64Url() + * ``` + * + * @see {@link decodeBase64Url} for the inverse operation to `Uint8Array` + * @see {@link decodeBase64UrlString} for the inverse operation to `string` + * @see {@link encodeBase64} for the standard Base64 variant + * + * @category Base64 getters + * @since 4.0.0 + */ +export function encodeBase64Url(): Getter { + return transform(Encoding.encodeBase64Url) +} + +/** + * Encodes a `Uint8Array` or string to a hexadecimal string. + * + * **Details** + * + * The getter is pure and never fails. + * + * **Example** (Encoding to hex) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeHex() + * ``` + * + * @see {@link decodeHex} for the inverse operation to `Uint8Array` + * @see {@link decodeHexString} for the inverse operation to `string` + * + * @category Hex getters + * @since 4.0.0 + */ +export function encodeHex(): Getter { + return transform(Encoding.encodeHex) +} + +/** + * Decodes a Base64 string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64. + * + * **Example** (Decoding Base64 to bytes) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeBase64() + * // Getter + * ``` + * + * @see {@link decodeBase64String} to decode to `string` instead + * @see {@link encodeBase64} for the inverse operation + * + * @category Base64 getters + * @since 4.0.0 + */ +export function decodeBase64(): Getter { + return transformOrFail((input) => + Effect.mapErrorEager( + Effect.fromResult(Encoding.decodeBase64(input)), + (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: e.message }) + ) + ) +} + +/** + * Decodes a Base64 string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64. + * + * **Example** (Decoding Base64 to string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeBase64String() + * // Getter + * ``` + * + * @see {@link decodeBase64} to decode to `Uint8Array` instead + * @see {@link encodeBase64} for the inverse operation + * + * @category Base64 getters + * @since 4.0.0 + */ +export function decodeBase64String(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64String(input), { + onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a URL-safe Base64 string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url. + * + * **Example** (Decoding Base64Url to bytes) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeBase64Url() + * // Getter + * ``` + * + * @see {@link decodeBase64UrlString} to decode to `string` instead + * @see {@link encodeBase64Url} for the inverse operation + * + * @category Base64 getters + * @since 4.0.0 + */ +export function decodeBase64Url(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64Url(input), { + onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a URL-safe Base64 string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url. + * + * **Example** (Decoding Base64Url to string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeBase64UrlString() + * // Getter + * ``` + * + * @see {@link decodeBase64Url} to decode to `Uint8Array` instead + * @see {@link encodeBase64Url} for the inverse operation + * + * @category Base64 getters + * @since 4.0.0 + */ +export function decodeBase64UrlString(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeBase64UrlString(input), { + onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a hexadecimal string to a `Uint8Array`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex. + * + * **Example** (Decoding hex to bytes) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeHex() + * // Getter + * ``` + * + * @see {@link decodeHexString} to decode to `string` instead + * @see {@link encodeHex} for the inverse operation + * + * @category Hex getters + * @since 4.0.0 + */ +export function decodeHex(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeHex(input), { + onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Decodes a hexadecimal string to a UTF-8 `string`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex. + * + * **Example** (Decoding hex to string) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeHexString() + * // Getter + * ``` + * + * @see {@link decodeHex} to decode to `Uint8Array` instead + * @see {@link encodeHex} for the inverse operation + * + * @category Hex getters + * @since 4.0.0 + */ +export function decodeHexString(): Getter { + return transformOrFail((input) => + Result.match(Encoding.decodeHexString(input), { + onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onSuccess: Effect.succeed + }) + ) +} + +/** + * Encodes a present string using `encodeURIComponent`. + * + * **Details** + * + * - Skips `None` inputs. + * - May throw a `URIError` for malformed surrogate pairs; this exception is not + * converted into an `Issue`. + * + * **Example** (Encoding a URI component) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeUriComponent() + * ``` + * + * @see {@link decodeUriComponent} for the inverse operation + * + * @category URI + * @since 4.0.0 + */ +export function encodeUriComponent(): Getter { + return transform(encodeURIComponent) +} + +/** + * Decodes a URI component encoded string using `decodeURIComponent`. + * + * **Details** + * + * - Fails with `SchemaIssue.InvalidValue` if the input contains malformed percent-encoding sequences. + * + * **Example** (Decoding a URI component) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeUriComponent() + * // Getter + * ``` + * + * @see {@link encodeUriComponent} for the inverse operation + * + * @category URI + * @since 4.0.0 + */ +export function decodeUriComponent(): Getter { + return transformOrFail((input) => { + try { + return Effect.succeed(globalThis.decodeURIComponent(input)) + } catch (e) { + return Effect.fail( + new SchemaIssue.InvalidValue(Option.some(input), { + message: e instanceof URIError ? e.message : "Invalid URI component" + }) + ) + } + }) +} + +/** + * Parses a `DateTime.Input` value into a `DateTime.Utc`. + * + * **When to use** + * + * Use when you need a schema getter to decode a present encoded date/time value + * to a `DateTime.Utc`. + * + * **Details** + * + * - Accepted input includes existing `DateTime` values, partial date/time parts, + * instant objects, zoned instant objects, JavaScript `Date` instances, epoch + * milliseconds, and date strings. + * - Converts successfully parsed values to UTC. + * - Fails with `SchemaIssue.InvalidValue` if the input cannot be parsed as a valid + * `DateTime`. + * + * **Example** (Parsing DateTime) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const parseDate = SchemaGetter.dateTimeUtcFromInput() + * // Getter + * ``` + * + * @see {@link Date} for a simpler coercion to `Date` (no validation) + * + * @category DateTime + * @since 4.0.0 + */ +export function dateTimeUtcFromInput(): Getter { + return transformOrFail((input) => { + return Option.match(DateTime.make(input), { + onNone: () => + Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: "Invalid DateTime input" })), + onSome: (dt) => Effect.succeed(DateTime.toUtc(dt)) + }) + }) +} + +/** + * Decodes a `FormData` object into a nested tree structure using bracket-path notation. + * + * **When to use** + * + * Use when you need a schema getter to parse `FormData` from HTTP requests into + * structured objects. + * + * **Details** + * + * The getter is pure and never fails. It interprets bracket-path keys such as + * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf + * value is a `string` or `Blob`. + * + * **Example** (Decoding FormData) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeFormData() + * // Getter, FormData> + * ``` + * + * @see {@link encodeFormData} for the inverse operation + * @see {@link makeTreeRecord} for the underlying bracket-path parser + * @see {@link decodeURLSearchParams} for the URLSearchParams variant + * + * @category FormData + * @since 4.0.0 + */ +export function decodeFormData(): Getter, FormData> { + return transform((input) => makeTreeRecord(Array.from(input.entries()))) +} + +const collectFormDataEntries = collectBracketPathEntries((value): value is string | Blob => + typeof value === "string" || (typeof Blob !== "undefined" && value instanceof Blob) +) + +/** + * Encodes a nested object into a `FormData` instance using bracket-path notation. + * + * **When to use** + * + * Use when you need a schema getter to serialize structured data to `FormData` + * for HTTP requests. + * + * **Details** + * + * The getter is pure and never fails. It flattens nested objects or arrays into + * bracket-path keys such as `user[name]` and `items[0]`. Non-object inputs + * produce an empty `FormData`. + * + * **Example** (Encoding to FormData) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeFormData() + * // Getter + * ``` + * + * @see {@link decodeFormData} for the inverse operation + * @see {@link collectBracketPathEntries} for the underlying flattener + * @see {@link encodeURLSearchParams} for the URLSearchParams variant + * + * @category FormData + * @since 4.0.0 + */ +export function encodeFormData(): Getter { + return transform((input) => { + const out = new FormData() + if (typeof input === "object" && input !== null) { + const entries = collectFormDataEntries(input) + entries.forEach(([key, value]) => { + out.append(key, value) + }) + } + return out + }) +} + +/** + * Decodes a `URLSearchParams` object into a nested tree structure using bracket-path notation. + * + * **When to use** + * + * Use when you need a schema getter to parse query parameters from URLs into + * structured objects. + * + * **Details** + * + * The getter is pure and never fails. It interprets bracket-path keys such as + * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf + * value is a `string`. + * + * **Example** (Decoding URLSearchParams) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const decode = SchemaGetter.decodeURLSearchParams() + * // Getter, URLSearchParams> + * ``` + * + * @see {@link encodeURLSearchParams} for the inverse operation + * @see {@link makeTreeRecord} for the underlying bracket-path parser + * @see {@link decodeFormData} for the FormData variant + * + * @category search params + * @since 4.0.0 + */ +export function decodeURLSearchParams(): Getter, URLSearchParams> { + return transform((input) => makeTreeRecord(Array.from(input.entries()))) +} + +const collectURLSearchParamsEntries = collectBracketPathEntries(Predicate.isString) + +/** + * Encodes a nested object into a `URLSearchParams` instance using bracket-path notation. + * + * **When to use** + * + * Use when you need a schema getter to serialize structured data to query + * parameters for URLs. + * + * **Details** + * + * The getter is pure and never fails. It flattens nested objects or arrays into + * bracket-path keys. Non-object inputs produce an empty `URLSearchParams`. + * + * **Example** (Encoding to URLSearchParams) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const encode = SchemaGetter.encodeURLSearchParams() + * // Getter + * ``` + * + * @see {@link decodeURLSearchParams} for the inverse operation + * @see {@link collectBracketPathEntries} for the underlying flattener + * @see {@link encodeFormData} for the FormData variant + * + * @category search params + * @since 4.0.0 + */ +export function encodeURLSearchParams(): Getter { + return transform((input) => { + if (typeof input === "object" && input !== null) { + return new URLSearchParams(collectURLSearchParamsEntries(input)) + } + return new URLSearchParams() + }) +} + +const INDEX_REGEXP = /^\d+$/ + +function bracketPathToTokens(bracketPath: string): Array { + // real empty path (from append("", value)) + if (bracketPath === "") { + return [""] + } + + const replaced = bracketPath.replace(/\[(.*?)\]/g, ".$1") + const parts = replaced.split(".") + // if bracket path started with "[...]" we get ".foo" => ["", "foo"]; drop the synthetic first "" + const start = replaced.startsWith(".") ? 1 : 0 + + return parts + .slice(start) + .map((part) => (INDEX_REGEXP.test(part) ? globalThis.Number(part) : part)) +} + +function getOrCreateContainer( + self: any, + key: PropertyKey, + shouldBeArray: boolean +): any { + const current = Object.hasOwn(self, key) ? self[key] : undefined + if (current !== undefined) { + return current + } + const container = shouldBeArray ? [] : {} + internalRecord.set(self, key, container) + return container +} + +/** + * Builds a nested tree object from a list of bracket-path entries. + * + * **When to use** + * + * Use when you need a schema getter to parse FormData or URLSearchParams + * entries into structured objects. + * - You have flat key-value pairs with bracket-path keys that need nesting. + * + * **Details** + * + * - A bracket path is a string like `"user[address][city]"` that describes nested + * object/array structure. + * - Interprets bracket paths and constructs the corresponding nested object. + * - Builds and returns a nested object from the input entries. + * - Supported syntax: + * - `"foo"` → object key `"foo"` + * - `"foo[bar]"` → nested `{ foo: { bar: ... } }` + * - `"foo[0]"` → array index `{ foo: [value] }` + * - `"foo[]"` → append to array `foo` + * - `""` → real empty key + * - Duplicate keys for the same path are merged into arrays. + * + * **Example** (Building a tree from bracket paths) + * + * ```ts + * import { SchemaGetter } from "effect" + * + * const tree = SchemaGetter.makeTreeRecord([ + * ["user[name]", "Alice"], + * ["user[tags][]", "admin"], + * ["user[tags][]", "editor"] + * ]) + * // { user: { name: "Alice", tags: ["admin", "editor"] } } + * ``` + * + * @see {@link collectBracketPathEntries} for the inverse operation (tree to flat entries) + * @see {@link decodeFormData} for a higher-level FormData decoder + * @see {@link decodeURLSearchParams} for a higher-level URLSearchParams decoder + * + * @category Tree + * @since 4.0.0 + */ +export function makeTreeRecord( + bracketPathEntries: ReadonlyArray +): Schema.TreeRecord { + const out: any = {} + bracketPathEntries.forEach(([key, value]) => { + const tokens = bracketPathToTokens(key) + let cur: any = out + tokens.forEach((token, i) => { + const isLast = i === tokens.length - 1 + + // We are inside an array and see "[]" (empty token) => append + if (Array.isArray(cur) && token === "") { + if (isLast) { + cur.push(value) + } else { + // bracket path: "foo[][bar]" => push a new element and descend into it + const next = tokens[i + 1] + const shouldBeArray = typeof next === "number" || next === "" + const index = cur.length + cur = getOrCreateContainer(cur, index, shouldBeArray) + } + } else if (isLast) { + // If we're setting a value at a path that already exists + // convert it to an array to support multiple values for the same key + const hasOwn = Object.hasOwn(cur, token) + if (hasOwn && Array.isArray(cur[token])) { + cur[token].push(value) + } else if (hasOwn) { + internalRecord.set(cur, token, [cur[token], value]) + } else { + internalRecord.set(cur, token, value) + } + } else { + const next = tokens[i + 1] + // if next is a number OR "" (from []), we are building an array + const shouldBeArray = typeof next === "number" || next === "" + cur = getOrCreateContainer(cur, token, shouldBeArray) + } + }) + }) + return out +} + +/** + * Flattens a nested object into bracket-path entries, filtering leaf values by a type guard. + * + * **When to use** + * + * Use when you need a schema getter to serialize structured objects to flat + * key-value entries. + * - Building custom `FormData` or `URLSearchParams` encoders. + * + * **Details** + * + * - This is the inverse of {@link makeTreeRecord}. + * - Takes a nested object and produces flat `[bracketPath, value]` pairs suitable for + * `FormData` or `URLSearchParams`. + * - Returns a curried function: first call provides the leaf type guard, second call provides the object. + * - Recursively traverses objects and arrays. + * - If all elements of an array are leaves, encodes them as multiple entries with the same key + * (e.g. `tags=a&tags=b`). Otherwise uses indexed bracket paths (e.g. `items[0]`, `items[1]`). + * - Non-leaf values that aren't objects or arrays are silently skipped. + * + * **Example** (Flattening an object to bracket paths) + * + * ```ts + * import { Predicate, SchemaGetter } from "effect" + * + * const collectStrings = SchemaGetter.collectBracketPathEntries(Predicate.isString) + * const entries = collectStrings({ user: { name: "Alice", tags: ["admin", "editor"] } }) + * // [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]] + * ``` + * + * @see {@link makeTreeRecord} for the inverse operation (flat entries to tree) + * @see {@link encodeFormData} for a higher-level FormData encoder + * @see {@link encodeURLSearchParams} for a higher-level URLSearchParams encoder + * + * @category Tree + * @since 4.0.0 + */ +export function collectBracketPathEntries(isLeaf: (value: unknown) => value is A) { + return (input: object): Array<[bracketPath: string, value: A]> => { + const bracketPathEntries: Array<[string, A]> = [] + + function append(key: string, value: unknown): void { + if (isLeaf(value)) { + bracketPathEntries.push([key, value]) + } else if (Array.isArray(value)) { + // If all values are leaves, encode as multiple entries with the same key + const allLeaves = value.every(isLeaf) + if (allLeaves) { + value.forEach((v) => { + bracketPathEntries.push([key, v]) + }) + } else { + value.forEach((v, i) => { + append(`${key}[${i}]`, v) + }) + } + } else if (typeof value === "object" && value !== null) { + for (const [k, v] of Object.entries(value)) { + append(`${key}[${k}]`, v) + } + } + } + + for (const [key, value] of Object.entries(input)) { + append(key, value) + } + + return bracketPathEntries + } +} diff --git a/repos/effect/packages/effect/src/SchemaIssue.ts b/repos/effect/packages/effect/src/SchemaIssue.ts new file mode 100644 index 0000000..365500b --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaIssue.ts @@ -0,0 +1,1176 @@ +/** + * Describes problems found while decoding, encoding, or checking data with + * schemas. + * + * An `Issue` records what failed and, for nested data, where the failure + * happened. The Schema system uses these values for missing keys, unexpected + * keys, invalid types, invalid values, failed filters, failed transformations, + * and alternatives that did not match. This module also formats issues and + * supports redaction for sensitive values. + * + * @since 4.0.0 + */ +import type { StandardSchemaV1 } from "@standard-schema/spec" +import * as Arr from "./Array.ts" +import { format, formatPath, type Formatter as FormatterI } from "./Formatter.ts" +import * as InternalAnnotations from "./internal/schema/annotations.ts" +import * as Option from "./Option.ts" +import { hasProperty } from "./Predicate.ts" +import * as Redacted from "./Redacted.ts" +import type * as Schema from "./Schema.ts" +import type * as SchemaAST from "./SchemaAST.ts" + +const TypeId = "~effect/SchemaIssue/Issue" + +/** + * Returns `true` if the given value is an {@link Issue}. + * + * **When to use** + * + * Use when you need to narrow an `unknown` value to `Issue` in error-handling + * code, such as distinguishing an `Issue` from other error types in a catch-all + * handler. + * + * **Details** + * + * - Checks for the internal `TypeId` brand on the value. + * + * **Example** (Type-guarding an unknown error) + * + * ```ts + * import { SchemaIssue } from "effect" + * + * const issue = new SchemaIssue.MissingKey(undefined) + * console.log(SchemaIssue.isIssue(issue)) + * // true + * console.log(SchemaIssue.isIssue("not an issue")) + * // false + * ``` + * + * @see {@link Issue} + * + * @category guards + * @since 4.0.0 + */ +export function isIssue(u: unknown): u is Issue { + return hasProperty(u, TypeId) +} + +/** + * Union of all terminal (leaf) issue types that have no inner `Issue` children. + * + * **When to use** + * + * Use when constraining formatter hooks to only handle terminal nodes or when + * pattern matching on the `_tag` of an issue and only leaf nodes matter. + * + * **Details** + * + * Members: {@link InvalidType}, {@link InvalidValue}, {@link MissingKey}, + * {@link UnexpectedKey}, {@link Forbidden}, {@link OneOf}. + * + * @see {@link Issue} — the full union including composite nodes + * @see {@link LeafHook} — formatter hook that operates on `Leaf` values + * + * @category models + * @since 4.0.0 + */ +export type Leaf = + | InvalidType + | InvalidValue + | MissingKey + | UnexpectedKey + | Forbidden + | OneOf + +/** + * The root discriminated union of all validation error nodes. + * + * **When to use** + * + * Use when typing the error channel in `Effect` results from + * schema parsing, or when writing custom formatters or issue-tree walkers. + * + * **Details** + * + * Every node has a `_tag` field for pattern-matching. The union includes both + * terminal {@link Leaf} types and composite types that wrap inner issues: + * {@link Filter}, {@link Encoding}, {@link Pointer}, {@link Composite}, + * {@link AnyOf}. All `Issue` instances have a `toString()` that delegates to + * the default formatter, so `String(issue)` produces a human-readable message. + * + * @see {@link Leaf} — the terminal subset + * @see {@link isIssue} — type guard + * @see {@link getActual} — extract the actual value from any issue + * + * @category models + * @since 4.0.0 + */ +export type Issue = + | Leaf + // composite + | Filter + | Encoding + | Pointer + | Composite + | AnyOf + +class Base { + readonly [TypeId] = TypeId + toString(this: Issue): string { + return defaultFormatter(this) + } +} + +/** + * Represents a schema issue produced when a schema filter (refinement check) fails. + * + * **When to use** + * + * Use when you need to inspect a schema issue that records which refinement + * check rejected the value. + * + * **Details** + * + * - `actual` is the raw input value that was tested (plain `unknown`, not + * wrapped in `Option`). + * - `filter` is the AST filter node that produced this issue. + * - `issue` is the inner issue describing the failure reason. + * + * **Example** (Matching a Filter issue) + * + * ```ts + * import { SchemaIssue } from "effect" + * + * function describe(issue: SchemaIssue.Issue): string { + * if (issue._tag === "Filter") { + * return `Filter failed on: ${JSON.stringify(issue.actual)}` + * } + * return String(issue) + * } + * ``` + * + * @see {@link Leaf} — terminal issue types that commonly appear as the inner `issue` + * @see {@link CheckHook} — formatter hook for `Filter` issues + * + * @category models + * @since 4.0.0 + */ +export class Filter extends Base { + readonly _tag = "Filter" + /** + * The input value that caused the issue. + */ + readonly actual: unknown + /** + * The filter that failed. + */ + readonly filter: SchemaAST.Filter + /** + * The issue that occurred. + */ + readonly issue: Issue + + constructor( + /** + * The input value that caused the issue. + */ + actual: unknown, + /** + * The filter that failed. + */ + filter: SchemaAST.Filter, + /** + * The issue that occurred. + */ + issue: Issue + ) { + super() + this.actual = actual + this.filter = filter + this.issue = issue + } +} + +/** + * Represents a schema issue produced when a schema transformation (encode/decode step) fails. + * + * **When to use** + * + * Use when you need to inspect failures from `Schema.decodeTo` / `Schema.encodeTo` + * transformations. + * + * **Details** + * + * - `ast` is the AST node for the transformation that failed. + * - `actual` is `Option.some(value)` when the input was present, or + * `Option.none()` when it was absent. + * - `issue` is the inner issue describing the failure. + * + * @see {@link Filter} — failure from a refinement check (not a transformation) + * @see {@link Composite} — multiple issues from a single schema node + * + * @category models + * @since 4.0.0 + */ +export class Encoding extends Base { + readonly _tag = "Encoding" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.AST + /** + * The input value that caused the issue. + */ + readonly actual: Option.Option + /** + * The issue that occurred. + */ + readonly issue: Issue + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.AST, + /** + * The input value that caused the issue. + */ + actual: Option.Option, + /** + * The issue that occurred. + */ + issue: Issue + ) { + super() + this.ast = ast + this.actual = actual + this.issue = issue + } +} + +/** + * Wraps an inner {@link Issue} with a property-key path, indicating *where* in + * a nested structure the error occurred. + * + * **When to use** + * + * Use when you need to walk the issue tree to accumulate path segments for error + * reporting. + * + * **Details** + * + * - `path` is an array of property keys (strings, numbers, or symbols). + * - Has no `actual` value — {@link getActual} returns `Option.none()`. + * - Formatters concatenate nested `Pointer` paths into a single path like + * `["a"]["b"][0]`. + * + * @see {@link getActual} — returns `Option.none()` for `Pointer` + * @see {@link Composite} — groups multiple issues under one schema node + * + * @category models + * @since 3.10.0 + */ +export class Pointer extends Base { + readonly _tag = "Pointer" + /** + * The path to the location in the input that caused the issue. + */ + readonly path: ReadonlyArray + /** + * The issue that occurred. + */ + readonly issue: Issue + + constructor( + /** + * The path to the location in the input that caused the issue. + */ + path: ReadonlyArray, + /** + * The issue that occurred. + */ + issue: Issue + ) { + super() + this.path = path + this.issue = issue + } +} + +/** + * Represents a schema issue produced when a required key or tuple index is missing from the input. + * + * **When to use** + * + * Use when you need to detect absent fields in struct/tuple validation. + * + * **Details** + * + * - Has no `actual` value — {@link getActual} returns `Option.none()`. + * - `annotations` may contain a custom `messageMissingKey` for formatting. + * + * @see {@link Pointer} — wraps this issue with the missing key's path + * @see {@link UnexpectedKey} — the opposite case (extra key present) + * + * @category models + * @since 4.0.0 + */ +export class MissingKey extends Base { + readonly _tag = "MissingKey" + /** + * The metadata for the issue. + */ + readonly annotations: Schema.Annotations.Key | undefined + + constructor( + /** + * The metadata for the issue. + */ + annotations: Schema.Annotations.Key | undefined + ) { + super() + this.annotations = annotations + } +} + +/** + * Represents a schema issue produced when an input object or tuple contains a key/index not + * declared by the schema. + * + * **When to use** + * + * Use when you need to detect excess properties during strict struct/tuple + * validation. + * + * **Details** + * + * - `actual` is the raw value at the unexpected key (plain `unknown`). + * - `ast` is the schema that was being validated against. + * - `annotations` on `ast` may contain a custom `messageUnexpectedKey`. + * + * @see {@link MissingKey} — the opposite case (required key absent) + * @see {@link Pointer} — wraps this issue with the unexpected key's path + * + * @category models + * @since 4.0.0 + */ +export class UnexpectedKey extends Base { + readonly _tag = "UnexpectedKey" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.AST + /** + * The input value that caused the issue. + */ + readonly actual: unknown + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.AST, + /** + * The input value that caused the issue. + */ + actual: unknown + ) { + super() + this.ast = ast + this.actual = actual + } +} + +/** + * Represents a schema issue that groups multiple child issues under a single schema node. + * + * **When to use** + * + * Use when you need to walk the issue tree for struct/tuple schemas that collect + * all field errors rather than failing on the first. + * + * **Details** + * + * - `issues` is a non-empty readonly array (at least one child). + * - `actual` is `Option.some(value)` when the input was present, or + * `Option.none()` when absent. + * - Formatters flatten `Composite` by recursing into each child. + * + * @see {@link AnyOf} — used for union no-match errors (similar but different semantics) + * @see {@link Pointer} — adds path context to individual issues + * + * @category models + * @since 3.10.0 + */ +export class Composite extends Base { + readonly _tag = "Composite" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.AST + /** + * The input value that caused the issue. + */ + readonly actual: Option.Option + /** + * The issues that occurred. + */ + readonly issues: readonly [Issue, ...Array] + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.AST, + /** + * The input value that caused the issue. + */ + actual: Option.Option, + /** + * The issues that occurred. + */ + issues: readonly [Issue, ...Array] + ) { + super() + this.ast = ast + this.actual = actual + this.issues = issues + } +} + +/** + * Represents a schema issue produced when the runtime type of the input does not match the type + * expected by the schema (e.g. got `null` when `string` was expected). + * + * **When to use** + * + * Use when you need to detect basic type mismatches, such as a wrong primitive + * or `null` where an object was expected. + * + * **Details** + * + * - `ast` is the schema node that expected a different type. + * - `actual` is `Option.some(value)` when the input was present, or + * `Option.none()` when no value was provided. + * - The default formatter renders this as `"Expected , got "`. + * + * **Example** (Formatting output) + * + * ```ts + * import { Schema } from "effect" + * + * try { + * Schema.decodeUnknownSync(Schema.String)(42) + * } catch (e) { + * if (Schema.isSchemaError(e)) { + * console.log(String(e.issue)) + * // "Expected string, got 42" + * } + * } + * ``` + * + * @see {@link InvalidValue} — the input has the right type but fails a value constraint + * + * @category models + * @since 4.0.0 + */ +export class InvalidType extends Base { + readonly _tag = "InvalidType" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.AST + /** + * The input value that caused the issue. + */ + readonly actual: Option.Option + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.AST, + /** + * The input value that caused the issue. + */ + actual: Option.Option + ) { + super() + this.ast = ast + this.actual = actual + } +} + +/** + * Represents a schema issue produced when the input has the correct type but its value violates a + * constraint (e.g. a string that is too short, a number out of range). + * + * **When to use** + * + * Use when you need to detect constraint violations from `Schema.filter`, + * `Schema.minLength`, `Schema.greaterThan`, or similar checks. + * + * **Details** + * + * - `actual` is `Option.some(value)` when the failing value is known, or + * `Option.none()` when absent. + * - `annotations` optionally carries a `message` string for formatting. + * - The default formatter renders this as `"Invalid data "` unless a + * custom `message` annotation is provided. + * + * **Example** (Returning InvalidValue from a custom filter) + * + * ```ts + * import { Option, SchemaIssue } from "effect" + * + * const issue = new SchemaIssue.InvalidValue( + * Option.some(""), + * { message: "must not be empty" } + * ) + * console.log(String(issue)) + * // "must not be empty" + * ``` + * + * @see {@link InvalidType} — the input has the wrong type entirely + * @see {@link Filter} — composite wrapper when a schema filter produces this issue + * + * @category models + * @since 4.0.0 + */ +export class InvalidValue extends Base { + readonly _tag = "InvalidValue" + /** + * The value that caused the issue. + */ + readonly actual: Option.Option + /** + * The metadata for the issue. + */ + readonly annotations: Schema.Annotations.Issue | undefined + + constructor( + /** + * The value that caused the issue. + */ + actual: Option.Option, + /** + * The metadata for the issue. + */ + annotations?: Schema.Annotations.Issue | undefined + ) { + super() + this.actual = actual + this.annotations = annotations + } +} + +/** + * Represents a schema issue produced when a forbidden operation is encountered during parsing, + * such as an asynchronous Effect running inside `Schema.decodeUnknownSync`. + * + * **When to use** + * + * Use when you need to detect that a schema requires async execution but was run + * synchronously. + * + * **Details** + * + * - `actual` is `Option.some(value)` when the input is known, or + * `Option.none()` when absent. + * - `annotations` optionally carries a `message` string. + * - The default formatter renders this as `"Forbidden operation"`. + * + * **Example** (Creating a Forbidden issue) + * + * ```ts + * import { Option, SchemaIssue } from "effect" + * + * const issue = new SchemaIssue.Forbidden( + * Option.none(), + * { message: "async operation not allowed in sync context" } + * ) + * console.log(String(issue)) + * // "async operation not allowed in sync context" + * ``` + * + * @see {@link InvalidValue} — for value-constraint failures (not operation failures) + * + * @category models + * @since 3.10.0 + */ +export class Forbidden extends Base { + readonly _tag = "Forbidden" + /** + * The input value that caused the issue. + */ + readonly actual: Option.Option + /** + * The metadata for the issue. + */ + readonly annotations: Schema.Annotations.Issue | undefined + + constructor( + /** + * The input value that caused the issue. + */ + actual: Option.Option, + /** + * The metadata for the issue. + */ + annotations: Schema.Annotations.Issue | undefined + ) { + super() + this.actual = actual + this.annotations = annotations + } +} + +/** + * Represents a schema issue produced when a value does not match *any* member of a union schema. + * + * **When to use** + * + * Use when you need to inspect which union members were attempted and why each + * failed. + * + * **Details** + * + * - `ast` is the `Union` AST node. + * - `actual` is the raw input value (plain `unknown`). + * - `issues` contains per-member failures. When empty, the formatter falls + * back to the union's `expected` annotation. + * + * @see {@link OneOf} — the opposite: *too many* members matched + * @see {@link Composite} — groups multiple issues under a non-union schema + * + * @category models + * @since 4.0.0 + */ +export class AnyOf extends Base { + readonly _tag = "AnyOf" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.Union + /** + * The input value that caused the issue. + */ + readonly actual: unknown + /** + * The issues that occurred. + */ + readonly issues: ReadonlyArray + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.Union, + /** + * The input value that caused the issue. + */ + actual: unknown, + /** + * The issues that occurred. + */ + issues: ReadonlyArray + ) { + super() + this.ast = ast + this.actual = actual + this.issues = issues + } +} + +/** + * Represents a schema issue produced when a value matches *multiple* members of a union that is + * configured to allow exactly one match (oneOf mode). + * + * **When to use** + * + * Use when you need to detect ambiguous union matches when `oneOf` validation is + * enabled. + * + * **Details** + * + * - `ast` is the `Union` AST node. + * - `actual` is the raw input value (plain `unknown`). + * - `successes` lists the AST nodes of each member that accepted the input. + * - The default formatter renders this as + * `"Expected exactly one member to match the input "`. + * + * @see {@link AnyOf} — the opposite: *no* members matched + * + * @category models + * @since 4.0.0 + */ +export class OneOf extends Base { + readonly _tag = "OneOf" + /** + * The schema that caused the issue. + */ + readonly ast: SchemaAST.Union + /** + * The input value that caused the issue. + */ + readonly actual: unknown + /** + * The schemas that were successful. + */ + readonly successes: ReadonlyArray + + constructor( + /** + * The schema that caused the issue. + */ + ast: SchemaAST.Union, + /** + * The input value that caused the issue. + */ + actual: unknown, + /** + * The schemas that were successful. + */ + successes: ReadonlyArray + ) { + super() + this.ast = ast + this.actual = actual + this.successes = successes + } +} + +/** + * Extracts the actual input value from any {@link Issue} variant. + * + * **When to use** + * + * Use when you need to retrieve an `Issue`'s offending input value for logging + * or custom error rendering. + * + * **Details** + * + * - Returns `Option.none()` for `Pointer` and `MissingKey` (they carry no + * value). + * - Returns the existing `Option` for variants that already store `actual` as + * `Option` (`InvalidType`, `InvalidValue`, `Forbidden`, `Encoding`, + * `Composite`). + * - Wraps `actual` with `Option.some` for variants that store it as plain + * `unknown` (`AnyOf`, `UnexpectedKey`, `OneOf`, `Filter`). + * + * **Example** (Extracting the actual value) + * + * ```ts + * import { Option, SchemaIssue } from "effect" + * + * const issue = new SchemaIssue.MissingKey(undefined) + * console.log(SchemaIssue.getActual(issue)) + * // { _tag: "None" } + * ``` + * + * @see {@link Issue} + * @see {@link isIssue} + * + * @category getters + * @since 4.0.0 + */ +export function getActual(issue: Issue): Option.Option { + switch (issue._tag) { + case "Pointer": + case "MissingKey": + return Option.none() + case "InvalidType": + case "InvalidValue": + case "Forbidden": + case "Encoding": + case "Composite": + return issue.actual + case "AnyOf": + case "UnexpectedKey": + case "OneOf": + case "Filter": + return Option.some(issue.actual) + } +} + +function makeFilterIssue(input: unknown, entry: Schema.FilterIssue): Issue { + if (isIssue(entry)) { + return entry + } + if (typeof entry === "string") { + return new InvalidValue(Option.some(input), { message: entry }) + } + const inner = typeof entry.issue === "string" + ? new InvalidValue(Option.some(input), { message: entry.issue }) + : entry.issue + return new Pointer(entry.path, inner) +} + +/** @internal */ +export function makeSingle(input: unknown, out: undefined | boolean | Schema.FilterIssue): Issue | undefined { + if (out === undefined) { + return undefined + } + if (typeof out === "boolean") { + return out ? undefined : new InvalidValue(Option.some(input)) + } + return makeFilterIssue(input, out) +} + +/** @internal */ +export function make(input: unknown, ast: SchemaAST.AST, out: Schema.FilterOutput): Issue | undefined { + if (Array.isArray(out)) { + if (Arr.isReadonlyArrayNonEmpty(out)) { + if (out.length === 1) { + return makeFilterIssue(input, out[0]) + } + return new Composite(ast, Option.some(input), Arr.map(out, (entry) => makeFilterIssue(input, entry))) + } + return undefined + } + return makeSingle(input, out as undefined | boolean | Schema.FilterIssue) +} + +/** + * A function type that converts an {@link Issue} into a formatted + * representation. Specialisation of the generic `Formatter` from + * `Formatter.ts` with `Value` fixed to `Issue`. + * + * @see {@link makeFormatterDefault} — creates a `Formatter` + * @see {@link makeFormatterStandardSchemaV1} — creates a `Formatter` + * + * @category Formatter + * @since 4.0.0 + */ +export interface Formatter extends FormatterI {} + +/** + * Callback type used to format {@link Leaf} issues into strings. + * + * **When to use** + * + * Use when customizing how {@link makeFormatterStandardSchemaV1} renders + * terminal issues. + * + * @see {@link defaultLeafHook} — the built-in implementation + * @see {@link Leaf} — the union of terminal issue types + * + * @category Formatter + * @since 4.0.0 + */ +export type LeafHook = (issue: Leaf) => string + +/** + * Returns the built-in {@link LeafHook} used by default formatters. + * + * **When to use** + * + * Use as the default leaf renderer when customizing only the {@link CheckHook}. + * + * **Details** + * + * - Checks for a `message` annotation first; returns it if present. + * - Otherwise generates a default message per `_tag`: + * - `InvalidType` → `"Expected , got "` + * - `InvalidValue` → `"Invalid data "` + * - `MissingKey` → `"Missing key"` + * - `UnexpectedKey` → `"Unexpected key with value "` + * - `Forbidden` → `"Forbidden operation"` + * - `OneOf` → `"Expected exactly one member to match the input "` + * + * **Example** (Formatting Standard Schema issues with defaultLeafHook) + * + * ```ts + * import { SchemaIssue } from "effect" + * + * const formatter = SchemaIssue.makeFormatterStandardSchemaV1({ + * leafHook: SchemaIssue.defaultLeafHook + * }) + * ``` + * + * @see {@link LeafHook} + * @see {@link makeFormatterStandardSchemaV1} + * + * @category Formatter + * @since 4.0.0 + */ +export const defaultLeafHook: LeafHook = (issue): string => { + const message = findMessage(issue) + if (message !== undefined) return message + switch (issue._tag) { + case "InvalidType": + return getExpectedMessage(InternalAnnotations.getExpected(issue.ast), formatOption(issue.actual)) + case "InvalidValue": + return `Invalid data ${formatOption(issue.actual)}` + case "MissingKey": + return "Missing key" + case "UnexpectedKey": + return `Unexpected key with value ${format(issue.actual)}` + case "Forbidden": + return "Forbidden operation" + case "OneOf": + return `Expected exactly one member to match the input ${format(issue.actual)}` + } +} + +/** + * Callback type used to format {@link Filter} issues into strings. + * + * **When to use** + * + * Use when customizing how {@link makeFormatterStandardSchemaV1} renders + * filter failures. + * + * **Details** + * + * - Returns `string` to override the message, or `undefined` to fall back to + * the default formatting. + * + * @see {@link defaultCheckHook} — the built-in implementation + * @see {@link Filter} — the issue type this hook formats + * + * @category Formatter + * @since 4.0.0 + */ +export type CheckHook = (issue: Filter) => string | undefined + +/** + * Returns the built-in {@link CheckHook} used by default formatters. + * + * **When to use** + * + * Use as the default filter renderer when customizing only the {@link LeafHook}. + * + * **Details** + * + * - Looks for a `message` annotation on the inner issue first, then on the + * filter itself. + * - Returns `undefined` when no annotation is found, causing the formatter to + * fall back to `"Expected , got "`. + * + * @see {@link CheckHook} + * @see {@link makeFormatterStandardSchemaV1} + * + * @category Formatter + * @since 4.0.0 + */ +export const defaultCheckHook: CheckHook = (issue): string | undefined => { + return findMessage(issue.issue) ?? findMessage(issue) +} + +/** + * Creates a {@link Formatter} that produces a `StandardSchemaV1.FailureResult`. + * + * **When to use** + * + * Use when you need schema parse errors in + * [Standard Schema V1](https://github.com/standard-schema/standard-schema) + * format, optionally customizing leaf or check issue rendering. + * + * **Details** + * + * - Returns a `Formatter`. + * - Each leaf issue is flattened into `{ message, path }` entries. + * - `Pointer` paths are accumulated to produce full property paths. + * - Falls back to {@link defaultLeafHook} / {@link defaultCheckHook} when no + * hooks are provided. + * + * **Example** (Creating a Standard Schema V1 formatter) + * + * ```ts + * import { SchemaIssue } from "effect" + * + * const formatter = SchemaIssue.makeFormatterStandardSchemaV1() + * ``` + * + * @see {@link makeFormatterDefault} — produces a plain string instead + * @see {@link LeafHook} + * @see {@link CheckHook} + * + * @category Formatter + * @since 4.0.0 + */ +export function makeFormatterStandardSchemaV1(options?: { + readonly leafHook?: LeafHook | undefined + readonly checkHook?: CheckHook | undefined +}): Formatter { + return (issue) => ({ + issues: toDefaultIssues(issue, [], options?.leafHook ?? defaultLeafHook, options?.checkHook ?? defaultCheckHook) + }) +} + +// A subtype of StandardSchemaV1.Issue +type DefaultIssue = { + readonly message: string + readonly path: ReadonlyArray +} + +function getExpectedMessage(expected: string, actual: string): string { + return `Expected ${expected}, got ${actual}` +} + +function toDefaultIssues( + issue: Issue, + path: ReadonlyArray, + leafHook: LeafHook, + checkHook: CheckHook +): Array { + switch (issue._tag) { + case "Filter": { + const message = checkHook(issue) + if (message !== undefined) { + return [{ path, message }] + } + switch (issue.issue._tag) { + case "InvalidValue": + return [{ + path, + message: getExpectedMessage(formatCheck(issue.filter), format(issue.actual)) + }] + default: + return toDefaultIssues(issue.issue, path, leafHook, checkHook) + } + } + case "Encoding": + return toDefaultIssues(issue.issue, path, leafHook, checkHook) + case "Pointer": + return toDefaultIssues(issue.issue, [...path, ...issue.path], leafHook, checkHook) + case "Composite": + return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) + case "AnyOf": { + const message = findMessage(issue) + if (issue.issues.length === 0) { + if (message !== undefined) return [{ path, message }] + + const expected = getExpectedMessage(InternalAnnotations.getExpected(issue.ast), format(issue.actual)) + return [{ path, message: expected }] + } + return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) + } + default: + return [{ path, message: leafHook(issue) }] + } +} + +function formatCheck(check: SchemaAST.Check): string { + const expected = check.annotations?.expected + if (typeof expected === "string") return expected + + switch (check._tag) { + case "Filter": + return "" + case "FilterGroup": + return check.checks.map((check) => formatCheck(check)).join(" & ") + } +} + +/** + * Creates a {@link Formatter} that converts an {@link Issue} into a + * human-readable multi-line string. + * + * **When to use** + * + * Use when you need to format a `SchemaIssue.Issue` as error messages for + * logging, CLI output, or developer-facing diagnostics. + * + * **Details** + * + * This is the default formatter used by `SchemaIssue.toString()`. + * + * - Flattens the issue tree into `{ message, path }` entries using + * {@link defaultLeafHook} and {@link defaultCheckHook}. + * - Each entry is rendered as `""` or `"\n at "`. + * - Multiple entries are joined with newlines. + * + * **Example** (Formatting an issue as a string) + * + * ```ts + * import { SchemaIssue } from "effect" + * + * const formatter = SchemaIssue.makeFormatterDefault() + * ``` + * + * @see {@link makeFormatterStandardSchemaV1} — produces Standard Schema V1 format instead + * @see {@link Formatter} + * + * @category Formatter + * @since 4.0.0 + */ +export function makeFormatterDefault(): Formatter { + return (issue) => + toDefaultIssues(issue, [], defaultLeafHook, defaultCheckHook) + .map(formatDefaultIssue) + .join("\n") +} + +/** @internal */ +export const defaultFormatter = makeFormatterDefault() + +function formatDefaultIssue(issue: DefaultIssue): string { + let out = issue.message + if (issue.path && issue.path.length > 0) { + const path = formatPath(issue.path as ReadonlyArray) + out += `\n at ${path}` + } + return out +} + +function findMessage(issue: Issue): string | undefined { + switch (issue._tag) { + case "InvalidType": + case "OneOf": + case "Composite": + case "AnyOf": + return getMessageAnnotation(issue.ast.annotations) + case "InvalidValue": + case "Forbidden": + return getMessageAnnotation(issue.annotations) + case "MissingKey": + return getMessageAnnotation(issue.annotations, "messageMissingKey") + case "UnexpectedKey": + return getMessageAnnotation(issue.ast.annotations, "messageUnexpectedKey") + case "Filter": + return getMessageAnnotation(issue.filter.annotations) + case "Encoding": + return findMessage(issue.issue) + } +} + +function getMessageAnnotation( + annotations: Schema.Annotations.Annotations | undefined, + type: "message" | "messageMissingKey" | "messageUnexpectedKey" = "message" +): string | undefined { + const message = annotations?.[type] + if (typeof message === "string") return message +} + +function formatOption(actual: Option.Option): string { + if (Option.isNone(actual)) return "no value provided" + return format(actual.value) +} + +/** @internal */ +export function redact(issue: Issue): Issue { + switch (issue._tag) { + case "MissingKey": + return issue + case "Forbidden": + return new Forbidden(Option.map(issue.actual, Redacted.make), issue.annotations) + case "Filter": + return new Filter(Redacted.make(issue.actual), issue.filter, redact(issue.issue)) + case "Pointer": + return new Pointer(issue.path, redact(issue.issue)) + + case "Encoding": + case "InvalidType": + case "InvalidValue": + case "Composite": + return new InvalidValue(Option.map(issue.actual, Redacted.make)) + + case "AnyOf": + case "OneOf": + case "UnexpectedKey": + return new InvalidValue(Option.some(Redacted.make(issue.actual))) + } +} diff --git a/repos/effect/packages/effect/src/SchemaParser.ts b/repos/effect/packages/effect/src/SchemaParser.ts new file mode 100644 index 0000000..e80b849 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaParser.ts @@ -0,0 +1,1113 @@ +/** + * Runs schemas against real values. + * + * Schema parsers construct values from schema input, check whether a value + * matches a schema, decode encoded input, and encode decoded values back to + * their external form. This module exposes those operations through several + * result styles, including `Effect`, `Promise`, `Exit`, `Option`, `Result`, and + * synchronous functions that throw. It also contains the lower-level runner that + * walks a schema AST and reports schema failures as `SchemaIssue.Issue` values. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import { memoize } from "./Function.ts" +import * as InternalSchemaCause from "./internal/schema/cause.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Result from "./Result.ts" +import type * as Schema from "./Schema.ts" +import * as SchemaAST from "./SchemaAST.ts" +import * as SchemaIssue from "./SchemaIssue.ts" + +const recurDefaults = memoize((ast: SchemaAST.AST): SchemaAST.AST => { + switch (ast._tag) { + case "Declaration": { + const getLink = ast.annotations?.[SchemaAST.ClassTypeId] + if (Predicate.isFunction(getLink)) { + const link = getLink(ast.typeParameters) + const to = recurDefaults(link.to) + return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + } + return ast + } + case "Objects": + case "Arrays": + return ast.recur((ast) => { + const defaultValue = ast.context?.defaultValue + if (defaultValue) { + return SchemaAST.replaceEncoding(recurDefaults(ast), defaultValue) + } + return recurDefaults(ast) + }) + case "Suspend": + return ast.recur(recurDefaults) + default: + return ast + } +}) + +/** + * Creates an effectful maker for the schema's decoded type side. + * + * **When to use** + * + * Use to construct decoded schema values in `Effect` while preserving + * construction failures as `SchemaIssue.Issue` values in the error channel. + * + * **Details** + * + * The returned function accepts constructor input, applies constructor defaults, + * runs type-side validation unless checks are disabled, and fails with a + * `SchemaIssue.Issue` when construction fails. + * + * @category constructors + * @since 4.0.0 + */ +export function makeEffect(schema: S) { + const ast = recurDefaults(SchemaAST.toType(schema.ast)) + const parser = run(ast) + return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect => { + return parser( + input, + options?.disableChecks + ? options?.parseOptions ? { ...options.parseOptions, disableChecks: true } : { disableChecks: true } + : options?.parseOptions + ) + } +} + +/** + * Creates a synchronous maker that returns `Option.some` with the constructed + * value on success, or `Option.none` when construction fails with schema issues. + * + * **When to use** + * + * Use when you need to validate schema constructor input and only care whether + * construction succeeds, without exposing `SchemaIssue.Issue` details. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `Option.none`. + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`. + * + * @category constructors + * @since 4.0.0 + */ +export function makeOption(schema: S) { + const parser = makeEffect(schema) + return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option => { + const exit = Effect.runSyncExit(parser(input, options)) + if (Exit.isSuccess(exit)) { + return Option.some(exit.value) + } + InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues") + return Option.none() + } +} + +/** + * Creates a synchronous maker for the schema's decoded type side. + * + * **When to use** + * + * Use to construct decoded schema values synchronously when invalid input + * should throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function constructs a value from constructor input and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` when construction fails. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @category constructors + * @since 4.0.0 + */ +export function make(schema: S) { + const parser = makeEffect(schema) + return (input: S["~type.make.in"], options?: Schema.MakeOptions): S["Type"] => { + const exit = Effect.runSyncExit(parser(input, options)) + if (Exit.isSuccess(exit)) { + return exit.value + } + const issue = InternalSchemaCause.getSchemaIssueOrThrow( + exit.cause, + "Constructor adapter can only throw schema issues" + ) + throw new Error(issue.toString(), { cause: issue }) + } +} + +/** + * Creates a type guard that checks whether an input satisfies the schema's decoded + * type side. + * + * **When to use** + * + * Use to build a type guard for checking the decoded side of a schema without + * exposing issue details. + * + * **Details** + * + * The guard returns `true` on successful validation and `false` when validation + * fails only with schema issues, without exposing issue details. + * + * **Gotchas** + * + * Only causes made entirely of schema issues are converted to `false`. Causes + * that contain defects, interruptions, or asynchronous work at this synchronous + * boundary throw an `Error` whose cause is the underlying `Cause`. + * + * @category Asserting + * @since 3.10.0 + */ +export function is(schema: S): (input: I) => input is I & S["Type"] { + return _is(schema.ast) +} + +/** @internal */ +export function _is(ast: SchemaAST.AST) { + const parser = asExit(run(SchemaAST.toType(ast))) + return (input: I): input is I & T => { + const exit = parser(input, SchemaAST.defaultParseOptions) + if (Exit.isSuccess(exit)) { + return true + } + InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Type guard adapter can only return false for schema issues") + return false + } +} + +/** @internal */ +export function _issue(ast: SchemaAST.AST) { + const parser = run(ast) + return (input: unknown, options: SchemaAST.ParseOptions): SchemaIssue.Issue | undefined => { + const exit = Effect.runSyncExit(parser(input, options)) + if (Exit.isSuccess(exit)) { + return undefined + } + return InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Issue adapter can only return schema issues") + } +} + +/** + * Asserts that an input satisfies the schema's decoded type side. + * + * **When to use** + * + * Use to assert that an input satisfies the decoded side of a schema when schema + * validation failures should throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The assertion returns normally when validation succeeds. When the input does + * not satisfy the schema with a schema-only failure, it throws an `Error` with + * the `SchemaIssue.Issue` in its `cause`. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @category Asserting + * @since 4.0.0 + */ +export function asserts(schema: S, input: I): asserts input is I & S["Type"] { + const parser = asExit(run(SchemaAST.toType(schema.ast))) + const exit = parser(input, SchemaAST.defaultParseOptions) + if (Exit.isFailure(exit)) { + const issue = InternalSchemaCause.getSchemaIssueOrThrow( + exit.cause, + "Assertion adapter can only throw schema issues" + ) + throw new Error(issue.toString(), { cause: issue }) + } +} + +/** + * Creates an effectful decoder for `unknown` input. + * + * **When to use** + * + * Use when you need to decode untyped boundary input in an `Effect` whose + * failure channel is `SchemaIssue.Issue`, while preserving transformations + * and service requirements. + * + * **Details** + * + * The returned function succeeds with the schema's decoded `Type` or fails with a + * `SchemaIssue.Issue`. Decoding service requirements are preserved in the returned + * `Effect`. Parse options may be provided when creating the decoder and overridden + * when applying it. + * + * @see {@link decodeEffect} for input already typed as the schema's `Encoded` type + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownEffect( + schema: S, + options?: SchemaAST.ParseOptions +): ( + input: unknown, + options?: SchemaAST.ParseOptions +) => Effect.Effect { + const parser = run(schema.ast) + return options === undefined + ? parser + : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions)) +} + +/** + * Creates an effectful decoder for input already typed as the schema's `Encoded` + * type. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and + * need an `Effect` whose failure channel is `SchemaIssue.Issue`, while + * preserving decoding service requirements. + * + * **Details** + * + * The returned function succeeds with the decoded `Type` or fails with a + * `SchemaIssue.Issue`, preserving any decoding service requirements in the + * returned `Effect`. + * + * @see {@link decodeUnknownEffect} for untyped boundary input + * @see {@link encodeEffect} for the opposite direction + * + * @category decoding + * @since 4.0.0 + */ +export const decodeEffect: ( + schema: S, + options?: SchemaAST.ParseOptions +) => ( + input: S["Encoded"], + options?: SchemaAST.ParseOptions +) => Effect.Effect = decodeUnknownEffect + +/** + * Creates a Promise-based decoder for `unknown` input. + * + * **When to use** + * + * Use when you need to decode untyped input with a service-free schema and + * return a JavaScript `Promise`. + * + * **Details** + * + * The returned function resolves with the decoded `Type` on success and rejects + * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or other non-schema reasons reject + * with an `Error` whose cause is the underlying `Cause`. + * + * @see {@link decodePromise} for input already typed as the schema's `Encoded` type + * @see {@link decodeUnknownEffect} for schemas that require decoding services or when failures should remain in `Effect` + * + * @category decoding + * @since 3.10.0 + */ +export function decodeUnknownPromise>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Promise { + return asPromise(decodeUnknownEffect(schema, options)) +} + +/** + * Creates a Promise-based decoder for input already typed as the schema's + * `Encoded` type. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and need + * decoding to return a JavaScript `Promise`. + * + * **Details** + * + * The returned function resolves with the decoded `Type` on success and rejects + * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or other non-schema reasons reject + * with an `Error` whose cause is the underlying `Cause`. + * + * @see {@link decodeUnknownPromise} for untyped input returning a JavaScript `Promise` + * @see {@link decodeEffect} for preserving decoding services and failures in `Effect` + * + * @category decoding + * @since 3.10.0 + */ +export function decodePromise>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise { + return asPromise(decodeEffect(schema, options)) +} + +/** + * Creates a synchronous decoder for `unknown` input that reports failure safely + * as an `Exit`. + * + * **When to use** + * + * Use when you need to decode unknown input synchronously into an `Exit` whose + * failure contains `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Exit.Success` with the decoded `Type`. + * Schema issues are represented by an `Exit.Failure` cause containing a + * `SchemaIssue.Issue`. + * + * **Gotchas** + * + * Because this adapter runs synchronously, async decoding work can produce an + * `Exit.Failure` with a defect cause. When the cause contains both schema + * issues and non-schema reasons, all reasons remain in the returned `Cause`. + * + * @see {@link decodeExit} for input already typed as the schema's `Encoded` type + * @see {@link decodeUnknownEffect} for preserving decoding services and failures in `Effect` + * @see {@link decodeUnknownResult} for returning schema issues as data + * @see {@link decodeUnknownSync} for throwing on decoding failure + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownExit>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { + return asExit(decodeUnknownEffect(schema, options)) +} + +/** + * Creates a synchronous decoder for input already typed as the schema's `Encoded` + * type, reporting failure safely as an `Exit`. + * + * **When to use** + * + * Use when you need synchronous decoding of already typed `Encoded` input into + * an `Exit` whose failure contains `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Exit.Success` with the decoded `Type` or + * `Exit.Failure` with a `SchemaIssue.Issue`. + * + * **Gotchas** + * + * Because this adapter runs synchronously, async decoding work can produce an + * `Exit.Failure` with a defect cause. When the cause contains both schema + * issues and non-schema reasons, all reasons remain in the returned `Cause`. + * + * @see {@link decodeUnknownExit} for untyped input with the same `Exit` result shape + * @see {@link decodeEffect} for preserving decoding services and failures in `Effect` + * + * @category decoding + * @since 4.0.0 + */ +export const decodeExit: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit.Exit = + decodeUnknownExit + +/** @internal */ +export function decodeUnknownOption>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { + return asOption(decodeUnknownEffect(schema, options)) +} + +/** @internal */ +export const decodeOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option.Option = decodeUnknownOption + +/** + * Creates a decoder for `unknown` input that reports failure safely as a + * `Result`. + * + * **When to use** + * + * Use when decoding untyped boundary input and you want `SchemaIssue.Issue` + * failures returned as data in a `Result`. + * + * **Details** + * + * The returned function produces `Result.succeed` with the decoded `Type` on + * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure. + * + * **Gotchas** + * + * This adapter runs synchronously. Causes made entirely of schema issues become + * `Result.fail`, but causes that contain defects, interruptions, or asynchronous + * work at this synchronous boundary throw instead. + * + * @see {@link decodeResult} for input already typed as the schema's `Encoded` type + * @see {@link decodeUnknownEffect} for effectful or service-requiring decoding + * + * @category decoding + * @since 4.0.0 + */ +export function decodeUnknownResult>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { + return asResult(decodeUnknownEffect(schema, options)) +} + +/** + * Creates a decoder for input already typed as the schema's `Encoded` type, + * reporting failure safely as a `Result`. + * + * **When to use** + * + * Use when you already have input typed as the schema's `Encoded` type and want + * schema decoding failures represented as `Result.fail` with `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Result.succeed` with the decoded `Type` on + * success or `Result.fail` with a `SchemaIssue.Issue` on decoding failure. + * + * **Gotchas** + * + * This synchronous adapter returns `Result.fail` for causes made entirely of + * schema issues, but causes that contain defects, interruptions, or other + * non-schema reasons throw instead. + * + * @see {@link decodeUnknownResult} for untyped input with the same `Result` shape + * @see {@link decodeEffect} for effectful or service-requiring decoding + * + * @category decoding + * @since 4.0.0 + */ +export const decodeResult: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result.Result = + decodeUnknownResult + +/** + * Creates a synchronous decoder for `unknown` input. + * + * **When to use** + * + * Use to decode untrusted or dynamically typed input at a synchronous boundary + * where invalid data should throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function returns the decoded `Type` on success and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @see {@link decodeSync} for input already typed as the schema's `Encoded` type + * @see {@link decodeUnknownEffect} for preserving decoding failures in `Effect` + * @see {@link decodeUnknownResult} for returning schema issues as data + * + * @category decoding + * @since 3.10.0 + */ +export function decodeUnknownSync>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => S["Type"] { + return asSync(decodeUnknownEffect(schema, options)) +} + +/** + * Creates a synchronous decoder for input already typed as the schema's `Encoded` + * type. + * + * **When to use** + * + * Use to decode values already typed as the schema's `Encoded` input when + * decoding failure should throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function returns the decoded `Type` on success and throws an + * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @see {@link decodeUnknownSync} for untrusted or dynamically typed input + * @see {@link decodeResult} for returning schema issues as data + * @see {@link decodeEffect} for preserving decoding failures in `Effect` + * + * @category decoding + * @since 3.10.0 + */ +export const decodeSync: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync + +/** + * Creates an effectful encoder for `unknown` input. + * + * **When to use** + * + * Use when you need to encode untyped boundary input in an `Effect` whose + * failure channel is `SchemaIssue.Issue`, while preserving service + * requirements. + * + * **Details** + * + * The returned function succeeds with the schema's `Encoded` value or fails with a + * `SchemaIssue.Issue`. Encoding service requirements are preserved in the returned + * `Effect`. Parse options may be provided when creating the encoder and overridden + * when applying it. + * + * @see {@link encodeEffect} for the typed-input variant when the value is already typed as the schema's decoded `Type` + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownEffect( + schema: S, + options?: SchemaAST.ParseOptions +): ( + input: unknown, + options?: SchemaAST.ParseOptions +) => Effect.Effect { + const parser = run(SchemaAST.flip(schema.ast)) + return options === undefined + ? parser + : (input, overrideOptions) => parser(input, mergeParseOptions(options, overrideOptions)) +} + +/** + * Creates an effectful encoder for input already typed as the schema's decoded + * `Type`. + * + * **When to use** + * + * Use when you need to encode values already typed as the schema's decoded + * `Type` in an `Effect` whose failure channel is `SchemaIssue.Issue`, while + * preserving service requirements. + * + * **Details** + * + * The returned function succeeds with the schema's `Encoded` value or fails with a + * `SchemaIssue.Issue`, preserving any encoding service requirements in the + * returned `Effect`. + * + * @see {@link encodeUnknownEffect} for encoding unknown input before the value is statically typed as the schema's `Type` + * + * @category encoding + * @since 4.0.0 + */ +export const encodeEffect: ( + schema: S, + options?: SchemaAST.ParseOptions +) => ( + input: S["Type"], + options?: SchemaAST.ParseOptions +) => Effect.Effect = encodeUnknownEffect + +/** + * Creates a Promise-based encoder for `unknown` input. + * + * **When to use** + * + * Use when you need to encode untrusted or dynamically typed values with a + * service-free schema and return a JavaScript `Promise`. + * + * **Details** + * + * The returned function resolves with the schema's `Encoded` value on success and + * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or other non-schema reasons reject + * with an `Error` whose cause is the underlying `Cause`. + * + * @see {@link encodePromise} for input already typed as the schema's decoded `Type` + * @see {@link encodeUnknownEffect} for schemas that require encoding services or when failures should remain in `Effect` + * + * @category encoding + * @since 3.10.0 + */ +export const encodeUnknownPromise = >( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Promise => + asPromise(encodeUnknownEffect(schema, options)) + +/** + * Creates a Promise-based encoder for input already typed as the schema's decoded + * `Type`. + * + * **When to use** + * + * Use when you already have values typed as the schema's decoded `Type` and + * need encoding to return a JavaScript `Promise`. + * + * **Details** + * + * The returned function resolves with the schema's `Encoded` value on success and + * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or other non-schema reasons reject + * with an `Error` whose cause is the underlying `Cause`. + * + * @see {@link encodeUnknownPromise} for encoding untyped input + * @see {@link encodeEffect} for effectful encoding or schemas with encoding service requirements + * + * @category encoding + * @since 3.10.0 + */ +export const encodePromise: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise + +/** + * Creates a synchronous encoder for `unknown` input that reports failure safely + * as an `Exit`. + * + * **When to use** + * + * Use when you need synchronous encoding of unknown input into an `Exit` whose + * failure contains `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Exit.Success` with the schema's `Encoded` value + * or `Exit.Failure` with a `SchemaIssue.Issue`. + * + * **Gotchas** + * + * Because this adapter runs synchronously, async encoding work can produce an + * `Exit.Failure` with a defect cause. When the cause contains both schema + * issues and non-schema reasons, all reasons remain in the returned `Cause`. + * + * @see {@link encodeExit} for input already typed as the schema's decoded `Type` + * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownExit>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { + return asExit(encodeUnknownEffect(schema, options)) +} + +/** + * Creates a synchronous encoder for input already typed as the schema's decoded + * `Type`, reporting failure safely as an `Exit`. + * + * **When to use** + * + * Use when you need synchronous encoding of already typed schema values into + * an `Exit` whose failure contains `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Exit.Success` with the schema's `Encoded` value + * or `Exit.Failure` with a `SchemaIssue.Issue`. + * + * **Gotchas** + * + * Because this adapter runs synchronously, async encoding work can produce an + * `Exit.Failure` with a defect cause. When the cause contains both schema + * issues and non-schema reasons, all reasons remain in the returned `Cause`. + * + * @see {@link encodeUnknownExit} for unknown input with the same `Exit` result shape + * @see {@link encodeEffect} for effectful encoding that preserves service requirements + * + * @category encoding + * @since 4.0.0 + */ +export const encodeExit: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit.Exit = + encodeUnknownExit + +/** @internal */ +export function encodeUnknownOption>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { + return asOption(encodeUnknownEffect(schema, options)) +} + +/** @internal */ +export const encodeOption: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option.Option = encodeUnknownOption + +/** + * Creates an encoder for `unknown` input that reports failure safely as a + * `Result`. + * + * **When to use** + * + * Use when encoding values from an unknown or dynamically typed boundary + * synchronously, and you want `SchemaIssue.Issue` failures returned as `Result` + * data. + * + * **Details** + * + * The returned function produces `Result.succeed` with the schema's `Encoded` + * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding + * failure. + * + * **Gotchas** + * + * This adapter runs synchronously. Causes made entirely of schema issues become + * `Result.fail`, but causes that contain defects, interruptions, or asynchronous + * work at this synchronous boundary throw instead. + * + * @see {@link encodeResult} for input already typed as the schema's decoded `Type` + * @see {@link encodeUnknownEffect} for effectful encoding, including schemas with encoding service requirements + * + * @category encoding + * @since 4.0.0 + */ +export function encodeUnknownResult>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { + return asResult(encodeUnknownEffect(schema, options)) +} + +/** + * Creates an encoder for input already typed as the schema's decoded `Type`, + * reporting failure safely as a `Result`. + * + * **When to use** + * + * Use when you already have input typed as the schema's decoded `Type` and want + * encoding failures returned as `Result.fail` with `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function produces `Result.succeed` with the schema's `Encoded` + * value on success or `Result.fail` with a `SchemaIssue.Issue` on encoding + * failure. + * + * **Gotchas** + * + * This synchronous adapter returns `Result.fail` for causes made entirely of + * schema issues, but causes that contain defects, interruptions, or other + * non-schema reasons throw instead. + * + * @see {@link encodeUnknownResult} for the same `Result` shape when the input is not already typed + * + * @category encoding + * @since 4.0.0 + */ +export const encodeResult: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result.Result = + encodeUnknownResult + +/** + * Creates a synchronous encoder for `unknown` input. + * + * **When to use** + * + * Use when you need to encode values from untyped input in synchronous code and + * want encoding failures to throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function returns the schema's `Encoded` value on success and throws + * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @see {@link encodeSync} for input already typed as the schema's decoded `Type` + * @see {@link encodeUnknownEffect} for effectful encoding that preserves service requirements + * + * @category encoding + * @since 3.10.0 + */ +export function encodeUnknownSync>( + schema: S, + options?: SchemaAST.ParseOptions +): (input: unknown, options?: SchemaAST.ParseOptions) => S["Encoded"] { + return asSync(encodeUnknownEffect(schema, options)) +} + +/** + * Creates a synchronous encoder for input already typed as the schema's decoded + * `Type`. + * + * **When to use** + * + * Use to encode already typed schema values synchronously when encoding failure + * should throw an `Error` whose cause is `SchemaIssue.Issue`. + * + * **Details** + * + * The returned function returns the schema's `Encoded` value on success and throws + * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. + * + * **Gotchas** + * + * Causes that contain defects, interruptions, or asynchronous work at this + * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, + * instead of being converted to a schema validation error. + * + * @see {@link encodeUnknownSync} for unknown input with the same throwing boundary + * @see {@link encodeResult} for returning schema issues as data + * @see {@link encodeEffect} for effectful encoding that preserves service requirements + * + * @category encoding + * @since 3.10.0 + */ +export const encodeSync: >( + schema: S, + options?: SchemaAST.ParseOptions +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync + +const mergeParseOptions = ( + options: SchemaAST.ParseOptions, + overrideOptions: SchemaAST.ParseOptions | undefined +): SchemaAST.ParseOptions => overrideOptions === undefined ? options : { ...options, ...overrideOptions } + +/** @internal */ +export function run(ast: SchemaAST.AST) { + const parser = recur(ast) + return (input: unknown, options?: SchemaAST.ParseOptions): Effect.Effect => + Effect.flatMapEager(parser(Option.some(input), options ?? SchemaAST.defaultParseOptions), (oa) => { + if (oa._tag === "None") { + return Effect.fail(new SchemaIssue.InvalidValue(oa)) + } + return Effect.succeed(oa.value as T) + }) +} + +function asPromise( + parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect +): (input: E, options?: SchemaAST.ParseOptions) => Promise { + return (input: E, options?: SchemaAST.ParseOptions) => + Effect.runPromiseExit(parser(input, options)).then((exit) => { + if (Exit.isSuccess(exit)) { + return exit.value + } + const issue = InternalSchemaCause.getSchemaIssueOrThrow( + exit.cause, + "Promise adapter can only reject schema issues" + ) + throw new Error(issue.toString(), { cause: issue }) + }) +} + +function asExit( + parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect +): (input: E, options?: SchemaAST.ParseOptions) => Exit.Exit { + return (input: E, options?: SchemaAST.ParseOptions) => Effect.runSyncExit(parser(input, options) as any) +} + +/** @internal */ +export function asOption( + parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect +): (input: E, options?: SchemaAST.ParseOptions) => Option.Option { + const parserExit = asExit(parser) + return (input: E, options?: SchemaAST.ParseOptions) => { + const exit = parserExit(input, options) + if (Exit.isSuccess(exit)) { + return Option.some(exit.value) + } + InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Option adapter can only return none for schema issues") + return Option.none() + } +} + +function asResult( + parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect +): (input: E, options?: SchemaAST.ParseOptions) => Result.Result { + const parserExit = asExit(parser) + return (input: E, options?: SchemaAST.ParseOptions) => { + const exit = parserExit(input, options) + if (Exit.isSuccess(exit)) { + return Result.succeed(exit.value) + } + return Result.fail( + InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Result adapter can only return schema issues") + ) + } +} + +function asSync( + parser: (input: E, options?: SchemaAST.ParseOptions) => Effect.Effect +): (input: E, options?: SchemaAST.ParseOptions) => T { + const parserExit = asExit(parser) + return (input: E, options?: SchemaAST.ParseOptions) => { + const exit = parserExit(input, options) + if (Exit.isSuccess(exit)) { + return exit.value + } + const issue = InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Sync adapter can only throw schema issues") + throw new Error(issue.toString(), { cause: issue }) + } +} + +function mapSchemaIssueEffect( + self: Effect.Effect, + f: (issue: SchemaIssue.Issue) => SchemaIssue.Issue +): Effect.Effect { + return Effect.catchCause(self, (cause) => Effect.failCauseSync(() => Cause.map(cause, f))) +} + +/** @internal */ +export interface Parser { + ( + input: Option.Option, + options: SchemaAST.ParseOptions + ): Effect.Effect, SchemaIssue.Issue, any> +} + +const recur = memoize( + (ast: SchemaAST.AST): Parser => { + let parser: Parser + const checks = ast.checks + const encoding = ast.encoding + const links = encoding + const len = links?.length ?? 0 + const encodingChecks = (ast as any).encodingChecks + const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations) + ?.["parseOptions"] + if (!ast.context && !encoding && !checks && !encodingChecks) { + return (ou, options) => { + parser ??= ast.getParser(recur) + if (astOptions) { + options = { ...options, ...astOptions } + } + return parser(ou, options) + } + } + const isStructural = SchemaAST.isArrays(ast) || SchemaAST.isObjects(ast) || + (SchemaAST.isDeclaration(ast) && ast.typeParameters.length > 0) + const structuralChecks = checks && isStructural ? + checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]) : + undefined + return (ou, options) => { + if (astOptions) { + options = { ...options, ...astOptions } + } + let srou: Effect.Effect, SchemaIssue.Issue, unknown> | undefined + if (links) { + for (let i = len - 1; i >= 0; i--) { + const link = links[i] + const to = link.to + const parser = recur(to) + srou = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) + if (link.transformation._tag === "Transformation") { + const getter = link.transformation.decode + srou = Effect.flatMapEager(srou, (ou) => getter.run(ou, options)) + } else { + srou = link.transformation.decode(srou, options) + } + } + srou = mapSchemaIssueEffect(srou!, (issue) => new SchemaIssue.Encoding(ast, ou, issue)) + } + + parser ??= ast.getParser(recur) + const parseLocal = (localOu: Option.Option) => { + let sroa = parser(localOu, options) + + if (encodingChecks && !options?.disableChecks) { + sroa = Effect.flatMapEager(sroa, (oa) => { + if (Option.isSome(localOu) && Option.isSome(oa)) { + const issues: Array = [] + + SchemaAST.collectIssues(encodingChecks, localOu.value, issues, ast, options) + + if (Arr.isArrayNonEmpty(issues)) { + return Effect.fail(new SchemaIssue.Composite(ast, localOu, issues)) + } + } + return Effect.succeed(oa) + }) + } + + if (checks && !options?.disableChecks) { + if (options?.errors === "all" && structuralChecks && structuralChecks.length > 0 && Option.isSome(localOu)) { + sroa = mapSchemaIssueEffect(sroa, (issue) => { + const issues: Array = [] + SchemaAST.collectIssues( + structuralChecks, + localOu.value, + issues, + ast, + options + ) + const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues) + ? issue._tag === "Composite" && issue.ast === ast + ? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues]) + : new SchemaIssue.Composite(ast, localOu, [issue, ...issues]) + : issue + return out + }) + } + sroa = Effect.flatMapEager(sroa, (oa) => { + if (Option.isSome(oa)) { + const value = oa.value + const issues: Array = [] + + SchemaAST.collectIssues(checks, value, issues, ast, options) + + if (Arr.isArrayNonEmpty(issues)) { + return Effect.fail(new SchemaIssue.Composite(ast, oa, issues)) + } + } + return Effect.succeed(oa) + }) + } + + return sroa + } + + const sroa = srou ? Effect.flatMapEager(srou, parseLocal) : parseLocal(ou) + + return sroa + } + } +) diff --git a/repos/effect/packages/effect/src/SchemaRepresentation.ts b/repos/effect/packages/effect/src/SchemaRepresentation.ts new file mode 100644 index 0000000..547ea6e --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaRepresentation.ts @@ -0,0 +1,3907 @@ +/** + * Plain data structures for describing schemas in a serializable form. A + * `Representation` is not the original `Schema` object; it is a JSON-friendly + * description of the schema's types, fields, unions, checks, annotations, and + * references. + * + * This module defines the representation node types, document types, and + * codecs used to validate those documents. It can build representation + * documents from schema ASTs, turn representation documents back into schemas, + * convert them to and from JSON Schema documents, and generate TypeScript code + * artifacts for schema definitions. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" +import { format, formatPropertyKey } from "./Formatter.ts" +import { collectBrands } from "./internal/schema/annotations.ts" +import * as InternalRepresentation from "./internal/schema/representation.ts" +import { unescapeToken } from "./JsonPointer.ts" +import type * as JsonSchema from "./JsonSchema.ts" +import { remainder } from "./Number.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import * as Rec from "./Record.ts" +import * as Schema from "./Schema.ts" +import * as SchemaAST from "./SchemaAST.ts" +import * as SchemaGetter from "./SchemaGetter.ts" + +// ----------------------------------------------------------------------------- +// specification +// ----------------------------------------------------------------------------- + +/** + * A custom type declaration, such as `Date`, `Option`, or `ReadonlySet`. + * + * **When to use** + * + * Use when inspecting or transforming non-primitive schema types. + * + * **Details** + * + * `typeParameters` holds the inner type arguments, such as the `A` in + * `Option`. `encodedSchema` is the fallback representation when no + * {@link Reviver} recognizes this declaration. `annotations.typeConstructor` + * identifies the declaration kind, such as `{ _tag: "effect/Option" }`. + * + * @see {@link Reviver} + * @see {@link toSchemaDefaultReviver} + * + * @category models + * @since 4.0.0 + */ +export interface Declaration { + readonly _tag: "Declaration" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly typeParameters: ReadonlyArray + readonly checks: ReadonlyArray> + readonly encodedSchema: Representation +} + +/** + * A lazily resolved representation used for recursive schemas. + * + * **Details** + * + * `thunk` points to the actual representation, possibly via a + * {@link Reference}. `checks` is always empty on `Suspend` nodes. + * + * @see {@link Reference} + * + * @category models + * @since 4.0.0 + */ +export interface Suspend { + readonly _tag: "Suspend" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: readonly [] + readonly thunk: Representation +} + +/** + * A named reference to a definition in the {@link References} map. + * + * **When to use** + * + * Use when a representation should point to a named definition instead of + * embedding the definition inline. + * + * **Details** + * + * `$ref` is the key into `Document.references` or `MultiDocument.references`. + * References are resolved lazily by {@link toSchema} and + * {@link toCodeDocument}. + * + * **Gotchas** + * + * Resolution throws at runtime if the key is not found in the references map. + * + * @see {@link References} + * @see {@link Document} + * + * @category models + * @since 4.0.0 + */ +export interface Reference { + readonly _tag: "Reference" + readonly $ref: string +} + +/** + * The `null` type. + * + * @category models + * @since 4.0.0 + */ +export interface Null { + readonly _tag: "Null" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `undefined` type. + * + * @category models + * @since 4.0.0 + */ +export interface Undefined { + readonly _tag: "Undefined" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `void` type. + * + * @category models + * @since 4.0.0 + */ +export interface Void { + readonly _tag: "Void" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `never` type (no valid values). + * + * @category models + * @since 4.0.0 + */ +export interface Never { + readonly _tag: "Never" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `unknown` type (any value accepted). + * + * @category models + * @since 4.0.0 + */ +export interface Unknown { + readonly _tag: "Unknown" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `any` type. + * + * @category models + * @since 4.0.0 + */ +export interface Any { + readonly _tag: "Any" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `string` type with optional validation checks. + * + * **Details** + * + * `checks` holds string-specific constraints, such as min/max length, pattern, + * and UUID checks. `contentMediaType` and `contentSchema` indicate that the + * string contains encoded data, such as `"application/json"` with a nested + * schema. + * + * @see {@link StringMeta} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export interface String { + readonly _tag: "String" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: ReadonlyArray> + readonly contentMediaType?: string | undefined + readonly contentSchema?: Representation | undefined +} + +/** + * The `number` type with optional validation checks. + * + * **Details** + * + * `checks` holds number-specific constraints, such as int, finite, min, max, + * multipleOf, and between checks. + * + * @see {@link NumberMeta} + * + * @category models + * @since 4.0.0 + */ +export interface Number { + readonly _tag: "Number" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: ReadonlyArray> +} + +/** + * The `boolean` type. + * + * @category models + * @since 4.0.0 + */ +export interface Boolean { + readonly _tag: "Boolean" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * The `bigint` type with optional validation checks. + * + * @see {@link BigIntMeta} + * + * @category models + * @since 4.0.0 + */ +export interface BigInt { + readonly _tag: "BigInt" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: ReadonlyArray> +} + +/** + * The `symbol` type. + * + * @category models + * @since 4.0.0 + */ +export interface Symbol { + readonly _tag: "Symbol" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * A specific literal value (`string`, `number`, `boolean`, or `bigint`). + * + * @category models + * @since 4.0.0 + */ +export interface Literal { + readonly _tag: "Literal" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly literal: string | number | boolean | bigint +} + +/** + * A specific unique `symbol` value. + * + * @category models + * @since 4.0.0 + */ +export interface UniqueSymbol { + readonly _tag: "UniqueSymbol" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly symbol: symbol +} + +/** + * The `object` keyword type (matches any non-primitive). + * + * @category models + * @since 4.0.0 + */ +export interface ObjectKeyword { + readonly _tag: "ObjectKeyword" + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * A TypeScript-style enum. Each entry is a `[name, value]` pair. + * + * @category models + * @since 4.0.0 + */ +export interface Enum { + readonly _tag: "Enum" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly enums: ReadonlyArray +} + +/** + * A template literal type composed of a sequence of parts (literals, strings, + * numbers, etc.). + * + * @category models + * @since 4.0.0 + */ +export interface TemplateLiteral { + readonly _tag: "TemplateLiteral" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly parts: ReadonlyArray +} + +/** + * An array or tuple type. + * + * **Details** + * + * `elements` are the fixed positional elements, or tuple prefix, and each may + * be optional. `rest` contains the variadic tail types; a single-element + * `rest` with no `elements` produces a plain `Array`. `checks` holds + * array-specific constraints, such as minLength, maxLength, and unique checks. + * + * @see {@link Element} + * @see {@link ArraysMeta} + * + * @category models + * @since 4.0.0 + */ +export interface Arrays { + readonly _tag: "Arrays" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly elements: ReadonlyArray + readonly rest: ReadonlyArray + readonly checks: ReadonlyArray> +} + +/** + * A positional element within an {@link Arrays} tuple. + * + * **Details** + * + * `isOptional` indicates whether this element can be absent. `type` is the + * schema representation for this element's value. + * + * @see {@link Arrays} + * + * @category models + * @since 4.0.0 + */ +export interface Element { + readonly isOptional: boolean + readonly type: Representation + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * An object/struct type with named properties and optional index signatures. + * + * **Details** + * + * `propertySignatures` are the explicitly named fields. `indexSignatures` + * define catch-all key/value types, such as `Record`. `checks` + * holds object-specific constraints, such as minProperties and maxProperties. + * + * @see {@link PropertySignature} + * @see {@link IndexSignature} + * @see {@link ObjectsMeta} + * + * @category models + * @since 4.0.0 + */ +export interface Objects { + readonly _tag: "Objects" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly propertySignatures: ReadonlyArray + readonly indexSignatures: ReadonlyArray + readonly checks: ReadonlyArray> +} + +/** + * A named property within an {@link Objects} representation. + * + * **Details** + * + * `name` is the property key, which can be a string, number, or symbol. + * `isOptional` indicates whether the key can be absent. `isMutable` indicates + * whether the property is mutable rather than readonly. + * + * @see {@link Objects} + * + * @category models + * @since 4.0.0 + */ +export interface PropertySignature { + readonly name: PropertyKey + readonly type: Representation + readonly isOptional: boolean + readonly isMutable: boolean + readonly annotations?: Schema.Annotations.Annotations | undefined +} + +/** + * An index signature, such as `[key: string]: number`, within an + * {@link Objects}. + * + * **Details** + * + * `parameter` is the key type representation. `type` is the value type + * representation. + * + * @see {@link Objects} + * + * @category models + * @since 4.0.0 + */ +export interface IndexSignature { + readonly parameter: Representation + readonly type: Representation +} + +/** + * A union of multiple representations. + * + * **Details** + * + * `types` are the union members. `mode` controls JSON Schema output as either + * `"anyOf"` (the default) or mutually exclusive `"oneOf"`. + * + * @category models + * @since 4.0.0 + */ +export interface Union { + readonly _tag: "Union" + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly types: ReadonlyArray + readonly mode: "anyOf" | "oneOf" +} + +/** + * The core tagged union of all supported schema shapes. + * + * **Details** + * + * Each variant has a `_tag` discriminator. Switch on `_tag` to handle each + * shape. Most variants carry optional `annotations` and some carry `checks` + * for validation constraints. + * + * @see {@link Document} + * @see {@link fromAST} + * + * @category models + * @since 4.0.0 + */ +export type Representation = + | Declaration + | Reference + | Suspend + | Null + | Undefined + | Void + | Never + | Unknown + | Any + | String + | Number + | Boolean + | BigInt + | Symbol + | Literal + | UniqueSymbol + | ObjectKeyword + | Enum + | TemplateLiteral + | Arrays + | Objects + | Union + +/** + * A validation constraint attached to a type. Either a single {@link Filter} + * or a {@link FilterGroup} combining multiple checks. + * + * @see {@link Filter} + * @see {@link FilterGroup} + * + * @category models + * @since 4.0.0 + */ +export type Check = Filter | FilterGroup + +/** + * A single validation constraint with typed metadata describing the check + * (e.g. `{ _tag: "isMinLength", minLength: 3 }`). + * + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export interface Filter { + readonly _tag: "Filter" + readonly annotations?: Schema.Annotations.Filter | undefined + readonly meta: M +} + +/** + * A group of validation constraints that are logically combined. Contains + * at least one {@link Check}. + * + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export interface FilterGroup { + readonly _tag: "FilterGroup" + readonly annotations?: Schema.Annotations.Filter | undefined + readonly checks: readonly [Check, ...Array>] +} + +/** + * Metadata union for string-specific validation checks (minLength, maxLength, + * pattern, UUID, trimmed, etc.). + * + * @see {@link String} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export type StringMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isStringFinite" + | "isStringBigInt" + | "isStringSymbol" + | "isMinLength" + | "isMaxLength" + | "isPattern" + | "isLengthBetween" + | "isTrimmed" + | "isUUID" + | "isGUID" + | "isULID" + | "isBase64" + | "isBase64Url" + | "isStartsWith" + | "isEndsWith" + | "isIncludes" + | "isUppercased" + | "isLowercased" + | "isCapitalized" + | "isUncapitalized" +] + +/** + * Metadata union for number-specific validation checks (int, finite, + * min, max, multipleOf, between). + * + * @see {@link Number} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export type NumberMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isInt" + | "isFinite" + | "isMultipleOf" + | "isGreaterThanOrEqualTo" + | "isLessThanOrEqualTo" + | "isGreaterThan" + | "isLessThan" + | "isBetween" +] + +/** + * Metadata union for bigint-specific validation checks (min, max, between). + * + * @see {@link BigInt} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export type BigIntMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isGreaterThanOrEqualToBigInt" + | "isLessThanOrEqualToBigInt" + | "isGreaterThanBigInt" + | "isLessThanBigInt" + | "isBetweenBigInt" +] + +/** + * Metadata union for array-specific validation checks (minLength, maxLength, + * length, unique). + * + * @see {@link Arrays} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export type ArraysMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isMinLength" + | "isMaxLength" + | "isLengthBetween" + | "isUnique" +] + +/** + * Metadata union for object-specific validation checks (minProperties, + * maxProperties, propertiesLength, propertyNames). + * + * @see {@link Objects} + * @see {@link Check} + * + * @category models + * @since 4.0.0 + */ +export type ObjectsMeta = + | Schema.Annotations.BuiltInMetaDefinitions[ + | "isMinProperties" + | "isMaxProperties" + | "isPropertiesLengthBetween" + ] + | { readonly _tag: "isPropertyNames"; readonly propertyNames: Representation } + +/** + * Metadata union for Date-specific validation checks (valid, min, max, between). + * + * @see {@link Declaration} + * @see {@link DeclarationMeta} + * + * @category models + * @since 4.0.0 + */ +export type DateMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isDateValid" + | "isGreaterThanDate" + | "isGreaterThanOrEqualToDate" + | "isLessThanDate" + | "isLessThanOrEqualToDate" + | "isBetweenDate" +] + +/** + * Metadata union for size-based validation checks (minSize, maxSize, size). + * Used for collection types like `Set`, `Map`. + * + * @see {@link Declaration} + * @see {@link DeclarationMeta} + * + * @category models + * @since 4.0.0 + */ +export type SizeMeta = Schema.Annotations.BuiltInMetaDefinitions[ + | "isMinSize" + | "isMaxSize" + | "isSizeBetween" +] + +/** + * Metadata union for {@link Declaration} checks — either {@link DateMeta} + * or {@link SizeMeta}. + * + * @category models + * @since 4.0.0 + */ +export type DeclarationMeta = DateMeta | SizeMeta + +/** @internal */ +export type Meta = StringMeta | NumberMeta | BigIntMeta | ArraysMeta | ObjectsMeta | DeclarationMeta + +/** + * A string-keyed map of named {@link Representation} definitions. Used by + * {@link Document} and {@link MultiDocument} for `$ref` resolution (analogous + * to JSON Schema `$defs`). + * + * @see {@link Reference} + * @see {@link Document} + * + * @category models + * @since 4.0.0 + */ +export interface References { + readonly [$ref: string]: Representation +} + +/** + * A single {@link Representation} together with its named {@link References}. + * + * **When to use** + * + * Use when representing a single Schema AST together with its named references + * before reconstructing a runtime Schema, converting to JSON Schema, or + * wrapping it as a {@link MultiDocument}. + * + * @see {@link MultiDocument} + * @see {@link fromAST} + * + * @category models + * @since 4.0.0 + */ +export type Document = { + readonly representation: Representation + readonly references: References +} + +/** + * One or more {@link Representation}s sharing a common {@link References} map. + * + * **When to use** + * + * Use when you use {@link fromASTs} to create this from multiple Schema ASTs, + * {@link toCodeDocument} to generate TypeScript code, and + * {@link toJsonSchemaMultiDocument} to convert to JSON Schema. + * + * @see {@link Document} + * @see {@link fromASTs} + * + * @category models + * @since 4.0.0 + */ +export type MultiDocument = { + readonly representations: readonly [Representation, ...Array] + readonly references: References +} + +// ----------------------------------------------------------------------------- +// schemas +// ----------------------------------------------------------------------------- + +const Representation$ref = Schema.suspend(() => $Representation) + +const toJsonAnnotationsBlacklist: Set = new Set([ + ...InternalRepresentation.fromASTBlacklist, + "expected", + "contentMediaType", + "contentSchema" +]) + +/** + * A tree of primitive values used to serialize annotations to JSON. + * + * @category Tree + * @since 4.0.0 + */ +export type PrimitiveTree = Schema.Tree + +/** + * Schema for {@link PrimitiveTree}. + * + * **When to use** + * + * Use to validate recursive annotation metadata trees whose leaves are `null`, + * `number`, `boolean`, `bigint`, `symbol`, or `string`. + * + * @see {@link PrimitiveTree} for the recursive tree type accepted by this codec + * @see {@link $Annotations} for the annotation codec that filters values through this codec + * + * @category schemas + * @since 4.0.0 + */ +export const $PrimitiveTree: Schema.Codec = Schema.Tree( + Schema.Union([ + Schema.Null, + Schema.Number, // allows NaN, Infinity, -Infinity + Schema.Boolean, + Schema.BigInt, + Schema.Symbol, + Schema.String + ]) +) + +const isPrimitiveTree = Schema.is($PrimitiveTree) + +/** + * Schema for serializing public `Schema.Annotations.Annotations` values. It + * filters out internal annotation keys and non-primitive values during + * encoding. + * + * **When to use** + * + * Use to serialize schema annotations in representation schemas while retaining + * only primitive-tree metadata. + * + * **Details** + * + * Decoding is passthrough. Encoding removes internal annotation keys and values + * that are not accepted by `$PrimitiveTree`. + * + * @see {@link $PrimitiveTree} for the codec used to filter annotation values + * + * @category schemas + * @since 4.0.0 + */ +export const $Annotations = Schema.Record(Schema.String, Schema.Unknown).pipe( + Schema.encodeTo(Schema.Record(Schema.String, $PrimitiveTree), { + decode: SchemaGetter.passthrough(), + encode: SchemaGetter.transformOptional(Option.flatMap((r) => { + const out: Record = {} + for (const [k, v] of Object.entries(r)) { + if (!toJsonAnnotationsBlacklist.has(k) && isPrimitiveTree(v)) { + out[k] = v + } + } + return Rec.isEmptyRecord(out) ? Option.none() : Option.some(out) + })) + }) +).annotate({ identifier: "Annotations" }) + +/** + * Schema for the {@link Null} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Null = Schema.Struct({ + _tag: Schema.tag("Null"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Null" }) + +/** + * Schema for the {@link Undefined} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Undefined = Schema.Struct({ + _tag: Schema.tag("Undefined"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Undefined" }) + +/** + * Schema for the {@link Void} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Void = Schema.Struct({ + _tag: Schema.tag("Void"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Void" }) + +/** + * Schema for the {@link Never} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Never = Schema.Struct({ + _tag: Schema.tag("Never"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Never" }) + +/** + * Schema for the {@link Unknown} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Unknown = Schema.Struct({ + _tag: Schema.tag("Unknown"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Unknown" }) + +/** + * Schema for the {@link Any} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Any = Schema.Struct({ + _tag: Schema.tag("Any"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Any" }) + +const $IsStringFinite = Schema.Struct({ + _tag: Schema.tag("isStringFinite"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsStringFinite" }) + +const $IsStringBigInt = Schema.Struct({ + _tag: Schema.tag("isStringBigInt"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsStringBigInt" }) + +const $IsStringSymbol = Schema.Struct({ + _tag: Schema.tag("isStringSymbol"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsStringSymbol" }) + +const $IsTrimmed = Schema.Struct({ + _tag: Schema.tag("isTrimmed"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsTrimmed" }) + +const $IsUUID = Schema.Struct({ + _tag: Schema.tag("isUUID"), + regExp: Schema.RegExp, + version: Schema.UndefinedOr(Schema.Literals([1, 2, 3, 4, 5, 6, 7, 8])) +}).annotate({ identifier: "IsUUID" }) + +const $IsGUID = Schema.Struct({ + _tag: Schema.tag("isGUID"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsGUID" }) + +const $IsULID = Schema.Struct({ + _tag: Schema.tag("isULID"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsULID" }) + +const $IsBase64 = Schema.Struct({ + _tag: Schema.tag("isBase64"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsBase64" }) + +const $IsBase64Url = Schema.Struct({ + _tag: Schema.tag("isBase64Url"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsBase64Url" }) + +const $IsStartsWith = Schema.Struct({ + _tag: Schema.tag("isStartsWith"), + startsWith: Schema.String, + regExp: Schema.RegExp +}).annotate({ identifier: "IsStartsWith" }) + +const $IsEndsWith = Schema.Struct({ + _tag: Schema.tag("isEndsWith"), + endsWith: Schema.String, + regExp: Schema.RegExp +}).annotate({ identifier: "IsEndsWith" }) + +const $IsIncludes = Schema.Struct({ + _tag: Schema.tag("isIncludes"), + includes: Schema.String, + regExp: Schema.RegExp +}).annotate({ identifier: "IsIncludes" }) + +const $IsUppercased = Schema.Struct({ + _tag: Schema.tag("isUppercased"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsUppercased" }) + +const $IsLowercased = Schema.Struct({ + _tag: Schema.tag("isLowercased"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsLowercased" }) + +const $IsCapitalized = Schema.Struct({ + _tag: Schema.tag("isCapitalized"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsCapitalized" }) + +const $IsUncapitalized = Schema.Struct({ + _tag: Schema.tag("isUncapitalized"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsUncapitalized" }) + +const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) + +const $IsMinLength = Schema.Struct({ + _tag: Schema.tag("isMinLength"), + minLength: NonNegativeInt +}).annotate({ identifier: "IsMinLength" }) + +const $IsMaxLength = Schema.Struct({ + _tag: Schema.tag("isMaxLength"), + maxLength: NonNegativeInt +}).annotate({ identifier: "IsMaxLength" }) + +const $IsLengthBetween = Schema.Struct({ + _tag: Schema.tag("isLengthBetween"), + minimum: NonNegativeInt, + maximum: NonNegativeInt +}).annotate({ identifier: "IsLengthBetween" }) + +const $IsPattern = Schema.Struct({ + _tag: Schema.tag("isPattern"), + regExp: Schema.RegExp +}).annotate({ identifier: "IsPattern" }) + +/** + * Schema for {@link StringMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $StringMeta = Schema.Union([ + $IsStringFinite, + $IsStringBigInt, + $IsStringSymbol, + $IsTrimmed, + $IsUUID, + $IsGUID, + $IsULID, + $IsBase64, + $IsBase64Url, + $IsStartsWith, + $IsEndsWith, + $IsIncludes, + $IsUppercased, + $IsLowercased, + $IsCapitalized, + $IsUncapitalized, + $IsMinLength, + $IsMaxLength, + $IsPattern, + $IsLengthBetween +]).annotate({ identifier: "StringMeta" }) + +function makeCheck(meta: Schema.Codec, identifier: string) { + const Check$ref = Schema.suspend(() => Check) + const Check: Schema.Codec> = Schema.Union([ + Schema.Struct({ + _tag: Schema.tag("Filter"), + annotations: Schema.optional($Annotations), + meta + }).annotate({ identifier: `${identifier}Filter` }), + Schema.Struct({ + _tag: Schema.tag("FilterGroup"), + annotations: Schema.optional($Annotations), + checks: Schema.NonEmptyArray(Check$ref) + }).annotate({ identifier: `${identifier}FilterGroup` }) + ]).annotate({ identifier: `${identifier}Check` }) + return Check +} + +/** + * Schema for the {@link String} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $String = Schema.Struct({ + _tag: Schema.tag("String"), + annotations: Schema.optional($Annotations), + checks: Schema.Array(makeCheck($StringMeta, "String")), + contentMediaType: Schema.optional(Schema.String), + contentSchema: Schema.optional(Representation$ref) +}).annotate({ identifier: "String" }) + +const $IsInt = Schema.Struct({ + _tag: Schema.tag("isInt") +}).annotate({ identifier: "IsInt" }) + +const $IsMultipleOf = Schema.Struct({ + _tag: Schema.tag("isMultipleOf"), + divisor: Schema.Finite +}).annotate({ identifier: "IsMultipleOf" }) + +const $IsFinite = Schema.Struct({ + _tag: Schema.tag("isFinite") +}).annotate({ identifier: "IsFinite" }) + +const $IsGreaterThan = Schema.Struct({ + _tag: Schema.tag("isGreaterThan"), + exclusiveMinimum: Schema.Finite +}).annotate({ identifier: "IsGreaterThan" }) + +const $IsGreaterThanOrEqualTo = Schema.Struct({ + _tag: Schema.tag("isGreaterThanOrEqualTo"), + minimum: Schema.Finite +}).annotate({ identifier: "IsGreaterThanOrEqualTo" }) + +const $IsLessThan = Schema.Struct({ + _tag: Schema.tag("isLessThan"), + exclusiveMaximum: Schema.Finite +}).annotate({ identifier: "IsLessThan" }) + +const $IsLessThanOrEqualTo = Schema.Struct({ + _tag: Schema.tag("isLessThanOrEqualTo"), + maximum: Schema.Finite +}).annotate({ identifier: "IsLessThanOrEqualTo" }) + +const $IsBetween = Schema.Struct({ + _tag: Schema.tag("isBetween"), + minimum: Schema.Finite, + maximum: Schema.Finite, + exclusiveMinimum: Schema.optional(Schema.Boolean), + exclusiveMaximum: Schema.optional(Schema.Boolean) +}).annotate({ identifier: "IsBetween" }) + +/** + * Schema for {@link NumberMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $NumberMeta = Schema.Union([ + $IsInt, + $IsMultipleOf, + $IsFinite, + $IsGreaterThan, + $IsGreaterThanOrEqualTo, + $IsLessThan, + $IsLessThanOrEqualTo, + $IsBetween +]).annotate({ identifier: "NumberMeta" }) + +/** + * Schema for the {@link Number} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Number = Schema.Struct({ + _tag: Schema.tag("Number"), + annotations: Schema.optional($Annotations), + checks: Schema.Array(makeCheck($NumberMeta, "Number")) +}).annotate({ identifier: "Number" }) + +/** + * Schema for the {@link Boolean} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Boolean = Schema.Struct({ + _tag: Schema.tag("Boolean"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Boolean" }) + +const $IsGreaterThanBigInt = Schema.Struct({ + _tag: Schema.tag("isGreaterThanBigInt"), + exclusiveMinimum: Schema.BigInt +}).annotate({ identifier: "IsGreaterThanBigInt" }) + +const $IsGreaterThanOrEqualToBigInt = Schema.Struct({ + _tag: Schema.tag("isGreaterThanOrEqualToBigInt"), + minimum: Schema.BigInt +}).annotate({ identifier: "IsGreaterThanOrEqualToBigInt" }) + +const $IsLessThanBigInt = Schema.Struct({ + _tag: Schema.tag("isLessThanBigInt"), + exclusiveMaximum: Schema.BigInt +}).annotate({ identifier: "IsLessThanBigInt" }) + +const $IsLessThanOrEqualToBigInt = Schema.Struct({ + _tag: Schema.tag("isLessThanOrEqualToBigInt"), + maximum: Schema.BigInt +}).annotate({ identifier: "IsLessThanOrEqualToBigInt" }) + +const $IsBetweenBigInt = Schema.Struct({ + _tag: Schema.tag("isBetweenBigInt"), + minimum: Schema.BigInt, + maximum: Schema.BigInt, + exclusiveMinimum: Schema.optional(Schema.Boolean), + exclusiveMaximum: Schema.optional(Schema.Boolean) +}).annotate({ identifier: "IsBetweenBigInt" }) + +const $BigIntMeta = Schema.Union([ + $IsGreaterThanBigInt, + $IsGreaterThanOrEqualToBigInt, + $IsLessThanBigInt, + $IsLessThanOrEqualToBigInt, + $IsBetweenBigInt +]).annotate({ identifier: "BigIntMeta" }) + +/** + * Schema for the {@link BigInt} representation node. + * + * **When to use** + * + * Use to encode, decode, or validate serialized `BigInt` representation nodes, + * not application `bigint` values. + * + * **Details** + * + * Accepts representation nodes with `_tag: "BigInt"`, optional annotations, + * and bigint-specific validation metadata in `checks`. + * + * @see {@link BigIntMeta} for the metadata accepted by the `checks` array + * + * @category schemas + * @since 4.0.0 + */ +export const $BigInt = Schema.Struct({ + _tag: Schema.tag("BigInt"), + annotations: Schema.optional($Annotations), + checks: Schema.Array(makeCheck($BigIntMeta, "BigInt")) +}).annotate({ identifier: "BigInt" }) + +/** + * Schema for the {@link Symbol} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Symbol = Schema.Struct({ + _tag: Schema.tag("Symbol"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Symbol" }) + +/** + * Schema for the literal value types allowed in a {@link Literal} node + * (string, finite number, boolean, or bigint). + * + * @category schemas + * @since 4.0.0 + */ +export const $LiteralValue = Schema.Union([ + Schema.String, + Schema.Finite, + Schema.Boolean, + Schema.BigInt +]).annotate({ identifier: "LiteralValue" }) + +/** + * Schema for the {@link Literal} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Literal = Schema.Struct({ + _tag: Schema.tag("Literal"), + annotations: Schema.optional($Annotations), + literal: $LiteralValue +}).annotate({ identifier: "Literal" }) + +/** + * Schema for the {@link UniqueSymbol} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $UniqueSymbol = Schema.Struct({ + _tag: Schema.tag("UniqueSymbol"), + annotations: Schema.optional($Annotations), + symbol: Schema.Symbol +}).annotate({ identifier: "UniqueSymbol" }) + +/** + * Schema for the {@link ObjectKeyword} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $ObjectKeyword = Schema.Struct({ + _tag: Schema.tag("ObjectKeyword"), + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "ObjectKeyword" }) + +/** + * Schema for the {@link Enum} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Enum = Schema.Struct({ + _tag: Schema.tag("Enum"), + annotations: Schema.optional($Annotations), + enums: Schema.Array( + Schema.Tuple([ + Schema.String, + Schema.Union([ + Schema.String, + Schema.Number // NaN, Infinity, -Infinity are allowed enum values + ]) + ]) + ) +}).annotate({ identifier: "Enum" }) + +/** + * Schema for the {@link TemplateLiteral} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $TemplateLiteral = Schema.Struct({ + _tag: Schema.tag("TemplateLiteral"), + annotations: Schema.optional($Annotations), + parts: Schema.Array(Representation$ref) +}).annotate({ identifier: "TemplateLiteral" }) + +/** + * Schema for the {@link Element} type (positional tuple element). + * + * @category schemas + * @since 4.0.0 + */ +export const $Element = Schema.Struct({ + isOptional: Schema.Boolean, + type: Representation$ref, + annotations: Schema.optional($Annotations) +}).annotate({ identifier: "Element" }) + +const $IsUnique = Schema.Struct({ + _tag: Schema.tag("isUnique") +}).annotate({ identifier: "IsUnique" }) + +const $ArraysMeta = Schema.Union([ + $IsMinLength, + $IsMaxLength, + $IsLengthBetween, + $IsUnique +]).annotate({ identifier: "ArraysMeta" }) + +/** + * Schema for the {@link Arrays} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Arrays = Schema.Struct({ + _tag: Schema.tag("Arrays"), + annotations: Schema.optional($Annotations), + elements: Schema.Array($Element), + rest: Schema.Array(Representation$ref), + checks: Schema.Array(makeCheck($ArraysMeta, "Arrays")) +}).annotate({ identifier: "Arrays" }) + +/** + * Schema for the {@link PropertySignature} type. + * + * @category schemas + * @since 4.0.0 + */ +export const $PropertySignature = Schema.Struct({ + annotations: Schema.optional($Annotations), + name: Schema.PropertyKey, + type: Representation$ref, + isOptional: Schema.Boolean, + isMutable: Schema.Boolean +}).annotate({ identifier: "PropertySignature" }) + +/** + * Schema for the {@link IndexSignature} type. + * + * @category schemas + * @since 4.0.0 + */ +export const $IndexSignature = Schema.Struct({ + parameter: Representation$ref, + type: Representation$ref +}).annotate({ identifier: "IndexSignature" }) + +const $IsMinProperties = Schema.Struct({ + _tag: Schema.tag("isMinProperties"), + minProperties: NonNegativeInt +}).annotate({ identifier: "IsMinProperties" }) + +const $IsMaxProperties = Schema.Struct({ + _tag: Schema.tag("isMaxProperties"), + maxProperties: NonNegativeInt +}).annotate({ identifier: "IsMaxProperties" }) + +const $IsPropertiesLengthBetween = Schema.Struct({ + _tag: Schema.tag("isPropertiesLengthBetween"), + minimum: NonNegativeInt, + maximum: NonNegativeInt +}).annotate({ identifier: "IsPropertiesLengthBetween" }) + +const $IsPropertyNames = Schema.Struct({ + _tag: Schema.tag("isPropertyNames"), + propertyNames: Representation$ref +}).annotate({ identifier: "IsPropertyNames" }) + +/** + * Schema for {@link ObjectsMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $ObjectsMeta = Schema.Union([ + $IsMinProperties, + $IsMaxProperties, + $IsPropertiesLengthBetween, + $IsPropertyNames +]).annotate({ identifier: "ObjectsMeta" }) + +/** + * Schema for the {@link Objects} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Objects = Schema.Struct({ + _tag: Schema.tag("Objects"), + annotations: Schema.optional($Annotations), + propertySignatures: Schema.Array($PropertySignature), + indexSignatures: Schema.Array($IndexSignature), + checks: Schema.Array(makeCheck($ObjectsMeta, "Objects")) +}).annotate({ identifier: "Objects" }) + +/** + * Schema for the {@link Union} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Union = Schema.Struct({ + _tag: Schema.tag("Union"), + annotations: Schema.optional($Annotations), + types: Schema.Array(Representation$ref), + mode: Schema.Literals(["anyOf", "oneOf"]) +}).annotate({ identifier: "Union" }) + +/** + * Schema for the {@link Reference} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Reference = Schema.Struct({ + _tag: Schema.tag("Reference"), + $ref: Schema.String +}).annotate({ identifier: "Reference" }) + +const $IsDateValid = Schema.Struct({ + _tag: Schema.tag("isDateValid") +}).annotate({ identifier: "IsDateValid" }) + +const $IsGreaterThanDate = Schema.Struct({ + _tag: Schema.tag("isGreaterThanDate"), + exclusiveMinimum: Schema.Date +}).annotate({ identifier: "IsGreaterThanDate" }) + +const $IsGreaterThanOrEqualToDate = Schema.Struct({ + _tag: Schema.tag("isGreaterThanOrEqualToDate"), + minimum: Schema.Date +}).annotate({ identifier: "IsGreaterThanOrEqualToDate" }) + +const $IsLessThanDate = Schema.Struct({ + _tag: Schema.tag("isLessThanDate"), + exclusiveMaximum: Schema.Date +}).annotate({ identifier: "IsLessThanDate" }) + +const $IsLessThanOrEqualToDate = Schema.Struct({ + _tag: Schema.tag("isLessThanOrEqualToDate"), + maximum: Schema.Date +}).annotate({ identifier: "IsLessThanOrEqualToDate" }) + +const $IsBetweenDate = Schema.Struct({ + _tag: Schema.tag("isBetweenDate"), + minimum: Schema.Date, + maximum: Schema.Date, + exclusiveMinimum: Schema.optional(Schema.Boolean), + exclusiveMaximum: Schema.optional(Schema.Boolean) +}).annotate({ identifier: "IsBetweenDate" }) + +/** + * Schema for {@link DateMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $DateMeta = Schema.Union([ + $IsDateValid, + $IsGreaterThanDate, + $IsGreaterThanOrEqualToDate, + $IsLessThanDate, + $IsLessThanOrEqualToDate, + $IsBetweenDate +]).annotate({ identifier: "DateMeta" }) + +const $IsMinSize = Schema.Struct({ + _tag: Schema.tag("isMinSize"), + minSize: NonNegativeInt +}).annotate({ identifier: "IsMinSize" }) + +const $IsMaxSize = Schema.Struct({ + _tag: Schema.tag("isMaxSize"), + maxSize: NonNegativeInt +}).annotate({ identifier: "IsMaxSize" }) + +const $IsSizeBetween = Schema.Struct({ + _tag: Schema.tag("isSizeBetween"), + minimum: NonNegativeInt, + maximum: NonNegativeInt +}).annotate({ identifier: "IsSizeBetween" }) + +/** + * Schema for {@link SizeMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $SizeMeta = Schema.Union([ + $IsMinSize, + $IsMaxSize, + $IsSizeBetween +]).annotate({ identifier: "SizeMeta" }) + +/** + * Schema for {@link DeclarationMeta}. + * + * @category schemas + * @since 4.0.0 + */ +export const $DeclarationMeta = Schema.Union([ + $DateMeta, + $SizeMeta +]).annotate({ identifier: "DeclarationMeta" }) + +/** + * Schema for the {@link Declaration} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Declaration = Schema.Struct({ + _tag: Schema.tag("Declaration"), + annotations: Schema.optional($Annotations), + typeParameters: Schema.Array(Representation$ref), + checks: Schema.Array(makeCheck($DeclarationMeta, "Declaration")), + encodedSchema: Representation$ref +}).annotate({ identifier: "Declaration" }) + +/** + * Schema for the {@link Suspend} representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Suspend = Schema.Struct({ + _tag: Schema.tag("Suspend"), + annotations: Schema.optional($Annotations), + checks: Schema.Tuple([]), + thunk: Representation$ref +}).annotate({ identifier: "Suspend" }) + +/** + * Type-level helper for the recursive {@link $Representation} codec. + * + * @category schemas + * @since 4.0.0 + */ +export interface $Representation extends Schema.Codec {} + +/** + * Schema for the full {@link Representation} union. It recursively validates + * and encodes any representation node. + * + * @category schemas + * @since 4.0.0 + */ +export const $Representation: $Representation = Schema.Union([ + $Null, + $Undefined, + $Void, + $Never, + $Unknown, + $Any, + $String, + $Number, + $Boolean, + $BigInt, + $Symbol, + $Literal, + $UniqueSymbol, + $ObjectKeyword, + $Enum, + $TemplateLiteral, + $Arrays, + $Objects, + $Union, + $Reference, + $Declaration, + $Suspend +]).annotate({ identifier: "Schema" }) + +/** + * Schema for {@link Document}. + * + * **When to use** + * + * Use to validate or serialize a single schema representation document with + * `Schema.decodeUnknownSync` or `Schema.encodeSync`. + * + * **Gotchas** + * + * This codec validates document structure but does not resolve `$ref` keys + * against `references`. + * + * @see {@link DocumentFromJson} for the JSON-string codec wrapper + * @see {@link $MultiDocument} for validating documents with multiple root representations + * + * @category schemas + * @since 4.0.0 + */ +export const $Document = Schema.Struct({ + representation: $Representation, + references: Schema.Record(Schema.String, $Representation) +}).annotate({ identifier: "Document" }) + +/** + * Schema for {@link MultiDocument}. + * + * @category schemas + * @since 4.0.0 + */ +export const $MultiDocument = Schema.Struct({ + representations: Schema.NonEmptyArray($Representation), + references: Schema.Record(Schema.String, $Representation) +}).annotate({ identifier: "MultiDocument" }) + +// ----------------------------------------------------------------------------- +// APIs +// ----------------------------------------------------------------------------- + +/** + * Converts a Schema AST into a {@link Document}. + * + * **When to use** + * + * Use when you have a single Schema AST and need a schema representation + * document. + * + * **Details** + * + * Shared/recursive sub-schemas are extracted into the `references` map. + * + * **Example** (Converting a Schema to a Document) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const Person = Schema.Struct({ + * name: Schema.String, + * age: Schema.Number + * }) + * + * const doc = SchemaRepresentation.fromAST(Person.ast) + * console.log(doc.representation._tag) + * // "Objects" + * ``` + * + * @see {@link Document} + * @see {@link fromASTs} + * + * @category constructors + * @since 4.0.0 + */ +export const fromAST: (ast: SchemaAST.AST) => Document = InternalRepresentation.fromAST + +/** + * Converts one or more Schema ASTs into a {@link MultiDocument}. + * + * **When to use** + * + * Use when you have multiple Schema ASTs and need one schema representation + * `MultiDocument` with shared references. + * + * **Details** + * + * All schemas share a single `references` map. + * + * @see {@link MultiDocument} + * @see {@link fromAST} + * + * @category constructors + * @since 4.0.0 + */ +export const fromASTs: (asts: readonly [SchemaAST.AST, ...Array]) => MultiDocument = + InternalRepresentation.fromASTs + +/** + * Schema that decodes a {@link Document} from JSON and encodes it back. + * + * **When to use** + * + * Use when you need a JSON codec for schema representation documents with + * `Schema.decodeUnknownSync` or `Schema.encodeSync`. + * + * **Example** (Round-tripping a Document through JSON) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const doc = SchemaRepresentation.fromAST(Schema.String.ast) + * const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)(doc) + * const back = Schema.decodeUnknownSync(SchemaRepresentation.DocumentFromJson)(json) + * ``` + * + * @see {@link $Document} + * @see {@link MultiDocumentFromJson} + * + * @category schemas + * @since 4.0.0 + */ +export const DocumentFromJson: Schema.Codec = Schema.toCodecJson($Document) + +/** + * Schema for `MultiDocument` values encoded as JSON. + * + * @see {@link $MultiDocument} + * @see {@link DocumentFromJson} + * + * @category schemas + * @since 4.0.0 + */ +export const MultiDocumentFromJson: Schema.Codec = Schema.toCodecJson($MultiDocument) + +/** + * Wraps a single {@link Document} as a {@link MultiDocument} with one + * representation. + * + * **When to use** + * + * Use when you need to pass a single schema representation `Document` where an + * API expects a `MultiDocument`. + * + * @see {@link Document} + * @see {@link MultiDocument} + * + * @category transforming + * @since 4.0.0 + */ +export function toMultiDocument(document: Document): MultiDocument { + return { + representations: [document.representation], + references: document.references + } +} + +/** + * A callback that handles {@link Declaration} nodes during reconstruction + * ({@link toSchema}) or code generation ({@link toCodeDocument}). + * + * **Details** + * + * Return a value to handle the declaration. Return `undefined` to fall back to + * default behavior, which uses `encodedSchema` for `toSchema` or the + * `generation` annotation for `toCodeDocument`. `recur` processes child + * representations recursively. + * + * @see {@link toSchema} + * @see {@link toSchemaDefaultReviver} + * @see {@link toCodeDocument} + * + * @category models + * @since 4.0.0 + */ +export type Reviver = (declaration: Declaration, recur: (representation: Representation) => T) => T | undefined + +/** + * Default {@link Reviver} for {@link toSchema} that handles built-in Effect + * types, including Option, Result, Redacted, Cause, Exit, ReadonlyMap, HashMap, + * ReadonlySet, Date, Duration, URL, and RegExp. + * + * **When to use** + * + * Use when you need the default `options.reviver` for {@link toSchema} to + * reconstruct runtime schemas for built-in Effect declarations. + * + * **Details** + * + * The reviver returns `undefined` for unrecognized declarations, causing + * fallback to `encodedSchema`. + * + * @see {@link toSchema} + * @see {@link Reviver} + * + * @category transforming + * @since 4.0.0 + */ +export const toSchemaDefaultReviver: Reviver = (s, recur) => { + const typeConstructor = s.annotations?.typeConstructor + if (Predicate.isObject(typeConstructor) && typeof typeConstructor._tag === "string") { + const typeParameters = s.typeParameters.map(recur) + switch (typeConstructor._tag) { + // built-in types + case "Date": + return Schema.Date + case "Error": + return Schema.Error(typeConstructor.options as Schema.ErrorOptions | undefined) + case "File": + return Schema.File + case "FormData": + return Schema.FormData + case "ReadonlyMap": + return Schema.ReadonlyMap(typeParameters[0], typeParameters[1]) + case "ReadonlySet": + return Schema.ReadonlySet(typeParameters[0]) + case "RegExp": + return Schema.RegExp + case "Uint8Array": + return Schema.Uint8Array + case "URL": + return Schema.URL + case "URLSearchParams": + return Schema.URLSearchParams + // effect types + case "effect/Option": + return Schema.Option(typeParameters[0]) + case "effect/Result": + return Schema.Result(typeParameters[0], typeParameters[1]) + case "effect/Redacted": + return Schema.Redacted(typeParameters[0], typeConstructor.options as any) + case "effect/DateTime.TimeZone": + return Schema.TimeZone + case "effect/DateTime.TimeZone.Named": + return Schema.TimeZoneNamed + case "effect/DateTime.TimeZone.Offset": + return Schema.TimeZoneOffset + case "effect/DateTime.Utc": + return Schema.DateTimeUtc + case "effect/DateTime.Zoned": + return Schema.DateTimeZoned + case "effect/BigDecimal": + return Schema.BigDecimal + case "effect/Chunk": + return Schema.Chunk(typeParameters[0]) + case "effect/Cause": + return Schema.Cause(typeParameters[0], typeParameters[1]) + case "effect/Cause/Failure": + return Schema.CauseReason(typeParameters[0], typeParameters[1]) + case "effect/Duration": + return Schema.Duration + case "effect/Exit": + return Schema.Exit(typeParameters[0], typeParameters[1], typeParameters[2]) + case "effect/Json": + return Schema.Json + case "effect/MutableJson": + return Schema.MutableJson + case "effect/HashMap": + return Schema.HashMap(typeParameters[0], typeParameters[1]) + case "effect/HashSet": + return Schema.HashSet(typeParameters[0]) + } + } +} + +/** + * Creates a runtime Schema from a {@link Document}. + * + * **When to use** + * + * Use when you have a serialized or computed schema representation document and + * need a runtime Schema for decoding/encoding. + * + * **Details** + * + * Pass `options.reviver`, such as {@link toSchemaDefaultReviver}, to handle + * {@link Declaration} nodes for types like `Date` and `Option`. Without a + * reviver, declarations fall back to their `encodedSchema`. Circular references + * are handled via lazy `Schema.suspend`. + * + * **Gotchas** + * + * This throws if a `$ref` is not found in `document.references`. + * + * **Example** (Reconstructing a Schema) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const doc = SchemaRepresentation.fromAST( + * Schema.Struct({ name: Schema.String }).ast + * ) + * + * const schema = SchemaRepresentation.toSchema(doc) + * console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2)) + * ``` + * + * @see {@link Document} + * @see {@link Reviver} + * @see {@link toSchemaDefaultReviver} + * + * @category Runtime Generation + * @since 4.0.0 + */ +export function toSchema(document: Document, options?: { + readonly reviver?: Reviver | undefined +}): S { + type Slot = { + // 0 = not started, 1 = building, 2 = done + state: 0 | 1 | 2 + value: Schema.Top | undefined + ref: Schema.Top + } + + const slots = new Map() + + return recur(document.representation) as S + + function recur(r: Representation): Schema.Top { + let out = on(r) + if ("annotations" in r && r.annotations) out = out.annotate(r.annotations) + out = toSchemaChecks(out, r) + return out + } + + function getSlot(identifier: string): Slot { + const existing = slots.get(identifier) + if (existing) return existing + + // Create the slot *before* resolving, so self-references can see it. + const slot: Slot = { + state: 0, + value: undefined, + ref: Schema.suspend(() => { + if (slot.value === undefined) { + return Schema.Unknown + } + return slot.value + }) + } + slots.set(identifier, slot) + return slot + } + + function resolveReference($ref: string): Schema.Top { + const definition = document.references[$ref] + if (definition === undefined) { + throw new Error(`Reference ${$ref} not found`) + } + + const slot = getSlot($ref) + + if (slot.state === 2) { + // Already built: return the built schema directly + return slot.value! + } + + if (slot.state === 1) { + // Circular: we're currently building this identifier. + return slot.ref + } + + // First time: build it. + slot.state = 1 + try { + slot.value = recur(definition) + slot.state = 2 + return slot.value + } catch (e) { + // Leave the slot in a safe state so future thunks don't silently succeed. + slot.state = 0 + slot.value = undefined + throw e + } + } + + function on(r: Representation): Schema.Top { + switch (r._tag) { + case "Declaration": + return options?.reviver?.(r, recur) ?? recur(r.encodedSchema) + case "Reference": + return resolveReference(r.$ref) + case "Suspend": + return recur(r.thunk) + case "Null": + return Schema.Null + case "Undefined": + return Schema.Undefined + case "Void": + return Schema.Void + case "Never": + return Schema.Never + case "Unknown": + return Schema.Unknown + case "Any": + return Schema.Any + case "String": { + const contentMediaType = r.contentMediaType + const contentSchema = r.contentSchema + if (contentMediaType === "application/json" && contentSchema !== undefined) { + return Schema.fromJsonString(recur(contentSchema)) + } + return Schema.String + } + case "Number": + return Schema.Number + case "Boolean": + return Schema.Boolean + case "BigInt": + return Schema.BigInt + case "Symbol": + return Schema.Symbol + case "Literal": + return Schema.Literal(r.literal) + case "UniqueSymbol": + return Schema.UniqueSymbol(r.symbol) + case "ObjectKeyword": + return Schema.ObjectKeyword + case "Enum": + return Schema.Enum(Object.fromEntries(r.enums)) + case "TemplateLiteral": { + const parts = r.parts.map(recur) as Schema.TemplateLiteral.Parts + return Schema.TemplateLiteral(parts) + } + case "Arrays": { + const elements = r.elements.map((e) => { + const s = recur(e.type) + return e.isOptional ? Schema.optionalKey(s) : s + }) + const rest = r.rest.map(recur) + if (Arr.isArrayNonEmpty(rest)) { + if (r.elements.length === 0 && r.rest.length === 1) { + return Schema.Array(rest[0]) + } + return Schema.TupleWithRest(Schema.Tuple(elements), rest) + } + return Schema.Tuple(elements) + } + case "Objects": { + const fields: Record = {} + + for (const ps of r.propertySignatures) { + const s = recur(ps.type) + const withOptional = ps.isOptional ? Schema.optionalKey(s) : s + fields[ps.name] = ps.isMutable ? Schema.mutableKey(withOptional) : withOptional + } + + const indexSignatures = r.indexSignatures.map((is) => + Schema.Record(recur(is.parameter) as Schema.Record.Key, recur(is.type)) + ) + + if (Arr.isArrayNonEmpty(indexSignatures)) { + if (r.propertySignatures.length === 0 && indexSignatures.length === 1) { + return indexSignatures[0] + } + return Schema.StructWithRest(Schema.Struct(fields), indexSignatures) + } + + return Schema.Struct(fields) + } + case "Union": { + if (r.types.length === 0) return Schema.Never + if (r.types.every((t) => t._tag === "Literal")) { + if (r.types.length === 1) { + return Schema.Literal(r.types[0].literal) + } + return Schema.Literals(r.types.map((t) => t.literal)) + } + return Schema.Union(r.types.map(recur), { mode: r.mode }) + } + } + } + + function toSchemaChecks(top: Schema.Top, schema: Representation): Schema.Top { + switch (schema._tag) { + default: + return top + case "String": + case "Number": + case "BigInt": + case "Arrays": + case "Objects": + case "Declaration": { + const checks = schema.checks.map(toSchemaCheck) + return Arr.isArrayNonEmpty(checks) ? top.check(...checks) : top + } + } + } + + function toSchemaCheck(check: Check): SchemaAST.Check { + switch (check._tag) { + case "Filter": + return toSchemaFilter(check) + case "FilterGroup": { + return Schema.makeFilterGroup(Arr.map(check.checks, toSchemaCheck), check.annotations) + } + } + } + + function toSchemaFilter(filter: Filter): SchemaAST.Check { + const a = filter.annotations + switch (filter.meta._tag) { + // String Meta + case "isStringFinite": + return Schema.isStringFinite(a) + case "isStringBigInt": + return Schema.isStringBigInt(a) + case "isStringSymbol": + return Schema.isStringSymbol(a) + case "isMinLength": + return Schema.isMinLength(filter.meta.minLength, a) + case "isMaxLength": + return Schema.isMaxLength(filter.meta.maxLength, a) + case "isLengthBetween": + return Schema.isLengthBetween(filter.meta.minimum, filter.meta.maximum, a) + case "isPattern": + return Schema.isPattern(filter.meta.regExp, a) + case "isTrimmed": + return Schema.isTrimmed(a) + case "isUUID": + return Schema.isUUID(filter.meta.version, a) + case "isGUID": + return Schema.isGUID(a) + case "isULID": + return Schema.isULID(a) + case "isBase64": + return Schema.isBase64(a) + case "isBase64Url": + return Schema.isBase64Url(a) + case "isStartsWith": + return Schema.isStartsWith(filter.meta.startsWith, a) + case "isEndsWith": + return Schema.isEndsWith(filter.meta.endsWith, a) + case "isIncludes": + return Schema.isIncludes(filter.meta.includes, a) + case "isUppercased": + return Schema.isUppercased(a) + case "isLowercased": + return Schema.isLowercased(a) + case "isCapitalized": + return Schema.isCapitalized(a) + case "isUncapitalized": + return Schema.isUncapitalized(a) + + // Number Meta + case "isFinite": + return Schema.isFinite(a) + case "isInt": + return Schema.isInt(a) + case "isMultipleOf": + return Schema.isMultipleOf(filter.meta.divisor, a) + case "isGreaterThan": + return Schema.isGreaterThan(filter.meta.exclusiveMinimum, a) + case "isGreaterThanOrEqualTo": + return Schema.isGreaterThanOrEqualTo(filter.meta.minimum, a) + case "isLessThan": + return Schema.isLessThan(filter.meta.exclusiveMaximum, a) + case "isLessThanOrEqualTo": + return Schema.isLessThanOrEqualTo(filter.meta.maximum, a) + case "isBetween": + return Schema.isBetween(filter.meta, a) + + // BigInt Meta + case "isGreaterThanBigInt": + return Schema.isGreaterThanBigInt(filter.meta.exclusiveMinimum, a) + case "isGreaterThanOrEqualToBigInt": + return Schema.isGreaterThanOrEqualToBigInt(filter.meta.minimum, a) + case "isLessThanBigInt": + return Schema.isLessThanBigInt(filter.meta.exclusiveMaximum, a) + case "isLessThanOrEqualToBigInt": + return Schema.isLessThanOrEqualToBigInt(filter.meta.maximum, a) + case "isBetweenBigInt": + return Schema.isBetweenBigInt(filter.meta, a) + + // Object Meta + case "isMinProperties": + return Schema.isMinProperties(filter.meta.minProperties, a) + case "isMaxProperties": + return Schema.isMaxProperties(filter.meta.maxProperties, a) + case "isPropertiesLengthBetween": + return Schema.isPropertiesLengthBetween(filter.meta.minimum, filter.meta.maximum, a) + case "isPropertyNames": + return Schema.isPropertyNames(recur(filter.meta.propertyNames) as Schema.Record.Key, a) + + // Arrays Meta + case "isUnique": + return Schema.isUnique(a) + + // Date Meta + case "isDateValid": + return Schema.isDateValid(a) + case "isGreaterThanDate": + return Schema.isGreaterThanDate(filter.meta.exclusiveMinimum, a) + case "isGreaterThanOrEqualToDate": + return Schema.isGreaterThanOrEqualToDate(filter.meta.minimum, a) + case "isLessThanDate": + return Schema.isLessThanDate(filter.meta.exclusiveMaximum, a) + case "isLessThanOrEqualToDate": + return Schema.isLessThanOrEqualToDate(filter.meta.maximum, a) + case "isBetweenDate": + return Schema.isBetweenDate(filter.meta, a) + + // Size Meta + case "isMinSize": + return Schema.isMinSize(filter.meta.minSize, a) + case "isMaxSize": + return Schema.isMaxSize(filter.meta.maxSize, a) + case "isSizeBetween": + return Schema.isSizeBetween(filter.meta.minimum, filter.meta.maximum, a) + } + } +} + +/** + * Converts a {@link Document} to a Draft 2020-12 JSON Schema document. + * + * **When to use** + * + * Use when you need to produce a standard JSON Schema document from a schema + * representation `Document`. + * + * **Gotchas** + * + * JSON Schema generation is best-effort. Some Effect schema representation + * semantics cannot be represented exactly in JSON Schema, and importing an + * emitted JSON Schema may produce an equivalent approximation rather than the + * original representation shape. + * + * **Example** (Generating JSON Schema) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const doc = SchemaRepresentation.fromAST(Schema.String.ast) + * const jsonSchema = SchemaRepresentation.toJsonSchemaDocument(doc) + * console.log(jsonSchema.schema.type) + * // "string" + * ``` + * + * @see {@link Document} + * @see {@link toJsonSchemaMultiDocument} + * @see {@link fromJsonSchemaDocument} + * + * @category transforming + * @since 4.0.0 + */ +export const toJsonSchemaDocument: ( + document: Document, + options?: Schema.ToJsonSchemaOptions +) => JsonSchema.Document<"draft-2020-12"> = InternalRepresentation.toJsonSchemaDocument + +/** + * Converts a {@link MultiDocument} to a Draft 2020-12 JSON Schema + * multi-document. + * + * **When to use** + * + * Use when you need to export related schema representation documents together + * so shared definitions stay in multi-document JSON Schema form. + * + * **Gotchas** + * + * JSON Schema generation is best-effort. Some Effect schema representation + * semantics cannot be represented exactly in JSON Schema, and importing an + * emitted JSON Schema may produce equivalent approximations rather than the + * original representation shapes. + * + * @see {@link MultiDocument} + * @see {@link toJsonSchemaDocument} + * @see {@link fromJsonSchemaMultiDocument} + * + * @category transforming + * @since 4.0.0 + */ +export const toJsonSchemaMultiDocument: ( + document: MultiDocument, + options?: Schema.ToJsonSchemaOptions +) => JsonSchema.MultiDocument<"draft-2020-12"> = InternalRepresentation.toJsonSchemaMultiDocument + +/** + * A pair of TypeScript source strings for a schema: `runtime` is the + * executable Schema expression, `Type` is the corresponding TypeScript type. + * + * @see {@link makeCode} + * @see {@link CodeDocument} + * + * @category Code Generation + * @since 4.0.0 + */ +export type Code = { + readonly runtime: string + readonly Type: string +} + +/** + * Constructs a {@link Code} value from a runtime expression string and a + * TypeScript type string. + * + * @see {@link Code} + * + * @category Code Generation + * @since 4.0.0 + */ +export function makeCode(runtime: string, Type: string): Code { + return { runtime, Type } +} + +/** + * An auxiliary code artifact produced during code generation — a symbol + * declaration, an enum declaration, or an import statement. + * + * @see {@link CodeDocument} + * @see {@link toCodeDocument} + * + * @category Code Generation + * @since 4.0.0 + */ +export type Artifact = + | { + readonly _tag: "Symbol" + readonly identifier: string + readonly generation: Code + } + | { + readonly _tag: "Enum" + readonly identifier: string + readonly generation: Code + } + | { + readonly _tag: "Import" + readonly importDeclaration: string + } + +/** + * The output of {@link toCodeDocument}: generated TypeScript code for one or + * more schemas plus their shared references and auxiliary artifacts. + * + * **Details** + * + * `codes` contains one {@link Code} per input representation. + * `references.nonRecursives` contains topologically sorted non-recursive + * definitions. `references.recursives` contains definitions involved in cycles. + * `artifacts` contains symbols, enums, and import statements needed by the + * code. + * + * @see {@link toCodeDocument} + * @see {@link Code} + * @see {@link Artifact} + * + * @category Code Generation + * @since 4.0.0 + */ +export type CodeDocument = { + readonly codes: ReadonlyArray + readonly references: { + readonly nonRecursives: ReadonlyArray<{ + readonly $ref: string + readonly code: Code + }> + readonly recursives: { + readonly [$ref: string]: Code + } + } + readonly artifacts: ReadonlyArray +} + +/** + * Generates TypeScript code strings from a {@link MultiDocument}. + * + * **When to use** + * + * Use when you need to produce source code for Effect Schema definitions from a + * schema representation `MultiDocument`. + * + * **Details** + * + * `options.reviver` can customize code generation for {@link Declaration} + * nodes. Return `undefined` to fall back to the default logic, which uses + * `generation` annotations or the encoded schema. References are + * topologically sorted so non-recursive definitions are emitted before their + * dependents. `$ref` keys are converted to sanitized JavaScript identifiers. + * + * **Example** (Generating TypeScript code) + * + * ```ts + * import { Schema, SchemaRepresentation } from "effect" + * + * const Person = Schema.Struct({ + * name: Schema.String, + * age: Schema.Int + * }) + * + * const multi = SchemaRepresentation.toMultiDocument( + * SchemaRepresentation.fromAST(Person.ast) + * ) + * const codeDoc = SchemaRepresentation.toCodeDocument(multi) + * console.log(codeDoc.codes[0].runtime) + * // Schema.Struct({ ... }) + * ``` + * + * @see {@link CodeDocument} + * @see {@link MultiDocument} + * @see {@link Reviver} + * + * @category Code Generation + * @since 4.0.0 + */ +export function toCodeDocument(multiDocument: MultiDocument, options?: { + /** + * The reviver can return `undefined` to indicate that the generation should be generated by the default logic + */ + readonly reviver?: Reviver | undefined +}): CodeDocument { + const artifacts: Array = [] + + const ts = topologicalSort(multiDocument.references) + + // Phase 1: Build sanitization map with collision handling + const sanitizedReferenceMap = new Map() + const uniqueSanitizedReferences = new Set() + const referenceCount = new Map() + + // Process all references first to build the map + const allRefs = [ + ...ts.nonRecursives.map(({ $ref }) => $ref), + ...Object.keys(ts.recursives) + ] + + for (const ref of allRefs) { + ensureUniqueSanitized(ref) + } + + // Phase 2: Use the map when processing references + const nonRecursives = ts.nonRecursives.map(({ $ref, representation }) => ({ + $ref: sanitizedReferenceMap.get($ref)!, + code: recur(representation) + })) + const recursives = Rec.mapEntries(ts.recursives, (representation, $ref) => [ + sanitizedReferenceMap.get($ref)!, + recur(representation) + ]) + + const codes = multiDocument.representations.map(recur) + + return { + codes, + references: { + nonRecursives: nonRecursives.filter(({ $ref }) => (referenceCount.get($ref) ?? 0) > 0), + recursives: Rec.filter(recursives, (_, $ref) => (referenceCount.get($ref) ?? 0) > 0) + }, + artifacts + } + + function ensureUniqueSanitized(originalRef: string): string { + // Check if already mapped (consistency) + const sanitized = sanitizedReferenceMap.get(originalRef) + if (sanitized !== undefined) { + return sanitized + } + + // Find unique sanitized name + const seed = sanitizeJavaScriptIdentifier(originalRef) + let candidate = seed + let suffix = 0 + + while (uniqueSanitizedReferences.has(candidate)) { + candidate = `${seed}${++suffix}` + } + + uniqueSanitizedReferences.add(candidate) + sanitizedReferenceMap.set(originalRef, candidate) + return candidate + } + + function addSymbol(s: symbol): string { + const identifier = ensureUniqueSanitized("_symbol") + const key = globalThis.Symbol.keyFor(s) + const description = s.description + const generation = key === undefined + ? makeCode(`Symbol(${description === undefined ? "" : format(description)})`, `typeof ${identifier}`) + : makeCode(`Symbol.for(${format(key)})`, `typeof ${identifier}`) + artifacts.push({ _tag: "Symbol", identifier, generation }) + return identifier + } + + function addEnum(s: Enum): string { + const identifier = ensureUniqueSanitized("_Enum") + artifacts.push({ + _tag: "Enum", + identifier, + generation: makeCode( + `enum ${identifier} { ${s.enums.map(([name, value]) => `${format(name)}: ${format(value)}`).join(", ")} }`, + `typeof ${identifier}` + ) + }) + return identifier + } + + function addImport(importDeclaration: string) { + if (!artifacts.some((a) => a._tag === "Import" && a.importDeclaration === importDeclaration)) { + artifacts.push({ _tag: "Import", importDeclaration }) + } + } + + function recur(s: Representation): Code { + const g = on(s) + switch (s._tag) { + default: + return makeCode( + g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations), + g.Type + toTypeBrand(s.annotations) + ) + case "Reference": + return g + case "Declaration": + case "String": + case "Number": + case "BigInt": + case "Arrays": + case "Objects": + case "Suspend": + return makeCode( + g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations) + toRuntimeChecks(s.checks), + g.Type + toTypeBrand(s.annotations) + toTypeChecks(s.checks) + ) + } + } + + function on(s: Representation): Code { + switch (s._tag) { + case "Declaration": { + // if there is a reviver, use it to generate the generation + if (options?.reviver !== undefined) { + // the reviver can return `undefined` to indicate that the generation should be generated by the default logic + const out = options.reviver(s, recur) + if (out !== undefined) { + return out + } + } + // otherwise, use the generation from the annotations + const generation = s.annotations?.generation + if ( + Predicate.isObject(generation) && typeof generation.runtime === "string" && + typeof generation.Type === "string" + ) { + const typeParameters = s.typeParameters.map(recur) + if (typeof generation.importDeclaration === "string") { + addImport(generation.importDeclaration) + } + return makeCode( + replacePlaceholders(generation.runtime, typeParameters.map((p) => p.runtime)), + replacePlaceholders(generation.Type, typeParameters.map((p) => p.Type)) + ) + } + // otherwise, use the generation from the encoded schema + return recur(s.encodedSchema) + } + case "Reference": { + const sanitized = ensureUniqueSanitized(s.$ref) + referenceCount.set(sanitized, (referenceCount.get(sanitized) ?? 0) + 1) + return makeCode(sanitized, sanitized) + } + case "Suspend": { + const thunk = recur(s.thunk) + return makeCode( + `Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`, + thunk.Type + ) + } + case "Null": + return makeCode(`Schema.Null`, "null") + case "Undefined": + return makeCode(`Schema.Undefined`, "undefined") + case "Void": + return makeCode(`Schema.Void`, "void") + case "Never": + return makeCode(`Schema.Never`, "never") + case "Unknown": + return makeCode(`Schema.Unknown`, "unknown") + case "Any": + return makeCode(`Schema.Any`, "any") + case "Number": + return makeCode(`Schema.Number`, "number") + case "Boolean": + return makeCode(`Schema.Boolean`, "boolean") + case "BigInt": + return makeCode(`Schema.BigInt`, "bigint") + case "Symbol": + return makeCode(`Schema.Symbol`, "symbol") + case "String": { + const contentMediaType = s.contentMediaType + const contentSchema = s.contentSchema + if (contentMediaType === "application/json" && contentSchema !== undefined) { + return makeCode(`Schema.fromJsonString(${recur(contentSchema)})`, "string") + } else { + return makeCode(`Schema.String`, "string") + } + } + case "Literal": { + const literal = format(s.literal) + return makeCode(`Schema.Literal(${literal})`, literal) + } + case "UniqueSymbol": { + const identifier = addSymbol(s.symbol) + return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`) + } + case "ObjectKeyword": + return makeCode(`Schema.ObjectKeyword`, "object") + case "Enum": { + const identifier = addEnum(s) + return makeCode(`Schema.Enum(${identifier})`, `typeof ${identifier}`) + } + case "TemplateLiteral": { + const parts = s.parts.map(recur) + const type = toTypeParts(s.parts).map((p) => "`" + p + "`").join(" | ") + return makeCode(`Schema.TemplateLiteral([${parts.map((p) => p.runtime).join(", ")}])`, type) + } + case "Arrays": { + const elements = s.elements.map((e) => { + return { + isOptional: e.isOptional, + type: recur(e.type), + annotations: e.annotations + } + }) + + const rest = s.rest.map(recur) + + if (Arr.isArrayNonEmpty(rest)) { + const item = rest[0] + if (elements.length === 0 && rest.length === 1) { + return makeCode( + `Schema.Array(${item.runtime})`, + `ReadonlyArray<${item.Type}>` + ) + } + const post = rest.slice(1) + return makeCode( + `Schema.TupleWithRest(Schema.Tuple([${ + elements.map((e) => + toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations) + ).join(", ") + }]), [${rest.map((r) => r.runtime).join(", ")}])`, + `readonly [${ + elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ") + }, ...Array<${item.Type}>${post.length > 0 ? `, ${post.map((p) => p.Type).join(", ")}` : ""}]` + ) + } + return makeCode( + `Schema.Tuple([${ + elements.map((e) => toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations)) + .join(", ") + }])`, + `readonly [${elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ")}]` + ) + } + case "Objects": { + const pss = s.propertySignatures.map((p) => { + const isSymbol = typeof p.name === "symbol" + const name = isSymbol ? addSymbol(p.name) : formatPropertyKey(p.name) + const nameType = toTypeIsOptional( + p.isOptional, + toTypeIsMutable(p.isMutable, isSymbol ? `[typeof ${name}]` : name) + ) + const type = recur(p.type) + return makeCode( + `${isSymbol ? `[${name}]` : name}: ${ + toRuntimeIsOptional(p.isOptional, toRuntimeIsMutable(p.isMutable, type.runtime)) + }` + + toRuntimeAnnotateKey(p.annotations), + `${nameType}: ${type.Type}` + ) + }) + + const iss = s.indexSignatures.map((is) => { + return { + parameter: recur(is.parameter), + type: recur(is.type) + } + }) + + if (iss.length === 0) { + // 1) Only properties -> Struct + return makeCode( + `Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} })`, + `{ ${pss.map((p) => p.Type).join(", ")} }` + ) + } else if (pss.length === 0 && iss.length === 1) { + // 2) Only one index signature and no properties -> Record + return makeCode( + `Schema.Record(${iss[0].parameter.runtime}, ${iss[0].type.runtime})`, + `{ readonly [x: ${iss[0].parameter.Type}]: ${iss[0].type.Type} }` + ) + } else { + // 3) Properties + index signatures -> StructWithRest + return makeCode( + `Schema.StructWithRest(Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} }), [${ + iss.map((is) => `Schema.Record(${is.parameter.runtime}, ${is.type.runtime})`).join(", ") + }])`, + `{ ${pss.map((p) => p.Type).join(", ")}, ${ + iss.map((is) => `readonly [x: ${is.parameter.Type}]: ${is.type.Type}`).join(", ") + } }` + ) + } + } + case "Union": { + if (s.types.length === 0) { + return makeCode("Schema.Never", "never") + } + if (s.types.every((t) => t._tag === "Literal")) { + const literals = s.types.map((l) => format(l.literal)) + if (literals.length === 1) { + return makeCode(`Schema.Literal(${literals[0]})`, literals[0]) + } + return makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | ")) + } + const mode = s.mode === "anyOf" ? "" : `, { mode: "oneOf" }` + const types = s.types.map((t) => recur(t)) + return makeCode( + `Schema.Union([${types.map((t) => t.runtime).join(", ")}]${mode})`, + types.map((t) => t.Type).join(" | ") + ) + } + } + } + + function toTypeBrand(annotations: Schema.Annotations.Annotations | undefined): string { + const brands = collectBrands(annotations) + if (brands.length === 0) return "" + addImport(`import type * as Brand from "effect/Brand"`) + return brands.map((b) => ` & Brand.Brand<${format(b)}>`).join("") + } + + function toTypeChecks(checks: ReadonlyArray>): string { + return checks.map((c) => toTypeCheck(c)).join("") + } + + function toTypeCheck(check: Check): string { + switch (check._tag) { + case "Filter": + return toTypeBrand(check.annotations) + case "FilterGroup": { + return toTypeChecks(check.checks) + } + } + } + + function toRuntimeChecks(checks: ReadonlyArray>): string { + return checks.map((c) => `.check(${toRuntimeCheck(c)})` + toRuntimeBrand(c.annotations)).join("") + } + + function toRuntimeCheck(check: Check): string { + switch (check._tag) { + case "Filter": + return toRuntimeFilter(check) + case "FilterGroup": { + const a = toRuntimeAnnotations(check.annotations) + const ca = a === "" ? "" : `, ${a}` + return `Schema.makeFilterGroup([${check.checks.map((c) => toRuntimeCheck(c)).join(", ")}]${ca})` + } + } + } + + function toRuntimeFilter(filter: Filter): string { + const a = toRuntimeAnnotations(filter.annotations) + const ca = a === "" ? "" : `, ${a}` + switch (filter.meta._tag) { + case "isTrimmed": + case "isGUID": + case "isULID": + case "isBase64": + case "isBase64Url": + case "isUppercased": + case "isLowercased": + case "isCapitalized": + case "isUncapitalized": + case "isFinite": + case "isInt": + case "isUnique": + case "isDateValid": + return `Schema.${filter.meta._tag}(${a})` + + case "isStringFinite": + case "isStringBigInt": + case "isStringSymbol": + case "isPattern": + return `Schema.${filter.meta._tag}(${toRuntimeRegExp(filter.meta.regExp)}${ca})` + + case "isMinLength": + return `Schema.isMinLength(${filter.meta.minLength}${ca})` + case "isMaxLength": + return `Schema.isMaxLength(${filter.meta.maxLength}${ca})` + case "isLengthBetween": + return `Schema.isLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` + case "isUUID": + return `Schema.isUUID(${filter.meta.version}${ca})` + case "isStartsWith": + return `Schema.isStartsWith(${format(filter.meta.startsWith)}${ca})` + case "isEndsWith": + return `Schema.isEndsWith(${format(filter.meta.endsWith)}${ca})` + case "isIncludes": + return `Schema.isIncludes(${format(filter.meta.includes)}${ca})` + + case "isGreaterThan": + case "isGreaterThanBigInt": + case "isGreaterThanDate": + return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMinimum)}${ca})` + case "isGreaterThanOrEqualTo": + case "isGreaterThanOrEqualToBigInt": + case "isGreaterThanOrEqualToDate": + return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.minimum)}${ca})` + case "isLessThan": + case "isLessThanBigInt": + case "isLessThanDate": + return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMaximum)}${ca})` + case "isLessThanOrEqualTo": + case "isLessThanOrEqualToBigInt": + case "isLessThanOrEqualToDate": + return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.maximum)}${ca})` + case "isBetween": + case "isBetweenBigInt": + case "isBetweenDate": + return `Schema.${filter.meta._tag}({ minimum: ${toRuntimeValue(filter.meta.minimum)}, maximum: ${ + toRuntimeValue(filter.meta.maximum) + }, exclusiveMinimum: ${toRuntimeValue(filter.meta.exclusiveMinimum)}, exclusiveMaximum: ${ + toRuntimeValue(filter.meta.exclusiveMaximum) + }${ca})` + + case "isMultipleOf": + return `Schema.isMultipleOf(${filter.meta.divisor}${ca})` + + case "isMinProperties": + return `Schema.isMinProperties(${filter.meta.minProperties}${ca})` + case "isMaxProperties": + return `Schema.isMaxProperties(${filter.meta.maxProperties}${ca})` + case "isPropertiesLengthBetween": + return `Schema.isPropertiesLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` + case "isPropertyNames": + return `Schema.isPropertyNames(${recur(filter.meta.propertyNames).runtime}${ca})` + + case "isMinSize": + return `Schema.isMinSize(${filter.meta.minSize}${ca})` + case "isMaxSize": + return `Schema.isMaxSize(${filter.meta.maxSize}${ca})` + case "isSizeBetween": + return `Schema.isSizeBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` + } + } +} + +const VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP = /^[A-Z_$][A-Za-z0-9_$]*$/ + +/** + * Converts an arbitrary string into a valid (ASCII) JavaScript identifier + * starting with an uppercase letter, `$`, or `_`. + * + * - Replaces invalid identifier characters with `_` + * - Uppercases a leading ASCII letter + * - If the first character is a digit, prefixes `_` + * - Empty input becomes `_` + * + * @internal + */ +export function sanitizeJavaScriptIdentifier(s: string): string { + if (s.length === 0) return "_" + if (VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP.test(s)) return s + + const out: Array = [] + let needsPrefix = false + let i = 0 + + for (const ch of s) { + if (i === 0) { + if (ch === "_" || ch === "$" || (ch >= "A" && ch <= "Z")) { + out.push(ch) + } else if (ch >= "a" && ch <= "z") { + out.push(ch.toUpperCase()) + } else if (ch >= "0" && ch <= "9") { + out.push(ch) + needsPrefix = true + } else { + out.push("_") + } + } else { + out.push(isAsciiIdPart(ch) ? ch : "_") + } + i++ + } + + return needsPrefix ? "_" + out.join("") : out.join("") +} + +function isAsciiIdStart(ch: string): boolean { + return ( + ch === "_" || + ch === "$" || + (ch >= "A" && ch <= "Z") || + (ch >= "a" && ch <= "z") + ) +} + +function isAsciiIdPart(ch: string): boolean { + return isAsciiIdStart(ch) || (ch >= "0" && ch <= "9") +} + +function replacePlaceholders(template: string, items: ReadonlyArray) { + let i = 0 + return template.replace(/\?/g, () => items[i++]) +} + +function toTypeParts(parts: ReadonlyArray): ReadonlyArray { + if (parts.length === 0) { + return [""] + } + const [first, ...rest] = parts + const restPatterns = toTypeParts(rest) + return toTypePart(first).flatMap((f) => restPatterns.map((r) => f + r)) +} + +function toTypePart(r: Representation): ReadonlyArray { + switch (r._tag) { + case "Literal": + return [globalThis.String(r.literal)] + case "String": + return ["${string}"] + case "Number": + return ["${number}"] + case "BigInt": + return ["${bigint}"] + case "TemplateLiteral": + return toTypeParts(r.parts) + case "Union": + return r.types.flatMap(toTypePart) + default: + return [] + } +} + +const toCodeAnnotationsBlacklist: Set = new Set([ + ...toJsonAnnotationsBlacklist, + "typeConstructor", + "generation", + "brands" +]) + +function toRuntimeAnnotations(annotations: Schema.Annotations.Annotations | undefined): string { + if (!annotations) return "" + const entries: Array = [] + for (const [key, value] of Object.entries(annotations)) { + if (toCodeAnnotationsBlacklist.has(key)) continue + entries.push(`${formatPropertyKey(key)}: ${format(value)}`) + } + if (entries.length === 0) return "" + return `{ ${entries.join(", ")} }` +} + +function toRuntimeBrand(annotations: Schema.Annotations.Annotations | undefined): string { + const brands = collectBrands(annotations) + return brands.length > 0 ? `.pipe(${brands.map((b) => `Schema.brand(${format(b)})`).join(", ")})` : "" +} + +function toRuntimeAnnotate(annotations: Schema.Annotations.Annotations | undefined): string { + const s = toRuntimeAnnotations(annotations) + return s === "" ? "" : `.annotate(${s})` +} + +function toRuntimeAnnotateKey(annotations: Schema.Annotations.Annotations | undefined): string { + const s = toRuntimeAnnotations(annotations) + return s === "" ? "" : `.annotateKey(${s})` +} + +function toRuntimeIsOptional(isOptional: boolean, runtime: string): string { + return isOptional ? `Schema.optionalKey(${runtime})` : runtime +} + +function toTypeIsOptional(isOptional: boolean, type: string): string { + return isOptional ? `${type}?` : type +} + +function toRuntimeIsMutable(isMutable: boolean, runtime: string): string { + return isMutable ? `Schema.mutableKey(${runtime})` : runtime +} + +function toTypeIsMutable(isMutable: boolean, type: string): string { + return isMutable ? type : `readonly ${type}` +} + +function toRuntimeValue(value: undefined | number | boolean | bigint | Date): string { + if (value instanceof Date) { + return `new Date(${value.getTime()})` + } + return format(value) +} + +function toRuntimeRegExp(regExp: RegExp): string { + const args = [format(regExp.source)] + const flags = regExp.flags.trim() + if (flags !== "") { + args.push(format(flags)) + } + return `new RegExp(${args.join(", ")})` +} + +/** + * Parses a Draft 2020-12 JSON Schema document into a {@link Document}. + * + * **When to use** + * + * Use when you need to import a Draft 2020-12 JSON Schema document into the + * Effect schema representation system. + * + * **Details** + * + * `options.onEnter` is an optional hook called on each JSON Schema node before + * processing, allowing pre-transformation. + * + * **Gotchas** + * + * JSON Schema import is best-effort. Some JSON Schema constructs do not map + * exactly to Effect schema representations, and importing a schema previously + * emitted by `toJsonSchemaDocument` may produce an equivalent approximation + * rather than the original representation shape. + * + * This throws if a `$ref` cannot be resolved within the document's definitions. + * Circular `$ref`s are detected and cause an error. + * + * @see {@link Document} + * @see {@link toJsonSchemaDocument} + * @see {@link fromJsonSchemaMultiDocument} + * + * @category constructors + * @since 4.0.0 + */ +export function fromJsonSchemaDocument(document: JsonSchema.Document<"draft-2020-12">, options?: { + readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined +}): Document { + const { references, representations: schemas } = fromJsonSchemaMultiDocument({ + dialect: document.dialect, + schemas: [document.schema], + definitions: document.definitions + }, options) + return { + representation: schemas[0], + references + } +} + +/** + * Parses a Draft 2020-12 JSON Schema multi-document into a + * {@link MultiDocument}. + * + * **When to use** + * + * Use when you need to import a Draft 2020-12 JSON Schema multi-document whose + * schemas share definitions. + * + * **Details** + * + * `options.onEnter` is an optional hook called on each JSON Schema node before + * processing. + * + * **Gotchas** + * + * JSON Schema import is best-effort. Some JSON Schema constructs do not map + * exactly to Effect schema representations, and importing schemas previously + * emitted by `toJsonSchemaMultiDocument` may produce equivalent approximations + * rather than the original representation shapes. + * + * This throws if a `$ref` cannot be resolved. + * + * @see {@link MultiDocument} + * @see {@link toJsonSchemaMultiDocument} + * @see {@link fromJsonSchemaDocument} + * + * @category constructors + * @since 4.0.0 + */ +export function fromJsonSchemaMultiDocument(document: JsonSchema.MultiDocument<"draft-2020-12">, options?: { + readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined +}): MultiDocument { + let definitionIdentifier: string | undefined + const references: Record = {} + + type ResolvedReference = Exclude + const resolvedReferences = new Map() + + function resolveReference($ref: string): ResolvedReference { + const definition = document.definitions[$ref] + if (definition === undefined) { + throw new Error(`Reference ${$ref} not found`) + } + + const resolved = resolvedReferences.get($ref) + if (resolved === null) { + throw new Error(`Circular reference detected: ${$ref}`) + } + if (resolved !== undefined) return resolved + + resolvedReferences.set($ref, null) + const value = recur(definition) + const out = value._tag === "Reference" ? resolveReference(value.$ref) : value + resolvedReferences.set($ref, out) + return out + } + + for (const [identifier, definition] of Object.entries(document.definitions)) { + definitionIdentifier = identifier + references[identifier] = unknownToJson(recur(definition)) + } + + definitionIdentifier = undefined + const representations = Arr.map(document.schemas, (schema) => unknownToJson(recur(schema))) + return { + representations, + references + } + + function recur(u: unknown): Representation { + if (u === false) return never + if (!Predicate.isObject(u)) return unknown + + let js: JsonSchema.JsonSchema = options?.onEnter?.(u) ?? u + if (Array.isArray(js.type)) { + if (js.type.every(isType)) { + const { type, ...rest } = js + js = { + anyOf: type.map((type) => ({ type })), + ...rest + } + } else { + js = {} + } + } + + let out = on(js) + + const annotations = collectAnnotations(js) + if (annotations !== undefined) { + out = combine(out, { _tag: "Unknown", annotations }) + } + + if (Array.isArray(js.allOf)) { + out = js.allOf.reduce((acc, curr) => combine(acc, recur(curr)), out) + } + if (Array.isArray(js.anyOf)) { + out = combine({ _tag: "Union", types: js.anyOf.map((type) => recur(type)), mode: "anyOf" }, out) + } + if (Array.isArray(js.oneOf)) { + out = combine({ _tag: "Union", types: js.oneOf.map((type) => recur(type)), mode: "oneOf" }, out) + } + + return out + } + + function on(js: JsonSchema.JsonSchema): Representation { + if (typeof js.$ref === "string") { + const $ref = js.$ref.slice(2).split("/").at(-1) + if ($ref !== undefined) { + const reference: Reference = { _tag: "Reference", $ref: unescapeToken($ref) } + if (definitionIdentifier === $ref) { + return { _tag: "Suspend", thunk: reference, checks: [] } + } else { + return reference + } + } + } else if ("const" in js) { + if (isLiteralValue(js.const)) { + return { _tag: "Literal", literal: js.const } + } else if (js.const === null) { + return null_ + } + } else if (Array.isArray(js.enum)) { + const types: Array = [] + for (const e of js.enum) { + if (isLiteralValue(e)) { + types.push({ _tag: "Literal", literal: e }) + } else if (e === null) { + types.push(null_) + } else { + types.push(recur(e)) + } + } + if (types.length === 1) { + return types[0] + } else { + return { _tag: "Union", types, mode: "anyOf" } + } + } + + const type = isType(js.type) ? js.type : getType(js) + if (type !== undefined) { + switch (type) { + case "null": + return null_ + case "string": { + const checks = collectStringChecks(js) + if (checks.length > 0) { + return { ...string, checks } + } + return string + } + case "number": + return { + _tag: "Number", + checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }, ...collectNumberChecks(js)] + } + case "integer": + return { + _tag: "Number", + checks: [{ _tag: "Filter", meta: { _tag: "isInt" } }, ...collectNumberChecks(js)] + } + case "boolean": + return boolean + case "array": { + const minItems = typeof js.minItems === "number" ? js.minItems : 0 + + const elements: Array = (Array.isArray(js.prefixItems) ? js.prefixItems : []).map((e, i) => ({ + isOptional: i + 1 > minItems, + type: recur(e) + })) + + const rest: Array = js.items !== undefined ? + [recur(js.items)] + : js.prefixItems !== undefined && typeof js.maxItems === "number" + ? [] + : [unknown] + + return { _tag: "Arrays", elements, rest, checks: collectArraysChecks(js) } + } + case "object": { + return { + _tag: "Objects", + propertySignatures: collectProperties(js), + indexSignatures: collectIndexSignatures(js), + checks: collectObjectsChecks(js) + } + } + } + } + + return { _tag: "Unknown" } + } + + function collectObjectsChecks(js: JsonSchema.JsonSchema): Array> { + const checks: Array> = [] + if (typeof js.minProperties === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMinProperties", minProperties: js.minProperties } }) + } + if (typeof js.maxProperties === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMaxProperties", maxProperties: js.maxProperties } }) + } + if (js.propertyNames !== undefined) { + const propertyNames = recur(js.propertyNames) + checks.push({ _tag: "Filter", meta: { _tag: "isPropertyNames", propertyNames } }) + } + return checks + } + + function combine(a: Representation, b: Representation): Representation { + switch (a._tag) { + default: + return never + case "Reference": + return combine(resolveReference(a.$ref), b) + case "Never": + return a + case "Unknown": { + const resolved = b._tag === "Reference" ? resolveReference(b.$ref) : b + return { ...resolved, ...combineAnnotations(a.annotations, resolved.annotations) } + } + case "Null": + case "String": + case "Number": + case "Boolean": + case "Literal": + case "Arrays": + case "Objects": + case "Union": + break + } + + if (b._tag === "Reference") { + return combine(a, resolveReference(b.$ref)) + } + if (b._tag === "Unknown") { + return { ...a, ...combineAnnotations(a.annotations, b.annotations) } + } + if (a._tag === "Union") { + const types = a.types.map((s) => combine(s, b)).filter((s) => s !== never) + if (types.length === 0) return never + return { + _tag: "Union", + types, + mode: a.mode, + ...makeAnnotations(a.annotations) + } + } + if (b._tag === "Union") { + return combine(b, a) + } + + switch (a._tag) { + case "Null": + return b._tag === "Null" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never + case "String": { + if (b._tag === "Literal") { + return satisfiesLiteral(a, b) ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never + } + if (b._tag !== "String") return never + const checks = combineChecks(a.checks, b.checks, b.annotations) + return { + _tag: "String", + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Number": { + if (b._tag === "Literal") { + return satisfiesLiteral(a, b) ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never + } + if (b._tag !== "Number") return never + const checks = combineNumberChecks(a.checks, b.checks, b.annotations) + return { + _tag: "Number", + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Boolean": + if (b._tag === "Boolean") { + return { _tag: "Boolean", ...combineAnnotations(a.annotations, b.annotations) } + } + return b._tag === "Literal" && typeof b.literal === "boolean" + ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } + : never + case "Literal": + switch (b._tag) { + case "Literal": + return a.literal === b.literal + ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } + : never + case "String": + case "Number": + return satisfiesLiteral(b, a) ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never + case "Boolean": + return typeof a.literal === "boolean" + ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } + : never + default: + return never + } + case "Arrays": { + if (b._tag !== "Arrays") return never + const arrays = combineArrays(a, b) + if (arrays === undefined) return never + const checks = combineArraysChecks(a.checks, b.checks, b.annotations) + return { + _tag: "Arrays", + elements: arrays.elements, + rest: arrays.rest, + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + case "Objects": { + if (b._tag !== "Objects") return never + const checks = combineChecks(a.checks, b.checks, b.annotations) + return { + _tag: "Objects", + propertySignatures: combinePropertySignatures(a.propertySignatures, b.propertySignatures), + indexSignatures: combineIndexSignatures(a.indexSignatures, b.indexSignatures), + checks: checks ?? a.checks, + ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) + } + } + default: + return never + } + } + + function satisfiesPrimitiveCheck(check: Check, value: unknown): boolean { + if (check._tag === "FilterGroup") { + return check.checks.every((check) => satisfiesPrimitiveCheck(check, value)) + } + const meta = check.meta + switch (meta._tag) { + case "isMinLength": + return typeof value === "string" && value.length >= meta.minLength + case "isMaxLength": + return typeof value === "string" && value.length <= meta.maxLength + case "isPattern": + return typeof value === "string" && meta.regExp.test(value) + case "isFinite": + return typeof value === "number" && globalThis.Number.isFinite(value) + case "isInt": + return typeof value === "number" && globalThis.Number.isSafeInteger(value) + case "isMultipleOf": + return typeof value === "number" && remainder(value, meta.divisor) === 0 + case "isGreaterThan": + return typeof value === "number" && value > meta.exclusiveMinimum + case "isGreaterThanOrEqualTo": + return typeof value === "number" && value >= meta.minimum + case "isLessThan": + return typeof value === "number" && value < meta.exclusiveMaximum + case "isLessThanOrEqualTo": + return typeof value === "number" && value <= meta.maximum + default: + return false + } + } + + function satisfiesLiteral(type: String | Number, literal: Literal): boolean { + const value = literal.literal + if (type._tag === "String" ? typeof value !== "string" : typeof value !== "number") { + return false + } + return type.checks.every((check) => satisfiesPrimitiveCheck(check, value)) + } + + function collectProperties(js: JsonSchema.JsonSchema): Array { + const properties: Record = Predicate.isObject(js.properties) ? js.properties : {} + const required = Array.isArray(js.required) ? js.required : [] + required.forEach((key) => { + if (!Object.hasOwn(properties, key)) { + properties[key] = {} + } + }) + return Object.entries(properties).map(([key, v]) => ({ + name: key, + type: recur(v), + isOptional: !required.includes(key), + isMutable: false + })) + } + + function collectIndexSignatures(js: JsonSchema.JsonSchema): Array { + const out: Array = [] + + if (Predicate.isObject(js.patternProperties)) { + for (const [pattern, value] of Object.entries(js.patternProperties)) { + out.push({ parameter: recur({ pattern }), type: recur(value) }) + } + } + + if (js.additionalProperties === undefined || js.additionalProperties === true) { + out.push({ parameter: string, type: unknown }) + } else if (Predicate.isObject(js.additionalProperties)) { + out.push({ parameter: string, type: recur(js.additionalProperties) }) + } + + return out + } + + function combineArrays(a: Arrays, b: Arrays): Pick | undefined { + const elements: Array = [] + const len = Math.max(a.elements.length, b.elements.length) + for (let i = 0; i < len; i++) { + const ae = a.elements[i] + const be = b.elements[i] + const isOptional = ae?.isOptional !== false && be?.isOptional !== false + const at = ae?.type ?? a.rest[0] + const bt = be?.type ?? b.rest[0] + if (at === undefined || bt === undefined) { + return isOptional ? { elements, rest: [] } : undefined + } + const type = combine(at, bt) + if (type === never) { + return isOptional ? { elements, rest: [] } : undefined + } + elements.push({ isOptional, type }) + } + + const ar = a.rest[0] + const br = b.rest[0] + if (ar === undefined || br === undefined) { + return { elements, rest: [] } + } + const rest = combine(ar, br) + return { elements, rest: rest === never ? [] : [rest] } + } + + function combinePropertySignatures( + a: ReadonlyArray, + b: ReadonlyArray + ): Array { + const propertySignatures: Array = [] + const thatPropertiesMap: Record = {} + for (const p of b) { + thatPropertiesMap[p.name] = p + } + const keys = new Set() + for (const p of a) { + keys.add(p.name) + const thatp = thatPropertiesMap[p.name] + if (thatp) { + propertySignatures.push( + { + name: p.name, + type: combine(p.type, thatp.type), + isOptional: p.isOptional && thatp.isOptional, + isMutable: p.isMutable + } + ) + } else { + propertySignatures.push(p) + } + } + for (const p of b) { + if (!keys.has(p.name)) propertySignatures.push(p) + } + return propertySignatures + } + + function combineIndexSignatures( + a: ReadonlyArray, + b: ReadonlyArray + ): Array { + if (a.length === 0 || b.length === 0) return [] + const out: Array = [...a] + for (const is of b) { + if (is.parameter === string) { + const i = a.findIndex((is) => is.parameter === string) + if (i !== -1) { + out[i] = { parameter: string, type: combine(a[i].type, is.type) } + } else { + out.push(is) + } + } else { + out.push(is) + } + } + return out + } + + function unknownToJson(representation: Representation): Representation { + switch (representation._tag) { + case "Unknown": + return representation.annotations === undefined ? + json : + { + ...json, + annotations: { + ...json.annotations, + ...representation.annotations + } + } + case "Suspend": { + const thunk = unknownToJson(representation.thunk) + return thunk === representation.thunk ? representation : { ...representation, thunk } + } + case "String": { + if (representation.contentSchema === undefined) return representation + const contentSchema = unknownToJson(representation.contentSchema) + return contentSchema === representation.contentSchema ? representation : { ...representation, contentSchema } + } + case "Arrays": { + const elements = SchemaAST.mapOrSame(representation.elements, (element) => { + const type = unknownToJson(element.type) + return type === element.type ? element : { ...element, type } + }) + const rest = SchemaAST.mapOrSame(representation.rest, unknownToJson) + return elements === representation.elements && rest === representation.rest ? + representation : + { ...representation, elements, rest } + } + case "Objects": { + const propertySignatures = SchemaAST.mapOrSame(representation.propertySignatures, (propertySignature) => { + const type = unknownToJson(propertySignature.type) + return type === propertySignature.type ? propertySignature : { ...propertySignature, type } + }) + const indexSignatures = SchemaAST.mapOrSame(representation.indexSignatures, (indexSignature) => { + const type = unknownToJson(indexSignature.type) + return type === indexSignature.type ? indexSignature : { ...indexSignature, type } + }) + return propertySignatures === representation.propertySignatures && + indexSignatures === representation.indexSignatures ? + representation : + { ...representation, propertySignatures, indexSignatures } + } + case "Union": { + const types = SchemaAST.mapOrSame(representation.types, unknownToJson) + return types === representation.types ? representation : { ...representation, types } + } + default: + return representation + } + } +} + +function asChecks( + checks: ReadonlyArray>, + annotations: Schema.Annotations.Annotations | undefined +): ReadonlyArray> | undefined { + if (Arr.isReadonlyArrayNonEmpty(checks)) { + if (annotations !== undefined) { + if (checks.length === 1) { + const check = checks[0] + if (check.annotations === undefined) { + return [{ ...check, annotations }] + } else { + return [{ _tag: "FilterGroup", checks, annotations }] + } + } else { + return [{ _tag: "FilterGroup", checks, annotations }] + } + } + return checks + } +} + +function combineChecks( + a: ReadonlyArray>, + b: ReadonlyArray>, + annotations: Schema.Annotations.Annotations | undefined +): Array> | undefined { + const checks = asChecks(b, annotations) + if (checks) { + return [...a, ...checks] + } +} + +function combineNumberChecks( + a: ReadonlyArray>, + b: ReadonlyArray>, + annotations: Schema.Annotations.Annotations | undefined +): Array> | undefined { + if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isFinite")) { + b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isFinite") + } + if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isInt")) { + b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isInt") + } + return combineChecks(a, b, annotations) +} + +function combineArraysChecks( + a: ReadonlyArray>, + b: ReadonlyArray>, + annotations: Schema.Annotations.Annotations | undefined +): Array> | undefined { + if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isUnique")) { + b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isUnique") + } + return combineChecks(a, b, annotations) +} + +function makeAnnotations( + annotations: Schema.Annotations.Annotations | undefined +): { annotations: Schema.Annotations.Annotations } | undefined { + return annotations ? { annotations } : undefined +} + +function combineAnnotations( + a: Schema.Annotations.Annotations | undefined, + b: Schema.Annotations.Annotations | undefined +): { annotations: Schema.Annotations.Annotations } | undefined { + if (a === undefined) return makeAnnotations(b) + if (b === undefined) return makeAnnotations(a) + return { annotations: { ...a, ...b } } // TODO: better merge +} + +function collectStringChecks(js: JsonSchema.JsonSchema): Array> { + const checks: Array> = [] + if (typeof js.minLength === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minLength } }) + } + if (typeof js.maxLength === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxLength } }) + } + if (typeof js.pattern === "string") { + checks.push({ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp(js.pattern) } }) + } + return checks +} + +function collectNumberChecks(js: JsonSchema.JsonSchema): Array> { + const checks: Array> = [] + if (typeof js.minimum === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: js.minimum } }) + } + if (typeof js.maximum === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: js.maximum } }) + } + if (typeof js.exclusiveMinimum === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThan", exclusiveMinimum: js.exclusiveMinimum } }) + } + if (typeof js.exclusiveMaximum === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isLessThan", exclusiveMaximum: js.exclusiveMaximum } }) + } + if (typeof js.multipleOf === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMultipleOf", divisor: js.multipleOf } }) + } + return checks +} + +function collectArraysChecks(js: JsonSchema.JsonSchema): Array> { + const checks: Array> = [] + if (js.prefixItems === undefined) { + if (typeof js.minItems === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minItems } }) + } + if (typeof js.maxItems === "number") { + checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxItems } }) + } + } + if (typeof js.uniqueItems === "boolean") { + checks.push({ _tag: "Filter", meta: { _tag: "isUnique" } }) + } + return checks +} + +const unknown: Unknown = { _tag: "Unknown" } +const json: Declaration = { + _tag: "Declaration", + annotations: { + expected: "JSON value", + generation: { + Type: "Schema.Json", + runtime: "Schema.Json" + }, + typeConstructor: { + _tag: "effect/Json" + } + }, + checks: [], + encodedSchema: unknown, + typeParameters: [] +} +const never: Never = { _tag: "Never" } +const null_: Null = { _tag: "Null" } +const string: String = { _tag: "String", checks: [] } +const boolean: Boolean = { _tag: "Boolean" } + +function collectAnnotations( + schema: JsonSchema.JsonSchema +): Schema.Annotations.Annotations | undefined { + const as: Record = {} + + if (typeof schema.title === "string") as.title = schema.title + if (typeof schema.description === "string") as.description = schema.description + if (schema.default !== undefined) as.default = schema.default + if (Array.isArray(schema.examples)) as.examples = schema.examples + if (typeof schema.readOnly === "boolean") as.readOnly = schema.readOnly + if (typeof schema.writeOnly === "boolean") as.writeOnly = schema.writeOnly + if (typeof schema.format === "string") as.format = schema.format + if (typeof schema.contentEncoding === "string") as.contentEncoding = schema.contentEncoding + if (typeof schema.contentMediaType === "string") as.contentMediaType = schema.contentMediaType + + return Rec.isEmptyRecord(as) ? undefined : as +} + +function isLiteralValue(value: unknown): value is SchemaAST.LiteralValue { + return typeof value === "string" || typeof value === "number" || typeof value === "boolean" +} + +const stringKeys = ["minLength", "maxLength", "pattern", "format", "contentMediaType", "contentSchema"] +const numberKeys = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] +const objectKeys = [ + "properties", + "required", + "additionalProperties", + "patternProperties", + "propertyNames", + "minProperties", + "maxProperties" +] +const arrayKeys = ["items", "prefixItems", "additionalItems", "minItems", "maxItems", "uniqueItems"] + +function getType(js: JsonSchema.JsonSchema): JsonSchema.Type | undefined { + if (stringKeys.some((key) => js[key] !== undefined)) { + return "string" + } + if (numberKeys.some((key) => js[key] !== undefined)) { + return "number" + } + if (objectKeys.some((key) => js[key] !== undefined)) { + return "object" + } + if (arrayKeys.some((key) => js[key] !== undefined)) { + return "array" + } +} + +const types = ["null", "string", "number", "integer", "boolean", "object", "array"] + +function isType(type: unknown): type is JsonSchema.Type { + return typeof type === "string" && types.includes(type) +} + +/** @internal */ +export type TopologicalSort = { + /** + * The definitions that are not recursive. + * The definitions that depends on other definitions are placed after the definitions they depend on + */ + readonly nonRecursives: ReadonlyArray<{ + readonly $ref: string + readonly representation: Representation + }> + /** + * The recursive definitions (with no particular order). + */ + readonly recursives: { + readonly [$ref: string]: Representation + } +} + +/** @internal */ +export function topologicalSort(references: References): TopologicalSort { + const identifiers = Object.keys(references) + const identifierSet = new Set(identifiers) + + const collectRefs = (root: Representation): Set => { + const refs = new Set() + const visited = new WeakSet() + const stack: Array = [root] + + while (stack.length > 0) { + const r = stack.pop()! + if (visited.has(r)) continue + visited.add(r) + + if (r._tag === "Reference") { + if (identifierSet.has(r.$ref)) { + refs.add(r.$ref) + } + } + + // Push nested Representation schemas onto the stack + switch (r._tag) { + case "Declaration": + for (const typeParam of r.typeParameters) stack.push(typeParam) + stack.push(r.encodedSchema) + break + case "Suspend": + stack.push(r.thunk) + break + case "String": + if (r.contentSchema !== undefined) stack.push(r.contentSchema) + break + case "TemplateLiteral": + for (const part of r.parts) stack.push(part) + break + case "Arrays": + for (const element of r.elements) stack.push(element.type) + for (const rest of r.rest) stack.push(rest) + break + case "Objects": + for (const propertySignature of r.propertySignatures) stack.push(propertySignature.type) + for (const indexSignature of r.indexSignatures) { + stack.push(indexSignature.parameter) + stack.push(indexSignature.type) + } + break + case "Union": + for (const type of r.types) stack.push(type) + break + } + } + + return refs + } + + // identifier -> internal identifiers it depends on + const dependencies = new Map>( + identifiers.map((id) => [id, collectRefs(references[id])]) + ) + + // Mark only nodes that are part of cycles + const recursive = new Set() + const state = new Map() // 0 = new, 1 = visiting, 2 = done + const stack: Array = [] + const indexInStack = new Map() + + const dfs = (id: string): void => { + const s = state.get(id) ?? 0 + if (s === 1) { + const start = indexInStack.get(id) + if (start !== undefined) { + for (let i = start; i < stack.length; i++) { + recursive.add(stack[i]) + } + } + return + } + if (s === 2) return + + state.set(id, 1) + indexInStack.set(id, stack.length) + stack.push(id) + + for (const dep of dependencies.get(id) ?? []) { + dfs(dep) + } + + stack.pop() + indexInStack.delete(id) + state.set(id, 2) + } + + for (const id of identifiers) dfs(id) + + // Topologically sort the non-recursive nodes (ignoring edges to recursive nodes) + const inDegree = new Map() + const dependents = new Map>() // dep -> nodes that depend on it + + for (const id of identifiers) { + if (!recursive.has(id)) { + inDegree.set(id, 0) + dependents.set(id, new Set()) + } + } + + for (const [id, deps] of dependencies) { + if (recursive.has(id)) continue + for (const dep of deps) { + if (recursive.has(dep)) continue + inDegree.set(id, (inDegree.get(id) ?? 0) + 1) + dependents.get(dep)?.add(id) + } + } + + const queue: Array = [] + for (const [id, deg] of inDegree) { + if (deg === 0) queue.push(id) + } + + const nonRecursives: Array<{ readonly $ref: string; readonly representation: Representation }> = [] + for (let i = 0; i < queue.length; i++) { + const $ref = queue[i] + nonRecursives.push({ $ref, representation: references[$ref] }) + + for (const next of dependents.get($ref) ?? []) { + const deg = (inDegree.get(next) ?? 0) - 1 + inDegree.set(next, deg) + if (deg === 0) queue.push(next) + } + } + + const recursives: Record = {} + for (const $ref of recursive) { + recursives[$ref] = references[$ref] + } + + return { nonRecursives, recursives } +} diff --git a/repos/effect/packages/effect/src/SchemaTransformation.ts b/repos/effect/packages/effect/src/SchemaTransformation.ts new file mode 100644 index 0000000..a9709e1 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaTransformation.ts @@ -0,0 +1,1857 @@ +/** + * Builds two-way conversions used by schemas. + * + * A `Transformation` describes how to decode an encoded value into a + * decoded value and how to encode it back again. Schema APIs use + * transformations to connect two representations, such as a string and a + * number, a JSON value and a richer TypeScript value, or a form field and an + * application value. This module includes transformation and middleware types, + * constructors for pure or effectful conversions, and common conversions used + * by the Schema module. + * + * @since 4.0.0 + */ + +import * as BigDecimal from "./BigDecimal.ts" +import * as DateTime from "./DateTime.ts" +import * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import { format, formatDate, formatJson } from "./Formatter.ts" +import * as Option from "./Option.ts" +import * as Predicate from "./Predicate.ts" +import type { ErrorOptions, Json } from "./Schema.ts" +import type * as SchemaAST from "./SchemaAST.ts" +import * as SchemaGetter from "./SchemaGetter.ts" +import * as SchemaIssue from "./SchemaIssue.ts" + +/** + * Middleware that wraps the entire parsing `Effect` pipeline for both + * decode and encode directions. + * + * **When to use** + * + * Use when you need a schema middleware to catch or recover from parsing + * errors (e.g. `Schema.catchDecoding`), run side effects around the parsing + * pipeline, or access the full `Effect` rather than a single decoded value. + * + * **Details** + * + * Unlike `Transformation`, which operates on individual values via `Getter`, + * `Middleware` receives the full `Effect` produced by the inner schema and can + * intercept, modify, retry, or replace it. + * + * - `decode` receives an `Effect, Issue, RDE>` and returns + * `Effect, Issue, RDT>`. + * - `encode` receives an `Effect, Issue, RET>` and returns + * `Effect, Issue, REE>`. + * - `flip()` swaps the decode and encode functions, producing a + * `Middleware`. + * + * Typically constructed indirectly via `Schema.middlewareDecoding` or + * `Schema.middlewareEncoding` rather than instantiating this class directly. + * + * **Example** (Creating a middleware that falls back on decode failure) + * + * ```ts + * import { Effect, Option, SchemaTransformation } from "effect" + * + * const fallback = new SchemaTransformation.Middleware( + * (effect) => Effect.catch(effect, () => Effect.succeed(Option.some("fallback"))), + * (effect) => effect + * ) + * ``` + * + * @see {@link Transformation} — value-level bidirectional transformation + * + * @category models + * @since 4.0.0 + */ +export class Middleware { + readonly _tag = "Middleware" + readonly decode: ( + effect: Effect.Effect, SchemaIssue.Issue, RDE>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, RDT> + readonly encode: ( + effect: Effect.Effect, SchemaIssue.Issue, RET>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, REE> + + constructor( + decode: ( + effect: Effect.Effect, SchemaIssue.Issue, RDE>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, RDT>, + encode: ( + effect: Effect.Effect, SchemaIssue.Issue, RET>, + options: SchemaAST.ParseOptions + ) => Effect.Effect, SchemaIssue.Issue, REE> + ) { + this.decode = decode + this.encode = encode + } + flip(): Middleware { + return new Middleware(this.encode, this.decode) + } +} + +const TypeId = "~effect/SchemaTransformation/Transformation" + +/** + * Represents a bidirectional transformation between a decoded type `T` and an encoded + * type `E`, built from a pair of `Getter`s. + * + * **When to use** + * + * Use when you need a schema transformation that defines how a schema converts + * between two representations. + * - You want to compose multiple transformations into a pipeline. + * - You want to flip a transformation to swap decode/encode. + * + * **Details** + * + * This is the primary building block for `Schema.decodeTo`, `Schema.encodeTo`, + * `Schema.decode`, `Schema.encode`, and `Schema.link`. Each direction is a + * `SchemaGetter.Getter` that handles optionality, failure, and Effect services. + * + * - Immutable — `flip()` and `compose()` return new instances. + * - `flip()` swaps the decode and encode getters. + * - `compose(other)` chains: `this.decode` then `other.decode` for decoding, + * `other.encode` then `this.encode` for encoding. + * + * **Example** (Composing two transformations) + * + * ```ts + * import { SchemaTransformation } from "effect" + * + * const trimAndLower = SchemaTransformation.trim().compose( + * SchemaTransformation.toLowerCase() + * ) + * // decode: trim then lowercase + * // encode: passthrough (both directions) + * ``` + * + * @see {@link make} — construct from `{ decode, encode }` getters + * @see {@link transform} — construct from pure functions + * @see {@link transformOrFail} — construct from effectful functions + * @see {@link Middleware} — effect-pipeline-level alternative + * + * @category models + * @since 4.0.0 + */ +export class Transformation { + readonly [TypeId] = TypeId + readonly _tag = "Transformation" + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter + + constructor( + decode: SchemaGetter.Getter, + encode: SchemaGetter.Getter + ) { + this.decode = decode + this.encode = encode + } + flip(): Transformation { + return new Transformation(this.encode, this.decode) + } + compose(other: Transformation): Transformation { + return new Transformation( + this.decode.compose(other.decode), + other.encode.compose(this.encode) + ) + } +} + +/** + * Returns `true` if `u` is a `Transformation` instance. + * + * **When to use** + * + * Use to check whether a value is already a schema transformation before + * wrapping it. + * + * **Details** + * + * - Pure predicate, no side effects. + * - Acts as a TypeScript type guard. + * + * **Example** (Checking a value) + * + * ```ts + * import { SchemaTransformation } from "effect" + * + * SchemaTransformation.isTransformation(SchemaTransformation.trim()) + * // true + * + * SchemaTransformation.isTransformation({ decode: null, encode: null }) + * // false + * ``` + * + * @see {@link Transformation} + * @see {@link make} + * + * @category guards + * @since 4.0.0 + */ +export function isTransformation(u: unknown): u is Transformation { + return Predicate.hasProperty(u, TypeId) +} + +/** + * Constructs a `Transformation` from an object with `decode` and `encode` + * `Getter`s. If the input is already a `Transformation`, returns it as-is. + * + * **When to use** + * + * Use when you already have schema getter instances and want to pair them into + * a schema transformation. + * - You want idempotent wrapping (won't double-wrap). + * + * **Details** + * + * - Returns the input unchanged if it is already a `Transformation`. + * + * **Example** (Wrapping existing getters) + * + * ```ts + * import { SchemaGetter, SchemaTransformation } from "effect" + * + * const t = SchemaTransformation.make({ + * decode: SchemaGetter.transform((s) => Number(s)), + * encode: SchemaGetter.transform((n) => String(n)) + * }) + * ``` + * + * @see {@link transform} — simpler constructor from pure functions + * @see {@link transformOrFail} — constructor from effectful functions + * @see {@link Transformation} + * + * @category constructors + * @since 3.10.0 + */ +export const make = (options: { + readonly decode: SchemaGetter.Getter + readonly encode: SchemaGetter.Getter +}): Transformation => { + if (isTransformation(options)) { + return options as any + } + return new Transformation(options.decode, options.encode) +} + +/** + * Creates a `Transformation` from effectful decode and encode functions that + * can fail with `Issue`. + * + * **When to use** + * + * Use when you need a schema transformation that may fail or require Effect + * services. + * + * **Details** + * + * - Each function receives the input value and `ParseOptions`. + * - Must return an `Effect` that succeeds with the output or fails with `Issue`. + * - Skips `None` inputs (missing keys) — functions are only called on present values. + * + * **Example** (Parsing a date string that can fail) + * + * ```ts + * import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" + * + * const DateFromString = Schema.String.pipe( + * Schema.decodeTo( + * Schema.Date, + * SchemaTransformation.transformOrFail({ + * decode: (s) => { + * const d = new Date(s) + * return isNaN(d.getTime()) + * ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "Invalid date" })) + * : Effect.succeed(d) + * }, + * encode: (d) => Effect.succeed(d.toISOString()) + * }) + * ) + * ) + * ``` + * + * @see {@link transform} — for infallible, pure transformations + * @see {@link transformOptional} — for transformations that handle missing keys + * @see {@link make} — for transformations from existing Getters + * + * @category constructors + * @since 3.10.0 + */ +export function transformOrFail(options: { + readonly decode: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect + readonly encode: (t: T, options: SchemaAST.ParseOptions) => Effect.Effect +}): Transformation { + return new Transformation( + SchemaGetter.transformOrFail(options.decode), + SchemaGetter.transformOrFail(options.encode) + ) +} + +/** + * Creates a `Transformation` from pure (sync, infallible) decode and encode + * functions. + * + * **When to use** + * + * Use when you need an infallible schema transformation that does not require + * Effect services. + * + * **Details** + * + * - Each function receives the input and returns the output directly. + * - Skips `None` inputs (missing keys) — functions are only called on present values. + * - Does not allocate Effects internally; uses optimized sync path. + * + * **Example** (Converting between cents and dollars) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const CentsFromDollars = Schema.Number.pipe( + * Schema.decodeTo( + * Schema.Number, + * SchemaTransformation.transform({ + * decode: (dollars) => dollars * 100, + * encode: (cents) => cents / 100 + * }) + * ) + * ) + * ``` + * + * @see {@link transformOrFail} — for fallible or effectful transformations + * @see {@link transformOptional} — for transformations that handle missing keys + * @see {@link passthrough} — when no conversion is needed + * + * @category constructors + * @since 3.10.0 + */ +export function transform(options: { + readonly decode: (input: E) => T + readonly encode: (input: T) => E +}): Transformation { + return new Transformation( + SchemaGetter.transform(options.decode), + SchemaGetter.transform(options.encode) + ) +} + +/** + * Creates a `Transformation` where decode and encode operate on `Option` + * values, giving full control over missing-key handling. + * + * **When to use** + * + * Use when you need a schema transformation to produce or consume `Option.None` + * for absent keys. + * - You are working with optional struct fields. + * + * **Details** + * + * - Each function receives `Option` and returns `Option`. + * - `Option.None` input means the key is absent; returning `Option.None` + * omits the key from the output. + * - Pure and synchronous. + * + * **Example** (Converting an optional key to Option) + * + * ```ts + * import { Option, Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Struct({ + * a: Schema.optionalKey(Schema.Number).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.Number), + * SchemaTransformation.transformOptional({ + * decode: Option.some, + * encode: Option.flatten + * }) + * ) + * ) + * }) + * ``` + * + * @see {@link transform} — when you don't need Option-level control + * @see {@link optionFromOptionalKey} — built-in for the common optional-key-to-Option pattern + * @see {@link optionFromOptional} — built-in for optional (undefined) to Option + * + * @category constructors + * @since 4.0.0 + */ +export function transformOptional(options: { + readonly decode: (input: Option.Option) => Option.Option + readonly encode: (input: Option.Option) => Option.Option +}): Transformation { + return new Transformation( + SchemaGetter.transformOptional(options.decode), + SchemaGetter.transformOptional(options.encode) + ) +} + +/** + * Transforms strings by trimming whitespace on decode. + * Encode is passthrough (no change). + * + * **When to use** + * + * Use when you need a schema transformation to normalize user input by + * stripping leading/trailing whitespace. + * + * **Details** + * + * Decoding applies `String.prototype.trim()`. Encoding is passthrough and + * returns the string unchanged. This is not round-trippable if the original had + * whitespace. + * + * **Example** (Trimming on decode) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Trimmed = Schema.String.pipe( + * Schema.decode(SchemaTransformation.trim()) + * ) + * ``` + * + * @see {@link toLowerCase} + * @see {@link toUpperCase} + * @see {@link snakeToCamel} + * + * @category String transformations + * @since 4.0.0 + */ +export function trim(): Transformation { + return new Transformation( + SchemaGetter.trim(), + SchemaGetter.passthrough() + ) +} + +/** + * Transforms strings by converting snake_case to camelCase + * on decode and camelCase to snake_case on encode. + * + * **When to use** + * + * Use when you need a schema transformation to convert API field names between + * snake_case and camelCase conventions. + * + * **Details** + * + * Decoding converts values such as `"my_field_name"` to `"myFieldName"`. + * Encoding converts values such as `"myFieldName"` back to `"my_field_name"`. + * The transformation is round-trippable for standard snake_case and camelCase. + * + * **Example** (Converting snake case to camel case) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const SnakeToCamel = Schema.String.pipe( + * Schema.decode(SchemaTransformation.snakeToCamel()) + * ) + * ``` + * + * @see {@link trim} + * @see {@link toLowerCase} + * + * @category String transformations + * @since 4.0.0 + */ +export function snakeToCamel(): Transformation { + return new Transformation( + SchemaGetter.snakeToCamel(), + SchemaGetter.camelToSnake() + ) +} + +/** + * Transforms strings by lowercasing on decode. + * Encode is passthrough. + * + * **When to use** + * + * Use when you need a schema transformation to normalize strings to lowercase + * (e.g. email addresses). + * + * **Details** + * + * Decoding applies `String.prototype.toLowerCase()`. Encoding is passthrough. + * This is not round-trippable if the original had uppercase characters. + * + * **Example** (Lowercasing on decode) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Lowered = Schema.String.pipe( + * Schema.decode(SchemaTransformation.toLowerCase()) + * ) + * ``` + * + * @see {@link toUpperCase} + * @see {@link trim} + * + * @category String transformations + * @since 4.0.0 + */ +export function toLowerCase(): Transformation { + return new Transformation( + SchemaGetter.toLowerCase(), + SchemaGetter.passthrough() + ) +} + +/** + * Transforms strings by uppercasing on decode. + * Encode is passthrough. + * + * **When to use** + * + * Use when you need a schema transformation to normalize strings to uppercase + * (e.g. country codes). + * + * **Details** + * + * Decoding applies `String.prototype.toUpperCase()`. Encoding is passthrough. + * This is not round-trippable if the original had lowercase characters. + * + * **Example** (Uppercasing on decode) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Uppered = Schema.String.pipe( + * Schema.decode(SchemaTransformation.toUpperCase()) + * ) + * ``` + * + * @see {@link toLowerCase} + * @see {@link trim} + * + * @category String transformations + * @since 4.0.0 + */ +export function toUpperCase(): Transformation { + return new Transformation( + SchemaGetter.toUpperCase(), + SchemaGetter.passthrough() + ) +} + +/** + * Transforms strings by capitalizing the first character on + * decode. Encode is passthrough. + * + * **When to use** + * + * Use when you need a schema transformation to normalize display names or + * titles. + * + * **Details** + * + * Decoding uppercases the first character and leaves the rest unchanged. + * Encoding is passthrough. + * + * **Example** (Capitalizing on decode) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Capitalized = Schema.String.pipe( + * Schema.decode(SchemaTransformation.capitalize()) + * ) + * ``` + * + * @see {@link uncapitalize} + * @see {@link toUpperCase} + * + * @category String transformations + * @since 4.0.0 + */ +export function capitalize(): Transformation { + return new Transformation( + SchemaGetter.capitalize(), + SchemaGetter.passthrough() + ) +} + +/** + * Transforms strings by lowercasing the first character on + * decode. Encode is passthrough. + * + * **When to use** + * + * Use when you need a schema transformation to normalize identifiers or field + * names. + * + * **Details** + * + * Decoding lowercases the first character and leaves the rest unchanged. + * Encoding is passthrough. + * + * **Example** (Uncapitalizing on decode) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Uncapitalized = Schema.String.pipe( + * Schema.decode(SchemaTransformation.uncapitalize()) + * ) + * ``` + * + * @see {@link capitalize} + * @see {@link toLowerCase} + * + * @category String transformations + * @since 4.0.0 + */ +export function uncapitalize(): Transformation { + return new Transformation( + SchemaGetter.uncapitalize(), + SchemaGetter.passthrough() + ) +} + +/** + * Transforms a string into a record of key-value pairs and + * encodes a record of key-value pairs into a string. + * + * **When to use** + * + * Use when you need a schema transformation to parse query-string-like or + * config-file-like strings into records. + * + * **Details** + * + * Decoding splits the string by `separator` (default `","`) into pairs, then + * splits each pair by `keyValueSeparator` (default `"="`). Encoding joins the + * record back into a string using the same separators. The transformation is + * round-trippable when keys and values do not contain the separators. + * + * **Example** (Parsing key-value pairs) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const Config = Schema.String.pipe( + * Schema.decodeTo( + * Schema.Record(Schema.String, Schema.String), + * SchemaTransformation.splitKeyValue({ separator: ";", keyValueSeparator: ":" }) + * ) + * ) + * // "host:localhost;port:3000" → { host: "localhost", port: "3000" } + * ``` + * + * @see {@link trim} + * @see {@link snakeToCamel} + * + * @category String transformations + * @since 4.0.0 + */ +export function splitKeyValue(options?: { + readonly separator?: string | undefined + readonly keyValueSeparator?: string | undefined +}): Transformation, string> { + return new Transformation( + SchemaGetter.splitKeyValue(options), + SchemaGetter.joinKeyValue(options) + ) +} + +const passthrough_ = new Transformation( + SchemaGetter.passthrough(), + SchemaGetter.passthrough() +) + +/** + * Transforms values by returning the input unchanged in both + * directions. + * + * **When to use** + * + * Use when you need a schema transformation to connect two schemas that share + * the same type with no actual conversion. + * + * **Details** + * + * - Both decode and encode are no-ops. + * - Returns a shared singleton instance (no allocation per call). + * - By default, `T` and `E` must be the same type. Pass `{ strict: false }` + * to bypass the type constraint. + * + * **Example** (Chaining schemas with no conversion) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Trim.pipe( + * Schema.decodeTo(Schema.FiniteFromString, SchemaTransformation.passthrough()) + * ) + * ``` + * + * @see {@link passthroughSupertype} + * @see {@link passthroughSubtype} + * @see {@link transform} + * + * @category constructors + * @since 4.0.0 + */ +export function passthrough(options: { readonly strict: false }): Transformation +export function passthrough(): Transformation +export function passthrough(): Transformation { + return passthrough_ +} + +/** + * Transforms values without changing them, typed so that `T extends E`, where the decoded + * type `T` is a subtype of the encoded type `E`. + * + * **When to use** + * + * Use when you need a no-op schema transformation whose decoded side is + * narrower than the encoded side. + * + * **Details** + * + * Both decode and encode are no-ops and return a shared singleton + * transformation. + * + * **Example** (Passing through supertypes) + * + * ```ts + * import { SchemaTransformation } from "effect" + * + * const t = SchemaTransformation.passthroughSupertype<"a" | "b", string>() + * ``` + * + * @see {@link passthrough} + * @see {@link passthroughSubtype} + * + * @category constructors + * @since 4.0.0 + */ +export function passthroughSupertype(): Transformation +export function passthroughSupertype(): Transformation { + return passthrough_ +} + +/** + * Transforms values without changing them, typed so that `E extends T` — the encoded + * type is a subtype of the decoded type. + * + * **When to use** + * + * Use when you need a no-op schema transformation whose encoded side is more + * specific than its decoded side. + * + * **Details** + * + * - Both decode and encode are no-ops (same as {@link passthrough}). + * - Returns a shared singleton instance. + * + * **Example** (Passing through subtypes) + * + * ```ts + * import { SchemaTransformation } from "effect" + * + * const t = SchemaTransformation.passthroughSubtype() + * ``` + * + * @see {@link passthrough} + * @see {@link passthroughSupertype} + * + * @category constructors + * @since 4.0.0 + */ +export function passthroughSubtype(): Transformation +export function passthroughSubtype(): Transformation { + return passthrough_ +} + +/** + * Decodes a `string` into a `number` and encodes a `number` back to a + * `string`. + * + * **When to use** + * + * Use when you need a schema transformation to parse numeric strings from APIs, + * form data, or URL parameters. + * + * **Details** + * + * Decoding coerces the string to a number like `Number(s)`. Encoding coerces + * the number to a string like `String(n)`. This does not validate that the + * result is finite; combine with `Schema.Finite` or `Schema.Int` for stricter + * checks. + * + * **Example** (Converting a string to a number) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Number, SchemaTransformation.numberFromString) + * ) + * ``` + * + * @see {@link bigintFromString} + * @see {@link transform} + * + * @category Coercions + * @since 4.0.0 + */ +export const numberFromString = new Transformation( + SchemaGetter.Number(), + SchemaGetter.String() +) + +/** + * Decodes a `string` into a `bigint` and encodes a `bigint` back to a + * `string`. + * + * **When to use** + * + * Use when you need a schema transformation to parse large integer strings + * (e.g. database IDs, blockchain values). + * + * **Details** + * + * Decoding coerces the string to a bigint like `BigInt(s)`. Encoding coerces + * the bigint to a string like `String(n)`. Decoding fails if the string is not + * a valid bigint representation. + * + * **Example** (Converting a string to a BigInt) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.BigInt, SchemaTransformation.bigintFromString) + * ) + * ``` + * + * @see {@link numberFromString} + * @see {@link transform} + * + * @category Coercions + * @since 4.0.0 + */ +export const bigintFromString = new Transformation( + SchemaGetter.BigInt(), + SchemaGetter.String() +) + +/** + * Decodes a `string` into a `Date` and encodes a `Date` back to a `string`. + * + * **When to use** + * + * Use when you need a schema transformation to parse ISO 8601 date strings from + * APIs or user input. + * + * **Details** + * + * Decoding creates a `Date` from the string like `new Date(s)`. Encoding + * converts the `Date` to an ISO string like `date.toISOString()`, returning + * `"Invalid Date"` for invalid dates. + * + * **Example** (Converting a string to a Date) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString) + * ) + * ``` + * + * @see {@link dateFromMillis} + * @see {@link dateTimeUtcFromString} + * + * @category Coercions + * @since 4.0.0 + */ +export const dateFromString: Transformation = new Transformation( + SchemaGetter.Date(), + SchemaGetter.transform(formatDate) +) + +/** + * Decodes epoch milliseconds into a `Date` and encodes a `Date` back to epoch + * milliseconds. + * + * **When to use** + * + * Use when you need a schema transformation for numeric timestamps represented + * as milliseconds since the Unix epoch. + * + * **Details** + * + * Decoding creates a `Date` from the number like `new Date(ms)`. Encoding + * returns the `Date` timestamp like `date.getTime()`. + * + * **Gotchas** + * + * This transformation does not validate date validity. `NaN`, `Infinity`, and + * `-Infinity` decode to invalid `Date` instances. + * + * **Example** (Converting milliseconds to a Date) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Number.pipe( + * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromMillis) + * ) + * ``` + * + * @see {@link dateFromString} + * @see {@link SchemaGetter.dateTimeUtcFromInput} + * + * @category Coercions + * @since 4.0.0 + */ +export const dateFromMillis: Transformation = new Transformation( + SchemaGetter.Date(), + SchemaGetter.transform((date) => date.getTime()) +) + +/** + * Decodes a `string` into a `Duration` and encodes a `Duration` back to a + * parseable `string`. + * + * **When to use** + * + * Use when you need a schema transformation to parse human-readable duration + * strings from APIs, config, or user input. + * + * **Details** + * + * Decoding accepts any string that `Duration.fromInput` can parse, including + * `"Infinity"` and `"-Infinity"`. Encoding returns `String(duration)`, + * producing strings such as `"2000 millis"` or `"10 nanos"` that round-trip + * through the parser. + * + * **Example** (Converting a string to a Duration) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromString) + * ) + * ``` + * + * @see {@link durationFromNanos} + * @see {@link durationFromMillis} + * + * @category transforming + * @since 4.0.0 + */ +export const durationFromString: Transformation = transformOrFail< + Duration.Duration, + string +>({ + decode: (s) => + Option.match(Duration.fromInput(s as Duration.Input), { + onNone: () => + Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid Duration string: ${s}` })), + onSome: Effect.succeed + }), + encode: (duration) => Effect.succeed(globalThis.String(duration)) +}) + +/** + * Decodes a `bigint` (nanoseconds) into a `Duration` and encodes a + * `Duration` back to `bigint` nanoseconds. + * + * **When to use** + * + * Use when you need a schema transformation for nanosecond-precision timestamps + * or intervals. + * + * **Details** + * + * Decoding always succeeds and creates a `Duration` from nanoseconds. Encoding + * fails with `InvalidValue` if the `Duration` cannot be represented as a + * `bigint`, such as `Duration.infinity`. + * + * **Example** (Converting nanoseconds to a Duration) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.BigInt.pipe( + * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromNanos) + * ) + * ``` + * + * @see {@link durationFromMillis} + * + * @category transforming + * @since 4.0.0 + */ +export const durationFromNanos: Transformation = transformOrFail({ + decode: (i) => Effect.succeed(Duration.nanos(i)), + encode: (a) => + Option.match(Duration.toNanos(a), { + onNone: () => + Effect.fail( + new SchemaIssue.InvalidValue(Option.some(a), { message: `Unable to encode ${a} into a bigint` }) + ), + onSome: (nanos) => Effect.succeed(nanos) + }) +}) + +/** + * Decodes a `number` of milliseconds into a `Duration` and encodes a `Duration` + * back to milliseconds. + * + * **When to use** + * + * Use when you need a schema transformation to decode timeouts, delays, elapsed + * intervals, or other duration values stored as millisecond counts. + * + * **Details** + * + * Decode creates a duration from the number, and encode returns the duration + * length in milliseconds. + * + * **Example** (Converting milliseconds to a Duration) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Number.pipe( + * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromMillis) + * ) + * ``` + * + * @see {@link durationFromNanos} + * + * @category transforming + * @since 4.0.0 + */ +export const durationFromMillis: Transformation = transform({ + decode: (i) => Duration.millis(i), + encode: (a) => Duration.toMillis(a) +}) + +type JsonError = { + message: string + name?: string + stack?: string + cause?: Json +} + +const isJsonError = (input: unknown): input is JsonError => + Predicate.isObject(input) && typeof input["message"] === "string" + +const decodeJsonError = (input: JsonError): Error => { + const hasCause = Object.hasOwn(input, "cause") + const err = hasCause + ? new Error(input.message, { cause: decodeDefect(input.cause as Json) }) + : new Error(input.message) + if (typeof input.name === "string" && input.name !== "Error") err.name = input.name + if (typeof input.stack === "string") err.stack = input.stack + return err +} + +const encodeUnknownAsJson = (input: unknown): Json => { + try { + const json = formatJson(input) + return json === undefined ? format(input) : JSON.parse(json) + } catch { + return format(input) + } +} + +const encodeJsonError = ( + input: Error, + options: ErrorOptions | undefined, + encodeDefect: (input: unknown) => Json +): JsonError => { + const encoded: JsonError = { + name: input.name, + message: typeof input.message === "string" ? input.message : "" + } + if (options?.includeStack && typeof input.stack === "string") { + encoded.stack = input.stack + } + if (!options?.excludeCause && input.cause !== undefined) { + encoded.cause = encodeDefect(input.cause) + } + return encoded +} + +const makeEncodeDefect = (options?: ErrorOptions): (input: unknown) => Json => { + const seen = new WeakSet() + const encode = (input: unknown): Json => { + if (Predicate.isError(input)) { + if (seen.has(input)) { + return "[Circular]" + } + seen.add(input) + const encoded = encodeJsonError(input, options, encode) + seen.delete(input) + return encoded + } + return encodeUnknownAsJson(input) + } + return encode +} + +const decodeDefect = (input: Json): unknown => isJsonError(input) ? decodeJsonError(input) : input + +/** @internal */ +export const errorFromJsonError = (options?: ErrorOptions): Transformation => + transform({ + decode: decodeJsonError, + encode: (input) => makeEncodeDefect(options)(input) as JsonError + }) + +/** @internal */ +export const defectFromJson = (options?: ErrorOptions) => + transform({ + decode: decodeDefect, + encode: makeEncodeDefect(options) + }) + +/** + * Decodes `T | null` into `Option` and encodes `Option` back to + * `T | null`. + * + * **When to use** + * + * Use when you need a schema transformation to convert nullable API fields to + * `Option`. + * + * **Details** + * + * Decoding maps `null` to `Option.none()` and non-null values to + * `Option.some(value)`. Encoding maps `Option.none()` to `null` and + * `Option.some(value)` to `value`. The transformation is pure and synchronous. + * + * **Example** (Converting nullable values to an Option) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.NullOr(Schema.String).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.String), + * SchemaTransformation.optionFromNullOr() + * ) + * ) + * ``` + * + * @see {@link optionFromNullishOr} + * + * @category transforming + * @since 4.0.0 + */ +export function optionFromNullOr(): Transformation, T | null> { + return transform({ + decode: Option.fromNullOr, + encode: Option.getOrNull + }) +} + +/** + * Decodes `T | undefined` into `Option` and encodes `Option.none()` back to + * `undefined`. + * + * **When to use** + * + * Use when you need a schema transformation to convert API fields that use + * `undefined` for absence to `Option`. + * + * **Details** + * + * Decoding maps `undefined` to `Option.none()` and non-undefined values to + * `Option.some(value)`. Encoding maps `Option.none()` to `undefined` and + * `Option.some(value)` to `value`. The transformation is pure and synchronous. + * + * **Example** (Converting undefined-or values to an Option) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.UndefinedOr(Schema.String).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.String), + * SchemaTransformation.optionFromUndefinedOr() + * ) + * ) + * ``` + * + * @see {@link optionFromOptionalKey} + * @see {@link optionFromOptional} + * + * @category transforming + * @since 4.0.0 + */ +export function optionFromUndefinedOr(): Transformation, T | undefined> { + return transform({ + decode: Option.fromUndefinedOr, + encode: Option.getOrUndefined + }) +} + +/** + * Decodes `T | null | undefined` into `Option` and encodes `Option` + * back to `T | null` or `T | undefined` depending on the provided + * `options.onNoneEncoding` (defaults to `undefined`). + * + * **When to use** + * + * Use when you need a schema transformation to convert nullish API fields to + * `Option` when both `null` and `undefined` represent absence. + * + * **Details** + * + * Decoding maps `null` and `undefined` to `Option.none()` and all other values + * to `Option.some(value)`. Encoding maps `Option.none()` to `null` or + * `undefined` according to `options.onNoneEncoding`, and maps + * `Option.some(value)` to `value`. The transformation is pure and synchronous. + * + * **Example** (Converting nullish values to an Option and encoding None as null) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.NullishOr(Schema.String).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.String), + * SchemaTransformation.optionFromNullishOr({ onNoneEncoding: null }) + * ) + * ) + * ``` + * + * @see {@link optionFromNullOr} + * @see {@link optionFromUndefinedOr} + * + * @category transforming + * @since 4.0.0 + */ +export function optionFromNullishOr( + options?: { + onNoneEncoding: null | undefined + } +): Transformation, T | null | undefined> { + return transform({ + decode: Option.fromNullishOr, + encode: options?.onNoneEncoding === null ? Option.getOrNull : Option.getOrUndefined + }) +} + +/** + * Decodes an optional struct key into `Option` and encodes `Option` + * back to an optional key. + * + * **When to use** + * + * Use when you need a schema transformation to convert optional struct keys + * (declared with `Schema.optionalKey`) to `Option` values. + * + * **Details** + * + * Decoding maps an absent key (`None`) to `Some(None)` and a present key + * (`Some(v)`) to `Some(Some(v))`. Encoding maps `Some(None)` to `None` to omit + * the key, and maps `Some(Some(v))` to `Some(v)`. This uses + * `transformOptional` under the hood. + * + * **Example** (Converting an optional key to an Option) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Struct({ + * name: Schema.optionalKey(Schema.String).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.String), + * SchemaTransformation.optionFromOptionalKey() + * ) + * ) + * }) + * ``` + * + * @see {@link optionFromOptional} + * @see {@link optionFromUndefinedOr} + * @see {@link transformOptional} + * + * @category transforming + * @since 4.0.0 + */ +export function optionFromOptionalKey(): Transformation, T> { + return transformOptional({ + decode: Option.some, + encode: Option.flatten + }) +} + +/** + * Decodes optional values into `Option` and encodes `Option.none()` back to + * an omitted optional value. + * + * **When to use** + * + * Use when you need a schema transformation to convert optional (possibly + * `undefined`) values to `Option`. + * + * **Details** + * + * Decoding maps an absent or `undefined` value to `Some(None)` and a present + * value to `Some(Some(v))`. Encoding maps `Some(None)` to `None` to omit the + * value, and maps `Some(Some(v))` to `Some(v)`. This uses + * `transformOptional` under the hood and filters out `undefined` on decode. + * + * **Example** (Converting an optional value to an Option) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.Struct({ + * age: Schema.optional(Schema.Number).pipe( + * Schema.decodeTo( + * Schema.Option(Schema.Number), + * SchemaTransformation.optionFromOptional() + * ) + * ) + * }) + * ``` + * + * @see {@link optionFromOptionalKey} + * @see {@link optionFromUndefinedOr} + * @see {@link transformOptional} + * + * @category transforming + * @since 4.0.0 + */ +export function optionFromOptional(): Transformation, T | undefined> { + return transformOptional, T | undefined>({ + decode: (ot) => ot.pipe(Option.filter(Predicate.isNotUndefined), Option.some), + encode: Option.flatten + }) +} + +/** + * Decodes a `string` into a `URL` and encodes a `URL` back to its `href` + * string. + * + * **When to use** + * + * Use when you need a schema transformation to parse URL strings from user + * input or API responses. + * + * **Details** + * + * Decoding checks `URL.canParse(s)` and fails with `InvalidValue` if the string + * is not a valid URL. Encoding returns `url.href`. + * + * **Example** (Converting a string to a URL) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.URL, SchemaTransformation.urlFromString) + * ) + * ``` + * + * @see {@link numberFromString} + * @see {@link transformOrFail} + * + * @category transforming + * @since 4.0.0 + */ +export const urlFromString: Transformation = transformOrFail({ + decode: (s) => + URL.canParse(s) + ? Effect.succeed(new URL(s)) + : Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` })), + encode: (url) => Effect.succeed(url.href) +}) + +/** + * Decodes a `string` into a `BigDecimal` and encodes a `BigDecimal` back to + * its string representation. + * + * **When to use** + * + * Use when you need a schema transformation to parse decimal number strings + * from APIs or user input. + * + * **Details** + * + * Decoding calls `BigDecimal.fromString(s)` and fails with `InvalidValue` if + * the string is not a valid `BigDecimal` representation. Encoding returns + * `BigDecimal.format(bd)`. + * + * @category transforming + * @since 4.0.0 + */ +export const bigDecimalFromString: Transformation = transformOrFail< + BigDecimal.BigDecimal, + string +>({ + decode: (s) => { + const result = BigDecimal.fromString(s) + return Option.isNone(result) + ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid BigDecimal string: ${s}` })) + : Effect.succeed(result.value) + }, + encode: (bd) => Effect.succeed(BigDecimal.format(bd)) +}) + +/** + * Decodes a Base64-encoded `string` into a `Uint8Array` and encodes a + * `Uint8Array` back to a Base64 string. + * + * **When to use** + * + * Use when you need a schema transformation for binary data transmitted as + * Base64 strings (e.g. file uploads, API payloads). + * + * **Details** + * + * Decoding parses the Base64 string into bytes. Encoding writes the byte array + * as a Base64 string. + * + * **Example** (Converting Base64 to a Uint8Array) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Uint8Array, SchemaTransformation.uint8ArrayFromBase64String) + * ) + * ``` + * + * @see {@link fromJsonString} + * @see `Schema.Uint8ArrayFromBase64` - a ready-made schema wrapping this transformation. + * + * @category encoding + * @since 4.0.0 + */ +export const uint8ArrayFromBase64String: Transformation, string> = new Transformation( + SchemaGetter.decodeBase64(), + SchemaGetter.encodeBase64() +) + +/** + * Decodes a Base64-encoded `string` into a UTF-8 `string` and encodes a + * UTF-8 `string` back to a Base64 string. + * + * **When to use** + * + * Use when you need a schema transformation for text data transmitted as Base64 + * strings. + * + * **Details** + * + * Decoding parses the Base64 string into a UTF-8 string. Encoding writes the + * string as a Base64 string. + * + * **Example** (Converting Base64 to a string) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromBase64String) + * ) + * ``` + * + * @see {@link uint8ArrayFromBase64String} + * @see `Schema.StringFromBase64` - a ready-made schema wrapping this transformation. + * + * @category encoding + * @since 4.0.0 + */ +export const stringFromBase64String: Transformation = new Transformation( + SchemaGetter.decodeBase64String(), + SchemaGetter.encodeBase64() +) + +/** + * Decodes a base64 (URL) encoded `string` into a UTF-8 `string` and encodes it back. + * + * **When to use** + * + * Use when you need a schema transformation for text data transmitted as Base64 + * URL-safe strings. + * + * **Details** + * + * Decoding parses the Base64 URL string into a UTF-8 string. Encoding writes + * the string as a Base64 URL string. + * + * **Example** (Converting Base64Url to a string) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromBase64UrlString) + * ) + * ``` + * + * @see {@link stringFromBase64String} + * @see `Schema.StringFromBase64Url` - a ready-made schema wrapping this transformation. + * + * @category encoding + * @since 4.0.0 + */ +export const stringFromBase64UrlString: Transformation = new Transformation( + SchemaGetter.decodeBase64UrlString(), + SchemaGetter.encodeBase64Url() +) + +/** + * Decodes a hex encoded `string` into a UTF-8 `string` and encodes it back. + * + * **When to use** + * + * Use when you need a schema transformation for text data transmitted as + * hexadecimal strings. + * + * **Details** + * + * Decoding parses the hex string into a UTF-8 string. Encoding writes the + * string as a hex string. + * + * **Example** (Converting hex to a string) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromHexString) + * ) + * ``` + * + * @see {@link stringFromBase64String} + * @see `Schema.StringFromHex` - a ready-made schema wrapping this transformation. + * + * @category encoding + * @since 4.0.0 + */ +export const stringFromHexString: Transformation = new Transformation( + SchemaGetter.decodeHexString(), + SchemaGetter.encodeHex() +) + +/** + * Decodes a URI component encoded string into a UTF-8 string and encodes a + * UTF-8 string into a URI component encoded string. + * + * **When to use** + * + * Use when you need a schema transformation to store structured data in URL + * query parameters or fragments, such as composing with `Schema.parseJson` to + * round-trip JSON through a URL. + * + * **Details** + * + * Decoding calls `decodeURIComponent` and fails if the input contains malformed + * percent-encoding sequences. Encoding calls `encodeURIComponent`. + * + * **Example** (Defining a URI component schema) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromUriComponent) + * ) + * ``` + * + * @see {@link stringFromBase64String} + * @see `Schema.StringFromUriComponent` - a ready-made schema wrapping this transformation. + * + * @category encoding + * @since 4.0.0 + */ +export const stringFromUriComponent: Transformation = new Transformation( + SchemaGetter.decodeUriComponent(), + SchemaGetter.encodeUriComponent() +) + +/** + * Decodes a JSON string with `JSON.parse` and encodes a value with + * `JSON.stringify`. + * + * **When to use** + * + * Use when you need a schema transformation to decode JSON stored or + * transmitted as a string, usually before composing with another schema that + * validates the parsed structure. + * + * **Details** + * + * Decode fails with `InvalidValue` for invalid JSON, and encode can fail with + * `InvalidValue` when `JSON.stringify` cannot serialize the value. + * + * **Example** (Parsing JSON) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.String.pipe( + * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromJsonString) + * ) + * ``` + * + * @see {@link uint8ArrayFromBase64String} + * @see {@link fromFormData} + * + * @category decoding + * @since 4.0.0 + */ +export const fromJsonString = new Transformation( + SchemaGetter.parseJson(), + SchemaGetter.stringifyJson() +) + +/** + * Decodes a `FormData` instance into a nested record using bracket-path keys and + * encodes object-like values back into `FormData`. + * + * **When to use** + * + * Use when you need a schema transformation for form or multipart payloads + * whose keys, such as `user[name]` or `items[0]`, should become nested data. + * + * **Details** + * + * Decode preserves string and `Blob` leaves. Encode flattens nested objects and + * arrays into bracket-path entries and returns an empty `FormData` for + * non-object inputs. + * + * **Example** (Decoding FormData) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.instanceOf(FormData).pipe( + * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromFormData) + * ) + * ``` + * + * @see {@link fromURLSearchParams} + * @see {@link fromJsonString} + * + * @category decoding + * @since 4.0.0 + */ +export const fromFormData = new Transformation( + SchemaGetter.decodeFormData(), + SchemaGetter.encodeFormData() +) + +/** + * Decodes `URLSearchParams` into a nested record using bracket-path keys and + * encodes object-like values back into `URLSearchParams`. + * + * **When to use** + * + * Use when you need a schema transformation for query strings whose keys, such + * as `filter[name]` or `items[0]`, should become nested data. + * + * **Details** + * + * Decode produces string leaves. Encode flattens nested objects and arrays into + * bracket-path entries and returns empty `URLSearchParams` for non-object + * inputs. + * + * **Example** (Decoding URLSearchParams) + * + * ```ts + * import { Schema, SchemaTransformation } from "effect" + * + * const schema = Schema.instanceOf(URLSearchParams).pipe( + * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromURLSearchParams) + * ) + * ``` + * + * @see {@link fromFormData} + * @see {@link fromJsonString} + * + * @category decoding + * @since 4.0.0 + */ +export const fromURLSearchParams = new Transformation( + SchemaGetter.decodeURLSearchParams(), + SchemaGetter.encodeURLSearchParams() +) + +/** + * Decodes a numeric time-zone offset in milliseconds into a + * `DateTime.TimeZone.Offset` and encodes it back to the offset number. + * + * **When to use** + * + * Use when you need a schema transformation to represent fixed-offset time + * zones with numeric millisecond offsets. + * + * **Details** + * + * Decode uses `DateTime.zoneMakeOffset`; encode returns the offset's `offset` + * field. + * + * @see {@link timeZoneFromString} for IANA or offset string encodings + * @see {@link timeZoneNamedFromString} for IANA named-zone strings + * + * @category transforming + * @since 4.0.0 + */ +export const timeZoneOffsetFromNumber: Transformation = transform< + DateTime.TimeZone.Offset, + number +>({ + decode: (n) => DateTime.zoneMakeOffset(n), + encode: (tz) => tz.offset +}) + +/** + * Decodes an IANA time-zone identifier string into a + * `DateTime.TimeZone.Named` and encodes a named time zone back to its `id`. + * + * **When to use** + * + * Use when you need a schema transformation to accept only IANA time-zone + * identifier strings and produce `DateTime.TimeZone.Named` values. + * + * **Details** + * + * Decode fails with `InvalidValue` when the string is not a valid IANA time-zone + * identifier. + * + * @see {@link timeZoneFromString} for time-zone strings that may be either IANA identifiers or offset strings + * + * @category transforming + * @since 4.0.0 + */ +export const timeZoneNamedFromString: Transformation = transformOrFail< + DateTime.TimeZone.Named, + string +>({ + decode: (s) => { + return Option.match(DateTime.zoneMakeNamed(s), { + onNone: () => + Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid IANA time zone: ${s}` })), + onSome: Effect.succeed + }) + }, + encode: (tz) => Effect.succeed(tz.id) +}) + +/** + * Decodes a string into a `DateTime.TimeZone` and encodes a time zone back to + * its string representation. + * + * **When to use** + * + * Use when you need a schema transformation to accept either an IANA time-zone + * identifier or an offset string and produce a general `DateTime.TimeZone`. + * + * **Details** + * + * Accepted decode inputs include valid IANA identifiers and offset strings such + * as `"+03:00"`. Decode fails with `InvalidValue` when the string cannot be + * parsed as a time zone. + * + * @see {@link timeZoneNamedFromString} for IANA named-zone strings only + * @see {@link timeZoneOffsetFromNumber} for fixed-offset zones encoded as numbers + * + * @category transforming + * @since 4.0.0 + */ +export const timeZoneFromString: Transformation = transformOrFail< + DateTime.TimeZone, + string +>({ + decode: (s) => { + return Option.match(DateTime.zoneFromString(s), { + onNone: () => Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid time zone: ${s}` })), + onSome: Effect.succeed + }) + }, + encode: (tz) => Effect.succeed(DateTime.zoneToString(tz)) +}) + +/** + * Decodes a date-time string into a `DateTime.Utc` and encodes it back to an ISO + * string. + * + * **When to use** + * + * Use when you need a schema transformation to decode date-time strings to a + * normalized `DateTime.Utc` and encode back as a UTC ISO string. + * + * **Details** + * + * Decode accepts strings supported by `DateTime.make`, converts the result to + * UTC, and fails with `InvalidValue` when parsing fails. Encode uses + * `DateTime.formatIso`. + * + * @see {@link dateFromString} for decoding into JavaScript `Date` + * @see {@link dateTimeZonedFromString} for ISO strings that should preserve zoned date-time information + * + * @category transforming + * @since 4.0.0 + */ +export const dateTimeUtcFromString: Transformation = transformOrFail< + DateTime.Utc, + string +>({ + decode: (s) => { + return Option.match(DateTime.make(s), { + onNone: () => + Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid UTC DateTime string: ${s}` })), + onSome: (result) => Effect.succeed(DateTime.toUtc(result)) + }) + }, + encode: (utc) => Effect.succeed(DateTime.formatIso(utc)) +}) + +/** + * Decodes a zoned date-time string into a `DateTime.Zoned` and encodes it back + * to an ISO zoned string. + * + * **When to use** + * + * Use when you need a schema transformation for ISO zoned date-time strings + * that decode to `DateTime.Zoned` and encode with `DateTime.formatIsoZoned`. + * + * **Details** + * + * Decode uses `DateTime.makeZonedFromString` and fails with `InvalidValue` when + * the input is not a valid zoned date-time. Encode uses + * `DateTime.formatIsoZoned`. + * + * @see {@link dateTimeUtcFromString} for date-time strings that should decode to `DateTime.Utc` and encode as UTC ISO strings + * + * @category transforming + * @since 4.0.0 + */ +export const dateTimeZonedFromString: Transformation = transformOrFail< + DateTime.Zoned, + string +>({ + decode: (s) => { + return Option.match(DateTime.makeZonedFromString(s), { + onNone: () => + Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid Zoned DateTime string: ${s}` })), + onSome: Effect.succeed + }) + }, + encode: (zoned) => Effect.succeed(DateTime.formatIsoZoned(zoned)) +}) diff --git a/repos/effect/packages/effect/src/SchemaUtils.ts b/repos/effect/packages/effect/src/SchemaUtils.ts new file mode 100644 index 0000000..1746d08 --- /dev/null +++ b/repos/effect/packages/effect/src/SchemaUtils.ts @@ -0,0 +1,51 @@ +/** + * Small helpers for schema patterns that are too specialized for the main + * `Schema` module. The current helper builds a schema for an existing class: + * the encoded input is checked with a struct schema, decoding calls the class + * constructor with the decoded properties, and the final value remains an + * instance of that class. + * + * @since 4.0.0 + */ +import { identity } from "./Function.ts" +import * as Schema from "./Schema.ts" +import * as SchemaTransformation from "./SchemaTransformation.ts" + +/** + * Builds an experimental schema for instances of a native class using a struct + * schema as the encoded representation. + * + * **When to use** + * + * Use when you need a schema for an existing native class while keeping a + * `Struct` schema as its encoded representation. + * + * **Details** + * + * Decoding constructs `new constructor(props)` from the encoded fields. + * Encoding uses the instance as the encoded shape, so the class should expose + * properties compatible with the provided encoding schema. + * + * @see {@link Schema.instanceOf} for validating existing class instances without a struct encoding + * @see {@link Schema.Class} for defining schema-backed classes directly + * @see {@link Schema.ErrorClass} for defining schema-backed error classes + * + * @category schemas + * @since 4.0.0 + */ +export function getNativeClassSchema any, S extends Schema.Struct>( + constructor: C, + options: { + readonly encoding: S + readonly annotations?: Schema.Annotations.Declaration> + } +): Schema.decodeTo, S["Iso"]>, S> { + const transformation = SchemaTransformation.transform, S["Type"]>({ + decode: (props) => new constructor(props), + encode: identity + }) + return Schema.instanceOf(constructor, { + toCodec: () => Schema.link>()(options.encoding, transformation), + ...options.annotations + }).pipe(Schema.encodeTo(options.encoding, transformation)) +} diff --git a/repos/effect/packages/effect/src/Scope.ts b/repos/effect/packages/effect/src/Scope.ts new file mode 100644 index 0000000..49a6dc2 --- /dev/null +++ b/repos/effect/packages/effect/src/Scope.ts @@ -0,0 +1,572 @@ +/** + * Controls how long resources stay open. + * + * A scope is a lifetime boundary. Code can register cleanup effects on it, and + * closing the scope runs those cleanups with the `Exit` value that ended the + * work. Most application code uses higher-level APIs such as `Effect.scoped` + * and `Layer`, while this module is useful when code needs to create, provide, + * fork, close, or inspect scopes directly. + * + * @since 2.0.0 + */ + +import type * as Context from "./Context.ts" +import type { Effect } from "./Effect.ts" +import type { Exit } from "./Exit.ts" +import * as effect from "./internal/effect.ts" + +const TypeId = effect.ScopeTypeId +const CloseableTypeId = effect.ScopeCloseableTypeId + +/** + * A `Scope` represents a context where resources can be acquired and + * automatically cleaned up when the scope is closed. Scopes can use + * either sequential or parallel finalization strategies. + * + * **Example** (Managing scoped resources) + * + * ```ts + * import { Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * const scope = yield* Scope.make("sequential") + * + * // Scope has a strategy and state + * console.log(scope.strategy) // "sequential" + * console.log(scope.state._tag) // "Open" + * + * // Close the scope + * yield* Scope.close(scope, Exit.void) + * console.log(scope.state._tag) // "Closed" + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Scope { + readonly [TypeId]: typeof TypeId + readonly strategy: "sequential" | "parallel" + state: State.Open | State.Closed | State.Empty +} +/** + * A `Closeable` scope extends the base `Scope` interface with the ability + * to be closed, executing all registered finalizers. + * + * **Example** (Closing a scope) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * const scope = yield* Scope.make() + * + * // Add a finalizer + * yield* Scope.addFinalizer(scope, Console.log("Cleanup!")) + * + * // Scope can be closed + * yield* Scope.close(scope, Exit.void) + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Closeable extends Scope { + readonly [CloseableTypeId]: typeof CloseableTypeId +} + +/** + * The `State` namespace contains the concrete states of a scope: `Empty` + * before any finalizers are registered, `Open` with registered finalizers, and + * `Closed` with the exit value used to close the scope. + * + * **Example** (Checking scope states) + * + * ```ts + * import { Effect, Exit, Scope } from "effect" + * + * // Example of checking scope states + * const program = Effect.gen(function*() { + * const scope = yield* Scope.make() + * + * // When open, the scope accepts finalizers + * if (scope.state._tag === "Open") { + * console.log("Scope is open") + * } + * + * yield* Scope.close(scope, Exit.void) + * + * // When closed, the scope no longer accepts finalizers + * if (scope.state._tag === "Closed") { + * console.log("Scope is closed") + * } + * }) + * ``` + * + * @since 4.0.0 + */ +export declare namespace State { + /** + * Represents an open scope with no registered finalizers yet. + * + * **Details** + * + * Adding the first finalizer transitions the scope to `Open`; closing an + * empty scope transitions directly to `Closed` without producing a finalizer + * effect. + * + * **Example** (Inspecting an empty scope state) + * + * ```ts + * import { Scope } from "effect" + * + * const scope = Scope.makeUnsafe() + * + * // When scope is open, you can check its state + * if (scope.state._tag === "Open") { + * console.log("Scope is open and accepting finalizers") + * console.log(scope.state.finalizers.size) // Number of registered finalizers + * } + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Empty = { + readonly _tag: "Empty" + } + /** + * Represents an open scope state where finalizers can be added and + * the scope is still accepting new resources. + * + * **Example** (Inspecting an open scope state) + * + * ```ts + * import { Scope } from "effect" + * + * const scope = Scope.makeUnsafe() + * + * // When scope is open, you can check its state + * if (scope.state._tag === "Open") { + * console.log("Scope is open and accepting finalizers") + * console.log(scope.state.finalizers.size) // Number of registered finalizers + * } + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Open = { + readonly _tag: "Open" + readonly finalizers: Map<{}, (exit: Exit) => Effect> + } + /** + * Represents a closed scope state where finalizers have been executed + * and the scope is no longer accepting new resources. + * + * **Example** (Inspecting a closed scope state) + * + * ```ts + * import { Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * const scope = yield* Scope.make() + * + * // Close the scope + * yield* Scope.close(scope, Exit.succeed("Done")) + * + * // Check if scope is closed + * if (scope.state._tag === "Closed") { + * console.log("Scope is closed") + * console.log(scope.state.exit) // The exit value used to close the scope + * } + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ + export type Closed = { + readonly _tag: "Closed" + readonly exit: Exit + } +} + +/** + * Service tag for the active resource lifetime. + * + * **When to use** + * + * Use to access the active lifetime when registering finalizers or sharing + * resources with the surrounding scope. + * + * **Example** (Accessing the scope service) + * + * ```ts + * import { Effect, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * // Access the scope from the context + * const scope = yield* Scope.Scope + * + * // Use the scope for resource management + * yield* Scope.addFinalizer(scope, Effect.log("Cleanup")) + * }) + * + * // Provide a scope to the program + * const scoped = Effect.scoped(program) + * ``` + * + * @category services + * @since 2.0.0 + */ +export const Scope: Context.Service = effect.scopeTag + +/** + * Creates a new `Scope` with the specified finalizer strategy. + * + * **Example** (Creating a scope) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a scope with sequential cleanup + * const scope = yield* Scope.make("sequential") + * + * // Add finalizers + * yield* Scope.addFinalizer(scope, Console.log("Cleanup 1")) + * yield* Scope.addFinalizer(scope, Console.log("Cleanup 2")) + * + * // Close the scope (finalizers run in reverse order) + * yield* Scope.close(scope, Exit.void) + * // Output: "Cleanup 2", then "Cleanup 1" + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: (finalizerStrategy?: "sequential" | "parallel") => Effect = effect.scopeMake + +/** + * Creates a new `Scope` synchronously without wrapping it in an `Effect`. + * This is useful when you need a scope immediately but should be used with caution + * as it doesn't provide the same safety guarantees as the `Effect`-wrapped version. + * + * **When to use** + * + * Use when a scope must be allocated synchronously and the caller will close it + * manually. + * + * **Example** (Creating a scope synchronously) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * // Create a scope immediately + * const scope = Scope.makeUnsafe("sequential") + * + * // Use it in an Effect program + * const program = Effect.gen(function*() { + * yield* Scope.addFinalizer(scope, Console.log("Cleanup")) + * yield* Scope.close(scope, Exit.void) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe: (finalizerStrategy?: "sequential" | "parallel") => Closeable = effect.scopeMakeUnsafe + +/** + * Provides a concrete `Scope` to an effect. + * + * **When to use** + * + * Use to run an effect that requires `Scope` with a scope managed by the + * caller. + * + * **Details** + * + * Providing the scope removes the `Scope` requirement from the effect context. + * + * **Example** (Providing a scope) + * + * ```ts + * import { Console, Effect, Scope } from "effect" + * + * // An effect that requires a Scope + * const program = Effect.gen(function*() { + * const scope = yield* Scope.Scope + * yield* Scope.addFinalizer(scope, Console.log("Cleanup")) + * yield* Console.log("Working...") + * }) + * + * // Provide a scope to the program + * const withScope = Effect.gen(function*() { + * const scope = yield* Scope.make() + * yield* Scope.provide(scope)(program) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const provide: { + (value: Scope): (self: Effect) => Effect> + (self: Effect, value: Scope): Effect> +} = effect.provideScope + +/** + * Registers an exit-aware finalizer on a scope. + * + * **When to use** + * + * Use when cleanup needs to know whether the scope closed with success, + * failure, or interruption. + * + * **Details** + * + * If the scope is open, the finalizer runs when the scope closes and receives + * the scope's exit value. If the scope is already closed, the finalizer runs + * immediately with the stored exit value. + * + * **Example** (Adding an exit-aware finalizer) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const withResource = Effect.gen(function*() { + * const scope = yield* Scope.make() + * + * // Add a finalizer for cleanup + * yield* Scope.addFinalizerExit( + * scope, + * (exit) => + * Console.log( + * `Cleaning up resource. Exit: ${ + * Exit.isSuccess(exit) ? "Success" : "Failure" + * }` + * ) + * ) + * + * // Use the resource + * yield* Console.log("Using resource") + * + * // Close the scope + * yield* Scope.close(scope, Exit.void) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const addFinalizerExit: (scope: Scope, finalizer: (exit: Exit) => Effect) => Effect = + effect.scopeAddFinalizerExit + +/** + * Registers a finalizer effect on a scope. + * + * **Details** + * + * If the scope is open, the finalizer runs when the scope closes, regardless of + * whether the scope closes successfully or with an error. If the scope is + * already closed, the finalizer runs immediately. + * + * **Example** (Adding finalizers) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * const scope = yield* Scope.make() + * + * // Add simple finalizers + * yield* Scope.addFinalizer(scope, Console.log("Cleanup task 1")) + * yield* Scope.addFinalizer(scope, Console.log("Cleanup task 2")) + * yield* Scope.addFinalizer(scope, Effect.log("Cleanup task 3")) + * + * // Do some work + * yield* Console.log("Doing work...") + * + * // Close the scope + * yield* Scope.close(scope, Exit.void) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const addFinalizer: (scope: Scope, finalizer: Effect) => Effect = effect.scopeAddFinalizer + +/** + * Creates a closeable child scope registered with a parent scope. + * + * **Details** + * + * Closing the parent closes the child with the same exit value, and closing the + * child detaches it from the parent. The optional finalizer strategy configures + * the child scope and defaults to `"sequential"` when omitted. + * + * **Example** (Creating a child scope) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const nestedScopes = Effect.gen(function*() { + * const parentScope = yield* Scope.make("sequential") + * + * // Add finalizer to parent + * yield* Scope.addFinalizer(parentScope, Console.log("Parent cleanup")) + * + * // Create child scope + * const childScope = yield* Scope.fork(parentScope, "parallel") + * + * // Add finalizer to child + * yield* Scope.addFinalizer(childScope, Console.log("Child cleanup")) + * + * // Close child first, then parent + * yield* Scope.close(childScope, Exit.void) + * yield* Scope.close(parentScope, Exit.void) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const fork: ( + scope: Scope, + finalizerStrategy?: "sequential" | "parallel" +) => Effect = effect.scopeFork + +/** + * Creates a closeable child scope synchronously and registers it with a parent scope. + * + * **When to use** + * + * Use when a child scope must be created synchronously and the caller controls + * both parent and child scope lifetimes. + * + * **Details** + * + * Closing the parent closes the child with the same exit value, and closing the + * child detaches it from the parent. The optional finalizer strategy configures + * the child scope and defaults to `"sequential"` when omitted. + * + * **Example** (Creating a child scope synchronously) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const program = Effect.gen(function*() { + * const parentScope = Scope.makeUnsafe("sequential") + * const childScope = Scope.forkUnsafe(parentScope, "parallel") + * + * // Add finalizers to both scopes + * yield* Scope.addFinalizer(parentScope, Console.log("Parent cleanup")) + * yield* Scope.addFinalizer(childScope, Console.log("Child cleanup")) + * + * // Close child first, then parent + * yield* Scope.close(childScope, Exit.void) + * yield* Scope.close(parentScope, Exit.void) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const forkUnsafe: (scope: Scope, finalizerStrategy?: "sequential" | "parallel") => Closeable = + effect.scopeForkUnsafe + +/** + * Closes a scope and runs its registered finalizers. + * + * **When to use** + * + * Use to close a scope manually with a specific exit value. + * + * **Details** + * + * Finalizers run in the scope's configured order and receive the supplied + * `Exit`. + * + * **Example** (Running scope finalizers) + * + * ```ts + * import { Console, Effect, Exit, Scope } from "effect" + * + * const resourceManagement = Effect.gen(function*() { + * const scope = yield* Scope.make("sequential") + * + * // Add multiple finalizers + * yield* Scope.addFinalizer(scope, Console.log("Close database connection")) + * yield* Scope.addFinalizer(scope, Console.log("Close file handle")) + * yield* Scope.addFinalizer(scope, Console.log("Release memory")) + * + * // Do some work... + * yield* Console.log("Performing operations...") + * + * // Close scope - finalizers run in reverse order of registration + * yield* Scope.close(scope, Exit.succeed("Success!")) + * // Output: "Release memory", "Close file handle", "Close database connection" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const close: (self: Scope, exit: Exit) => Effect = effect.scopeClose + +/** + * Closes a scope unsafely with the provided exit value. + * + * **When to use** + * + * Use when implementing lower-level scope machinery that must transition a + * scope to `Closed` immediately and can run the returned finalizer effect when + * one is produced. + * + * **Details** + * + * Returns an effect that runs registered finalizers, or `undefined` when the + * scope was already closed or no finalizers need to run. + * + * **Gotchas** + * + * Ignoring the returned effect skips registered finalizers. + * + * @see {@link close} for the usual effectful close operation that always returns an `Effect` + * + * @category unsafe + * @since 4.0.0 + */ +export const closeUnsafe: (self: Scope, exit_: Exit) => Effect | undefined = + effect.scopeCloseUnsafe + +/** + * Runs an effect with the provided closeable scope in its context and closes + * that scope when the effect exits. + * + * **When to use** + * + * Use when you already have a `Closeable` scope and want to run an effect that + * requires `Scope` while automatically closing that scope when the effect exits. + * + * **Details** + * + * The scope is closed with the same exit value as the effect, so registered + * finalizers can observe whether the effect succeeded, failed, or was + * interrupted. + * + * @see `provide` for providing a scope without closing it automatically + * @see `Effect.scoped` for creating and closing a fresh scope around a workflow + * + * @category combinators + * @since 2.0.0 + */ +export const use: { + (scope: Closeable): (self: Effect) => Effect> + (self: Effect, scope: Closeable): Effect> +} = effect.scopeUse diff --git a/repos/effect/packages/effect/src/ScopedCache.ts b/repos/effect/packages/effect/src/ScopedCache.ts new file mode 100644 index 0000000..c80da9d --- /dev/null +++ b/repos/effect/packages/effect/src/ScopedCache.ts @@ -0,0 +1,820 @@ +/** + * Caches values that need scoped resource management. + * + * Each cached entry owns its own `Scope`, so resources opened while creating a + * value stay alive while that entry is cached and are released when the entry is + * removed. A `ScopedCache` also belongs to an outer scope, which closes all + * remaining entries when the cache is closed. Lookups for the same missing key + * share one in-progress effect, and entries can expire, be refreshed, be + * invalidated, or be evicted by capacity limits. + * + * @since 4.0.0 + */ +import * as Arr from "./Array.ts" +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" +import * as Duration from "./Duration.ts" +import type * as Effect from "./Effect.ts" +import type * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import { dual, identity } from "./Function.ts" +import * as core from "./internal/core.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as effect from "./internal/effect.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Predicate from "./Predicate.ts" +import * as Scope from "./Scope.ts" + +const TypeId = "~effect/ScopedCache" + +/** + * A scoped cache whose values are acquired by a lookup effect and stored in + * per-entry scopes. + * + * **When to use** + * + * Use to cache values that acquire scoped resources and must release those + * resources when entries expire, are evicted, or are invalidated. + * + * **Details** + * + * Concurrent requests for the same key share the same in-flight lookup. + * Entries can expire based on the lookup exit, are evicted when capacity is + * exceeded, and release their entry scopes when invalidated, evicted, expired, + * or when the cache's owning scope closes. + * + * @see {@link make} for creating a scoped cache with a fixed time-to-live + * @see {@link makeWith} for creating a scoped cache with dynamic time-to-live + * + * @category models + * @since 2.0.0 + */ +export interface ScopedCache extends Pipeable { + readonly [TypeId]: typeof TypeId + state: State + readonly capacity: number + readonly lookup: (key: Key) => Effect.Effect + readonly timeToLive: (exit: Exit.Exit, key: Key) => Duration.Duration +} + +/** + * Represents whether a `ScopedCache` is open or closed. + * + * **When to use** + * + * Use when inspecting the low-level lifecycle state of a scoped cache. + * + * **Details** + * + * `Open` stores cached entries in access order for reuse and eviction. + * `Closed` means the owning scope has closed and the cache can no longer + * perform lookup operations. + * + * @category models + * @since 4.0.0 + */ +export type State = { + readonly _tag: "Open" + readonly map: MutableHashMap.MutableHashMap> +} | { + readonly _tag: "Closed" +} + +/** + * A single scoped cache entry. + * + * **When to use** + * + * Use when inspecting the open state of a `ScopedCache` and you need the stored + * deferred result, entry scope, or expiration timestamp for a key. + * + * **Details** + * + * The entry contains the deferred lookup result shared by readers, the scope + * that owns resources acquired while computing the value, and an optional + * expiration time in milliseconds. Removing the entry closes its scope. + * + * @see {@link State} for the open/closed cache state that stores entries by key + * + * @category models + * @since 4.0.0 + */ +export interface Entry { + expiresAt: number | undefined + readonly deferred: Deferred.Deferred + readonly scope: Scope.Closeable +} + +/** + * Creates a `ScopedCache` from a lookup function, maximum capacity, and a + * time-to-live function computed from each lookup exit and key. + * + * **When to use** + * + * Use when you need a scoped cache whose entry lifetime depends on each lookup + * result or key. + * + * **Details** + * + * The cache must be constructed in a `Scope`. Each lookup runs in its own entry + * scope, and that scope is closed when the entry expires, is invalidated, is + * evicted by capacity, or when the cache's owning scope closes. + * `requireServicesAt` controls whether lookup services are captured at + * construction time or required when lookup operations run. + * + * @see {@link make} for creating a scoped cache with one fixed time-to-live + * + * @category constructors + * @since 2.0.0 + */ +export const makeWith = < + Key, + A, + E = never, + R = never, + ServiceMode extends "lookup" | "construction" = never +>(options: { + readonly lookup: (key: Key) => Effect.Effect + readonly capacity: number + readonly timeToLive?: ((exit: Exit.Exit, key: Key) => Duration.Input) | undefined + readonly requireServicesAt?: ServiceMode | undefined +}): Effect.Effect< + ScopedCache : never>, + never, + ("lookup" extends ServiceMode ? never : R) | Scope.Scope +> => + effect.contextWith((context: Context.Context) => { + const scope = Context.get(context, Scope.Scope) + const self = Object.create(Proto) + self.lookup = (key: Key): Effect.Effect => + effect.updateContext( + options.lookup(key), + (input) => Context.merge(context, input) + ) + const map = MutableHashMap.empty>() + self.state = { _tag: "Open", map } + self.capacity = options.capacity + self.timeToLive = options.timeToLive + ? (exit: Exit.Exit, key: Key) => Duration.fromInputUnsafe(options.timeToLive!(exit, key)) + : defaultTimeToLive + return effect.as( + Scope.addFinalizer( + scope, + core.withFiber((fiber) => { + self.state = { _tag: "Closed" } + return invalidateAllImpl(fiber, map) + }) + ), + self + ) + }) + +/** + * Creates a `ScopedCache` with a fixed time-to-live for every lookup result. + * + * **When to use** + * + * Use to create a scoped cache when every cached lookup result should share the + * same lifetime. + * + * **Details** + * + * This is the constant-TTL variant of `makeWith`: values are acquired by the + * lookup effect in per-entry scopes, capacity can evict older entries, and + * entry scopes are closed when entries expire, are invalidated, are evicted, or + * when the cache's owning scope closes. + * + * @see {@link makeWith} for computing time-to-live from each lookup result and key + * + * @category constructors + * @since 2.0.0 + */ +export const make = < + Key, + A, + E = never, + R = never, + ServiceMode extends "lookup" | "construction" = never +>( + options: { + readonly lookup: (key: Key) => Effect.Effect + readonly capacity: number + readonly timeToLive?: Duration.Input | undefined + readonly requireServicesAt?: ServiceMode | undefined + } +): Effect.Effect< + ScopedCache : never>, + never, + ("lookup" extends ServiceMode ? never : R) | Scope.Scope +> => + makeWith({ + ...options, + timeToLive: options.timeToLive ? () => options.timeToLive! : defaultTimeToLive + }) + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + toJSON(this: ScopedCache) { + return { + _id: "ScopedCache", + capacity: this.capacity, + state: this.state + } + } +} + +const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Duration => Duration.infinity + +/** + * Gets the value for a key, running the cache lookup when no unexpired entry is + * present. + * + * **When to use** + * + * Use to retrieve a scoped cached value by key when a missing or expired entry + * should run the cache lookup and share the in-flight lookup with concurrent + * callers. + * + * **Details** + * + * Concurrent `get` calls for the same key share the same in-flight lookup. + * Successful and failed lookup exits are cached according to the configured + * TTL. If the cache is closed, the effect is interrupted. + * + * @see {@link getOption} for reading only when an unexpired entry is already cached + * @see {@link getSuccess} for inspecting an already-completed successful entry + * @see {@link refresh} for forcing a new lookup + * + * @category combinators + * @since 4.0.0 + */ +export const get: { + (key: Key): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key): Effect.Effect +} = dual( + 2, + (self: ScopedCache, key: Key): Effect.Effect => + effect.uninterruptibleMask((restore) => + core.withFiber((fiber) => { + const state = self.state + if (state._tag === "Closed") { + return effect.interrupt + } + const oentry = MutableHashMap.get(state.map, key) + if (Option.isSome(oentry) && !hasExpired(oentry.value, fiber)) { + // Move the entry to the end of the map to keep it fresh + MutableHashMap.remove(state.map, key) + MutableHashMap.set(state.map, key, oentry.value) + return restore(Deferred.await(oentry.value.deferred)) + } + const scope = Scope.makeUnsafe() + const deferred = Deferred.makeUnsafe() + const entry: Entry = { + expiresAt: undefined, + deferred, + scope + } + MutableHashMap.set(state.map, key, entry) + return checkCapacity(fiber, state.map, self.capacity).pipe( + Option.isSome(oentry) ? effect.flatMap(() => Scope.close(oentry.value.scope, effect.exitVoid)) : identity, + effect.flatMap(() => Scope.provide(restore(self.lookup(key)), scope)), + effect.onExit((exit) => { + Deferred.doneUnsafe(deferred, exit) + const ttl = self.timeToLive(exit, key) + if (Duration.isFinite(ttl)) { + entry.expiresAt = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + } + return effect.void + }) + ) + }) + ) +) + +const hasExpired = (entry: Entry, fiber: Fiber.Fiber): boolean => { + if (entry.expiresAt === undefined) { + return false + } + return fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() >= entry.expiresAt +} + +const checkCapacity = ( + parent: Fiber.Fiber, + map: MutableHashMap.MutableHashMap>, + capacity: number +): Effect.Effect => { + if (!Number.isFinite(capacity)) return effect.void + let diff = MutableHashMap.size(map) - capacity + if (diff <= 0) return effect.void + // MutableHashMap has insertion order, so we can remove the oldest entries + const fibers = Arr.empty>() + for (const [key, entry] of map) { + MutableHashMap.remove(map, key) + fibers.push(effect.forkUnsafe(parent as any, Scope.close(entry.scope, effect.exitVoid), true)) + diff-- + if (diff === 0) break + } + return effect.fiberAwaitAll(fibers) +} + +/** + * Reads an existing unexpired cache entry without running the lookup function. + * + * **When to use** + * + * Use to read a scoped value only when it is already cached, without starting + * the lookup for missing or expired keys. + * + * **Details** + * + * Returns `Option.none` when the key is absent or expired. If an entry exists, + * the effect waits for its cached result and returns `Option.some(value)` on + * success, or fails with the cached lookup error. + * + * @see {@link get} for running the lookup on missing or expired keys + * @see {@link getSuccess} for inspecting only already-completed successful entries + * + * @category combinators + * @since 4.0.0 + */ +export const getOption: { + (key: Key): (self: ScopedCache) => Effect.Effect, E> + (self: ScopedCache, key: Key): Effect.Effect, E> +} = dual( + 2, + (self: ScopedCache, key: Key): Effect.Effect, E> => + effect.uninterruptibleMask((restore) => + core.withFiber((fiber) => + effect.flatMap( + getImpl(self, key, fiber), + (entry) => entry ? effect.asSome(restore(Deferred.await(entry.deferred))) : effect.succeedNone + ) + ) + ) +) + +const getImpl = ( + self: ScopedCache, + key: Key, + fiber: Fiber.Fiber, + isRead = true +): Effect.Effect | undefined> => { + if (self.state._tag === "Closed") { + return effect.interrupt + } + const state = self.state + const oentry = MutableHashMap.get(state.map, key) + if (Option.isNone(oentry)) { + return effect.undefined + } else if (hasExpired(oentry.value, fiber)) { + MutableHashMap.remove(state.map, key) + return effect.as( + Scope.close(oentry.value.scope, effect.exitVoid), + undefined + ) + } else if (isRead) { + MutableHashMap.remove(state.map, key) + MutableHashMap.set(state.map, key, oentry.value) + } + return effect.succeed(oentry.value) +} + +/** + * Retrieves the value associated with the specified key from the cache, only if + * it contains a resolved successful value. + * + * **When to use** + * + * Use to inspect an already-completed successful scoped cache entry without + * running or awaiting the lookup effect. + * + * **Details** + * + * Returns `Option.some` for a resolved successful entry. Returns `Option.none` + * for missing, expired, failed, or still-pending entries. + * + * @see {@link get} for awaiting or starting the lookup effect + * @see {@link getOption} for awaiting an already-cached entry without starting a lookup + * + * @category combinators + * @since 4.0.0 + */ +export const getSuccess: { + (key: Key): (self: ScopedCache) => Effect.Effect> + (self: ScopedCache, key: Key): Effect.Effect> +} = dual( + 2, + (self: ScopedCache, key: Key): Effect.Effect> => + effect.uninterruptible( + core.withFiber((fiber) => + effect.map( + getImpl(self, key, fiber), + (entry) => { + const exit = entry?.deferred.effect as Exit.Exit | undefined + if (exit && effect.exitIsSuccess(exit)) { + return Option.some(exit.value) + } + return Option.none() + } + ) + ) + ) +) + +/** + * Sets a successful value for a key without running the lookup function. + * + * **When to use** + * + * Use to seed or overwrite a scoped cache entry with an already available + * successful value. + * + * **Details** + * + * This replaces and closes any existing entry scope for the key, applies the + * cache's TTL using a successful exit for the value, and may evict older + * entries if the cache capacity is exceeded. + * + * @see {@link get} for reading or computing a cached value + * @see {@link refresh} for replacing an entry by running the lookup function + * + * @category combinators + * @since 4.0.0 + */ +export const set: { + (key: Key, value: A): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key, value: A): Effect.Effect +} = dual( + 3, + (self: ScopedCache, key: Key, value: A): Effect.Effect => + effect.uninterruptible( + core.withFiber((fiber) => { + if (self.state._tag === "Closed") { + return effect.interrupt + } + const oentry = MutableHashMap.get(self.state.map, key) + const state = self.state + const exit = core.exitSucceed(value) + const deferred = Deferred.makeUnsafe() + Deferred.doneUnsafe(deferred, exit) + const ttl = self.timeToLive(exit, key) + MutableHashMap.set(state.map, key, { + scope: Scope.makeUnsafe(), + deferred, + expiresAt: Duration.isFinite(ttl) + ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + : undefined + }) + const check = checkCapacity(fiber, state.map, self.capacity) + return Option.isSome(oentry) + ? effect.flatMap(Scope.close(oentry.value.scope, effect.exitVoid), () => check) + : check + }) + ) +) + +/** + * Checks whether the cache contains an entry for the specified key. + * + * **When to use** + * + * Use to test whether an unexpired entry exists for a key without running the + * cache lookup. + * + * **Details** + * + * This does not start lookups and does not refresh access order. Expired + * entries are treated as absent and their scopes are closed while checking. If + * the cache is closed, the effect is interrupted. + * + * @see {@link getOption} for reading an existing cached entry + * @see {@link get} for running the lookup on missing or expired keys + * + * @category combinators + * @since 4.0.0 + */ +export const has: { + (key: Key): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key): Effect.Effect +} = dual( + 2, + (self: ScopedCache, key: Key): Effect.Effect => + effect.uninterruptible( + core.withFiber((fiber) => effect.map(getImpl(self, key, fiber, false), Predicate.isNotUndefined)) + ) +) + +/** + * Removes the entry associated with a key and closes its entry scope. + * + * **When to use** + * + * Use to remove a single key from a scoped cache and release any resources owned + * by that entry before a later lookup computes it again. + * + * **Details** + * + * If the key is absent, this is a no-op. + * + * **Gotchas** + * + * If the cache is closed, the effect is interrupted. + * + * @see {@link refresh} for replacing a key by running a new lookup immediately + * @see {@link invalidateWhen} for invalidating only when a cached value matches a predicate + * @see {@link invalidateAll} for removing every cached entry + * + * @category combinators + * @since 4.0.0 + */ +export const invalidate: { + (key: Key): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key): Effect.Effect +} = dual(2, (self: ScopedCache, key: Key): Effect.Effect => + effect.uninterruptible( + effect.suspend(() => { + if (self.state._tag === "Closed") { + return effect.interrupt + } + const oentry = MutableHashMap.get(self.state.map, key) + if (Option.isNone(oentry)) { + return effect.void + } + MutableHashMap.remove(self.state.map, key) + return Scope.close(oentry.value.scope, effect.exitVoid) + }) + )) + +/** + * Invalidates the entry associated with the specified key in the cache when the + * predicate returns true for the cached value. + * + * **When to use** + * + * Use to remove an already-cached scoped value only when the successful cached + * value satisfies a predicate. + * + * **Details** + * + * Returns `true` only when a successful cached value matches and is removed. It + * returns `false` for absent, expired, failed, or non-matching entries. + * + * **Gotchas** + * + * A matching invalidation closes the entry scope and releases its resources. + * + * @see {@link invalidate} for unconditional removal by key + * + * @category combinators + * @since 4.0.0 + */ +export const invalidateWhen: { + (key: Key, f: Predicate.Predicate): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key, f: Predicate.Predicate): Effect.Effect +} = dual( + 3, + (self: ScopedCache, key: Key, f: Predicate.Predicate): Effect.Effect => + effect.uninterruptibleMask((restore) => + core.withFiber((fiber) => + effect.flatMap(getImpl(self, key, fiber, false), (entry) => { + if (entry === undefined) { + return effect.succeed(false) + } + return restore(Deferred.await(entry.deferred)).pipe( + effect.flatMap((value) => { + if (self.state._tag === "Closed") { + return effect.succeed(false) + } else if (f(value)) { + MutableHashMap.remove(self.state.map, key) + return effect.as(Scope.close(entry.scope, effect.exitVoid), true) + } + return effect.succeed(false) + }), + effect.catch_(() => effect.succeed(false)) + ) + }) + ) + ) +) + +/** + * Forces a refresh of the value associated with the specified key in the cache. + * + * **When to use** + * + * Use to recompute a scoped cache entry immediately, even when an unexpired + * value is already cached. + * + * **Details** + * + * It will always invoke the lookup function to construct a new value, + * overwriting any existing value for that key. + * + * @see {@link get} for reusing an unexpired entry before running the lookup + * @see {@link invalidate} for removing an entry without recomputing it + * + * @category combinators + * @since 4.0.0 + */ +export const refresh: { + (key: Key): (self: ScopedCache) => Effect.Effect + (self: ScopedCache, key: Key): Effect.Effect +} = dual( + 2, + (self: ScopedCache, key: Key): Effect.Effect => + effect.uninterruptibleMask(effect.fnUntraced(function*(restore) { + if (self.state._tag === "Closed") return yield* effect.interrupt + const fiber = Fiber.getCurrent()! + const scope = Scope.makeUnsafe() + const deferred = Deferred.makeUnsafe() + const entry: Entry = { + scope, + expiresAt: undefined, + deferred + } + const newEntry = !MutableHashMap.has(self.state.map, key) + if (newEntry) { + MutableHashMap.set(self.state.map, key, entry) + yield* checkCapacity(fiber, self.state.map, self.capacity) + } + const exit = yield* effect.exit(restore(Scope.provide(self.lookup(key), scope))) + Deferred.doneUnsafe(deferred, exit) + // @ts-ignore async gap + if (self.state._tag === "Closed") { + if (!newEntry) { + yield* Scope.close(scope, effect.exitVoid) + } + return yield* effect.interrupt + } + const ttl = self.timeToLive(exit, key) + entry.expiresAt = Duration.isFinite(ttl) + ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) + : undefined + if (!newEntry) { + const oentry = MutableHashMap.get(self.state.map, key) + MutableHashMap.set(self.state.map, key, entry) + if (Option.isSome(oentry)) { + yield* Scope.close(oentry.value.scope, effect.exitVoid) + } + } + return yield* exit + })) +) + +/** + * Removes every entry from the cache and closes each entry scope. + * + * **When to use** + * + * Use to clear a scoped cache and release resources owned by all cached entries. + * + * **Details** + * + * If the cache is closed, the effect is interrupted. + * + * @see {@link invalidate} for removing one cached entry + * + * @category combinators + * @since 4.0.0 + */ +export const invalidateAll = (self: ScopedCache): Effect.Effect => + core.withFiber((parent) => { + if (self.state._tag === "Closed") { + return effect.interrupt + } + return invalidateAllImpl(parent, self.state.map) + }) + +const invalidateAllImpl = ( + parent: Fiber.Fiber, + map: MutableHashMap.MutableHashMap> +): Effect.Effect => { + const fibers = Arr.empty>() + for (const [, entry] of map) { + fibers.push(effect.forkUnsafe(parent as any, Scope.close(entry.scope, effect.exitVoid), true, true)) + } + MutableHashMap.clear(map) + return effect.fiberAwaitAll(fibers) +} + +/** + * Retrieves the approximate number of entries in the cache. + * + * **When to use** + * + * Use to inspect how many entries are currently stored in the scoped cache. + * + * **Gotchas** + * + * Note that expired entries are counted until they are accessed and removed. + * The size reflects the current number of entries stored, not the number + * of valid entries. + * + * @category combinators + * @since 4.0.0 + */ +export const size = (self: ScopedCache): Effect.Effect => + effect.sync(() => self.state._tag === "Closed" ? 0 : MutableHashMap.size(self.state.map)) + +/** + * Retrieves all active keys from the cache, automatically filtering out expired entries. + * + * **When to use** + * + * Use to inspect currently cached unexpired keys without running cache lookups. + * + * **Gotchas** + * + * Expired entries are removed and their scopes are closed while filtering. + * + * @see {@link entries} for retrieving successful cached key-value pairs + * @see {@link values} for retrieving only successfully cached values + * + * @category combinators + * @since 4.0.0 + */ +export const keys = (self: ScopedCache): Effect.Effect> => + core.withFiber((fiber) => { + if (self.state._tag === "Closed") return effect.succeed([]) + const state = self.state + const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + const fibers = Arr.empty>() + const keys: Array = [] + for (const [key, entry] of state.map) { + if (entry.expiresAt === undefined || entry.expiresAt > now) { + keys.push(key) + } else { + MutableHashMap.remove(state.map, key) + fibers.push(effect.forkUnsafe(fiber, Scope.close(entry.scope, effect.exitVoid), true, true)) + } + } + return fibers.length === 0 ? effect.succeed(keys) : effect.as(effect.fiberAwaitAll(fibers), keys) + }) + +/** + * Retrieves all successfully cached values from the cache, excluding failed + * lookups and expired entries. + * + * **When to use** + * + * Use to inspect currently successful cached values without running cache + * lookups. + * + * **Gotchas** + * + * Expired entries are removed and their scopes are closed while filtering. + * + * @see {@link entries} for retrieving successful cached key-value pairs + * @see {@link keys} for retrieving only cached keys + * + * @category combinators + * @since 4.0.0 + */ +export const values = (self: ScopedCache): Effect.Effect> => + effect.map(entries(self), Arr.map(([, value]) => value)) + +/** + * Retrieves all key-value pairs from the cache as an array. This function + * only returns entries with successfully resolved values, filtering out any + * failed lookups or expired entries. + * + * **When to use** + * + * Use to inspect the currently successful cached key-value pairs without + * running cache lookups. + * + * **Gotchas** + * + * Expired entries are removed and their scopes are closed while filtering. + * + * @see {@link keys} for retrieving only cached keys + * @see {@link values} for retrieving only cached values + * + * @category combinators + * @since 4.0.0 + */ +export const entries = (self: ScopedCache): Effect.Effect> => + core.withFiber((fiber) => { + if (self.state._tag === "Closed") return effect.succeed([]) + const state = self.state + const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + const fibers = Arr.empty>() + const arr: Array<[Key, A]> = [] + for (const [key, entry] of state.map) { + if (entry.expiresAt === undefined || entry.expiresAt > now) { + const exit = entry.deferred.effect + if (core.isExit(exit) && !effect.exitIsFailure(exit)) { + arr.push([key, exit.value as A]) + } + } else { + MutableHashMap.remove(state.map, key) + fibers.push(effect.forkUnsafe(fiber, Scope.close(entry.scope, effect.exitVoid), true, true)) + } + } + return fibers.length === 0 + ? effect.succeed(arr) + : effect.as(effect.fiberAwaitAll(fibers), arr) + }) diff --git a/repos/effect/packages/effect/src/ScopedRef.ts b/repos/effect/packages/effect/src/ScopedRef.ts new file mode 100644 index 0000000..f708463 --- /dev/null +++ b/repos/effect/packages/effect/src/ScopedRef.ts @@ -0,0 +1,192 @@ +/** + * Stores a current value together with the scope that owns it. + * + * A `ScopedRef` is useful for resource-backed values such as clients, + * connections, subscriptions, or handles. Replacing the value acquires the + * replacement in a new scope and releases the resources owned by the previous + * value. Reads can be effectful or synchronous, and updates are synchronized so + * only one replacement happens at a time. + * + * @since 2.0.0 + */ +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import { dual, type LazyArg } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import type { Pipeable } from "./Pipeable.ts" +import * as Scope from "./Scope.ts" +import * as Synchronized from "./SynchronizedRef.ts" + +const TypeId = "~effect/ScopedRef" + +/** + * A `ScopedRef` is a reference whose value is associated with resources, + * which must be released properly. You can both get the current value of any + * `ScopedRef`, as well as set it to a new value (which may require new + * resources). The reference itself takes care of properly releasing resources + * for the old value whenever a new value is obtained. + * + * **When to use** + * + * Use when an application needs to keep a current resource-backed value and + * later replace it with another acquired value while ensuring the previous + * value is released. + * + * @category models + * @since 2.0.0 + */ +export interface ScopedRef extends Pipeable { + readonly [TypeId]: typeof TypeId + readonly backing: Synchronized.SynchronizedRef +} + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + toJSON(this: ScopedRef) { + return { + _id: "ScopedRef", + value: this.backing.backing.ref.current[1] + } + } +} + +const makeUnsafe = ( + scope: Scope.Closeable, + value: A +): ScopedRef => { + const self = Object.create(Proto) + self.backing = Synchronized.makeUnsafe([scope, value] as const) + return self +} + +/** + * Creates a new `ScopedRef` from an effect that acquires the initial value. + * + * **When to use** + * + * Use when creating a `ScopedRef` whose initial value requires acquiring + * resources that must be released. + * + * @see {@link make} for creating a `ScopedRef` from a value that does not require resource acquisition + * @category constructors + * @since 2.0.0 + */ +export const fromAcquire: ( + acquire: Effect.Effect +) => Effect.Effect, E, Scope.Scope | R> = Effect.fnUntraced(function*( + acquire: Effect.Effect +) { + const scope = Scope.makeUnsafe() + const value = yield* acquire.pipe( + Scope.provide(scope), + Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) + ) + const self = makeUnsafe(scope, value) + yield* Effect.addFinalizer((exit) => Scope.close(self.backing.backing.ref.current[0], exit)) + return self +}, Effect.uninterruptible) + +/** + * Retrieves the current value of the scoped reference synchronously. + * + * **When to use** + * + * Use when you need immediate synchronous access to the current `ScopedRef` + * value and can guarantee that reading outside the `Effect` API is safe. + * + * @see {@link get} for Effect-wrapped access in Effect programs + * + * @category getters + * @since 4.0.0 + */ +export const getUnsafe = (self: ScopedRef): A => self.backing.backing.ref.current[1] + +/** + * Retrieves the current value of the scoped reference effectfully. + * + * **When to use** + * + * Use to read the value currently stored in a `ScopedRef` inside an `Effect` + * workflow. + * + * @see {@link getUnsafe} for reading the current value synchronously when an unsafe read is acceptable + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: ScopedRef): Effect.Effect => Effect.sync(() => getUnsafe(self)) + +/** + * Creates a new `ScopedRef` from the specified value. + * + * **When to use** + * + * Use to create a `ScopedRef` when the initial value is already available or + * can be produced without acquiring resources. + * + * **Details** + * + * The `evaluate` function runs when the returned effect runs. The returned + * effect requires a `Scope`, and the reference closes the currently stored + * value's scope when that outer scope closes. + * + * **Gotchas** + * + * Do not use `make` for an initial value whose creation acquires resources; use + * `fromAcquire` so acquisition and finalization are tracked. + * + * @see {@link fromAcquire} for creating a `ScopedRef` from an effect that acquires the initial value + * @see {@link set} for replacing the current value with a newly acquired value + * + * @category constructors + * @since 2.0.0 + */ +export const make = (evaluate: LazyArg): Effect.Effect, never, Scope.Scope> => + Effect.suspend(() => { + const scope = Scope.makeUnsafe() + const value = evaluate() + const self = makeUnsafe(scope, value) + return Effect.as(Effect.addFinalizer((exit) => Scope.close(self.backing.backing.ref.current[0], exit)), self) + }) + +/** + * Sets the value of this reference to a newly acquired scoped value, releasing + * any resources associated with the old value. + * + * **When to use** + * + * Use to replace the current value of an existing `ScopedRef` with a newly + * acquired scoped value while releasing resources for the previous value. + * + * **Details** + * + * This method will not return until either the reference is successfully + * changed to the new value, with old resources released, or until the attempt + * to acquire a new value fails. + * + * @category setters + * @since 2.0.0 + */ +export const set: { + (acquire: Effect.Effect): (self: ScopedRef) => Effect.Effect> + (self: ScopedRef, acquire: Effect.Effect): Effect.Effect> +} = dual( + 2, + Effect.fnUntraced( + function*( + self: ScopedRef, + acquire: Effect.Effect + ) { + yield* Scope.close(self.backing.backing.ref.current[0], Exit.void) + const scope = Scope.makeUnsafe() + const value = yield* acquire.pipe( + Scope.provide(scope), + Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) + ) + self.backing.backing.ref.current = [scope, value] + }, + Effect.uninterruptible, + (effect, self) => self.backing.semaphore.withPermit(effect) + ) +) diff --git a/repos/effect/packages/effect/src/Semaphore.ts b/repos/effect/packages/effect/src/Semaphore.ts new file mode 100644 index 0000000..12c0758 --- /dev/null +++ b/repos/effect/packages/effect/src/Semaphore.ts @@ -0,0 +1,517 @@ +/** + * Limits how many effects can use a shared resource at the same time. + * + * A `Semaphore` owns a number of permits. Work can run only after acquiring the + * permits it needs, and those permits are returned when the work finishes. This + * module includes constructors, automatic wrappers that acquire and release + * permits around an effect, manual permit operations, a non-waiting variant for + * work that should only run immediately, and resizing support for an existing + * semaphore. + * + * @since 4.0.0 + */ +import type * as Effect from "./Effect.ts" +import type { Fiber } from "./Fiber.ts" +import { dual } from "./Function.ts" +import * as core from "./internal/core.ts" +import * as internal from "./internal/effect.ts" +import type * as Option from "./Option.ts" + +/** + * A counting semaphore that coordinates concurrent access with permits. + * + * **When to use** + * + * Use to coordinate concurrent effects that need bounded access to a shared + * resource. + * + * **Details** + * + * Effects can acquire permits, wait until enough permits are available, + * release permits, or run with permits that are automatically released when + * the effect exits. + * + * **Example** (Controlling concurrent access) + * + * ```ts + * import { Effect, Semaphore } from "effect" + * + * // Create and use a semaphore for controlling concurrent access + * const program = Effect.gen(function*() { + * const semaphore = yield* Semaphore.make(2) + * + * return yield* semaphore.withPermits(1)( + * Effect.succeed("Resource accessed") + * ) + * }) + * ``` + * + * @see {@link make} for creating a semaphore inside Effect code + * @see {@link makeUnsafe} for creating a semaphore synchronously + * + * @category models + * @since 4.0.0 + */ +export interface Semaphore { + /** + * Adjusts the number of permits available in the semaphore. + * + * **When to use** + * + * Use to change the total permit count of an existing semaphore. + */ + resize(this: Semaphore, permits: number): Effect.Effect + + /** + * Runs an effect with the given number of permits and releases the permits + * when the effect completes. + * + * **When to use** + * + * Use to run an effect while holding a specified number of semaphore permits. + * + * **Details** + * + * This function acquires the specified number of permits before executing + * the provided effect. Once the effect finishes, the permits are released. + * If insufficient permits are available, the function will wait until they + * are released by other tasks. + */ + withPermits(this: Semaphore, permits: number): (self: Effect.Effect) => Effect.Effect + + /** + * Runs an effect with the given number of permits and releases the permits + * when the effect completes. + * + * **When to use** + * + * Use to run an effect while holding exactly one semaphore permit. + * + * **Details** + * + * This function acquires the specified number of permits before executing + * the provided effect. Once the effect finishes, the permits are released. + * If insufficient permits are available, the function will wait until they + * are released by other tasks. + */ + withPermit(self: Effect.Effect): Effect.Effect + + /** + * Runs an effect only if the specified number of permits are immediately + * available. + * + * **When to use** + * + * Use when guarded work should run only if the requested permits are + * immediately available. + * + * **Details** + * + * This function attempts to acquire the specified number of permits. If they + * are available, it runs the effect and releases the permits after the effect + * completes. If permits are not available, the effect does not execute, and + * the result is `Option.none`. + */ + withPermitsIfAvailable( + this: Semaphore, + permits: number + ): (self: Effect.Effect) => Effect.Effect, E, R> + + /** + * Acquires the specified number of permits and returns the resulting + * available permits, suspending the task if they are not yet available. + * Concurrent pending `take` calls are processed in a first-in, first-out manner. + * + * **When to use** + * + * Use to manually acquire permits for lower-level coordination protocols. + */ + take(this: Semaphore, permits: number): Effect.Effect + + /** + * Releases the specified number of permits and returns the resulting + * available permits. + * + * **When to use** + * + * Use to manually return permits acquired by a lower-level coordination + * protocol. + */ + release(this: Semaphore, permits: number): Effect.Effect + + /** + * Releases all permits held by this semaphore and returns the resulting available permits. + * + * **When to use** + * + * Use to return every currently taken permit to the semaphore at once. + */ + readonly releaseAll: Effect.Effect +} + +/** + * Creates a `Semaphore` synchronously with the specified total + * number of permits. + * + * **When to use** + * + * Use to construct a semaphore synchronously when an immediate value is + * required outside an Effect workflow. + * + * **Example** (Creating an unsafe semaphore) + * + * ```ts + * import { Effect, Semaphore } from "effect" + * + * const semaphore = Semaphore.makeUnsafe(3) + * + * const task = (id: number) => + * semaphore.withPermits(1)( + * Effect.gen(function*() { + * yield* Effect.log(`Task ${id} started`) + * yield* Effect.sleep("1 second") + * yield* Effect.log(`Task ${id} completed`) + * }) + * ) + * + * // Only 3 tasks can run concurrently + * const program = Effect.all([ + * task(1), + * task(2), + * task(3), + * task(4), + * task(5) + * ], { concurrency: "unbounded" }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (permits: number): Semaphore => new SemaphoreImpl(permits) + +class SemaphoreImpl implements Semaphore { + public waiters = new Set<() => void>() + public taken = 0 + public permits: number + + constructor(permits: number) { + this.permits = permits + } + + get free() { + return this.permits - this.taken + } + + take(n: number): Effect.Effect { + const take: Effect.Effect = internal.suspend(() => { + if (this.free < n) { + return internal.callback((resume) => { + if (this.free >= n) return resume(take) + const observer = () => { + if (this.free < n) return + this.waiters.delete(observer) + resume(take) + } + this.waiters.add(observer) + return internal.sync(() => { + this.waiters.delete(observer) + }) + }) + } + this.taken += n + return internal.succeed(n) + }) + return take + } + + updateTakenUnsafe(fiber: Fiber, f: (n: number) => number): number { + this.taken = f(this.taken) + if (this.waiters.size > 0) { + fiber.currentDispatcher.scheduleTask(() => { + const iter = this.waiters.values() + let item = iter.next() + while (item.done === false && this.free > 0) { + item.value() + item = iter.next() + } + }, 0) + } + return this.free + } + + updateTaken(f: (n: number) => number): Effect.Effect { + return core.withFiber((fiber) => internal.succeed(this.updateTakenUnsafe(fiber, f))) + } + + resize(permits: number) { + return core.withFiber((fiber) => { + this.permits = permits + if (this.free < 0) return internal.void + this.updateTakenUnsafe(fiber, (taken) => taken) + return internal.void + }) + } + + release(n: number): Effect.Effect { + return this.updateTaken((taken) => taken - n) + } + + get releaseAll(): Effect.Effect { + return this.updateTaken((_) => 0) + } + + withPermits(n: number) { + return (self: Effect.Effect) => + internal.uninterruptibleMask((restore) => + internal.flatMap( + restore(this.take(n)), + (permits) => + internal.onExitPrimitive( + restore(self), + () => { + this.updateTakenUnsafe(internal.getCurrentFiber()!, (taken) => taken - permits) + return undefined + }, + true + ) + ) + ) + } + + readonly withPermit = this.withPermits(1) + + withPermitsIfAvailable(n: number) { + return (self: Effect.Effect) => + internal.uninterruptibleMask((restore) => { + if (this.free < n) return internal.succeedNone + this.taken += n + return internal.onExitPrimitive(restore(internal.asSome(self)), () => { + this.updateTakenUnsafe(internal.getCurrentFiber()!, (taken) => taken - n) + return undefined + }, true) + }) + } +} + +/** + * Creates a `Semaphore` initialized with the specified total number of permits. + * + * **When to use** + * + * Use to create a semaphore inside Effect code for bounding concurrency with + * automatic or manual permit management. + * + * **Example** (Creating a semaphore) + * + * ```ts + * import { Effect, Semaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* Semaphore.make(2) + * + * const task = (id: number) => + * semaphore.withPermits(1)( + * Effect.gen(function*() { + * yield* Effect.log(`Task ${id} acquired permit`) + * yield* Effect.sleep("1 second") + * yield* Effect.log(`Task ${id} releasing permit`) + * }) + * ) + * + * // Run 4 tasks, but only 2 can run concurrently + * yield* Effect.all([task(1), task(2), task(3), task(4)]) + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = (permits: number): Effect.Effect => internal.sync(() => new SemaphoreImpl(permits)) + +/** + * Sets the total number of permits managed by the semaphore. + * + * **When to use** + * + * Use to change the concurrency limit of an existing semaphore while keeping + * current acquisitions in place. + * + * **Details** + * + * Existing acquisitions remain taken after resizing. If the new total is less + * than the currently taken permit count, new acquisitions wait until enough + * permits are released. + * + * @see {@link make} for creating a semaphore with an initial permit count + * @see {@link release} for returning permits without changing semaphore capacity + * + * @category combinators + * @since 4.0.0 + */ +export const resize: { + (permits: number): (self: Semaphore) => Effect.Effect + (self: Semaphore, permits: number): Effect.Effect +} = dual(2, (self: Semaphore, permits: number) => self.resize(permits)) + +/** + * Runs an effect with the given number of permits and releases the permits when + * the effect completes. + * + * **When to use** + * + * Use to run an effect while holding a specified number of semaphore permits + * for the duration of that effect. + * + * **Details** + * + * The effect waits until enough permits are available. Acquired permits are + * released when the wrapped effect exits. + * + * @see {@link withPermit} for acquiring exactly one permit + * @see {@link withPermitsIfAvailable} for running only when permits are immediately available + * @see {@link take} for manually acquiring permits + * @see {@link release} for manually returning permits + * + * @category combinators + * @since 4.0.0 + */ +export const withPermits: { + (self: Semaphore, permits: number): (effect: Effect.Effect) => Effect.Effect + (self: Semaphore, permits: number, effect: Effect.Effect): Effect.Effect +} = ((self: Semaphore, permits: number, effect?: Effect.Effect) => { + const withPermits = self.withPermits(permits) + return effect ? withPermits(effect) : withPermits +}) as any + +/** + * Runs an effect with a single permit and releases the permit when the effect + * completes. + * + * **When to use** + * + * Use to guard an effect with exactly one semaphore permit while automatically + * releasing that permit when the effect exits. + * + * @see {@link withPermits} for acquiring more than one permit + * @see {@link withPermitsIfAvailable} for running only when permits are immediately available + * @see {@link take} for manually acquiring permits + * @see {@link release} for manually returning permits + * + * @category combinators + * @since 4.0.0 + */ +export const withPermit: { + (self: Semaphore): (effect: Effect.Effect) => Effect.Effect + (self: Semaphore, effect: Effect.Effect): Effect.Effect +} = ((self: Semaphore, effect?: Effect.Effect) => { + if (!effect) return self.withPermit + return self.withPermit(effect) +}) as any + +/** + * Runs an effect only if the specified number of permits are immediately + * available. + * + * **When to use** + * + * Use when guarded work should run only if the requested permits are + * immediately available. + * + * **Details** + * + * When the permits are unavailable, the effect is not run and the result is + * `Option.none`. When permits are available, the effect is run, its result is + * wrapped in `Option.some`, and the acquired permits are released when the + * effect exits. + * + * @see {@link withPermits} for the variant that waits until permits are available + * + * @category combinators + * @since 4.0.0 + */ +export const withPermitsIfAvailable: { + (self: Semaphore, permits: number): (effect: Effect.Effect) => Effect.Effect, E, R> + ( + self: Semaphore, + permits: number, + effect: Effect.Effect + ): Effect.Effect, E, R> +} = ((self: Semaphore, permits: number, effect?: Effect.Effect) => { + const withPermits = self.withPermitsIfAvailable(permits) + return effect ? withPermits(effect) : withPermits +}) as any + +/** + * Acquires the specified number of permits and returns the acquired permit + * count. + * + * **When to use** + * + * Use when you need manual permit acquisition for a lower-level protocol with + * explicit acquisition and release control. + * + * **Details** + * + * The effect waits until enough permits are available. + * + * @see {@link withPermit} for automatically acquiring and releasing one permit around an effect + * @see {@link withPermits} for automatically acquiring and releasing multiple permits around an effect + * @see {@link release} for returning manually acquired permits + * + * @category combinators + * @since 4.0.0 + */ +export const take: { + (permits: number): (self: Semaphore) => Effect.Effect + (self: Semaphore, permits: number): Effect.Effect +} = dual(2, (self: Semaphore, permits: number) => self.take(permits)) + +/** + * Releases the specified number of permits and returns the resulting available + * permits. + * + * **When to use** + * + * Use when you need to return permits acquired with `take` in a lower-level + * permit protocol with explicit release control. + * + * **Details** + * + * Running the effect releases the requested permits, wakes waiting acquirers + * when permits become available, and returns the current available permit + * count. + * + * **Gotchas** + * + * Manual `take` / `release` usage must keep permit counts balanced. Prefer + * `withPermit` or `withPermits` when the acquisition can be scoped to one + * effect. + * + * @see {@link take} for manually acquiring permits + * @see {@link releaseAll} for returning every currently taken permit + * @see {@link withPermits} for automatic acquire and release around an effect + * + * @category combinators + * @since 4.0.0 + */ +export const release: { + (permits: number): (self: Semaphore) => Effect.Effect + (self: Semaphore, permits: number): Effect.Effect +} = dual(2, (self: Semaphore, permits: number) => self.release(permits)) + +/** + * Releases all permits held by this semaphore and returns the resulting + * available permits. + * + * **When to use** + * + * Use to return every currently taken permit to a semaphore at once, typically + * during cleanup of manual `take` / `release` protocols. + * + * @see {@link release} for releasing a known permit count + * @see {@link withPermits} for automatic acquire and release around an effect + * + * @category combinators + * @since 4.0.0 + */ +export const releaseAll = (self: Semaphore): Effect.Effect => self.releaseAll diff --git a/repos/effect/packages/effect/src/Sink.ts b/repos/effect/packages/effect/src/Sink.ts new file mode 100644 index 0000000..8dd5bd6 --- /dev/null +++ b/repos/effect/packages/effect/src/Sink.ts @@ -0,0 +1,2174 @@ +/** + * Consumes values from a `Stream` and produces one final result. + * + * A `Sink` may read no input, a fixed amount of input, or keep reading until a + * condition is met. If it reads more than it needs, it can return leftovers so + * the stream can continue from those values. Sinks are used to collect, fold, + * search, count, or otherwise reduce streamed input, and they can be composed + * when a stream needs more than one consuming step. + * + * @since 2.0.0 + */ +import type { NonEmptyReadonlyArray } from "./Array.ts" +import * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Channel from "./Channel.ts" +import * as Clock from "./Clock.ts" +import type * as Context from "./Context.ts" +import * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import type * as Filter from "./Filter.ts" +import type { LazyArg } from "./Function.ts" +import { constant, constFalse, constTrue, constVoid, dual, identity, pipe } from "./Function.ts" +import * as internalStream from "./internal/stream.ts" +import * as Option from "./Option.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import type { Predicate, Refinement } from "./Predicate.ts" +import { hasProperty } from "./Predicate.ts" +import * as PubSub from "./PubSub.ts" +import * as Pull from "./Pull.ts" +import * as Queue from "./Queue.ts" +import * as Result from "./Result.ts" +import * as Scope from "./Scope.ts" +import type { Stream } from "./Stream.ts" +import type * as Types from "./Types.ts" +import type * as Unify from "./Unify.ts" + +const TypeId = "~effect/Sink" + +/** + * A `Sink` is used to consume elements produced by a `Stream`. + * You can think of a sink as a function that will consume a variable amount of + * `In` elements (could be 0, 1, or many), might fail with an error of type `E`, + * and will eventually yield a value of type `A` together with a remainder of + * type `L` (i.e. any leftovers). + * + * **Example** (Running a sink with a stream) + * + * ```ts + * import { Effect, Sink, Stream } from "effect" + * + * // Create a simple sink that always succeeds with a value + * const sink: Sink.Sink = Sink.succeed(42) + * + * // Use the sink to consume a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).then(console.log) + * // Output: 42 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Sink + extends Sink.Variance, Pipeable +{ + readonly transform: ( + upstream: Pull.Pull, never, void>, + scope: Scope.Scope + ) => Effect.Effect, E, R> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: SinkUnify + [Unify.ignoreSymbol]?: SinkUnifyIgnore +} + +/** + * Tuple returned when a `Sink` finishes. + * + * **Details** + * + * The first element is the sink result. The optional second element contains a + * non-empty array of leftover input that was pulled but not consumed. + * + * @category models + * @since 4.0.0 + */ +export type End = readonly [value: A, leftover?: NonEmptyReadonlyArray | undefined] + +const endVoid = Effect.succeed([void 0] as End) + +/** + * Type-level unification support for `Sink` values. + * + * **Details** + * + * This preserves the result, input, leftover, error, and service type + * parameters when Effect's `Unify` machinery normalizes generic values that + * include sinks. Users normally do not need to reference this interface + * directly. + * + * @category models + * @since 2.0.0 + */ +export interface SinkUnify extends Effect.EffectUnify { + Sink?: () => A[Unify.typeSymbol] extends + | Sink< + infer A, + infer In, + infer L, + infer E, + infer R + > + | infer _ ? Sink + : never +} + +/** + * Marker used by Effect's `Unify` machinery for `Sink` values. + * + * **Details** + * + * It prevents the inherited `Effect` unifier from being selected when + * sink-specific unification should preserve the `Sink` type parameters. Users + * normally do not need to reference this interface directly. + * + * @category models + * @since 2.0.0 + */ +export interface SinkUnifyIgnore { + Effect?: true +} + +/** + * Namespace containing types and interfaces for Sink variance and type relationships. + * + * @since 2.0.0 + */ +export declare namespace Sink { + /** + * Type-level variance marker for `Sink`. + * + * **Details** + * + * The result `A`, leftovers `L`, errors `E`, and services `R` are + * covariant. The input type `In` is contravariant because values flow into + * the sink. + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: VarianceStruct + } + /** + * Structural encoding used by `Sink.Variance` to record each `Sink` type + * parameter's variance. + * + * **Details** + * + * `_A`, `_L`, `_E`, and `_R` are covariant markers. `_In` is a + * contravariant marker. + * + * @category models + * @since 2.0.0 + */ + export interface VarianceStruct { + _A: Types.Covariant + _In: Types.Contravariant + _L: Types.Covariant + _E: Types.Covariant + _R: Types.Covariant + } +} + +const sinkVariance = { + _A: identity, + _In: identity, + _L: identity, + _E: identity, + _R: identity +} + +const SinkProto = { + [TypeId]: sinkVariance, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Checks whether a value is a Sink. + * + * **Example** (Checking for a sink) + * + * ```ts + * import { Sink } from "effect" + * + * const sink = Sink.never + * const notStream = { data: [1, 2, 3] } + * + * console.log(Sink.isSink(sink)) // true + * console.log(Sink.isSink(notStream)) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isSink = (u: unknown): u is Sink => hasProperty(u, TypeId) + +/** + * Creates a sink from a `Channel`. + * + * **When to use** + * + * Use to create a `Sink` from a `Channel` that processes non-empty arrays of + * input values. + * + * @see {@link toChannel} for converting a `Sink` back to a `Channel` + * @category constructors + * @since 2.0.0 + */ +export const fromChannel = ( + channel: Channel.Channel< + never, + E, + End, + NonEmptyReadonlyArray, + never, + void, + R + > +): Sink => + fromTransform((upstream, scope) => + Channel.toTransform(channel)(upstream, scope).pipe( + Effect.flatMap(Effect.forever({ disableYield: true })), + Pull.catchDone(Effect.succeed) + ) as Effect.Effect, E, R> + ) + +/** + * Creates a `Sink` from a low-level transform function. + * + * **Details** + * + * The transform receives the upstream pull of non-empty input arrays and the + * active scope, and returns an effect that completes with the sink's `End` + * value. + * + * @category constructors + * @since 4.0.0 + */ +export const fromTransform = ( + transform: ( + upstream: Pull.Pull, never, void>, + scope: Scope.Scope + ) => Effect.Effect, E, R> +): Sink => { + const self = Object.create(SinkProto) + self.transform = transform + return self +} + +/** + * Creates a `Channel` from a Sink. + * + * **Example** (Converting a sink to a channel) + * + * ```ts + * import { Sink } from "effect" + * + * // Create a sink and extract its channel + * const sink = Sink.succeed(42) + * const channel = Sink.toChannel(sink) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const toChannel = ( + self: Sink +): Channel.Channel, NonEmptyReadonlyArray, never, void, R> => + Channel.fromTransform((upstream, scope) => + Effect.succeed(Effect.flatMap( + self.transform(upstream, scope), + Cause.done + )) + ) + +/** + * Creates a pipe-style constructor for sinks over input type `In`. + * + * **Details** + * + * The returned function exposes the sink input as a `Stream`, applies the + * provided pipeline, and uses the final effect's success value as the sink + * result. + * + * @category constructors + * @since 4.0.0 + */ +export const make = (): make.Constructor => (...fns: []) => + fromTransform((upstream, scope) => + pipe( + internalStream.fromChannel(Channel.fromPull(Effect.succeed(upstream))), + ...fns as any as [() => Effect.Effect], + Effect.flatMap((a) => Cause.done>([a])), + Scope.provide(scope) + ) + ) + +/** + * Companion namespace containing overload types for the pipe-style sink + * constructor returned by `Sink.make`. + * + * @since 4.0.0 + */ +export declare namespace make { + /** + * Overloaded function type returned by `Sink.make`. + * + * **Details** + * + * The first pipeline function receives the sink input as a `Stream`. The + * final pipeline step must return an `Effect`, whose success value becomes + * the sink result. + * + * @category models + * @since 4.0.0 + */ + export interface Constructor { + (ab: (_: Stream) => Effect.Effect): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => G, + gh: (_: G) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => Effect.Effect + ): Sink> + ( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => Effect.Effect + ): Sink> + < + E, + R, + B = never, + C = never, + D = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never + >( + ab: (_: Stream) => B, + bc: (_: B) => C, + cd: (_: C) => D, + df: (_: D) => F, + fg: (_: F) => G, + gh: (_: G) => H, + hi: (_: H) => I, + ij: (_: I) => J, + jk: (_: J) => K, + kl: (_: K) => Effect.Effect + ): Sink> + } +} + +/** + * Creates a sink that ignores upstream input and completes from an effect that + * already returns an `End`. + * + * **When to use** + * + * Use when you need to create a sink from an effect that returns both the sink + * result value and optional leftovers. + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectEnd = ( + effect: Effect.Effect, E, R> +): Sink => fromTransform(() => effect) + +/** + * Creates a sink that ignores upstream input and completes with the success + * value of the provided effect. + * + * **Details** + * + * If the effect fails, the sink fails with the same error. + * + * @category constructors + * @since 2.0.0 + */ +export const fromEffect = ( + effect: Effect.Effect +): Sink => fromEffectEnd(Effect.map(effect, (a) => [a])) + +/** + * Creates a sink that offers every consumed input element to a queue. + * + * **Details** + * + * When the upstream stream ends, the sink ends the queue and completes with + * `void`. + * + * @category constructors + * @since 2.0.0 + */ +export const fromQueue = ( + queue: Queue.Queue +): Sink => + fromTransform((upstream) => + upstream.pipe( + Effect.flatMap((arr) => Queue.offerAll(queue, arr)), + Effect.forever({ disableYield: true }), + Pull.catchDone((_) => { + Queue.endUnsafe(queue) + return endVoid + }) + ) + ) + +/** + * Creates a sink that publishes every consumed input element to a `PubSub`. + * + * **Details** + * + * The sink completes with `void` when the upstream stream ends. + * + * @category constructors + * @since 2.0.0 + */ +export const fromPubSub = ( + pubsub: PubSub.PubSub +): Sink => forEachArray((arr) => PubSub.publishAll(pubsub, arr)) + +/** + * A sink that immediately ends with the specified value. + * + * **Example** (Succeeding with a value) + * + * ```ts + * import { Effect, Sink, Stream } from "effect" + * + * // Create a sink that always yields the same value + * const sink = Sink.succeed(42) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).then(console.log) + * // Output: 42 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const succeed = (a: A, leftovers?: NonEmptyReadonlyArray | undefined): Sink => + fromEffectEnd(Effect.succeed([a, leftovers])) + +/** + * A sink that immediately ends with the specified lazily evaluated value. + * + * @category constructors + * @since 2.0.0 + */ +export const sync = (a: LazyArg): Sink => fromEffect(Effect.sync(a)) + +/** + * A sink that is created from a lazily evaluated sink. + * + * @category constructors + * @since 2.0.0 + */ +export const suspend = (evaluate: LazyArg>): Sink => + fromTransform((upstream, scope) => evaluate().transform(upstream, scope)) + +/** + * A sink that always fails with the specified error. + * + * **Example** (Failing with an error) + * + * ```ts + * import { Effect, Sink, Stream } from "effect" + * + * // Create a sink that always fails + * const sink = Sink.fail(new Error("Sink failed")) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).catch(console.log) + * // Output: Error: Sink failed + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fail = (e: E): Sink => fromEffectEnd(Effect.fail(e)) + +/** + * A sink that always fails with the specified lazily evaluated error. + * + * **Example** (Failing with a lazy error) + * + * ```ts + * import { Effect, Sink, Stream } from "effect" + * + * // Create a sink that fails with a lazy error + * const sink = Sink.failSync(() => new Error("Lazy error")) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).catch(console.log) + * // Output: Error: Lazy error + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failSync = (evaluate: LazyArg): Sink => + fromEffectEnd(Effect.failSync(evaluate)) + +/** + * Creates a sink halting with a specified `Cause`. + * + * **Example** (Failing with a cause) + * + * ```ts + * import { Cause, Effect, Sink, Stream } from "effect" + * + * // Create a sink that fails with a specific cause + * const sink = Sink.failCause(Cause.fail(new Error("Custom cause"))) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).catch(console.log) + * // Output: Error: Custom cause + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCause = (cause: Cause.Cause): Sink => + fromEffectEnd(Effect.failCause(cause)) + +/** + * Creates a sink halting with a specified lazily evaluated `Cause`. + * + * **Example** (Failing with a lazy cause) + * + * ```ts + * import { Cause, Effect, Sink, Stream } from "effect" + * + * // Create a sink that fails with a lazy cause + * const sink = Sink.failCauseSync(() => Cause.fail(new Error("Lazy cause"))) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).catch(console.log) + * // Output: Error: Lazy cause + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCauseSync = (evaluate: LazyArg>): Sink => + fromEffectEnd(Effect.failCauseSync(evaluate)) + +/** + * Creates a sink halting with a specified defect. + * + * **Example** (Dying with a defect) + * + * ```ts + * import { Effect, Sink, Stream } from "effect" + * + * // Create a sink that dies with a defect + * const sink = Sink.die(new Error("Defect error")) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program).catch(console.log) + * // Output: Error: Defect error + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const die = (defect: unknown): Sink => fromEffectEnd(Effect.die(defect)) + +/** + * A sink that never completes. + * + * @category constructors + * @since 2.0.0 + */ +export const never: Sink = fromEffectEnd(Effect.never) + +/** + * Drops leftovers produced by a sink. + * + * **Details** + * + * The sink result is preserved, but any leftover elements are discarded + * instead of being returned to downstream sink composition. This does not + * continue pulling additional elements from the upstream stream. + * + * @category filtering + * @since 2.0.0 + */ +export const ignoreLeftover = (self: Sink): Sink => + mapEnd(self, ([a]) => [a]) + +/** + * Consumes and ignores all stream inputs. + * + * **When to use** + * + * Use to consume all upstream input and complete with void when the input + * values and any aggregate result are not needed. + * + * @see {@link count} for consuming all input while returning the number of elements + * @see {@link forEach} for consuming all input while running an effect for each element + * + * @category constructors + * @since 2.0.0 + */ +export const drain: Sink = fromTransform((upstream) => + Pull.catchDone( + Effect.forever(upstream, { disableYield: true }), + () => endVoid + ) +) + +/** + * A sink that folds its inputs with the provided function, termination + * predicate and initial state. + * + * **When to use** + * + * Use to accumulate stream input element by element with an effectful step and + * stop based on the accumulated state. + * + * **Details** + * + * The initial state is evaluated lazily. Each input element is folded with the + * effectful function, and the sink continues while `contFn` returns `true`. If + * the sink stops in the middle of a pulled array, the remaining elements from + * that array are returned as leftovers. + * + * @see {@link foldArray} for folding each pulled non-empty input array at once + * @see {@link foldUntil} for folding until a fixed maximum number of elements is consumed + * + * @category folding + * @since 2.0.0 + */ +export const fold = ( + s: LazyArg, + contFn: Predicate, + f: (s: S, input: In) => Effect.Effect +): Sink => + fromTransform((upstream) => { + let state = s() + return Effect.gen(function*() { + while (true) { + const arr = yield* upstream + for (let i = 0; i < arr.length; i++) { + state = yield* f(state, arr[i]) + if (contFn(state)) continue + return [ + state, + (i + 1) < arr.length ? (arr.slice(i + 1) as any) : undefined + ] as const + } + } + }).pipe( + Pull.catchDone(() => Effect.succeed>([state])) + ) + }) + +/** + * Folds non-empty input arrays into state with an effectful function. + * + * **When to use** + * + * Use to update state with an effectful function once per pulled non-empty + * input array when batch-level processing is the natural unit. + * + * **Details** + * + * The initial state is evaluated lazily. After each pulled array is folded, + * the sink continues while `contFn` returns `true`; otherwise it completes + * with the current state. + * + * @see {@link fold} for folding element by element and returning leftovers when stopping mid-array + * @see {@link reduceWhileArrayEffect} for array-level effectful reducing that checks the predicate before consuming input + * + * @category folding + * @since 4.0.0 + */ +export const foldArray = ( + s: LazyArg, + contFn: Predicate, + f: (s: S, input: Arr.NonEmptyReadonlyArray) => Effect.Effect +): Sink => + fromTransform((upstream) => { + let state = s() + return Effect.gen(function*() { + while (true) { + const arr = yield* upstream + state = yield* f(state, arr) + if (contFn(state)) continue + return [state] as const + } + }).pipe( + Pull.catchDone(() => Effect.succeed>([state])) + ) + }) + +/** + * Folds input elements into state until the specified maximum number of + * elements has been consumed or the upstream stream ends. + * + * **Details** + * + * If the sink stops in the middle of a pulled array, the remaining elements + * from that array are returned as leftovers. + * + * @category folding + * @since 2.0.0 + */ +export const foldUntil = ( + s: LazyArg, + max: number, + f: (s: S, input: In) => Effect.Effect +): Sink => + fold( + () => [s(), 0], + (tuple) => tuple[1] < max, + ([output, count], input) => Effect.map(f(output, input), (s) => [s, count + 1] as const) + ).pipe( + map((tuple) => tuple[0]) + ) + +/** + * A sink that returns whether all elements satisfy the specified predicate. + * + * **When to use** + * + * Use to reduce a stream to a boolean that is true only when every input + * satisfies a pure predicate. + * + * @see {@link some} for the dual any-match check + * + * @category constructors + * @since 2.0.0 + */ +export const every = (predicate: Predicate): Sink => + fold( + constTrue, + identity, + (_, a) => Effect.succeed(predicate(a)) + ) + +/** + * A sink that returns whether an element satisfies the specified predicate. + * + * **When to use** + * + * Use to reduce a stream to a boolean that is true when any input satisfies a + * pure predicate. + * + * @see {@link every} for the all-match check + * + * @category constructors + * @since 2.0.0 + */ +export const some = (predicate: Predicate): Sink => + fold( + constFalse, + (b) => !b, + (_, a) => Effect.succeed(predicate(a)) + ) + +/** + * Transforms this sink's result. + * + * **When to use** + * + * Use to compute a new result from the original sink result while preserving + * the sink's input consumption behavior. + * + * **Details** + * + * The transformed sink preserves the original sink's input type, leftovers, + * errors, and requirements. + * + * @see {@link mapEffect} for effectful result transformations + * @see {@link as} for replacing the result with a constant value + * @see {@link mapEnd} for transforming both the result and leftovers + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A) => A2): (self: Sink) => Sink + (self: Sink, f: (a: A) => A2): Sink +} = dual( + 2, + (self: Sink, f: (a: A) => A2): Sink => + mapEnd(self, ([a, l]) => [f(a), l]) +) + +/** + * Sets the sink's result to a constant value. + * + * **When to use** + * + * Use to keep a sink's input consumption, errors, requirements, and leftovers + * while replacing only its result with a known value. + * + * @see {@link map} for computing the replacement from the original result + * + * @category mapping + * @since 2.0.0 + */ +export const as: { + (a2: A2): (self: Sink) => Sink + (self: Sink, a2: A2): Sink +} = dual( + 2, + (self: Sink, a2: A2): Sink => map(self, () => a2) +) + +/** + * Transforms this sink's input elements. + * + * @category mapping + * @since 2.0.0 + */ +export const mapInput: { + (f: (input: In0) => In): (self: Sink) => Sink + (self: Sink, f: (input: In0) => In): Sink +} = dual( + 2, + (self: Sink, f: (input: In0) => In): Sink => + mapInputArray(self, Arr.map(f)) +) + +/** + * Transforms this sink's input elements effectfully. + * + * @category mapping + * @since 2.0.0 + */ +export const mapInputEffect: { + ( + f: (input: In0) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (input: In0) => Effect.Effect + ): Sink +} = dual( + 2, + ( + self: Sink, + f: (input: In0) => Effect.Effect + ): Sink => mapInputArrayEffect(self, Effect.forEach(f)) +) + +/** + * Transforms each non-empty array of upstream input before it is fed to this + * sink. + * + * @category mapping + * @since 4.0.0 + */ +export const mapInputArray: { + ( + f: (input: Arr.NonEmptyReadonlyArray) => Arr.NonEmptyReadonlyArray + ): (self: Sink) => Sink + ( + self: Sink, + f: (input: Arr.NonEmptyReadonlyArray) => Arr.NonEmptyReadonlyArray + ): Sink +} = dual( + 2, + ( + self: Sink, + f: (input: Arr.NonEmptyReadonlyArray) => Arr.NonEmptyReadonlyArray + ): Sink => fromTransform((upstream, scope) => self.transform(Effect.map(upstream, f), scope)) +) + +/** + * Transforms each non-empty array of upstream input effectfully before it is + * fed to this sink. + * + * @category mapping + * @since 4.0.0 + */ +export const mapInputArrayEffect: { + ( + f: (input: Arr.NonEmptyReadonlyArray) => Effect.Effect, E2, R2> + ): (self: Sink) => Sink + ( + self: Sink, + f: (input: Arr.NonEmptyReadonlyArray) => Effect.Effect, E2, R2> + ): Sink +} = dual( + 2, + ( + self: Sink, + f: (input: Arr.NonEmptyReadonlyArray) => Effect.Effect, E2, R2> + ): Sink => + fromTransform((upstream, scope) => + self.transform( + Effect.flatMap(upstream, f) as any, + scope + ) + ) +) + +/** + * Transforms the full `End` produced by this sink. + * + * **Details** + * + * This can change both the result value and the optional leftovers. + * + * @category mapping + * @since 4.0.0 + */ +export const mapEnd: { + ( + f: (a: End) => End + ): (self: Sink) => Sink + (self: Sink, f: (a: End) => End): Sink +} = dual( + 2, + ( + self: Sink, + f: (a: End) => End + ): Sink => + fromTransform((upstream, scope) => + Effect.map( + self.transform(upstream, scope), + f + ) + ) +) + +const transformEffect = ( + self: Sink, + f: (effect: Effect.Effect, E, R>) => Effect.Effect, E2, R2> +): Sink => fromTransform((upstream, scope) => f(self.transform(upstream, scope))) + +/** + * Transforms the full `End` produced by this sink effectfully. + * + * **Details** + * + * This can change both the result value and the optional leftovers, and the + * transformation can fail or require services. + * + * @category mapping + * @since 4.0.0 + */ +export const mapEffectEnd: { + ( + f: (end: End) => Effect.Effect, E2, R2> + ): (self: Sink) => Sink + ( + self: Sink, + f: (end: End) => Effect.Effect, E2, R2> + ): Sink +} = dual(2, ( + self: Sink, + f: (end: End) => Effect.Effect, E2, R2> +): Sink => transformEffect(self, Effect.flatMap(f))) + +/** + * Transforms this sink's result effectfully. + * + * **When to use** + * + * Use when you need a sink result transformation that is effectful, can fail, + * or requires services. + * + * **Details** + * + * The transformed sink preserves the original sink's input consumption and + * leftovers while adding the errors and requirements of the transformation. + * + * @see {@link map} for pure result transformations + * @see {@link mapEffectEnd} for effectfully transforming both the result and leftovers + * @see {@link flatMap} for continuing with another sink based on the result + * + * @category mapping + * @since 2.0.0 + */ +export const mapEffect: { + ( + f: (a: A) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (a: A) => Effect.Effect + ): Sink +} = dual(2, ( + self: Sink, + f: (a: A) => Effect.Effect +): Sink => mapEffectEnd(self, ([a, l]) => Effect.map(f(a), (a2) => [a2, l] as End))) + +/** + * Transforms the errors emitted by this sink using `f`. + * + * @category mapping + * @since 2.0.0 + */ +export const mapError: { + (f: (error: E) => E2): (self: Sink) => Sink + (self: Sink, f: (error: E) => E2): Sink +} = dual(2, ( + self: Sink, + f: (error: E) => E2 +): Sink => transformEffect(self, Effect.mapError(f))) + +/** + * Transforms the leftovers emitted by this sink using `f`. + * + * @category mapping + * @since 2.0.0 + */ +export const mapLeftover: { + (f: (leftover: L) => L2): (self: Sink) => Sink + (self: Sink, f: (leftover: L) => L2): Sink +} = dual(2, ( + self: Sink, + f: (leftover: L) => L2 +): Sink => mapEnd(self, ([a, l]) => [a, l && Arr.map(l, f)])) + +/** + * Collects up to `n` input elements into an array. + * + * **Details** + * + * If `n` is less than or equal to zero, the sink completes with an empty array. + * If more elements are pulled than needed, the remaining elements from the same + * array are returned as leftovers. + * + * @category collecting + * @since 2.0.0 + */ +export const take = (n: number): Sink, In, In> => + fromTransform((upstream) => { + const taken: Array = [] + if (n <= 0) { + return Effect.succeed([taken] as const) + } + let leftover: NonEmptyReadonlyArray | undefined = undefined + return upstream.pipe( + Effect.flatMap((arr) => { + if (taken.length + arr.length <= n) { + taken.push(...arr) + if (taken.length === n) { + return Cause.done() + } + return Effect.void + } + for (let i = 0; i < arr.length; i++) { + taken.push(arr[i]) + if (taken.length === n) { + if ((i + 1) < arr.length) { + leftover = arr.slice(i + 1) as any + } + return Cause.done() + } + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([taken, leftover] as const)) + ) + }) + +/** + * Runs this sink until it yields a result, then uses that result to create + * another sink from the provided function which will continue to run until it + * yields a result. + * + * **When to use** + * + * Use to compose sinks when the next sink depends on the result produced by the + * previous sink. + * + * **Details** + * + * Leftovers from the first sink are fed to the sink returned by `f` before more + * upstream input is pulled. + * + * @see {@link map} for transforming the result without switching sinks + * @see {@link mapEffect} for effectfully transforming the result without switching sinks + * + * @category sequencing + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (a: A) => Sink + ): (self: Sink) => Sink + ( + self: Sink, + f: (a: A) => Sink + ): Sink +} = dual(2, ( + self: Sink, + f: (a: A) => Sink +): Sink => + fromTransform((upstream, scope) => { + let upstreamDone = false + const pull = Effect.catchCause(upstream, (cause) => { + upstreamDone = true + return Effect.failCause(cause) + }) + return Effect.flatMap( + self.transform(pull, scope), + ([a, leftover]) => + f(a).transform( + Effect.suspend(() => { + if (leftover) { + const arr = leftover as Arr.NonEmptyReadonlyArray + leftover = undefined + return Effect.succeed(arr) + } else if (upstreamDone) { + return Cause.done() + } + return upstream + }), + scope + ) + ) + })) + +/** + * A sink that reduces input elements from the provided `initial` state with + * `f` while the specified `predicate` returns `true`. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceWhile = ( + initial: LazyArg, + predicate: Predicate, + f: (s: S, input: In) => S +): Sink => + fromTransform((upstream) => { + let state = initial() + let leftover: NonEmptyReadonlyArray | undefined = undefined + if (!predicate(state)) { + return Effect.succeed([state] as const) + } + return upstream.pipe( + Effect.flatMap((arr) => { + for (let i = 0; i < arr.length; i++) { + state = f(state, arr[i]) + if (!predicate(state)) { + if ((i + 1) < arr.length) { + leftover = arr.slice(i + 1) as any + } + return Cause.done() + } + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([state, leftover] as const)) + ) + }) + +/** + * A sink that effectfully reduces input elements from the provided `initial` + * state with `f` while the specified `predicate` returns `true`. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceWhileEffect = ( + initial: LazyArg, + predicate: Predicate, + f: (s: S, input: In) => Effect.Effect +): Sink => + fromTransform((upstream) => { + let state = initial() + let leftover: NonEmptyReadonlyArray | undefined = undefined + if (!predicate(state)) { + return Effect.succeed([state] as const) + } + return upstream.pipe( + Effect.flatMap((arr) => { + let i = 0 + return Effect.whileLoop({ + while: () => i < arr.length, + body: constant(Effect.flatMap(Effect.suspend(() => f(state, arr[i++])), (s) => { + state = s + if (!predicate(state)) { + if (i < arr.length) { + leftover = arr.slice(i) as any + } + return Cause.done() + } + return Effect.void + })), + step: constVoid + }) + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([state, leftover] as const)) + ) + }) + +/** + * A sink that reduces non-empty input arrays from the provided `initial` state + * with `f` while the specified `predicate` returns `true`. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceWhileArray = ( + initial: LazyArg, + contFn: Predicate, + f: (s: S, input: NonEmptyReadonlyArray) => S +): Sink => + fromTransform((upstream) => { + let state = initial() + if (!contFn(state)) { + return Effect.succeed([state] as const) + } + return upstream.pipe( + Effect.flatMap((arr) => { + for (let i = 0; i < arr.length; i++) { + state = f(state, arr) + if (!contFn(state)) { + return Cause.done() + } + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([state] as const)) + ) + }) + +/** + * A sink that effectfully reduces non-empty input arrays from the provided + * `initial` state with `f` while the specified `predicate` returns `true`. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceWhileArrayEffect = ( + initial: LazyArg, + predicate: Predicate, + f: (s: S, input: NonEmptyReadonlyArray) => Effect.Effect +): Sink => + fromTransform((upstream) => { + let state = initial() + if (!predicate(state)) { + return Effect.succeed([state] as const) + } + return upstream.pipe( + Effect.flatMap((arr) => f(state, arr)), + Effect.flatMap((s) => { + state = s + if (!predicate(state)) { + return Cause.done() + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([state] as const)) + ) + }) + +/** + * A sink that reduces its inputs using the provided function `f` starting from + * the provided `initial` state. + * + * @category reducing + * @since 4.0.0 + */ +export const reduce = (initial: LazyArg, f: (s: S, input: In) => S): Sink => + reduceArray(initial, (s, arr) => { + for (let i = 0; i < arr.length; i++) { + s = f(s, arr[i]) + } + return s + }) + +/** + * A sink that reduces its inputs using the provided function `f` starting from + * the specified `initial` state. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceArray = ( + initial: LazyArg, + f: (s: S, input: NonEmptyReadonlyArray) => S +): Sink => + fromTransform((upstream) => { + let state = initial() + return upstream.pipe( + Effect.flatMap((arr) => { + state = f(state, arr) + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([state] as const)) + ) + }) + +/** + * A sink that reduces its inputs using the provided effectful function `f` + * starting from the specified `initial` state. + * + * @category reducing + * @since 4.0.0 + */ +export const reduceEffect = ( + initial: LazyArg, + f: (s: S, input: In) => Effect.Effect +): Sink => reduceWhileEffect(initial, constTrue, f) as any + +const head_ = reduceWhile(Option.none, Option.isNone, (_, in_) => Option.some(in_)) + +/** + * Creates a sink containing the first value. + * + * **Details** + * + * Returns `Option.some(first)` for non-empty input, or `Option.none` when the + * upstream ends without input. The first element is consumed; later elements + * from the same pulled array are emitted as leftovers. + * + * @category constructors + * @since 2.0.0 + */ +export const head = (): Sink, In, In> => head_ as any + +const last_ = reduceArray(Option.none, (_, arr) => Arr.last(arr)) + +/** + * Creates a sink containing the last value. + * + * **When to use** + * + * Use when you need to consume all upstream input and keep only the final + * element. + * + * **Details** + * + * Returns `Option.some(last)` with the final input value, or `Option.none` when + * the upstream ends without input. + * + * **Gotchas** + * + * This sink produces a result only when the upstream ends, so it does not + * complete for a stream that does not end. + * + * @see {@link head} for taking the first input value instead + * + * @category constructors + * @since 2.0.0 + */ +export const last = (): Sink, In> => last_ as any + +/** + * Creates a sink containing the first value matched by a synchronous predicate. + * + * **When to use** + * + * Use to scan stream input until the first matching element is found and return + * that element as an `Option`. + * + * **Details** + * + * Returns `Option.none` if the upstream stream ends before a match is found. + * Refinement predicates narrow the returned value type. The matching input is + * consumed; any later elements from the same pulled array are returned as + * leftovers. + * + * @see {@link findEffect} for an effectful predicate that can fail or require services + * + * @category constructors + * @since 4.0.0 + */ +export const find: { + (refinement: Refinement): Sink, In, In> + (predicate: Predicate): Sink, In, In> +} = (predicate: Predicate): Sink, In, In> => + reduceWhile( + Option.none, + Option.isNone, + (acc, in_) => predicate(in_) ? Option.some(in_) : acc + ) + +/** + * Creates a sink containing the first value matched by an effectful predicate. + * + * **When to use** + * + * Use when you need to run effects, fail, or use services while searching for + * the first matching input. + * + * **Details** + * + * Returns `Option.some` with the first input whose predicate result is `true`, + * or `Option.none` if the upstream stream ends first. If the predicate effect + * fails, the sink fails with the same error. + * + * @see {@link find} for the synchronous predicate variant + * + * @category constructors + * @since 2.0.0 + */ +export const findEffect = ( + predicate: (input: In) => Effect.Effect +): Sink, In, In, E, R> => + reduceWhileEffect( + Option.none, + Option.isNone, + (acc, in_) => Effect.map(predicate(in_), (b) => b ? Option.some(in_) : acc) + ) + +/** + * Creates a sink which sums up its inputs. + * + * @category constructors + * @since 2.0.0 + */ +export const sum: Sink = reduceArray(() => 0, (s, arr) => { + for (let i = 0; i < arr.length; i++) { + s += arr[i] + } + return s +}) + +/** + * A sink that counts the number of elements fed to it. + * + * **When to use** + * + * Use to consume input and return only the number of elements received. + * + * @category constructors + * @since 2.0.0 + */ +export const count: Sink = reduceArray(() => 0, (s, arr) => s + arr.length) + +/** + * Accumulates incoming elements into an array. + * + * **When to use** + * + * Use when you need a sink result containing all upstream input elements. + * + * @see {@link take} for collecting only a fixed number of input elements + * + * @category constructors + * @since 4.0.0 + */ +export const collect = (): Sink, In> => + reduceArray(Arr.empty, (s, arr) => { + s.push(...arr) + return s + }) + +/** + * Collects the longest input prefix whose elements satisfy the predicate or + * refinement. + * + * **Details** + * + * The first failing input is consumed and excluded from the result. Any later + * elements from the same pulled array are returned as leftovers. + * + * @category constructors + * @since 4.0.0 + */ +export const takeWhile: { + (refinement: Refinement): Sink, In, In> + (predicate: Predicate): Sink, In, In> +} = (predicate: Predicate): Sink, In, In> => + fromTransform((upstream) => { + const out = Arr.empty() + return upstream.pipe( + Effect.flatMap((arr) => { + for (let i = 0; i < arr.length; i++) { + if (!predicate(arr[i])) { + const leftover: Arr.NonEmptyReadonlyArray | undefined = (i + 1) < arr.length + ? arr.slice(i + 1) as any + : undefined + return Cause.done([out, leftover] as const) + } + out.push(arr[i]) + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone((end) => Effect.succeed, In>>(end ?? [out])) + ) + }) + +/** + * Applies a `Filter` to input elements while it succeeds, collecting each + * successful output. + * + * **Details** + * + * The first input for which the filter fails is consumed and excluded from the + * result. Any later elements from the same pulled array are returned as + * leftovers. + * + * @category constructors + * @since 4.0.0 + */ +export const takeWhileFilter = ( + filter: Filter.Filter +): Sink, In, In> => + fromTransform((upstream) => { + const out = Arr.empty() + return upstream.pipe( + Effect.flatMap((arr) => { + for (let i = 0; i < arr.length; i++) { + const result = filter(arr[i]) + if (Result.isFailure(result)) { + const leftover: Arr.NonEmptyReadonlyArray | undefined = (i + 1) < arr.length + ? arr.slice(i + 1) as any + : undefined + return Cause.done([out, leftover] as const) + } + out.push(result.success) + } + return Effect.void + }), + Effect.forever({ disableYield: true }), + Pull.catchDone((end) => Effect.succeed, In>>(end ?? [out])) + ) + }) + +/** + * Collects input elements effectfully while the predicate succeeds. + * + * **Details** + * + * The first input for which the predicate returns `false` is consumed and + * excluded from the result. Any later elements from the same pulled array are + * returned as leftovers. + * + * @category constructors + * @since 4.0.0 + */ +export const takeWhileEffect: { + (predicate: (input: In) => Effect.Effect): Sink, In, In, E, R> +} = ( + predicate: (input: In) => Effect.Effect +): Sink, In, In, E, R> => + fromTransform((upstream) => { + const out = Arr.empty() + let leftover: Arr.NonEmptyReadonlyArray | undefined = undefined + return upstream.pipe( + Effect.flatMap((arr) => { + let i = 0 + return Effect.whileLoop({ + while: () => i < arr.length, + body: constant(Effect.flatMap( + Effect.suspend(() => { + const input = arr[i++] + return Effect.map(predicate(input), (passes) => [input, passes] as const) + }), + ([input, passes]) => { + if (!passes) { + if (i < arr.length) { + leftover = arr.slice(i) as any + } + return Cause.done() + } + out.push(input) + return Effect.void + } + )), + step: constVoid + }) + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([out, leftover] as const)) + ) + }) + +/** + * Applies a `FilterEffect` to input elements effectfully while it succeeds, + * collecting each successful output. + * + * **Details** + * + * The first input for which the filter fails is consumed and excluded from the + * result. Any later elements from the same pulled array are returned as + * leftovers. + * + * @category constructors + * @since 4.0.0 + */ +export const takeWhileFilterEffect = ( + filter: Filter.FilterEffect +): Sink, In, In, E, R> => + fromTransform((upstream) => { + const out = Arr.empty() + let leftover: Arr.NonEmptyReadonlyArray | undefined = undefined + return upstream.pipe( + Effect.flatMap((arr) => { + let i = 0 + return Effect.whileLoop({ + while: () => i < arr.length, + body: constant(Effect.flatMap(Effect.suspend(() => filter(arr[i++])), (result) => { + if (Result.isFailure(result)) { + if (i < arr.length) { + leftover = arr.slice(i) as any + } + return Cause.done() + } + out.push(result.success) + return Effect.void + })), + step: constVoid + }) + }), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => Effect.succeed([out, leftover] as const)) + ) + }) + +/** + * Collects input elements until the predicate returns `true`, including the + * matching element in the result. + * + * @category constructors + * @since 4.0.0 + */ +export const takeUntil = (predicate: Predicate): Sink, In, In> => + suspend(() => { + let done = false + return takeWhile((i) => { + if (done) return false + done = predicate(i) + return true + }) + }) + +/** + * Collects input elements effectfully until the predicate returns `true`, + * including the matching element in the result. + * + * **Details** + * + * If the predicate effect fails, the sink fails with the same error. + * + * @category constructors + * @since 4.0.0 + */ +export const takeUntilEffect = ( + predicate: (input: In) => Effect.Effect +): Sink, In, In, E, R> => + suspend(() => { + let done = false + return takeWhileEffect((input) => { + if (done) { + return Effect.succeed(false) + } + return Effect.map(predicate(input), (b) => { + done = b + return true + }) + }) + }) + +/** + * A sink that executes the provided effectful function for every item fed + * to it. + * + * **Example** (Running effects for each item) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * // Create a sink that logs each item + * const sink = Sink.forEach((item: number) => Console.log(`Processing: ${item}`)) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program) + * // Output: + * // Processing: 1 + * // Processing: 2 + * // Processing: 3 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const forEach = ( + f: (input: In) => Effect.Effect +): Sink => forEachArray(Effect.forEach((_) => f(_), { discard: true })) + +/** + * A sink that executes the provided effectful function for every Chunk fed + * to it. + * + * **Example** (Running effects for each chunk) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * // Create a sink that processes chunks + * const sink = Sink.forEachArray((chunk: ReadonlyArray) => + * Console.log( + * `Processing chunk of ${chunk.length} items: [${chunk.join(", ")}]` + * ) + * ) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3, 4, 5) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program) + * // Output: Processing chunk of 5 items: [1, 2, 3, 4, 5] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const forEachArray = ( + f: (input: NonEmptyReadonlyArray) => Effect.Effect +): Sink => + fromTransform((upstream) => + upstream.pipe( + Effect.flatMap(f), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => endVoid) + ) + ) + +/** + * Runs an effectful function for each input element while it returns `true`. + * + * **Details** + * + * The sink stops consuming input when the function returns `false` or when the + * upstream stream ends, and completes with `void`. + * + * @category constructors + * @since 2.0.0 + */ +export const forEachWhile = ( + f: (input: In) => Effect.Effect +): Sink => + forEachWhileArray(Effect.fnUntraced(function*(input) { + for (let i = 0; i < input.length; i++) { + const cont = yield* f(input[i]) + if (!cont) return false + } + return true + })) + +/** + * Runs an effectful function for each non-empty input array while it returns + * `true`. + * + * **Details** + * + * The sink stops consuming input when the function returns `false` or when the + * upstream stream ends, and completes with `void`. + * + * @category constructors + * @since 4.0.0 + */ +export const forEachWhileArray = ( + f: (input: NonEmptyReadonlyArray) => Effect.Effect +): Sink => + fromTransform((upstream) => + upstream.pipe( + Effect.flatMap(f), + Effect.flatMap((cont) => cont ? Effect.void : Cause.done()), + Effect.forever({ disableYield: true }), + Pull.catchDone(() => endVoid) + ) + ) + +/** + * Creates a sink produced from a scoped effect. + * + * **Example** (Unwrapping a sink effect) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * // Create a sink from an effect that produces a sink + * const sinkEffect = Effect.succeed( + * Sink.forEach((item: number) => Console.log(`Item: ${item}`)) + * ) + * const sink = Sink.unwrap(sinkEffect) + * + * // Use it with a stream + * const stream = Stream.make(1, 2, 3) + * const program = Stream.run(stream, sink) + * + * Effect.runPromise(program) + * // Output: + * // Item: 1 + * // Item: 2 + * // Item: 3 + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unwrap = ( + effect: Effect.Effect, E, R> +): Sink | R2> => fromChannel(Channel.unwrap(Effect.map(effect, toChannel))) + +/** + * Runs a summary effect when the sink starts and again when it completes. + * + * @category mapping + * @since 2.0.0 + */ +export const summarized: { + ( + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ): (self: Sink) => Sink<[A, A3], In, L, E2 | E, R2 | R> + ( + self: Sink, + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 + ): Sink<[A, A3], In, L, E | E2, R | R2> +} = dual(3, ( + self: Sink, + summary: Effect.Effect, + f: (start: A2, end: A2) => A3 +): Sink<[A, A3], In, L, E | E2, R | R2> => + fromTransform(Effect.fnUntraced(function*(upstream, scope) { + const start = yield* summary + const [done, leftover] = yield* self.transform(upstream, scope) + const end = yield* summary + return [[done, f(start, end)], leftover] as const + }))) + +/** + * Returns the sink that executes this one and times its execution. + * + * @category mapping + * @since 2.0.0 + */ +export const withDuration = ( + self: Sink +): Sink<[A, Duration.Duration], In, L, E, R> => + summarized(self, Clock.currentTimeNanos, (start, end) => Duration.nanos(end - start)) + +/** + * A sink that drains all input and returns the elapsed duration. + * + * @category constructors + * @since 2.0.0 + */ +export const timed: Sink = map(withDuration(drain), ([, duration]) => duration) + +/** + * Provides a `Context` to this sink. + * + * **Details** + * + * Services contained in the provided context are removed from the sink's + * service requirements. + * + * @category services + * @since 2.0.0 + */ +export const provideContext: { + ( + context: Context.Context + ): (self: Sink) => Sink> + ( + self: Sink, + context: Context.Context + ): Sink> +} = dual(2, ( + self: Sink, + context: Context.Context +): Sink> => + fromTransform((upstream, scope) => + self.transform(upstream, scope).pipe( + Effect.provideContext(context) + ) + )) + +/** + * Provides a single service implementation to this sink. + * + * **Details** + * + * The service identified by `key` is removed from the sink's service + * requirements. + * + * @category services + * @since 4.0.0 + */ +export const provideService: { + ( + key: Context.Key, + value: Types.NoInfer + ): (self: Sink) => Sink> + ( + self: Sink, + key: Context.Key, + value: Types.NoInfer + ): Sink> +} = dual(3, ( + self: Sink, + key: Context.Key, + value: Types.NoInfer +): Sink> => + fromTransform((upstream, scope) => + self.transform(upstream, scope).pipe( + Effect.provideService(key, value) + ) + )) + +/** + * Runs a fallback sink if this sink fails with a typed error. + * + * **Details** + * + * The fallback is built from the error and continues consuming from the same + * upstream stream. If the upstream stream had already ended, the fallback sees + * the upstream end instead. + * + * @category error handling + * @since 2.0.0 + */ +export const orElse: { + ( + f: (error: Types.NoInfer) => Sink + ): (self: Sink) => Sink + ( + self: Sink, + f: (error: E) => Sink + ): Sink +} = dual(2, ( + self: Sink, + f: (error: E) => Sink +): Sink => + fromTransform((upstream, scope) => { + let upstreamDone = false + const pull = Effect.catchCause(upstream, (cause) => { + upstreamDone = true + return Effect.failCause(cause) + }) + return Effect.catch( + self.transform(pull, scope) as Effect.Effect, E, R>, + (error) => + f(error).transform( + Effect.suspend(() => { + if (upstreamDone) { + return Cause.done() + } + return upstream + }), + scope + ) + ) + })) + +/** + * Handles failures from this sink by inspecting the full `Cause`. + * + * **When to use** + * + * Use to recover from a sink failure based on the full `Cause` instead of only + * the typed error value. + * + * **Details** + * + * When this sink fails, the handler effect is run and its success value + * becomes the sink result. If the handler fails, the returned sink fails with + * that error. + * + * @see {@link catch_ catch} for recovering from typed errors only + * @see {@link orElse} for recovering by switching to another sink + * + * @category error handling + * @since 4.0.0 + */ +export const catchCause: { + ( + f: (error: Cause.Cause>) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (error: Cause.Cause) => Effect.Effect + ): Sink +} = dual(2, ( + self: Sink, + f: (error: Cause.Cause) => Effect.Effect +): Sink => + transformEffect( + self, + Effect.catchCause((cause) => Effect.map(f(cause), (a2) => [a2 as A | A2] as const)) + )) + +const catch_: { + ( + f: (error: Types.NoInfer) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (error: E) => Effect.Effect + ): Sink +} = dual(2, ( + self: Sink, + f: (error: E) => Effect.Effect +): Sink => + transformEffect( + self, + Effect.catch((error) => Effect.map(f(error), (a2) => [a2 as A | A2] as const)) + )) + +export { + /** + * Handles typed errors from this sink with an effectful fallback value. + * + * **When to use** + * + * Use to recover from a typed sink failure by producing the replacement + * result with an `Effect`. + * + * @see {@link catchCause} for recovering from the full failure cause + * @see {@link orElse} for recovering by switching to another sink + * + * @category error handling + * @since 4.0.0 + */ + catch_ as catch +} + +/** + * Runs an effect after this sink completes, fails, or is interrupted. + * + * **Details** + * + * The effect receives the sink's `Exit` for the result value. The original + * sink result and leftovers are preserved unless the finalizer itself fails. + * + * @category Finalization + * @since 4.0.0 + */ +export const onExit: { + ( + f: (exit: Exit.Exit) => Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + f: (exit: Exit.Exit) => Effect.Effect + ): Sink +} = dual(2, ( + self: Sink, + f: (exit: Exit.Exit) => Effect.Effect +): Sink => + transformEffect( + self, + Effect.onExit((exit) => f(Exit.map(exit, ([a]) => a))) + )) + +/** + * Runs a finalizer effect after this sink completes, fails, or is interrupted. + * + * **Details** + * + * The original sink result and leftovers are preserved unless the finalizer + * itself fails. + * + * @category Finalization + * @since 2.0.0 + */ +export const ensuring: { + ( + effect: Effect.Effect + ): (self: Sink) => Sink + ( + self: Sink, + effect: Effect.Effect + ): Sink +} = dual(2, ( + self: Sink, + effect: Effect.Effect +): Sink => onExit(self, () => effect)) diff --git a/repos/effect/packages/effect/src/Stdio.ts b/repos/effect/packages/effect/src/Stdio.ts new file mode 100644 index 0000000..899c75e --- /dev/null +++ b/repos/effect/packages/effect/src/Stdio.ts @@ -0,0 +1,143 @@ +/** + * Service contract for command-line arguments and standard input, output, and + * error output. It lets programs depend on standard I/O through the Effect + * environment instead of reading from or writing to global process handles + * directly. + * + * The service exposes arguments as an `Effect`, stdout and stderr as `Sink`s + * that accept strings or bytes, and stdin as a byte `Stream`. This module also + * provides a constructor for service values and a small test layer with + * overridable defaults. + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as Layer from "./Layer.ts" +import type { PlatformError } from "./PlatformError.ts" +import * as Sink from "./Sink.ts" +import * as Stream from "./Stream.ts" + +/** + * String literal type used as the unique brand for the `Stdio` service. + * + * **When to use** + * + * Use to type the runtime identifier stored on `Stdio` service implementations. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/Stdio" + +/** + * Runtime identifier stored on `Stdio` service implementations. + * + * **Details** + * + * This marker is part of the runtime representation of `Stdio` service + * implementations. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~effect/Stdio" + +/** + * Defines the service interface for process standard I/O. + * + * **When to use** + * + * Use to depend on command-line arguments and standard I/O through the Effect + * environment. + * + * **Details** + * + * The service provides command-line arguments, sinks for standard output and + * standard error, and a stream of standard input bytes. I/O operations can fail + * with `PlatformError`. + * + * @category models + * @since 4.0.0 + */ +export interface Stdio { + readonly [TypeId]: TypeId + readonly args: Effect.Effect> + stdout(options?: { + readonly endOnDone?: boolean | undefined + }): Sink.Sink + stderr(options?: { + readonly endOnDone?: boolean | undefined + }): Sink.Sink + readonly stdin: Stream.Stream +} +/** + * Service tag for process standard I/O. + * + * **When to use** + * + * Use when you need command-line arguments or standard I/O streams supplied by + * an effect's environment. + * + * @see {@link make} for constructing a `Stdio` service directly + * @see {@link layerTest} for a test layer with defaults and overrides + * + * @category services + * @since 4.0.0 + */ +export const Stdio: Context.Service = Context.Service(TypeId) + +/** + * Creates a `Stdio` service implementation from the provided fields and + * attaches the `Stdio` type identifier. + * + * **When to use** + * + * Use when you need to assemble a concrete `Stdio` service from command-line + * arguments and standard I/O implementations. + * + * **Details** + * + * The returned service reuses the supplied fields unchanged and only adds the + * `Stdio` type identifier; it does not create a `Layer` or provide defaults. + * + * @see {@link layerTest} for a test layer with default fields that can be overridden + * + * @category constructors + * @since 4.0.0 + */ +export const make = (options: Omit): Stdio => ({ + [TypeId]: TypeId, + ...options +}) + +/** + * Creates a test layer for `Stdio`. + * + * **When to use** + * + * Use to provide deterministic standard I/O in tests while overriding only the + * command-line arguments, input stream, or output sinks relevant to the case. + * + * **Details** + * + * Any provided fields override defaults. By default, arguments are empty, + * standard output and error are draining sinks, and standard input is an empty + * stream. + * + * @see {@link make} for constructing a `Stdio` service directly without a `Layer` or defaults + * + * @category layers + * @since 4.0.0 + */ +export const layerTest = (impl: Partial): Layer.Layer => + Layer.succeed( + Stdio, + make({ + args: Effect.succeed([]), + stdout: () => Sink.drain, + stderr: () => Sink.drain, + stdin: Stream.empty, + ...impl + }) + ) diff --git a/repos/effect/packages/effect/src/Stream.ts b/repos/effect/packages/effect/src/Stream.ts new file mode 100644 index 0000000..a292699 --- /dev/null +++ b/repos/effect/packages/effect/src/Stream.ts @@ -0,0 +1,11722 @@ +/** + * Describes effectful sources that emit values over time. + * + * A `Stream` can emit many `A` values, fail with `E`, and require + * services `R` while it is being consumed. Streams are useful for data that is + * pulled in steps, such as values from collections, queues, pubsubs, schedules, + * callbacks, async iterables, or platform streams. The APIs here cover the full + * stream lifecycle: create a stream, transform or combine it, control buffering + * and timing, handle failures, and finally consume it. + * + * @since 2.0.0 + */ +// @effect-diagnostics returnEffectInGen:off +import * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Channel from "./Channel.ts" +import { Clock } from "./Clock.ts" +import * as Context from "./Context.ts" +import * as Duration from "./Duration.ts" +import * as Effect from "./Effect.ts" +import * as Equal from "./Equal.ts" +import * as ExecutionPlan from "./ExecutionPlan.ts" +import * as Exit from "./Exit.ts" +import * as Fiber from "./Fiber.ts" +import type * as Filter from "./Filter.ts" +import type { LazyArg } from "./Function.ts" +import { constant, constTrue, constVoid, dual, identity } from "./Function.ts" +import type { TypeLambda } from "./HKT.ts" +import * as internalExecutionPlan from "./internal/executionPlan.ts" +import * as internal from "./internal/stream.ts" +import { addSpanStackTrace } from "./internal/tracer.ts" +import * as Iterable from "./Iterable.ts" +import * as Latch from "./Latch.ts" +import type * as Layer from "./Layer.ts" +import type { Severity } from "./LogLevel.ts" +import * as MutableHashMap from "./MutableHashMap.ts" +import * as MutableList from "./MutableList.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Predicate, Refinement } from "./Predicate.ts" +import { hasProperty, isNotUndefined, isTagged } from "./Predicate.ts" +import * as PubSub from "./PubSub.ts" +import * as Pull from "./Pull.ts" +import * as Queue from "./Queue.ts" +import * as RcMap from "./RcMap.ts" +import * as RcRef from "./RcRef.ts" +import * as Result from "./Result.ts" +import * as Schedule from "./Schedule.ts" +import * as Scope from "./Scope.ts" +import * as Sink from "./Sink.ts" +import { isString } from "./String.ts" +import type * as Take from "./Take.ts" +import type { ParentSpan, SpanOptions } from "./Tracer.ts" +import type { + Covariant, + ExcludeReason, + ExcludeTag, + ExtractReason, + ExtractTag, + NarrowReason, + NoInfer, + OmitReason, + ReasonTags, + Tags, + TupleOf, + unassigned +} from "./Types.ts" +import type * as Unify from "./Unify.ts" + +/** + * String literal type used as the unique brand for `Stream` values. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/Stream" + +/** + * Runtime identifier stored on `Stream` values and used by `isStream` to + * recognize them. + * + * **Details** + * + * This marker is part of the runtime representation of `Stream` values. Prefer + * `isStream` when narrowing unknown values. + * + * @see {@link isStream} for the public guard that checks this identifier + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~effect/Stream" + +/** + * A `Stream` describes a program that can emit many `A` values, fail + * with `E`, and require `R`. + * + * **Details** + * + * Streams are pull-based with backpressure and emit chunks to amortize effect + * evaluation. They support monadic composition and error handling similar to + * `Effect`, adapted for multiple values. + * + * **Example** (Creating and consuming streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Stream.make(1, 2, 3).pipe( + * Stream.map((n) => n * 2), + * Stream.runForEach((n) => Console.log(n)) + * ) + * }) + * + * Effect.runPromise(program) + * // Output: + * // 2 + * // 4 + * // 6 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Stream extends Variance, Pipeable { + readonly channel: Channel.Channel, E, void, unknown, unknown, unknown, R> + [Unify.typeSymbol]?: unknown + [Unify.unifySymbol]?: StreamUnify + [Unify.ignoreSymbol]?: StreamUnifyIgnore +} + +/** + * Type-level unification hook for Stream within the Effect type system. + * + * @category models + * @since 2.0.0 + */ +export interface StreamUnify extends Effect.EffectUnify { + Stream?: () => A[Unify.typeSymbol] extends Stream | infer _ ? Stream : never +} + +/** + * Type-level marker that excludes Stream from unification. + * + * @category models + * @since 2.0.0 + */ +export interface StreamUnifyIgnore { + Effect?: true +} + +/** + * Type lambda for Stream used in higher-kinded type operations. + * + * **Example** (Using the stream type lambda) + * + * ```ts + * import type { HKT, Stream } from "effect" + * + * // Create a Stream type using the type lambda + * type NumberStream = HKT.Kind + * // Equivalent to: Stream + * ``` + * + * @category type lambdas + * @since 2.0.0 + */ +export interface StreamTypeLambda extends TypeLambda { + readonly type: Stream +} + +/** + * Type-level variance marker for `Stream`. + * + * **Details** + * + * The emitted value `A`, error `E`, and service requirement `R` type + * parameters are covariant. + * + * @category models + * @since 2.0.0 + */ +export interface Variance { + readonly [TypeId]: VarianceStruct +} + +/** + * Structural encoding used by `Variance` to record each `Stream` type + * parameter's variance. + * + * **Details** + * + * `_A`, `_E`, and `_R` are covariant markers. + * + * @category models + * @since 3.4.0 + */ +export interface VarianceStruct { + readonly _A: Covariant + readonly _E: Covariant + readonly _R: Covariant +} + +/** + * Extract the success type from a Stream type. + * + * **Example** (Extracting the success type from a Stream type) + * + * ```ts + * import type { Stream } from "effect" + * + * type NumberStream = Stream.Stream + * type SuccessType = Stream.Success + * // SuccessType is number + * ``` + * + * @category utility types + * @since 3.4.0 + */ +export type Success> = [T] extends [Stream] ? _A : never + +/** + * Extract the error type from a Stream type. + * + * **Example** (Extracting the error type from a Stream type) + * + * ```ts + * import type { Stream } from "effect" + * + * type NumberStream = Stream.Stream + * type ErrorType = Stream.Error + * // ErrorType is string + * ``` + * + * @category utility types + * @since 3.4.0 + */ +export type Error> = [T] extends [Stream] ? _E : never + +/** + * Extract the services type from a Stream type. + * + * **Example** (Extracting the services type from a Stream type) + * + * ```ts + * import type { Stream } from "effect" + * + * interface Database { + * query: (sql: string) => unknown + * } + * type NumberStream = Stream.Stream + * type RequiredServices = Stream.Services + * // RequiredServices is { db: Database } + * ``` + * + * @category utility types + * @since 4.0.0 + */ +export type Services> = [T] extends [Stream] ? _R + : never + +/** + * Checks whether a value is a Stream. + * + * **Example** (Checking whether a value is a Stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3) + * const notStream = { data: [1, 2, 3] } + * + * yield* Console.log(Stream.isStream(stream)) + * // true + * yield* Console.log(Stream.isStream(notStream)) + * // false + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isStream = (u: unknown): u is Stream => hasProperty(u, TypeId) + +/** + * The default chunk size used by Stream constructors and combinators. + * + * **Example** (Reading the default chunk size) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Console.log(Stream.DefaultChunkSize) + * }) + * + * Effect.runPromise(program) + * // Output: 4096 + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const DefaultChunkSize: number = Channel.DefaultChunkSize + +/** + * Describes how merged streams decide when to halt. + * + * @category models + * @since 4.0.0 + */ +export type HaltStrategy = Channel.HaltStrategy + +/** + * Creates a stream from a array-emitting `Channel`. + * + * **Example** (Creating a stream from an array-emitting channel) + * + * ```ts + * import { Channel, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const channel = Channel.succeed([1, 2, 3] as const) + * const stream = Stream.fromChannel(channel) + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromChannel: , E, R>( + channel: Channel.Channel +) => Stream ? A : never, E, R> = internal.fromChannel + +/** + * Creates a stream from an effect. + * + * **Example** (Creating a stream from an effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromEffect(Effect.succeed(42)) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 42 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromEffect = (effect: Effect.Effect): Stream => + fromChannel(Channel.fromEffect(Effect.map(effect, Arr.of))) + +/** + * Accesses a service from the context and emits it as a single element. + * + * **Example** (Accessing a service as a stream) + * + * ```ts + * import { Context, Effect, Stream } from "effect" + * + * class Greeter extends Context.Service string + * }>()("Greeter") {} + * + * const stream = Stream.service(Greeter).pipe( + * Stream.map((greeter) => greeter.greet("World")) + * ) + * + * const program = Effect.gen(function*() { + * return yield* stream.pipe( + * Stream.provideService(Greeter, { + * greet: (name) => `Hello, ${name}!` + * }), + * Stream.runCollect + * ) + * }) + * + * Effect.runPromise(program) + * // Output: [ "Hello, World!" ] + * ``` + * + * @category context + * @since 4.0.0 + */ +export const service = (service: Context.Key): Stream => fromEffect(Effect.service(service)) + +/** + * Optionally accesses a service from the context and emits the result as a + * single element. + * + * **When to use** + * + * Use when you need a stream that emits an optional service from the context + * without requiring that service to be present. + * + * **Example** (Accessing an optional service as a stream) + * + * ```ts + * import { Context, Effect, Option, Stream } from "effect" + * + * class Greeter extends Context.Service string + * }>()("Greeter") {} + * + * const stream = Stream.serviceOption(Greeter).pipe( + * Stream.map((maybeGreeter) => + * Option.match(maybeGreeter, { + * onNone: () => "No greeter", + * onSome: (greeter) => greeter.greet("World") + * }) + * ) + * ) + * + * const program = Effect.gen(function*() { + * return yield* stream.pipe( + * Stream.provideService(Greeter, { + * greet: (name) => `Hello, ${name}!` + * }), + * Stream.runCollect + * ) + * }) + * + * Effect.runPromise(program) + * // Output: [ "Hello, World!" ] + * ``` + * + * @category context + * @since 4.0.0 + */ +export const serviceOption = (service: Context.Key): Stream> => + fromEffect(Effect.serviceOption(service)) + +/** + * Creates a stream that runs the effect and emits no elements. + * + * **Example** (Draining an effect into a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * yield* Stream.fromEffectDrain(Console.log("Draining side effect")).pipe( + * Stream.runDrain + * ) + * }) + * + * Effect.runPromise(program) + * // Output: Draining side effect + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectDrain = (effect: Effect.Effect): Stream => + fromPull(Effect.succeed(Effect.flatMap(effect, () => Cause.done()))) + +/** + * Creates a stream from an effect producing a value of type `A` which repeats forever. + * + * **Example** (Repeating an effect forever) + * + * ```ts + * import { Console, Effect, Random, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromEffectRepeat(Random.nextInt).pipe( + * Stream.take(5) + * ) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3891571149, 4239494205, 2352981603, 2339111046, 1488052210 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectRepeat = (effect: Effect.Effect): Stream, R> => + fromPull(Effect.succeed(Effect.map(effect, Arr.of))) + +/** + * Creates a stream from an effect producing a value of type `A`, which is + * repeated using the specified schedule. + * + * **Example** (Repeating an effect with a schedule) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromEffectSchedule( + * Effect.succeed("ping"), + * Schedule.recurs(2) + * ) + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ "ping", "ping", "ping" ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromEffectSchedule = ( + effect: Effect.Effect, + schedule: Schedule.Schedule +): Stream => + fromPull(Effect.gen(function*() { + const step = yield* Schedule.toStepWithMetadata(schedule) + let s = yield* Effect.provideService(effect, Schedule.CurrentMetadata, Schedule.CurrentMetadata.defaultValue()) + let initial = true + const pull = Effect.suspend(() => step(s as AS)).pipe( + Effect.flatMap((meta) => Effect.provideService(effect, Schedule.CurrentMetadata, meta)), + Effect.map((next) => { + s = next + return Arr.of(next) + }) + ) as Pull.Pull, E | ES, void, R | RS> + return Effect.suspend(() => { + if (initial) { + initial = false + return Effect.succeed(Arr.of(s)) + } + return pull + }) + })) + +/** + * Creates a stream that emits `void` immediately once, then emits another + * `void` after each specified interval. + * + * **Example** (Emitting ticks on an interval) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const ticks = yield* Stream.tick("200 millis").pipe( + * Stream.take(3), + * Stream.runCollect + * ) + * yield* Console.log(ticks) + * }) + * + * Effect.runPromise(program) + * // Output: [ undefined, undefined, undefined ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const tick = (interval: Duration.Input): Stream => + fromPull(Effect.sync(() => { + let first = true + const effect = Effect.succeed(Arr.of(undefined)) + const delayed = Effect.delay(effect, interval) + return Effect.suspend(() => { + if (first) { + first = false + return effect + } + return delayed + }) + })) + +/** + * Creates a stream from a pull effect, such as one produced by `Stream.toPull`. + * + * **Details** + * + * A pull effect yields chunks on demand and completes when the upstream stream ends. + * See `Stream.toPull` for a matching producer. + * + * **Example** (Creating a stream from a pull effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const source = Stream.make(1, 2, 3) + * const pull = yield* Stream.toPull(source) + * const stream = Stream.fromPull(Effect.succeed(pull)) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * ) + * + * Effect.runPromise(program) + * // Output: [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromPull = ( + pull: Effect.Effect, E, void, R>, EX, RX> +): Stream | EX, R | RX> => fromChannel(Channel.fromPull(pull)) + +/** + * Derives a stream by transforming its pull effect. + * + * **Example** (Transforming a pull effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const transformed = Stream.transformPull(stream, (pull) => Effect.succeed(pull)) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(transformed) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const transformPull = ( + self: Stream, + f: (pull: Pull.Pull, E, void>, scope: Scope.Scope) => Effect.Effect< + Pull.Pull, E2, void, R2>, + EX, + RX + > +): Stream, R | R2 | RX> => + fromChannel( + Channel.fromTransform((_, scope) => + Effect.flatMap(Channel.toPullScoped(self.channel, scope), (pull) => f(pull as any, scope)) + ) + ) + +/** + * Transforms a stream by effectfully transforming its pull effect. + * + * **Details** + * + * A forked scope is also provided to the transformation function, which is + * closed once the resulting stream has finished processing. + * + * **Example** (Transforming a stream by effectfully transforming its pull effect) + * + * ```ts + * import { Console, Effect, Scope, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const transformed = Stream.transformPullBracket( + * stream, + * (pull, _scope, forkedScope) => + * Effect.gen(function*() { + * yield* Scope.addFinalizer(forkedScope, Console.log("Releasing scope")) + * return pull + * }) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(transformed) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [1, 2, 3] + * // Releasing scope + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const transformPullBracket = ( + self: Stream, + f: ( + pull: Pull.Pull, E, void, R>, + scope: Scope.Scope, + forkedScope: Scope.Scope + ) => Effect.Effect< + Pull.Pull, E2, void, R2>, + EX, + RX + > +): Stream, R | R2 | RX> => + fromChannel( + Channel.fromTransformBracket((_, scope, forkedScope) => + Effect.flatMap(Channel.toPullScoped(self.channel, scope), (pull) => f(pull, scope, forkedScope)) + ) + ) + +/** + * Creates a channel from a stream. + * + * **Example** (Converting a stream to a channel) + * + * ```ts + * import { Channel, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3) + * const channel = Stream.toChannel(stream) + * const values = yield* Channel.runCollect(channel) + * yield* Console.log(values.flat()) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const toChannel = ( + stream: Stream +): Channel.Channel, E, void, unknown, unknown, unknown, R> => stream.channel + +/** + * Creates a stream from a callback that can emit values into a queue. + * + * **When to use** + * + * Use when you need callback-based code to emit stream values by offering to a + * `Queue`, or signal stream completion through the `Queue` module APIs. + * + * By default it uses an "unbounded" buffer size. + * You can customize the buffer size and strategy by passing an object as the + * second argument with the `bufferSize` and `strategy` fields. + * + * **Example** (Creating a stream from a callback that can emit values into a queue) + * + * ```ts + * import { Console, Effect, Queue, Stream } from "effect" + * + * const stream = Stream.callback((queue) => + * Effect.sync(() => { + * // Emit values to the stream + * Queue.offerUnsafe(queue, 1) + * Queue.offerUnsafe(queue, 2) + * Queue.offerUnsafe(queue, 3) + * // Signal completion + * Queue.endUnsafe(queue) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* stream.pipe(Stream.runCollect) + * yield* Console.log(values) + * // [ 1, 2, 3 ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const callback = ( + f: (queue: Queue.Queue) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + } +): Stream> => fromChannel(Channel.callbackArray(f, options)) + +/** + * Creates an empty stream. + * + * **Example** (Creating an empty stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.empty.pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // [] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty: Stream = fromChannel(Channel.empty) + +/** + * Creates a single-valued pure stream. + * + * **Example** (Creating a single-valued pure stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.succeed(3).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // [ 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const succeed = (value: A): Stream => fromChannel(Channel.succeed(Arr.of(value))) + +/** + * Creates a stream from a sequence of values. + * + * **Example** (Creating a stream from a sequence of values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) // [ 1, 2, 3 ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = >(...values: As): Stream => fromArray(values) + +/** + * Creates a stream that synchronously evaluates a function and emits the result as a single value. + * + * **Details** + * + * The function is evaluated each time the stream is run. + * + * **Example** (Evaluating a value synchronously) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.sync(() => 2 + 1).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sync = (evaluate: LazyArg): Stream => fromChannel(Channel.sync(() => Arr.of(evaluate()))) + +/** + * Creates a lazily constructed stream. + * + * **Details** + * + * The stream factory is evaluated each time the stream is run. + * + * **Example** (Creating a lazily constructed stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.suspend(() => Stream.make(1, 2, 3)).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const suspend = (stream: LazyArg>): Stream => + fromChannel(Channel.suspend(() => stream().channel)) + +/** + * Terminates with the specified error. + * + * **Example** (Failing a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fail("Uh oh!") + * const exit = yield* Effect.exit(Stream.runCollect(stream)) + * yield* Console.log(exit) + * // Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fail = (error: E): Stream => fromChannel(Channel.fail(error)) + +/** + * Terminates with the specified lazily evaluated error. + * + * **Example** (Failing a stream lazily) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.failSync(() => "Uh oh!") + * + * const program = Effect.gen(function*() { + * const exit = yield* Stream.runCollect(stream).pipe(Effect.exit) + * yield* Console.log(exit) + * }) + * + * Effect.runPromise(program) + * // Output: + * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failSync = (evaluate: LazyArg): Stream => fromChannel(Channel.failSync(evaluate)) + +/** + * Creates a stream that fails with the specified `Cause`. + * + * **Example** (Failing with a cause) + * + * ```ts + * import { Cause, Console, Effect, Stream } from "effect" + * + * const stream = Stream.failCause(Cause.fail("Database connection failed")).pipe( + * Stream.catchCause(() => Stream.succeed("recovered")) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * // Output: [ "recovered" ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCause = (cause: Cause.Cause): Stream => fromChannel(Channel.failCause(cause)) + +/** + * The stream that dies with the specified defect. + * + * **Example** (Dying with a defect) + * + * ```ts + * import { Cause, Console, Effect, Exit, Stream } from "effect" + * + * const defect = new Error("Boom") + * const stream = Stream.die(defect) + * + * const program = Effect.gen(function*() { + * const exit = yield* Effect.exit(Stream.runCollect(stream)) + * const message = Exit.match(exit, { + * onSuccess: () => "Exit.Success", + * onFailure: (cause) => { + * const reason = cause.reasons[0] + * const defect = Cause.isDieReason(reason) ? String(reason.defect) : "Unexpected reason" + * return `Exit.Failure(${defect})` + * } + * }) + * yield* Console.log(message) + * }) + * + * Effect.runPromise(program) + * // Output: Exit.Failure(Error: Boom) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const die = (defect: unknown): Stream => fromChannel(Channel.die(defect)) + +/** + * The stream that always fails with the specified lazily evaluated `Cause`. + * + * **Example** (Failing with a lazy cause) + * + * ```ts + * import { Cause, Console, Effect, Stream } from "effect" + * + * const stream = Stream.failCauseSync(() => + * Cause.fail("Connection timeout after retries") + * ) + * + * const program = Effect.gen(function*() { + * const exit = yield* Stream.runCollect(stream).pipe(Effect.exit) + * yield* Console.log(exit) + * }) + * + * Effect.runPromise(program) + * // Output: + * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Connection timeout after retries' } } + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const failCauseSync = (evaluate: LazyArg>): Stream => + fromChannel(Channel.failCauseSync(evaluate)) + +/** + * Creates a stream that consumes values from an iterator. + * + * **Details** + * + * The `maxChunkSize` parameter controls how many values are pulled per chunk. + * + * **Example** (Consuming values from an iterator) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * function* numbers() { + * yield 1 + * yield 2 + * yield 3 + * } + * + * const stream = Stream.fromIteratorSucceed(numbers()) + * + * const program = Effect.gen(function* () { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIteratorSucceed = (iterator: IterableIterator, maxChunkSize?: number): Stream => + fromChannel(Channel.fromIteratorArray(() => iterator, maxChunkSize)) + +/** + * Creates a new `Stream` from an iterable collection of values. + * + * **Details** + * + * - `chunkSize`: Maximum number of values emitted per chunk. + * + * **Example** (Creating a stream from an iterable) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const numbers = [1, 2, 3] + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromIterable(numbers) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = ( + iterable: Iterable, + options?: { + readonly chunkSize?: number | undefined + } +): Stream => + Array.isArray(iterable) && options?.chunkSize === undefined + ? fromArray(iterable) + : fromChannel(Channel.fromIterableArray(iterable, options?.chunkSize)) + +/** + * Creates a stream from an effect producing an iterable of values. + * + * **When to use** + * + * Use when the iterable must be acquired from an Effect before the stream emits, + * and acquisition services or failures should be part of the stream. + * + * **Example** (Creating a stream from an iterable effect) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class UserRepo extends Context.Service> + * }>()("UserRepo") {} + * + * const listUsers = Effect.service(UserRepo).pipe( + * Effect.andThen((repo) => repo.list) + * ) + * + * const stream = Stream.fromIterableEffect(listUsers) + * + * const program = Effect.gen(function*() { + * const users = yield* stream.pipe( + * Stream.provideService(UserRepo, { + * list: Effect.succeed(["user1", "user2"]) + * }), + * Stream.runCollect + * ) + * yield* Console.log(users) + * }) + * + * Effect.runPromise(program) + * // Output: [ "user1", "user2" ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterableEffect = (iterable: Effect.Effect, E, R>): Stream => + unwrap(Effect.map(iterable, fromIterable)) + +/** + * Creates a stream by repeatedly running an effect that yields an iterable of values. + * + * **Example** (Repeating an iterable effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromIterableEffectRepeat(Effect.succeed([1, 2])).pipe( + * Stream.take(5) + * ) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 1, 2, 1 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIterableEffectRepeat = ( + iterable: Effect.Effect, E, R> +): Stream, R> => flatMap(fromEffectRepeat(iterable), fromIterable) + +/** + * Creates a stream from an array of values. + * + * **Example** (Creating a stream from an array of values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromArray([1, 2, 3]) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromArray = (array: ReadonlyArray): Stream => + Arr.isReadonlyArrayNonEmpty(array) ? fromChannel(Channel.succeed(array)) : empty + +/** + * Creates a stream from an effect that produces an array of values. + * + * **When to use** + * + * Use when the array must be acquired from an Effect before the stream emits, + * and acquisition services or failures should be part of the stream. + * + * **Example** (Creating a stream from an effect that produces an array of values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromArrayEffect(Effect.succeed(["Ada", "Grace"])) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "Ada", "Grace" ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromArrayEffect = ( + effect: Effect.Effect, E, R> +): Stream, R> => unwrap(Effect.map(effect, fromArray)) as any + +/** + * Creates a stream from an arbitrary number of arrays. + * + * **Example** (Creating a stream from an arbitrary number of arrays) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromArrays([1, 2], [3, 4]) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromArrays = >>( + ...arrays: Arr +): Stream => fromChannel(Channel.fromArray(Arr.filter(arrays, Arr.isReadonlyArrayNonEmpty))) + +/** + * Creates a stream that pulls values from a `Queue.Dequeue`. + * + * **Details** + * + * The stream emits non-empty batches of queued values and ends when the queue + * fails with `Cause.Done`; other queue failures are propagated. + * + * **Example** (Creating a stream from a queue of values) + * + * ```ts + * import { Console, Effect, Queue, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.unbounded() + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * yield* Queue.offer(queue, 3) + * yield* Queue.shutdown(queue) + * + * const stream = Stream.fromQueue(queue) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromQueue = (queue: Queue.Dequeue): Stream> => + fromChannel(Channel.fromQueueArray(queue)) + +/** + * Creates a stream from a subscription to a `PubSub`. + * + * **Example** (Creating a stream from a subscription to a PubSub) + * + * ```ts + * import { Console, Effect, Fiber, PubSub, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.unbounded() + * + * const fiber = yield* Stream.fromPubSub(pubsub).pipe( + * Stream.take(3), + * Stream.runCollect, + * Effect.forkChild + * ) + * + * yield* PubSub.publish(pubsub, 1) + * yield* PubSub.publish(pubsub, 2) + * yield* PubSub.publish(pubsub, 3) + * + * const values = yield* Fiber.join(fiber) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromPubSub = (pubsub: PubSub.PubSub): Stream => fromChannel(Channel.fromPubSubArray(pubsub)) + +/** + * Creates a stream from a PubSub of `Take` values. + * + * **Details** + * + * `Take` values include end and failure signals. + * + * **Example** (Creating a stream from PubSub takes) + * + * ```ts + * import { Console, Effect, Exit, PubSub, Stream, Take } from "effect" + * + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.unbounded>({ + * replay: 3 + * }) + * + * yield* PubSub.publish(pubsub, [1]) + * yield* PubSub.publish(pubsub, [2]) + * yield* PubSub.publish(pubsub, Exit.succeed(undefined)) + * + * const values = yield* Stream.fromPubSubTake(pubsub).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromPubSubTake = (pubsub: PubSub.PubSub>): Stream => + fromChannel(Channel.fromPubSubTake(pubsub)) + +/** + * Creates a stream from a lazily supplied Web `ReadableStream`. + * + * **Details** + * + * The stream reads from a `ReadableStreamDefaultReader`, maps read failures + * with `onError`, and closes the reader when the stream finalizes. By default + * the reader is canceled; set `releaseLockOnEnd` to release the lock instead. + * + * **Example** (Creating a stream from a ReadableStream) + * + * ```ts + * import { Console, Data, Effect, Stream } from "effect" + * + * class StreamError extends Data.TaggedError("StreamError")<{ readonly cause: unknown }> {} + * + * const readableStream = new ReadableStream({ + * start(controller) { + * controller.enqueue(1) + * controller.enqueue(2) + * controller.enqueue(3) + * controller.close() + * } + * }) + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromReadableStream({ + * evaluate: () => readableStream, + * onError: (cause) => new StreamError({ cause }) + * }) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromReadableStream = ( + options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined + } +): Stream => + fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) { + const reader = options.evaluate().getReader() + yield* Scope.addFinalizer( + scope, + options.releaseLockOnEnd + ? Effect.sync(() => reader.releaseLock()) + : Effect.promise(() => reader.cancel().catch(constVoid)) + ) + return Effect.flatMap( + Effect.tryPromise({ + try: () => reader.read(), + catch: (reason) => options.onError(reason) + }), + ({ done, value }) => done ? Cause.done() : Effect.succeed(Arr.of(value)) + ) + }))) + +/** + * Creates a stream from an AsyncIterable. + * + * **Example** (Creating a stream from an AsyncIterable) + * + * ```ts + * import { Data, Effect, Stream } from "effect" + * + * class StreamError extends Data.TaggedError("StreamError")<{ readonly cause: unknown }> {} + * + * const iterable = (async function*() { + * yield 1 + * yield 2 + * yield 3 + * })() + * + * Effect.runPromise(Effect.gen(function*() { + * const stream = Stream.fromAsyncIterable(iterable, (cause) => new StreamError({ cause })) + * const values = yield* Stream.runCollect(stream) + * yield* Effect.sync(() => console.log(values)) + * })) + * + * // [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromAsyncIterable = ( + iterable: AsyncIterable, + onError: (error: unknown) => E +): Stream => fromChannel(Channel.fromAsyncIterableArray(iterable, onError)) + +/** + * Creates a stream that emits each output of a schedule that does not require input, + * for as long as the schedule continues. + * + * **Example** (Creating a stream from a schedule) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const schedule = Schedule.spaced("50 millis").pipe( + * Schedule.upTo({ times: 3 }) + * ) + * const stream = Stream.fromSchedule(schedule) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 0, 1, 2 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromSchedule = (schedule: Schedule.Schedule): Stream => + fromPull( + Effect.map( + Schedule.toStepWithSleep(schedule), + (step) => Pull.catchDone(Effect.map(step(void 0), Arr.of), () => Cause.done()) + ) + ) + +/** + * Creates a stream from a PubSub subscription. + * + * **When to use** + * + * Use when you already have a `PubSub.Subscription` and want to expose its + * messages as a `Stream`, with `Stream.take` or cancellation controlling how + * many values are consumed. + * + * **Example** (Creating a stream from a PubSub subscription) + * + * ```ts + * import { Console, Effect, PubSub, Stream } from "effect" + * + * const program = Effect.scoped(Effect.gen(function*() { + * const pubsub = yield* PubSub.unbounded() + * const subscription = yield* PubSub.subscribe(pubsub) + * + * yield* PubSub.publish(pubsub, 1) + * yield* PubSub.publish(pubsub, 2) + * + * const stream = Stream.fromSubscription(subscription) + * const values = yield* stream.pipe(Stream.take(2), Stream.runCollect) + * yield* Console.log(values) + * })) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromSubscription = (pubsub: PubSub.Subscription): Stream => + fromChannel(Channel.fromSubscriptionArray(pubsub)) + +/** + * Interface representing an event listener target. + * + * @category models + * @since 3.4.0 + */ +export interface EventListener { + addEventListener( + event: string, + f: (event: A) => void, + options?: { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly signal?: AbortSignal + } | boolean + ): void + removeEventListener( + event: string, + f: (event: A) => void, + options?: { + readonly capture?: boolean + } | boolean + ): void +} + +/** + * Creates a stream from an event listener. + * + * **Example** (Creating a stream from an event listener) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * class NumberTarget implements Stream.EventListener { + * addEventListener(event: string, f: (event: number) => void) { + * if (event === "data") { + * f(1) + * f(2) + * f(3) + * } + * } + * removeEventListener(_event: string, _f: (event: number) => void) {} + * } + * + * Effect.runPromise(Effect.gen(function*() { + * const stream = Stream.fromEventListener(new NumberTarget(), "data").pipe( + * Stream.take(3) + * ) + * const values = yield* Stream.runCollect(stream) + * yield* Effect.sync(() => console.log(values)) + * })) + * + * // [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 3.1.0 + */ +export const fromEventListener = ( + target: EventListener, + type: string, + options?: boolean | { + readonly capture?: boolean + readonly passive?: boolean + readonly once?: boolean + readonly bufferSize?: number | undefined + } | undefined +): Stream => + callback((queue) => { + function emit(event: A) { + Queue.offerUnsafe(queue, event) + } + return Effect.acquireRelease( + Effect.sync(() => target.addEventListener(type, emit, options)), + () => Effect.sync(() => target.removeEventListener(type, emit, options)) + ) + }, { bufferSize: typeof options === "object" ? options.bufferSize : undefined }) + +/** + * Creates a stream by repeatedly applying an effectful step function to a + * state. + * + * **Details** + * + * Each `readonly [value, nextState]` result emits `value` and continues with + * `nextState`; returning `undefined` ends the stream. + * + * **Example** (Unfolding stream state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.unfold(1, (n) => Effect.succeed([n, n + 1] as const)) + * const values = yield* Stream.runCollect(stream.pipe(Stream.take(5))) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4, 5 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unfold = ( + s: S, + f: (s: S) => Effect.Effect +): Stream => + fromPull(Effect.sync(() => { + let state = s + return Effect.flatMap(Effect.suspend(() => f(state)), (next) => { + if (next === undefined) return Cause.done() + state = next[1] + return Effect.succeed(Arr.of(next[0])) + }) + })) + +/** + * Creates a stream by repeatedly evaluating an effectful page function. + * + * **When to use** + * + * Use to consume paginated APIs where each step returns a batch of values + * together with an optional next state. + * + * **Details** + * + * This is similar to {@link unfold}, but each step can emit zero or more values + * and independently decide whether another state should be requested. + * + * **Example** (Paginating stream state) + * + * ```ts + * import { Console, Effect, Option, Stream } from "effect" + * + * const stream = Stream.paginate(0, (n: number) => + * Effect.succeed( + * [ + * [n], + * n < 3 ? Option.some(n + 1) : Option.none() + * ] as const + * )) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // Output: [ 0, 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const paginate = ( + s: S, + f: ( + s: S + ) => Effect.Effect, Option.Option], E, R> +): Stream => + fromPull(Effect.sync(() => { + let state = s + let done = false + return Effect.suspend(function loop(): Pull.Pull, E, void, R> { + if (done) return Cause.done() + return Effect.flatMap(f(state), ([a, s]) => { + if (Option.isNone(s)) { + done = true + } else { + state = s.value + } + if (!Arr.isReadonlyArrayNonEmpty(a)) return loop() + return Effect.succeed(a) + }) + }) + })) + +/** + * Creates an infinite stream by repeatedly applying a function to a seed value. + * + * **Example** (Iterating from a seed value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.iterate(1, (n) => n + 1).pipe(Stream.take(3)) + * + * const program = Effect.gen(function* () { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const iterate = (value: A, next: (value: A) => A): Stream => + unfold(value, (a) => Effect.succeed([a, next(a)])) + +/** + * Constructs a stream from a range of integers, including both endpoints. + * + * **Details** + * + * If the provided `min` is greater than `max`, the stream will not emit any + * values. + * + * **Example** (Creating a numeric range) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.range(1, 5).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4, 5 ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const range = ( + min: number, + max: number, + chunkSize = Channel.DefaultChunkSize +): Stream => + min > max ? empty : fromPull(Effect.sync(() => { + let start = min + let done = false + return Effect.suspend(() => { + if (done) return Cause.done() + const remaining = max - start + 1 + if (remaining > chunkSize) { + const chunk = Arr.range(start, start + chunkSize - 1) + start += chunkSize + return Effect.succeed(chunk) + } + const chunk = Arr.range(start, start + remaining - 1) + done = true + return Effect.succeed(chunk) + }) + })) + +/** + * The stream that never produces any value or fails with any error. + * + * **Example** (Creating a never-ending stream) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * const program = Stream.never.pipe( + * Stream.take(0), + * Stream.runCollect + * ) + * + * Effect.runPromise(program).then(console.log) + * // [] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const never: Stream = fromChannel(Channel.never) + +/** + * Creates a stream produced from an `Effect`. + * + * **Example** (Unwrapping a stream effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const effect = Effect.succeed(Stream.make(1, 2, 3)) + * + * const stream = Stream.unwrap(effect) + * + * const program = Effect.gen(function*() { + * const chunk = yield* Stream.runCollect(stream) + * yield* Console.log(chunk) + * }) + * // [1, 2, 3] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unwrap = ( + effect: Effect.Effect, E, R> +): Stream> => fromChannel(Channel.unwrap(Effect.map(effect, toChannel))) + +/** + * Runs a stream that requires `Scope` in a managed scope, ensuring its + * finalizers are run when the stream completes. + * + * **Example** (Scoping a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.scoped( + * Stream.fromEffect( + * Effect.acquireRelease( + * Console.log("acquire").pipe(Effect.as("resource")), + * () => Console.log("release") + * ) + * ) + * ) + * + * Effect.runPromise(Stream.runCollect(stream)).then(console.log) + * // acquire + * // release + * // [ "resource" ] + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const scoped = ( + self: Stream +): Stream> => fromChannel(Channel.scoped(self.channel)) + +/** + * Transforms the elements of this stream using the supplied function. + * + * **Example** (Mapping stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.fromArray([1, 2, 3]).pipe(Stream.map((n, i) => n + i)) + * const program = Stream.runCollect(stream).pipe( + * Effect.tap((values) => Console.log(values)) + * ) + * + * Effect.runPromise(program) + * // [ 1, 3, 5 ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const map: { + (f: (a: A, i: number) => B): (self: Stream) => Stream + (self: Stream, f: (a: A, i: number) => B): Stream +} = dual(2, (self: Stream, f: (a: A, i: number) => B): Stream => + suspend(() => { + let i = 0 + return fromChannel(Channel.map( + self.channel, + Arr.map((o) => f(o, i++)) + )) + })) + +/** + * Maps both the failure and success channels of a stream. + * + * **Example** (Mapping both the failure and success channels of a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const mapper = { + * onFailure: (error: string) => `error: ${error}`, + * onSuccess: (value: number) => value * 2 + * } + * + * const program = Effect.gen(function*() { + * const success = yield* Stream.make(1, 2).pipe( + * Stream.mapBoth(mapper), + * Stream.runCollect + * ) + * yield* Console.log(success) + * + * const failure = yield* Stream.fail("boom").pipe( + * Stream.mapBoth(mapper), + * Stream.catch((error: string) => Stream.succeed(error)), + * Stream.runCollect + * ) + * yield* Console.log(failure) + * }) + * + * Effect.runPromise(program) + * // Output: [ 2, 4 ] + * // Output: [ "error: boom" ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Stream +} = dual(2, ( + self: Stream, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } +): Stream => + self.pipe( + map(options.onSuccess), + mapError(options.onFailure) + )) + +/** + * Transforms each emitted chunk using the provided function, which receives the chunk and its index. + * + * **Example** (Mapping stream chunks) + * + * ```ts + * import { Array, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.rechunk(2), + * Stream.mapArray((chunk, index) => Array.map(chunk, (n) => n + index)), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 4, 5 ] + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const mapArray: { + ( + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Arr.NonEmptyReadonlyArray + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Arr.NonEmptyReadonlyArray + ): Stream +} = dual(2, ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Arr.NonEmptyReadonlyArray +): Stream => fromChannel(Channel.map(self.channel, f))) + +/** + * Maps over elements of the stream with the specified effectful function. + * + * **When to use** + * + * Use when each stream element transformation needs an Effect, service + * dependency, failure channel, or configured concurrency. + * + * **Example** (Effectfully mapping stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const mappedStream = stream.pipe( + * Stream.mapEffect((n) => + * Effect.gen(function*() { + * yield* Console.log(`Processing: ${n}`) + * return n * 2 + * }) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(mappedStream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: + * // Processing: 1 + * // Processing: 2 + * // Processing: 3 + * // [2, 4, 6] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapEffect: { + ( + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.mapEffect(f, options), + Channel.map(Arr.of), + fromChannel + )) + +/** + * Flattens a stream of `Effect` values into a stream of their results. + * + * **When to use** + * + * Use when stream elements already are effects and their successes should become + * stream elements while their failures enter the stream error channel. + * + * **Example** (Flattening a stream of Effect values into a stream of their results) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream.pipe(Stream.flattenEffect())) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [1, 2, 3] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const flattenEffect: < + Arg extends Stream, any, any> | { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined = { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } +>( + selfOrOptions?: Arg, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined +) => [Arg] extends [Stream, infer _E, infer _R>] ? + Stream<_A, _EX | _E, _RX | _R> + : (self: Stream, E, R>) => Stream = dual( + (args) => isStream(args[0]), + ( + self: Stream, E, R>, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly unordered?: boolean | undefined + } | undefined + ): Stream => mapEffect(self, identity, options) + ) + +/** + * Maps over non-empty array chunks emitted by the stream effectfully. + * + * **When to use** + * + * Use when transformation needs to see and replace each non-empty emitted chunk + * effectfully instead of mapping individual stream elements. + * + * **Example** (Effectfully mapping stream chunks) + * + * ```ts + * import { Array, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.fromArray([1, 2, 3, 4]).pipe( + * Stream.rechunk(2), + * Stream.mapArrayEffect((chunk, index) => + * Effect.succeed(Array.map(chunk, (n) => n + index * 10)) + * ), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [1, 2, 13, 14] + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const mapArrayEffect: { + ( + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Effect.Effect, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Effect.Effect, E2, R2> + ): Stream +} = dual(2, ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray, i: number) => Effect.Effect, E2, R2> +): Stream => fromChannel(Channel.mapEffect(self.channel, f))) + +/** + * Lifts failures and successes into a `Result`, yielding a stream that cannot fail. + * + * **Details** + * + * The stream ends after the first failure, emitting a `Result.fail` value. + * + * **Example** (Converting failures to results) + * + * ```ts + * import { Console, Effect, Result, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const results = yield* Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.result, + * Stream.map(Result.match({ + * onFailure: (error) => `failure: ${error}`, + * onSuccess: (value) => `success: ${value}` + * })), + * Stream.runCollect + * ) + * yield* Console.log(results) + * }) + * + * Effect.runPromise(program) + * // Output: [ "success: 1", "success: 2", "failure: boom" ] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const result = (self: Stream): Stream, never, R> => + self.pipe( + map(Result.succeed), + catch_((e) => succeed(Result.fail(e))) + ) + +/** + * Runs the provided effect for each element while preserving the elements. + * + * **Example** (Tapping stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.fromArray([1, 2, 3]).pipe( + * Stream.tap((n) => Console.log(`before mapping: ${n}`)), + * Stream.map((n) => n * 2), + * Stream.tap((n) => Console.log(`after mapping: ${n}`)), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: + * // before mapping: 1 + * // after mapping: 2 + * // before mapping: 2 + * // after mapping: 4 + * // before mapping: 3 + * // after mapping: 6 + * // [ 2, 4, 6 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tap: { + ( + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } | undefined + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + } | undefined +): Stream => + mapEffect( + self, + (a) => Effect.as(f(a), a), + options + )) + +/** + * Returns a stream that effectfully "peeks" at elements and failures. + * + * **Example** (Tapping values and errors) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.tapBoth({ + * onElement: (value) => Console.log(`seen: ${value}`), + * onError: (error) => Console.log(`error: ${error}`) + * }), + * Stream.catch(() => Stream.make(3)) + * ) + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: + * // seen: 1 + * // seen: 2 + * // error: boom + * // [ 1, 2, 3 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapBoth: { + ( + options: { + readonly onElement: (a: NoInfer) => Effect.Effect + readonly onError: (a: NoInfer) => Effect.Effect + readonly concurrency?: number | "unbounded" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly onElement: (a: NoInfer) => Effect.Effect + readonly onError: (a: NoInfer) => Effect.Effect + readonly concurrency?: number | "unbounded" | undefined + } + ): Stream +} = dual(2, ( + self: Stream, + options: { + readonly onElement: (a: NoInfer) => Effect.Effect + readonly onError: (a: NoInfer) => Effect.Effect + readonly concurrency?: number | "unbounded" | undefined + } +): Stream => + self.pipe( + tapError(options.onError), + tap(options.onElement, { concurrency: options.concurrency }) + )) + +/** + * Runs a sink for all stream elements while still emitting them downstream. + * + * **Example** (Tapping values with a sink) + * + * ```ts + * import { Console, Effect, Ref, Sink, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const seen = yield* Ref.make>([]) + * const sink = Sink.forEach((value: number) => + * Ref.update(seen, (items) => [...items, value]) + * ) + * const result = yield* Stream.make(1, 2, 3).pipe( + * Stream.tapSink(sink), + * Stream.runCollect + * ) + * const tapped = yield* Ref.get(seen) + * yield* Console.log(tapped) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [1, 2, 3] + * // Output: [1, 2, 3] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const tapSink: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = dual( + 2, + ( + self: Stream, + sink: Sink.Sink + ): Stream => + transformPullBracket( + self, + Effect.fnUntraced(function*(pull, _, scope) { + const upstreamLatch = Latch.makeUnsafe() + const sinkLatch = Latch.makeUnsafe() + let chunk: Arr.NonEmptyReadonlyArray | undefined = undefined + let causeSink: Cause.Cause | undefined = undefined + let sinkDone = false + let streamDone = false + + const sinkUpstream = upstreamLatch.whenOpen(Effect.suspend(() => { + if (chunk) { + const arr = chunk! + chunk = undefined + if (!streamDone) upstreamLatch.closeUnsafe() + return Effect.as(sinkLatch.open, arr) + } + return Cause.done() + })) + + yield* Effect.suspend(() => sink.transform(sinkUpstream, scope)).pipe( + (eff) => + Effect.onExitPrimitive(eff, (exit) => { + sinkDone = true + if (Exit.isFailure(exit)) { + causeSink = exit.cause + } + return sinkLatch.open + }, true), + Effect.forkIn(scope) + ) + + const pullAndOffer = pull.pipe( + Effect.flatMap((chunk_) => { + chunk = chunk_ + sinkLatch.closeUnsafe() + upstreamLatch.openUnsafe() + return Effect.as(sinkLatch.await, chunk_) + }), + Pull.catchDone(() => { + streamDone = true + sinkLatch.closeUnsafe() + upstreamLatch.openUnsafe() + return Effect.flatMap(sinkLatch.await, () => Cause.done()) + }) + ) + + return Effect.suspend((): Pull.Pull, E | E2, void, R> => { + if (causeSink) { + return Effect.failCause(causeSink) + } else if (sinkDone) { + return pull + } + return pullAndOffer + }) + }) + ) +) + +/** + * Maps each element to a stream and flattens the resulting streams. + * + * **Details** + * + * With the default sequential concurrency, inner streams are concatenated in + * input order. When `concurrency` is greater than `1` or `"unbounded"`, + * multiple inner streams may run at the same time and their outputs are merged + * as they arrive. + * + * **Example** (Flat mapping stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.flatMap((n) => Stream.make(n, n * 2)), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 2, 4, 3, 6 ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const flatMap: { + ( + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.flatMap((a) => f(a).channel, options), + fromChannel + )) + +/** + * Switches to the latest stream produced by the mapping function, interrupting + * the previous stream when a new element arrives. + * + * **Example** (Switching to the latest stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Stream.make(1, 2, 3).pipe( + * Stream.switchMap((n) => (n === 3 ? Stream.make(n) : Stream.never)), + * Stream.runCollect + * ) + * + * Effect.gen(function*() { + * const result = yield* program + * yield* Console.log(result) + * // Output: [ 3 ] + * }) + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const switchMap: { + ( + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: A) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.switchMap((a) => f(a).channel, options), + fromChannel + )) + +/** + * Flattens a stream of streams into a single stream. + * + * **Details** + * + * With the default sequential concurrency, inner streams are concatenated in + * strict order. When `concurrency` is greater than `1` or `"unbounded"`, + * multiple inner streams may run at the same time and their outputs are merged + * as they arrive. + * + * **Example** (Flattening nested streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const streamOfStreams = Stream.make( + * Stream.make(1, 2), + * Stream.make(3, 4), + * Stream.make(5, 6) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(Stream.flatten(streamOfStreams)) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4, 5, 6 ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const flatten: < + Arg extends Stream, any, any> | { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined = { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } +>( + selfOrOptions?: Arg, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined +) => [Arg] extends [Stream, infer _E2, infer _R2>] ? Stream<_A, _E | _E2, _R | _R2> + : (self: Stream, E2, R2>) => Stream = dual( + (args) => isStream(args[0]), + ( + self: Stream, E2, R2>, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): Stream => flatMap(self, identity, options) + ) + +/** + * Flattens a stream of non-empty arrays into a stream of elements. + * + * **Example** (Flattening a stream of non-empty arrays into a stream of elements) + * + * ```ts + * import { Array, Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(Array.make(1, 2), Array.make(3)) + * + * const program = Effect.gen(function* () { + * const result = yield* Stream.runCollect(Stream.flattenArray(stream)) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const flattenArray = (self: Stream, E, R>): Stream => + fromChannel(Channel.flattenArray(self.channel)) + +/** + * Converts this stream to one that runs its effects but emits no elements. + * + * **Example** (Draining stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.range(1, 6).pipe(Stream.drain, Stream.runCollect) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const drain = (self: Stream): Stream => fromChannel(Channel.drain(self.channel)) + +/** + * Runs the provided stream in the background while this stream runs, interrupting it + * when this stream completes and failing if the background stream fails or defects. + * + * **Example** (Draining a stream in the background) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const foreground = Stream.make(1, 2) + * const background = Stream.fromEffect(Console.log("background task")) + * + * const program = Effect.gen(function*() { + * const values = yield* foreground.pipe( + * Stream.drainFork(background), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: background task + * // Output: [ 1, 2 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const drainFork: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = dual( + 2, + (self: Stream, that: Stream): Stream => + mergeEffect(self, runDrain(that)) +) + +/** + * Repeats the entire stream according to the provided schedule. + * + * **Example** (Repeating a stream on a schedule) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function* () { + * const result = yield* Stream.make(1).pipe( + * Stream.repeat(Schedule.recurs(4)), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 1, 1, 1, 1 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const repeat: { + ( + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule) => Schedule.Schedule + ) => Schedule.Schedule) + ): (self: Stream) => Stream + ( + self: Stream, + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule) => Schedule.Schedule + ) => Schedule.Schedule) + ): Stream +} = dual(2, ( + self: Stream, + schedule: + | Schedule.Schedule + | (( + $: (_: Schedule.Schedule) => Schedule.Schedule + ) => Schedule.Schedule) +): Stream => fromChannel(Channel.repeat(self.channel, schedule))) + +/** + * Schedules the stream's elements according to the provided schedule. + * + * **Example** (Scheduling stream elements) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3).pipe( + * Stream.schedule(Schedule.spaced("10 millis")), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const schedule: { + ( + schedule: Schedule.Schedule, E2, R2> + ): (self: Stream) => Stream + ( + self: Stream, + schedule: Schedule.Schedule, E2, R2> + ): Stream +} = dual(2, ( + self: Stream, + schedule: Schedule.Schedule, E2, R2> +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.schedule(schedule), + Channel.map(Arr.of), + fromChannel + )) + +/** + * Ends the stream if it does not produce a value within the specified duration. + * + * **Example** (Timing out a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1).pipe( + * Stream.concat(Stream.never), + * Stream.timeout("1 second"), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1 ] + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const timeout: { + (duration: Duration.Input): (self: Stream) => Stream + (self: Stream, duration: Duration.Input): Stream +} = dual( + 2, + (self: Stream, duration: Duration.Input): Stream => + timeoutOrElse(self, { + duration, + orElse: () => empty + }) +) + +/** + * Switches to a fallback stream if this stream does not emit a value within + * the specified duration. + * + * **When to use** + * + * Use when a stream should continue with another stream if an upstream pull + * waits longer than the allowed duration. + * + * **Details** + * + * The timeout is checked for each pull. A zero duration uses `orElse` + * immediately, while an infinite duration leaves the original stream + * unchanged. + * + * **Gotchas** + * + * The fallback stream is not timed after the switch. + * + * @see {@link timeout} for ending the stream instead of switching to a fallback stream + * + * @category rate limiting + * @since 4.0.0 + */ +export const timeoutOrElse: { + (options: { + readonly duration: Duration.Input + readonly orElse: () => Stream + }): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly duration: Duration.Input + readonly orElse: () => Stream + } + ): Stream +} = dual( + 2, + ( + self: Stream, + options: { + readonly duration: Duration.Input + readonly orElse: () => Stream + } + ): Stream => { + const duration = Duration.fromInputUnsafe(options.duration) + if (!Duration.isFinite(duration)) return self + if (Duration.isZero(duration)) return suspend(options.orElse) + const timeoutSymbol = Symbol() + return catchCause( + suspend(() => { + const parent = Fiber.getCurrent()! + const clock = parent.getRef(Clock) + const durationMs = Duration.toMillis(duration) + let deadline: number | undefined = undefined + const latch = Latch.makeUnsafe(false) + return merge( + transformPull(self, (pull, _scope) => + Effect.suspend(() => { + deadline = clock.currentTimeMillisUnsafe() + durationMs + latch.openUnsafe() + return pull + }).pipe( + Effect.map((arr) => { + latch.closeUnsafe() + deadline = undefined + return arr + }), + Effect.succeed + )), + fromEffectDrain(Effect.gen(function*() { + while (true) { + yield* latch.await + if (deadline === undefined) continue + yield* Effect.sleep(deadline - clock.currentTimeMillisUnsafe()) + if (deadline === undefined) continue + const remaining = deadline - clock.currentTimeMillisUnsafe() + if (remaining > 0) continue + return yield* Effect.die(timeoutSymbol) + } + })), + { haltStrategy: "left" } + ) + }), + (cause): Stream => { + const isTimeout = cause.reasons.find((r) => r._tag === "Die" && r.defect === timeoutSymbol) + if (isTimeout) return options.orElse() + return failCause(cause as Cause.Cause) + } + ) + } +) + +/** + * Repeats each element of the stream according to the provided schedule, + * including the original emission. + * + * **Example** (Repeating stream elements) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make("A", "B", "C").pipe( + * Stream.repeatElements(Schedule.recurs(1)), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "A", "A", "B", "B", "C", "C" ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const repeatElements: { + ( + schedule: Schedule.Schedule + ): (self: Stream) => Stream + ( + self: Stream, + schedule: Schedule.Schedule + ): Stream +} = dual( + 2, + ( + self: Stream, + schedule: Schedule.Schedule + ): Stream => + fromChannel(Channel.fromTransform((upstream, scope) => + Effect.map( + Channel.toTransform(Channel.flattenArray(self.channel))(upstream, scope), + (pullElement) => { + let pullRepeat: Pull.Pull, E | E2, void, R | R2> | undefined = undefined + + const pull: Pull.Pull< + Arr.NonEmptyReadonlyArray, + E, + void, + R | R2 + > = Effect.gen(function*() { + const element = yield* pullElement + const chunk = Arr.of(element) + const step = yield* Schedule.toStepWithSleep(schedule) + pullRepeat = step(element).pipe( + Effect.as(chunk), + Pull.catchDone((_) => { + pullRepeat = undefined + return pull + }) + ) + return chunk + }) + + return Effect.suspend(() => pullRepeat ?? pull) + } + ) + )) +) + +/** + * Repeats this stream forever. + * + * **Example** (Repeating a stream forever) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make("A", "B").pipe( + * Stream.forever, + * Stream.take(5) + * ) + * + * const program = Effect.gen(function*() { + * const output = yield* Stream.runCollect(stream) + * yield* Console.log(output) + * }) + * + * Effect.runPromise(program) + * // Output: [ "A", "B", "A", "B", "A" ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const forever = (self: Stream): Stream => fromChannel(Channel.forever(self.channel)) + +/** + * Flattens the iterables emitted by this stream into the stream's structure. + * + * **Example** (Flattening iterable values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make([1, 2], [3, 4]).pipe(Stream.flattenIterable) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4 ] + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const flattenIterable = (self: Stream, E, R>): Stream => + flatMap(self, fromIterable) + +/** + * Unwraps `Take` values, emitting elements from non-empty arrays and ending or + * failing when the `Exit` signals completion. + * + * **Example** (Flattening Take values) + * + * ```ts + * import { Array, Console, Effect, Exit, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const takes = Stream.make( + * Array.make(1, 2), + * Array.make(3), + * Exit.succeed(undefined) + * ) + * + * const values = yield* Stream.flattenTake(takes).pipe(Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const flattenTake = (self: Stream, E2, R>): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.flattenTake, + fromChannel + ) + +/** + * Concatenates two streams, emitting all elements from the first stream + * followed by all elements from the second stream. + * + * **Example** (Concatenating streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.concat(Stream.make(1, 2, 3), Stream.make(4, 5, 6)) + * + * Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * // Output: [ 1, 2, 3, 4, 5, 6 ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const concat: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = dual( + 2, + (self: Stream, that: Stream): Stream => + flatten(fromArray>([self, that])) +) + +/** + * Prepends the values from the provided iterable before the stream's elements. + * + * **Example** (Prepending values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(3, 4).pipe( + * Stream.prepend([1, 2]), + * Stream.runCollect + * ) + * + * yield* Console.log(values) + * // Output: [ 1, 2, 3, 4 ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const prepend: { + (values: Iterable): (self: Stream) => Stream + (self: Stream, values: Iterable): Stream +} = dual(2, ( + self: Stream, + values: Iterable +): Stream => concat(fromIterable(values), self)) + +/** + * Merges two streams, emitting elements from both as they arrive. + * + * **Details** + * + * By default, the merged stream ends when both streams end. Use + * `haltStrategy` to change the termination behavior. + * + * **Example** (Merging stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const fast = Stream.make(1, 2, 3) + * const slow = Stream.fromEffect(Effect.delay(Effect.succeed(4), "50 millis")) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(Stream.merge(fast, slow)) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3, 4 ] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const merge: { + ( + that: Stream, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined + ): Stream +} = dual( + (args) => isStream(args[0]) && isStream(args[1]), + ( + self: Stream, + that: Stream, + options?: { + readonly haltStrategy?: HaltStrategy | undefined + } | undefined + ): Stream => fromChannel(Channel.merge(toChannel(self), toChannel(that), options)) +) + +/** + * Merges this stream with a background effect, keeping the stream's elements. + * + * **When to use** + * + * Use when an effect should run concurrently for the lifetime of a stream while + * only the stream's elements remain in the output. + * + * **Details** + * + * The effect runs concurrently, fails the stream if it fails, and is interrupted + * when the stream completes. + * + * **Example** (Merging with a background effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.mergeEffect(Console.log("side task")), + * Stream.runCollect + * ) + * + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: side task + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category merging + * @since 4.0.0 + */ +export const mergeEffect: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = dual( + 2, + (self: Stream, effect: Effect.Effect): Stream => + self.channel.pipe( + Channel.mergeEffect(effect), + fromChannel + ) +) + +/** + * Merges this stream and the specified stream together, tagging values from the + * left stream as `Result.succeed` and values from the right stream as `Result.fail`. + * + * **When to use** + * + * Use when values from both streams should be emitted and downstream code needs + * left values wrapped as successful `Result` values and right values wrapped as + * failed `Result` values. + * + * **Example** (Merging streams into results) + * + * ```ts + * import { Console, Effect, Result, Stream } from "effect" + * + * const left = Stream.fromEffect(Effect.succeed("left")) + * const right = Stream.fromEffect(Effect.delay(Effect.succeed("right"), "10 millis")) + * + * const merged = left.pipe( + * Stream.mergeResult(right), + * Stream.map( + * Result.match({ + * onFailure: (value) => `right:${value}`, + * onSuccess: (value) => `left:${value}` + * }) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(merged) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ "left:left", "right:right" ] + * ``` + * + * @category merging + * @since 4.0.0 + */ +export const mergeResult: { + ( + that: Stream + ): (self: Stream) => Stream, E2 | E, R2 | R> + (self: Stream, that: Stream): Stream, E | E2, R | R2> +} = dual( + 2, + ( + self: Stream, + that: Stream + ): Stream, E | E2, R | R2> => + merge( + map(self, Result.succeed), + map(that, Result.fail) + ) +) + +/** + * Merges two streams while emitting only the values from the left stream. + * + * **When to use** + * + * Use when the right stream is needed for its effects or failures, but downstream + * consumers should only receive values from the left stream. + * + * **Details** + * + * The right stream still runs for its effects, and any failures from the right + * stream are propagated. The merged stream completes when the left stream + * completes, interrupting the right stream. + * + * **Example** (Merging streams while keeping left values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const left = Stream.make(1, 2) + * const right = Stream.make("a", "b") + * const values = yield* left.pipe(Stream.mergeLeft(right), Stream.runCollect) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const mergeLeft: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = dual( + 2, + (left: Stream, right: Stream): Stream => + mergeEffect(left, runDrain(right)) +) + +/** + * Merges this stream and the specified stream together, emitting only the + * values from the right stream while the left stream runs for its effects. + * + * **When to use** + * + * Use when the left stream is needed for its effects or failures, but downstream + * consumers should only receive values from the right stream. + * + * **Details** + * + * The merged stream ends when the right stream completes, interrupting the + * left stream. Failures from the left stream still fail the merged stream. + * + * **Example** (Merging streams while keeping right values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const left = Stream.make("left-1", "left-2").pipe( + * Stream.tap(() => Effect.sync(() => undefined)) + * ) + * const right = Stream.make(1, 2) + * + * const merged = Stream.mergeRight(left, right) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(merged) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const mergeRight: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = dual( + 2, + (left: Stream, right: Stream): Stream => + mergeEffect(right, runDrain(left)) +) + +/** + * Merges a collection of streams, running up to the specified number concurrently. + * + * **When to use** + * + * Use to merge an iterable of already-created streams while bounding how many + * inner streams may run at the same time. + * + * **Details** + * + * The `concurrency` option is required and may be a number or `"unbounded"`. + * `bufferSize` controls buffering between inner streams, and outputs are + * emitted as they arrive under concurrent merging. + * + * **Example** (Merging streams with bounded concurrency) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const streams = [ + * Stream.fromEffect(Effect.delay(Effect.succeed("A"), "20 millis")), + * Stream.fromEffect(Effect.delay(Effect.succeed("B"), "10 millis")) + * ] + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.mergeAll(streams, { concurrency: 2 }).pipe( + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "B", "A" ] + * ``` + * + * @see {@link merge} for merging exactly two streams and choosing a halt strategy + * @see {@link flatten} for flattening a stream that already emits streams + * + * @category merging + * @since 2.0.0 + */ +export const mergeAll: { + ( + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): (streams: Iterable>) => Stream + ( + streams: Iterable>, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } + ): Stream +} = dual(2, ( + streams: Iterable>, + options: { + readonly concurrency: number | "unbounded" + readonly bufferSize?: number | undefined + } +): Stream => flatten(fromIterable(streams), options)) + +/** + * Creates the cartesian product of two streams, running the `right` stream for + * each element in the `left` stream. + * + * **Details** + * + * See also `Stream.zip` for the more common point-wise variant. + * + * **Example** (Computing cartesian products) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const left = Stream.make(1, 2) + * const right = Stream.make("a", "b") + * const values = yield* Stream.runCollect(Stream.cross(left, right)) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, "a" ], [ 1, "b" ], [ 2, "a" ], [ 2, "b" ] ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const cross: { + (right: Stream): (left: Stream) => Stream<[AL, AR], EL | ER, RL | RR> + (left: Stream, right: Stream): Stream<[AL, AR], EL | ER, RL | RR> +} = dual(2, ( + left: Stream, + right: Stream +): Stream<[AL, AR], EL | ER, RL | RR> => crossWith(left, right, (l, r) => [l, r])) + +/** + * Creates a cartesian product of elements from two streams using a function. + * + * **Details** + * + * The `right` stream is rerun for every element in the `left` stream. + * + * See also `Stream.zipWith` for the more common point-wise variant. + * + * **Example** (Combining cartesian products) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const left = Stream.make(1, 2) + * const right = Stream.make("a", "b") + * const combined = Stream.crossWith(left, right, (n, s) => `${n}-${s}`) + * const result = yield* Stream.runCollect(combined) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ "1-a", "1-b", "2-a", "2-b" ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const crossWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = dual(3, ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A +): Stream => flatMap(left, (l) => map(right, (r) => f(l, r)))) + +/** + * Zips two streams point-wise with a combining function, ending when either stream ends. + * + * **Example** (Zipping streams with a function) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream1 = Stream.make(1, 2, 3, 4, 5, 6) + * const stream2 = Stream.make("a", "b", "c") + * + * const zipped = Stream.zipWith(stream1, stream2, (n, s) => `${n}-${s}`) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(zipped) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ "1-a", "2-b", "3-c" ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = dual(3, ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A +): Stream => zipWithArray(left, right, zipArrays(f))) + +const zipArrays = ( + f: (left: AL, right: AR) => A +) => +( + leftArr: Arr.NonEmptyReadonlyArray, + rightArr: Arr.NonEmptyReadonlyArray +) => { + const minLength = Math.min(leftArr.length, rightArr.length) + const result: Arr.NonEmptyArray = [] as any + + for (let i = 0; i < minLength; i++) { + result.push(f(leftArr[i], rightArr[i])) + } + + return [result, leftArr.slice(minLength), rightArr.slice(minLength)] as const +} + +/** + * Zips two streams by applying a function to non-empty arrays of elements. + * + * **Details** + * + * The function returns output plus leftover arrays that carry into the next pull. + * + * **Example** (Zipping stream chunks) + * + * ```ts + * import { Array, Console, Effect, Stream } from "effect" + * + * const left = Stream.fromArrays([1, 2, 3], [4, 5]) + * const right = Stream.fromArrays(["a", "b"], ["c", "d", "e"]) + * + * const zipped = Stream.zipWithArray(left, right, (leftChunk, rightChunk) => { + * const minLength = Math.min(leftChunk.length, rightChunk.length) + * const output = Array.makeBy(minLength, (i) => [leftChunk[i], rightChunk[i]] as const) + * + * return [output, leftChunk.slice(minLength), rightChunk.slice(minLength)] + * }) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(zipped) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"]] + * ``` + * + * @category zipping + * @since 4.0.0 + */ +export const zipWithArray: { + ( + right: Stream, + f: ( + left: Arr.NonEmptyReadonlyArray, + right: Arr.NonEmptyReadonlyArray + ) => readonly [ + output: Arr.NonEmptyReadonlyArray, + leftoverLeft: ReadonlyArray, + leftoverRight: ReadonlyArray + ] + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: ( + left: Arr.NonEmptyReadonlyArray, + right: Arr.NonEmptyReadonlyArray + ) => readonly [ + output: Arr.NonEmptyReadonlyArray, + leftoverLeft: ReadonlyArray, + leftoverRight: ReadonlyArray + ] + ): Stream +} = dual(3, ( + left: Stream, + right: Stream, + f: ( + left: Arr.NonEmptyReadonlyArray, + right: Arr.NonEmptyReadonlyArray + ) => readonly [ + output: Arr.NonEmptyReadonlyArray, + leftoverLeft: ReadonlyArray, + leftoverRight: ReadonlyArray + ] +): Stream => + fromChannel(Channel.fromTransformBracket(Effect.fnUntraced(function*(_, scope) { + const pullLeft = yield* Channel.toPullScoped(left.channel, scope) + const pullRight = yield* Channel.toPullScoped(right.channel, scope) + const pullBoth = Effect.gen(function*() { + const fiberLeft = yield* Effect.forkIn(pullLeft, scope) + const fiberRight = yield* Effect.forkIn(pullRight, scope) + return (yield* Fiber.joinAll([fiberLeft, fiberRight])) as [ + Arr.NonEmptyReadonlyArray, + Arr.NonEmptyReadonlyArray + ] + }) + + type State = + | { _tag: "PullBoth" } + | { _tag: "PullLeft"; rightArray: Arr.NonEmptyReadonlyArray } + | { _tag: "PullRight"; leftArray: Arr.NonEmptyReadonlyArray } + let state: State = { _tag: "PullBoth" } + + const pull: Effect.Effect< + Arr.NonEmptyReadonlyArray, + EL | ER | Cause.Done, + RL | RR + > = Effect.gen(function*() { + const [left, right] = state._tag === "PullBoth" + ? yield* pullBoth + : state._tag === "PullLeft" + ? [yield* pullLeft, state.rightArray] + : [state.leftArray, yield* pullRight] + const result = f(left, right) + if (Arr.isReadonlyArrayNonEmpty(result[1])) { + state = { _tag: "PullRight", leftArray: result[1] } + } else if (Arr.isReadonlyArrayNonEmpty(result[2])) { + state = { _tag: "PullLeft", rightArray: result[2] } + } else { + state = { _tag: "PullBoth" } + } + return result[0] + }) + + return pull + })))) + +/** + * Zips this stream with another point-wise and emits tuples of elements from + * both streams. The new stream ends when either stream ends. + * + * **Example** (Zipping streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream1 = Stream.make(1, 2, 3) + * const stream2 = Stream.make("a", "b", "c") + * + * const zipped = Stream.zip(stream1, stream2) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(zipped) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [[1, "a"], [2, "b"], [3, "c"]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zip: { + (that: Stream): (self: Stream) => Stream<[A, A2], E2 | E, R2 | R> + (self: Stream, that: Stream): Stream<[A, A2], E | E2, R | R2> +} = dual( + 2, + ( + self: Stream, + that: Stream + ): Stream<[A, A2], E | E2, R | R2> => zipWith(self, that, (a, a2) => [a, a2]) +) + +/** + * Zips this stream with another point-wise and keeps only the values from + * the left stream. + * + * **Details** + * + * The resulting stream ends when either side ends. + * + * **Example** (Zipping streams while keeping left values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream1 = Stream.make(1, 2, 3, 4) + * const stream2 = Stream.make("a", "b") + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [1, 2] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipLeft: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = dual( + 2, + ( + left: Stream, + right: Stream + ): Stream => + zipWithArray(left, right, (leftArr, rightArr) => { + const minLength = Math.min(leftArr.length, rightArr.length) + const output = leftArr.slice(0, minLength) as Arr.NonEmptyArray + const leftoverLeft = leftArr.slice(minLength) + const leftoverRight = rightArr.slice(minLength) + + return [output, leftoverLeft, leftoverRight] as const + }) +) + +/** + * Zips this stream with another point-wise, keeping only right values and ending when either stream ends. + * + * **Example** (Zipping streams while keeping right values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream1 = Stream.make(1, 2) + * const stream2 = Stream.make("a", "b", "c", "d") + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: ["a", "b"] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipRight: { + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream +} = dual( + 2, + ( + left: Stream, + right: Stream + ): Stream => + zipWithArray(left, right, (leftArr, rightArr) => { + const minLength = Math.min(leftArr.length, rightArr.length) + const output = rightArr.slice(0, minLength) as Arr.NonEmptyArray + const leftoverLeft = leftArr.slice(minLength) + const leftoverRight = rightArr.slice(minLength) + + return [output, leftoverLeft, leftoverRight] as const + }) +) + +/** + * Zips this stream with another point-wise and emits tuples of elements from + * both streams, flattening the left tuple. + * + * **Details** + * + * The new stream will end when one of the sides ends. + * + * **Example** (Zipping and flattening tuples) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream1 = Stream.make( + * [1, "a"] as const, + * [2, "b"] as const, + * [3, "c"] as const + * ) + * const stream2 = Stream.make("x", "y", "z") + * const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect) + * + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [[1, "a", "x"], [2, "b", "y"], [3, "c", "z"]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipFlatten: { + ( + that: Stream + ): , E, R>(self: Stream) => Stream<[...A, A2], E2 | E, R2 | R> + , E, R, A2, E2, R2>( + self: Stream, + that: Stream + ): Stream<[...A, A2], E | E2, R | R2> +} = dual( + 2, + , E, R, A2, E2, R2>( + self: Stream, + that: Stream + ): Stream<[...A, A2], E | E2, R | R2> => zipWith(self, that, (a, a2) => [...a, a2]) +) + +/** + * Zips this stream together with the index of elements. + * + * **Example** (Zipping elements with indices) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const indexed = yield* Stream.make("a", "b", "c", "d").pipe( + * Stream.zipWithIndex, + * Stream.runCollect + * ) + * yield* Console.log(indexed) + * }) + * + * Effect.runPromise(program) + * // Output: [["a", 0], ["b", 1], ["c", 2], ["d", 3]] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWithIndex = (self: Stream): Stream<[A, number], E, R> => map(self, (a, i) => [a, i]) + +/** + * Zips each element with the next element, pairing the final element with + * `Option.none()`. + * + * **Example** (Zipping elements with next values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.zipWithNext(Stream.make(1, 2, 3, 4)) + * + * Effect.runPromise(Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * })) + * // Output: [ + * // [ 1, { _id: 'Option', _tag: 'Some', value: 2 } ], + * // [ 2, { _id: 'Option', _tag: 'Some', value: 3 } ], + * // [ 3, { _id: 'Option', _tag: 'Some', value: 4 } ], + * // [ 4, { _id: 'Option', _tag: 'None' } ] + * // ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWithNext = (self: Stream): Stream<[A, Option.Option], E, R> => + mapAccumArray(self, Option.none, (acc, arr) => { + let i = 0 + if (acc._tag === "None") { + i = 1 + acc = Option.some(arr[0]) as Option.Some + } + const pairs = Arr.empty<[A, Option.Option]>() + for (; i < arr.length; i++) { + const value = acc.value + acc = Option.some(arr[i]) as Option.Some + pairs.push([value, acc]) + } + return [acc, pairs] + }, { + onHalt(state) { + return state._tag === "Some" ? [[state.value, Option.none()]] : [] + } + }) + +/** + * Zips each element with its previous element, starting with `None`. + * + * **Example** (Zipping elements with previous values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.zipWithPrevious(Stream.make(1, 2, 3, 4)) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ + * // [ { _id: 'Option', _tag: 'None' }, 1 ], + * // [ { _id: 'Option', _tag: 'Some', value: 1 }, 2 ], + * // [ { _id: 'Option', _tag: 'Some', value: 2 }, 3 ], + * // [ { _id: 'Option', _tag: 'Some', value: 3 }, 4 ] + * // ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWithPrevious = (self: Stream): Stream<[Option.Option, A], E, R> => + mapAccumArray(self, Option.none, (acc, arr) => { + const pairs = Arr.empty<[Option.Option, A]>() + for (let i = 0; i < arr.length; i++) { + const value = arr[i] + pairs.push([acc, value]) + acc = Option.some(arr[i]) + } + return [acc, pairs] + }) + +/** + * Zips each element with its previous and next values. + * + * **Example** (Zipping elements with neighbors) + * + * ```ts + * import { Console, Effect, Option, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.zipWithPreviousAndNext, + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ [Option.none(), 1, Option.some(2)], [Option.some(1), 2, Option.some(3)], [Option.some(2), 3, Option.none()] ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipWithPreviousAndNext = ( + self: Stream +): Stream<[Option.Option, A, Option.Option], E, R> => + mapAccumArray(self, () => ({ + prev: Option.none(), + current: Option.none() + }), (acc, arr) => { + let i = 0 + let current: A + if (acc.current._tag === "None") { + i = 1 + current = arr[0] + acc.current = Option.some(current) + } else { + current = acc.current.value + } + const pairs = Arr.empty<[Option.Option, A, Option.Option]>() + for (; i < arr.length; i++) { + const element = arr[i] + acc.current = Option.some(element) as Option.Some + pairs.push([acc.prev, current, acc.current]) + acc.prev = Option.some(current) + current = element + } + return [acc, pairs] + }, { + onHalt(acc) { + return acc.current._tag === "Some" ? [[acc.prev, acc.current.value, Option.none()]] : [] + } + }) + +/** + * Zips multiple streams so that when a value is emitted by any stream, it is + * combined with the latest values from the other streams to produce a result. + * + * **When to use** + * + * Use when each stream should contribute its latest value after all streams have + * emitted at least once. + * + * **Gotchas** + * + * Note: tracking the latest value is done on a per-array basis. That means + * that emitted elements that are not the last value in arrays will never be + * used for zipping. + * + * **Example** (Zipping latest values from many streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.zipLatestAll( + * Stream.make(1, 2, 3).pipe(Stream.rechunk(1)), + * Stream.make("a", "b", "c").pipe(Stream.rechunk(1)), + * Stream.make(true, false, true).pipe(Stream.rechunk(1)) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, "a", true ], [ 2, "a", true ], [ 3, "a", true ], [ 3, "b", true ], [ 3, "c", true ], [ 3, "c", false ], [ 3, "c", true ] ] + * ``` + * + * @category zipping + * @since 3.3.0 + */ +export const zipLatestAll = >>( + ...streams: T +): Stream< + [T[number]] extends [never] ? never + : { [K in keyof T]: T[K] extends Stream ? A : never }, + [T[number]] extends [never] ? never : T[number] extends Stream ? _E : never, + [T[number]] extends [never] ? never : T[number] extends Stream ? _R : never +> => + fromChannel(Channel.suspend(() => { + const latest: Array = [] + const emitted = new Set() + const readyLatch = Latch.makeUnsafe() + return Channel.mergeAll( + Channel.fromArray( + streams.map((s, i) => + s.channel.pipe( + Channel.flattenArray, + Channel.mapEffect((a) => { + latest[i] = a + if (!emitted.has(i)) { + emitted.add(i) + if (emitted.size < streams.length) { + return readyLatch.await as Effect.Effect + } + return Effect.as(readyLatch.open, Arr.of(latest.slice())) + } + return Effect.succeed(Arr.of(latest.slice())) + }), + Channel.filter(isNotUndefined) + ) + ) + ), + { + concurrency: "unbounded", + bufferSize: 0 + } + ) + })) as any + +/** + * Combines two streams by emitting each new element with the latest value from the other stream. + * + * **When to use** + * + * Use when two streams should start emitting combined pairs after both have + * produced at least one value. + * + * **Gotchas** + * + * Note: tracking the latest value is done on a per-array basis. That means + * that emitted elements that are not the last value in arrays will never be + * used for zipping. + * + * **Example** (Zipping latest values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.zipLatest( + * Stream.make(1), + * Stream.make("a") + * ).pipe(Stream.runCollect) + * + * yield* Console.log(result) + * }) + * // Output: [ [1, "a"] ] + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipLatest: { + ( + right: Stream + ): (left: Stream) => Stream<[AL, AR], EL | ER, RL | RR> + ( + left: Stream, + right: Stream + ): Stream<[AL, AR], EL | ER, RL | RR> +} = dual( + 2, + ( + left: Stream, + right: Stream + ): Stream<[AL, AR], EL | ER, RL | RR> => zipLatestAll(left, right) +) + +/** + * Combines the latest values from both streams whenever either emits, using + * the provided function. + * + * **When to use** + * + * Use when two streams should start emitting custom combined values after both + * have produced at least one value. + * + * **Gotchas** + * + * Note: tracking the latest value is done on a per-array basis. That means + * that emitted elements that are not the last value in arrays will never be + * used for zipping. + * + * **Example** (Zipping latest values with a function) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3).pipe( + * Stream.rechunk(1), + * Stream.zipLatestWith( + * Stream.make(10, 20).pipe(Stream.rechunk(1)), + * (n, m) => n + m + * ), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * // Output: [ 11, 12, 22, 23 ] + * }) + * ``` + * + * @category zipping + * @since 2.0.0 + */ +export const zipLatestWith: { + ( + right: Stream, + f: (left: AL, right: AR) => A + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream +} = dual( + 3, + ( + left: Stream, + right: Stream, + f: (left: AL, right: AR) => A + ): Stream => map(zipLatestAll(left, right), ([a, a2]) => f(a, a2)) +) + +/** + * Runs all streams concurrently until one stream emits its first value, then + * mirrors that winning stream and interrupts the rest. + * + * **Details** + * + * Failures or completion from losing streams before a winner is chosen are + * ignored unless every stream fails or completes before emitting. After a + * winner is chosen, that stream's later failures are propagated. + * + * **Example** (Racing multiple streams) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.raceAll( + * Stream.fromSchedule(Schedule.spaced("1 second")), + * Stream.make(0, 1, 2) + * ).pipe(Stream.runCollect) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 0, 1, 2 ] + * ``` + * + * @category racing + * @since 3.5.0 + */ +export const raceAll = >>( + ...streams: S +): Stream, Error, Services> => + fromChannel(Channel.fromTransform((_, scope) => + Effect.sync(() => { + let winner: + | Pull.Pull>, Error, void, Services> + | undefined + const race = Effect.raceAll(streams.map((stream) => { + const childScope = Scope.forkUnsafe(scope) + return Channel.toPullScoped(stream.channel, childScope).pipe( + Effect.flatMap((pull) => Effect.zip(Effect.succeed(pull), pull)), + Effect.onExit((exit) => { + if (exit._tag === "Success") { + if (winner) { + return Scope.close(childScope, exit) + } + winner = exit.value[0] + return Effect.void + } + return Scope.close(childScope, exit) + }), + Effect.map(([, chunk]) => chunk) + ) + })) + return Effect.suspend(() => winner ?? race) + }) + )) + +/** + * Runs both streams concurrently until one stream emits its first value, then + * mirrors that winning stream and interrupts the other. + * + * **Details** + * + * A failure or completion from one side before the other side emits does not + * win the race unless both sides fail or complete before emitting. After a + * winner is chosen, that stream's later failures are propagated. + * + * **Example** (Racing two streams) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.race( + * Stream.make(0, 1, 2), + * Stream.fromSchedule(Schedule.spaced("1 second")) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 0, 1, 2 ] + * ``` + * + * @category racing + * @since 3.7.0 + */ +export const race: { + ( + right: Stream + ): (left: Stream) => Stream + ( + left: Stream, + right: Stream + ): Stream +} = dual(2, ( + left: Stream, + right: Stream +): Stream => raceAll(left, right)) + +/** + * Filters a stream to the elements that satisfy a predicate. + * + * **Example** (Filtering stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3, 4).pipe( + * Stream.filter((n) => n % 2 === 0) + * ) + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 2, 4 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (refinement: Refinement, B>): (self: Stream) => Stream + (predicate: Predicate>): (self: Stream) => Stream + (self: Stream, refinement: Refinement): Stream + ( + self: Stream, + predicate: Predicate + ): Stream +} = dual( + 2, + ( + self: Stream, + predicate: Predicate + ): Stream => fromChannel(Channel.filterArray(toChannel(self), predicate)) +) + +/** + * Filters and maps stream elements in one pass using a `Filter`. + * + * **When to use** + * + * Use to keep only stream elements accepted by a `Filter` and emit each filter + * success value. + * + * **Details** + * + * `Result.succeed` values are emitted and `Result.fail` values are skipped. + * + * @see {@link filter} for keeping original elements with a boolean predicate or refinement + * @see {@link filterMapEffect} for an effectful `Filter` + * @see {@link partition} for consuming both filter success and failure values + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + ( + filter: Filter.Filter, B, X> + ): (self: Stream) => Stream + ( + self: Stream, + filter: Filter.Filter + ): Stream +} = dual( + 2, + ( + self: Stream, + filter: Filter.Filter + ): Stream => fromChannel(Channel.filterMapArray(toChannel(self), filter)) +) + +/** + * Filters elements in a single pass effectfully. + * + * **Example** (Effectfully filtering stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4).pipe(Stream.filterEffect((n) => Effect.succeed(n > 2))) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 4 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterEffect: { + ( + predicate: (a: NoInfer, i: number) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: NoInfer, i: number) => Effect.Effect + ): Stream +} = dual( + 2, + ( + self: Stream, + predicate: (a: NoInfer, i: number) => Effect.Effect + ): Stream => fromChannel(Channel.filterArrayEffect(toChannel(self), predicate)) +) + +/** + * Filters and maps elements in one pass effectfully using a `FilterEffect`. + * + * **When to use** + * + * Use to apply effectful logic that can reject stream elements or emit + * transformed values before they continue downstream. + * + * **Details** + * + * `Result.succeed` values are emitted, `Result.fail` values are skipped, and + * effect failures fail the stream. + * + * @see {@link filterMap} for the synchronous `Filter` variant + * @see {@link filterEffect} for effectfully keeping original elements + * @see {@link mapEffect} for effectfully transforming every element + * + * @category filtering + * @since 2.0.0 + */ +export const filterMapEffect: { + ( + filter: Filter.FilterEffect, B, X, EX, RX> + ): (self: Stream) => Stream + ( + self: Stream, + filter: Filter.FilterEffect + ): Stream +} = dual( + 2, + ( + self: Stream, + filter: Filter.FilterEffect + ): Stream => fromChannel(Channel.filterMapArrayEffect(toChannel(self), filter)) +) + +/** + * Partitions a stream using a `Filter` and exposes passing and failing values + * as scoped queues. + * + * **Details** + * + * The queues are backed by a fiber in the current scope and should be consumed + * while that scope remains open. Each queue fails with the stream error or + * `Cause.Done` when the source ends. + * + * **Example** (Partitioning a stream into queues) + * + * ```ts + * import { Console, Effect, Result, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const [passes, fails] = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.partitionQueue((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)) + * ) + * + * const passValues = yield* Stream.fromQueue(passes).pipe(Stream.runCollect) + * const failValues = yield* Stream.fromQueue(fails).pipe(Stream.runCollect) + * + * yield* Console.log(passValues) + * // Output: [ 2, 4 ] + * yield* Console.log(failValues) + * // Output: [ 1, 3 ] + * }) + * + * Effect.runPromise(Effect.scoped(program)) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const partitionQueue: { + (filter: Filter.Filter, Pass, Fail>, options?: { + readonly capacity?: number | "unbounded" | undefined + }): (self: Stream) => Effect.Effect< + [ + passes: Queue.Dequeue, + fails: Queue.Dequeue + ], + never, + R | Scope.Scope + > + ( + self: Stream, + filter: Filter.Filter, Pass, Fail>, + options?: { + readonly capacity?: number | "unbounded" | undefined + } + ): Effect.Effect< + [ + passes: Queue.Dequeue, + fails: Queue.Dequeue + ], + never, + R | Scope.Scope + > +} = dual( + (args) => isStream(args[0]), + Effect.fnUntraced( + function*( + self: Stream, + filter: Filter.Filter, Pass, Fail>, + options?: { + readonly capacity?: number | "unbounded" | undefined + } + ): Effect.fn.Return< + [ + passes: Queue.Dequeue, + fails: Queue.Dequeue + ], + never, + R | Scope.Scope + > { + const scope = yield* Effect.scope + const pull = yield* Channel.toPullScoped(self.channel, scope) + const capacity = options?.capacity === "unbounded" ? undefined : options?.capacity ?? DefaultChunkSize + const passes = yield* Queue.make({ capacity }) + const fails = yield* Queue.make({ capacity }) + + yield* Effect.gen(function*() { + while (true) { + const chunk = yield* pull + const excluded: Array = [] + const satisfying: Array = [] + for (let i = 0; i < chunk.length; i++) { + const result = filter(chunk[i] as NoInfer) + if (Result.isFailure(result)) { + excluded.push(result.failure) + } else { + satisfying.push(result.success) + } + } + let passFiber: Fiber.Fiber | undefined = undefined + if (satisfying.length > 0) { + const leftover = Queue.offerAllUnsafe(passes, satisfying) + if (leftover.length > 0) { + passFiber = yield* Effect.forkChild(Queue.offerAll(passes, leftover)) + } + } + if (excluded.length > 0) { + const leftover = Queue.offerAllUnsafe(fails, excluded) + if (leftover.length > 0) { + yield* Queue.offerAll(fails, leftover) + } + } + if (passFiber) yield* Fiber.join(passFiber) + } + }).pipe( + Effect.onError((cause) => { + Queue.failCauseUnsafe(passes, cause) + Queue.failCauseUnsafe(fails, cause) + return Effect.void + }), + Effect.forkIn(scope) + ) + + return [passes, fails] + } + ) +) + +/** + * Splits a stream with an effectful `Filter`, returning scoped streams for + * filter successes and failures. + * + * **When to use** + * + * Use when you need to classify each stream element with an effectful `Filter` + * and consume both passing and failing mapped values as streams. + * + * **Details** + * + * The returned streams are backed by queues in the current scope and should be + * consumed while that scope remains open. The first stream emits success values + * from the filter, and the second emits failure values. + * + * @see {@link partition} for the pure `Filter` variant, which returns the failing stream before the passing stream + * @see {@link partitionQueue} for the lower-level queue result + * @see {@link filterMapEffect} for effectful filtering that discards failed filter results + * + * @category filtering + * @since 4.0.0 + */ +export const partitionEffect: { + (filter: Filter.FilterEffect, Pass, Fail, EX, RX>, options?: { + readonly capacity?: number | "unbounded" | undefined + readonly concurrency?: number | "unbounded" | undefined + }): (self: Stream) => Effect.Effect< + [ + passes: Stream, + fails: Stream + ], + never, + R | RX | Scope.Scope + > + ( + self: Stream, + filter: Filter.FilterEffect, Pass, Fail, EX, RX>, + options?: { + readonly capacity?: number | "unbounded" | undefined + readonly concurrency?: number | "unbounded" | undefined + } + ): Effect.Effect< + [ + passes: Stream, + fails: Stream + ], + never, + R | RX | Scope.Scope + > +} = dual( + (args) => isStream(args[0]), + ( + self: Stream, + filter: Filter.FilterEffect, Pass, Fail, EX, RX>, + options?: { + readonly capacity?: number | "unbounded" | undefined + readonly concurrency?: number | "unbounded" | undefined + } + ): Effect.Effect< + [ + passes: Stream, + fails: Stream + ], + never, + R | RX | Scope.Scope + > => + Effect.map( + partitionQueue, E | EX, R | RX, Pass, Fail>( + mapEffect(self, (a) => filter(a as NoInfer), options), + (result) => result, + options + ), + ([passes, fails]) => [fromQueue(passes), fromQueue(fails)] as const + ) +) + +/** + * Splits a stream into scoped excluded and satisfying substreams using a + * `Filter`. + * + * **Details** + * + * The returned streams are backed by queues in the current scope and should be + * consumed while that scope remains open. The faster stream may advance up to + * `bufferSize` elements ahead of the slower one. + * + * **Example** (Partitioning a stream) + * + * ```ts + * import { Console, Effect, Result, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const [excluded, satisfying] = yield* Stream.partition( + * Stream.make(1, 2, 3, 4), + * (n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n) + * ) + * const left = yield* Stream.runCollect(excluded) + * const right = yield* Stream.runCollect(satisfying) + * yield* Console.log(left) + * // Output: [ 1, 3 ] + * yield* Console.log(right) + * // Output: [ 2, 4 ] + * }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const partition: { + ( + filter: Filter.Filter, Pass, Fail>, + options?: { readonly bufferSize?: number | undefined } + ): ( + self: Stream + ) => Effect.Effect< + [excluded: Stream, satisfying: Stream], + never, + R | Scope.Scope + > + ( + self: Stream, + filter: Filter.Filter, Pass, Fail>, + options?: { readonly bufferSize?: number | undefined } + ): Effect.Effect< + [excluded: Stream, satisfying: Stream], + never, + R | Scope.Scope + > +} = dual( + (args) => isStream(args[0]), + ( + self: Stream, + filter: Filter.Filter, Pass, Fail>, + options?: { readonly bufferSize?: number | undefined } + ): Effect.Effect< + [excluded: Stream, satisfying: Stream], + never, + R | Scope.Scope + > => + Effect.map( + partitionQueue(self, filter, { capacity: options?.bufferSize ?? 16 }), + ([passes, fails]) => [fromQueue(fails), fromQueue(passes)] as const + ) +) + +/** + * Returns the specified stream if the given condition is satisfied, otherwise + * returns an empty stream. + * + * **Example** (Conditionally keeping a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect( + * Stream.when(Stream.make(1, 2, 3), Effect.succeed(false)) + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const when: { + ( + test: Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + test: Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + test: Effect.Effect +): Stream => + test.pipe( + Effect.map((pass) => pass ? self : empty), + unwrap + )) + +/** + * Runs a sink to peel off enough elements to produce a value and returns that + * value with the remaining stream in a scope. + * + * **Details** + * + * The returned stream is only valid within the scope. + * + * **Example** (Peeling a stream with a sink) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * const stream = Stream.fromArrays([1, 2, 3], [4, 5, 6]) + * const sink = Sink.take(3) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const [peeled, rest] = yield* Stream.peel(stream, sink) + * const remaining = yield* Stream.runCollect(rest) + * yield* Console.log([peeled, remaining]) + * }) + * ) + * + * Effect.runPromise(program) + * // Output: [ [1, 2, 3], [4, 5, 6] ] + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const peel: { + ( + sink: Sink.Sink + ): (self: Stream) => Effect.Effect<[A2, Stream], E2 | E, Scope.Scope | R2 | R> + ( + self: Stream, + sink: Sink.Sink + ): Effect.Effect<[A2, Stream], E | E2, Scope.Scope | R | R2> +} = dual( + 2, + Effect.fnUntraced(function*( + self: Stream, + sink: Sink.Sink + ): Effect.fn.Return<[A2, Stream], E | E2, Scope.Scope | R | R2> { + let cause: Cause.Cause> | undefined = undefined + const originalPull = yield* Channel.toPull(self.channel) + const pull: Pull.Pull< + Arr.NonEmptyReadonlyArray, + E + > = Effect.catchCause(originalPull, (cause_) => { + cause = cause_ + return Effect.failCause(cause_) + }) + + let stream = fromPull(Effect.succeed(pull)) as Stream + const leftover = yield* run(stream, sink) + if (cause) return [leftover, empty] + + stream = fromPull(Effect.succeed(originalPull)) + return [leftover, stream] + }) +) + +/** + * Buffers up to `capacity` elements so a faster producer can progress + * independently of a slower consumer. + * + * **Details** + * + * Finite buffers use the configured queue strategy: `"suspend"` applies + * backpressure, while `"dropping"` and `"sliding"` may discard elements when + * the buffer is full. This combinator destroys chunking; use `Stream.rechunk` + * afterward if you need fixed chunk sizes. + * + * **Example** (Buffering stream elements) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.buffer({ capacity: 1 }), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const buffer: { + ( + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Stream +} = dual(2, ( + self: Stream, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } +): Stream => fromChannel(Channel.bufferArray(self.channel, options))) + +/** + * Allows a faster producer to progress independently of a slower consumer by + * buffering up to `capacity` chunks in a queue. + * + * **Details** + * + * Finite buffers use the configured queue strategy: `"suspend"` applies + * backpressure, while `"dropping"` and `"sliding"` may discard chunks when the + * buffer is full. This combinator preserves chunking and is best with + * power-of-2 capacities. + * + * **Example** (Buffering stream chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.fromArrays([1, 2], [3, 4]).pipe( + * Stream.bufferArray({ capacity: 2 }), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * // Output: [ 1, 2, 3, 4 ] + * ``` + * + * @category rate limiting + * @since 4.0.0 + */ +export const bufferArray: { + ( + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Stream +} = dual(2, ( + self: Stream, + options: { readonly capacity: "unbounded" } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } +): Stream => fromChannel(Channel.buffer(self.channel, options))) + +/** + * Switches over to the stream produced by the provided function in case this + * one fails. Allows recovery from all causes of failure, including + * interruption if the stream is uninterruptible. + * + * **Example** (Catching stream causes) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("Oops!")), + * Stream.concat(Stream.make(3, 4)) + * ) + * + * const recovered = stream.pipe( + * Stream.catchCause(() => Stream.make(999)) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(recovered) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 999 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchCause: { + ( + f: (cause: Cause.Cause) => Stream + ): (self: Stream) => Stream + ( + self: Stream, + f: (cause: Cause.Cause) => Stream + ): Stream +} = dual(2, ( + self: Stream, + f: (cause: Cause.Cause) => Stream +): Stream => + self.channel.pipe( + Channel.catchCause((cause) => f(cause).channel), + fromChannel + )) + +/** + * Runs an effect when the stream fails without changing its values or error, + * unless the tap effect itself fails. + * + * **Example** (Tapping stream causes) + * + * ```ts + * import { Cause, Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.tapCause((cause) => Console.log(Cause.isReason(cause))), + * Stream.catch(() => Stream.succeed(0)) + * ) + * + * const program = Effect.gen(function* () { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: true + * // Output: [ 1, 2, 0 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const tapCause: { + ( + f: (cause: Cause.Cause) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + f: (cause: Cause.Cause) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + f: (cause: Cause.Cause) => Effect.Effect +): Stream => + self.channel.pipe( + Channel.tapCause(f), + fromChannel + )) + +const catch_: { + ( + f: (error: E) => Stream + ): (self: Stream) => Stream + ( + self: Stream, + f: (error: E) => Stream + ): Stream +} = dual(2, ( + self: Stream, + f: (error: E) => Stream +): Stream => fromChannel(Channel.catch(self.channel, (error) => f(error).channel))) + +export { + /** + * Switches over to the stream produced by the provided function if this one fails. + * + * **Example** (Catching stream failures) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("Oops!")), + * Stream.catch(() => Stream.make(999)) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 999 ] + * ``` + * + * @category error handling + * @since 4.0.0 + */ + catch_ as catch +} + +/** + * Peeks at errors effectfully without changing the stream unless the tap fails. + * + * **Example** (Effectfully peeking at errors) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.tapError((error) => Console.log(`tapError: ${error}`)), + * Stream.catch(() => Stream.make(999)) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: + * // tapError: boom + * // [ 1, 2, 999 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const tapError: { + ( + f: (error: E) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + f: (error: E) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + f: (error: E) => Effect.Effect +): Stream => + self.channel.pipe( + Channel.tapError(f), + fromChannel + )) + +/** + * Recovers from errors that match a predicate by switching to a recovery stream. + * + * **Details** + * + * When a failure matches the filter, the stream switches to the recovery + * stream. Non-matching failures propagate downstream, so the error type is + * preserved unless the filter narrows it. + * + * **Example** (Catching matching failures) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.concat(Stream.fail(42)), + * Stream.catchIf( + * (error): error is 42 => error === 42, + * () => Stream.make(999) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * // Output: [ 1, 2, 999 ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchIf: { + ( + refinement: Refinement, EB>, + f: (e: EB) => Stream, + orElse?: ((e: Exclude) => Stream) | undefined + ): ( + self: Stream + ) => Stream, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> + ( + predicate: Predicate>, + f: (e: NoInfer) => Stream, + orElse?: ((e: NoInfer) => Stream) | undefined + ): ( + self: Stream + ) => Stream, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> + ( + self: Stream, + refinement: Refinement, + f: (e: EB) => Stream, + orElse?: ((e: Exclude) => Stream) | undefined + ): Stream, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> + ( + self: Stream, + predicate: Predicate, + f: (e: E) => Stream, + orElse?: ((e: E) => Stream) | undefined + ): Stream, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> +} = dual((args) => isStream(args[0]), < + A, + E, + R, + A2, + E2, + R2, + A3 = never, + E3 = E, + R3 = never +>( + self: Stream, + predicate: Predicate, + f: (failure: E) => Stream, + orElse?: ((failure: E) => Stream) | undefined +): Stream => + fromChannel( + Channel.catchIf( + toChannel(self), + predicate, + (e) => f(e).channel, + orElse && ((e) => orElse(e).channel) + ) + )) + +/** + * Recovers from errors that match a `Filter` by switching to a recovery + * stream. + * + * **When to use** + * + * Use to recover from stream errors with a reusable `Filter` when matching can + * also narrow or transform the error before choosing the recovery stream. + * + * **Details** + * + * Successful filter results are passed to `f`. Failed filter results go to + * `orElse` when provided; otherwise the filter failure is re-failed. + * + * @see {@link catchIf} for predicate or refinement based recovery + * @see {@link catchTag} for `_tag` based recovery from one tagged error + * @see {@link catchTags} for `_tag` based recovery from multiple tagged errors + * @see {@link catchCauseFilter} for filtering full causes + * + * @category error handling + * @since 4.0.0 + */ +export const catchFilter: { + ( + filter: Filter.Filter, EB, X>, + f: (failure: EB) => Stream, + orElse?: ((failure: X) => Stream) | undefined + ): ( + self: Stream + ) => Stream, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> + ( + self: Stream, + filter: Filter.Filter, EB, X>, + f: (failure: EB) => Stream, + orElse?: ((failure: X) => Stream) | undefined + ): Stream, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> +} = dual((args) => isStream(args[0]), < + A, + E, + R, + EB, + A2, + E2, + R2, + X, + A3 = never, + E3 = X, + R3 = never +>( + self: Stream, + filter: Filter.Filter, EB, X>, + f: (failure: EB) => Stream, + orElse?: ((failure: X) => Stream) | undefined +): Stream => + fromChannel( + Channel.catchFilter( + toChannel(self), + filter, + (e) => f(e).channel, + orElse && ((e) => orElse(e).channel) + ) + )) + +/** + * Recovers from failures whose `_tag` matches the provided value by switching to + * the stream returned by `f`. + * + * **When to use** + * + * Use when you need to handle a specific error case from a stream whose error + * type is a tagged union with a readonly `_tag` field. + * + * **Example** (Catching tagged failures) + * + * ```ts + * import { Console, Data, Effect, Stream } from "effect" + * + * class HttpError extends Data.TaggedError("HttpError")<{ message: string }> {} + * + * const stream = Stream.fail(new HttpError({ message: "timeout" })) + * + * const recovered = Stream.catchTag(stream, "HttpError", (error) => + * Stream.make(`Recovered: ${error.message}`) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(recovered) + * yield* Console.log(values) + * // Output: [ "Recovered: timeout" ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchTag: { + < + const K extends Tags | Arr.NonEmptyReadonlyArray>, + E, + A1, + E1, + R1, + A2 = unassigned, + E2 = never, + R2 = never + >( + k: K, + f: ( + e: ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Stream, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Stream) + | undefined + ): ( + self: Stream + ) => Stream< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Stream, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Stream, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Stream) + | undefined + ): Stream< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > +} = dual( + (args) => isStream(args[0]), + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1, + A2 = never, + E2 = ExcludeTag ? K[number] : K>, + R2 = never + >( + self: Stream, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Stream, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Stream) + | undefined + ): Stream => { + const pred = Array.isArray(k) + ? ((e: E): e is any => hasProperty(e, "_tag") && k.includes(e._tag)) + : isTagged(k as string) + return catchIf(self, pred, f, orElse as any) as any + } +) + +/** + * Switches to a recovery stream based on matching `_tag` handlers. + * + * **Example** (Catching tagged failures with handlers) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * class NotFound { + * readonly _tag = "NotFound" + * constructor(readonly resource: string) {} + * } + * + * class Unauthorized { + * readonly _tag = "Unauthorized" + * constructor(readonly user: string) {} + * } + * + * const stream = Stream.fail(new NotFound("profile")) + * + * const program = Effect.gen(function* () { + * const result = yield* stream.pipe( + * Stream.catchTags({ + * NotFound: () => Stream.succeed("fallback"), + * Unauthorized: () => Stream.succeed("login") + * }), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * // Output: [ "fallback" ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const catchTags: { + < + E, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Stream + } : + {}), + A2 = unassigned, + E2 = never, + R2 = never + >( + cases: Cases, + orElse?: ((e: Exclude) => Stream) | undefined + ): (self: Stream) => Stream< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? R : never + }[keyof Cases] + > + < + R, + E, + A, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Stream + } : + {}), + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Stream, + cases: Cases, + orElse?: ((e: Exclude) => Stream) | undefined + ): Stream< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Stream) ? R : never + }[keyof Cases] + > +} = dual((args) => isStream(args[0]), (self, cases, orElse) => { + let keys: Array + return catchFilter( + self, + (e: any) => { + keys ??= Object.keys(cases) + return hasProperty(e, "_tag") && isString(e["_tag"]) && keys.includes(e["_tag"]) + ? Result.succeed(e) + : Result.fail(e) + }, + (e: any) => cases[e["_tag"] as string](e), + orElse + ) +}) + +/** + * Catches a specific reason within a tagged error. + * + * **When to use** + * + * Use to handle nested error causes without removing the parent error + * from the error channel. + * + * **Details** + * + * The handler receives the unwrapped reason. + * + * **Example** (Catching a tagged error reason) + * + * ```ts + * import { Console, Data, Effect, Stream } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * const stream = Stream.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* stream.pipe( + * Stream.catchReason("AiError", "RateLimitError", (reason) => + * Stream.succeed(`retry: ${reason.retryAfter}`) + * ), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "retry: 60" ] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchReason: { + < + K extends Tags, + E, + RK extends ReasonTags, K>>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + errorTag: K, + reasonTag: RK, + f: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Stream, + orElse?: + | (( + reason: ExcludeReason, K>, RK>, + error: OmitReason, K>, RK> + ) => Stream) + | undefined + ): ( + self: Stream + ) => Stream< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > + < + A, + E, + R, + K extends Tags, + RK extends ReasonTags>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + self: Stream, + errorTag: K, + reasonTag: RK, + f: (reason: ExtractReason, RK>, error: NarrowReason, RK>) => Stream, + orElse?: + | ((reason: ExcludeReason, RK>, error: OmitReason, RK>) => Stream) + | undefined + ): Stream< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > +} = dual( + (args) => isStream(args[0]), + < + A, + E, + R, + K extends Tags, + RK extends ReasonTags>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + self: Stream, + errorTag: K, + reasonTag: RK, + f: (reason: ExtractReason, RK>, error: NarrowReason, RK>) => Stream, + orElse?: + | ((reason: ExcludeReason, RK>, error: OmitReason, RK>) => Stream) + | undefined + ): Stream< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > => + fromChannel( + Channel.catchReason( + toChannel(self), + errorTag, + reasonTag, + (reason, error) => f(reason, error).channel, + orElse && ((reason, error) => orElse(reason, error).channel) + ) + ) as any +) + +/** + * Catches multiple reasons within a tagged error using an object of handlers. + * + * **Example** (Catching tagged error reasons) + * + * ```ts + * import { Console, Data, Effect, Stream } from "effect" + * + * class RateLimitError extends Data.TaggedError("RateLimitError")<{ + * retryAfter: number + * }> {} + * + * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ + * limit: number + * }> {} + * + * class AiError extends Data.TaggedError("AiError")<{ + * reason: RateLimitError | QuotaExceededError + * }> {} + * + * const stream = Stream.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* stream.pipe( + * Stream.catchReasons("AiError", { + * RateLimitError: (reason) => Stream.succeed(`retry: ${reason.retryAfter}`), + * QuotaExceededError: (reason) => Stream.succeed(`quota: ${reason.limit}`) + * }), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "retry: 60" ] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchReasons: { + < + K extends Tags, + E, + Cases extends { + [RK in ReasonTags, K>>]+?: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Stream + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Stream) + | undefined + ): (self: Stream) => Stream< + | A + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? A : never + }[keyof Cases], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? E : never + }[keyof Cases], + | R + | R2 + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? R : never + }[keyof Cases] + > + < + A, + E, + R, + K extends Tags, + Cases extends { + [RK in ReasonTags>]+?: ( + reason: ExtractReason, RK>, + error: NarrowReason, RK> + ) => Stream + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Stream, + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Stream) + | undefined + ): Stream< + | A + | Exclude + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? A : never + }[keyof Cases], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? E : never + }[keyof Cases], + | R + | R2 + | { + [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Stream ? R : never + }[keyof Cases] + > +} = dual((args) => isStream(args[0]), (self, errorTag, cases, orElse) => { + const handlers: Record Channel.Channel> = {} + for (const key of Object.keys(cases)) { + const handler = (cases as any)[key] + handlers[key] = (reason, error) => handler(reason, error).channel + } + const orElseHandler = orElse && ((reason: any, error: any) => orElse(reason, error).channel) + return fromChannel( + Channel.catchReasons(self.channel, errorTag as any, handlers as any, orElseHandler as any) as Channel.Channel< + Arr.NonEmptyReadonlyArray, + any, + void, + unknown, + unknown, + unknown, + any + > + ) as any +}) + +/** + * Transforms the errors emitted by this stream using `f`. + * + * **Example** (Mapping stream errors) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.fail("bad").pipe( + * Stream.mapError((error) => `mapped: ${error}`), + * Stream.catch((error) => Stream.make(`recovered from ${error}`)), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ "recovered from mapped: bad" ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const mapError: { + (f: (error: E) => E2): (self: Stream) => Stream + (self: Stream, f: (error: E) => E2): Stream +} = dual(2, ( + self: Stream, + f: (error: E) => E2 +): Stream => fromChannel(Channel.mapError(self.channel, f))) + +/** + * Recovers from stream failures by filtering the `Cause` and switching to a recovery stream. + * Non-matching causes are re-emitted as failures. + * + * **Example** (Catching matching causes) + * + * ```ts + * import { Cause, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const failingStream = Stream.fail("NetworkError") + * const recovered = Stream.catchCauseIf( + * failingStream, + * (cause) => Cause.hasFails(cause), + * (cause) => Stream.make(`Recovered: ${Cause.squash(cause)}`) + * ) + * + * const output = yield* Stream.runCollect(recovered) + * yield* Console.log(output) + * }) + * + * Effect.runPromise(program) + * // Output: [ "Recovered: NetworkError" ] + * ``` + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseIf: { + ( + predicate: Predicate>, + f: (cause: Cause.Cause) => Stream + ): ( + self: Stream + ) => Stream + ( + self: Stream, + predicate: Predicate>, + f: (cause: Cause.Cause) => Stream + ): Stream +} = dual(3, ( + self: Stream, + predicate: Predicate>, + f: (cause: Cause.Cause) => Stream +): Stream => + fromChannel( + Channel.catchCauseIf( + self.channel, + predicate, + (cause) => f(cause).channel + ) + )) + +/** + * Recovers from stream failures by filtering the `Cause` and switching to a + * recovery stream. + * + * **When to use** + * + * Use when you need to recover a stream only from causes selected by a + * `Filter`, while giving the recovery both the selected value and the original + * `Cause`. + * + * **Details** + * + * The filter is applied to the full `Cause`. A successful filter result is + * passed to `f` together with the original cause; a failed filter result + * re-fails with the residual cause. + * + * @see {@link catchCauseIf} for predicate-based cause selection + * @see {@link catchFilter} for filtering typed error values instead of full causes + * @see {@link catchCause} for recovering from every cause without filtering + * + * @category error handling + * @since 4.0.0 + */ +export const catchCauseFilter: { + >( + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Stream + ): ( + self: Stream + ) => Stream | E2, R2 | R> + >( + self: Stream, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Stream + ): Stream | E2, R | R2> +} = dual(3, >( + self: Stream, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Stream +): Stream | E2, R | R2> => + fromChannel( + Channel.catchCauseFilter( + self.channel, + filter, + (failure, cause) => f(failure, cause).channel + ) + )) + +/** + * Switches to a fallback stream if this stream is empty. + * + * **Example** (Switching on empty streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.empty.pipe( + * Stream.orElseIfEmpty(() => Stream.make(1, 2)), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const orElseIfEmpty: { + ( + orElse: LazyArg> + ): (self: Stream) => Stream + ( + self: Stream, + orElse: LazyArg> + ): Stream +} = dual(2, ( + self: Stream, + orElse: LazyArg> +): Stream => + fromChannel(Channel.orElseIfEmpty( + self.channel, + (_) => toChannel(orElse()) + ))) + +/** + * Returns a stream that emits a fallback value when this stream fails. + * + * **Example** (Recovering with a fallback value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fail("NetworkError").pipe( + * Stream.orElseSucceed((error) => `Recovered: ${error}`) + * ) + * + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ "Recovered: NetworkError" ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const orElseSucceed: { + ( + f: (error: E) => A2 + ): (self: Stream) => Stream + ( + self: Stream, + f: (error: E) => A2 + ): Stream +} = dual(2, ( + self: Stream, + f: (error: E) => A2 +): Stream => catch_(self, (e) => succeed(f(e)))) + +/** + * Turns typed failures into defects, making the stream infallible. + * + * **Example** (Turning failures into defects) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.orDie, + * Stream.runCollect + * ) + * + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const orDie = (self: Stream): Stream => fromChannel(Channel.orDie(self.channel)) + +/** + * Ignores failures and ends the stream on error. + * + * **When to use** + * + * Use when you want a failing stream to end gracefully rather than propagate + * the error. + * + * **Details** + * + * The `log` option controls whether the failure is logged before the stream + * terminates. + * + * **Example** (Ignoring stream failures) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.ignore, + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * **Example** (Configuring ignore logging) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function*() { + * const values = yield* Stream.fail("boom").pipe( + * Stream.ignore({ log: false }), + * Stream.runCollect + * ) + * yield* Effect.sync(() => console.log(values)) + * })) + * + * // [] + * ``` + * + * @see {@link ignoreCause} for a variant that also ignores defects, not just typed failures + * + * @category error handling + * @since 4.0.0 + */ +export const ignore: < + Arg extends Stream | { + readonly log?: boolean | Severity | undefined + } | undefined +>( + selfOrOptions: Arg, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined +) => [Arg] extends [Stream] ? Stream + : (self: Stream) => Stream = dual( + (args) => isStream(args[0]), + ( + self: Stream, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined + ): Stream => fromChannel(Channel.ignore(self.channel, options)) + ) + +/** + * Ignores the stream's failure cause, including defects, and ends the stream. + * + * **When to use** + * + * Use when you need to silently suppress a stream's entire failure cause, + * including both typed errors and defects, rather than propagate it downstream. + * + * **Example** (Ignoring stream failure causes) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function*() { + * const values = yield* Stream.make(1, 2).pipe( + * Stream.concat(Stream.die("boom")), + * Stream.ignoreCause({ log: false }), + * Stream.runCollect + * ) + * yield* Effect.sync(() => console.log(values)) + * })) + * + * // [ 1, 2 ] + * ``` + * + * @see {@link ignore} to ignore only typed failures without suppressing defects + * + * @category error handling + * @since 4.0.0 + */ +export const ignoreCause: < + Arg extends Stream | { + readonly log?: boolean | Severity | undefined + } | undefined +>( + streamOrOptions: Arg, + options?: { + readonly log?: boolean | Severity | undefined + } | undefined +) => [Arg] extends [Stream] ? Stream + : (self: Stream) => Stream = dual( + (args) => isStream(args[0]), + ( + self: Stream, + options?: { readonly log?: boolean | Severity | undefined } | undefined + ): Stream => fromChannel(Channel.ignoreCause(self.channel, options)) + ) + +/** + * Retries the stream according to the given schedule when it fails. + * + * **Details** + * + * This retries the entire stream, so will re-execute all of the stream's + * acquire operations. + * + * The schedule is reset as soon as the first element passes through the + * stream again. + * + * **Example** (Retrying stream failures) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.retry(Schedule.recurs(1)), + * Stream.take(2), + * Stream.runCollect + * ) + * + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 1 ] + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const retry: { + ( + policy: + | Schedule.Schedule, E2, R2> + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, E2, R2>) + ): (self: Stream) => Stream + ( + self: Stream, + policy: + | Schedule.Schedule, E2, R2> + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, E2, R2>) + ): Stream +} = dual( + 2, + ( + self: Stream, + policy: + | Schedule.Schedule, E2, R2> + | (( + $: (_: Schedule.Schedule, SE, SR>) => Schedule.Schedule + ) => Schedule.Schedule, E2, R2>) + ): Stream => fromChannel(Channel.retry(self.channel, policy)) +) + +/** + * Applies an `ExecutionPlan` to a stream, retrying with step-provided resources + * until it succeeds or the plan is exhausted. + * + * **Details** + * + * By default, a failing step can fallback even after emitting elements; set + * `preventFallbackOnPartialStream` to fail instead of mixing partial output with + * a later fallback. + * + * **Example** (Applying an execution plan) + * + * ```ts + * import { Console, Context, Effect, ExecutionPlan, Layer, Stream } from "effect" + * + * class Service extends Context.Service()("Service", { + * make: Effect.succeed({ + * stream: Stream.fail("A") as Stream.Stream + * }) + * }) { + * static Bad = Layer.succeed(Service, Service.of({ stream: Stream.fail("A") })) + * static Good = Layer.succeed(Service, Service.of({ stream: Stream.make(1, 2, 3) })) + * } + * + * const plan = ExecutionPlan.make( + * { provide: Service.Bad }, + * { provide: Service.Good } + * ) + * + * const stream = Stream.unwrap(Effect.map(Service, (_) => _.stream)) + * + * const program = Effect.gen(function*() { + * const items = yield* stream.pipe(Stream.withExecutionPlan(plan), Stream.runCollect) + * yield* Console.log(items) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category error handling + * @since 3.16.0 + */ +export const withExecutionPlan: { + ( + policy: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, + options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } + ): (self: Stream) => Stream> + ( + self: Stream, + policy: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, + options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } + ): Stream> +} = dual((args) => isStream(args[0]), ( + self: Stream, + policy: ExecutionPlan.ExecutionPlan<{ + provides: Provides + input: Input + error: PolicyE + requirements: R2 + }>, + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + } +): Stream> => + suspend(() => { + const preventFallbackOnPartialStream = options?.preventFallbackOnPartialStream ?? false + let i = 0 + let meta: ExecutionPlan.Metadata = { + attempt: 0, + stepIndex: 0 + } + const provideMeta = provideServiceEffect( + ExecutionPlan.CurrentMetadata, + Effect.sync(() => { + meta = { + attempt: meta.attempt + 1, + stepIndex: i + } + return meta + }) + ) + let lastError = Option.none() + const loop: Stream< + A, + E | PolicyE, + R2 | Exclude + > = suspend(() => { + const step = policy.steps[i] + if (!step) { + return fail(Option.getOrThrow(lastError)) + } + + let nextStream: Stream> = provideMeta(provide(self, step.provide)) + let receivedElements = false + + if (Option.isSome(lastError)) { + const error = lastError.value + let attempted = false + const wrapped = nextStream + // ensure the schedule is applied at least once + nextStream = suspend(() => { + if (attempted) return wrapped + attempted = true + return fail(error) + }) + nextStream = retry(nextStream, internalExecutionPlan.scheduleFromStep(step, false) as any) + } else { + const schedule = internalExecutionPlan.scheduleFromStep(step, true) + nextStream = schedule ? retry(nextStream, schedule as any) : nextStream + } + + return catch_( + preventFallbackOnPartialStream ? + onFirst(nextStream, (_) => { + receivedElements = true + return Effect.void + }) : + nextStream, + (error) => { + i++ + if (preventFallbackOnPartialStream && receivedElements) { + return fail(error) + } + lastError = Option.some(error) + return loop + } + ) + }) + return loop + })) + +/** + * Takes the first `n` elements from this stream, returning `Stream.empty` when `n < 1`. + * + * **Example** (Taking values from the left) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.take(3), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const take: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = dual( + 2, + (self: Stream, n: number): Stream => + n < 1 ? empty : takeUntil(self, (_, i) => i === (n - 1)) +) + +/** + * Keeps the last `n` elements from this stream. + * + * **Example** (Taking elements from the right) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.range(1, 6).pipe( + * Stream.takeRight(3), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 4, 5, 6 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = dual( + 2, + (self: Stream, n: number): Stream => + mapAccumArray(self, MutableList.make, (list, arr) => { + MutableList.appendAll(list, arr) + if (list.length > n) { + MutableList.takeNVoid(list, list.length - n) + } + return [list, emptyArr] + }, { + onHalt(list) { + return MutableList.takeAll(list) + } + }) +) + +/** + * Takes elements until the predicate matches. + * + * **Details** + * + * When `excludeLast` is `true`, the matching element is dropped. + * + * **Example** (Taking until a predicate matches) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.range(1, 5) + * + * const program = Effect.gen(function*() { + * const inclusive = yield* stream.pipe( + * Stream.takeUntil((n) => n % 3 === 0), + * Stream.runCollect + * ) + * yield* Console.log(inclusive) + * // Output: [ 1, 2, 3 ] + * + * const exclusive = yield* stream.pipe( + * Stream.takeUntil((n) => n % 3 === 0, { excludeLast: true }), + * Stream.runCollect + * ) + * yield* Console.log(exclusive) + * // Output: [ 1, 2 ] + * }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const takeUntil: { + (predicate: (a: NoInfer, n: number) => boolean, options?: { + readonly excludeLast?: boolean | undefined + }): (self: Stream) => Stream + (self: Stream, predicate: (a: A, n: number) => boolean, options?: { + readonly excludeLast?: boolean | undefined + }): Stream +} = dual( + (args) => isStream(args[0]), + (self: Stream, predicate: (a: A, n: number) => boolean, options?: { + readonly excludeLast?: boolean | undefined + }): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let i = 0 + let done = false + const pump: Pull.Pull, E, void, R> = Effect.flatMap( + Effect.suspend(() => done ? Cause.done() : pull), + (chunk) => { + const index = chunk.findIndex((a) => predicate(a, i++)) + if (index >= 0) { + done = true + const arr = chunk.slice(0, options?.excludeLast ? index : index + 1) + return Arr.isReadonlyArrayNonEmpty(arr) ? Effect.succeed(arr) : Cause.done() + } + return Effect.succeed(chunk) + } + ) + return pump + })) +) + +/** + * Takes stream elements until an effectful predicate returns `true`. + * + * **When to use** + * + * Use when the stopping condition needs an Effect or service and predicate + * failure should fail the stream. + * + * **Example** (Taking until an effectful predicate matches) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.range(1, 5).pipe( + * Stream.takeUntilEffect((n) => Effect.succeed(n % 3 === 0)), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const takeUntilEffect: { + ( + predicate: (a: NoInfer, n: number) => Effect.Effect, + options?: { + readonly excludeLast?: boolean | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: A, n: number) => Effect.Effect, + options?: { + readonly excludeLast?: boolean | undefined + } + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + predicate: (a: A, n: number) => Effect.Effect, + options?: { + readonly excludeLast?: boolean | undefined + } +): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let i = 0 + let done = false + return Effect.gen(function*() { + if (done) return yield* Cause.done() + const chunk = yield* pull + for (let j = 0; j < chunk.length; j++) { + if (yield* predicate(chunk[j], i++)) { + done = true + const arr = chunk.slice(0, options?.excludeLast ? j : j + 1) + return Arr.isReadonlyArrayNonEmpty(arr) ? arr : yield* Cause.done() + } + } + return chunk + }) + }))) + +/** + * Takes the longest initial prefix of elements that satisfy the predicate. + * + * **Example** (Taking while a predicate holds) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.range(1, 5).pipe( + * Stream.takeWhile((n) => n % 3 !== 0) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const takeWhile: { + (refinement: (a: NoInfer, n: number) => a is B): (self: Stream) => Stream + (predicate: (a: NoInfer, n: number) => boolean): (self: Stream) => Stream + (self: Stream, refinement: (a: NoInfer, n: number) => a is B): Stream + (self: Stream, predicate: (a: NoInfer, n: number) => boolean): Stream +} = dual( + 2, + ( + self: Stream, + predicate: (a: A, n: number) => boolean + ): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let i = 0 + let done = false + const pump: Pull.Pull, E, void, R> = Effect.flatMap( + Effect.suspend(() => done ? Cause.done() : pull), + (chunk) => { + const out: Array = [] + for (let j = 0; j < chunk.length; j++) { + if (!predicate(chunk[j], i++)) { + done = true + break + } + out.push(chunk[j]) + } + return Arr.isReadonlyArrayNonEmpty(out) ? Effect.succeed(out) : done ? Cause.done() : pump + } + ) + return pump + })) +) + +/** + * Takes the longest initial prefix accepted by a `Filter` and emits the + * filter's success values. + * + * **When to use** + * + * Use to keep the leading stream elements that a `Filter` accepts, emit the + * filter's success values, and stop at the first filter failure. + * + * **Details** + * + * The stream stops at the first `Result.fail` returned by the filter. + * + * @see {@link takeWhile} for keeping original elements with a boolean predicate or refinement + * @see {@link filterMap} for filtering across the whole stream instead of only the leading prefix + * @see {@link dropWhileFilter} for dropping the accepted prefix and keeping the remaining original elements + * + * @category filtering + * @since 4.0.0 + */ +export const takeWhileFilter: { + (f: Filter.Filter, B, X>): (self: Stream) => Stream + (self: Stream, f: Filter.Filter, B, X>): Stream +} = dual( + 2, + ( + self: Stream, + filter: Filter.Filter, B, X> + ): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let done = false + const pump: Pull.Pull, E, void, R> = Effect.flatMap( + Effect.suspend(() => done ? Cause.done() : pull), + (chunk) => { + const out: Array = [] + for (let j = 0; j < chunk.length; j++) { + const result = filter(chunk[j]) + if (Result.isFailure(result)) { + done = true + break + } + out.push(result.success) + } + return Arr.isReadonlyArrayNonEmpty(out) ? Effect.succeed(out) : done ? Cause.done() : pump + } + ) + return pump + })) +) + +/** + * Takes elements from the stream while the effectful predicate is `true`. + * + * **When to use** + * + * Use when the leading-prefix predicate needs an Effect or service and the + * stream should stop before the first false result. + * + * **Example** (Effectfully taking while a predicate holds) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.range(1, 5).pipe( + * Stream.takeWhileEffect((n) => Effect.succeed(n % 3 !== 0)), + * Stream.runCollect + * ) + * Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2 ] + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const takeWhileEffect: { + ( + predicate: (a: NoInfer, n: number) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: NoInfer, n: number) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + predicate: (a: NoInfer, n: number) => Effect.Effect +) => + takeUntilEffect(self, (a, n) => + Effect.map( + predicate(a, n), + (b) => !b + ), { excludeLast: true })) + +/** + * Drops the first `n` elements from this stream. + * + * **Example** (Dropping values from the left) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * const result = Stream.drop(stream, 2) + * + * const program = Effect.gen(function*() { + * const items = yield* Stream.runCollect(result) + * yield* Console.log(items) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 4, 5 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const drop: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = dual( + 2, + (self: Stream, n: number): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let dropped = 0 + const pump: Pull.Pull, E, void, R> = pull.pipe( + Effect.flatMap((chunk) => { + if (dropped >= n) return Effect.succeed(chunk) + dropped += chunk.length + if (dropped <= n) return pump + return Effect.succeed(chunk.slice(n - dropped) as Arr.NonEmptyArray) + }) + ) + return pump + })) +) + +/** + * Drops elements until the specified predicate evaluates to `true`, then drops + * that matching element. + * + * **Example** (Dropping until a predicate matches) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * const result = Stream.dropUntil(stream, (n) => n >= 3) + * + * Effect.gen(function*() { + * const output = yield* Stream.runCollect(result) + * yield* Console.log(output) // Output: [ 4, 5 ] + * }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dropUntil: { + (predicate: (a: NoInfer, index: number) => boolean): (self: Stream) => Stream + (self: Stream, predicate: (a: NoInfer, index: number) => boolean): Stream +} = dual(2, ( + self: Stream, + predicate: (a: NoInfer, index: number) => boolean +): Stream => drop(dropWhile(self, (a, i) => !predicate(a, i)), 1)) + +/** + * Drops all elements of the stream until the specified effectful predicate + * evaluates to `true`. + * + * **When to use** + * + * Use when dropping the leading prefix requires an Effect or service and the + * first matching element should also be dropped. + * + * **Example** (Dropping until an effectful predicate matches) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.range(1, 5).pipe( + * Stream.dropUntilEffect((n) => Effect.succeed(n % 3 === 0)), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 4, 5 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dropUntilEffect: { + ( + predicate: (a: NoInfer, index: number) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: NoInfer, index: number) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + predicate: (a: NoInfer, index: number) => Effect.Effect +): Stream => + drop( + dropWhileEffect( + self, + (a, i) => Effect.map(predicate(a, i), (b) => !b) + ), + 1 + )) + +/** + * Drops elements from the stream while the specified predicate evaluates to `true`. + * + * **Example** (Dropping while a predicate holds) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.dropWhile((n) => n < 3), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 4, 5 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dropWhile: { + (predicate: (a: NoInfer, index: number) => boolean): (self: Stream) => Stream + (self: Stream, predicate: (a: NoInfer, index: number) => boolean): Stream +} = dual(2, ( + self: Stream, + predicate: (a: A, index: number) => boolean +): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let dropping = true + let index = 0 + const filtered: Pull.Pull, E> = Effect.flatMap(pull, (arr) => { + const found = arr.findIndex((a) => !predicate(a, index++)) + if (found === -1) return filtered + dropping = false + return Effect.succeed(arr.slice(found) as Arr.NonEmptyArray) + }) + return Effect.suspend(() => dropping ? filtered : pull) + }))) + +/** + * Drops elements while the filter succeeds. + * + * **When to use** + * + * Use when you need to remove a leading stream prefix based on a synchronous + * `Filter` result while preserving the remaining original stream elements. + * + * **Details** + * + * `Result.succeed` drops the current element. The first `Result.fail` stops + * dropping, emits that original element, and the rest of the source stream is + * emitted without further filtering. + * + * @see {@link dropWhile} for boolean predicate prefix dropping + * @see {@link takeWhileFilter} for keeping the accepted prefix as filter success values + * @see {@link dropWhileEffect} for effectful predicate prefix dropping + * + * @category filtering + * @since 4.0.0 + */ +export const dropWhileFilter: { + (filter: Filter.Filter, B, X>): (self: Stream) => Stream + (self: Stream, filter: Filter.Filter, B, X>): Stream +} = dual(2, ( + self: Stream, + filter: Filter.Filter, B, X> +): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let dropping = true + const filtered: Pull.Pull, E> = Effect.flatMap(pull, (arr) => { + const found = arr.findIndex((a) => Result.isFailure(filter(a))) + if (found === -1) return filtered + dropping = false + return Effect.succeed(arr.slice(found) as Arr.NonEmptyArray) + }) + return Effect.suspend(() => dropping ? filtered : pull) + }))) + +/** + * Drops elements while the specified effectful predicate evaluates to `true`. + * + * **Example** (Effectfully dropping while a predicate holds) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.dropWhileEffect((n) => Effect.succeed(n < 3)), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 4, 5 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dropWhileEffect: { + ( + predicate: (a: NoInfer, index: number) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + predicate: (a: A, index: number) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + predicate: (a: NoInfer, index: number) => Effect.Effect +): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let dropping = true + let index = 0 + const filtered: Pull.Pull, E | E2, void, R2> = Effect.gen(function*() { + while (true) { + const arr = yield* pull + for (let i = 0; i < arr.length; i++) { + const drop = yield* predicate(arr[i], index++) + if (drop) continue + dropping = false + return arr.slice(i) as Arr.NonEmptyArray + } + } + }) + return Effect.suspend((): Pull.Pull, E | E2, void, R | R2> => + dropping ? filtered : pull + ) + }))) + +/** + * Drops the last specified number of elements from this stream. + * + * **Details** + * + * Keeps the last `n` elements in memory to drop them on completion. + * + * **Example** (Dropping values from the right) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.dropRight(2), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const dropRight: { + (n: number): (self: Stream) => Stream + (self: Stream, n: number): Stream +} = dual( + 2, + (self: Stream, n: number): Stream => { + if (n <= 0) return self + return transformPull(self, (pull, _scope) => + Effect.sync(() => { + const list = MutableList.make() + const emit: Pull.Pull, E> = Effect.flatMap(pull, (arr) => { + MutableList.appendAllUnsafe(list, arr) + const toTake = list.length - n + const items = MutableList.takeN(list, toTake) + return Arr.isArrayNonEmpty(items) ? Effect.succeed(items) : emit + }) + return emit + })) + } +) + +/** + * Exposes the underlying chunks as a stream of non-empty arrays. + * + * **Example** (Exposing stream chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const chunks = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.rechunk(2), + * Stream.chunks, + * Stream.runCollect + * ) + * yield* Console.log(chunks) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, 2 ], [ 3, 4 ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const chunks = (self: Stream): Stream, E, R> => + self.channel.pipe( + Channel.map(Arr.of), + fromChannel + ) + +/** + * Groups the stream into arrays of the specified size, preserving element order. + * + * **Details** + * + * The size is clamped to at least 1. + * + * **Example** (Rechunking stream elements) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.rechunk(2), + * Stream.chunks, + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const rechunk: { + (size: number): (self: Stream) => Stream + (self: Stream, size: number): Stream +} = dual(2, (self: Stream, target: number): Stream => { + target = Math.max(1, target) + return transformPull(self, (pull, _scope) => + Effect.sync(() => { + let chunk = Arr.empty() as Arr.NonEmptyArray + let index = 0 + let current: Arr.NonEmptyReadonlyArray | undefined + let done = false + + return Effect.suspend(function loop(): Pull.Pull, E, void, R> { + if (done) return Cause.done() + else if (current === undefined) { + return Effect.flatMap(pull, (arr) => { + if (chunk.length === 0 && arr.length === target) { + return Effect.succeed(arr) + } else if (chunk.length + arr.length < target) { + chunk.push(...arr) + return loop() + } + current = arr + return loop() + }) + } + for (; index < current.length;) { + chunk.push(current[index++]) + if (chunk.length === target) { + const result = chunk + chunk = [] as any + return Effect.succeed(result) + } + } + index = 0 + current = undefined + return loop() + }).pipe( + Pull.catchDone(() => { + if (chunk.length === 0) return Cause.done() + const result = chunk + done = true + chunk = [] as any + return Effect.succeed(result) + }) + ) + })) +}) + +/** + * Emits a sliding window of `n` elements. + * + * **Example** (Emitting sliding windows) + * + * ```ts + * import { Console, Effect, pipe, Stream } from "effect" + * + * Effect.gen(function*() { + * const result = yield* pipe( + * Stream.make(1, 2, 3, 4, 5), + * Stream.sliding(2), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * // Output: [ [1, 2], [2, 3], [3, 4], [4, 5] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const sliding: { + (chunkSize: number): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number): Stream, E, R> +} = dual( + 2, + (self: Stream, chunkSize: number): Stream, E, R> => + slidingSize(self, chunkSize, 1) +) + +/** + * Emits sliding windows of `chunkSize` elements, advancing by `stepSize`. + * + * **Example** (Emitting sliding windows with a step size) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const chunks = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.slidingSize(3, 2), + * Stream.runCollect + * ) + * yield* Console.log(chunks) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, 2, 3 ], [ 3, 4, 5 ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const slidingSize: { + (chunkSize: number, stepSize: number): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number, stepSize: number): Stream, E, R> +} = dual( + 3, + (self: Stream, chunkSize: number, stepSize: number): Stream, E, R> => + transformPull(self, (upstream, _scope) => + Effect.sync(() => { + let cause: Cause.Cause | null = null + const list = MutableList.make() + let emitted = false + const pull: Pull.Pull< + Arr.NonEmptyReadonlyArray>, + E | Cause.Done + > = Effect.matchCauseEffect(upstream, { + onSuccess(arr) { + MutableList.appendAllUnsafe(list, arr) + if (list.length < chunkSize) return pull + emitted = true + const chunks = [] as any as Arr.NonEmptyArray> + while (list.length >= chunkSize) { + if (chunkSize === stepSize) { + chunks.push(MutableList.takeN(list, chunkSize) as any) + } else { + chunks.push(MutableList.toArrayN(list, chunkSize) as any) + if (chunkSize === 1) { + MutableList.take(list) + } else { + MutableList.takeNVoid(list, stepSize) + } + } + } + return Effect.succeed(chunks) + }, + onFailure(cause_) { + if (emitted) MutableList.takeNVoid(list, chunkSize - stepSize) + if (list.length === 0) return Effect.failCause(cause_) + cause = cause_ + return Effect.succeed(Arr.of(MutableList.takeAll(list) as any)) + } + }) + + return Effect.suspend(() => cause ? Effect.failCause(cause) : pull) + })) +) + +/** + * Splits the stream into non-empty groups whenever the predicate matches. + * + * **Details** + * + * Matching elements act as delimiters and are not included in the output. + * + * **Example** (Splitting on matching values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.range(0, 9).pipe( + * Stream.split((n) => n % 4 === 0), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ [1, 2, 3], [5, 6, 7], [9] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const split: { + ( + refinement: Refinement, B> + ): (self: Stream) => Stream>, E, R> + (predicate: Predicate>): (self: Stream) => Stream, E, R> + ( + self: Stream, + refinement: Refinement + ): Stream>, E, R> + (self: Stream, predicate: Predicate): Stream, E, R> +} = dual(2, ( + self: Stream, + predicate: Predicate> +): Stream, E, R> => + mapAccumArray(self, Arr.empty, (acc, arr) => { + const out = Arr.empty>() + for (let i = 0; i < arr.length; i++) { + if (predicate(arr[i])) { + if (Arr.isArrayNonEmpty(acc)) { + out.push(acc) + acc = [] + } + } else { + acc.push(arr[i]) + } + } + return [acc, out] + }, { + onHalt(arr) { + return Arr.isArrayNonEmpty(arr) ? Arr.of(arr) : emptyArr + } + })) + +/** + * Combines elements from this stream and the specified stream by repeatedly + * applying a stateful function that can pull from either side. + * + * **Details** + * + * Where possible, prefer `Stream.combineArray` for a more efficient + * implementation. + * + * **Example** (Combining streams with state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.combine( + * Stream.make("A", "B", "C"), + * Stream.make(1, 2, 3), + * () => true, + * (takeLeft, pullLeft, pullRight) => + * takeLeft + * ? Effect.map(pullLeft, (value) => [`L:${value}`, false] as const) + * : Effect.map(pullRight, (value) => [`R:${value}`, true] as const) + * ) + * + * const program = Effect.gen(function*() { + * const output = yield* Stream.runCollect(stream) + * yield* Console.log(output) + * }) + * + * Effect.runPromise(program) + * // Output: [ "L:A", "R:1", "L:B", "R:2", "L:C", "R:3" ] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const combine: { + ( + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect + ): Stream +} = dual(4, ( + self: Stream, + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, + pullRight: Pull.Pull + ) => Effect.Effect +): Stream => + Channel.combine( + Channel.flattenArray(self.channel), + Channel.flattenArray(that.channel), + s, + f + ).pipe( + Channel.map(Arr.of), + fromChannel + )) + +/** + * Combines two streams chunk-by-chunk with a stateful pull function. + * + * **When to use** + * + * Use to coordinate pulling chunks from two streams when each emitted chunk + * depends on both sides and local state. + * + * **Details** + * + * The combining function receives the current state and pull functions for the + * left and right streams. It returns the next non-empty chunk together with the + * next state. + * + * **Example** (Combining stream chunks with state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2).pipe( + * Stream.combineArray( + * Stream.make(10, 20), + * () => true, + * (useLeft, pullLeft, pullRight) => + * Effect.gen(function*() { + * const array = useLeft ? yield* pullLeft : yield* pullRight + * return [array, !useLeft] as const + * }) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 10, 20 ] + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const combineArray: { + ( + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, E, void>, + pullRight: Pull.Pull, E2, void> + ) => Effect.Effect, S], E3, R3> + ): (self: Stream) => Stream, R2 | R3 | R> + ( + self: Stream, + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, E, void>, + pullRight: Pull.Pull, E2, void> + ) => Effect.Effect, S], E3, R3> + ): Stream, R | R2 | R3> +} = dual(4, ( + self: Stream, + that: Stream, + s: LazyArg, + f: ( + s: S, + pullLeft: Pull.Pull, E, void>, + pullRight: Pull.Pull, E2, void> + ) => Effect.Effect, S], E3, R3> +): Stream, R | R2 | R3> => + fromChannel(Channel.combine( + self.channel, + that.channel, + s, + f + ))) + +/** + * Maps elements statefully, emitting zero or more outputs per input. + * + * **Example** (Statefully mapping stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const totals = yield* Stream.make(0, 1, 2, 3, 4, 5, 6).pipe( + * Stream.mapAccum(() => 0, (total, n) => { + * const next = total + n + * return [next, [next]] as const + * }), + * Stream.runCollect + * ) + * + * yield* Console.log(totals) + * }) + * + * Effect.runPromise(program) + * // Output: [ 0, 1, 3, 6, 10, 15, 21 ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapAccum: { + ( + initial: LazyArg, + f: (s: S, a: A) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + initial: LazyArg, + f: (s: S, a: A) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + initial: LazyArg, + f: (s: S, a: A) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } +): Stream => + fromChannel(Channel.mapAccum( + self.channel, + initial, + (state, arr) => { + const acc = Arr.empty() + for (let index = 0; index < arr.length; index++) { + const [newState, values] = f(state, arr[index]) + state = newState + acc.push(...values) + } + return [state, Arr.isArrayNonEmpty(acc) ? Arr.of(acc) : emptyArr] + }, + options?.onHalt ? + { + onHalt(state) { + const arr = options.onHalt!(state) + return Arr.isReadonlyArrayNonEmpty(arr) ? Arr.of(arr) : emptyArr + } + } : + undefined + ))) + +/** + * Maps over non-empty chunk arrays statefully, emitting zero or more values per chunk. + * + * **Details** + * + * The mapping function runs once per chunk and the state is threaded across chunks. + * + * **Example** (Statefully mapping stream chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const output = yield* Stream.make(1, 2, 3, 4, 5, 6).pipe( + * Stream.rechunk(2), + * Stream.mapAccumArray(() => 0, (sum: number, chunk) => { + * const next = chunk.reduce((acc, n) => acc + n, sum) + * return [next, [next]] + * }), + * Stream.runCollect + * ) + * yield* Console.log(output) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 10, 21 ] + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const mapAccumArray: { + ( + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => Array) | undefined + } + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => readonly [state: S, values: ReadonlyArray], + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } +): Stream => + fromChannel(Channel.mapAccum( + self.channel, + initial, + (state, arr) => { + const [newState, values] = f(state, arr) + state = newState + return [state, Arr.isReadonlyArrayNonEmpty(values) ? Arr.of(values) : emptyArr] + }, + options?.onHalt ? + { + onHalt(state) { + const arr = options.onHalt!(state) + return Arr.isReadonlyArrayNonEmpty(arr) ? Arr.of(arr) : emptyArr + } + } : + undefined + ))) + +const emptyArr = Arr.empty() + +/** + * Maps each element statefully and effectfully, emitting zero or more output + * values per input. + * + * **When to use** + * + * Use when stateful element mapping needs Effects or can fail while emitting + * zero or more values per input element. + * + * **Details** + * + * The mapping effect receives the current state and element, then returns the + * next state plus the values to emit. The state is threaded through the + * stream. + * + * **Example** (Effectfully mapping stream values with state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.make(1, 1, 1).pipe( + * Stream.mapAccumEffect(() => 0, (total, n) => + * Effect.succeed([total + n, [total + n]]) + * ), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // Output: [ 1, 2, 3 ] + * ``` + * + * @category mapping + * @since 2.0.0 + */ +export const mapAccumEffect: { + ( + initial: LazyArg, + f: (s: S, a: A) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + initial: LazyArg, + f: (s: S, a: A) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): Stream +} = dual((args) => isStream(args[0]), ( + self: Stream, + initial: LazyArg, + f: (s: S, a: A) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.mapAccum( + initial, + (state, a) => + Effect.map( + f(state, a), + ([state, values]) => [ + state, + Arr.isReadonlyArrayNonEmpty(values) ? Arr.of(values) : Arr.empty>() + ] + ), + options?.onHalt ? + { + onHalt(state) { + const arr = options.onHalt!(state) + return Arr.isReadonlyArrayNonEmpty(arr) ? Arr.of(arr) : emptyArr + } + } : + undefined + ), + fromChannel + )) + +/** + * Maps each non-empty input chunk statefully and effectfully, emitting zero or + * more output values per chunk. + * + * **When to use** + * + * Use when stateful mapping should process each emitted non-empty chunk with an + * Effect instead of each element separately. + * + * **Details** + * + * The mapping effect receives the current state and chunk, then returns the + * next state plus the values to emit. The state is threaded across chunks. + * + * **Example** (Effectfully mapping stream chunks with state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const totals = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.rechunk(2), + * Stream.mapAccumArrayEffect(() => 0, (total, chunk) => + * Effect.gen(function*() { + * const next = chunk.reduce((sum, value) => sum + value, total) + * return [next, [next]] as const + * }) + * ), + * Stream.runCollect + * ) + * yield* Console.log(totals) + * }) + * + * Effect.runPromise(program) + * // Output: [ 3, 10 ] + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const mapAccumArrayEffect: { + ( + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): (self: Stream) => Stream + ( + self: Stream, + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } + ): Stream +} = dual((args) => isStream(args), ( + self: Stream, + initial: LazyArg, + f: (s: S, a: Arr.NonEmptyReadonlyArray) => Effect.Effect], E2, R2>, + options?: { + readonly onHalt?: ((state: S) => ReadonlyArray) | undefined + } +): Stream => + self.channel.pipe( + Channel.mapAccum( + initial, + (state, a) => + Effect.map( + f(state, a), + ([state, values]) => [ + state, + Arr.isReadonlyArrayNonEmpty(values) ? Arr.of(values) : emptyArr + ] + ), + options?.onHalt ? + { + onHalt(state) { + const arr = options.onHalt!(state) + return Arr.isReadonlyArrayNonEmpty(arr) ? Arr.of(arr) : emptyArr + } + } : + undefined + ), + fromChannel + )) + +/** + * Accumulates state across the stream, emitting the initial state and each updated state. + * + * **Example** (Scanning stream state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.scan(0, (acc, n) => acc + n), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ 0, 1, 3, 6 ] + * ``` + * + * @category Accumulation + * @since 2.0.0 + */ +export const scan: { + ( + initial: S, + f: (s: S, a: A) => S + ): (self: Stream) => Stream + ( + self: Stream, + initial: S, + f: (s: S, a: A) => S + ): Stream +} = dual(3, ( + self: Stream, + initial: S, + f: (s: S, a: A) => S +): Stream => + suspend(() => { + let isFirst = true + return fromChannel(Channel.mapAccum(self.channel, constant(initial), (state, arr) => { + const states = Arr.empty() as Arr.NonEmptyArray + if (isFirst) { + isFirst = false + states.push(state) + } + for (let index = 0; index < arr.length; index++) { + state = f(state, arr[index]) + states.push(state) + } + return [state, Arr.of(states)] + })) + })) + +/** + * Accumulates state effectfully and emits the initial state plus each accumulated state. + * + * **Example** (Effectfully scanning stream state) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const states = yield* Stream.make(1, 2, 3).pipe( + * Stream.scanEffect(0, (sum, n) => Effect.succeed(sum + n)), + * Stream.runCollect + * ) + * yield* Console.log(states) + * // Output: [ 0, 1, 3, 6 ] + * }) + * ``` + * + * @category Accumulation + * @since 2.0.0 + */ +export const scanEffect: { + ( + initial: S, + f: (s: S, a: A) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + initial: S, + f: (s: S, a: A) => Effect.Effect + ): Stream +} = dual(3, ( + self: Stream, + initial: S, + f: (s: S, a: A) => Effect.Effect +): Stream => + self.channel.pipe( + Channel.flattenArray, + Channel.scanEffect(initial, f), + Channel.map(Arr.of), + fromChannel + )) + +/** + * Drops earlier elements within the debounce window and emits only the latest element after the pause. + * + * **Example** (Debouncing stream elements) + * + * ```ts + * import { Console, Duration, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.concat(Stream.fromEffect(Effect.sleep(Duration.millis(50)).pipe(Effect.as(4)))), + * Stream.concat(Stream.make(5)), + * Stream.debounce(Duration.millis(30)) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * // Output: [ 3, 5 ] + * }) + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const debounce: { + (duration: Duration.Input): (self: Stream) => Stream + (self: Stream, duration: Duration.Input): Stream +} = dual( + 2, + (self: Stream, duration: Duration.Input): Stream => + transformPull( + self, + Effect.fnUntraced(function*(pull, scope) { + const clock = yield* Clock + const durationMs = Duration.toMillis(Duration.fromInputUnsafe(duration)) + let lastArr: Arr.NonEmptyReadonlyArray | undefined + let cause: Cause.Cause | undefined + let emitAtMs = Infinity + const pullLatch = Latch.makeUnsafe() + const emitLatch = Latch.makeUnsafe() + const endLatch = Latch.makeUnsafe() + + yield* pull.pipe( + pullLatch.whenOpen, + Effect.flatMap((arr) => { + emitLatch.openUnsafe() + lastArr = arr + emitAtMs = clock.currentTimeMillisUnsafe() + durationMs + return Effect.void + }), + Effect.forever({ disableYield: true }), + Effect.onError((cause_) => { + cause = cause_ + emitAtMs = clock.currentTimeMillisUnsafe() + emitLatch.openUnsafe() + endLatch.openUnsafe() + return Effect.void + }), + Effect.forkIn(scope) + ) + + const sleepLoop = Effect.suspend(function loop(): Pull.Pull, E, void, R> { + const now = clock.currentTimeMillisUnsafe() + const timeMs = emitAtMs < now ? durationMs : Math.min(durationMs, emitAtMs - now) + return Effect.flatMap(Effect.raceFirst(Effect.sleep(timeMs), endLatch.await), () => { + const now = clock.currentTimeMillisUnsafe() + if (now < emitAtMs) { + return loop() + } else if (lastArr) { + emitLatch.closeUnsafe() + pullLatch.closeUnsafe() + const eff = Effect.succeed(Arr.of(Arr.lastNonEmpty(lastArr))) + lastArr = undefined + return eff + } else if (cause) { + return Effect.failCause(cause!) + } + return loop() + }) + }) + + return Effect.suspend(() => { + if (cause) { + if (lastArr) { + const eff = Effect.succeed(Arr.of(Arr.lastNonEmpty(lastArr))) + lastArr = undefined + return eff + } + return Effect.failCause(cause) + } + pullLatch.openUnsafe() + return emitLatch.whenOpen(sleepLoop) + }) + }) + ) +) + +/** + * Rate-limits stream chunks with an effectful cost function. + * + * **When to use** + * + * Use to throttle chunks when computing each chunk's cost requires an effect. + * + * **Details** + * + * Uses a token bucket. The bucket can accumulate up to `units + burst` tokens, + * and each chunk consumes the cost returned by the effectful `cost` function. + * + * If using the "enforce" strategy, arrays that do not meet the bandwidth + * constraints are dropped. If using the "shape" strategy, arrays are delayed + * until they can be emitted without exceeding the bandwidth constraints. + * + * Defaults to the "shape" strategy. + * + * **Example** (Throttling stream chunks effectfully) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( + * Stream.take(6), + * Stream.throttleEffect({ + * cost: (arr) => Effect.succeed(arr.length), + * units: 1, + * duration: "100 millis", + * strategy: "shape" + * }) + * ) + * + * Effect.runPromise(Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * })) + * // Output: [0, 1, 2, 3, 4, 5] + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const throttleEffect: { + (options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => Effect.Effect + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + }): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => Effect.Effect + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream +} = dual( + 2, + ( + self: Stream, + options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => Effect.Effect + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream => { + const burst = options.burst ?? 0 + if (options.strategy === "enforce") { + return throttleEnforceEffect(self, options.cost, options.units, options.duration, burst) + } + return throttleShapeEffect(self, options.cost, options.units, options.duration, burst) + } +) + +const throttleEnforceEffect = ( + self: Stream, + cost: (arr: Arr.NonEmptyReadonlyArray) => Effect.Effect, + units: number, + duration: Duration.Input, + burst: number +): Stream => + transformPull(self, (pull) => + Effect.clockWith((clock) => { + const durationMs = Duration.toMillis(Duration.fromInputUnsafe(duration)) + const max = units + burst < 0 ? Number.POSITIVE_INFINITY : units + burst + let tokens = units + let timestampMs = clock.currentTimeMillisUnsafe() + + return Effect.succeed( + Effect.flatMap(pull, function loop(arr): Pull.Pull, E | E2, void, R | R2> { + return Effect.flatMap(cost(arr), (weight) => { + const currentMs = clock.currentTimeMillisUnsafe() + const elapsed = currentMs - timestampMs + const cycles = elapsed / durationMs + const sum = tokens + (cycles * units) + const available = sum < 0 ? max : Math.min(sum, max) + + if (weight <= available) { + tokens = available - weight + timestampMs = currentMs + return Effect.succeed(arr) + } + + // Drop the array and continue + return Effect.flatMap(pull, loop) + }) + }) + ) + })) + +const throttleShapeEffect = ( + self: Stream, + cost: (arr: Arr.NonEmptyReadonlyArray) => Effect.Effect, + units: number, + duration: Duration.Input, + burst: number +): Stream => + transformPull(self, (pull) => + Effect.clockWith((clock) => { + const durationMs = Duration.toMillis(Duration.fromInputUnsafe(duration)) + const max = units + burst < 0 ? Number.POSITIVE_INFINITY : units + burst + let tokens = units + let timestampMs = clock.currentTimeMillisUnsafe() + + return Effect.succeed(Effect.flatMap(pull, (arr) => + Effect.flatMap(cost(arr), (weight) => { + const currentMs = clock.currentTimeMillisUnsafe() + const elapsed = currentMs - timestampMs + const cycles = elapsed / durationMs + const sum = tokens + (cycles * units) + const available = sum < 0 ? max : Math.min(sum, max) + const remaining = available - weight + + if (remaining >= 0) { + tokens = remaining + timestampMs = currentMs + return Effect.succeed(arr) + } + + // Calculate delay needed + const waitCycles = -remaining / units + const delayMs = Math.max(0, waitCycles * durationMs) + + if (delayMs > 0) { + return Effect.flatMap(Effect.sleep(delayMs), () => { + tokens = remaining + timestampMs = currentMs + return Effect.succeed(arr) + }) + } + + tokens = remaining + timestampMs = currentMs + return Effect.succeed(arr) + }))) + })) + +/** + * Rate-limits stream chunks with a synchronous cost function. + * + * **When to use** + * + * Use to throttle chunks when each chunk's cost can be computed synchronously. + * + * **Details** + * + * Uses a token bucket. The bucket can accumulate up to `units + burst` tokens, + * and each chunk consumes the cost returned by `cost`. + * + * If using the "enforce" strategy, arrays that do not meet the bandwidth + * constraints are dropped. If using the "shape" strategy, arrays are delayed + * until they can be emitted without exceeding the bandwidth constraints. + * + * Defaults to the "shape" strategy. + * + * **Example** (Throttling stream chunks) + * + * ```ts + * import { Console, Effect, Schedule, Stream } from "effect" + * + * const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( + * Stream.take(6), + * Stream.throttle({ + * cost: (arr) => arr.length, + * units: 1, + * duration: "100 millis", + * strategy: "shape" + * }) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * // Output: [ 0, 1, 2, 3, 4, 5 ] + * }) + * ``` + * + * @category rate limiting + * @since 2.0.0 + */ +export const throttle: { + (options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => number + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + }): (self: Stream) => Stream + ( + self: Stream, + options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => number + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream +} = dual( + 2, + ( + self: Stream, + options: { + readonly cost: (arr: Arr.NonEmptyReadonlyArray) => number + readonly units: number + readonly duration: Duration.Input + readonly burst?: number | undefined + readonly strategy?: "enforce" | "shape" | undefined + } + ): Stream => + throttleEffect(self, { + ...options, + cost: (arr) => Effect.succeed(options.cost(arr)) + }) +) + +/** + * Partitions the stream into non-empty arrays of the specified size. + * + * **Details** + * + * The final array may be smaller if there are not enough elements to fill it. + * + * **Example** (Grouping elements by size) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const grouped = yield* Stream.range(1, 8).pipe( + * Stream.grouped(3), + * Stream.runCollect + * ) + * yield* Console.log(grouped) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8 ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const grouped: { + (n: number): (self: Stream) => Stream, E, R> + (self: Stream, n: number): Stream, E, R> +} = dual( + 2, + (self: Stream, n: number): Stream, E, R> => chunks(rechunk(self, n)) +) + +/** + * Partitions the stream into arrays, emitting when the chunk size is reached + * or the duration passes. + * + * **Example** (Grouping elements by size or time) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.groupedWithin(2, "5 seconds"), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ 1, 2 ], [ 3 ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupedWithin: { + ( + chunkSize: number, + duration: Duration.Input + ): (self: Stream) => Stream, E, R> + (self: Stream, chunkSize: number, duration: Duration.Input): Stream, E, R> +} = dual(3, ( + self: Stream, + chunkSize: number, + duration: Duration.Input +): Stream, E, R> => + aggregateWithin( + self, + Sink.take(chunkSize), + Schedule.spaced(duration) + )) + +/** + * Groups elements into keyed substreams using an effectful classifier. + * + * **Example** (Grouping elements into keyed substreams using an effectful classifier) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const grouped = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.groupBy((n) => + * Effect.succeed([n % 2 === 0 ? "even" : "odd", n] as const) + * ), + * Stream.mapEffect( + * Effect.fnUntraced(function*([key, stream]) { + * return [key, yield* Stream.runCollect(stream)] as const + * }), + * { concurrency: "unbounded" } + * ), + * Stream.runCollect + * ) + * + * yield* Console.log(grouped) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ "odd", [ 1, 3, 5 ] ], [ "even", [ 2, 4 ] ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupBy: { + ( + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): (self: Stream) => Stream], E | E2, R | R2> + ( + self: Stream, + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): Stream], E | E2, R | R2> +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: NoInfer) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } +): Stream], E | E2, R | R2> => + groupByImpl( + self, + Effect.fnUntraced(function*(arr, queues, queueMap) { + for (let i = 0; i < arr.length; i++) { + const [key, value] = yield* f(arr[i]) + const oentry = MutableHashMap.get(queueMap, key) + const queue = Option.isSome(oentry) + ? oentry.value + : yield* Effect.scoped(RcMap.get(queues, key)) + yield* RcMap.touch(queues, key) + yield* Queue.offer(queue, value) + } + }), + options + )) + +/** + * Groups elements by a key and emits a stream per key. + * + * **Example** (Grouping elements by key) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const grouped = yield* Stream.make(1, 2, 3, 4, 5).pipe( + * Stream.groupByKey((n) => n % 2 === 0 ? "even" : "odd"), + * Stream.mapEffect( + * ([key, stream]) => + * Stream.runCollect(stream).pipe( + * Effect.map((values) => [key, values] as const) + * ), + * { concurrency: "unbounded" } + * ), + * Stream.runCollect + * ) + * yield* Console.log(grouped) + * }) + * + * Effect.runPromise(program) + * // Output: [ [ "odd", [ 1, 3, 5 ] ], [ "even", [ 2, 4 ] ] ] + * ``` + * + * @category grouping + * @since 2.0.0 + */ +export const groupByKey: { + ( + f: (a: NoInfer) => K, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): (self: Stream) => Stream], E, R> + ( + self: Stream, + f: (a: NoInfer) => K, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): Stream], E, R> +} = dual((args) => isStream(args[0]), ( + self: Stream, + f: (a: NoInfer) => K, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } +): Stream], E, R> => + suspend(() => { + const batch = MutableHashMap.empty>() + return groupByImpl( + self, + Effect.fnUntraced(function*(arr, queues, queueMap) { + for (let i = 0; i < arr.length; i++) { + const key = f(arr[i]) + const ovalues = MutableHashMap.get(batch, key) + if (Option.isNone(ovalues)) { + MutableHashMap.set(batch, key, [arr[i]]) + } else { + ovalues.value.push(arr[i]) + } + } + for (const [key, values] of batch) { + const oentry = MutableHashMap.get(queueMap, key) + const queue = Option.isSome(oentry) + ? oentry.value + : yield* Effect.scoped(RcMap.get(queues, key)) + yield* RcMap.touch(queues, key) + yield* Queue.offerAll(queue, values) + } + MutableHashMap.clear(batch) + }), + options + ) + })) + +const groupByImpl = ( + self: Stream, + f: ( + arr: Arr.NonEmptyReadonlyArray, + queues: RcMap.RcMap>, + queueMap: MutableHashMap.MutableHashMap> + ) => Effect.Effect, + options?: { + readonly bufferSize?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } +): Stream], E | E2, R | R2> => + transformPullBracket( + self, + Effect.fnUntraced(function*(pull, scope, forkedScope) { + const out = yield* Queue.unbounded], E | E2 | Cause.Done>() + yield* Scope.addFinalizer(scope, Queue.shutdown(out)) + + const queueMap = MutableHashMap.empty>() + const queues = yield* RcMap.make({ + lookup: (key: K) => + Effect.acquireRelease( + Queue.make({ capacity: options?.bufferSize ?? 4096 }).pipe( + Effect.tap((queue) => { + MutableHashMap.set(queueMap, key, queue) + return Queue.offer(out, [key, fromQueue(queue)]) + }) + ), + (queue) => { + MutableHashMap.remove(queueMap, key) + return Queue.end(queue) + } + ), + idleTimeToLive: options?.idleTimeToLive ?? Duration.infinity + }).pipe(Scope.provide(forkedScope)) + + yield* Effect.whileLoop({ + while: constTrue, + body: constant(Effect.flatMap(pull, (arr) => f(arr, queues, queueMap))), + step: constVoid + }).pipe( + Effect.catchCause((cause) => Queue.failCause(out, cause)), + Effect.forkIn(scope) + ) + + return Queue.takeAll(out) + }) + ) + +/** + * Groups consecutive elements that have equal keys into non-empty arrays. + * + * **When to use** + * + * Use when you already have a stream ordered by the grouping key and want to + * emit each consecutive run as a non-empty array while keeping later + * non-adjacent runs separate. + * + * **Details** + * + * The key is computed with `f`; adjacent elements whose keys are equal by + * `Equal.equals` are emitted as one `[key, group]`. Later non-adjacent runs + * with the same key are emitted separately. + * + * @see {@link groupByKey} for grouping all elements with the same key across the stream + * @see {@link groupBy} for custom grouped stream construction + * + * @category grouping + * @since 2.0.0 + */ +export const groupAdjacentBy: { + ( + f: (a: NoInfer) => K + ): (self: Stream) => Stream], E, R> + ( + self: Stream, + f: (a: NoInfer) => K + ): Stream], E, R> +} = dual(2, ( + self: Stream, + f: (a: NoInfer) => K +): Stream], E, R> => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let currentKey: K = undefined as any + let group: Arr.NonEmptyArray | undefined + let toEmit = Arr.empty]>() + const loop: Pull.Pull< + Arr.NonEmptyReadonlyArray]>, + E + > = pull.pipe( + Effect.flatMap((chunk) => { + for (let i = 0; i < chunk.length; i++) { + const item = chunk[i] + const key = f(item) + if (group === undefined) { + currentKey = key + group = [item] + continue + } else if (Equal.equals(key, currentKey)) { + group.push(item) + continue + } + toEmit.push([currentKey, group]) + currentKey = key + group = [item] + } + if (Arr.isArrayNonEmpty(toEmit)) { + const out = toEmit + toEmit = [] + return Effect.succeed(out) + } + return loop + }) + ) + let done = false + return Pull.catchDone(Effect.suspend(() => done ? Cause.done() : loop), () => { + done = true + const out = group + group = undefined + return out && Arr.isArrayNonEmpty(out) ? Effect.succeed(Arr.of([currentKey, out])) : Cause.done() + }) + }))) + +/** + * Applies a sink transducer to the stream and emits each sink result. + * + * **Example** (Transducing with a sink) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * const program = Effect.gen(function* () { + * const result = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.transduce(Sink.take(2)), + * Stream.runCollect + * ) + * + * yield* Console.log(result) + * // Output: [ [ 1, 2 ], [ 3, 4 ] ] + * }) + * ``` + * + * @category Aggregation + * @since 2.0.0 + */ +export const transduce = dual< + ( + sink: Sink.Sink + ) => (self: Stream) => Stream, + ( + self: Stream, + sink: Sink.Sink + ) => Stream +>( + 2, + ( + self: Stream, + sink: Sink.Sink + ): Stream => + transformPull(self, (upstream, scope) => + Effect.sync(() => { + let done: Exit.Exit | E> | undefined + let leftover: Arr.NonEmptyReadonlyArray | undefined + const upstreamWithLeftover = Effect.suspend(() => { + if (leftover !== undefined) { + const chunk = leftover + leftover = undefined + return Effect.succeed(chunk) + } + return upstream + }).pipe( + Effect.catch((error) => { + done = Exit.fail(error) + return Cause.done() + }) + ) + const pull = Effect.map( + Effect.suspend(() => sink.transform(upstreamWithLeftover, scope)), + ([value, leftover_]) => { + leftover = leftover_ + return Arr.of(value) + } + ) + return Effect.suspend((): Pull.Pull< + Arr.NonEmptyReadonlyArray, + E | E2, + void, + R2 + > => done ? done : pull) + })) +) + +/** + * Aggregates elements using the provided sink and emits each sink result as a stream element. + * + * **Details** + * + * The stream runs the upstream and downstream in separate fibers, so the sink can keep + * consuming input while downstream is busy processing the previous output. + * + * **Example** (Aggregating with a sink) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function* () { + * const aggregated = yield* Stream.runCollect( + * Stream.make(1, 2, 3, 4, 5, 6).pipe( + * Stream.aggregate( + * Sink.foldUntil(() => 0, 3, (sum, n) => Effect.succeed(sum + n)) + * ) + * ) + * ) + * yield* Console.log(aggregated) + * })) + * // [ 6, 15 ] + * ``` + * + * @category Aggregation + * @since 2.0.0 + */ +export const aggregate: { + ( + sink: Sink.Sink + ): (self: Stream) => Stream + ( + self: Stream, + sink: Sink.Sink + ): Stream +} = dual(2, ( + self: Stream, + sink: Sink.Sink +): Stream => aggregateWithin(self, sink, Schedule.forever)) + +/** + * Aggregates elements with a sink, emitting each result when the sink completes or the schedule triggers. + * + * **Details** + * + * The schedule can flush the current aggregation even if the sink has not finished. + * + * **Example** (Aggregating with a sink and schedule) + * + * ```ts + * import { Console, Effect, Schedule, Sink, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function* () { + * const aggregated = yield* Stream.runCollect( + * Stream.make(1, 2, 3, 4, 5, 6).pipe( + * Stream.aggregateWithin( + * Sink.foldUntil(() => 0, 3, (sum, n) => Effect.succeed(sum + n)), + * Schedule.spaced("1 minute") + * ) + * ) + * ) + * yield* Console.log(aggregated) + * })) + * // Output: [ 6, 15 ] + * ``` + * + * @category Aggregation + * @since 2.0.0 + */ +export const aggregateWithin: { + ( + sink: Sink.Sink, + schedule: Schedule.Schedule, E3, R3> + ): (self: Stream) => Stream + ( + self: Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, E3, R3> + ): Stream +} = dual(3, ( + self: Stream, + sink: Sink.Sink, + schedule: Schedule.Schedule, E3, R3> +): Stream => + fromChannel(Channel.fromTransformBracket(Effect.fnUntraced(function*(_upstream, _, scope) { + const pull = yield* Channel.toPullScoped(self.channel, _) + + const pullLatch = Latch.makeUnsafe(false) + const scheduleStep = Symbol() + const buffer = yield* Queue.make | typeof scheduleStep, E | Cause.Done>({ + capacity: 0 + }) + + // upstream -> buffer + yield* pull.pipe( + pullLatch.whenOpen, + Effect.flatMap((arr) => { + pullLatch.closeUnsafe() + return Queue.offer(buffer, arr) + }), + Effect.forever, // don't disable autoYield to prevent choking the schedule + Effect.catchCause((cause) => Queue.failCause(buffer, cause)), + Effect.forkIn(scope) + ) + + // schedule -> buffer + let lastOutput = Option.none() + let leftover: Arr.NonEmptyReadonlyArray | undefined + let sinkHasInput = false + const step = yield* Schedule.toStepWithSleep(schedule) + const stepToBuffer = Effect.suspend(function loop(): Pull.Pull { + return step(lastOutput).pipe( + Effect.flatMap(() => !sinkHasInput ? loop() : Queue.offer(buffer, scheduleStep)), + Effect.flatMap(() => Effect.never), + Pull.catchDone(() => Cause.done()) + ) + }) + + // buffer -> sink + const pullFromBuffer: Pull.Pull< + Arr.NonEmptyReadonlyArray, + E + > = Queue.take(buffer).pipe( + Effect.flatMap((arr) => { + if (arr === scheduleStep) { + return Cause.done() + } + sinkHasInput = true + return Effect.succeed(arr) + }) + ) + + const sinkUpstream = Effect.suspend((): Pull.Pull, E> => { + if (leftover !== undefined) { + const chunk = leftover + leftover = undefined + sinkHasInput = true + return Effect.succeed(chunk) + } + pullLatch.openUnsafe() + return pullFromBuffer + }) + const catchSinkHalt = Effect.flatMap(([value, leftover_]: Sink.End) => { + // ignore the last output if the upstream only pulled a halt + if (!sinkHasInput && buffer.state._tag === "Done") return Cause.done() + lastOutput = Option.some(value) + leftover = leftover_ + return Effect.succeed(Arr.of(value)) + }) + + return Effect.suspend(() => { + // if the buffer has exited and there is no more data to process + if (buffer.state._tag === "Done" && leftover === undefined) { + return buffer.state.exit as Exit.Exit | E> + } + sinkHasInput = leftover !== undefined + return Effect.succeed(Effect.suspend(() => sink.transform(sinkUpstream as any, scope))) + }).pipe( + Effect.flatMap((pull) => Effect.raceFirst(catchSinkHalt(pull), stepToBuffer)) + ) + })))) + +/** + * Creates a fixed-size tuple of streams that each emit + * the same elements as the source stream. + * + * **Details** + * + * The source stream starts after all downstream streams have been subscribed. + * With the default suspend strategy, the source can only advance `capacity` + * chunks ahead of the slowest downstream stream. If a downstream stream is + * interrupted, it unsubscribes from the broadcast so it no longer contributes + * backpressure. + * + * **Example** (Broadcasting to two consumers) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const [left, right] = yield* Stream.make(1, 2, 3).pipe( + * Stream.broadcastN({ n: 2, capacity: 8 }) + * ) + * + * const values = yield* Effect.all([ + * Stream.runCollect(left), + * Stream.runCollect(right) + * ], { concurrency: "unbounded" }) + * + * yield* Console.log(values) + * }) + * ) + * + * Effect.runPromise(program) + * // Output: [[1, 2, 3], [1, 2, 3]] + * ``` + * + * @category Broadcast + * @since 4.0.0 + */ +export const broadcastN: { + ( + options: { + readonly n: N + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly n: N + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect>, never, Scope.Scope | R> + ( + self: Stream, + options: { + readonly n: N + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly n: N + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, Scope.Scope | R> +} = dual( + 2, + Effect.fnUntraced(function*( + self: Stream, + options: { + readonly n: N + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly n: N + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) { + const pubsub = yield* makePubSub>(options) + const streams = new Array(options.n) + const parentScope = yield* Scope.Scope + for (let i = 0; i < options.n; i++) { + const scope = Scope.forkUnsafe(parentScope) + const subscription = yield* PubSub.subscribe(pubsub).pipe( + Effect.provideService(Scope.Scope, scope) + ) + streams[i] = Channel.fromEffectTake(PubSub.take(subscription)).pipe( + Channel.onExit((exit) => Scope.close(scope, exit)), + fromChannel + ) + } + yield* Channel.runForEach(self.channel, (value) => PubSub.publish(pubsub, value)).pipe( + Effect.onExit((exit) => PubSub.publish(pubsub, exit)), + Effect.forkScoped + ) + return streams as TupleOf> + }) +) + +const makePubSub = ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } +) => + Effect.acquireRelease( + options.capacity === "unbounded" + ? PubSub.unbounded(options) + : options.strategy === "dropping" + ? PubSub.dropping(options) + : options.strategy === "sliding" + ? PubSub.sliding(options) + : PubSub.bounded(options), + PubSub.shutdown + ) + +/** + * Creates a PubSub-backed stream that multicasts the source to all subscribers. + * + * **Details** + * + * The returned stream is scoped and uses the provided PubSub capacity and replay settings. + * + * **Example** (Broadcasting a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.scoped( + * Effect.gen(function* () { + * const broadcasted = yield* Stream.broadcast(Stream.fromArray([1, 2, 3]), { + * capacity: 8, + * replay: 3 + * }) + * + * const [left, right] = yield* Effect.all([ + * Stream.runCollect(broadcasted), + * Stream.runCollect(broadcasted) + * ], { concurrency: "unbounded" }) + * + * yield* Console.log([left, right]) + * }) + * ) + * + * Effect.runPromise(program) + * // Output: [[1, 2, 3], [1, 2, 3]] + * ``` + * + * @category Broadcast + * @since 2.0.0 + */ +export const broadcast: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect, never, Scope.Scope | R> + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect, never, Scope.Scope | R> +} = dual(2, ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect, never, Scope.Scope | R> => Effect.map(toPubSubTake(self, options), fromPubSubTake)) + +/** + * Returns a new Stream that multicasts the original stream, subscribing when the first consumer starts. + * + * **Details** + * + * The upstream continues running while there is at least one consumer and is finalized after the last one exits. + * If `idleTimeToLive` is set, the upstream is kept alive for that duration so a later subscriber can continue from + * the next element instead of restarting. + * + * **Example** (Sharing a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * Effect.runPromise( + * Effect.scoped( + * Effect.gen(function*() { + * const shared = yield* Stream.make(1, 2, 3).pipe( + * Stream.share({ capacity: 16 }) + * ) + * + * const first = yield* shared.pipe(Stream.take(1), Stream.runCollect) + * const second = yield* shared.pipe(Stream.take(1), Stream.runCollect) + * + * yield* Console.log([first, second]) + * }) + * ) + * ) + * // output: [[1], [1]] + * ``` + * + * @category Broadcast + * @since 3.8.0 + */ +export const share: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): (self: Stream) => Effect.Effect, never, Scope.Scope | R> + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } + ): Effect.Effect, never, Scope.Scope | R> +} = dual(2, ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.Input | undefined + } +): Effect.Effect, never, Scope.Scope | R> => + Effect.map( + RcRef.make({ + acquire: broadcast(self, options), + idleTimeToLive: options.idleTimeToLive + }), + (ref) => unwrap(RcRef.get(ref)) + )) + +/** + * Pipes this stream through a channel that consumes and emits chunked elements. + * + * **Details** + * + * The channel receives `NonEmptyReadonlyArray` chunks and can transform both the + * output elements and error type. + * + * **Example** (Piping through a channel) + * + * ```ts + * import { Array, Channel, Console, Effect, Stream } from "effect" + * + * type NumberChunk = readonly [number, ...Array] + * + * const doubleChunks = Channel.identity().pipe( + * Channel.map((chunk) => Array.map(chunk, (n) => n * 2)) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.fromArray([1, 2, 3]).pipe( + * Stream.rechunk(2), + * Stream.pipeThroughChannel(doubleChunks), + * Stream.runCollect + * ) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // => [2, 4, 6] + * ``` + * + * @category Pipe + * @since 2.0.0 + */ +export const pipeThroughChannel: { + ( + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> + ): (self: Stream) => Stream + ( + self: Stream, + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> + ): Stream +} = dual(2, ( + self: Stream, + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> +): Stream => fromChannel(Channel.pipeTo(self.channel, channel))) + +/** + * Pipes values through the provided channel while preserving this stream's + * failures alongside any channel failures. + * + * **Details** + * + * Upstream failures are not passed to the channel, so the resulting stream can + * fail with either the original stream error or the channel error. + * + * **Example** (Piping through a channel with failures) + * + * ```ts + * import { Array, Channel, Effect, Stream } from "effect" + * + * type NumberChunk = readonly [number, ...Array] + * + * const stringifyChunks = Channel.identity().pipe( + * Channel.map((chunk) => Array.map(chunk, String)) + * ) + * + * Effect.runPromise(Effect.gen(function*() { + * const result = yield* Stream.make(1, 2, 3).pipe( + * Stream.rechunk(2), + * Stream.pipeThroughChannelOrFail(stringifyChunks), + * Stream.runCollect + * ) + * + * yield* Effect.sync(() => console.log(result)) + * })) + * // [ "1", "2", "3" ] + * ``` + * + * @category Pipe + * @since 2.0.0 + */ +export const pipeThroughChannelOrFail: { + ( + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> + ): (self: Stream) => Stream + ( + self: Stream, + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> + ): Stream +} = dual(2, ( + self: Stream, + channel: Channel.Channel, E2, unknown, Arr.NonEmptyReadonlyArray, E, unknown, R2> +): Stream => fromChannel(Channel.pipeToOrFail(self.channel, channel))) + +/** + * Pipes the stream through `Sink.toChannel`, emitting only the sink leftovers. + * + * **Details** + * + * If the sink completes mid-chunk, the remaining elements become the output stream. + * + * **Example** (Piping through a sink) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const leftovers = yield* Stream.make(1, 2, 3, 4).pipe( + * Stream.pipeThrough(Sink.take(2)), + * Stream.runCollect + * ) + * + * yield* Console.log(leftovers) + * }) + * + * Effect.runPromise(program) + * //=> [ 3, 4 ] + * ``` + * + * @category Pipe + * @since 2.0.0 + */ +export const pipeThrough: { + (sink: Sink.Sink): (self: Stream) => Stream + (self: Stream, sink: Sink.Sink): Stream +} = dual( + 2, + (self: Stream, sink: Sink.Sink): Stream => + self.channel.pipe( + Channel.pipeToOrFail(Sink.toChannel(sink)), + Channel.concatWith(([_, leftover]) => leftover ? Channel.succeed(leftover) : Channel.empty), + fromChannel + ) +) + +/** + * Collects all elements into an array and emits it as a single element. + * + * **Example** (Collecting values into a stream element) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const program = Effect.gen(function*() { + * const collected = yield* stream.pipe(Stream.collect, Stream.runCollect) + * yield* Console.log(collected[0]) + * }) + * + * Effect.runPromise(program) + * // [1, 2, 3] + * ``` + * + * @category Accumulation + * @since 4.0.0 + */ +export const collect = (self: Stream): Stream, E, R> => fromEffect(runCollect(self)) + +/** + * Accumulates elements into a growing array, emitting the cumulative array for each input chunk. + * + * **Example** (Accumulating stream elements) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const accumulated = yield* Stream.runCollect( + * Stream.fromArray([1, 2, 3]).pipe( + * Stream.rechunk(1), + * Stream.accumulate + * ) + * ) + * yield* Console.log(accumulated) + * }) + * + * Effect.runPromise(program) + * //=> { _id: 'Chunk', values: [ [ 1 ], [ 1, 2 ], [ 1, 2, 3 ] ] } + * ``` + * + * @category Accumulation + * @since 2.0.0 + */ +export const accumulate = (self: Stream): Stream, E, R> => + mapAccumArray(self, Arr.empty, (acc, as) => { + const combined = Arr.appendAll(acc, as) + return [combined, [combined]] + }) + +/** + * Emits only elements that differ from the previous one. + * + * **Example** (Emitting changed values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.fromIterable([1, 1, 2, 2, 3]).pipe( + * Stream.changes, + * Stream.runCollect + * ) + * + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // [1, 2, 3] + * ``` + * + * @category Deduplication + * @since 2.0.0 + */ +export const changes = (self: Stream): Stream => changesWith(self, Equal.equals) + +/** + * Returns a stream that only emits elements that are not equal to the previously emitted element, as determined by the specified predicate. + * + * **Example** (Emitting values that changed by equivalence) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make("A", "a", "B", "b", "b").pipe( + * Stream.changesWith((left, right) => left.toLowerCase() === right.toLowerCase()) + * ) + * + * Effect.runPromise( + * Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * ) + * // ["A", "B"] + * ``` + * + * @category Deduplication + * @since 2.0.0 + */ +export const changesWith: { + (f: (x: A, y: A) => boolean): (self: Stream) => Stream + (self: Stream, f: (x: A, y: A) => boolean): Stream +} = dual( + 2, + (self: Stream, f: (x: A, y: A) => boolean): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let first = true + let last: A + return Effect.flatMap(pull, function loop(arr): Pull.Pull, E> { + const out: Array = [] + let i = 0 + if (first) { + first = false + last = arr[0] + i = 1 + out.push(last) + } + for (; i < arr.length; i++) { + const a = arr[i] + if (f(a, last)) continue + last = a + out.push(a) + } + return Arr.isArrayNonEmpty(out) ? Effect.succeed(out) : Effect.flatMap(pull, loop) + }) + })) +) + +/** + * Emits only elements that differ from the previous element, using an effectful equality check. + * + * **Details** + * + * The predicate runs for each element after the first; returning `true` treats it as equal and skips it. + * + * **Example** (Effectfully emitting changed values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 1, 2, 2, 3, 3).pipe( + * Stream.changesWithEffect((a, b) => Effect.succeed(a === b)) + * ) + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // { _id: "Chunk", values: [ 1, 2, 3 ] } + * ``` + * + * @category Deduplication + * @since 2.0.0 + */ +export const changesWithEffect: { + ( + f: (x: A, y: A) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + f: (x: A, y: A) => Effect.Effect + ): Stream +} = dual( + 2, + ( + self: Stream, + f: (x: A, y: A) => Effect.Effect + ): Stream => + transformPull(self, (pull, _scope) => + Effect.sync(() => { + let first = true + let last: A + return Effect.flatMap( + pull, + Effect.fnUntraced(function* loop(arr): Generator< + Pull.Pull, + Arr.NonEmptyReadonlyArray, + any + > { + const out: Array = [] + let i = 0 + if (first) { + first = false + last = arr[0] + i = 1 + out.push(last) + } + for (; i < arr.length; i++) { + const a = arr[i] + if (yield* f(a, last)) continue + last = a + out.push(a) + } + return Arr.isArrayNonEmpty(out) ? out : yield* Effect.flatMap(pull, Effect.fnUntraced(loop)) + }) + ) + })) +) + +/** + * Decodes Uint8Array chunks into strings using TextDecoder with an optional encoding. + * + * **Example** (Decoding Uint8Array chunks into strings using TextDecoder with an optional encoding) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const encoder = new TextEncoder() + * const stream = Stream.make( + * encoder.encode("Hello"), + * encoder.encode(" World") + * ) + * + * const program = Effect.gen(function*() { + * const decoded = yield* stream.pipe( + * Stream.decodeText, + * Stream.runCollect + * ) + * yield* Console.log(decoded) + * }) + * + * Effect.runPromise(program) + * // ["Hello", " World"] + * ``` + * + * @category encoding + * @since 2.0.0 + */ +export const decodeText: < + Arg extends Stream | { + readonly encoding?: string | undefined + } | undefined = { + readonly encoding?: string | undefined + } +>( + streamOrOptions?: Arg, + options?: { + readonly encoding?: string | undefined + } | undefined +) => [Arg] extends [Stream] ? Stream + : (self: Stream) => Stream = dual( + (args) => isStream(args[0]), + (self: Stream, options?: { + readonly encoding?: string | undefined + }): Stream => + suspend(() => { + const decoder = new TextDecoder(options?.encoding) + return map(self, (chunk) => decoder.decode(chunk, { stream: true })) + }) + ) + +/** + * Encodes a stream of strings into UTF-8 `Uint8Array` chunks. + * + * **Example** (Encoding a stream of strings into UTF-8 Uint8Array chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make("Hello", " ", "World") + * const program = Effect.gen(function*() { + * const encoded = Stream.encodeText(stream) + * const chunks = yield* Stream.runCollect(encoded) + * const bytes = chunks.map((chunk) => [...chunk]) + * yield* Console.log(bytes) + * }) + * + * Effect.runPromise(program) + * // [[72, 101, 108, 108, 111], [32], [87, 111, 114, 108, 100]] + * ``` + * + * @category encoding + * @since 2.0.0 + */ +export const encodeText = (self: Stream): Stream => + suspend(() => { + const encoder = new TextEncoder() + return map(self, (chunk) => encoder.encode(chunk)) + }) + +/** + * Splits a stream of strings into lines, handling `\n`, `\r`, and `\r\n` delimiters across chunks. + * + * **Example** (Splitting streamed text into lines) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function* () { + * const lines = yield* Stream.runCollect( + * Stream.make("a\nb\r\n", "c\n").pipe(Stream.splitLines) + * ) + * yield* Console.log(lines) + * })) + * // ["a", "b", "c"] + * ``` + * + * @category encoding + * @since 2.0.0 + */ +export const splitLines = (self: Stream): Stream => + self.channel.pipe( + Channel.pipeTo(Channel.splitLines()), + fromChannel + ) + +/** + * Inserts the provided element between emitted elements. + * + * **Example** (Interspersing stream elements) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3, 4).pipe(Stream.intersperse(0)) + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // [1, 0, 2, 0, 3, 0, 4] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const intersperse: { + (element: A2): (self: Stream) => Stream + (self: Stream, element: A2): Stream +} = dual(2, (self: Stream, element: A2): Stream => + mapArray(self, (arr, i) => { + const out: Arr.NonEmptyArray = i === 0 ? [] as any : [element] + const lastIndex = arr.length - 1 + for (let j = 0; j < arr.length; j++) { + if (j === lastIndex) { + out.push(arr[j]) + } else { + out.push(arr[j], element) + } + } + return out + })) + +/** + * Adds a start value, middle value, and end value around stream elements. + * + * **Details** + * + * The start and end values are always emitted, even when the stream is empty. + * + * **Example** (Interspersing stream affixes) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make("a", "b", "c").pipe( + * Stream.intersperseAffixes({ start: "[", middle: ",", end: "]" }) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // [ "[", "a", ",", "b", ",", "c", "]" ] + * ``` + * + * @category sequencing + * @since 2.0.0 + */ +export const intersperseAffixes: { + ( + options: { readonly start: A2; readonly middle: A3; readonly end: A4 } + ): (self: Stream) => Stream + ( + self: Stream, + options: { readonly start: A2; readonly middle: A3; readonly end: A4 } + ): Stream +} = dual(2, ( + self: Stream, + options: { readonly start: A2; readonly middle: A3; readonly end: A4 } +): Stream => + succeed(options.start).pipe( + concat(intersperse(self, options.middle)), + concat(succeed(options.end)) + )) + +/** + * Interleaves this stream with the specified stream by alternating pulls from + * each stream; when one ends, the remaining values from the other stream are + * emitted. + * + * **Example** (Interleaving streams) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.interleave( + * Stream.make(2, 3), + * Stream.make(5, 6, 7) + * ) + * + * const program = Effect.gen(function*() { + * const collected = yield* Stream.runCollect(stream) + * yield* Console.log(collected) + * }) + * + * Effect.runPromise(program) + * // [2, 5, 3, 6, 7] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const interleave: { + (that: Stream): (self: Stream) => Stream + (self: Stream, that: Stream): Stream +} = dual( + 2, + (self: Stream, that: Stream): Stream => + interleaveWith( + self, + that, + fromIterable(Iterable.forever([true, false])) + ) +) + +/** + * Interleaves two streams deterministically by following a boolean decider stream. + * + * **Details** + * + * The decider controls how many elements are pulled; if one side ends, pulls for + * that side are ignored. + * + * **Example** (Interleaving two streams deterministically by following a boolean decider stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const left = Stream.make(1, 3, 5) + * const right = Stream.make(2, 4, 6) + * const decider = Stream.make(true, false, false, true, true) + * + * const values = yield* Stream.runCollect( + * Stream.interleaveWith(left, right, decider) + * ) + * + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // [ 1, 2, 4, 3, 5 ] + * ``` + * + * @category merging + * @since 2.0.0 + */ +export const interleaveWith: { + ( + that: Stream, + decider: Stream + ): (self: Stream) => Stream + ( + self: Stream, + that: Stream, + decider: Stream + ): Stream +} = dual(3, ( + self: Stream, + that: Stream, + decider: Stream +): Stream => + fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(upstream, scope) { + const pullDecider = yield* Channel.toTransform(Channel.flattenArray(decider.channel))(upstream, scope) + const retry = Symbol() + type retry = typeof retry + let leftDone = false + let rightDone = false + const pullLeft = (yield* Channel.toTransform(Channel.flattenArray(self.channel))( + upstream, + scope + )).pipe( + Pull.catchDone(() => { + leftDone = true + return Effect.succeed(retry) + }) + ) + const pullRight = (yield* Channel.toTransform(Channel.flattenArray(that.channel))( + upstream, + scope + )).pipe( + Pull.catchDone(() => { + rightDone = true + return Effect.succeed(retry) + }) + ) + + return Effect.gen(function*() { + while (true) { + if (leftDone && rightDone) { + return yield* Cause.done() + } + const side = yield* pullDecider + if (side && leftDone) continue + if (!side && rightDone) continue + const elem = yield* (side ? pullLeft : pullRight) + if (elem === retry) continue + return Arr.of(elem) + } + }) + })))) + +/** + * Interrupts the evaluation of this stream when the provided effect + * completes. The given effect will be forked as part of this stream, and its + * success will be discarded. This combinator will also interrupt any + * in-progress element being pulled from upstream. + * + * **Details** + * + * If the effect completes with a failure before the stream completes, the + * returned stream will emit that failure. + * + * **Example** (Interrupting when an effect completes) + * + * ```ts + * import { Console, Deferred, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const interrupt = yield* Deferred.make() + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.tap((value) => + * value === 2 + * ? Deferred.succeed(interrupt, void 0) + * : Effect.void + * ), + * Stream.interruptWhen(Deferred.await(interrupt)) + * ) + * + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // => [1, 2] + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const interruptWhen: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = dual( + 2, + (self: Stream, effect: Effect.Effect): Stream => + fromChannel(Channel.interruptWhen(self.channel, effect)) +) + +/** + * Stops a stream after the current element when an effect completes. + * + * **When to use** + * + * Use to stop before the next pull after an external signal completes. + * + * **Details** + * + * The effect is forked, its success value is discarded, and its failure fails + * the stream. + * + * **Gotchas** + * + * This does not interrupt an in-progress pull. Use {@link interruptWhen} when + * the stream should be interrupted immediately. + * + * **Example** (Halting a stream after an effect completes) + * + * ```ts + * import { Console, Deferred, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const halt = yield* Deferred.make() + * const values = yield* Stream.fromArray([1, 2, 3]).pipe( + * Stream.tap((value) => value === 2 ? Deferred.succeed(halt, void 0) : Effect.void), + * Stream.haltWhen(Deferred.await(halt)), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: + * // [1, 2] + * ``` + * + * @category interruption + * @since 2.0.0 + */ +export const haltWhen: { + (effect: Effect.Effect): (self: Stream) => Stream + (self: Stream, effect: Effect.Effect): Stream +} = dual( + 2, + (self: Stream, effect: Effect.Effect): Stream => + fromChannel(Channel.haltWhen(self.channel, effect)) +) + +/** + * Runs the provided finalizer when the stream exits, passing the exit value. + * + * **Example** (Running a finalizer on exit) + * + * ```ts + * import { Console, Effect, Exit, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.onExit((exit) => + * Exit.isSuccess(exit) + * ? Console.log("Stream completed successfully") + * : Console.log("Stream failed") + * ) + * ) + * + * Effect.runPromise(Effect.gen(function*() { + * yield* Stream.runCollect(stream) + * })) + * // Output: + * // Stream completed successfully + * ``` + * + * @category Finalization + * @since 4.0.0 + */ +export const onExit: { + ( + finalizer: (exit: Exit.Exit) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + finalizer: (exit: Exit.Exit) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + finalizer: (exit: Exit.Exit) => Effect.Effect +): Stream => fromChannel(Channel.onExit(self.channel, finalizer))) + +/** + * Runs the provided effect when the stream fails, passing the failure cause. + * + * **Gotchas** + * + * Note: Unlike `Effect.onError` there is no guarantee that the provided + * effect will not be interrupted. + * + * **Example** (Running an effect on errors) + * + * ```ts + * import { Cause, Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.concat(Stream.fail("boom")), + * Stream.onError((cause) => Console.log(`Stream failed: ${Cause.squash(cause)}`)) + * ) + * + * yield* Stream.runCollect(stream) + * }) + * + * Effect.runPromiseExit(program) + * // Output: + * // Stream failed: boom + * ``` + * + * @category error handling + * @since 2.0.0 + */ +export const onError: { + ( + cleanup: (cause: Cause.Cause) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + cleanup: (cause: Cause.Cause) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + cleanup: (cause: Cause.Cause) => Effect.Effect +): Stream => fromChannel(Channel.onError(self.channel, cleanup))) + +/** + * Runs the provided effect before this stream starts. + * + * **Example** (Running an effect on start) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.fromArray([1, 2, 3]).pipe( + * Stream.onStart(Console.log("Stream started")) + * ) + * + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Output: + * // Stream started + * // [1, 2, 3] + * ``` + * + * @category sequencing + * @since 3.6.0 + */ +export const onStart: { + ( + onStart: Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + onStart: Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + onStart: Effect.Effect +): Stream => fromChannel(Channel.onStart(self.channel, onStart))) + +/** + * Runs the provided effect with the first element emitted by the stream. + * + * **Example** (Running an effect on the first value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * Effect.runPromise(Effect.gen(function* () { + * yield* Stream.fromArray([1, 2, 3]).pipe( + * Stream.onFirst((value) => Console.log(`first=${value}`)), + * Stream.runDrain + * ) + * })) + * // Output: first=1 + * ``` + * + * @category sequencing + * @since 4.0.0 + */ +export const onFirst: { + ( + onFirst: (element: NoInfer) => Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + onFirst: (element: NoInfer) => Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + onFirst: (element: NoInfer) => Effect.Effect +): Stream => fromChannel(Channel.onFirst(self.channel, (arr) => onFirst(arr[0])))) + +/** + * Runs the provided effect when the stream ends successfully. + * + * **Example** (Running an effect on end) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.make(1, 2, 3).pipe( + * Stream.onEnd(Console.log("Stream ended")), + * Stream.runCollect + * ) + * yield* Console.log(values) + * }) + * + * Effect.runPromise(program) + * // Stream ended + * // [1, 2, 3] + * ``` + * + * @category sequencing + * @since 3.6.0 + */ +export const onEnd: { + ( + onEnd: Effect.Effect + ): (self: Stream) => Stream + ( + self: Stream, + onEnd: Effect.Effect + ): Stream +} = dual(2, ( + self: Stream, + onEnd: Effect.Effect +): Stream => fromChannel(Channel.onEnd(self.channel, onEnd))) + +/** + * Executes the provided finalizer after this stream's finalizers run. + * + * **Example** (Ensuring finalization) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.fromArray([1, 2]).pipe( + * Stream.ensuring(Effect.orDie(Console.log("cleanup"))) + * ) + * + * const program = Effect.gen(function*() { + * const collected = yield* Stream.runCollect(stream) + * yield* Console.log(collected) + * }) + * + * Effect.runPromise(program) + * //=> cleanup + * //=> [1, 2] + * ``` + * + * @category Finalization + * @since 2.0.0 + */ +export const ensuring: { + (finalizer: Effect.Effect): (self: Stream) => Stream + (self: Stream, finalizer: Effect.Effect): Stream +} = dual( + 2, + (self: Stream, finalizer: Effect.Effect): Stream => + fromChannel(Channel.ensuring(self.channel, finalizer)) +) + +/** + * Provides a layer or context to the stream, removing the corresponding + * service requirements. Use `options.local` to build the layer every time; by + * default, layers are shared between provide calls. + * + * **Example** (Providing stream requirements) + * + * ```ts + * import { Console, Context, Effect, Layer, Stream } from "effect" + * + * class Env extends Context.Service()("Env") {} + * + * const layer = Layer.succeed(Env)({ name: "Ada" }) + * + * const stream = Stream.fromEffect( + * Effect.gen(function*() { + * const env = yield* Effect.service(Env) + * return `Hello, ${env.name}` + * }) + * ) + * + * const withEnv = stream.pipe(Stream.provide(layer)) + * + * const program = Stream.runCollect(withEnv).pipe( + * Effect.flatMap((values) => Console.log(values)) + * ) + * + * Effect.runPromise(program) + * // Output: + * // ["Hello, Ada"] + * ``` + * + * @category services + * @since 4.0.0 + */ +export const provide: { + ( + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined + ): ( + self: Stream + ) => Stream | RL> + ( + self: Stream, + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined + ): Stream | RL> +} = dual((args) => isStream(args[0]), ( + self: Stream, + layer: Layer.Layer | Context.Context, + options?: { + readonly local?: boolean | undefined + } | undefined +): Stream | RL> => fromChannel(Channel.provide(self.channel, layer, options))) + +/** + * Provides multiple services to the stream using a context. + * + * **Example** (Providing multiple services to the stream using a context) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class Config extends Context.Service()("Config") {} + * class Greeter extends Context.Service string }>()("Greeter") {} + * + * const context = Context.make(Config, { prefix: "Hello" }).pipe( + * Context.add(Greeter, { greet: (name: string) => `${name}!` }) + * ) + * + * const stream = Stream.fromEffect( + * Effect.gen(function*() { + * const config = yield* Effect.service(Config) + * const greeter = yield* Effect.service(Greeter) + * return greeter.greet(config.prefix) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(Stream.provideContext(stream, context)) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // ["Hello!"] + * ``` + * + * @category services + * @since 2.0.0 + */ +export const provideContext: { + (context: Context.Context): (self: Stream) => Stream> + (self: Stream, context: Context.Context): Stream> +} = dual( + 2, + (self: Stream, context: Context.Context): Stream> => + fromChannel(Channel.provideContext(self.channel, context)) +) + +/** + * Provides the stream with a single required service, eliminating that + * requirement from its environment. + * + * **Example** (Providing a stream service) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class Greeter extends Context.Service string + * }>()("Greeter") {} + * + * const stream = Stream.fromEffect( + * Effect.service(Greeter).pipe( + * Effect.map((greeter) => greeter.greet("Ada")) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const collected = yield* Stream.runCollect( + * stream.pipe( + * Stream.provideService(Greeter, { + * greet: (name) => `Hello, ${name}` + * }) + * ) + * ) + * yield* Console.log(collected) + * }) + * + * Effect.runPromise(program) + * //=> ["Hello, Ada"] + * ``` + * + * @category services + * @since 2.0.0 + */ +export const provideService: { + ( + key: Context.Key, + service: NoInfer + ): ( + self: Stream + ) => Stream> + ( + self: Stream, + key: Context.Key, + service: NoInfer + ): Stream> +} = dual(3, ( + self: Stream, + key: Context.Key, + service: NoInfer +): Stream> => fromChannel(Channel.provideService(self.channel, key, service))) + +/** + * Provides a service to the stream using an effect, removing the requirement and adding the effect's error and environment. + * + * **Example** (Providing a stream service effectfully) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class ApiConfig extends Context.Service()("ApiConfig") {} + * + * const stream = Stream.fromEffect( + * Effect.gen(function*() { + * const config = yield* Effect.service(ApiConfig) + * return config.baseUrl + * }) + * ) + * + * const withConfig = stream.pipe( + * Stream.provideServiceEffect( + * ApiConfig, + * Effect.succeed({ baseUrl: "https://example.com" }).pipe( + * Effect.tap(() => Console.log("Loading config...")) + * ) + * ) + * ) + * + * const program = Stream.runCollect(withConfig).pipe( + * Effect.flatMap((values) => Console.log(values)) + * ) + * + * Effect.runPromise(program) + * // Output: + * // Loading config... + * // ["https://example.com"] + * ``` + * + * @category services + * @since 2.0.0 + */ +export const provideServiceEffect: { + ( + key: Context.Key, + service: Effect.Effect, ES, RS> + ): ( + self: Stream + ) => Stream | RS> + ( + self: Stream, + key: Context.Key, + service: Effect.Effect, ES, RS> + ): Stream | RS> +} = dual(3, ( + self: Stream, + key: Context.Key, + service: Effect.Effect, ES, RS> +): Stream | RS> => fromChannel(Channel.provideServiceEffect(self.channel, key, service))) + +/** + * Transforms the stream's required services by mapping the current context + * to a new one. + * + * **Example** (Updating the stream context) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class Logger extends Context.Service()("Logger") {} + * class Config extends Context.Service()("Config") {} + * + * const stream = Stream.fromEffect( + * Effect.gen(function*() { + * const logger = yield* Effect.service(Logger) + * const config = yield* Effect.service(Config) + * return `${logger.prefix}${config.name}` + * }) + * ) + * + * const updated = stream.pipe( + * Stream.updateContext((context: Context.Context) => + * Context.add(context, Config, { name: "World" }) + * ) + * ) + * + * const program = Effect.gen(function*() { + * const values = yield* Stream.runCollect(updated) + * yield* Console.log(values) + * }) + * + * Effect.runPromise( + * Effect.provideService(program, Logger, { prefix: "Hello " }) + * ) + * //=> [ "Hello World" ] + * ``` + * + * @category services + * @since 4.0.0 + */ +export const updateContext: { + ( + f: (context: Context.Context) => Context.Context + ): ( + self: Stream + ) => Stream + ( + self: Stream, + f: (context: Context.Context) => Context.Context + ): Stream +} = dual(2, ( + self: Stream, + f: (context: Context.Context) => Context.Context +): Stream => fromChannel(Channel.updateContext(self.channel, f))) + +/** + * Updates a single service in the stream environment by applying a function. + * + * **Example** (Updating a stream service) + * + * ```ts + * import { Console, Context, Effect, Stream } from "effect" + * + * class Counter extends Context.Service()("Counter") {} + * + * const stream = Stream.fromEffect(Effect.service(Counter)).pipe( + * Stream.updateService(Counter, (counter) => ({ count: counter.count + 1 })) + * ) + * + * const program = Effect.gen(function*() { + * const counters = yield* Stream.runCollect(stream) + * yield* Console.log(`Updated count: ${counters[0].count}`) + * }) + * + * Effect.runPromise(Effect.provideService(program, Counter, { count: 0 })) + * // Output: Updated count: 1 + * ``` + * + * @category services + * @since 2.0.0 + */ +export const updateService: { + ( + key: Context.Key, + f: (service: NoInfer) => S + ): ( + self: Stream + ) => Stream + ( + self: Stream, + key: Context.Key, + f: (service: NoInfer) => S + ): Stream +} = dual(3, ( + self: Stream, + service: Context.Key, + f: (service: NoInfer) => S +): Stream => + updateContext(self, (context) => + Context.add( + context, + service, + f(Context.get(context, service)) + ))) + +/** + * Wraps the stream with a new span for tracing. + * + * **Example** (Wrapping a stream in a span) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.fromArray([1, 2, 3]).pipe(Stream.withSpan("numbers")) + * + * Effect.runPromise( + * Effect.gen(function*() { + * const values = yield* Stream.runCollect(stream) + * yield* Console.log(values) + * }) + * ) + * // [1, 2, 3] + * ``` + * + * @category tracing + * @since 2.0.0 + */ +export const withSpan: { + (name: string, options?: SpanOptions): (self: Stream) => Stream> + (self: Stream, name: string, options?: SpanOptions): Stream> +} = function() { + const dataFirst = isStream(arguments[0]) + const name = dataFirst ? arguments[1] : arguments[0] + const options = addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] as Stream + return fromChannel(Channel.withSpan(self.channel, name, options)) + } + return (self: Stream) => fromChannel(Channel.withSpan(self.channel, name, options)) +} as any + +/** + * Provides the entry point for do-notation style stream composition. + * + * **Example** (Starting stream do notation) + * + * ```ts + * import { Console, Effect, pipe, Stream } from "effect" + * + * const program = pipe( + * Stream.Do, + * Stream.bind("value", () => Stream.fromArray([1, 2])), + * Stream.let("next", ({ value }) => value + 1) + * ) + * + * const effect = Effect.gen(function*() { + * const collected = yield* Stream.runCollect(program) + * yield* Console.log(collected) + * }) + * + * Effect.runPromise(effect) + * //=> [{ value: 1, next: 2 }, { value: 2, next: 3 }] + * ``` + * + * @category do notation + * @since 2.0.0 + */ +export const Do: Stream<{}> = succeed({}) + +const let_: { + ( + name: Exclude, + f: (a: NoInfer) => B + ): (self: Stream) => Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> + ( + self: Stream, + name: Exclude, + f: (a: NoInfer) => B + ): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> +} = dual(3, ( + self: Stream, + name: Exclude, + f: (a: NoInfer) => B +): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E, R> => + map(self, (a) => ({ ...a, [name]: f(a) } as any))) +export { + /** + * Adds a computed field to the current Do-notation record. + * + * **Example** (Adding a computed field) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.Do.pipe( + * Stream.let("x", () => 2), + * Stream.let("y", ({ x }) => x * 3) + * ) + * + * const program = Effect.gen(function*() { + * const records = yield* Stream.runCollect(stream) + * yield* Console.log(records) + * }) + * + * Effect.runPromise(program) + * // [{ x: 2, y: 6 }] + * ``` + * + * @category do notation + * @since 2.0.0 + */ + let_ as let +} + +/** + * Binds the result of a stream to a field in the do-notation record. + * + * **Example** (Binding a stream value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Stream.Do.pipe( + * Stream.bind("a", () => Stream.make(1, 2)), + * Stream.bind("b", ({ a }) => Stream.succeed(a + 1)) + * ) + * + * const result = Stream.runCollect(program) + * + * Effect.runPromise(Effect.flatMap(result, Console.log)) + * // [{ a: 1, b: 2 }, { a: 2, b: 3 }] + * ``` + * + * @category do notation + * @since 2.0.0 + */ +export const bind: { + ( + tag: Exclude, + f: (_: NoInfer) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): (self: Stream) => Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E2 | E, R2 | R> + ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined + ): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E | E2, R | R2> +} = dual((args) => isStream(args[0]), ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Stream, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + } | undefined +): Stream<{ [K in N | keyof A]: K extends keyof A ? A[K] : B }, E | E2, R | R2> => + flatMap(self, (a) => map(f(a), (b) => ({ ...a, [tag]: b } as any)), options)) + +/** + * Binds an Effect-produced value into the do-notation record for each stream element. + * + * **Example** (Binding an effect value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.Do.pipe( + * Stream.bind("value", () => Stream.make(1, 2)), + * Stream.bindEffect("double", ({ value }) => Effect.succeed(value * 2)) + * ) + * + * const program = Effect.gen(function*() { + * const result = yield* Stream.runCollect(stream) + * yield* Console.log(result) + * }) + * + * Effect.runPromise(program) + * // [{ value: 1, double: 2 }, { value: 2, double: 4 }] + * ``` + * + * @category do notation + * @since 2.0.0 + */ +export const bindEffect: { + ( + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly unordered?: boolean | undefined + } + ): (self: Stream) => Stream<{ [K in keyof A | N]: K extends keyof A ? A[K] : B }, E | E2, R | R2> + ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly unordered?: boolean | undefined + } + ): Stream<{ [K in keyof A | N]: K extends keyof A ? A[K] : B }, E | E2, R | R2> +} = dual((args) => isStream(args[0]), ( + self: Stream, + tag: Exclude, + f: (_: NoInfer) => Effect.Effect, + options?: { + readonly concurrency?: number | "unbounded" | undefined + readonly bufferSize?: number | undefined + readonly unordered?: boolean | undefined + } | undefined +): Stream<{ [K in keyof A | N]: K extends keyof A ? A[K] : B }, E | E2, R | R2> => + mapEffect(self, (a) => Effect.map(f(a), (b) => ({ ...a, [tag]: b } as any)), options)) + +/** + * Maps each element into a record keyed by the provided name. + * + * **Example** (Binding values to a record key) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3).pipe(Stream.bindTo("value")) + * + * const program = Stream.runCollect(stream).pipe(Effect.flatMap(Console.log)) + * + * Effect.runPromise(program) + * // [{ value: 1 }, { value: 2 }, { value: 3 }] + * ``` + * + * @category do notation + * @since 2.0.0 + */ +export const bindTo: { + (name: N): (self: Stream) => Stream<{ [K in N]: A }, E, R> + (self: Stream, name: N): Stream<{ [K in N]: A }, E, R> +} = dual(2, ( + self: Stream, + name: N +): Stream<{ [K in N]: A }, E, R> => map(self, (a) => ({ [name]: a } as any))) + +/** + * Runs a stream with a sink and returns the sink result. + * + * **Example** (Running a stream with a sink) + * + * ```ts + * import { Console, Effect, Sink, Stream } from "effect" + * + * const program = Stream.run(Stream.make(1, 2, 3), Sink.sum) + * + * Effect.runPromise(Effect.flatMap(program, Console.log)) + * // 6 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const run: { + ( + sink: Sink.Sink + ): (self: Stream) => Effect.Effect + ( + self: Stream, + sink: Sink.Sink + ): Effect.Effect +} = dual(2, ( + self: Stream, + sink: Sink.Sink +): Effect.Effect => + Effect.scopedWith((scope) => + Channel.toPullScoped(self.channel, scope).pipe( + Effect.flatMap((upstream) => sink.transform(upstream as any, scope)), + Effect.map(([a]) => a) + ) + )) + +/** + * Runs the stream and collects all elements into an array. + * + * **Example** (Collecting stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * + * const program = Effect.gen(function*() { + * const collected = yield* Stream.runCollect(stream) + * yield* Console.log(collected) + * }) + * + * Effect.runPromise(program) + * // [1, 2, 3, 4, 5] + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runCollect = (self: Stream): Effect.Effect, E, R> => + Channel.runFold( + self.channel, + () => [] as Array, + (acc, chunk) => { + for (let i = 0; i < chunk.length; i++) { + acc.push(chunk[i]) + } + return acc + } + ) + +/** + * Runs the stream and returns the number of elements emitted. + * + * **Example** (Counting stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * + * const program = Effect.gen(function* () { + * const count = yield* Stream.runCount(stream) + * yield* Console.log(count) + * }) + * + * Effect.runPromise(program) + * // 5 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runCount = (self: Stream): Effect.Effect => + Channel.runFold(self.channel, () => 0, (acc, chunk) => acc + chunk.length) + +/** + * Runs the stream and returns the numeric sum of its elements. + * + * **Example** (Summing stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const total = yield* Stream.runSum(Stream.make(1, 2, 3)) + * yield* Console.log(total) + * }) + * + * Effect.runPromise(program) + * // 6 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runSum = (self: Stream): Effect.Effect => + Channel.runFold(self.channel, () => 0, (acc, chunk) => { + for (let i = 0; i < chunk.length; i++) { + acc += chunk[i] + } + return acc + }) + +/** + * Runs the stream and folds elements using a pure reducer. + * + * **Example** (Folding stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const total = yield* Stream.runFold( + * Stream.make(1, 2, 3), + * () => 0, + * (acc, n) => acc + n + * ) + * yield* Console.log(total) + * }) + * + * Effect.runPromise(program) + * // 6 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runFold: { + ( + initial: LazyArg, + f: (acc: Z, a: A) => Z + ): ( + self: Stream + ) => Effect.Effect + ( + self: Stream, + initial: LazyArg, + f: (acc: Z, a: A) => Z + ): Effect.Effect +} = dual(3, ( + self: Stream, + initial: LazyArg, + f: (acc: Z, a: A) => Z +): Effect.Effect => + Channel.runFold(self.channel, initial, (acc, arr) => { + for (let i = 0; i < arr.length; i++) { + acc = f(acc, arr[i]) + } + return acc + })) + +/** + * Runs the stream and folds elements using an effectful reducer. + * + * **When to use** + * + * Use when reducing stream elements needs Effects, services, or failures in the + * reducer. + * + * **Example** (Effectfully folding stream values) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const total = yield* Stream.runFoldEffect( + * Stream.make(1, 2, 3), + * () => 0, + * (acc, n) => Effect.succeed(acc + n) + * ) + * yield* Console.log(total) + * }) + * + * Effect.runPromise(program) + * // 6 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runFoldEffect: { + ( + initial: LazyArg, + f: (acc: Z, a: A) => Effect.Effect + ): ( + self: Stream + ) => Effect.Effect + ( + self: Stream, + initial: LazyArg, + f: (acc: Z, a: A) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Stream, + initial: LazyArg, + f: (acc: Z, a: A) => Effect.Effect +): Effect.Effect => + Channel.runFoldEffect(self.channel, initial, (acc, arr) => { + let i = 0 + let s = acc + return Effect.map( + Effect.whileLoop({ + while: () => i < arr.length, + body: () => f(s, arr[i]), + step(z) { + s = z + i++ + } + }), + () => s + ) + })) + +/** + * Runs the stream and returns the first element as an `Option`. + * + * **Example** (Getting the first stream value) + * + * ```ts + * import { Console, Effect, Option, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const head = yield* Stream.runHead(Stream.make(1, 2, 3)) + * yield* Console.log(Option.getOrThrow(head)) + * }) + * + * Effect.runPromise(program) + * // 1 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runHead = (self: Stream): Effect.Effect, E, R> => + Effect.map(Channel.runHead(self.channel), Option.map(Arr.getUnsafe(0))) + +/** + * Runs the stream and returns the last element as an `Option`. + * + * **When to use** + * + * Use to consume a finite stream when only the final emitted element matters. + * + * **Details** + * + * `Option.some` contains the last emitted element. `Option.none` means the + * stream completed without emitting. + * + * **Gotchas** + * + * The returned effect waits for the stream to complete before it can produce a + * value. + * + * @see {@link runHead} for consuming only the first emitted element + * @see {@link runCollect} for collecting every emitted element + * @see {@link runDrain} for consuming the stream while discarding emitted elements + * + * @category destructors + * @since 2.0.0 + */ +export const runLast = (self: Stream): Effect.Effect, E, R> => + Effect.map(Channel.runLast(self.channel), Option.map(Arr.lastNonEmpty)) + +/** + * Runs the provided effectful callback for each element of the stream. + * + * **Example** (Running an effect for each value) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const program = Effect.gen(function*() { + * yield* Stream.runForEach(stream, (n) => Console.log(`Processing: ${n}`)) + * }) + * + * Effect.runPromise(program) + * // Processing: 1 + * // Processing: 2 + * // Processing: 3 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runForEach: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => + Channel.runForEach(self.channel, (arr) => { + let i = 0 + return Effect.whileLoop({ + while: () => i < arr.length, + body: () => f(arr[i++]), + step: constVoid + }) + })) + +/** + * Runs the stream, applying the effectful predicate to each element and + * stopping when it returns `false`. + * + * **Example** (Running effects while a predicate holds) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3, 4, 5) + * + * yield* Stream.runForEachWhile(stream, (n) => + * Effect.gen(function*() { + * yield* Console.log(`Processing: ${n}`) + * return n < 3 + * }) + * ) + * }) + * + * Effect.runPromise(program) + * // Processing: 1 + * // Processing: 2 + * // Processing: 3 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runForEachWhile: { + ( + f: (a: A) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Stream, + f: (a: A) => Effect.Effect +): Effect.Effect => + Channel.runForEachWhile(self.channel, (arr) => { + let done = false + let i = 0 + return Effect.map( + Effect.whileLoop({ + while: () => !done && i < arr.length, + body: () => f(arr[i]), + step(b) { + i++ + if (!b) done = true + } + }), + () => !done + ) + })) + +/** + * Consumes the stream in chunks, passing each non-empty array to the callback. + * + * **Example** (Consuming stream chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * const program = Effect.gen(function*() { + * yield* Stream.runForEachArray( + * stream, + * (chunk) => Console.log(`Processing chunk: ${chunk.join(", ")}`) + * ) + * }) + * + * Effect.runPromise(program) + * // Processing chunk: 1, 2, 3, 4, 5 + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const runForEachArray: { + ( + f: (a: Arr.NonEmptyReadonlyArray) => Effect.Effect + ): (self: Stream) => Effect.Effect + ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: Stream, + f: (a: Arr.NonEmptyReadonlyArray) => Effect.Effect +): Effect.Effect => Channel.runForEach(self.channel, f)) + +/** + * Runs the stream for its effects, discarding emitted elements. + * + * **Example** (Draining a stream run) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const stream = Stream.make(1, 2, 3).pipe( + * Stream.mapEffect((n) => Console.log(`Processing: ${n}`)) + * ) + * + * yield* Stream.runDrain(stream) + * }) + * + * Effect.runPromise(program) + * // Processing: 1 + * // Processing: 2 + * // Processing: 3 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runDrain = (self: Stream): Effect.Effect => Channel.runDrain(self.channel) + +/** + * Returns a scoped pull for manually consuming the stream's output chunks. + * + * **Details** + * + * The pull fails with `Cause.Done` when the stream ends and with the stream + * error on failure. + * + * **Example** (Creating a scoped pull) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const program = Effect.scoped( + * Effect.gen(function*() { + * const pull = yield* Stream.toPull(stream) + * const chunk = yield* pull + * yield* Console.log(chunk) + * }) + * ) + * + * Effect.runPromise(program) + * // [1, 2, 3] + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toPull = ( + self: Stream +): Effect.Effect, E>, never, R | Scope.Scope> => Channel.toPull(self.channel) + +/** + * Concatenates all emitted strings into a single string. + * + * **Example** (Joining strings from a stream) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make("Hello", " ", "World", "!") + * const program = Effect.gen(function*() { + * const text = yield* Stream.mkString(stream) + * yield* Console.log(text) + * }) + * + * Effect.runPromise(program) + * // Hello World! + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const mkString = (self: Stream): Effect.Effect => + Channel.runFold( + self.channel, + () => "", + (acc, chunk) => acc + chunk.join("") + ) + +/** + * Concatenates the stream's `Uint8Array` chunks into a single `Uint8Array`. + * + * **Example** (Joining Uint8Array chunks) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(new Uint8Array([1, 2]), new Uint8Array([3, 4])) + * const program = Effect.gen(function*() { + * const bytes = yield* Stream.mkUint8Array(stream) + * yield* Console.log([...bytes]) + * }) + * + * Effect.runPromise(program) + * // [1, 2, 3, 4] + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const mkUint8Array = (self: Stream): Effect.Effect => + Effect.map( + Channel.runFold( + self.channel, + (): { + bytes: number + readonly arrays: Array + } => ({ + bytes: 0, + arrays: [] + }), + (acc, chunk) => { + for (let i = 0; i < chunk.length; i++) { + acc.bytes += chunk[i].length + acc.arrays.push(chunk[i]) + } + return acc + } + ), + ({ arrays, bytes }) => { + const result = new Uint8Array(bytes) + let offset = 0 + for (let i = 0; i < arrays.length; i++) { + const array = arrays[i] + result.set(array, offset) + offset += array.length + } + return result + } + ) + +/** + * Converts the stream to a `ReadableStream` using the provided services. + * + * **When to use** + * + * Use when bridging to Web Streams and you already have the `Context` required + * to run the stream outside an `Effect`. + * + * **Details** + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * **Example** (Converting to a ReadableStream with services) + * + * ```ts + * import { Context, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * const readableStream = Stream.toReadableStreamWith(stream, Context.empty()) + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toReadableStreamWith = dual< + ( + context: Context.Context, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => (self: Stream) => ReadableStream, + ( + self: Stream, + context: Context.Context, + options?: { readonly strategy?: QueuingStrategy | undefined } + ) => ReadableStream +>( + (args) => isStream(args[0]), + ( + self: Stream, + context: Context.Context, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream => { + let currentResolve: (() => void) | undefined = undefined + let fiber: Fiber.Fiber | undefined = undefined + const latch = Latch.makeUnsafe(false) + + return new ReadableStream({ + start(controller) { + fiber = Effect.runFork(Effect.provideContext( + runForEachArray(self, (chunk) => + latch.whenOpen(Effect.sync(() => { + latch.closeUnsafe() + for (let i = 0; i < chunk.length; i++) { + controller.enqueue(chunk[i]) + } + currentResolve!() + currentResolve = undefined + }))), + context + )) + fiber.addObserver((exit) => { + if (exit._tag === "Failure") { + controller.error(Cause.squash(exit.cause)) + } else { + controller.close() + } + }) + }, + pull() { + return new Promise((resolve) => { + currentResolve = resolve + latch.openUnsafe() + }) + }, + cancel() { + if (!fiber) return + return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber))) + } + }, options?.strategy) + } +) + +/** + * Converts a stream to a `ReadableStream`. + * + * **Details** + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * **Example** (Converting a stream to a ReadableStream) + * + * ```ts + * import { Stream } from "effect" + * + * const readableStream = Stream.toReadableStream(Stream.make(1, 2, 3)) + * const reader = readableStream.getReader() + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toReadableStream: { + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ( + self: Stream + ) => ReadableStream + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream +} = dual( + (args) => isStream(args[0]), + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ReadableStream => toReadableStreamWith(self, Context.empty(), options) +) + +/** + * Creates an Effect that builds a ReadableStream from the stream. + * + * **When to use** + * + * Use when bridging to Web Streams from inside an `Effect` so the required + * services can be captured from the current context. + * + * **Details** + * + * See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream. + * + * **Example** (Creating a ReadableStream effect) + * + * ```ts + * import { Console, Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3, 4, 5) + * + * const effect = Effect.gen(function*() { + * const readableStream = yield* Stream.toReadableStreamEffect(stream) + * yield* Console.log(readableStream instanceof ReadableStream) // true + * }) + * + * Effect.runPromise(effect) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toReadableStreamEffect: { + ( + options?: { readonly strategy?: QueuingStrategy | undefined } + ): ( + self: Stream + ) => Effect.Effect, never, R> + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): Effect.Effect, never, R> +} = dual( + (args) => isStream(args[0]), + ( + self: Stream, + options?: { readonly strategy?: QueuingStrategy | undefined } + ): Effect.Effect, never, R> => + Effect.map( + Effect.context(), + (context) => toReadableStreamWith(self, context, options) + ) +) + +/** + * Converts the stream to an `AsyncIterable` using the provided services. + * + * **When to use** + * + * Use when converting outside an Effect and you already have the `Context` + * needed to run the stream. + * + * **Example** (Converting to an AsyncIterable with services) + * + * ```ts + * import { Context, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * const iterable = Stream.toAsyncIterableWith(stream, Context.empty()) + * + * const collect = async () => { + * const results: Array = [] + * for await (const value of iterable) { + * results.push(value) + * } + * console.log(results) + * } + * + * collect() + * // [ 1, 2, 3 ] + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toAsyncIterableWith: { + (context: Context.Context): (self: Stream) => AsyncIterable + ( + self: Stream, + context: Context.Context + ): AsyncIterable +} = dual( + 2, + ( + self: Stream, + context: Context.Context + ): AsyncIterable => ({ + [Symbol.asyncIterator]() { + const runPromise = Effect.runPromiseWith(context) + const runPromiseExit = Effect.runPromiseExitWith(context) + const scope = Scope.makeUnsafe() + let pull: Pull.Pull, E, void, R> | undefined + let currentIter: Iterator | undefined + return { + async next(): Promise> { + if (currentIter) { + const next = currentIter.next() + if (!next.done) return next + currentIter = undefined + } + pull ??= await runPromise(Channel.toPullScoped(self.channel, scope)) + const exit = await runPromiseExit(pull) + if (Exit.isSuccess(exit)) { + currentIter = exit.value[Symbol.iterator]() + return currentIter.next() + } else if (Pull.isDoneCause(exit.cause)) { + return { done: true, value: undefined } + } + throw Cause.squash(exit.cause) + }, + return(_) { + return runPromise(Effect.as( + Scope.close(scope, Exit.void), + { done: true, value: undefined } + )) + } + } + } + }) +) + +/** + * Creates an effect that yields an `AsyncIterable` using the current services. + * + * **When to use** + * + * Use when the `AsyncIterable` should be created inside Effect with the current + * context supplying the stream's services. + * + * **Example** (Creating an AsyncIterable effect) + * + * ```ts + * import { Effect, Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const program = Effect.gen(function*() { + * const iterable = yield* Stream.toAsyncIterableEffect(stream) + * const values = yield* Effect.promise(async () => { + * const collected: Array = [] + * for await (const value of iterable) { + * collected.push(value) + * } + * return collected + * }) + * yield* Effect.sync(() => console.log(values)) + * }) + * + * Effect.runPromise(program) + * // [ 1, 2, 3 ] + * ``` + * + * @category destructors + * @since 3.15.0 + */ +export const toAsyncIterableEffect = (self: Stream): Effect.Effect, never, R> => + Effect.map( + Effect.context(), + (context) => toAsyncIterableWith(self, context) + ) + +/** + * Converts a stream to an `AsyncIterable` for `for await...of` consumption. + * + * **Example** (Converting to an async iterable) + * + * ```ts + * import { Stream } from "effect" + * + * const stream = Stream.make(1, 2, 3) + * + * const collect = async () => { + * const iterable = Stream.toAsyncIterable(stream) + * const values: Array = [] + * for await (const value of iterable) { + * values.push(value) + * } + * console.log(values) + * } + * + * collect() + * // [ 1, 2, 3 ] + * ``` + * + * @category destructors + * @since 3.15.0 + */ +export const toAsyncIterable = (self: Stream): AsyncIterable => + toAsyncIterableWith(self, Context.empty()) + +/** + * Runs the stream, publishing elements into the provided PubSub. + * + * **Details** + * + * `shutdownOnEnd` controls whether the PubSub is shut down when the stream ends. + * It only shuts down when set to `true`. + * + * **Example** (Running a stream into a PubSub) + * + * ```ts + * import { Console, Effect, PubSub, Stream } from "effect" + * + * const program = Effect.scoped(Effect.gen(function* () { + * const pubsub = yield* PubSub.unbounded() + * const subscription = yield* PubSub.subscribe(pubsub) + * + * yield* Stream.runIntoPubSub(Stream.fromIterable([1, 2]), pubsub) + * + * const first = yield* PubSub.take(subscription) + * const second = yield* PubSub.take(subscription) + * + * yield* Console.log(first) + * yield* Console.log(second) + * })) + * + * Effect.runPromise(program) + * //=> 1 + * //=> 2 + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runIntoPubSub: { + ( + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): (self: Stream) => Effect.Effect + ( + self: Stream, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined + ): Effect.Effect +} = dual((args) => isStream(args[0]), ( + self: Stream, + pubsub: PubSub.PubSub, + options?: { + readonly shutdownOnEnd?: boolean | undefined + } | undefined +): Effect.Effect => Channel.runIntoPubSubArray(self.channel, pubsub, options)) + +/** + * Converts a stream to a PubSub of emitted values for concurrent consumption. + * + * **Details** + * + * `shutdownOnEnd` indicates whether the PubSub should be shut down when the + * stream ends. By default this is `true`. + * + * **Example** (Converting a stream to a PubSub for concurrent consumption) + * + * ```ts + * import { Console, Effect, PubSub, Stream } from "effect" + * + * const program = Effect.scoped(Effect.gen(function* () { + * const pubsub = yield* Stream.fromArray([1, 2]).pipe( + * Stream.toPubSub({ capacity: 8 }) + * ) + * const subscription = yield* PubSub.subscribe(pubsub) + * const first = yield* PubSub.take(subscription) + * + * yield* Console.log(first) + * })) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toPubSub: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): (self: Stream) => Effect.Effect, never, R | Scope.Scope> + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): Effect.Effect, never, R | Scope.Scope> +} = dual( + 2, + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + readonly shutdownOnEnd?: boolean | undefined + } + ): Effect.Effect, never, R | Scope.Scope> => Channel.toPubSubArray(self.channel, options) +) + +/** + * Converts a stream to a PubSub of `Take` values for concurrent consumption. + * + * **Details** + * + * `Take` values include the stream's end and failure signals. + * + * **Example** (Converting to a PubSub of takes) + * + * ```ts + * import { Console, Effect, PubSub, Stream } from "effect" + * + * const program = Effect.gen(function* () { + * const pubsub = yield* Stream.fromArray([1, 2, 3]).pipe( + * Stream.toPubSubTake({ capacity: 8 }) + * ) + * const subscription = yield* PubSub.subscribe(pubsub) + * const take = yield* PubSub.take(subscription) + * + * if (Array.isArray(take)) { + * yield* Console.log(take) + * } + * }) + * ``` + * + * @category destructors + * @since 4.0.0 + */ +export const toPubSubTake: { + ( + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ): (self: Stream) => Effect.Effect>, never, R | Scope.Scope> + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, R | Scope.Scope> +} = dual( + 2, + ( + self: Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + readonly replay?: number | undefined + } + ): Effect.Effect>, never, R | Scope.Scope> => + Channel.toPubSubTake(self.channel, options) +) + +/** + * Creates a scoped dequeue that is fed by the stream for concurrent + * consumption. + * + * **Details** + * + * Elements are offered to the queue as the stream runs. Stream completion is + * signaled with `Cause.Done`, stream failures fail the queue, and the queue is + * shut down when the surrounding scope closes. + * + * **Example** (Converting a stream to a Queue for concurrent consumption) + * + * ```ts + * import { Effect, Queue, Stream } from "effect" + * + * const program = Effect.gen(function* () { + * const queue = yield* Stream.toQueue(Stream.fromIterable([1, 2, 3]), { capacity: 8 }) + * const chunk = yield* Queue.takeBetween(queue, 1, 3) + * return chunk + * }) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const toQueue: { + ( + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): (self: Stream) => Effect.Effect, never, R | Scope.Scope> + ( + self: Stream, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Effect.Effect, never, R | Scope.Scope> +} = dual( + 2, + ( + self: Stream, + options: { + readonly capacity: "unbounded" + } | { + readonly capacity: number + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } + ): Effect.Effect, never, R | Scope.Scope> => + Channel.toQueueArray(self.channel, options) +) + +/** + * Runs the stream, offering each element to the provided queue and ending it + * with `Cause.Done` when the stream completes. + * + * **Example** (Running a stream into a queue) + * + * ```ts + * import { Cause, Effect, Queue, Stream } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(4) + * + * yield* Effect.forkChild( + * Stream.runIntoQueue(Stream.fromIterable([1, 2, 3]), queue) + * ) + * + * const values = [ + * yield* Queue.take(queue), + * yield* Queue.take(queue), + * yield* Queue.take(queue) + * ] + * const done = yield* Effect.flip(Queue.take(queue)) + * + * return { values, done } + * }) + * ``` + * + * @category destructors + * @since 2.0.0 + */ +export const runIntoQueue: { + (queue: Queue.Queue): (self: Stream) => Effect.Effect + (self: Stream, queue: Queue.Queue): Effect.Effect +} = dual(2, ( + self: Stream, + queue: Queue.Queue +): Effect.Effect => Channel.runIntoQueueArray(self.channel, queue)) diff --git a/repos/effect/packages/effect/src/String.ts b/repos/effect/packages/effect/src/String.ts new file mode 100644 index 0000000..615ec1d --- /dev/null +++ b/repos/effect/packages/effect/src/String.ts @@ -0,0 +1,1461 @@ +/** + * Works with TypeScript `string` values. + * + * This module exposes common string operations in a pipe-friendly style. The + * helpers cover checks, comparison, concatenation, trimming, casing, slicing, + * padding, replacement, normalization, safe character access, search helpers + * that return `Option`, and joining strings through a reducer. + * + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "./Array.ts" +import * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as readonlyArray from "./internal/array.ts" +import * as number from "./Number.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import type * as Ordering from "./Ordering.ts" +import type { Refinement } from "./Predicate.ts" +import * as predicate from "./Predicate.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Exposes the global string constructor. + * + * **When to use** + * + * Use to access native JavaScript string coercion or constructor behavior from + * the Effect module namespace. + * + * **Gotchas** + * + * Calling `String(value)` returns a primitive string. Calling + * `new String(value)` creates a boxed `String` object. + * + * @see {@link isString} for checking whether a value is a primitive string + * + * @category constructors + * @since 4.0.0 + */ +export const String = globalThis.String + +/** + * Checks whether a value is a `string`. + * + * **Example** (Checking for strings) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.isString("a"), true) + * assert.deepStrictEqual(String.isString(1), false) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isString: Refinement = predicate.isString + +/** + * Provides an `Order` instance for comparing strings using lexicographic + * ordering. + * + * **Example** (Comparing strings lexicographically) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.Order("apple", "banana")) // -1 + * console.log(String.Order("banana", "apple")) // 1 + * console.log(String.Order("apple", "apple")) // 0 + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Order: order.Order = order.String + +/** + * Provides an `Equivalence` instance for strings using strict equality (`===`). + * + * **Example** (Comparing strings for equality) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.Equivalence("hello", "hello")) // true + * console.log(String.Equivalence("hello", "world")) // false + * ``` + * + * @category instances + * @since 2.0.0 + */ +export const Equivalence: Equ.Equivalence = Equ.String + +/** + * Provides the empty string `""`. + * + * **When to use** + * + * Use when you need the canonical empty string value from the `String` module. + * + * **Example** (Referencing the empty string) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.empty) // "" + * console.log(String.isEmpty(String.empty)) // true + * ``` + * + * @category constants + * @since 2.0.0 + */ +export const empty: "" = "" as const + +/** + * Concatenates two strings at the type level. + * + * **Example** (Concatenating string literal types) + * + * ```ts + * import type { String } from "effect" + * + * // Type-level concatenation + * type Result = String.Concat<"hello", "world"> // "helloworld" + * ``` + * + * @category models + * @since 2.0.0 + */ +export type Concat = `${A}${B}` + +/** + * Concatenates two strings at runtime. + * + * **Example** (Concatenating strings) + * + * ```ts + * import { pipe, String } from "effect" + * + * const result1 = String.concat("hello", "world") + * console.log(result1) // "helloworld" + * + * const result2 = pipe("hello", String.concat("world")) + * console.log(result2) // "helloworld" + * ``` + * + * @category combining + * @since 2.0.0 + */ +export const concat: { + (that: B): (self: A) => Concat + (self: A, that: B): Concat +} = dual(2, (self: string, that: string): string => self + that) + +/** + * Converts a string to uppercase. + * + * **Example** (Converting strings to uppercase) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("a", String.toUpperCase), "A") + * assert.deepStrictEqual(String.toUpperCase("hello"), "HELLO") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const toUpperCase = (self: S): Uppercase => self.toUpperCase() as Uppercase + +/** + * Converts a string to lowercase. + * + * **Example** (Converting strings to lowercase) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("A", String.toLowerCase), "a") + * assert.deepStrictEqual(String.toLowerCase("HELLO"), "hello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const toLowerCase = (self: T): Lowercase => self.toLowerCase() as Lowercase + +/** + * Capitalizes the first character of a string. + * + * **Example** (Capitalizing a string) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("abc", String.capitalize), "Abc") + * assert.deepStrictEqual(String.capitalize("hello"), "Hello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const capitalize = (self: T): Capitalize => { + if (self.length === 0) return self as Capitalize + + return (toUpperCase(self[0]) + self.slice(1)) as Capitalize +} + +/** + * Uncapitalizes the first character of a string. + * + * **Example** (Uncapitalizing a string) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("ABC", String.uncapitalize), "aBC") + * assert.deepStrictEqual(String.uncapitalize("Hello"), "hello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const uncapitalize = (self: T): Uncapitalize => { + if (self.length === 0) return self as Uncapitalize + + return (toLowerCase(self[0]) + self.slice(1)) as Uncapitalize +} + +/** + * Replaces matches in a string using `String.prototype.replace`. + * + * **Details** + * + * String search values and non-global regular expressions replace the first + * match; global regular expressions replace every match. + * + * **Example** (Replacing a substring) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("abc", String.replace("b", "d")), "adc") + * assert.deepStrictEqual( + * pipe("hello world", String.replace("world", "Effect")), + * "hello Effect" + * ) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const replace = (searchValue: string | RegExp, replaceValue: string) => (self: string): string => + self.replace(searchValue, replaceValue) + +/** + * Type-level representation of trimming whitespace from both ends of a string. + * + * **Example** (Trimming whitespace at the type level) + * + * ```ts + * import type { String } from "effect" + * + * type Result = String.Trim<" hello "> // "hello" + * ``` + * + * @category models + * @since 2.0.0 + */ +export type Trim = TrimEnd> + +/** + * Removes whitespace from both ends of a string. + * + * **Example** (Trimming whitespace) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.trim(" a "), "a") + * assert.deepStrictEqual(String.trim(" hello world "), "hello world") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const trim = (self: A): Trim => self.trim() as Trim + +/** + * Type-level representation of trimming whitespace from the start of a string. + * + * **Example** (Trimming leading whitespace at the type level) + * + * ```ts + * import type { String } from "effect" + * + * type Result = String.TrimStart<" hello"> // "hello" + * ``` + * + * @category models + * @since 2.0.0 + */ +export type TrimStart = A extends `${" " | "\n" | "\t" | "\r"}${infer B}` ? TrimStart : A + +/** + * Removes whitespace from the start of a string. + * + * **Example** (Trimming leading whitespace) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.trimStart(" a "), "a ") + * assert.deepStrictEqual(String.trimStart(" hello world"), "hello world") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const trimStart = (self: A): TrimStart => self.trimStart() as TrimStart + +/** + * Type-level representation of trimming whitespace from the end of a string. + * + * **Example** (Trimming trailing whitespace at the type level) + * + * ```ts + * import type { String } from "effect" + * + * type Result = String.TrimEnd<"hello "> // "hello" + * ``` + * + * @category models + * @since 2.0.0 + */ +export type TrimEnd = A extends `${infer B}${" " | "\n" | "\t" | "\r"}` ? TrimEnd : A + +/** + * Removes whitespace from the end of a string. + * + * **Example** (Trimming trailing whitespace) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.trimEnd(" a "), " a") + * assert.deepStrictEqual(String.trimEnd("hello world "), "hello world") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const trimEnd = (self: A): TrimEnd => self.trimEnd() as TrimEnd + +/** + * Extracts a section of a string and returns it as a new string. + * + * **Example** (Slicing strings) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("abcd", String.slice(1, 3)), "bc") + * assert.deepStrictEqual(pipe("hello world", String.slice(0, 5)), "hello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const slice = (start?: number, end?: number) => (self: string): string => self.slice(start, end) + +/** + * Checks whether a `string` is empty. + * + * **Example** (Checking for empty strings) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.isEmpty(""), true) + * assert.deepStrictEqual(String.isEmpty("a"), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const isEmpty = (self: string): self is "" => self.length === 0 + +/** + * Checks whether a `string` is non-empty. + * + * **Example** (Checking for non-empty strings) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.isNonEmpty(""), false) + * assert.deepStrictEqual(String.isNonEmpty("a"), true) + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isNonEmpty = (self: string): boolean => self.length > 0 + +/** + * Returns the JavaScript string length, measured in UTF-16 code units. + * + * **Example** (Getting string length) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.length("abc"), 3) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const length = (self: string): number => self.length + +/** + * Splits a string into an array of substrings using a separator. + * + * **Example** (Splitting strings) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("abc", String.split("")), ["a", "b", "c"]) + * assert.deepStrictEqual(pipe("", String.split("")), [""]) + * assert.deepStrictEqual(String.split("hello,world", ","), ["hello", "world"]) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const split: { + (separator: string | RegExp): (self: string) => NonEmptyArray + (self: string, separator: string | RegExp): NonEmptyArray +} = dual(2, (self: string, separator: string | RegExp): NonEmptyArray => { + const out = self.split(separator) + return readonlyArray.isArrayNonEmpty(out) ? out : [self] +}) + +/** + * Returns `true` if `searchString` appears as a substring of `self`, at one or more positions that are + * greater than or equal to `position`; otherwise, returns `false`. + * + * **Example** (Checking for substrings) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("hello world", String.includes("world")), true) + * assert.deepStrictEqual(pipe("hello world", String.includes("foo")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const includes = (searchString: string, position?: number) => (self: string): boolean => + self.includes(searchString, position) + +/** + * Returns `true` if the string starts with the specified search string. + * + * **Example** (Checking string prefixes) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("hello world", String.startsWith("hello")), true) + * assert.deepStrictEqual(pipe("hello world", String.startsWith("world")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const startsWith = (searchString: string, position?: number) => (self: string): boolean => + self.startsWith(searchString, position) + +/** + * Returns `true` if the string ends with the specified search string. + * + * **Example** (Checking string suffixes) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("hello world", String.endsWith("world")), true) + * assert.deepStrictEqual(pipe("hello world", String.endsWith("hello")), false) + * ``` + * + * @category predicates + * @since 2.0.0 + */ +export const endsWith = (searchString: string, position?: number) => (self: string): boolean => + self.endsWith(searchString, position) + +/** + * Returns the character code at the specified index safely, or `None` if the index is out of bounds. + * + * **Example** (Reading character codes) + * + * ```ts + * import { String } from "effect" + * + * String.charCodeAt("abc", 1) // Option.some(98) + * String.charCodeAt("abc", 4) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const charCodeAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual( + 2, + (self: string, index: number): Option.Option => + Option.filter(Option.some(self.charCodeAt(index)), (charCode) => !isNaN(charCode)) +) + +/** + * Extracts characters from a string between two specified indices. + * + * **Example** (Extracting substrings) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abcd", String.substring(1)) // "bcd" + * pipe("abcd", String.substring(1, 3)) // "bc" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const substring = (start: number, end?: number) => (self: string): string => self.substring(start, end) + +/** + * Returns the character at the specified relative index safely, or `None` if the index is out of bounds. + * + * **Example** (Accessing characters safely) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abc", String.at(1)) // Option.some("b") + * pipe("abc", String.at(4)) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const at: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual(2, (self: string, index: number): Option.Option => Option.fromUndefinedOr(self.at(index))) + +/** + * Returns the character at the specified non-negative index safely, or `None` if the index is out of bounds. + * + * **Example** (Reading characters safely) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abc", String.charAt(1)) // Option.some("b") + * pipe("abc", String.charAt(4)) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const charAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual( + 2, + (self: string, index: number): Option.Option => Option.filter(Option.some(self.charAt(index)), isNonEmpty) +) + +/** + * Returns the Unicode code point at the specified index safely, or `None` if the index is out of bounds. + * + * **Example** (Reading code points) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abc", String.codePointAt(1)) // Option.some(98) + * pipe("abc", String.codePointAt(10)) // Option.none() + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const codePointAt: { + (index: number): (self: string) => Option.Option + (self: string, index: number): Option.Option +} = dual(2, (self: string, index: number): Option.Option => Option.fromUndefinedOr(self.codePointAt(index))) + +/** + * Returns the index of the first occurrence of a substring safely, or `None` if not found. + * + * **Example** (Finding the first substring index) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abbbc", String.indexOf("b")) // Option.some(1) + * pipe("abbbc", String.indexOf("z")) // Option.none() + * ``` + * + * @category searching + * @since 2.0.0 + */ +export const indexOf = (searchString: string) => (self: string): Option.Option => + Option.filter(Option.some(self.indexOf(searchString)), number.isGreaterThanOrEqualTo(0)) + +/** + * Returns the index of the last occurrence of a substring safely, or `None` if not found. + * + * **Example** (Finding the last substring index) + * + * ```ts + * import { pipe, String } from "effect" + * + * pipe("abbbc", String.lastIndexOf("b")) // Option.some(3) + * pipe("abbbc", String.lastIndexOf("d")) // Option.none() + * ``` + * + * @category searching + * @since 2.0.0 + */ +export const lastIndexOf = (searchString: string) => (self: string): Option.Option => + Option.filter(Option.some(self.lastIndexOf(searchString)), number.isGreaterThanOrEqualTo(0)) + +/** + * Computes locale-aware ordering for two strings, with optional locales and + * collator options, and returns the result as an `Ordering` (`-1`, `0`, or + * `1`). + * + * **Example** (Comparing strings by locale) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("a", String.localeCompare("b")), -1) + * assert.deepStrictEqual(pipe("b", String.localeCompare("a")), 1) + * assert.deepStrictEqual(pipe("a", String.localeCompare("a")), 0) + * ``` + * + * @category comparing + * @since 2.0.0 + */ +export const localeCompare = + (that: string, locales?: Array, options?: Intl.CollatorOptions) => (self: string): Ordering.Ordering => + number.sign(self.localeCompare(that, locales, options)) + +/** + * Matches a string against a pattern safely and returns `Option.some` with the match + * array, or `Option.none` when the pattern does not match. + * + * **Example** (Matching regular expressions) + * + * ```ts + * import { Option, pipe, String } from "effect" + * + * const match = pipe("hello", String.match(/l+/)) + * + * if (Option.isSome(match)) { + * console.log(`${match.value[0]}@${match.value.index}`) // "ll@2" + * } + * + * console.log(Option.isNone(pipe("hello", String.match(/x/)))) // true + * ``` + * + * @category searching + * @since 2.0.0 + */ +export const match = (regExp: RegExp | string) => (self: string): Option.Option => + Option.fromNullOr(self.match(regExp)) + +/** + * Returns an iterator over all regular expression matches in the string using + * native `String.prototype.matchAll` semantics. + * + * **Example** (Iterating regular expression matches) + * + * ```ts + * import { pipe, String } from "effect" + * + * const matches = pipe("hello world", String.matchAll(/l/g)) + * console.log( + * Array.from(matches, (match) => `${match[0]}@${match.index}`).join(", ") + * ) // "l@2, l@3, l@9" + * ``` + * + * @category searching + * @since 2.0.0 + */ +export const matchAll = (regExp: RegExp) => (self: string): IterableIterator => self.matchAll(regExp) + +/** + * Normalizes a string according to the specified Unicode normalization form. + * + * **Example** (Normalizing Unicode strings) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * const str = "\u1E9B\u0323" + * assert.deepStrictEqual(pipe(str, String.normalize()), "\u1E9B\u0323") + * assert.deepStrictEqual(pipe(str, String.normalize("NFC")), "\u1E9B\u0323") + * assert.deepStrictEqual(pipe(str, String.normalize("NFD")), "\u017F\u0323\u0307") + * assert.deepStrictEqual(pipe(str, String.normalize("NFKC")), "\u1E69") + * assert.deepStrictEqual( + * pipe(str, String.normalize("NFKD")), + * "\u0073\u0323\u0307" + * ) + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const normalize = (form?: "NFC" | "NFD" | "NFKC" | "NFKD") => (self: string): string => self.normalize(form) + +/** + * Pads the string from the end with a given fill string to a specified length. + * + * **Example** (Padding strings at the end) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("a", String.padEnd(5)), "a ") + * assert.deepStrictEqual(pipe("a", String.padEnd(5, "_")), "a____") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const padEnd = (maxLength: number, fillString?: string) => (self: string): string => + self.padEnd(maxLength, fillString) + +/** + * Pads the string from the start with a given fill string to a specified length. + * + * **Example** (Padding strings at the start) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("a", String.padStart(5)), " a") + * assert.deepStrictEqual(pipe("a", String.padStart(5, "_")), "____a") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const padStart = (maxLength: number, fillString?: string) => (self: string): string => + self.padStart(maxLength, fillString) + +/** + * Repeats the string the specified number of times. + * + * **Example** (Repeating strings) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("a", String.repeat(5)), "aaaaa") + * assert.deepStrictEqual(pipe("hello", String.repeat(3)), "hellohellohello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const repeat = (count: number) => (self: string): string => self.repeat(count) + +/** + * Replaces all occurrences of a substring or pattern in a string. + * + * **Example** (Replacing all matches) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(pipe("ababb", String.replaceAll("b", "c")), "acacc") + * assert.deepStrictEqual(pipe("ababb", String.replaceAll(/ba/g, "cc")), "accbb") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const replaceAll = (searchValue: string | RegExp, replaceValue: string) => (self: string): string => + self.replaceAll(searchValue, replaceValue) + +/** + * Returns the index of the first match for a string or regular expression safely, or + * `Option.none` when no match is found. + * + * **Example** (Searching strings) + * + * ```ts + * import { String } from "effect" + * + * String.search("ababb", "b") // Option.some(1) + * String.search("ababb", /abb/) // Option.some(2) + * String.search("ababb", "d") // Option.none() + * ``` + * + * @category searching + * @since 2.0.0 + */ +export const search: { + (regExp: RegExp | string): (self: string) => Option.Option + (self: string, regExp: RegExp | string): Option.Option +} = dual( + 2, + (self: string, regExp: RegExp | string): Option.Option => + Option.filter(Option.some(self.search(regExp)), number.isGreaterThanOrEqualTo(0)) +) + +/** + * Converts the string to lowercase according to the specified locale. + * + * **Example** (Lowercasing strings by locale) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * const str = "\u0130" + * assert.deepStrictEqual(pipe(str, String.toLocaleLowerCase("tr")), "i") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const toLocaleLowerCase = (locale?: string | Array) => (self: string): string => + self.toLocaleLowerCase(locale) + +/** + * Converts the string to uppercase according to the specified locale. + * + * **Example** (Uppercasing strings by locale) + * + * ```ts + * import { pipe, String } from "effect" + * import * as assert from "node:assert" + * + * const str = "i\u0307" + * assert.deepStrictEqual(pipe(str, String.toLocaleUpperCase("lt-LT")), "I") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const toLocaleUpperCase = (locale?: string | Array) => (self: string): string => + self.toLocaleUpperCase(locale) + +/** + * Keeps the specified number of characters from the start of a string. + * + * **Details** + * + * If `n` is larger than the available number of characters, the string will + * be returned whole. + * + * If `n` is not a positive number, an empty string will be returned. + * + * If `n` is a float, it will be rounded down to the nearest integer. + * + * **Example** (Taking characters from the start) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.takeLeft("Hello World", 5), "Hello") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const takeLeft: { + (n: number): (self: string) => string + (self: string, n: number): string +} = dual(2, (self: string, n: number): string => self.slice(0, Math.max(n, 0))) + +/** + * Keeps the specified number of characters from the end of a string. + * + * **Details** + * + * If `n` is larger than the available number of characters, the string will + * be returned whole. + * + * If `n` is not a positive number, an empty string will be returned. + * + * If `n` is a float, it will be rounded down to the nearest integer. + * + * **Example** (Taking characters from the end) + * + * ```ts + * import { String } from "effect" + * import * as assert from "node:assert" + * + * assert.deepStrictEqual(String.takeRight("Hello World", 5), "World") + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const takeRight: { + (n: number): (self: string) => string + (self: string, n: number): string +} = dual( + 2, + (self: string, n: number): string => self.slice(Math.max(0, self.length - Math.floor(n)), Infinity) +) + +const CR = 0x0d +const LF = 0x0a + +/** + * Returns an `IterableIterator` which yields each line contained within the + * string, trimming off the trailing newline character. + * + * **Example** (Iterating lines without separators) + * + * ```ts + * import { String } from "effect" + * + * const lines = String.linesIterator("hello\nworld\n") + * console.log(Array.from(lines)) // ["hello", "world"] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const linesIterator = (self: string): LinesIterator => linesSeparated(self, true) + +/** + * Returns an `IterableIterator` which yields each line contained within the + * string as well as the trailing newline character. + * + * **Example** (Iterating lines with separators) + * + * ```ts + * import { String } from "effect" + * + * const lines = String.linesWithSeparators("hello\nworld\n") + * console.log(Array.from(lines)) // ["hello\n", "world\n"] + * ``` + * + * @category splitting + * @since 2.0.0 + */ +export const linesWithSeparators = (s: string): LinesIterator => linesSeparated(s, false) + +/** + * Strips a leading margin prefix from every line using the supplied margin + * character. + * + * **Example** (Stripping custom margins) + * + * ```ts + * import { String } from "effect" + * + * const text = " |hello\n |world" + * const result = String.stripMarginWith(text, "|") + * console.log(result) // "hello\nworld" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const stripMarginWith: { + (marginChar: string): (self: string) => string + (self: string, marginChar: string): string +} = dual(2, (self: string, marginChar: string): string => { + let out = "" + + for (const line of linesWithSeparators(self)) { + let index = 0 + + while (index < line.length && line.charAt(index) <= " ") { + index = index + 1 + } + + const stripped = index < line.length && line.charAt(index) === marginChar + ? line.substring(index + 1) + : line + + out = out + stripped + } + + return out +}) + +/** + * Strips a leading `|` margin prefix from every line. + * + * **Example** (Stripping pipe margins) + * + * ```ts + * import { String } from "effect" + * + * const text = " |hello\n |world" + * const result = String.stripMargin(text) + * console.log(result) // "hello\nworld" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const stripMargin = (self: string): string => stripMarginWith(self, "|") + +/** + * Converts a snake_case string to camelCase. + * + * **Example** (Converting snake_case to camelCase) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.snakeToCamel("hello_world")) // "helloWorld" + * console.log(String.snakeToCamel("foo_bar_baz")) // "fooBarBaz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const snakeToCamel = (self: string): string => { + let str = self[0] + for (let i = 1; i < self.length; i++) { + str += self[i] === "_" ? self[++i].toUpperCase() : self[i] + } + return str +} + +/** + * Converts a snake_case string to PascalCase. + * + * **Example** (Converting snake_case to PascalCase) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.snakeToPascal("hello_world")) // "HelloWorld" + * console.log(String.snakeToPascal("foo_bar_baz")) // "FooBarBaz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const snakeToPascal = (self: string): string => { + let str = self[0].toUpperCase() + for (let i = 1; i < self.length; i++) { + str += self[i] === "_" ? self[++i].toUpperCase() : self[i] + } + return str +} + +/** + * Converts a snake_case string to kebab-case. + * + * **Example** (Converting snake_case to kebab-case) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.snakeToKebab("hello_world")) // "hello-world" + * console.log(String.snakeToKebab("foo_bar_baz")) // "foo-bar-baz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const snakeToKebab = (self: string): string => self.replace(/_/g, "-") + +/** + * Converts a camelCase string to snake_case. + * + * **Example** (Converting camelCase to snake_case) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.camelToSnake("helloWorld")) // "hello_world" + * console.log(String.camelToSnake("fooBarBaz")) // "foo_bar_baz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const camelToSnake = (self: string): string => self.replace(/([A-Z])/g, "_$1").toLowerCase() + +/** + * Converts a PascalCase string to snake_case. + * + * **Example** (Converting PascalCase to snake_case) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.pascalToSnake("HelloWorld")) // "hello_world" + * console.log(String.pascalToSnake("FooBarBaz")) // "foo_bar_baz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const pascalToSnake = (self: string): string => + (self.slice(0, 1) + self.slice(1).replace(/([A-Z])/g, "_$1")).toLowerCase() + +/** + * Converts a kebab-case string to snake_case. + * + * **Example** (Converting kebab-case to snake_case) + * + * ```ts + * import { String } from "effect" + * + * console.log(String.kebabToSnake("hello-world")) // "hello_world" + * console.log(String.kebabToSnake("foo-bar-baz")) // "foo_bar_baz" + * ``` + * + * @category transforming + * @since 2.0.0 + */ +export const kebabToSnake = (self: string): string => self.replace(/-/g, "_") + +class LinesIterator implements IterableIterator { + private index: number + private readonly length: number + readonly s: string + readonly stripped: boolean + + constructor( + s: string, + stripped: boolean = false + ) { + this.s = s + this.stripped = stripped + this.index = 0 + this.length = s.length + } + + next(): IteratorResult { + if (this.done) { + return { done: true, value: undefined } + } + const start = this.index + while (!this.done && !isLineBreak(this.s[this.index]!)) { + this.index = this.index + 1 + } + let end = this.index + if (!this.done) { + const char = this.s[this.index]! + this.index = this.index + 1 + if (!this.done && isLineBreak2(char, this.s[this.index]!)) { + this.index = this.index + 1 + } + if (!this.stripped) { + end = this.index + } + } + return { done: false, value: this.s.substring(start, end) } + } + + [Symbol.iterator](): IterableIterator { + return new LinesIterator(this.s, this.stripped) + } + + private get done(): boolean { + return this.index >= this.length + } +} + +/** + * Checks whether the provided character is a line break character (i.e. either `"\r"` + * or `"\n"`). + */ +const isLineBreak = (char: string): boolean => { + const code = char.charCodeAt(0) + return code === CR || code === LF +} + +/** + * Checks whether the provided characters combine to form a carriage return/line-feed + * (i.e. `"\r\n"`). + */ +const isLineBreak2 = (char0: string, char1: string): boolean => char0.charCodeAt(0) === CR && char1.charCodeAt(0) === LF + +const linesSeparated = (self: string, stripped: boolean): LinesIterator => new LinesIterator(self, stripped) + +/** + * Normalizes a string by splitting it into word parts, transforming each part, + * and joining the parts with a configurable delimiter. + * + * **When to use** + * + * Use when you need custom word-case output with a delimiter or part transform + * that the fixed case helpers do not provide. + * + * @see {@link pascalCase} for fixed PascalCase output + * @see {@link camelCase} for fixed lower-initial camelCase output + * @see {@link constantCase} for fixed uppercase underscore-separated output + * @see {@link kebabCase} for fixed lowercase hyphen-separated output + * @see {@link snakeCase} for fixed lowercase underscore-separated output + * + * @category transforming + * @since 4.0.0 + */ +export const noCase: { + (options?: { + readonly splitRegExp?: RegExp | ReadonlyArray | undefined + readonly stripRegExp?: RegExp | ReadonlyArray | undefined + readonly delimiter?: string | undefined + readonly transform?: (part: string, index: number, parts: ReadonlyArray) => string + }): (self: string) => string + (self: string, options?: { + readonly splitRegExp?: RegExp | ReadonlyArray | undefined + readonly stripRegExp?: RegExp | ReadonlyArray | undefined + readonly delimiter?: string | undefined + readonly transform?: (part: string, index: number, parts: ReadonlyArray) => string + }): string +} = dual((args) => typeof args[0] === "string", (input: string, options?: { + readonly splitRegExp?: RegExp | ReadonlyArray | undefined + readonly stripRegExp?: RegExp | ReadonlyArray | undefined + readonly delimiter?: string | undefined + readonly transform?: (part: string, index: number, parts: ReadonlyArray) => string +}): string => { + const delimiter = options?.delimiter ?? " " + const transform = options?.transform ?? toLowerCase + return normalizeCase(input, SPLIT_REGEXP, STRIP_REGEXP, delimiter, transform) +}) + +const normalizeCase = ( + input: string, + splitRegExp: ReadonlyArray, + stripRegExp: RegExp, + delimiter: string, + transform: (part: string, index: number, parts: ReadonlyArray) => string +): string => { + let result = input + for (const regexp of splitRegExp) { + result = result.replace(regexp, "$1\0$2") + } + result = result.replace(stripRegExp, "\0") + let start = 0 + let end = result.length + // Trim the delimiter from around the output string. + while (result.charAt(start) === "\0") { + start++ + } + while (result.charAt(end - 1) === "\0") { + end-- + } + + // Transform each token independently. + return result.slice(start, end).split("\0").map(transform).join(delimiter) +} + +// Support camel case ("camelCase" -> "camel Case" and "CAMELCase" -> "CAMEL Case") +// and digit boundaries ("camel2case" -> "camel 2 case"). +const SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g, /([A-Z])([0-9])/gi, /([0-9])([A-Z])/gi] + +// Config paths preserve digit groups such as "v2" while still supporting camel case. +const CONFIG_SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g] + +// Remove all non-word characters. +const STRIP_REGEXP = /[^A-Z0-9]+/gi + +const pascalCaseTransform = (input: string): string => { + const firstChar = input.charAt(0) + const lowerChars = input.substring(1).toLowerCase() + return `${firstChar.toUpperCase()}${lowerChars}` +} + +/** + * Converts a string to PascalCase. + * + * **When to use** + * + * Use to normalize strings from spaces, separators, or camel/Pascal word + * boundaries into PascalCase. + * + * @see {@link camelCase} for lower-initial camelCase output + * @see {@link noCase} for configurable delimiters and part transforms + * @see {@link snakeToPascal} for converting known snake_case input only + * + * @category transforming + * @since 4.0.0 + */ +export const pascalCase: (self: string) => string = noCase({ + delimiter: "", + transform: pascalCaseTransform +}) + +const camelCaseTransform = (input: string, index: number): string => + index === 0 + ? input.toLowerCase() + : pascalCaseTransform(input) + +/** + * Converts a string to camelCase. + * + * **When to use** + * + * Use to normalize mixed word separators or existing PascalCase/camelCase text + * into lower-initial camelCase identifiers. + * + * @see {@link noCase} for configurable delimiters and part transforms + * @see {@link pascalCase} for upper-initial PascalCase output + * @see {@link snakeCase} for lowercase underscore-separated output + * @see {@link kebabCase} for lowercase hyphen-separated output + * @see {@link constantCase} for uppercase underscore-separated output + * + * @category transforming + * @since 4.0.0 + */ +export const camelCase: (self: string) => string = noCase({ + delimiter: "", + transform: camelCaseTransform +}) + +/** + * Converts a string to CONSTANT_CASE (uppercase with underscores). + * + * **When to use** + * + * Use to normalize words from mixed input formats into uppercase, + * underscore-separated identifiers. + * + * @see {@link snakeCase} for lowercase underscore-separated output + * @see {@link kebabCase} for lowercase hyphen-separated output + * @see {@link camelCase} for lower-initial camelCase output + * @see {@link pascalCase} for upper-initial PascalCase output + * @see {@link configCase} for configuration key casing that preserves numeric word groups + * @see {@link noCase} for configurable delimiters and part transforms + * + * @category transforming + * @since 4.0.0 + */ +export const constantCase: (self: string) => string = noCase({ + delimiter: "_", + transform: toUpperCase +}) + +/** + * Converts a string to CONFIG_CASE (uppercase with underscores) for + * configuration keys. + * + * **When to use** + * + * Use to normalize configuration path segments into environment-variable-like + * keys while preserving numeric word groups such as `v2`. + * + * **Details** + * + * Unlike {@link constantCase}, digit-letter boundaries are not split. For + * example, `"api-v2 xml"` becomes `"API_V2_XML"`. + * + * @see {@link constantCase} for standard uppercase underscore-separated output + * @category transforming + * @since 4.0.0 + */ +export const configCase: (self: string) => string = (self) => + normalizeCase(self, CONFIG_SPLIT_REGEXP, STRIP_REGEXP, "_", toUpperCase) + +/** + * Converts a string to kebab-case (lowercase with hyphens). + * + * **When to use** + * + * Use to normalize free-form labels, identifiers, or keys into lowercase + * hyphen-separated text. + * + * @see {@link noCase} for configurable delimiters and part transforms + * @see {@link snakeCase} for lowercase underscore-separated output + * @see {@link constantCase} for uppercase underscore-separated output + * @see {@link camelCase} for lower-initial camelCase output + * @see {@link pascalCase} for upper-initial PascalCase output + * + * @category transforming + * @since 4.0.0 + */ +export const kebabCase: (self: string) => string = noCase({ + delimiter: "-" +}) + +/** + * Converts a string to snake_case (lowercase with underscores). + * + * **When to use** + * + * Use to normalize mixed-case or separator-delimited text into lowercase words + * joined with underscores. + * + * @see {@link noCase} for configurable lower-level normalization + * @see {@link kebabCase} for lowercase hyphen-separated output + * @see {@link constantCase} for uppercase underscore-separated output + * + * @category transforming + * @since 4.0.0 + */ +export const snakeCase: (self: string) => string = noCase({ + delimiter: "_" +}) + +/** + * Reducer for concatenating `string`s. + * + * **When to use** + * + * Use to concatenate many strings through APIs that consume a `Reducer`. + * + * **Details** + * + * The reducer starts from `""`, so combining an empty collection returns `""`. + * + * @see {@link concat} for concatenating two strings directly + * + * @category combining + * @since 4.0.0 + */ +export const ReducerConcat: Reducer.Reducer = Reducer.make((a, b) => a + b, "") diff --git a/repos/effect/packages/effect/src/Struct.ts b/repos/effect/packages/effect/src/Struct.ts new file mode 100644 index 0000000..9f0318f --- /dev/null +++ b/repos/effect/packages/effect/src/Struct.ts @@ -0,0 +1,979 @@ +/** + * Works with plain TypeScript objects, also called structs. + * + * The runtime helpers in this module create new objects instead of mutating + * their inputs. They cover common object workflows such as reading properties, + * listing typed keys, picking or omitting fields, assigning and renaming keys, + * transforming values, deriving comparison helpers, and creating records from a + * list of keys. The module also includes type-level helpers for simplifying and + * merging object shapes. + * + * @since 2.0.0 + */ + +import * as Combiner from "./Combiner.ts" +import * as Equivalence from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as order from "./Order.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Flattens intersection types into a single object type for readability. + * + * **When to use** + * + * Use when hovering over a type shows `A & B & C` instead of the merged shape. + * + * **Details** + * + * This helper is purely cosmetic at the type level and has no runtime effect. + * It preserves `readonly` modifiers; use {@link Mutable} to strip them. + * + * **Example** (Flattening an intersection) + * + * ```ts + * import type { Struct } from "effect" + * + * type Original = { a: string } & { b: number } + * + * // Without Simplify, the type displays as `{ a: string } & { b: number }` + * type Simplified = Struct.Simplify + * // { a: string; b: number } + * ``` + * + * @see {@link Mutable} – also flattens but removes `readonly` + * @see {@link Assign} – merges two types with right-side precedence + * @category utility types + * @since 4.0.0 + */ +export type Simplify = { [K in keyof T]: T[K] } & {} + +/** + * Removes `readonly` modifiers from all properties of an object type. + * + * **When to use** + * + * Use when you need a mutable version of a readonly interface. + * + * **Details** + * + * This helper is purely cosmetic at the type level and has no runtime effect. + * It also flattens intersections like {@link Simplify}. + * + * **Example** (Making a readonly type mutable) + * + * ```ts + * import type { Struct } from "effect" + * + * type ReadOnly = { readonly a: string; readonly b: number } + * type Writable = Struct.Mutable + * // { a: string; b: number } + * ``` + * + * @see {@link Simplify} – flattens intersections without removing `readonly` + * @category utility types + * @since 4.0.0 + */ +export type Mutable = { -readonly [K in keyof T]: T[K] } & {} + +/** + * Merges two object types with properties from `U` taking precedence over `T` + * on overlapping keys (like `Object.assign` at the type level). + * + * **When to use** + * + * Use when you need the type-level equivalent of `{ ...T, ...U }`. + * + * **Details** + * + * When no keys overlap, this returns a simple intersection for efficiency. + * When keys overlap, the type from `U` wins. + * + * **Example** (Merging two types with overlapping keys) + * + * ```ts + * import type { Struct } from "effect" + * + * type A = { a: string; b: number } + * type B = { b: boolean; c: string } + * type Merged = Struct.Assign + * // { a: string; b: boolean; c: string } + * ``` + * + * @see {@link assign} – the runtime equivalent + * @see {@link Simplify} – flatten the resulting intersection + * @category utility types + * @since 4.0.0 + */ +export type Assign = Simplify & U> + +/** + * Retrieves the value at `key` from a struct. + * + * **When to use** + * + * Use to extract a single property from a struct in a pipeline. + * + * **Details** + * + * The return type is narrowed to `S[K]`. + * + * **Example** (Extracting a property in a pipeline) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const name = pipe({ name: "Alice", age: 30 }, Struct.get("name")) + * console.log(name) // "Alice" + * ``` + * + * @see {@link keys} – list all string keys of a struct + * @see {@link pick} – extract multiple properties into a new struct + * @category getters + * @since 2.0.0 + */ +export const get: { + (key: K): (self: S) => S[K] + (self: S, key: K): S[K] +} = dual(2, (self: S, key: K): S[K] => self[key]) + +/** + * Returns the string keys of a struct as a properly typed `Array`. + * + * **When to use** + * + * Use when you want a typed replacement for `Object.keys` that narrows the result + * to the known string keys of the struct. + * + * **Gotchas** + * + * Symbol keys are excluded; only string keys are returned. + * + * **Example** (Reading typed keys) + * + * ```ts + * import { Struct } from "effect" + * + * const user = { name: "Alice", age: 30, [Symbol.for("id")]: 1 } + * + * const k: Array<"name" | "age"> = Struct.keys(user) + * console.log(k) // ["name", "age"] + * ``` + * + * @see {@link get} – access a single key's value + * @see {@link pick} – select a subset of keys into a new struct + * @category Key utilities + * @since 3.6.0 + */ +export const keys = (self: S): Array<(keyof S) & string> => + Object.keys(self) as Array<(keyof S) & string> + +/** + * Creates a new struct containing only the specified keys. + * + * **When to use** + * + * Use to narrow a struct down to a subset of its properties. + * + * **Gotchas** + * + * Keys not present in the struct are silently ignored. + * + * **Example** (Selecting specific properties) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const user = { name: "Alice", age: 30, admin: true } + * const nameAndAge = pipe(user, Struct.pick(["name", "age"])) + * console.log(nameAndAge) // { name: "Alice", age: 30 } + * ``` + * + * @see {@link omit} – the inverse (exclude keys instead) + * @see {@link get} – extract a single value + * @category filtering + * @since 2.0.0 + */ +export const pick: { + >( + keys: Keys + ): (self: S) => Simplify> + >(self: S, keys: Keys): Simplify> +} = dual( + 2, + >(self: S, keys: Keys) => { + return buildStruct(self, (k, v) => (keys.includes(k) ? [k, v] : undefined)) + } +) + +/** + * Creates a new struct with the specified keys removed. + * + * **When to use** + * + * Use to exclude sensitive or irrelevant fields from a struct. + * + * **Gotchas** + * + * Keys not present in the struct are silently ignored. + * + * **Example** (Removing a property) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const user = { name: "Alice", age: 30, password: "secret" } + * const safe = pipe(user, Struct.omit(["password"])) + * console.log(safe) // { name: "Alice", age: 30 } + * ``` + * + * @see {@link pick} – the inverse (keep only specified keys) + * @category filtering + * @since 2.0.0 + */ +export const omit: { + >( + keys: Keys + ): (self: S) => Simplify> + >(self: S, keys: Keys): Simplify> +} = dual( + 2, + >(self: S, keys: Keys) => { + return buildStruct(self, (k, v) => (!keys.includes(k) ? [k, v] : undefined)) + } +) + +/** + * Merges two structs into a new struct. When both structs share a key, the + * value from `that` (the second struct) wins. + * + * **When to use** + * + * Use when you want `{ ...self, ...that }` with proper types. + * + * **Details** + * + * The result type is `Simplify>`. + * + * **Example** (Merging structs with overlapping keys) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const defaults = { theme: "light", lang: "en" } + * const overrides = { theme: "dark", fontSize: 14 } + * const config = pipe(defaults, Struct.assign(overrides)) + * console.log(config) // { theme: "dark", lang: "en", fontSize: 14 } + * ``` + * + * @see {@link Assign} – the type-level equivalent + * @see {@link evolve} – transform individual values instead of replacing them + * @category combining + * @since 4.0.0 + */ +export const assign: { + (that: O): (self: S) => Assign + (self: S, that: O): Assign +} = dual( + 2, + (self: S, that: O) => { + return { ...self, ...that } + } +) + +type Evolver = { readonly [K in keyof S]?: (a: S[K]) => unknown } + +type Evolved = Simplify< + { [K in keyof S]: K extends keyof E ? (E[K] extends (...a: any) => infer R ? R : S[K]) : S[K] } +> + +/** + * Transforms values of a struct selectively using per-key functions. Keys + * without a corresponding function are copied unchanged. + * + * **When to use** + * + * Use when you want to update specific fields while keeping the rest intact. + * + * **Details** + * + * Each transform function receives the current value and returns the new value; + * the return type can differ from the input type. + * + * **Example** (Transforming selected values) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const result = pipe( + * { name: "alice", age: 30, active: true }, + * Struct.evolve({ + * name: (s) => s.toUpperCase(), + * age: (n) => n + 1 + * }) + * ) + * console.log(result) // { name: "ALICE", age: 31, active: true } + * ``` + * + * @see {@link evolveKeys} – transform keys instead of values + * @see {@link evolveEntries} – transform both keys and values + * @see {@link map} – apply the same transformation to all values + * @category transforming + * @since 2.0.0 + */ +export const evolve: { + >(e: E): (self: S) => Evolved + >(self: S, e: E): Evolved +} = dual( + 2, + >(self: S, e: E): Evolved => { + return buildStruct(self, (k, v) => [k, Object.hasOwn(e, k) ? (e as any)[k](v) : v]) + } +) + +type KeyEvolver = { readonly [K in keyof S]?: (k: K) => PropertyKey } + +type KeyEvolved = Simplify< + { [K in keyof S as K extends keyof E ? (E[K] extends ((k: K) => infer R extends PropertyKey) ? R : K) : K]: S[K] } +> + +/** + * Transforms keys of a struct selectively using per-key functions. Keys without + * a corresponding function are copied unchanged. + * + * **When to use** + * + * Use when you need computed key names, such as uppercasing or prefixing. + * + * **Details** + * + * Each transform function receives the key name and must return a new + * `PropertyKey`. + * + * **Example** (Renaming keys with functions) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const result = pipe( + * { name: "Alice", age: 30 }, + * Struct.evolveKeys({ + * name: (k) => k.toUpperCase() + * }) + * ) + * console.log(result) // { NAME: "Alice", age: 30 } + * ``` + * + * @see {@link renameKeys} – rename keys with a static mapping + * @see {@link evolve} – transform values instead of keys + * @see {@link evolveEntries} – transform both keys and values + * @category Key utilities + * @since 4.0.0 + */ +export const evolveKeys: { + >(e: E): (self: S) => KeyEvolved + >(self: S, e: E): KeyEvolved +} = dual( + 2, + >(self: S, e: E): KeyEvolved => { + return buildStruct(self, (k, v) => [Object.hasOwn(e, k) ? (e as any)[k](k) : k, v]) + } +) + +type EntryEvolver = { readonly [K in keyof S]?: (k: K, v: S[K]) => [PropertyKey, unknown] } + +type EntryEvolved = { + [ + K in keyof S as K extends keyof E ? + E[K] extends ((k: K, v: S[K]) => [infer NK extends PropertyKey, infer _V]) ? NK : K + : K + ]: K extends keyof E ? E[K] extends ((k: K, v: S[K]) => [infer _NK, infer V]) ? V + : S[K] : + S[K] +} + +/** + * Transforms both keys and values of a struct selectively. Each per-key + * function receives `(key, value)` and must return a `[newKey, newValue]` + * tuple. Keys without a corresponding function are copied unchanged. + * + * **When to use** + * + * Use when you need to rename a key and change its value in one step. + * + * **Details** + * + * The return type is fully tracked at the type level. + * + * **Example** (Transforming keys and values together) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const result = pipe( + * { amount: 100, label: "total" }, + * Struct.evolveEntries({ + * amount: (k, v) => [`${k}Cents`, v * 100], + * label: (k, v) => [k, v.toUpperCase()] + * }) + * ) + * console.log(result) // { amountCents: 10000, label: "TOTAL" } + * ``` + * + * @see {@link evolve} – transform values only + * @see {@link evolveKeys} – transform keys only + * @category transforming + * @since 4.0.0 + */ +export const evolveEntries: { + >(e: E): (self: S) => EntryEvolved + >(self: S, e: E): EntryEvolved +} = dual( + 2, + >(self: S, e: E): EntryEvolved => { + return buildStruct(self, (k, v) => (Object.hasOwn(e, k) ? (e as any)[k](k, v) : [k, v])) + } +) + +/** + * Renames keys in a struct using a static `{ oldKey: newKey }` mapping. Keys + * not mentioned in the mapping are copied unchanged. + * + * **When to use** + * + * Use when you need simple, declarative key renaming without custom logic. + * + * **Details** + * + * For computed key names, use {@link evolveKeys} instead. + * + * **Example** (Renaming keys) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * const result = pipe( + * { firstName: "Alice", lastName: "Smith", age: 30 }, + * Struct.renameKeys({ firstName: "first", lastName: "last" }) + * ) + * console.log(result) // { first: "Alice", last: "Smith", age: 30 } + * ``` + * + * @see {@link evolveKeys} – rename keys using functions + * @see {@link evolveEntries} – rename keys and transform values + * @category Key utilities + * @since 4.0.0 + */ +export const renameKeys: { + ( + mapping: M + ): (self: S) => { [K in keyof S as K extends keyof M ? M[K] extends PropertyKey ? M[K] : K : K]: S[K] } + ( + self: S, + mapping: M + ): { [K in keyof S as K extends keyof M ? M[K] extends PropertyKey ? M[K] : K : K]: S[K] } +} = dual(2, (self: S, mapping: M) => { + return buildStruct(self, (k, v) => [Object.hasOwn(mapping, k) ? mapping[k]! : k, v]) +}) + +/** + * Creates an `Equivalence` for a struct by providing an `Equivalence` for each + * property. Two structs are equivalent when all their corresponding properties + * are equivalent. + * + * **When to use** + * + * Use when you need equality for a record-like object to be decided field by + * field, with a custom equality rule for each property. + * + * **Details** + * + * This is an alias of `Equivalence.Struct`. Each property's equivalence is + * checked independently; all must return `true` for the overall result to be + * `true`. + * + * **Example** (Comparing structs for equivalence) + * + * ```ts + * import { Equivalence, Struct } from "effect" + * + * const PersonEquivalence = Struct.makeEquivalence({ + * name: Equivalence.strictEqual(), + * age: Equivalence.strictEqual() + * }) + * + * console.log(PersonEquivalence({ name: "Alice", age: 30 }, { name: "Alice", age: 30 })) + * // true + * console.log(PersonEquivalence({ name: "Alice", age: 30 }, { name: "Bob", age: 30 })) + * // false + * ``` + * + * @see {@link makeOrder} – create an `Order` for structs + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = Equivalence.Struct + +/** + * Creates an `Order` for a struct by providing an `Order` for each property. + * Properties are compared in the order they appear in the fields object; the + * first non-zero comparison determines the result. + * + * **When to use** + * + * Use when you need to sort record-like objects lexicographically by several + * fields, with each field using its own ordering rule. + * + * **Details** + * + * This is an alias of `Order.Struct`. The order of keys in the `fields` object + * determines comparison priority. + * + * **Example** (Ordering structs by name then age) + * + * ```ts + * import { Number, String, Struct } from "effect" + * + * const PersonOrder = Struct.makeOrder({ + * name: String.Order, + * age: Number.Order + * }) + * + * console.log(PersonOrder({ name: "Alice", age: 30 }, { name: "Bob", age: 25 })) + * // -1 (Alice comes before Bob) + * ``` + * + * @see {@link makeEquivalence} – create an `Equivalence` for structs + * @category ordering + * @since 4.0.0 + */ +export const makeOrder = order.Struct + +/** + * Interface for type-level functions used by {@link map}, {@link mapPick}, and + * {@link mapOmit}. + * + * **When to use** + * + * Use when defining a typed function for {@link map}, {@link mapPick}, or + * {@link mapOmit}. + * + * **Details** + * + * Extend this interface with concrete `~lambda.in` and `~lambda.out` types to + * describe how a function transforms values at the type level. At runtime, + * create lambda values with {@link lambda}. + * + * **Example** (Defining a lambda type) + * + * ```ts + * import type { Struct } from "effect" + * + * interface ToString extends Struct.Lambda { + * readonly "~lambda.out": string + * } + * ``` + * + * @see {@link Apply} – apply a Lambda to a concrete type + * @see {@link lambda} – create a runtime lambda value + * @see {@link map} – use a lambda to transform all struct values + * @category Lambda + * @since 4.0.0 + */ +export interface Lambda { + readonly "~lambda.in": unknown + readonly "~lambda.out": unknown +} + +/** + * Applies a {@link Lambda} type-level function to a value type `V`, producing + * the output type. + * + * **When to use** + * + * Use when you need to compute what type a Lambda would produce for a + * given input. + * + * **Details** + * + * This works by intersecting the Lambda with `{ "~lambda.in": V }` and reading + * `"~lambda.out"`. + * + * **Example** (Computing the output type of a lambda) + * + * ```ts + * import type { Struct } from "effect" + * + * interface ToString extends Struct.Lambda { + * readonly "~lambda.out": string + * } + * + * // Result is `string` + * type Result = Struct.Apply + * ``` + * + * @see {@link Lambda} – the base interface + * @category Lambda + * @since 4.0.0 + */ +export type Apply = (L & { readonly "~lambda.in": V })["~lambda.out"] + +/** + * Wraps a plain function as a {@link Lambda} value so it can be used with + * {@link map}, {@link mapPick}, and {@link mapOmit}. + * + * **When to use** + * + * Use to create a typed lambda for struct mapping APIs that need type-level + * input and output tracking. + * + * **Details** + * + * The type parameter `L` encodes both the input and output types at the type + * level, allowing the compiler to track how struct value types change. At + * runtime, the returned value is the same function; `lambda` only adjusts the + * type. + * + * **Example** (Wrapping values in arrays) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe({ x: 1, y: "hello" }, Struct.map(asArray)) + * console.log(result) // { x: [1], y: ["hello"] } + * ``` + * + * @see {@link Lambda} – the type-level interface + * @see {@link map} – apply a lambda to all struct values + * @category Lambda + * @since 4.0.0 + */ +export const lambda = any>( + f: (a: Parameters[0]) => ReturnType +): L => f as any + +/** + * Applies a {@link Lambda} transformation to every value in a struct. + * + * **When to use** + * + * Use when you want to apply the same function to every value in a struct. + * + * **Details** + * + * The lambda must be created with {@link lambda} so the compiler can track the + * output types. + * + * **Example** (Wrapping every value in an array) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe({ width: 10, height: 20 }, Struct.map(asArray)) + * console.log(result) // { width: [10], height: [20] } + * ``` + * + * @see {@link mapPick} – apply a lambda only to selected keys + * @see {@link mapOmit} – apply a lambda to all keys except selected ones + * @see {@link evolve} – apply different functions to different keys + * @category mapping + * @since 4.0.0 + */ +export const map: { + ( + lambda: L + ): (self: S) => { [K in keyof S]: Apply } + ( + self: S, + lambda: L + ): { [K in keyof S]: Apply } +} = dual( + 2, + (self: S, lambda: L) => { + return buildStruct(self, (k, v) => [k, lambda(v)]) + } +) + +/** + * Applies a {@link Lambda} transformation only to the specified keys; all + * other keys are copied unchanged. + * + * **When to use** + * + * Use when you want to apply the same transformation to a subset of properties. + * + * **Example** (Wrapping only selected values in arrays) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe( + * { x: 1, y: 2, z: 3 }, + * Struct.mapPick(["x", "z"], asArray) + * ) + * console.log(result) // { x: [1], y: 2, z: [3] } + * ``` + * + * @see {@link map} – apply a lambda to all keys + * @see {@link mapOmit} – apply a lambda to all keys except selected ones + * @category mapping + * @since 4.0.0 + */ +export const mapPick: { + , L extends Lambda>( + keys: Keys, + lambda: L + ): ( + self: S + ) => { [K in keyof S]: K extends Keys[number] ? Apply : S[K] } + , L extends Lambda>( + self: S, + keys: Keys, + lambda: L + ): { [K in keyof S]: K extends Keys[number] ? Apply : S[K] } +} = dual( + 3, + , L extends Function>( + self: S, + keys: Keys, + lambda: L + ) => { + return buildStruct(self, (k, v) => [k, keys.includes(k) ? lambda(v) : v]) + } +) + +/** + * Applies a {@link Lambda} transformation to all keys except the specified + * ones; the excluded keys are copied unchanged. + * + * **When to use** + * + * Use when most keys should be transformed but a few should be preserved. + * + * **Example** (Wrapping all values except one in arrays) + * + * ```ts + * import { pipe, Struct } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe( + * { x: 1, y: 2, z: 3 }, + * Struct.mapOmit(["y"], asArray) + * ) + * console.log(result) // { x: [1], y: 2, z: [3] } + * ``` + * + * @see {@link map} – apply a lambda to all keys + * @see {@link mapPick} – apply a lambda only to selected keys + * @category mapping + * @since 4.0.0 + */ +export const mapOmit: { + , L extends Lambda>( + keys: Keys, + lambda: L + ): ( + self: S + ) => { [K in keyof S]: K extends Keys[number] ? S[K] : Apply } + , L extends Lambda>( + self: S, + keys: Keys, + lambda: L + ): { [K in keyof S]: K extends Keys[number] ? S[K] : Apply } +} = dual( + 3, + , L extends Function>( + self: S, + keys: Keys, + lambda: L + ) => { + return buildStruct(self, (k, v) => [k, !keys.includes(k) ? lambda(v) : v]) + } +) + +/** + * Walk `source`; for each key decide what to emit via the small callback. + * + * The callback returns either + * • `undefined` → nothing is copied, or + * • `[newKey, newVal]` + * + * so every public API just supplies a different callback. + */ +function buildStruct< + S extends object, + f extends (k: keyof S, v: S[keyof S]) => [PropertyKey, unknown] | undefined +>( + source: S, + f: f +): any { + const out: Record = {} + for (const k of Reflect.ownKeys(source) as Array) { + if (!Object.prototype.propertyIsEnumerable.call(source, k)) continue + const res = f(k, source[k]) + if (res) { + const [nk, nv] = res + out[nk] = nv + } + } + return out +} + +/** + * Creates a `Combiner` for a struct shape by providing a `Combiner` for each + * property. When two structs are combined, each property is merged using its + * corresponding combiner. + * + * **When to use** + * + * Use when you need to merge two same-shape records by combining each property + * independently, such as summing counters or concatenating strings. + * + * **Details** + * + * Pass `omitKeyWhen` to drop properties whose merged value matches a predicate, + * such as omitting zero counters. + * + * **Example** (Combining struct properties) + * + * ```ts + * import { Number, String, Struct } from "effect" + * + * const C = Struct.makeCombiner<{ readonly n: number; readonly s: string }>({ + * n: Number.ReducerSum, + * s: String.ReducerConcat + * }) + * + * const result = C.combine({ n: 1, s: "hello" }, { n: 2, s: " world" }) + * console.log(result) // { n: 3, s: "hello world" } + * ``` + * + * @see {@link makeReducer} – like `makeCombiner` but with an initial value + * @category combining + * @since 4.0.0 + */ +export function makeCombiner( + combiners: { readonly [K in keyof A]: Combiner.Combiner }, + options?: { + readonly omitKeyWhen?: ((a: A[keyof A]) => boolean) | undefined + } +): Combiner.Combiner { + const omitKeyWhen = options?.omitKeyWhen ?? (() => false) + return Combiner.make((self, that) => { + const keys = Reflect.ownKeys(combiners) as Array + const out = {} as A + for (const key of keys) { + const merge = combiners[key].combine(self[key], that[key]) + if (omitKeyWhen(merge)) continue + out[key] = merge + } + return out + }) +} + +/** + * Creates a `Reducer` for a struct shape by providing a `Reducer` for each + * property. The initial value is derived from each property's + * `Reducer.initialValue`. When reducing a collection of structs, each property + * is combined independently. + * + * **When to use** + * + * Use when you need to fold same-shape records by accumulating each property + * independently into one summary record. + * + * **Details** + * + * Pass `omitKeyWhen` to drop properties whose reduced value matches a + * predicate. + * + * **Example** (Reducing a collection of structs) + * + * ```ts + * import { Number, String, Struct } from "effect" + * + * const R = Struct.makeReducer<{ readonly n: number; readonly s: string }>({ + * n: Number.ReducerSum, + * s: String.ReducerConcat + * }) + * + * const result = R.combineAll([ + * { n: 1, s: "a" }, + * { n: 2, s: "b" }, + * { n: 3, s: "c" } + * ]) + * console.log(result) // { n: 6, s: "abc" } + * ``` + * + * @see {@link makeCombiner} – like `makeReducer` but without an initial value + * @category folding + * @since 4.0.0 + */ +export function makeReducer( + reducers: { readonly [K in keyof A]: Reducer.Reducer }, + options?: { + readonly omitKeyWhen?: ((a: A[keyof A]) => boolean) | undefined + } +): Reducer.Reducer { + const combine = makeCombiner(reducers, options).combine + const initialValue = {} as A + for (const key of Reflect.ownKeys(reducers) as Array) { + const iv = reducers[key].initialValue + if (options?.omitKeyWhen?.(iv)) continue + initialValue[key] = iv + } + return Reducer.make(combine, initialValue) +} + +/** + * Creates a record with the given keys and value. + * + * **When to use** + * + * Use to build an object where each provided key receives the same value. + * + * **Example** (Creating a record) + * + * ```ts + * import { Struct } from "effect" + * + * const record = Struct.Record(["a", "b"], "value") + * console.log(record) // { a: "value", b: "value" } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function Record, Value>( + keys: Keys, + value: Value +): Record { + const out: any = {} + for (const key of keys) { + out[key] = value + } + return out +} diff --git a/repos/effect/packages/effect/src/SubscriptionRef.ts b/repos/effect/packages/effect/src/SubscriptionRef.ts new file mode 100644 index 0000000..57a9f48 --- /dev/null +++ b/repos/effect/packages/effect/src/SubscriptionRef.ts @@ -0,0 +1,1021 @@ +/** + * Stores mutable state and publishes changes as a stream. + * + * A `SubscriptionRef` stores the latest value, publishes the initial value, + * and publishes every committed update so subscribers can observe state over + * time. Updates are serialized so only one change is applied at a time. This + * module includes constructors, current-value reads, the `changes` stream, + * writes, updates, partial updates, and effectful update helpers. + * + * @since 2.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual, identity } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as PubSub from "./PubSub.ts" +import * as Semaphore from "./Semaphore.ts" +import * as Stream from "./Stream.ts" +import type { Invariant } from "./Types.ts" + +const TypeId = "~effect/SubscriptionRef" + +/** + * A mutable reference whose updates are serialized and published to + * subscribers. + * + * **When to use** + * + * Use to observe the current value and subsequent updates as a + * stream. + * + * @category models + * @since 2.0.0 + */ +export interface SubscriptionRef extends SubscriptionRef.Variance, Pipeable { + value: A + readonly semaphore: Semaphore.Semaphore + readonly pubsub: PubSub.PubSub +} + +/** + * Returns `true` if the provided value is a `SubscriptionRef`. + * + * **When to use** + * + * Use to narrow an unknown value before calling `SubscriptionRef` operations + * that require a subscription reference. + * + * @category guards + * @since 4.0.0 + */ +export const isSubscriptionRef: (u: unknown) => u is SubscriptionRef = ( + u: unknown +): u is SubscriptionRef => hasProperty(u, TypeId) + +/** + * The `SubscriptionRef` namespace containing type definitions associated with + * subscription references. + * + * @since 2.0.0 + */ +export declare namespace SubscriptionRef { + /** + * Type-level variance marker for the value type carried by a + * `SubscriptionRef`. + * + * @category models + * @since 2.0.0 + */ + export interface Variance { + readonly [TypeId]: { + readonly _A: Invariant + } + } +} + +const Proto = { + ...PipeInspectableProto, + [TypeId]: { + _A: identity + }, + toJSON(this: SubscriptionRef) { + return { + _id: "SubscriptionRef", + value: this.value + } + } +} + +/** + * Constructs a new `SubscriptionRef` from an initial value. + * + * **When to use** + * + * Use to create a `SubscriptionRef` when consumers need to read the latest + * value and subscribe to every update. + * + * **Details** + * + * The initial value is published during construction, so `changes` starts new + * subscribers with that value before future updates. + * + * @see {@link changes} for streaming the current value and subsequent updates + * @see {@link set} for replacing the value and notifying subscribers + * + * @category constructors + * @since 2.0.0 + */ +export const make = (value: A): Effect.Effect> => + Effect.map(PubSub.unbounded({ replay: 1 }), (pubsub) => { + const self = Object.create(Proto) + self.semaphore = Semaphore.makeUnsafe(1) + self.value = value + self.pubsub = pubsub + PubSub.publishUnsafe(self.pubsub, value) + return self + }) + +/** + * Creates a stream that emits the current value and all subsequent changes to + * the `SubscriptionRef`. + * + * **Details** + * + * The stream will first emit the current value, then emit all future changes + * as they occur. + * + * **Example** (Streaming changes) + * + * ```ts + * import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(0) + * const ready = yield* Deferred.make() + * + * const fiber = yield* SubscriptionRef.changes(ref).pipe( + * Stream.tap(() => Deferred.succeed(ready, void 0)), + * Stream.take(3), + * Stream.runCollect, + * Effect.forkChild + * ) + * + * yield* Deferred.await(ready) + * yield* SubscriptionRef.set(ref, 1) + * yield* SubscriptionRef.set(ref, 2) + * + * const values = yield* Fiber.join(fiber) + * console.log(values) // [ 0, 1, 2 ] + * }) + * + * Effect.runPromise(program) + * ``` + * + * @category changes + * @since 4.0.0 + */ +export const changes = (self: SubscriptionRef): Stream.Stream => Stream.fromPubSub(self.pubsub) + +/** + * Retrieves the current value of the `SubscriptionRef` unsafely. + * + * **When to use** + * + * Use when you are in synchronous internals or test setup where concurrent + * updates are controlled. + * + * **Gotchas** + * + * This function directly accesses the underlying reference without any + * synchronization. It should only be used when you are certain there are no + * concurrent modifications. + * + * **Example** (Reading the current value unsafely) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(42) + * + * const value = SubscriptionRef.getUnsafe(ref) + * console.log(value) + * }) + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const getUnsafe = (self: SubscriptionRef): A => self.value + +/** + * Retrieves the current value of the `SubscriptionRef`. + * + * **Example** (Reading the current value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(42) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: SubscriptionRef): Effect.Effect => Effect.sync(() => self.value) + +/** + * Retrieves the current value and sets a new value atomically, notifying + * subscribers of the change. + * + * **Example** (Getting and setting a value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const oldValue = yield* SubscriptionRef.getAndSet(ref, 20) + * console.log("Old value:", oldValue) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getAndSet: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = dual(2, (self: SubscriptionRef, value: A) => + self.semaphore.withPermit(Effect.sync(() => { + const current = self.value + setUnsafe(self, value) + return current + }))) + +const setUnsafe = (self: SubscriptionRef, value: A) => { + self.value = value + PubSub.publishUnsafe(self.pubsub, value) +} + +/** + * Retrieves the current value and updates it atomically with the result of + * applying a function, notifying subscribers of the change. + * + * **Example** (Getting and updating a value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const oldValue = yield* SubscriptionRef.getAndUpdate(ref, (n) => n * 2) + * console.log("Old value:", oldValue) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getAndUpdate: { + (update: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => A): Effect.Effect +} = dual(2, (self: SubscriptionRef, update: (a: A) => A) => + self.semaphore.withPermit(Effect.sync(() => { + const current = self.value + const newValue = update(current) + setUnsafe(self, newValue) + return current + }))) + +/** + * Retrieves the current value and updates it atomically with the result of + * applying an effectful function, notifying subscribers of the change. + * + * **Example** (Getting and updating with an effect) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const oldValue = yield* SubscriptionRef.getAndUpdateEffect( + * ref, + * (n) => Effect.succeed(n + 5) + * ) + * console.log("Old value:", oldValue) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getAndUpdateEffect: { + (update: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Effect.Effect): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect +) => + self.semaphore.withPermit(Effect.sync(() => { + const current = self.value + return Effect.map(update(current), (newValue) => { + setUnsafe(self, newValue) + return current + }) + }))) + +/** + * Retrieves the current value and optionally updates the reference. + * + * **When to use** + * + * Use to read the old `SubscriptionRef` value while applying a synchronous + * update only when a new value is available. + * + * **Details** + * + * If the function returns `Option.some`, the new value is set and published. If + * it returns `Option.none`, the reference is left unchanged and no update is + * published. + * + * **Example** (Getting and conditionally updating a value) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const oldValue = yield* SubscriptionRef.getAndUpdateSome( + * ref, + * (n) => n > 5 ? Option.some(n * 2) : Option.none() + * ) + * console.log("Old value:", oldValue) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getAndUpdateSome: { + (update: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Option.Option): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Option.Option +) => + self.semaphore.withPermit(Effect.sync(() => { + const current = self.value + const option = update(current) + if (Option.isNone(option)) { + return Effect.succeed(current) + } + setUnsafe(self, option.value) + return current + }))) + +/** + * Retrieves the current value and optionally updates the reference effectfully. + * + * **When to use** + * + * Use to read the old `SubscriptionRef` value while applying an effectful + * update only when a new value is available. + * + * **Details** + * + * If the effect succeeds with `Option.some`, the new value is set and + * published. If it succeeds with `Option.none`, the reference is left unchanged + * and no update is published. + * + * **Example** (Getting and conditionally updating with an effect) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const oldValue = yield* SubscriptionRef.getAndUpdateSomeEffect( + * ref, + * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) + * ) + * console.log("Old value:", oldValue) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const getAndUpdateSomeEffect: { + ( + update: (a: A) => Effect.Effect, E, R> + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect, E, R> + ): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect, E, R> +) => + self.semaphore.withPermit(Effect.suspend(() => { + const current = self.value + return Effect.map(update(current), (option) => { + if (Option.isNone(option)) return current + setUnsafe(self, option.value) + return current + }) + }))) + +/** + * Modifies the `SubscriptionRef` atomically with a function that computes a + * return value and a new value, notifying subscribers of the change. + * + * **Example** (Modifying a value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const result = yield* SubscriptionRef.modify(ref, (n) => [ + * `Old value was ${n}`, + * n * 2 + * ]) + * console.log(result) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category modifications + * @since 2.0.0 + */ +export const modify: { + (modify: (a: A) => readonly [B, A]): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, f: (a: A) => readonly [B, A]): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + modify: (a: A) => readonly [B, A] +) => + self.semaphore.withPermit(Effect.sync(() => { + const [b, newValue] = modify(self.value) + setUnsafe(self, newValue) + return b + }))) + +/** + * Modifies the `SubscriptionRef` atomically with an effectful function that + * computes a return value and a new value, notifying subscribers of the + * change. + * + * **Example** (Modifying with an effect) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const result = yield* SubscriptionRef.modifyEffect( + * ref, + * (n) => Effect.succeed([`Doubled from ${n}`, n * 2] as const) + * ) + * console.log(result) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category modifications + * @since 2.0.0 + */ +export const modifyEffect: { + ( + modify: (a: A) => Effect.Effect + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + modify: (a: A) => Effect.Effect + ): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + modify: (a: A) => Effect.Effect +): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => + Effect.map(modify(self.value), ([b, newValue]) => { + setUnsafe(self, newValue) + return b + }) + ))) + +/** + * Computes a return value and optionally updates the reference. + * + * **When to use** + * + * Use to return a separate result while synchronously deciding whether to + * publish a new `SubscriptionRef` value. + * + * **Details** + * + * If the function returns `Option.some` for the new value, the value is set and + * published. If it returns `Option.none`, the reference is left unchanged and + * no update is published. + * + * **Example** (Conditionally modifying a value) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const result = yield* SubscriptionRef.modifySome( + * ref, + * (n) => + * n > 5 ? ["Updated", Option.some(n * 2)] : ["Not updated", Option.none()] + * ) + * console.log(result) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category modifications + * @since 2.0.0 + */ +export const modifySome: { + ( + modify: (a: A) => readonly [B, Option.Option] + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + modify: (a: A) => readonly [B, Option.Option] + ): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + modify: (a: A) => readonly [B, Option.Option] +) => + self.semaphore.withPermit(Effect.sync(() => { + const [b, option] = modify(self.value) + if (Option.isNone(option)) return b + setUnsafe(self, option.value) + return b + }))) + +/** + * Computes a return value and optionally updates the reference effectfully. + * + * **When to use** + * + * Use to return a separate result while effectfully deciding whether to publish + * a new `SubscriptionRef` value. + * + * **Details** + * + * If the effect succeeds with `Option.some`, the new value is set and + * published. If it succeeds with `Option.none`, the reference is left unchanged + * and no update is published. + * + * **Example** (Conditionally modifying with an effect) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const result = yield* SubscriptionRef.modifySomeEffect( + * ref, + * (n) => + * Effect.succeed( + * n > 5 + * ? (["Updated", Option.some(n + 5)] as const) + * : (["Not updated", Option.none()] as const) + * ) + * ) + * console.log(result) + * + * const newValue = yield* SubscriptionRef.get(ref) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category modifications + * @since 2.0.0 + */ +export const modifySomeEffect: { + ( + modify: (a: A) => Effect.Effect], E, R> + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + modify: (a: A) => Effect.Effect], E, R> + ): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + modify: (a: A) => Effect.Effect], E, R> +) => + self.semaphore.withPermit(Effect.suspend(() => + Effect.map(modify(self.value), ([b, option]) => { + if (Option.isNone(option)) return b + setUnsafe(self, option.value) + return b + }) + ))) + +/** + * Sets the value of the `SubscriptionRef`, notifying all subscribers of the + * change. + * + * **Example** (Setting a value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(0) + * + * yield* SubscriptionRef.set(ref, 42) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category setters + * @since 2.0.0 + */ +export const set: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = dual( + 2, + (self: SubscriptionRef, value: A) => self.semaphore.withPermit(Effect.sync(() => setUnsafe(self, value))) +) + +/** + * Sets the value of the `SubscriptionRef` and returns the new value, + * notifying all subscribers of the change. + * + * **Example** (Setting and reading the new value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(0) + * + * const newValue = yield* SubscriptionRef.setAndGet(ref, 42) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category setters + * @since 2.0.0 + */ +export const setAndGet: { + (value: A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, value: A): Effect.Effect +} = dual(2, (self: SubscriptionRef, value: A) => + self.semaphore.withPermit(Effect.sync(() => { + setUnsafe(self, value) + return value + }))) + +/** + * Updates the value of the `SubscriptionRef` with the result of applying a + * function, notifying subscribers of the change. + * + * **Example** (Updating a value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * yield* SubscriptionRef.update(ref, (n) => n * 2) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const update: { + (update: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => A): Effect.Effect +} = dual( + 2, + (self: SubscriptionRef, update: (a: A) => A) => + self.semaphore.withPermit(Effect.sync(() => setUnsafe(self, update(self.value)))) +) + +/** + * Updates the value of the `SubscriptionRef` with the result of applying an + * effectful function, notifying subscribers of the change. + * + * **Example** (Updating with an effect) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5)) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateEffect: { + (update: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Effect.Effect): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect +) => + self.semaphore.withPermit( + Effect.suspend(() => Effect.map(update(self.value), (newValue) => setUnsafe(self, newValue))) + )) + +/** + * Updates the value of the `SubscriptionRef` with the result of applying a + * function and returns the new value, notifying subscribers of the change. + * + * **Example** (Updating and reading the new value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const newValue = yield* SubscriptionRef.updateAndGet(ref, (n) => n * 2) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateAndGet: { + (update: (a: A) => A): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => A): Effect.Effect +} = dual(2, (self: SubscriptionRef, update: (a: A) => A) => + self.semaphore.withPermit(Effect.sync(() => { + const newValue = update(self.value) + setUnsafe(self, newValue) + return newValue + }))) + +/** + * Updates the value of the `SubscriptionRef` with the result of applying an + * effectful function and returns the new value, notifying subscribers of the + * change. + * + * **Example** (Updating with an effect and reading the new value) + * + * ```ts + * import { Effect, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const newValue = yield* SubscriptionRef.updateAndGetEffect( + * ref, + * (n) => Effect.succeed(n + 5) + * ) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateAndGetEffect: { + (update: (a: A) => Effect.Effect): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Effect.Effect): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect +) => + self.semaphore.withPermit(Effect.suspend(() => + Effect.map(update(self.value), (newValue) => { + setUnsafe(self, newValue) + return newValue + }) + ))) + +/** + * Applies an update function to the current value. If it returns + * `Option.some`, sets and publishes that value; if it returns `Option.none`, + * leaves the reference unchanged and does not publish. + * + * **Example** (Conditionally updating a value) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * yield* SubscriptionRef.updateSome( + * ref, + * (n) => n > 5 ? Option.some(n * 2) : Option.none() + * ) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateSome: { + (update: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Option.Option): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Option.Option +) => + self.semaphore.withPermit(Effect.sync(() => { + const option = update(self.value) + if (Option.isNone(option)) return + setUnsafe(self, option.value) + }))) + +/** + * Applies an effectful update only when it produces a new value. + * + * **When to use** + * + * Use to conditionally update a `SubscriptionRef` with an effectful function + * while discarding the resulting value. + * + * **Details** + * + * If the effect succeeds with `Option.some`, the new value is set and + * published. If it succeeds with `Option.none`, the reference is left unchanged + * and no update is published. + * + * **Example** (Conditionally updating with an effect) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * yield* SubscriptionRef.updateSomeEffect( + * ref, + * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) + * ) + * + * const value = yield* SubscriptionRef.get(ref) + * console.log(value) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateSomeEffect: { + ( + update: (a: A) => Effect.Effect, E, R> + ): (self: SubscriptionRef) => Effect.Effect + ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect, E, R> + ): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect, E, R> +) => + self.semaphore.withPermit(Effect.suspend(() => + Effect.map(update(self.value), (option) => { + if (Option.isNone(option)) return + setUnsafe(self, option.value) + }) + ))) + +/** + * Applies an optional update and returns the current value afterward. + * + * **When to use** + * + * Use to conditionally update a `SubscriptionRef` and read the value that is + * current after the update decision. + * + * **Details** + * + * If the function returns `Option.some`, the new value is set, published, and + * returned. If it returns `Option.none`, the unchanged current value is + * returned without publishing. + * + * **Example** (Conditionally updating and reading the new value) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const newValue = yield* SubscriptionRef.updateSomeAndGet( + * ref, + * (n) => n > 5 ? Option.some(n * 2) : Option.none() + * ) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateSomeAndGet: { + (update: (a: A) => Option.Option): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Option.Option): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Option.Option +) => + self.semaphore.withPermit(Effect.sync(() => { + const current = self.value + const option = update(current) + if (Option.isNone(option)) return current + setUnsafe(self, option.value) + return option.value + }))) + +/** + * Applies an effectful optional update and returns the current value afterward. + * + * **When to use** + * + * Use to conditionally update a `SubscriptionRef` effectfully and read the + * value that is current after the update decision. + * + * **Details** + * + * If the effect succeeds with `Option.some`, the new value is set, published, + * and returned. If it succeeds with `Option.none`, the unchanged current value + * is returned without publishing. + * + * **Example** (Conditionally updating with an effect and reading the new value) + * + * ```ts + * import { Effect, Option, SubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* SubscriptionRef.make(10) + * + * const newValue = yield* SubscriptionRef.updateSomeAndGetEffect( + * ref, + * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) + * ) + * console.log("New value:", newValue) + * }) + * ``` + * + * @category updating + * @since 2.0.0 + */ +export const updateSomeAndGetEffect: { + ( + update: (a: A) => Effect.Effect, E, R> + ): (self: SubscriptionRef) => Effect.Effect + (self: SubscriptionRef, update: (a: A) => Effect.Effect, E, R>): Effect.Effect +} = dual(2, ( + self: SubscriptionRef, + update: (a: A) => Effect.Effect, E, R> +) => + self.semaphore.withPermit(Effect.suspend(() => { + const current = self.value + return Effect.map(update(current), (option) => { + if (Option.isNone(option)) return current + setUnsafe(self, option.value) + return option.value + }) + }))) diff --git a/repos/effect/packages/effect/src/Symbol.ts b/repos/effect/packages/effect/src/Symbol.ts new file mode 100644 index 0000000..b359115 --- /dev/null +++ b/repos/effect/packages/effect/src/Symbol.ts @@ -0,0 +1,31 @@ +/** + * The `Symbol` module contains the runtime predicate for JavaScript primitive + * `symbol` values. It is most useful at boundaries where a value is `unknown` + * and must be narrowed before it can be used as a symbol key, identifier, or + * discriminant. + * + * @since 2.0.0 + */ + +import * as predicate from "./Predicate.ts" + +/** + * Checks whether a value is a `symbol`. + * + * **When to use** + * + * Use to validate unknown input before treating it as a JavaScript `symbol`. + * + * **Example** (Checking for symbols) + * + * ```ts + * import { Symbol } from "effect" + * + * console.log(Symbol.isSymbol(globalThis.Symbol.for("a"))) // true + * console.log(Symbol.isSymbol("a")) // false + * ``` + * + * @category guards + * @since 2.0.0 + */ +export const isSymbol: (u: unknown) => u is symbol = predicate.isSymbol diff --git a/repos/effect/packages/effect/src/SynchronizedRef.ts b/repos/effect/packages/effect/src/SynchronizedRef.ts new file mode 100644 index 0000000..2828496 --- /dev/null +++ b/repos/effect/packages/effect/src/SynchronizedRef.ts @@ -0,0 +1,668 @@ +/** + * Stores mutable state whose updates run one at a time. + * + * A `SynchronizedRef` behaves like a `Ref` for reading and simple state + * storage, but update and modify operations are serialized so each change sees + * a consistent current value. This is especially useful when the next value is + * computed by an effect, because the effectful transition is still protected + * from concurrent updates. This module includes constructors, reads, writes, + * updates, partial updates, and effectful update helpers. + * + * @since 2.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import { PipeInspectableProto } from "./internal/core.ts" +import * as Option from "./Option.ts" +import * as Ref from "./Ref.ts" +import * as Semaphore from "./Semaphore.ts" + +const TypeId = "~effect/SynchronizedRef" + +/** + * A mutable reference whose update and modify operations are serialized with an + * internal semaphore, including effectful transformations. + * + * **When to use** + * + * Use when shared state may be updated by multiple fibers and each update, + * including effectful state transitions, must observe one current value and run + * one at a time. + * + * @see {@link Ref.Ref} for a plain `Ref` when updates do not need effectful synchronization + * + * @category models + * @since 2.0.0 + */ +export interface SynchronizedRef extends Ref.Ref { + readonly [TypeId]: typeof TypeId + readonly backing: Ref.Ref + readonly semaphore: Semaphore.Semaphore +} + +const Proto = { + ...PipeInspectableProto, + [TypeId]: TypeId, + toJSON(this: SynchronizedRef) { + return { + _id: "SynchronizedRef", + value: this.backing.ref.current + } + } +} + +/** + * Creates a `SynchronizedRef` synchronously from an initial value. + * + * **When to use** + * + * Use when you need synchronous `SynchronizedRef` construction outside an + * Effect workflow. + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (value: A): SynchronizedRef => { + const self = Object.create(Proto) + self.semaphore = Semaphore.makeUnsafe(1) + self.backing = Ref.makeUnsafe(value) + return self +} + +/** + * Creates a `SynchronizedRef` from an initial value, wrapped in an `Effect`. + * + * **When to use** + * + * Use to create a `SynchronizedRef` inside an Effect program when later updates + * may run effects and must be serialized. + * + * **Details** + * + * The returned effect constructs a fresh `SynchronizedRef` by delegating to + * `makeUnsafe` when the effect is evaluated. + * + * @see {@link makeUnsafe} for synchronous construction when the caller controls safe initialization + * @see {@link Ref.make} for a plain `Ref` when updates do not need effectful synchronization + * + * @category constructors + * @since 2.0.0 + */ +export const make = (value: A): Effect.Effect> => Effect.sync(() => makeUnsafe(value)) + +/** + * Reads the current value synchronously, bypassing the `Effect` API and the + * ref's semaphore. + * + * **When to use** + * + * Use when you need immediate synchronous access to a `SynchronizedRef` value + * in low-level code that can safely read outside an `Effect`. + * + * @see {@link get} for the Effect-wrapped read when composing inside Effect programs + * + * @category getters + * @since 4.0.0 + */ +export const getUnsafe = (self: SynchronizedRef): A => self.backing.ref.current + +/** + * Returns an `Effect` that reads the current value of the `SynchronizedRef`. + * + * **When to use** + * + * Use to read the current value of a `SynchronizedRef` inside an `Effect` + * program without changing it. + * + * @see {@link getUnsafe} for synchronous reads when the caller controls safe access outside `Effect` + * + * @category getters + * @since 2.0.0 + */ +export const get = (self: SynchronizedRef): Effect.Effect => Effect.sync(() => getUnsafe(self)) + +/** + * Sets a new value atomically and returns the previous value, serialized by the + * ref's semaphore. + * + * **When to use** + * + * Use to replace a `SynchronizedRef` with a known value when the previous value + * is also needed. + * + * @see {@link set} for setting a value without returning the previous value + * @see {@link setAndGet} for setting a value and returning the new value + * @see {@link getAndUpdate} for deriving the new value from the current value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndSet: { + (value: A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, value: A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, value: A): Effect.Effect => + self.semaphore.withPermit(Ref.getAndSet(self.backing, value)) +) + +/** + * Updates the current value atomically with a function and returns the previous + * value, serialized by the ref's semaphore. + * + * **When to use** + * + * Use to run a pure `SynchronizedRef` state update when the previous stored + * value is also needed. + * + * @see {@link update} for updating without returning a value + * @see {@link updateAndGet} for updating and returning the new value + * @see {@link getAndUpdateEffect} for effectful updates that return the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdate: { + (f: (a: A) => A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect => + self.semaphore.withPermit(Ref.getAndUpdate(self.backing, f)) +) + +/** + * Runs an effectful update atomically while holding the ref's semaphore, sets + * the new value if the effect succeeds, and returns the previous value. + * + * **When to use** + * + * Use when you need an effectful `SynchronizedRef` state transition to return + * the previous stored value. + * + * @see {@link getAndUpdate} for pure updates that return the previous value + * @see {@link updateEffect} for effectful updates without returning a value + * @see {@link updateAndGetEffect} for effectful updates that return the new value + * @see {@link modifyEffect} for effectful updates with a custom return value + * @see {@link getAndUpdateSomeEffect} for conditional effectful updates that return the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdateEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.map(f(value), (newValue) => { + self.backing.ref.current = newValue + return value + }) + })) +) + +/** + * Applies a partial update atomically and returns the previous value. If the + * function returns `Option.some`, the ref is updated; if it returns + * `Option.none`, the ref is left unchanged. + * + * **When to use** + * + * Use to return the previous `SynchronizedRef` value while applying a pure + * conditional update. + * + * @see {@link getAndUpdate} for always applying a pure update + * @see {@link updateSome} for applying a pure conditional update without returning the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdateSome: { + (pf: (a: A) => Option.Option): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Option.Option): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, pf: (a: A) => Option.Option): Effect.Effect => + self.semaphore.withPermit(Ref.getAndUpdateSome(self, pf)) +) + +/** + * Runs an effectful partial update atomically while holding the ref's semaphore + * and returns the previous value. `Option.some` updates the ref; `Option.none` + * leaves it unchanged. + * + * **When to use** + * + * Use to return the previous `SynchronizedRef` value while running an effectful + * conditional update. + * + * @see {@link getAndUpdateSome} for the pure conditional variant + * @see {@link updateSomeEffect} for effectful conditional updates without returning the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const getAndUpdateSomeEffect: { + (pf: (a: A) => Effect.Effect, E, R>): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.flatMap(pf(value), (option) => { + if (Option.isNone(option)) { + return Effect.succeed(value) + } + self.backing.ref.current = option.value + return Effect.succeed(value) + }) + })) +) + +/** + * Computes a return value and a new ref value atomically, stores the new value, + * and returns the computed result. + * + * **When to use** + * + * Use to derive a separate result and the next stored `SynchronizedRef` value + * from the same current value in one serialized pure update. + * + * @see {@link modifyEffect} for effectfully deriving both the result and next stored value + * @see {@link modifySome} for deriving a result and optionally updating the stored value + * @see {@link updateAndGet} for returning the new stored value instead of a separate result + * + * @category mutations + * @since 2.0.0 + */ +export const modify: { + (f: (a: A) => readonly [B, A]): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => readonly [B, A]): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => readonly [B, A]): Effect.Effect => + self.semaphore.withPermit(Ref.modify(self.backing, f)) +) + +/** + * Runs an effectful modification atomically while holding the ref's semaphore, + * stores the new value if the effect succeeds, and returns the computed result. + * + * **When to use** + * + * Use to effectfully compute both a separate return value and the next stored + * `SynchronizedRef` value in one serialized update. + * + * @see {@link modify} for the pure variant + * @see {@link updateEffect} for effectfully storing a new value without a separate result + * + * @category mutations + * @since 2.0.0 + */ +export const modifyEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.map(f(value), ([b, a]) => { + self.backing.ref.current = a + return b + }) + })) +) + +/** + * Computes a return value and an optional new ref value atomically. + * `Option.some` updates the ref; `Option.none` leaves it unchanged. + * + * **When to use** + * + * Use to compute a return value while optionally updating a `SynchronizedRef` + * under its semaphore. + * + * @see {@link modify} for always storing a new value + * @see {@link updateSome} for optional updates without a separate return value + * + * @category mutations + * @since 2.0.0 + */ +export const modifySome: { + ( + pf: (a: A) => readonly [B, Option.Option] + ): (self: SynchronizedRef) => Effect.Effect + ( + self: SynchronizedRef, + pf: (a: A) => readonly [B, Option.Option] + ): Effect.Effect +} = dual( + 2, + ( + self: SynchronizedRef, + pf: (a: A) => readonly [B, Option.Option] + ): Effect.Effect => self.semaphore.withPermit(Ref.modifySome(self.backing, pf)) +) + +/** + * Runs an effectful modification atomically while holding the ref's semaphore. + * The effect computes a return value and an optional new ref value; + * `Option.some` updates the ref and `Option.none` leaves it unchanged. + * + * **When to use** + * + * Use to effectfully compute a return value while optionally updating the + * stored `SynchronizedRef` value. + * + * @see {@link modifySome} for the pure variant + * @see {@link updateSomeEffect} for effectful optional updates without a separate return value + * + * @category mutations + * @since 2.0.0 + */ +export const modifySomeEffect: { + ( + fallback: B, + pf: (a: A) => Effect.Effect], E, R> + ): (self: SynchronizedRef) => Effect.Effect + ( + self: SynchronizedRef, + pf: (a: A) => Effect.Effect], E, R> + ): Effect.Effect +} = dual( + 2, + ( + self: SynchronizedRef, + pf: (a: A) => Effect.Effect], E, R> + ): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.flatMap(pf(value), ([b, maybeA]) => { + if (Option.isNone(maybeA)) { + return Effect.succeed(b) + } + self.backing.ref.current = maybeA.value + return Effect.succeed(b) + }) + })) +) + +/** + * Sets the value of the `SynchronizedRef`, serialized by the ref's semaphore. + * + * **When to use** + * + * Use to replace the current value of a `SynchronizedRef` with a known value + * while keeping the write serialized with other synchronized updates. + * + * @see {@link getAndSet} for replacing the value when the previous value is needed + * @see {@link setAndGet} for replacing the value when the new value should be returned + * @see {@link update} for deriving the next value from the current value + * + * @category mutations + * @since 2.0.0 + */ +export const set: { + (value: A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, value: A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, value: A): Effect.Effect => + self.semaphore.withPermit(Ref.set(self.backing, value)) +) + +/** + * Sets the value of the `SynchronizedRef` and returns the new value. + * + * **When to use** + * + * Use to replace the current `SynchronizedRef` value with a known value and + * return that new value. + * + * @see {@link set} for setting without returning a value + * @see {@link getAndSet} for setting while returning the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const setAndGet: { + (value: A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, value: A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, value: A): Effect.Effect => + self.semaphore.withPermit(Ref.setAndGet(self.backing, value)) +) + +/** + * Updates the value of the `SynchronizedRef` with a function, serialized by the + * ref's semaphore. + * + * **When to use** + * + * Use to apply a pure state transition to a `SynchronizedRef` as a serialized + * `Effect`. + * + * @see {@link updateEffect} for effectfully deriving the next value + * @see {@link updateAndGet} for returning the new stored value + * @see {@link getAndUpdate} for returning the previous stored value + * + * @category mutations + * @since 2.0.0 + */ +export const update: { + (f: (a: A) => A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect => + self.semaphore.withPermit(Ref.update(self.backing, f)) +) + +/** + * Runs an effectful update while holding the ref's semaphore and stores the new + * value if the effect succeeds. + * + * **When to use** + * + * Use to run an effectful state transition on a `SynchronizedRef` when storing + * the new value is the only result you need. + * + * @see {@link update} for a pure state transition + * @see {@link getAndUpdateEffect} for returning the previous stored value + * @see {@link updateAndGetEffect} for returning the new stored value + * @see {@link modifyEffect} for returning a separate result while storing a new value + * @see {@link updateSomeEffect} for effectfully applying only some state transitions + * + * @category mutations + * @since 2.0.0 + */ +export const updateEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.map(f(value), (newValue) => { + self.backing.ref.current = newValue + }) + })) +) + +/** + * Updates the value of the `SynchronizedRef` with a function and returns the + * new value. + * + * **When to use** + * + * Use to apply a pure `SynchronizedRef` state transition and return the new + * stored value. + * + * @see {@link update} for updating without returning the new value + * @see {@link getAndUpdate} for updating while returning the previous value + * + * @category mutations + * @since 2.0.0 + */ +export const updateAndGet: { + (f: (a: A) => A): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => A): Effect.Effect => + self.semaphore.withPermit(Ref.updateAndGet(self.backing, f)) +) + +/** + * Runs an effectful update while holding the ref's semaphore, stores the new + * value if the effect succeeds, and returns that new value. + * + * **When to use** + * + * Use to run an effectful `SynchronizedRef` state transition and return the new + * stored value. + * + * @see {@link updateEffect} for effectful updates without returning the new value + * @see {@link updateAndGet} for the pure variant + * + * @category mutations + * @since 2.0.0 + */ +export const updateAndGetEffect: { + (f: (a: A) => Effect.Effect): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => Effect.Effect): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.map(f(value), (newValue) => { + self.backing.ref.current = newValue + return newValue + }) + })) +) + +/** + * Applies a partial update to the current value. `Option.some` stores the new + * value; `Option.none` leaves the ref unchanged. + * + * **When to use** + * + * Use to apply a pure conditional `SynchronizedRef` update without returning a + * value. + * + * @see {@link update} for always applying a pure update + * @see {@link updateSomeAndGet} for returning the resulting current value + * + * @category mutations + * @since 2.0.0 + */ +export const updateSome: { + (f: (a: A) => Option.Option): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, f: (a: A) => Option.Option): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, f: (a: A) => Option.Option): Effect.Effect => + self.semaphore.withPermit(Ref.updateSome(self.backing, f)) +) + +/** + * Runs an effectful partial update while holding the ref's semaphore. + * `Option.some` stores the new value; `Option.none` leaves the ref unchanged. + * + * **When to use** + * + * Use to run an effectful conditional `SynchronizedRef` update without + * returning a value. + * + * @see {@link updateSome} for the pure conditional variant + * @see {@link updateEffect} for effectful updates that always store a new value + * + * @category mutations + * @since 2.0.0 + */ +export const updateSomeEffect: { + ( + pf: (a: A) => Effect.Effect, E, R> + ): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.map(pf(value), (option) => { + if (Option.isNone(option)) { + return + } + self.backing.ref.current = option.value + }) + })) +) + +/** + * Applies a partial update and returns the resulting current value. + * `Option.some` stores and returns the new value; `Option.none` returns the + * unchanged value. + * + * **When to use** + * + * Use to apply a pure conditional `SynchronizedRef` update and return the + * resulting current value. + * + * @see {@link updateSome} for conditional updates without returning a value + * @see {@link updateAndGet} for always applying a pure update and returning the new value + * + * @category mutations + * @since 2.0.0 + */ +export const updateSomeAndGet: { + (pf: (a: A) => Option.Option): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Option.Option): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, pf: (a: A) => Option.Option): Effect.Effect => + self.semaphore.withPermit(Ref.updateSomeAndGet(self.backing, pf)) +) + +/** + * Runs an effectful partial update while holding the ref's semaphore and + * returns the resulting current value. `Option.some` stores and returns the new + * value; `Option.none` returns the unchanged value. + * + * **When to use** + * + * Use to run an effectful conditional `SynchronizedRef` update and return the + * resulting current value. + * + * @see {@link updateSomeEffect} for effectful conditional updates without returning a value + * @see {@link updateAndGetEffect} for effectful updates that always store and return a new value + * + * @category mutations + * @since 2.0.0 + */ +export const updateSomeAndGetEffect: { + (pf: (a: A) => Effect.Effect, E, R>): (self: SynchronizedRef) => Effect.Effect + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect +} = dual( + 2, + (self: SynchronizedRef, pf: (a: A) => Effect.Effect, E, R>): Effect.Effect => + self.semaphore.withPermit(Effect.suspend(() => { + const value = getUnsafe(self) + return Effect.flatMap(pf(value), (option) => { + if (Option.isNone(option)) { + return Effect.succeed(value) + } + self.backing.ref.current = option.value + return Effect.succeed(option.value) + }) + })) +) diff --git a/repos/effect/packages/effect/src/Take.ts b/repos/effect/packages/effect/src/Take.ts new file mode 100644 index 0000000..48c2dd7 --- /dev/null +++ b/repos/effect/packages/effect/src/Take.ts @@ -0,0 +1,46 @@ +/** + * The `Take` module provides the stored representation of one pull result from + * a stream-like producer. A `Take` is either a non-empty batch of + * emitted values, a failed `Exit`, or a successful `Exit` carrying the + * completion value. + * + * @since 2.0.0 + */ +import type { NonEmptyReadonlyArray } from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Effect from "./Effect.ts" +import * as Exit from "./Exit.ts" +import type * as Pull from "./Pull.ts" + +/** + * Represents one pull result: either a non-empty batch of values, a failure + * `Exit`, or a successful `Exit` that signals completion with a `Done` value. + * + * **When to use** + * + * Use to store, transfer, or interpret pull results later while preserving + * emitted values, failures, and normal completion. + * + * @see {@link toPull} for interpreting a `Take` as a `Pull` step + * + * @category models + * @since 2.0.0 + */ +export type Take = NonEmptyReadonlyArray | Exit.Exit + +/** + * Converts a `Take` into a `Pull`, succeeding with value batches, failing with + * failure exits, and translating successful exits into pull completion. + * + * **When to use** + * + * Use to interpret a stored or transferred `Take` as a `Pull` step while + * preserving emitted batches, ordinary failures, and completion values. + * + * @category converting + * @since 4.0.0 + */ +export const toPull = (take: Take): Pull.Pull, E, Done> => + Exit.isExit(take) + ? Exit.isSuccess(take) ? Cause.done(take.value) : (take as Exit.Exit) + : Effect.succeed(take) diff --git a/repos/effect/packages/effect/src/Terminal.ts b/repos/effect/packages/effect/src/Terminal.ts new file mode 100644 index 0000000..f591756 --- /dev/null +++ b/repos/effect/packages/effect/src/Terminal.ts @@ -0,0 +1,188 @@ +/** + * Service definition for interactive terminal capabilities. Programs can query + * terminal dimensions, read a line of input, receive low-level key events, and + * display text without depending directly on a specific platform implementation. + * + * This module defines the `Terminal` service, input event shapes, key metadata, + * the `QuitError` used when a user cancels input, a guard for that error, and a + * constructor for custom terminal service implementations. + * + * @since 4.0.0 + */ +import type * as Cause from "./Cause.ts" +import * as Context from "./Context.ts" +import type * as Effect from "./Effect.ts" +import type * as Option from "./Option.ts" +import type { PlatformError } from "./PlatformError.ts" +import * as Predicate from "./Predicate.ts" +import type * as Queue from "./Queue.ts" +import * as Schema from "./Schema.ts" +import type * as Scope from "./Scope.ts" + +const TypeId = "~effect/platform/Terminal" + +/** + * A `Terminal` represents a command-line interface which can read input from a + * user and display messages to a user. + * + * @category models + * @since 4.0.0 + */ +export interface Terminal { + readonly [TypeId]: typeof TypeId + + /** + * The number of columns available on the platform's terminal interface. + */ + readonly columns: Effect.Effect + /** + * The number of rows available on the platform's terminal interface. + */ + + readonly rows: Effect.Effect + /** + * Reads input events from the default standard input. + */ + readonly readInput: Effect.Effect, never, Scope.Scope> + /** + * Reads a single line from the default standard input. + */ + readonly readLine: Effect.Effect + /** + * Displays text to the default standard output. + */ + readonly display: (text: string) => Effect.Effect +} + +/** + * Keyboard key metadata for terminal input, including the key name and + * modifier state. + * + * @category models + * @since 4.0.0 + */ +export interface Key { + /** + * The name of the key being pressed. + */ + readonly name: string + /** + * If set to `true`, then the user is also holding down the `Ctrl` key. + */ + readonly ctrl: boolean + /** + * If set to `true`, then the user is also holding down the `Meta` key. + */ + readonly meta: boolean + /** + * If set to `true`, then the user is also holding down the `Shift` key. + */ + readonly shift: boolean +} + +/** + * A terminal input event containing an optional raw character and the parsed + * key that was pressed. + * + * **When to use** + * + * Use when consuming low-level terminal input events from `Terminal.readInput` + * and you need both raw character input and parsed key metadata. + * + * @see {@link Key} for the parsed key metadata stored on each input event + * + * @category models + * @since 4.0.0 + */ +export interface UserInput { + /** + * The character read from the user (if any). + */ + readonly input: Option.Option + /** + * The key that the user pressed. + */ + readonly key: Key +} + +const QuitErrorTypeId = "effect/platform/Terminal/QuitError" + +/** + * Represents an error that occurs when a user attempts to + * quit out of a `Terminal` prompt for input (usually by entering `ctrl`+`c`). + * + * **When to use** + * + * Use when implementing terminal input or prompts that need to signal + * user-requested cancellation through the typed error channel. + * + * @see {@link isQuitError} for checking unknown errors when handling terminal cancellation + * + * @category QuitError + * @since 4.0.0 + */ +export class QuitError extends Schema.ErrorClass("QuitError")({ + _tag: Schema.tag("QuitError") +}) { + /** + * Marks this value as a terminal quit error for runtime guards. + * + * @since 4.0.0 + */ + readonly [QuitErrorTypeId] = QuitErrorTypeId +} + +/** + * Returns `true` if the provided value is a `Terminal.QuitError`. + * + * **When to use** + * + * Use to narrow unknown failures to `QuitError` when handling terminal input + * cancellation. + * + * **Details** + * + * Returns `true` when the value carries the `QuitError` runtime marker and + * narrows it to `QuitError`. + * + * @see {@link QuitError} for the error value produced when terminal input is quit + * + * @category guards + * @since 4.0.0 + */ +export const isQuitError = (u: unknown): u is QuitError => Predicate.hasProperty(u, QuitErrorTypeId) + +/** + * Service tag for command-line input and output services. + * + * **When to use** + * + * Use to access or provide platform terminal capabilities such as reading + * input, writing output, and inspecting terminal dimensions. + * + * @category services + * @since 4.0.0 + */ +export const Terminal: Context.Service = Context.Service("effect/platform/Terminal") + +/** + * Creates a `Terminal` service implementation. + * + * **When to use** + * + * Use to construct a custom `Terminal` service implementation from concrete + * terminal capabilities when writing a platform adapter, test implementation, + * or custom runtime service. + * + * **Details** + * + * The implementation object supplies `columns`, `rows`, `readInput`, + * `readLine`, and `display`; `make` attaches the `Terminal` service marker so + * the result can be provided through the `Terminal` context service. + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + impl: Omit +): Terminal => Terminal.of({ ...impl, [TypeId]: TypeId }) diff --git a/repos/effect/packages/effect/src/Tracer.ts b/repos/effect/packages/effect/src/Tracer.ts new file mode 100644 index 0000000..028829d --- /dev/null +++ b/repos/effect/packages/effect/src/Tracer.ts @@ -0,0 +1,683 @@ +/** + * Defines the low-level tracing model used by Effect. + * + * A span records the lifetime of an operation, including its name, parent, + * attributes, links, annotations, sampling decision, kind, and completion + * status. The module also defines the tracer service, parent-span context, + * external span support, trace propagation settings, and the default in-memory + * span implementation. + * + * @since 2.0.0 + */ +import * as Context from "./Context.ts" +import type * as Exit from "./Exit.ts" +import type { Fiber } from "./Fiber.ts" +import { constFalse, type LazyArg } from "./Function.ts" +import type * as core from "./internal/core.ts" +import type { LogLevel } from "./LogLevel.ts" +import * as Option from "./Option.ts" + +/** + * A tracing backend used by Effect to create spans. Custom tracers implement + * `span` to allocate a span from the supplied name, parent, annotations, + * links, start time, kind, root flag, and sampling decision. + * + * @category models + * @since 2.0.0 + */ +export interface Tracer { + span(this: Tracer, options: { + readonly name: string + readonly parent: Option.Option + readonly annotations: Context.Context + readonly links: Array + readonly startTime: bigint + readonly kind: SpanKind + readonly root: boolean + readonly sampled: boolean + }): Span + readonly context?: + | ((primitive: EffectPrimitive, fiber: Fiber) => X) + | undefined +} + +const evaluate = "~effect/Effect/evaluate" satisfies core.evaluate + +/** + * A low-level Effect primitive that can be evaluated by a tracer-specific + * context for the current fiber. + * + * @category models + * @since 4.0.0 + */ +export interface EffectPrimitive { + [evaluate](this: EffectPrimitive, fiber: Fiber): X +} + +/** + * Lifecycle state of a span, where `Started` records the start time and + * `Ended` records the start time, end time, and exit value with which the span + * completed. + * + * **Example** (Creating span statuses) + * + * ```ts + * import { Exit } from "effect" + * import type { Tracer } from "effect" + * + * const startTime = 1_000_000_000n + * const endTime = 1_500_000_000n + * + * const startedStatus: Tracer.SpanStatus = { + * _tag: "Started", + * startTime + * } + * + * const endedStatus: Tracer.SpanStatus = { + * _tag: "Ended", + * startTime, + * endTime, + * exit: Exit.succeed("result") + * } + * + * console.log(startedStatus._tag) // "Started" + * console.log(endedStatus.endTime - endedStatus.startTime) // 500000000n + * ``` + * + * @category models + * @since 2.0.0 + */ +export type SpanStatus = { + _tag: "Started" + startTime: bigint +} | { + _tag: "Ended" + startTime: bigint + endTime: bigint + exit: Exit.Exit +} + +/** + * A span value that can participate in tracing, either an Effect-managed + * `Span` or an `ExternalSpan` propagated from another tracing system. + * + * **Example** (Accepting any span) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Function that accepts any span type + * const logSpan = (span: Tracer.AnySpan) => { + * console.log(`Span ID: ${span.spanId}, Trace ID: ${span.traceId}`) + * return Effect.succeed(span) + * } + * + * // Works with both Span and ExternalSpan + * const externalSpan = Tracer.externalSpan({ + * spanId: "span-123", + * traceId: "trace-456" + * }) + * ``` + * + * @category models + * @since 2.0.0 + */ +export type AnySpan = Span | ExternalSpan + +/** + * Defines the string key for the parent-span context service. + * + * **When to use** + * + * Use when you need the raw context key for parent span lookup in lower-level + * tracing code. + * + * **Example** (Reading the parent span key) + * + * ```ts + * import { Tracer } from "effect" + * + * // The key used to identify parent spans in the context + * console.log(Tracer.ParentSpanKey) // "effect/Tracer/ParentSpan" + * ``` + * + * @category constants + * @since 4.0.0 + */ +export const ParentSpanKey = "effect/Tracer/ParentSpan" + +/** + * Context service containing the `Span` or `ExternalSpan` to use as the parent + * of newly-created child spans. + * + * **Example** (Accessing the parent span) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Access the parent span from the context + * const program = Effect.gen(function*() { + * const parentSpan = yield* Effect.service(Tracer.ParentSpan) + * console.log(`Parent span: ${parentSpan.spanId}`) + * }) + * ``` + * + * @category services + * @since 2.0.0 + */ +export class ParentSpan extends Context.Service()(ParentSpanKey) {} + +/** + * Represents a span created outside Effect's tracer, carrying trace and span + * identifiers, sampling state, and annotations so it can be used as a parent or + * link in Effect tracing. + * + * **Example** (Creating an external span value) + * + * ```ts + * import { Context } from "effect" + * import type { Tracer } from "effect" + * + * // Create an external span from another tracing system + * const externalSpan: Tracer.ExternalSpan = { + * _tag: "ExternalSpan", + * spanId: "span-abc-123", + * traceId: "trace-xyz-789", + * sampled: true, + * annotations: Context.empty() + * } + * + * console.log(`External span: ${externalSpan.spanId}`) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface ExternalSpan { + readonly _tag: "ExternalSpan" + readonly spanId: string + readonly traceId: string + readonly sampled: boolean + readonly annotations: Context.Context +} + +/** + * Options accepted by span-creating APIs, combining span metadata such as + * attributes, links, parent/root selection, kind, sampling, and trace level + * with stack trace capture settings. + * + * **Example** (Configuring span options) + * + * ```ts + * import { Effect } from "effect" + * import type { Tracer } from "effect" + * + * // Create an effect with span options + * const options: Tracer.SpanOptions = { + * attributes: { "user.id": "123", "operation": "data-processing" }, + * kind: "internal", + * root: false, + * captureStackTrace: true + * } + * + * const program = Effect.succeed("Hello World").pipe( + * Effect.withSpan("my-operation", options) + * ) + * ``` + * + * @category options + * @since 3.1.0 + */ +export interface SpanOptions extends SpanOptionsNoTrace, TraceOptions {} + +/** + * Span creation options that do not control stack trace capture, including + * attributes, links, parent or root selection, annotations, span kind, + * sampling, and the trace level used for filtering. + * + * @category options + * @since 4.0.0 + */ +export interface SpanOptionsNoTrace { + readonly attributes?: Record | undefined + readonly links?: ReadonlyArray | undefined + readonly parent?: AnySpan | undefined + readonly root?: boolean | undefined + readonly annotations?: Context.Context | undefined + readonly kind?: SpanKind | undefined + readonly sampled?: boolean | undefined + readonly level?: LogLevel | undefined +} + +/** + * Options that control stack trace capture for tracing wrappers. + * `captureStackTrace` can disable capture or provide a lazy stack string. + * + * @category options + * @since 4.0.0 + */ +export interface TraceOptions { + readonly captureStackTrace?: boolean | LazyArg | undefined +} + +/** + * OpenTelemetry-style role describing the kind of operation represented by a + * span: internal work, server handling, client calls, producing, or consuming. + * + * **Example** (Configuring span kinds) + * + * ```ts + * import { Effect } from "effect" + * import type { Tracer } from "effect" + * + * // Different span kinds for different operations + * const serverSpan = Effect.withSpan("handle-request", { + * kind: "server" as Tracer.SpanKind + * }) + * + * const clientSpan = Effect.withSpan("api-call", { + * kind: "client" as Tracer.SpanKind + * }) + * + * const internalSpan = Effect.withSpan("internal-process", { + * kind: "internal" as Tracer.SpanKind + * }) + * ``` + * + * @category models + * @since 3.1.0 + */ +export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer" + +/** + * A span created by an Effect tracer. It carries trace identity, parent, + * annotations, attributes, links, sampling and kind information, lifecycle + * status, and methods to end the span or add attributes, events, and links. + * + * **Example** (Working with spans) + * + * ```ts + * import { Context, Exit, Option } from "effect" + * import type { Tracer } from "effect" + * + * const attributes = new Map() + * const links: Array = [] + * let status: Tracer.SpanStatus = { + * _tag: "Started", + * startTime: 1_000_000_000n + * } + * + * const span: Tracer.Span = { + * _tag: "Span", + * name: "load-user", + * spanId: "span-1", + * traceId: "trace-1", + * parent: Option.none(), + * annotations: Context.empty(), + * get status() { + * return status + * }, + * attributes, + * links, + * sampled: true, + * kind: "internal", + * end(endTime, exit) { + * status = { _tag: "Ended", startTime: status.startTime, endTime, exit } + * }, + * attribute(key, value) { + * attributes.set(key, value) + * }, + * event(name, startTime, eventAttributes = {}) { + * console.log(`${name} at ${startTime} with ${Object.keys(eventAttributes).length} attributes`) + * }, + * addLinks(newLinks) { + * links.push(...newLinks) + * } + * } + * + * span.attribute("user.id", "123") + * span.end(1_500_000_000n, Exit.succeed("user")) + * + * console.log(span.name) // "load-user" + * console.log(span.attributes.get("user.id")) // "123" + * console.log(span.status._tag) // "Ended" + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Span { + readonly _tag: "Span" + readonly name: string + readonly spanId: string + readonly traceId: string + readonly parent: Option.Option + readonly annotations: Context.Context + readonly status: SpanStatus + readonly attributes: ReadonlyMap + readonly links: ReadonlyArray + readonly sampled: boolean + readonly kind: SpanKind + end(endTime: bigint, exit: Exit.Exit): void + attribute(key: string, value: unknown): void + event(name: string, startTime: bigint, attributes?: Record): void + addLinks(links: ReadonlyArray): void +} + +/** + * A relationship from one span to another span, with attributes describing the + * relationship. + * + * **Example** (Linking spans) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Create a span link to connect spans + * const externalSpan = Tracer.externalSpan({ + * spanId: "external-span-123", + * traceId: "trace-456" + * }) + * + * const link: Tracer.SpanLink = { + * span: externalSpan, + * attributes: { "link.type": "follows-from", "service": "external-api" } + * } + * + * const program = Effect.succeed("result").pipe( + * Effect.withSpan("linked-operation", { links: [link] }) + * ) + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface SpanLink { + readonly span: AnySpan + readonly attributes: Readonly> +} + +/** + * Creates a `Tracer` value from a tracer implementation object. + * + * **When to use** + * + * Use to create a custom tracing backend value that Effect can use when + * creating spans. + * + * **Details** + * + * `make` returns the supplied implementation object unchanged. The object must + * satisfy the `Tracer` contract, including a `span` method that returns a + * `Span`. + * + * @see {@link Span} for the span values returned by tracer implementations + * + * @category constructors + * @since 2.0.0 + */ +export const make = (options: Tracer): Tracer => options + +/** + * Creates an `ExternalSpan` from trace and span identifiers, defaulting + * `sampled` to `true` and annotations to an empty context when they are not + * provided. + * + * **Example** (Creating an external span) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Create an external span from another tracing system + * const span = Tracer.externalSpan({ + * spanId: "span-abc-123", + * traceId: "trace-xyz-789", + * sampled: true + * }) + * + * // Use the external span as a parent + * const program = Effect.succeed("Hello").pipe( + * Effect.withSpan("child-operation", { parent: span }) + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const externalSpan = ( + options: { + readonly spanId: string + readonly traceId: string + readonly sampled?: boolean | undefined + readonly annotations?: Context.Context | undefined + } +): ExternalSpan => ({ + _tag: "ExternalSpan", + spanId: options.spanId, + traceId: options.traceId, + sampled: options.sampled ?? true, + annotations: options.annotations ?? Context.empty() +}) + +/** + * Context reference for disabling trace propagation. + * + * **When to use** + * + * Use to prevent spans in a scope from propagating tracing context. + * + * **Details** + * + * When enabled on fiber or span annotations, new spans are created as + * non-propagating no-op spans and disabled spans are skipped when deriving a + * parent span. + * + * **Example** (Disabling span propagation) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Disable span propagation for a specific effect + * const program = Effect.gen(function*() { + * yield* Effect.log("This will not propagate parent span") + * }).pipe( + * Effect.provideService(Tracer.DisablePropagation, true) + * ) + * ``` + * + * @category references + * @since 3.12.0 + */ +export const DisablePropagation = Context.Reference( + "effect/Tracer/DisablePropagation", + { defaultValue: constFalse } +) + +/** + * Context reference for controlling the current trace level for dynamic filtering. + * + * **When to use** + * + * Use to set the default trace level for spans in a scope when span options do + * not provide `level`. + * + * **Details** + * + * The default value is `"Info"`. Span creation uses `options.level ?? + * CurrentTraceLevel` before applying `MinimumTraceLevel`. + * + * @see {@link MinimumTraceLevel} for the threshold that decides whether spans at that level are sampled + * + * @category references + * @since 4.0.0 + */ +export const CurrentTraceLevel: Context.Reference = Context.Reference( + "effect/Tracer/CurrentTraceLevel", + { defaultValue: () => "Info" } +) + +/** + * Context reference for setting the minimum trace level threshold. Spans and their + * descendants below this level will have their sampling decision forced to + * false, preventing them from being exported. + * + * **When to use** + * + * Use to set the trace-level threshold that controls whether spans are sampled + * by default. + * + * **Details** + * + * The default value is `"All"`. Span creation compares the span level from + * `options.level ?? CurrentTraceLevel` against this threshold. + * + * **Gotchas** + * + * Explicit `options.sampled` bypasses threshold computation. + * + * @see {@link CurrentTraceLevel} for the default span level used when options do not specify one + * + * @category references + * @since 4.0.0 + */ +export const MinimumTraceLevel = Context.Reference< + LogLevel +>("effect/Tracer/MinimumTraceLevel", { defaultValue: () => "All" }) + +/** + * Defines the string key for the active tracer context reference. + * + * **When to use** + * + * Use when you need the raw context key for active tracer lookup in lower-level + * tracing code. + * + * @category references + * @since 4.0.0 + */ +export const TracerKey = "effect/Tracer" + +/** + * Context reference for the active tracer service. By default it uses the + * native tracer, which creates `NativeSpan` instances. + * + * **Example** (Accessing the current tracer) + * + * ```ts + * import { Effect, Tracer } from "effect" + * + * // Access the current tracer from the context + * const program = Effect.gen(function*() { + * const tracer = yield* Effect.service(Tracer.Tracer) + * console.log("Using current tracer") + * }) + * + * // Or use the built-in tracer effect + * const tracerEffect = Effect.gen(function*() { + * const tracer = yield* Effect.tracer + * console.log("Current tracer obtained") + * }) + * ``` + * + * @category references + * @since 2.0.0 + */ +export const Tracer: Context.Reference = Context.Reference(TracerKey, { + defaultValue: () => + make({ + span: (options) => new NativeSpan(options) + }) +}) + +/** + * Default in-memory `Span` implementation used by the native tracer. It + * generates span and trace identifiers, stores attributes, events, and links, + * and records `Started` or `Ended` status. + * + * **Details** + * + * The constructor initializes the span with `Started` status, inherits the + * parent trace id or generates a new one, and always generates a new span id. + * Attributes, events, links, and status are then mutated through `Span` methods. + * + * @see {@link Span} for the interface implemented by native spans + * + * @category native tracer + * @since 4.0.0 + */ +export class NativeSpan implements Span { + readonly _tag = "Span" + readonly spanId: string + readonly traceId: string = "native" + readonly sampled: boolean + + readonly name: string + readonly parent: Option.Option + readonly annotations: Context.Context + readonly links: Array + readonly startTime: bigint + readonly kind: SpanKind + + status: SpanStatus + attributes: Map + events: Array<[name: string, startTime: bigint, attributes: Record]> = [] + + constructor(options: { + readonly name: string + readonly parent: Option.Option + readonly annotations: Context.Context + readonly links: Array + readonly startTime: bigint + readonly kind: SpanKind + readonly sampled: boolean + }) { + this.name = options.name + this.parent = options.parent + this.annotations = options.annotations + this.links = options.links + this.startTime = options.startTime + this.kind = options.kind + this.sampled = options.sampled + this.status = { + _tag: "Started", + startTime: options.startTime + } + this.attributes = new Map() + this.traceId = Option.getOrUndefined(options.parent)?.traceId ?? randomHexString(32) + this.spanId = randomHexString(16) + } + + end(endTime: bigint, exit: Exit.Exit): void { + this.status = { + _tag: "Ended", + endTime, + exit, + startTime: this.status.startTime + } + } + + attribute(key: string, value: unknown): void { + this.attributes.set(key, value) + } + + event(name: string, startTime: bigint, attributes?: Record): void { + this.events.push([name, startTime, attributes ?? {}]) + } + + addLinks(links: ReadonlyArray): void { + // oxlint-disable-next-line no-restricted-syntax + this.links.push(...links) + } +} + +const randomHexString = (function() { + const characters = "abcdef0123456789" + const charactersLength = characters.length + return function(length: number) { + let result = "" + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + } + return result + } +})() diff --git a/repos/effect/packages/effect/src/Trie.ts b/repos/effect/packages/effect/src/Trie.ts new file mode 100644 index 0000000..025fc04 --- /dev/null +++ b/repos/effect/packages/effect/src/Trie.ts @@ -0,0 +1,953 @@ +/** + * Stores string-keyed values in an immutable prefix tree. + * + * A `Trie` is similar to a map whose keys are strings, but it is built + * for looking up keys by prefix. It is useful for autocomplete, route tables, + * dictionaries, and command lookup. Updates return new tries, and the module + * includes exact lookup, prefix lookup, longest-prefix lookup, iteration, + * mapping, filtering, reducing, and traversal helpers. + * + * @since 2.0.0 + */ +import type { Equal } from "./Equal.ts" +import type { Inspectable } from "./Inspectable.ts" +import * as TR from "./internal/trie.ts" +import type { Option } from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { Result } from "./Result.ts" +import type { Covariant, NoInfer } from "./Types.ts" + +const TypeId = TR.TrieTypeId + +/** + * An immutable string-keyed map optimized for prefix lookup. Iteration yields + * `[key, value]` pairs in key order, and update operations such as insert and + * remove return new `Trie` values. + * + * **Example** (Using a trie for prefix search) + * + * ```ts + * import { Trie } from "effect" + * + * // Create a trie with string-to-number mappings + * const trie: Trie.Trie = Trie.make( + * ["apple", 1], + * ["app", 2], + * ["application", 3], + * ["banana", 4] + * ) + * + * // Get values by exact key + * console.log(Trie.get(trie, "apple")) // Some(1) + * console.log(Trie.get(trie, "grape")) // None + * + * // Find all keys with a prefix + * console.log(Array.from(Trie.keysWithPrefix(trie, "app"))) + * // ["app", "apple", "application"] + * + * // Iterate over all entries (sorted alphabetically) + * for (const [key, value] of trie) { + * console.log(`${key}: ${value}`) + * } + * // Output: "app: 2", "apple: 1", "application: 3", "banana: 4" + * + * // Check if key exists + * console.log(Trie.has(trie, "app")) // true + * + * // Get size + * console.log(Trie.size(trie)) // 4 + * ``` + * + * @category models + * @since 2.0.0 + */ +export interface Trie extends Iterable<[string, Value]>, Equal, Pipeable, Inspectable { + readonly [TypeId]: { + readonly _Value: Covariant + } +} + +/** + * Creates an empty `Trie`. + * + * **Example** (Creating an empty trie) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty() + * + * assert.equal(Trie.size(trie), 0) + * assert.deepStrictEqual(Array.from(trie), []) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty: () => Trie = TR.empty + +/** + * Creates a new `Trie` from an iterable collection of key/value pairs (e.g. `Array<[string, V]>`). + * + * **Example** (Creating a trie from entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const iterable: Array = [["call", 0], ["me", 1], [ + * "mind", + * 2 + * ], ["mid", 3]] + * const trie = Trie.fromIterable(iterable) + * + * // The entries in the `Trie` are extracted in alphabetical order, regardless of the insertion order + * assert.deepStrictEqual(Array.from(trie), [["call", 0], ["me", 1], ["mid", 3], [ + * "mind", + * 2 + * ]]) + * assert.equal( + * Equal.equals( + * Trie.make(["call", 0], ["me", 1], ["mind", 2], ["mid", 3]), + * trie + * ), + * true + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable: (entries: Iterable) => Trie = TR.fromIterable + +/** + * Constructs a new `Trie` from the specified entries (`[string, V]`). + * + * **Example** (Constructing a trie from entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.make(["ca", 0], ["me", 1]) + * + * assert.deepStrictEqual(Array.from(trie), [["ca", 0], ["me", 1]]) + * assert.equal( + * Equal.equals(Trie.fromIterable([["ca", 0], ["me", 1]]), trie), + * true + * ) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make: >( + ...entries: Entries +) => Trie = TR.make + +/** + * Inserts a new entry in the `Trie`. + * + * **Example** (Inserting entries) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie1 = Trie.empty().pipe( + * Trie.insert("call", 0) + * ) + * const trie2 = trie1.pipe(Trie.insert("me", 1)) + * const trie3 = trie2.pipe(Trie.insert("mind", 2)) + * const trie4 = trie3.pipe(Trie.insert("mid", 3)) + * + * assert.deepStrictEqual(Array.from(trie1), [["call", 0]]) + * assert.deepStrictEqual(Array.from(trie2), [["call", 0], ["me", 1]]) + * assert.deepStrictEqual(Array.from(trie3), [["call", 0], ["me", 1], ["mind", 2]]) + * assert.deepStrictEqual(Array.from(trie4), [["call", 0], ["me", 1], ["mid", 3], [ + * "mind", + * 2 + * ]]) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const insert: { + (key: string, value: V): (self: Trie) => Trie + (self: Trie, key: string, value: V): Trie +} = TR.insert + +/** + * Returns an `IterableIterator` of the keys within the `Trie`. + * + * **Details** + * + * The keys are returned in alphabetical order, regardless of insertion order. + * + * **Example** (Reading keys in alphabetical order) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("cab", 0), + * Trie.insert("abc", 1), + * Trie.insert("bca", 2) + * ) + * + * const result = Array.from(Trie.keys(trie)) + * assert.deepStrictEqual(result, ["abc", "bca", "cab"]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const keys: (self: Trie) => IterableIterator = TR.keys + +/** + * Returns an `IterableIterator` of the values within the `Trie`. + * + * **Details** + * + * Values are ordered based on their key in alphabetical order, regardless of insertion order. + * + * **Example** (Reading values by key order) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("and", 2) + * ) + * + * const result = Array.from(Trie.values(trie)) + * assert.deepStrictEqual(result, [2, 0, 1]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const values: (self: Trie) => IterableIterator = TR.values + +/** + * Returns an `IterableIterator` of the entries within the `Trie`. + * + * **Details** + * + * The entries are returned by keys in alphabetical order, regardless of insertion order. + * + * **Example** (Reading entries in alphabetical order) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * + * const result = Array.from(Trie.entries(trie)) + * assert.deepStrictEqual(result, [["call", 0], ["me", 1]]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const entries: (self: Trie) => IterableIterator<[string, V]> = TR.entries + +/** + * Returns an `Array<[string, V]>` of the entries within the `Trie`. + * + * **Details** + * + * Equivalent to `Array.from(Trie.entries(trie))`. + * + * **Example** (Converting entries to an array) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * const result = Trie.toEntries(trie) + * + * assert.deepStrictEqual(result, [["call", 0], ["me", 1]]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toEntries = (self: Trie): Array<[string, V]> => Array.from(entries(self)) + +/** + * Returns an `IterableIterator` of the keys within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * **Example** (Finding keys with a prefix) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.keysWithPrefix(trie, "she")) + * assert.deepStrictEqual(result, ["she", "shells"]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const keysWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator + (self: Trie, prefix: string): IterableIterator +} = TR.keysWithPrefix + +/** + * Returns an `IterableIterator` of the values within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * **Example** (Finding values with a prefix) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.valuesWithPrefix(trie, "she")) + * + * // 0: "she", 1: "shells" + * assert.deepStrictEqual(result, [0, 1]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const valuesWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator + (self: Trie, prefix: string): IterableIterator +} = TR.valuesWithPrefix + +/** + * Returns an `IterableIterator` of the entries within the `Trie` + * that have `prefix` as prefix (`prefix` included if it exists). + * + * **Example** (Finding entries with a prefix) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("she", 0), + * Trie.insert("shells", 1), + * Trie.insert("sea", 2), + * Trie.insert("shore", 3) + * ) + * + * const result = Array.from(Trie.entriesWithPrefix(trie, "she")) + * assert.deepStrictEqual(result, [["she", 0], ["shells", 1]]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const entriesWithPrefix: { + (prefix: string): (self: Trie) => IterableIterator<[string, V]> + (self: Trie, prefix: string): IterableIterator<[string, V]> +} = TR.entriesWithPrefix + +/** + * Returns an `Array<[string, V]>` of the entries within the `Trie` whose keys + * start with `prefix`, including the entry for `prefix` itself when it exists. + * + * **Example** (Converting prefixed entries to an array) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("sea", 2), + * Trie.insert("she", 3) + * ) + * + * const result = Trie.toEntriesWithPrefix(trie, "she") + * assert.deepStrictEqual(result, [["she", 3], ["shells", 0]]) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const toEntriesWithPrefix: { + (prefix: string): (self: Trie) => Array<[string, V]> + (self: Trie, prefix: string): Array<[string, V]> +} = TR.toEntriesWithPrefix + +/** + * Returns the longest key/value in the `Trie` + * that is a prefix of that `key` if it exists, `None` otherwise. + * + * **Example** (Finding the longest prefix) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const none = Trie.longestPrefixOf(trie, "sell") + * const some = Trie.longestPrefixOf(trie, "sells") + * + * assert.equal(none._tag, "None") + * assert.equal(some._tag, "Some") + * if (some._tag === "Some") { + * assert.deepStrictEqual(some.value, ["sells", 1]) + * } + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const longestPrefixOf: { + (key: string): (self: Trie) => Option<[string, V]> + (self: Trie, key: string): Option<[string, V]> +} = TR.longestPrefixOf + +/** + * Returns the size of the `Trie` (number of entries in the `Trie`). + * + * **Example** (Getting the size) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("a", 0), + * Trie.insert("b", 1) + * ) + * + * assert.equal(Trie.size(trie), 2) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size: (self: Trie) => number = TR.size + +/** + * Looks up the value for the specified key in the `Trie` safely. + * + * **Example** (Looking up values safely) + * + * ```ts + * import { Option, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * assert.deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) + * assert.deepStrictEqual(Trie.get(trie, "me"), Option.some(1)) + * assert.deepStrictEqual(Trie.get(trie, "mind"), Option.some(2)) + * assert.deepStrictEqual(Trie.get(trie, "mid"), Option.some(3)) + * assert.deepStrictEqual(Trie.get(trie, "cale"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "ma"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "midn"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie, "mea"), Option.none()) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const get: { + (key: string): (self: Trie) => Option + (self: Trie, key: string): Option +} = TR.get + +/** + * Checks whether the given key exists in the `Trie`. + * + * **Example** (Checking key membership) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * assert.equal(Trie.has(trie, "call"), true) + * assert.equal(Trie.has(trie, "me"), true) + * assert.equal(Trie.has(trie, "mind"), true) + * assert.equal(Trie.has(trie, "mid"), true) + * assert.equal(Trie.has(trie, "cale"), false) + * assert.equal(Trie.has(trie, "ma"), false) + * assert.equal(Trie.has(trie, "midn"), false) + * assert.equal(Trie.has(trie, "mea"), false) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (key: string): (self: Trie) => boolean + (self: Trie, key: string): boolean +} = TR.has + +/** + * Returns `true` when the `Trie` contains no entries. + * + * **Example** (Checking whether a trie is empty) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty() + * const trie1 = trie.pipe(Trie.insert("ma", 0)) + * + * assert.equal(Trie.isEmpty(trie), true) + * assert.equal(Trie.isEmpty(trie1), false) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const isEmpty: (self: Trie) => boolean = TR.isEmpty + +/** + * Looks up the value for the specified key in the `Trie` unsafely. + * + * **When to use** + * + * Use when the trie key is known to exist and a missing key should be treated + * as a programming error. + * + * **Gotchas** + * + * `getUnsafe` throws if the key is not found. Use `get` instead to safely get + * a value from the `Trie`. + * + * **Example** (Looking up values unsafely) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1) + * ) + * + * assert.throws(() => Trie.getUnsafe(trie, "mae")) + * ``` + * + * @category unsafe + * @since 4.0.0 + */ +export const getUnsafe: { + (key: string): (self: Trie) => V + (self: Trie, key: string): V +} = TR.getUnsafe + +/** + * Removes the entry for the specified key in the `Trie`. + * + * **Example** (Removing entries) + * + * ```ts + * import { Option, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("call", 0), + * Trie.insert("me", 1), + * Trie.insert("mind", 2), + * Trie.insert("mid", 3) + * ) + * + * const trie1 = trie.pipe(Trie.remove("call")) + * const trie2 = trie1.pipe(Trie.remove("mea")) + * + * assert.deepStrictEqual(Trie.get(trie, "call"), Option.some(0)) + * assert.deepStrictEqual(Trie.get(trie1, "call"), Option.none()) + * assert.deepStrictEqual(Trie.get(trie2, "call"), Option.none()) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (key: string): (self: Trie) => Trie + (self: Trie, key: string): Trie +} = TR.remove + +/** + * Reduces a state over the entries of the `Trie`. + * + * **Example** (Reducing entries) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.equal( + * trie.pipe( + * Trie.reduce(0, (acc, n) => acc + n) + * ), + * 3 + * ) + * assert.equal( + * trie.pipe( + * Trie.reduce(10, (acc, n) => acc + n) + * ), + * 13 + * ) + * assert.equal( + * trie.pipe( + * Trie.reduce("", (acc, _, key) => acc + key) + * ), + * "sellssheshells" + * ) + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + (zero: Z, f: (accumulator: Z, value: V, key: string) => Z): (self: Trie) => Z + (self: Trie, zero: Z, f: (accumulator: Z, value: V, key: string) => Z): Z +} = TR.reduce + +/** + * Maps over the entries of the `Trie` using the specified function. + * + * **Example** (Mapping entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("shells", 1), + * Trie.insert("sells", 2), + * Trie.insert("she", 3) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 6), + * Trie.insert("sells", 5), + * Trie.insert("she", 3) + * ) + * + * assert.equal(Equal.equals(Trie.map(trie, (v) => v + 1), trieMapV), true) + * assert.equal(Equal.equals(Trie.map(trie, (_, k) => k.length), trieMapK), true) + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const map: { + (f: (value: V, key: string) => A): (self: Trie) => Trie + (self: Trie, f: (value: V, key: string) => A): Trie +} = TR.map + +/** + * Filters entries out of a `Trie` using the specified predicate. + * + * **Example** (Filtering entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("she", 2) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1) + * ) + * + * assert.equal(Equal.equals(Trie.filter(trie, (v) => v > 1), trieMapV), true) + * assert.equal( + * Equal.equals(Trie.filter(trie, (_, k) => k.length > 3), trieMapK), + * true + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filter: { + (f: (a: NoInfer, k: string) => a is B): (self: Trie) => Trie + (f: (a: NoInfer, k: string) => boolean): (self: Trie) => Trie + (self: Trie, f: (a: A, k: string) => a is B): Trie + (self: Trie, f: (a: A, k: string) => boolean): Trie +} = TR.filter + +/** + * Maps over the entries of the `Trie` using the specified filter and keeps + * only successful results. + * + * **Example** (Filtering and mapping entries) + * + * ```ts + * import { Equal, Result, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("she", 2) + * ) + * + * const trieMapK = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1) + * ) + * + * assert.equal( + * Equal.equals( + * Trie.filterMap(trie, (v) => v > 1 ? Result.succeed(v) : Result.failVoid), + * trieMapV + * ), + * true + * ) + * assert.equal( + * Equal.equals( + * Trie.filterMap( + * trie, + * (v, k) => k.length > 3 ? Result.succeed(v) : Result.failVoid + * ), + * trieMapK + * ), + * true + * ) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const filterMap: { + (f: (input: A, key: string) => Result): (self: Trie) => Trie + (self: Trie, f: (input: A, key: string) => Result): Trie +} = TR.filterMap + +/** + * Filters out `None` values from a `Trie` of `Options`s. + * + * **Example** (Compacting optional values) + * + * ```ts + * import { Equal, Option, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty>().pipe( + * Trie.insert("shells", Option.some(0)), + * Trie.insert("sells", Option.none()), + * Trie.insert("she", Option.some(2)) + * ) + * + * const trieMapV = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("she", 2) + * ) + * + * assert.equal(Equal.equals(Trie.compact(trie), trieMapV), true) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const compact: (self: Trie>) => Trie = TR.compact + +/** + * Applies the specified function to the entries of the `Trie`. + * + * **Example** (Iterating over entries) + * + * ```ts + * import { Trie } from "effect" + * import * as assert from "node:assert" + * + * let value = 0 + * + * Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2), + * Trie.forEach((n, key) => { + * value += n + key.length + * }) + * ) + * + * assert.equal(value, 17) + * ``` + * + * @category traversing + * @since 2.0.0 + */ +export const forEach: { + (f: (value: V, key: string) => void): (self: Trie) => void + (self: Trie, f: (value: V, key: string) => void): void +} = TR.forEach + +/** + * Updates the value of the specified key within the `Trie` if it exists. + * + * **Example** (Modifying an existing value) + * + * ```ts + * import { Equal, Option, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.deepStrictEqual( + * trie.pipe(Trie.modify("she", (v) => v + 10), Trie.get("she")), + * Option.some(12) + * ) + * + * assert.equal(Equal.equals(trie.pipe(Trie.modify("me", (v) => v)), trie), true) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const modify: { + (key: string, f: (v: V) => V): (self: Trie) => Trie + (self: Trie, key: string, f: (v: V) => V): Trie +} = TR.modify + +/** + * Removes all entries in the `Trie` which have the specified keys. + * + * **Example** (Removing multiple entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * assert.equal( + * Equal.equals( + * trie.pipe(Trie.removeMany(["she", "sells"])), + * Trie.empty().pipe(Trie.insert("shells", 0)) + * ), + * true + * ) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const removeMany: { + (keys: Iterable): (self: Trie) => Trie + (self: Trie, keys: Iterable): Trie +} = TR.removeMany + +/** + * Inserts multiple entries in the `Trie` at once. + * + * **Example** (Inserting multiple entries) + * + * ```ts + * import { Equal, Trie } from "effect" + * import * as assert from "node:assert" + * + * const trie = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insert("sells", 1), + * Trie.insert("she", 2) + * ) + * + * const trieInsert = Trie.empty().pipe( + * Trie.insert("shells", 0), + * Trie.insertMany( + * [["sells", 1], ["she", 2]] + * ) + * ) + * + * assert.equal( + * Equal.equals(trie, trieInsert), + * true + * ) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const insertMany: { + (iter: Iterable<[string, V]>): (self: Trie) => Trie + (self: Trie, iter: Iterable<[string, V]>): Trie +} = TR.insertMany diff --git a/repos/effect/packages/effect/src/Tuple.ts b/repos/effect/packages/effect/src/Tuple.ts new file mode 100644 index 0000000..8e4251f --- /dev/null +++ b/repos/effect/packages/effect/src/Tuple.ts @@ -0,0 +1,737 @@ +/** + * Works with fixed-length arrays, also called tuples. + * + * The runtime helpers in this module create new tuples instead of mutating + * their inputs, and the types preserve element positions where possible. The + * helpers cover tuple construction, indexed access, selecting or removing + * positions, appending values, transforming elements, renaming indices, mapping + * typed positions, and deriving comparison or combination helpers for tuple + * shapes. + * + * @since 2.0.0 + */ +import * as Combiner from "./Combiner.ts" +import * as Equivalence from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as order from "./Order.ts" +import * as Reducer from "./Reducer.ts" +import type { Apply, Lambda } from "./Struct.ts" + +/** + * Creates a tuple from the provided arguments. + * + * **When to use** + * + * Use when you need a properly typed tuple without writing `[a, b, c] as const` + * or another manual cast. + * + * **Details** + * + * The returned value has the exact tuple type, with each element's literal type + * preserved. + * + * **Example** (Creating a tuple) + * + * ```ts + * import { Tuple } from "effect" + * + * const point = Tuple.make(10, 20, "red") + * console.log(point) // [10, 20, "red"] + * ``` + * + * @see {@link get} – access a single element by index + * @see {@link appendElement} – append an element to a tuple + * @category constructors + * @since 2.0.0 + */ +export const make = >(...elements: Elements): Elements => elements + +type Indices> = Exclude["length"], T["length"]> + +/** + * Retrieves the element at the specified index from a tuple. + * + * **When to use** + * + * Use when a single tuple element should be extracted in a pipeline. + * + * **Details** + * + * The index is constrained to valid tuple positions at the type level. + * + * **Example** (Extracting an element by index) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const last = pipe(Tuple.make(1, true, "hello"), Tuple.get(2)) + * console.log(last) // "hello" + * ``` + * + * @see {@link make} – create a tuple + * @see {@link pick} – extract multiple elements into a new tuple + * @category getters + * @since 4.0.0 + */ +export const get: { + , I extends Indices & keyof T>(index: I): (self: T) => T[I] + , I extends Indices & keyof T>(self: T, index: I): T[I] +} = dual(2, , I extends keyof T>(self: T, index: I): T[I] => self[index]) + +type _BuildTuple< + T extends ReadonlyArray, + K, + Acc extends ReadonlyArray = [], + I extends ReadonlyArray = [] // current index counter +> = I["length"] extends T["length"] ? Acc + : _BuildTuple< + T, + K, + // If current index is in K, keep the element; otherwise skip it + I["length"] extends K ? [...Acc, T[I["length"]]] : Acc, + [...I, unknown] + > + +type PickTuple, K> = _BuildTuple + +/** + * Creates a new tuple containing only the elements at the specified indices. + * + * **When to use** + * + * Use to select a subset of elements from a tuple by position. + * + * **Details** + * + * The result order matches the order of the provided indices. + * + * **Example** (Selecting elements by index) + * + * ```ts + * import { Tuple } from "effect" + * + * const result = Tuple.pick(["a", "b", "c", "d"], [0, 2, 3]) + * console.log(result) // ["a", "c", "d"] + * ``` + * + * @see {@link omit} – the inverse (exclude indices instead) + * @see {@link get} – extract a single element + * @category filtering + * @since 4.0.0 + */ +export const pick: { + , const I extends ReadonlyArray>>( + indices: I + ): (self: T) => PickTuple + , const I extends ReadonlyArray>>( + self: T, + indices: I + ): PickTuple +} = dual( + 2, + >( + self: T, + indices: ReadonlyArray + ) => { + return indices.map((i) => self[i]) + } +) + +type OmitTuple, K> = _BuildTuple, K>> + +/** + * Creates a new tuple with the elements at the specified indices removed. + * + * **When to use** + * + * Use to drop elements from a tuple by position. + * + * **Details** + * + * Elements not at the specified indices are kept in their original order. + * + * **Example** (Removing elements by index) + * + * ```ts + * import { Tuple } from "effect" + * + * const result = Tuple.omit(["a", "b", "c", "d"], [1, 3]) + * console.log(result) // ["a", "c"] + * ``` + * + * @see {@link pick} – the inverse (keep only specified indices) + * @category filtering + * @since 4.0.0 + */ +export const omit: { + , const I extends ReadonlyArray>>( + indices: I + ): (self: T) => OmitTuple + , const I extends ReadonlyArray>>( + self: T, + indices: I + ): OmitTuple +} = dual( + 2, + >( + self: T, + indices: ReadonlyArray + ) => { + const toDrop = new Set(indices) + return self.filter((_, i) => !toDrop.has(i)) + } +) + +/** + * Appends a single element to the end of a tuple. + * + * **When to use** + * + * Use when you need the appended value to remain part of the tuple's type-level + * shape and preserve literal element positions. + * + * **Details** + * + * The result type is `[...T, E]`, preserving all existing element types. + * + * **Example** (Appending an element) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const result = pipe(Tuple.make(1, 2), Tuple.appendElement("end")) + * console.log(result) // [1, 2, "end"] + * ``` + * + * @see {@link appendElements} – append multiple elements (another tuple) + * @category combining + * @since 2.0.0 + */ +export const appendElement: { + (element: E): >(self: T) => [...T, E] + , const E>(self: T, element: E): [...T, E] +} = dual(2, , E>(self: T, element: E): [...T, E] => [...self, element]) + +/** + * Concatenates two tuples into a single tuple. + * + * **When to use** + * + * Use to append all elements from one tuple to another tuple. + * + * **Details** + * + * The result type is `[...T1, ...T2]`, preserving all element types from both + * tuples. Neither input tuple is mutated; a fresh tuple is returned. + * + * **Example** (Concatenating tuples) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const result = pipe(Tuple.make(1, 2), Tuple.appendElements(["a", "b"] as const)) + * console.log(result) // [1, 2, "a", "b"] + * ``` + * + * @see {@link appendElement} – append a single element + * @category combining + * @since 4.0.0 + */ +export const appendElements: { + >( + that: T2 + ): >(self: T1) => [...T1, ...T2] + , const T2 extends ReadonlyArray>(self: T1, that: T2): [...T1, ...T2] +} = dual( + 2, + , T2 extends ReadonlyArray>( + self: T1, + that: T2 + ): [...T1, ...T2] => [...self, ...that] +) + +type Evolver = { readonly [I in keyof T]?: ((a: T[I]) => unknown) | undefined } + +type Evolved = { [I in keyof T]: I extends keyof E ? (E[I] extends (...a: any) => infer R ? R : T[I]) : T[I] } + +/** + * Transforms elements of a tuple by providing an array of transform functions. + * Each function applies to the element at the same position. Positions beyond + * the array's length are copied unchanged. + * + * **When to use** + * + * Use when you want to update the first N elements while keeping the rest. + * + * **Details** + * + * Each transform function receives the current value and can return a different + * type. + * + * **Example** (Transforming selected elements) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const result = pipe( + * Tuple.make("hello", 42, true), + * Tuple.evolve([ + * (s) => s.toUpperCase(), + * (n) => n * 2 + * ]) + * ) + * console.log(result) // ["HELLO", 84, true] + * ``` + * + * @see {@link map} – apply the same transformation to all elements + * @see {@link renameIndices} – swap element positions + * @category mapping + * @since 4.0.0 + */ +export const evolve: { + , const E extends Evolver>(evolver: E): (self: T) => Evolved + , const E extends Evolver>(self: T, evolver: E): Evolved +} = dual( + 2, + , const E extends Evolver>(self: T, evolver: E) => { + return self.map((e, i) => (evolver[i] !== undefined ? evolver[i](e) : e)) + } +) + +/** + * Renames tuple indices by providing an array of stringified source + * indices. Each position in the array specifies which index to read from + * (e.g., `["2", "1", "0"]` reverses a 3-element tuple). + * + * **When to use** + * + * Use to reorder tuple elements while preserving index-specific types. + * + * **Details** + * + * The mapping returns a tuple in the requested index order. + * + * **Gotchas** + * + * The mapping uses stringified source indices, not arbitrary names. + * + * **Example** (Swapping elements) + * + * ```ts + * import { pipe, Tuple } from "effect" + * + * const result = pipe( + * Tuple.make("a", "b", "c"), + * Tuple.renameIndices(["2", "1", "0"]) + * ) + * console.log(result) // ["c", "b", "a"] + * ``` + * + * @see {@link evolve} – transform element values instead of positions + * @category Index utilities + * @since 4.0.0 + */ +export const renameIndices: { + , const M extends { readonly [I in keyof T]?: `${keyof T & string}` }>( + mapping: M + ): (self: T) => { [I in keyof T]: I extends keyof M ? M[I] extends keyof T ? T[M[I]] : T[I] : T[I] } + , const M extends { readonly [I in keyof T]?: `${keyof T & string}` }>( + self: T, + mapping: M + ): { [I in keyof T]: I extends keyof M ? M[I] extends keyof T ? T[M[I]] : T[I] : T[I] } +} = dual( + 2, + , const M extends { readonly [I in keyof T]?: `${keyof T & string}` }>( + self: T, + mapping: M + ) => { + return self.map((e, i) => mapping[i] !== undefined ? self[mapping[i]] : e) + } +) + +/** + * Applies a `Struct.Lambda` transformation to every element in a tuple. + * + * **When to use** + * + * Use when you want to apply the same transformation to every tuple element. + * + * **Details** + * + * The lambda lets the compiler track the output type for each element. + * + * **Gotchas** + * + * The lambda must be created with `Struct.lambda`; a plain function will not + * type-check. + * + * **Example** (Wrapping every element in an array) + * + * ```ts + * import { pipe, Struct, Tuple } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe(Tuple.make(1, "hello", true), Tuple.map(asArray)) + * console.log(result) // [[1], ["hello"], [true]] + * ``` + * + * @see {@link mapPick} – apply a lambda only to selected indices + * @see {@link mapOmit} – apply a lambda to all indices except selected ones + * @see {@link evolve} – apply different functions to different indices + * @category mapping + * @since 3.9.0 + */ +export const map: { + ( + lambda: L + ): >( + self: T + ) => { [K in keyof T]: Apply } + , L extends Lambda>( + self: T, + lambda: L + ): { [K in keyof T]: Apply } +} = dual( + 2, + , L extends Function>(self: T, lambda: L) => { + return self.map((e) => lambda(e)) + } +) + +/** + * Applies a `Struct.Lambda` transformation only to the elements at the + * specified indices; all other elements are copied unchanged. + * + * **When to use** + * + * Use when you want to apply the same transformation to a subset of + * positions. + * + * **Example** (Wrapping only selected elements in arrays) + * + * ```ts + * import { pipe, Struct, Tuple } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe( + * Tuple.make(1, "hello", true), + * Tuple.mapPick([0, 2], asArray) + * ) + * console.log(result) // [[1], "hello", [true]] + * ``` + * + * @see {@link map} – apply a lambda to all elements + * @see {@link mapOmit} – apply a lambda to all elements except selected ones + * @category mapping + * @since 4.0.0 + */ +export const mapPick: { + , const I extends ReadonlyArray>, L extends Lambda>( + indices: I, + lambda: L + ): ( + self: T + ) => { [K in keyof T]: K extends `${I[number]}` ? Apply : T[K] } + , const I extends ReadonlyArray>, L extends Lambda>( + self: T, + indices: I, + lambda: L + ): { [K in keyof T]: K extends `${I[number]}` ? Apply : T[K] } +} = dual( + 3, + , L extends Function>( + self: T, + indices: ReadonlyArray, + lambda: L + ) => { + const toPick = new Set(indices) + return self.map((e, i) => (toPick.has(i) ? lambda(e) : e)) + } +) + +/** + * Applies a `Struct.Lambda` transformation to all elements except those at the + * specified indices; the excluded elements are copied unchanged. + * + * **When to use** + * + * Use when most elements should be transformed but a few should be + * preserved. + * + * **Example** (Wrapping all elements except one in arrays) + * + * ```ts + * import { pipe, Struct, Tuple } from "effect" + * + * interface AsArray extends Struct.Lambda { + * (self: A): Array + * readonly "~lambda.out": Array + * } + * + * const asArray = Struct.lambda((a) => [a]) + * const result = pipe( + * Tuple.make(1, "hello", true), + * Tuple.mapOmit([1], asArray) + * ) + * console.log(result) // [[1], "hello", [true]] + * ``` + * + * @see {@link map} – apply a lambda to all elements + * @see {@link mapPick} – apply a lambda only to selected indices + * @category mapping + * @since 4.0.0 + */ +export const mapOmit: { + , const I extends ReadonlyArray>, L extends Lambda>( + indices: I, + lambda: L + ): ( + self: T + ) => { [K in keyof T]: K extends `${I[number]}` ? T[K] : Apply } + , const I extends ReadonlyArray>, L extends Lambda>( + self: T, + indices: I, + lambda: L + ): { [K in keyof T]: K extends `${I[number]}` ? T[K] : Apply } +} = dual( + 3, + , L extends Function>( + self: T, + indices: ReadonlyArray, + lambda: L + ) => { + const toOmit = new Set(indices) + return self.map((e, i) => (toOmit.has(i) ? e : lambda(e))) + } +) + +/** + * Creates an `Equivalence` for tuples by comparing corresponding elements + * using the provided per-position `Equivalence`s. Two tuples are equivalent + * when all their corresponding elements are equivalent. + * + * **When to use** + * + * Use when you need an `Equivalence` to compare tuples element-by-element. + * + * **Details** + * + * This is an alias of `Equivalence.Tuple`. + * + * **Example** (Comparing tuples for equivalence) + * + * ```ts + * import { Equivalence, Tuple } from "effect" + * + * const eq = Tuple.makeEquivalence([ + * Equivalence.strictEqual(), + * Equivalence.strictEqual() + * ]) + * + * console.log(eq(["Alice", 30], ["Alice", 30])) // true + * console.log(eq(["Alice", 30], ["Bob", 30])) // false + * ``` + * + * @see {@link makeOrder} – create an `Order` for tuples + * @category instances + * @since 4.0.0 + */ +export const makeEquivalence = Equivalence.Tuple + +/** + * Creates an `Order` for tuples by comparing corresponding elements using the + * provided per-position `Order`s. Elements are compared left-to-right; the + * first non-zero comparison determines the result. + * + * **When to use** + * + * Use when you need to sort fixed-position arrays lexicographically, with each + * position using its own ordering rule. + * + * **Details** + * + * This is an alias of `Order.Tuple`. + * + * **Example** (Ordering tuples) + * + * ```ts + * import { Number, String, Tuple } from "effect" + * + * const ord = Tuple.makeOrder([String.Order, Number.Order]) + * + * console.log(ord(["Alice", 30], ["Bob", 25])) // -1 + * console.log(ord(["Alice", 30], ["Alice", 30])) // 0 + * ``` + * + * @see {@link makeEquivalence} – create an `Equivalence` for tuples + * @category ordering + * @since 4.0.0 + */ +export const makeOrder = order.Tuple + +export { + /** + * Checks whether an array has exactly `N` elements, narrowing the type to a + * fixed-length tuple. + * + * **When to use** + * + * Use to guard that an array has exactly the tuple length expected at + * runtime. + * + * **Details** + * + * This is a re-export of `Predicate.isTupleOf`. It narrows the type to + * `TupleOf` in the truthy branch. + * + * **Gotchas** + * + * This only checks `.length`; it does not validate element types. + * + * **Example** (Checking exact length) + * + * ```ts + * import { Tuple } from "effect" + * + * const arr: Array = [1, 2, 3] + * if (Tuple.isTupleOf(arr, 3)) { + * console.log(arr) + * // ^? [number, number, number] + * } + * ``` + * + * @see `isTupleOfAtLeast` – check for a minimum length + * @category guards + * @since 3.3.0 + */ + isTupleOf, + /** + * Checks whether an array has at least `N` elements, narrowing the type to a + * tuple with a minimum length. + * + * **When to use** + * + * Use to guard that an array has at least the tuple length expected at + * runtime. + * + * **Details** + * + * This is a re-export of `Predicate.isTupleOfAtLeast`. It narrows the type to + * `TupleOfAtLeast` in the truthy branch. + * + * **Gotchas** + * + * This only checks `.length`; it does not validate element types. + * + * **Example** (Checking minimum length) + * + * ```ts + * import { Tuple } from "effect" + * + * const arr: Array = [1, 2, 3, 4] + * if (Tuple.isTupleOfAtLeast(arr, 3)) { + * console.log(arr) + * // ^? [number, number, number, ...number[]] + * } + * ``` + * + * @see `isTupleOf` – check for an exact length + * @category guards + * @since 3.3.0 + */ + isTupleOfAtLeast +} from "./Predicate.ts" + +/** + * Creates a `Combiner` for a tuple shape by providing a `Combiner` for each + * position. When two tuples are combined, each element is merged using its + * corresponding combiner. + * + * **When to use** + * + * Use when you need to merge two same-shape tuples by combining each position + * independently, such as summing counters or concatenating strings. + * + * **Example** (Combining tuple elements) + * + * ```ts + * import { Number, String, Tuple } from "effect" + * + * const C = Tuple.makeCombiner([ + * Number.ReducerSum, + * String.ReducerConcat + * ]) + * + * const result = C.combine([1, "hello"], [2, " world"]) + * console.log(result) // [3, "hello world"] + * ``` + * + * @see {@link makeReducer} – like `makeCombiner` but with an initial value + * @category combining + * @since 4.0.0 + */ +export function makeCombiner>( + combiners: { readonly [K in keyof A]: Combiner.Combiner } +): Combiner.Combiner { + return Combiner.make((self, that) => { + const out = [] + for (let i = 0; i < self.length; i++) { + out.push(combiners[i].combine(self[i], that[i])) + } + return out as any + }) +} + +/** + * Creates a `Reducer` for a tuple shape by providing a `Reducer` for each + * position. The initial value is derived from each position's + * `Reducer.initialValue`. When reducing a collection of tuples, each element + * is combined independently. + * + * **When to use** + * + * Use when you need to fold same-shape tuples by accumulating each position + * independently into one summary tuple. + * + * **Example** (Reducing a collection of tuples) + * + * ```ts + * import { Number, String, Tuple } from "effect" + * + * const R = Tuple.makeReducer([ + * Number.ReducerSum, + * String.ReducerConcat + * ]) + * + * const result = R.combineAll([ + * [1, "a"], + * [2, "b"], + * [3, "c"] + * ]) + * console.log(result) // [6, "abc"] + * ``` + * + * @see {@link makeCombiner} – like `makeReducer` but without an initial value + * @category folding + * @since 4.0.0 + */ +export function makeReducer>( + reducers: { readonly [K in keyof A]: Reducer.Reducer } +): Reducer.Reducer { + const combine = makeCombiner(reducers).combine + const initialValue = [] + for (let i = 0; i < reducers.length; i++) { + initialValue.push(reducers[i].initialValue) + } + return Reducer.make(combine, initialValue as unknown as A) +} diff --git a/repos/effect/packages/effect/src/TxChunk.ts b/repos/effect/packages/effect/src/TxChunk.ts new file mode 100644 index 0000000..ffabde7 --- /dev/null +++ b/repos/effect/packages/effect/src/TxChunk.ts @@ -0,0 +1,822 @@ +/** + * Stores a `Chunk` inside transactional state. + * + * A `TxChunk` keeps its current `Chunk` in a `TxRef`, so reads and + * updates can be committed atomically with other transactional operations. This + * module offers a transactional version of common chunk workflows, including + * creating collections, reading or replacing the current chunk, adding or + * removing values, checking size, slicing, mapping, filtering, and combining + * chunks. + * + * @since 4.0.0 + */ +import * as Chunk from "./Chunk.ts" +import * as Effect from "./Effect.ts" +import { format } from "./Formatter.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import * as TxRef from "./TxRef.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = "~effect/transactions/TxChunk" + +/** + * TxChunk is a transactional chunk data structure that provides Software Transactional Memory (STM) + * semantics for chunk operations. + * + * **Details** + * + * Accessed values are tracked by the transaction in order to detect conflicts and to track changes. + * A transaction will retry whenever a conflict is detected or whenever the transaction explicitly + * calls `Effect.txRetry` and any of the accessed TxChunk values change. + * + * **Example** (Using a transactional chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional chunk + * const txChunk: TxChunk.TxChunk = yield* TxChunk.fromIterable([ + * 1, + * 2, + * 3 + * ]) + * + * // Single operations - no explicit transaction needed + * yield* TxChunk.append(txChunk, 4) + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] + * + * // Multi-step atomic operation - use explicit transaction + * yield* Effect.tx( + * Effect.gen(function*() { + * yield* TxChunk.prepend(txChunk, 0) + * yield* TxChunk.append(txChunk, 5) + * }) + * ) + * + * const finalResult = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(finalResult)) // [0, 1, 2, 3, 4, 5] + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxChunk extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly ref: TxRef.TxRef> +} + +const TxChunkProto = { + [NodeInspectSymbol](this: TxChunk) { + return this.toJSON() + }, + toString(this: TxChunk) { + return `TxChunk(${format(toJson((this).ref))})` + }, + toJSON(this: TxChunk) { + return { + _id: "TxChunk", + ref: toJson((this).ref) + } + }, + pipe(this: TxChunk) { + return pipeArguments(this, arguments) + } +} + +/** + * Creates a new `TxChunk` with the specified initial chunk. + * + * **Details** + * + * This function returns a new TxChunk reference containing the provided initial chunk. No existing + * TxChunk instances are modified. + * + * **Example** (Creating a TxChunk from a chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a TxChunk with initial values + * const initialChunk = Chunk.fromIterable([1, 2, 3]) + * const txChunk = yield* TxChunk.make(initialChunk) + * + * // Read the value - automatically transactional + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3] + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = (initial: Chunk.Chunk): Effect.Effect> => + Effect.map(TxRef.make(initial), (ref) => makeUnsafe(ref)) + +/** + * Creates a new empty `TxChunk`. + * + * **Details** + * + * This function returns a new TxChunk reference that is initially empty. No existing TxChunk + * instances are modified. + * + * **Example** (Creating an empty TxChunk) + * + * ```ts + * import { Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an empty TxChunk + * const txChunk = yield* TxChunk.empty() + * + * // Check if it's empty - automatically transactional + * const isEmpty = yield* TxChunk.isEmpty(txChunk) + * console.log(isEmpty) // true + * + * // Add elements - automatically transactional + * yield* TxChunk.append(txChunk, 42) + * + * const isStillEmpty = yield* TxChunk.isEmpty(txChunk) + * console.log(isStillEmpty) // false + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const empty = (): Effect.Effect> => + Effect.map(TxRef.make(Chunk.empty()), (ref) => makeUnsafe(ref)) + +/** + * Creates a new `TxChunk` from an iterable. + * + * **Details** + * + * This function returns a new TxChunk reference containing elements from the provided iterable. No + * existing TxChunk instances are modified. + * + * **Example** (Creating from an iterable) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * // Create TxChunk from array + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5]) + * + * // Read the contents - automatically transactional + * const chunk = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(chunk)) // [1, 2, 3, 4, 5] + * + * // Multi-step atomic modification - use explicit transaction + * yield* Effect.tx( + * Effect.gen(function*() { + * yield* TxChunk.append(txChunk, 6) + * yield* TxChunk.prepend(txChunk, 0) + * }) + * ) + * + * const updated = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(updated)) // [0, 1, 2, 3, 4, 5, 6] + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromIterable = (iterable: Iterable): Effect.Effect> => + Effect.map(TxRef.make(Chunk.fromIterable(iterable)), (ref) => makeUnsafe(ref)) + +/** + * Creates a new `TxChunk` with the specified TxRef. + * + * **Details** + * + * This function returns a new TxChunk reference wrapping the provided TxRef. No existing TxChunk + * instances are modified. + * + * **Example** (Wrapping an existing TxRef) + * + * ```ts + * import { Chunk, TxChunk, TxRef } from "effect" + * + * // Create a TxChunk from an existing TxRef (advanced usage) + * const ref = TxRef.makeUnsafe(Chunk.fromIterable([1, 2, 3])) + * const txChunk = TxChunk.makeUnsafe(ref) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (ref: TxRef.TxRef>): TxChunk => { + const txChunk = Object.create(TxChunkProto) + txChunk[TypeId] = TypeId + txChunk.ref = ref + return txChunk +} + +/** + * Modifies the value of the `TxChunk` using the provided function. + * + * **Details** + * + * This function mutates the original TxChunk by updating its internal state. It does not return a + * new TxChunk reference. + * + * **Example** (Modifying while returning a value) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Modify and return both old size and new chunk + * const oldSize = yield* TxChunk.modify(txChunk, (chunk) => [ + * Chunk.size(chunk), // return value (old size) + * Chunk.append(chunk, 4) // new value + * ]) + * + * console.log(oldSize) // 3 + * + * const newChunk = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(newChunk)) // [1, 2, 3, 4] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const modify: { + ( + f: (current: Chunk.Chunk>) => [returnValue: R, newValue: Chunk.Chunk] + ): (self: TxChunk) => Effect.Effect + ( + self: TxChunk, + f: (current: Chunk.Chunk) => [returnValue: R, newValue: Chunk.Chunk] + ): Effect.Effect +} = dual( + 2, + ( + self: TxChunk, + f: (current: Chunk.Chunk) => [returnValue: R, newValue: Chunk.Chunk] + ): Effect.Effect => TxRef.modify(self.ref, f) +) + +/** + * Updates the value of the `TxChunk` using the provided function. + * + * **Details** + * + * This function mutates the original TxChunk by updating its internal state. It does not return a + * new TxChunk reference. + * + * **Example** (Updating the stored chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Update the chunk by reversing it atomically + * yield* TxChunk.update(txChunk, (chunk) => Chunk.reverse(chunk)) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [3, 2, 1] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const update: { + (f: (current: Chunk.Chunk>) => Chunk.Chunk): (self: TxChunk) => Effect.Effect + (self: TxChunk, f: (current: Chunk.Chunk) => Chunk.Chunk): Effect.Effect +} = dual( + 2, + ( + self: TxChunk, + f: (current: Chunk.Chunk) => Chunk.Chunk + ): Effect.Effect => TxRef.update(self.ref, f) +) + +/** + * Reads the current chunk from the `TxChunk`. + * + * **Example** (Reading the current chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Read the current value within a transaction + * const chunk = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(chunk)) // [1, 2, 3] + * + * // The value is tracked for conflict detection + * const size = Chunk.size(chunk) + * console.log(size) // 3 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const get = (self: TxChunk): Effect.Effect> => TxRef.get(self.ref) + +/** + * Sets the value of the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by replacing its internal state with the provided + * chunk. It does not return a new TxChunk reference. + * + * **Example** (Replacing the stored chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Replace the entire chunk content + * const newChunk = Chunk.fromIterable([10, 20, 30, 40]) + * yield* TxChunk.set(txChunk, newChunk) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [10, 20, 30, 40] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const set: { + (chunk: Chunk.Chunk): (self: TxChunk) => Effect.Effect + (self: TxChunk, chunk: Chunk.Chunk): Effect.Effect +} = dual( + 2, + (self: TxChunk, chunk: Chunk.Chunk): Effect.Effect => TxRef.set(self.ref, chunk) +) + +/** + * Appends an element to the end of the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by adding the element to the end. It does not return a + * new TxChunk reference. + * + * **Example** (Appending an element) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Add element to the end atomically + * yield* TxChunk.append(txChunk, 4) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const append: { + (element: A): (self: TxChunk) => Effect.Effect + (self: TxChunk, element: A): Effect.Effect +} = dual( + 2, + (self: TxChunk, element: A): Effect.Effect => update(self, (current) => Chunk.append(current, element)) +) + +/** + * Prepends an element to the beginning of the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by adding the element to the beginning. It does not + * return a new TxChunk reference. + * + * **Example** (Prepending an element) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([2, 3, 4]) + * + * // Add element to the beginning atomically + * yield* TxChunk.prepend(txChunk, 1) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const prepend: { + (element: A): (self: TxChunk) => Effect.Effect + (self: TxChunk, element: A): Effect.Effect +} = dual( + 2, + (self: TxChunk, element: A): Effect.Effect => update(self, (current) => Chunk.prepend(current, element)) +) + +/** + * Gets the size of the `TxChunk`. + * + * **Example** (Getting the size) + * + * ```ts + * import { Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5]) + * + * // Get the current size - automatically transactional + * const currentSize = yield* TxChunk.size(txChunk) + * console.log(currentSize) // 5 + * + * // Size is tracked for conflict detection + * yield* TxChunk.append(txChunk, 6) + * const newSize = yield* TxChunk.size(txChunk) + * console.log(newSize) // 6 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const size = (self: TxChunk): Effect.Effect => + modify(self, (current) => [Chunk.size(current), current]) + +/** + * Checks whether the `TxChunk` is empty. + * + * **Example** (Checking for an empty chunk) + * + * ```ts + * import { Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const emptyChunk = yield* TxChunk.empty() + * const nonEmptyChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Check if chunks are empty - automatically transactional + * const isEmpty1 = yield* TxChunk.isEmpty(emptyChunk) + * const isEmpty2 = yield* TxChunk.isEmpty(nonEmptyChunk) + * + * console.log(isEmpty1) // true + * console.log(isEmpty2) // false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isEmpty = (self: TxChunk): Effect.Effect => + modify(self, (current) => [Chunk.isEmpty(current), current]) + +/** + * Checks whether the `TxChunk` is non-empty. + * + * **Example** (Checking for a non-empty chunk) + * + * ```ts + * import { Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const emptyChunk = yield* TxChunk.empty() + * const nonEmptyChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * + * // Check if chunks are non-empty - automatically transactional + * const isNonEmpty1 = yield* TxChunk.isNonEmpty(emptyChunk) + * const isNonEmpty2 = yield* TxChunk.isNonEmpty(nonEmptyChunk) + * + * console.log(isNonEmpty1) // false + * console.log(isNonEmpty2) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isNonEmpty = (self: TxChunk): Effect.Effect => + modify(self, (current) => [Chunk.isNonEmpty(current), current]) + +/** + * Takes the first `n` elements from the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by keeping only the first n elements. It does not + * return a new TxChunk reference. + * + * **Example** (Taking leading elements) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5]) + * + * // Take only the first 3 elements - automatically transactional + * yield* TxChunk.take(txChunk, 3) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const take: { + (n: number): (self: TxChunk) => Effect.Effect + (self: TxChunk, n: number): Effect.Effect +} = dual( + 2, + (self: TxChunk, n: number): Effect.Effect => update(self, (current) => Chunk.take(current, n)) +) + +/** + * Drops the first `n` elements from the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by removing the first n elements. It does not return a + * new TxChunk reference. + * + * **Example** (Dropping leading elements) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5]) + * + * // Drop the first 2 elements - automatically transactional + * yield* TxChunk.drop(txChunk, 2) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [3, 4, 5] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const drop: { + (n: number): (self: TxChunk) => Effect.Effect + (self: TxChunk, n: number): Effect.Effect +} = dual( + 2, + (self: TxChunk, n: number): Effect.Effect => update(self, (current) => Chunk.drop(current, n)) +) + +/** + * Takes a slice of the `TxChunk` from `start` to `end` (exclusive). + * + * **Details** + * + * This function mutates the original TxChunk by keeping only the elements in the specified range. It + * does not return a new TxChunk reference. + * + * **Example** (Taking a slice) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5, 6, 7]) + * + * // Take elements from index 2 to 5 (exclusive) - automatically transactional + * yield* TxChunk.slice(txChunk, 2, 5) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [3, 4, 5] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const slice: { + (start: number, end: number): (self: TxChunk) => Effect.Effect + (self: TxChunk, start: number, end: number): Effect.Effect +} = dual( + 3, + (self: TxChunk, start: number, end: number): Effect.Effect => + update(self, (current) => Chunk.take(Chunk.drop(current, start), end - start)) +) + +/** + * Maps each element of the `TxChunk` using a function that returns the same + * element type. + * + * **Details** + * + * This function mutates the original `TxChunk` by transforming each element in place. It does not + * return a new `TxChunk` reference. + * + * **Example** (Mapping elements) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4]) + * + * // Transform each element atomically (must maintain same type) + * yield* TxChunk.map(txChunk, (n) => n * 2) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [2, 4, 6, 8] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const map: { + (f: (a: NoInfer) => A): (self: TxChunk) => Effect.Effect + (self: TxChunk, f: (a: A) => A): Effect.Effect +} = dual( + 2, + (self: TxChunk, f: (a: A) => A): Effect.Effect => update(self, (current) => Chunk.map(current, f)) +) + +/** + * Filters the `TxChunk` keeping only elements that satisfy the predicate. + * + * **Details** + * + * This function mutates the original TxChunk by removing elements that don't match the predicate. It + * does not return a new TxChunk reference. + * + * **Example** (Filtering elements) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3, 4, 5, 6]) + * + * // Keep only even numbers atomically + * yield* TxChunk.filter(txChunk, (n) => n % 2 === 0) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [2, 4, 6] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const filter: { + (refinement: (a: A) => a is B): (self: TxChunk) => Effect.Effect + (predicate: (a: A) => boolean): (self: TxChunk) => Effect.Effect + (self: TxChunk, refinement: (a: A) => a is B): Effect.Effect + (self: TxChunk, predicate: (a: A) => boolean): Effect.Effect +} = dual( + 2, + (self: TxChunk, predicate: (a: A) => boolean): Effect.Effect => + update(self, (current) => Chunk.filter(current, predicate)) +) + +/** + * Concatenates another chunk to the end of the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by appending all elements from the other chunk. It does + * not return a new TxChunk reference. + * + * **Example** (Appending another chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([1, 2, 3]) + * const otherChunk = Chunk.fromIterable([4, 5, 6]) + * + * // Append all elements from another chunk atomically + * yield* TxChunk.appendAll(txChunk, otherChunk) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const appendAll: { + (other: Chunk.Chunk): (self: TxChunk) => Effect.Effect + (self: TxChunk, other: Chunk.Chunk): Effect.Effect +} = dual( + 2, + (self: TxChunk, other: Chunk.Chunk): Effect.Effect => + update(self, (current) => Chunk.appendAll(current, other)) +) + +/** + * Concatenates another chunk to the beginning of the `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by prepending all elements from the other chunk. It + * does not return a new TxChunk reference. + * + * **Example** (Prepending another chunk) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk = yield* TxChunk.fromIterable([4, 5, 6]) + * const otherChunk = Chunk.fromIterable([1, 2, 3]) + * + * // Prepend all elements from another chunk atomically + * yield* TxChunk.prependAll(txChunk, otherChunk) + * + * const result = yield* TxChunk.get(txChunk) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const prependAll: { + (other: Chunk.Chunk): (self: TxChunk) => Effect.Effect + (self: TxChunk, other: Chunk.Chunk): Effect.Effect +} = dual( + 2, + (self: TxChunk, other: Chunk.Chunk): Effect.Effect => + update(self, (current) => Chunk.prependAll(current, other)) +) + +/** + * Concatenates another `TxChunk` to the end of this `TxChunk`. + * + * **Details** + * + * This function mutates the original TxChunk by appending all elements from the other TxChunk. It + * does not return a new TxChunk reference. + * + * **Example** (Concatenating TxChunks) + * + * ```ts + * import { Chunk, Effect, TxChunk } from "effect" + * + * const program = Effect.gen(function*() { + * const txChunk1 = yield* TxChunk.fromIterable([1, 2, 3]) + * const txChunk2 = yield* TxChunk.fromIterable([4, 5, 6]) + * + * // Concatenate atomically within a transaction + * yield* TxChunk.concat(txChunk1, txChunk2) + * + * const result = yield* TxChunk.get(txChunk1) + * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] + * + * // Original txChunk2 is unchanged + * const original = yield* TxChunk.get(txChunk2) + * console.log(Chunk.toReadonlyArray(original)) // [4, 5, 6] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const concat: { + (other: TxChunk): (self: TxChunk) => Effect.Effect + (self: TxChunk, other: TxChunk): Effect.Effect +} = dual( + 2, + (self: TxChunk, other: TxChunk): Effect.Effect => + Effect.gen(function*() { + const otherChunk = yield* get(other) + yield* appendAll(self, otherChunk) + }).pipe(Effect.tx) +) diff --git a/repos/effect/packages/effect/src/TxDeferred.ts b/repos/effect/packages/effect/src/TxDeferred.ts new file mode 100644 index 0000000..851f811 --- /dev/null +++ b/repos/effect/packages/effect/src/TxDeferred.ts @@ -0,0 +1,319 @@ +/** + * Transactional deferred values for coordinating Effect transactions. + * + * A `TxDeferred` is a write-once cell whose completion is a + * `Result` stored in transactional state. Readers can wait for the value + * from inside a transaction: while the cell is empty the transaction retries, + * and when another transaction completes the deferred the waiting transaction + * can resume with either the success value or the typed failure. + * + * @since 4.0.0 + */ + +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Option } from "./Option.ts" +import * as O from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type { Result } from "./Result.ts" +import * as Res from "./Result.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxDeferred" + +/** + * A transactional deferred is a write-once cell readable within transactions. + * Readers block (retry the transaction) until a value is committed, and writers + * succeed only on the first call; subsequent writes return `false`. + * + * **When to use** + * + * Use to coordinate transaction-local readers and one-time completion with a + * success or failure result. + * + * **Example** (Completing a transactional deferred) + * + * ```ts + * import { Effect, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * + * // Complete the deferred + * const first = yield* TxDeferred.succeed(deferred, 42) + * console.log(first) // true + * + * // Second write is a no-op + * const second = yield* TxDeferred.succeed(deferred, 99) + * console.log(second) // false + * + * // Read the value + * const value = yield* TxDeferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxDeferred extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly ref: TxRef.TxRef>> +} + +const TxDeferredProto: Omit, typeof TypeId | "ref"> = { + [NodeInspectSymbol](this: TxDeferred) { + return toJson(this) + }, + toJSON(this: TxDeferred) { + return { + _id: "TxDeferred" + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeTxDeferred = (ref: TxRef.TxRef>>): TxDeferred => { + const self = Object.create(TxDeferredProto) + self[TypeId] = TypeId + self.ref = ref + return self +} + +/** + * Creates a new empty `TxDeferred`. + * + * **When to use** + * + * Use to create a transactional deferred that can be completed exactly once. + * + * **Example** (Creating a transactional deferred) + * + * ```ts + * import { Effect, Option, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * const state = yield* TxDeferred.poll(deferred) + * console.log(Option.isNone(state)) // true + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect.Effect> => + Effect.map(TxRef.make>>(O.none()), makeTxDeferred) + +/** + * Reads the deferred value. Retries the transaction if the deferred has not + * been completed yet. + * + * **Example** (Awaiting a deferred value) + * + * ```ts + * import { Effect, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * yield* TxDeferred.succeed(deferred, 42) + * const value = yield* TxDeferred.await(deferred) + * console.log(value) // 42 + * }) + * ``` + * + * @category getters + * @since 4.0.0 + */ +const await_ = (self: TxDeferred): Effect.Effect => + Effect.gen(function*() { + const option = yield* TxRef.get(self.ref) + if (O.isNone(option)) { + return yield* Effect.txRetry + } + return Res.isSuccess(option.value) + ? option.value.success + : yield* Effect.fail(option.value.failure) + }).pipe(Effect.tx) + +export { + /** + * Reads the deferred value. Retries the transaction if the deferred has not + * been completed yet. + * + * **When to use** + * + * Use to read the success value of a `TxDeferred` while retrying until the + * deferred is completed. + * + * @see {@link poll} for inspecting the current completion state without retrying the transaction + * + * @category getters + * @since 4.0.0 + */ + await_ as await +} + +/** + * Reads the current state of the deferred without retrying. Returns `None` if + * not yet completed. + * + * **When to use** + * + * Use to inspect a `TxDeferred` without retrying when it is not completed yet. + * + * **Example** (Polling a deferred) + * + * ```ts + * import { Effect, Option, Result, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * const before = yield* TxDeferred.poll(deferred) + * console.log(Option.isNone(before)) // true + * + * yield* TxDeferred.succeed(deferred, 42) + * const after = yield* TxDeferred.poll(deferred) + * console.log(after) // Some(Success(42)) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const poll = (self: TxDeferred): Effect.Effect>> => TxRef.get(self.ref) + +/** + * Completes the deferred with a `Result`. Returns `true` if this was the first + * completion, `false` if already completed. + * + * **When to use** + * + * Use to complete a `TxDeferred` with an already computed `Result`. + * + * **Example** (Completing with a result) + * + * ```ts + * import { Effect, Result, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * const first = yield* TxDeferred.done(deferred, Result.succeed(42)) + * console.log(first) // true + * const second = yield* TxDeferred.done(deferred, Result.succeed(99)) + * console.log(second) // false + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const done: { + (result: Result): (self: TxDeferred) => Effect.Effect + (self: TxDeferred, result: Result): Effect.Effect +} = dual( + 2, + (self: TxDeferred, result: Result): Effect.Effect => + TxRef.modify(self.ref, (current) => { + if (O.isSome(current)) { + return [false, current] + } + return [true, O.some(result)] + }) +) + +/** + * Completes the deferred with a success value. Returns `true` if this was the + * first completion, `false` if already completed. + * + * **When to use** + * + * Use to complete a `TxDeferred` with a successful value. + * + * **Example** (Completing with a success value) + * + * ```ts + * import { Effect, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * const first = yield* TxDeferred.succeed(deferred, 42) + * console.log(first) // true + * const second = yield* TxDeferred.succeed(deferred, 99) + * console.log(second) // false + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const succeed: { + (value: A): (self: TxDeferred) => Effect.Effect + (self: TxDeferred, value: A): Effect.Effect +} = dual( + 2, + (self: TxDeferred, value: A): Effect.Effect => done(self, Res.succeed(value)) +) + +/** + * Completes the deferred with a failure. Returns `true` if this was the first + * completion, `false` if already completed. + * + * **When to use** + * + * Use to complete a `TxDeferred` with a typed failure value. + * + * **Example** (Completing with a failure) + * + * ```ts + * import { Effect, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * const first = yield* TxDeferred.fail(deferred, "boom") + * console.log(first) // true + * const second = yield* TxDeferred.fail(deferred, "boom2") + * console.log(second) // false + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const fail: { + (error: E): (self: TxDeferred) => Effect.Effect + (self: TxDeferred, error: E): Effect.Effect +} = dual( + 2, + (self: TxDeferred, error: E): Effect.Effect => done(self, Res.fail(error)) +) + +/** + * Determines if the provided value is a `TxDeferred`. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a transactional deferred. + * + * **Example** (Checking transactional deferreds) + * + * ```ts + * import { Effect, TxDeferred } from "effect" + * + * const program = Effect.gen(function*() { + * const deferred = yield* TxDeferred.make() + * console.log(TxDeferred.isTxDeferred(deferred)) // true + * console.log(TxDeferred.isTxDeferred("not a deferred")) // false + * }) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxDeferred = (u: unknown): u is TxDeferred => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/TxHashMap.ts b/repos/effect/packages/effect/src/TxHashMap.ts new file mode 100644 index 0000000..c18ff1e --- /dev/null +++ b/repos/effect/packages/effect/src/TxHashMap.ts @@ -0,0 +1,2165 @@ +/** + * Transactional hash maps for storing and updating key-value pairs inside + * Effect transactions. + * + * A `TxHashMap` stores an immutable `HashMap` in a `TxRef`, so map reads and + * writes can commit atomically with other transactional operations. Use it for + * shared registries, counters, indexes, and other maps that need safe + * read-modify-write sequences alongside related transactional state. + * + * @since 2.0.0 + */ + +import * as Effect from "./Effect.ts" +import { format } from "./Formatter.ts" +import { dual } from "./Function.ts" +import * as HashMap from "./HashMap.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type { Result } from "./Result.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxHashMap" + +const TxHashMapProto = { + [TypeId]: TypeId, + [NodeInspectSymbol](this: TxHashMap) { + return toJson(this) + }, + toString(this: TxHashMap) { + return `TxHashMap(${format(toJson((this).ref))})` + }, + toJSON(this: TxHashMap) { + return { + _id: "TxHashMap", + ref: toJson((this).ref) + } + }, + pipe(this: TxHashMap) { + return pipeArguments(this, arguments) + } +} + +/** + * A TxHashMap is a transactional hash map data structure that provides atomic operations + * on key-value pairs within Effect transactions. It uses an immutable HashMap internally + * with TxRef for transactional semantics, ensuring all operations are performed atomically. + * + * **Example** (Using transactional hash maps) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional hash map + * const txMap = yield* TxHashMap.make(["user1", "Alice"], ["user2", "Bob"]) + * + * // Single operations are automatically transactional + * yield* TxHashMap.set(txMap, "user3", "Charlie") + * const user = yield* TxHashMap.get(txMap, "user1") + * console.log(user) // Option.some("Alice") + * + * // Multi-step atomic operations + * yield* Effect.tx( + * Effect.gen(function*() { + * const currentUser = yield* TxHashMap.get(txMap, "user1") + * if (currentUser._tag === "Some") { + * yield* TxHashMap.set(txMap, "user1", currentUser.value + "_updated") + * yield* TxHashMap.remove(txMap, "user2") + * } + * }) + * ) + * + * const size = yield* TxHashMap.size(txMap) + * console.log(size) // 2 + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxHashMap extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly ref: TxRef.TxRef> +} + +/** + * The TxHashMap namespace contains type-level utilities and helper types + * for working with TxHashMap instances. + * + * **Example** (Reusing extracted TxHashMap types) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional inventory map + * const inventory = yield* TxHashMap.make( + * ["laptop", { stock: 5, price: 999 }], + * ["mouse", { stock: 20, price: 29 }] + * ) + * + * // Extract types for reuse + * type ProductId = TxHashMap.TxHashMap.Key // string + * type Product = TxHashMap.TxHashMap.Value // { stock: number, price: number } + * type InventoryEntry = TxHashMap.TxHashMap.Entry // [string, Product] + * + * // Use extracted types in functions + * const updateStock = (id: ProductId, newStock: number) => + * TxHashMap.modify( + * inventory, + * id, + * (product) => ({ ...product, stock: newStock }) + * ) + * + * yield* updateStock("laptop", 3) + * }) + * ``` + * + * @since 4.0.0 + */ +export declare namespace TxHashMap { + /** + * Extracts the key type from a TxHashMap type. + * + * **Example** (Extracting key types) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a user map to extract key type from + * const userMap = yield* TxHashMap.make( + * ["alice", { name: "Alice", age: 30 }], + * ["bob", { name: "Bob", age: 25 }] + * ) + * + * // Extract the key type (string) + * type UserKey = TxHashMap.TxHashMap.Key + * + * // Use the extracted type in functions + * const getUserById = (id: UserKey) => TxHashMap.get(userMap, id) + * const alice = yield* getUserById("alice") // Option<{ name: string, age: number }> + * }) + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type Key> = T extends TxHashMap ? K : never + + /** + * Extracts the value type from a TxHashMap type. + * + * **Example** (Extracting value types) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a product catalog TxHashMap + * const catalog = yield* TxHashMap.make( + * ["laptop", { price: 999, category: "electronics" }], + * ["book", { price: 29, category: "education" }] + * ) + * + * // Extract the value type (Product) + * type Product = TxHashMap.TxHashMap.Value + * + * // Use the extracted type for type-safe operations + * const processProduct = (product: Product) => { + * return `${product.category}: $${product.price}` + * } + * + * const laptop = yield* TxHashMap.get(catalog, "laptop") + * // laptop has type Option thanks to type extraction + * }) + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type Value> = T extends TxHashMap ? V : never + + /** + * Extracts the entry type from a TxHashMap type. + * + * **Example** (Extracting entry types) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a configuration TxHashMap + * const config = yield* TxHashMap.make( + * ["api_url", "https://api.example.com"], + * ["timeout", "5000"], + * ["retries", "3"] + * ) + * + * // Extract the entry type [string, string] + * type ConfigEntry = TxHashMap.TxHashMap.Entry + * + * // Use the extracted type for processing entries + * const processEntry = ([key, value]: ConfigEntry) => { + * return `${key}=${value}` + * } + * + * // Get all entries and process them + * const entries = yield* TxHashMap.entries(config) + * const configLines = entries.map(processEntry) + * console.log(configLines) // ["api_url=https://api.example.com", ...] + * }) + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type Entry> = T extends TxHashMap ? readonly [K, V] : never +} + +/** + * Creates an empty TxHashMap. + * + * **Example** (Creating an empty map) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an empty transactional hash map + * const emptyMap = yield* TxHashMap.empty() + * + * // Verify it's empty + * const isEmpty = yield* TxHashMap.isEmpty(emptyMap) + * console.log(isEmpty) // true + * + * const size = yield* TxHashMap.size(emptyMap) + * console.log(size) // 0 + * + * // Start adding elements + * yield* TxHashMap.set(emptyMap, "first", 1) + * const newSize = yield* TxHashMap.size(emptyMap) + * console.log(newSize) // 1 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Effect.Effect> => + Effect.gen(function*() { + const ref = yield* TxRef.make(HashMap.empty()) + return Object.assign(Object.create(TxHashMapProto), { ref }) + }) + +/** + * Creates a TxHashMap from the provided key-value pairs. + * + * **Example** (Creating a map from entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a user directory + * const userMap = yield* TxHashMap.make( + * ["alice", { name: "Alice Smith", role: "admin" }], + * ["bob", { name: "Bob Johnson", role: "user" }], + * ["charlie", { name: "Charlie Brown", role: "user" }] + * ) + * + * // Check the initial size + * const size = yield* TxHashMap.size(userMap) + * console.log(size) // 3 + * + * // Access users + * const alice = yield* TxHashMap.get(userMap, "alice") + * console.log(alice) // Option.some({ name: "Alice Smith", role: "admin" }) + * + * const nonExistent = yield* TxHashMap.get(userMap, "david") + * console.log(nonExistent) // Option.none() + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = ( + ...entries: Array +): Effect.Effect> => + Effect.gen(function*() { + const hashMap = HashMap.make(...entries) + const ref = yield* TxRef.make(hashMap) + return Object.assign(Object.create(TxHashMapProto), { ref }) + }) + +/** + * Creates a TxHashMap from an iterable of key-value pairs. + * + * **Example** (Creating a map from an iterable) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create from various iterable sources + * const configEntries = [ + * ["database.host", "localhost"], + * ["database.port", "5432"], + * ["cache.enabled", "true"], + * ["logging.level", "info"] + * ] as const + * + * const configMap = yield* TxHashMap.fromIterable(configEntries) + * + * // Verify the configuration was loaded + * const size = yield* TxHashMap.size(configMap) + * console.log(size) // 4 + * + * const dbHost = yield* TxHashMap.get(configMap, "database.host") + * console.log(dbHost) // Option.some("localhost") + * + * // Can also create from Map, Set of tuples, etc. + * const jsMap = new Map([["key1", "value1"], ["key2", "value2"]]) + * const txMapFromJs = yield* TxHashMap.fromIterable(jsMap) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = ( + entries: Iterable +): Effect.Effect> => + Effect.gen(function*() { + const hashMap = HashMap.fromIterable(entries) + const ref = yield* TxRef.make(hashMap) + return Object.assign(Object.create(TxHashMapProto), { ref }) + }) + +/** + * Looks up the value for the specified key in the TxHashMap. + * + * **Example** (Looking up values safely) + * + * ```ts + * import { Effect, Option, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const userMap = yield* TxHashMap.make( + * ["alice", { name: "Alice", role: "admin" }], + * ["bob", { name: "Bob", role: "user" }] + * ) + * + * // Safe lookup - returns Option + * const alice = yield* TxHashMap.get(userMap, "alice") + * console.log(alice) // Option.some({ name: "Alice", role: "admin" }) + * + * const nonExistent = yield* TxHashMap.get(userMap, "charlie") + * console.log(nonExistent) // Option.none() + * + * // Use with pipe syntax for type-safe access + * const bobRole = yield* TxHashMap.get(userMap, "bob") + * if (bobRole._tag === "Some") { + * console.log(bobRole.value.role) // "user" + * } + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const get: { + (key: K1): (self: TxHashMap) => Effect.Effect> + (self: TxHashMap, key: K1): Effect.Effect> +} = dual( + 2, + (self: TxHashMap, key: K1): Effect.Effect> => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.get(map, key) + }) +) + +/** + * Sets the value for the specified key in the TxHashMap. + * + * **Details** + * + * This function mutates the original TxHashMap by updating its internal state. + * It does not return a new TxHashMap reference. + * + * **Example** (Setting values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const inventory = yield* TxHashMap.make( + * ["laptop", 5], + * ["mouse", 20] + * ) + * + * // Update existing item + * yield* TxHashMap.set(inventory, "laptop", 3) + * const laptopStock = yield* TxHashMap.get(inventory, "laptop") + * console.log(laptopStock) // Option.some(3) + * + * // Add new item + * yield* TxHashMap.set(inventory, "keyboard", 15) + * const keyboardStock = yield* TxHashMap.get(inventory, "keyboard") + * console.log(keyboardStock) // Option.some(15) + * + * // Use with pipe syntax + * yield* TxHashMap.set("tablet", 8)(inventory) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const set: { + (key: K, value: V): (self: TxHashMap) => Effect.Effect + (self: TxHashMap, key: K, value: V): Effect.Effect +} = dual( + 3, + (self: TxHashMap, key: K, value: V): Effect.Effect => + TxRef.update(self.ref, (map) => HashMap.set(map, key, value)) +) + +/** + * Checks whether the specified key exists in the TxHashMap. + * + * **Example** (Checking for keys) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const permissions = yield* TxHashMap.make( + * ["alice", ["read", "write"]], + * ["bob", ["read"]], + * ["charlie", ["admin"]] + * ) + * + * // Check if users exist + * const hasAlice = yield* TxHashMap.has(permissions, "alice") + * console.log(hasAlice) // true + * + * const hasDavid = yield* TxHashMap.has(permissions, "david") + * console.log(hasDavid) // false + * + * // Use direct method call for type-safe access + * const hasBob = yield* TxHashMap.has(permissions, "bob") + * console.log(hasBob) // true + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const has: { + (key: K1): (self: TxHashMap) => Effect.Effect + (self: TxHashMap, key: K1): Effect.Effect +} = dual( + 2, + (self: TxHashMap, key: K1): Effect.Effect => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.has(map, key) + }) +) + +/** + * Removes the specified key from the TxHashMap. + * + * **Details** + * + * This function mutates the original TxHashMap by removing the specified + * key-value pair. It does not return a new TxHashMap reference. + * + * **Example** (Removing keys) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const cache = yield* TxHashMap.make( + * ["user:1", { name: "Alice", lastSeen: "2024-01-01" }], + * ["user:2", { name: "Bob", lastSeen: "2024-01-02" }], + * ["user:3", { name: "Charlie", lastSeen: "2023-12-30" }] + * ) + * + * // Remove expired user + * const removed = yield* TxHashMap.remove(cache, "user:3") + * console.log(removed) // true (key existed and was removed) + * + * // Try to remove non-existent key + * const notRemoved = yield* TxHashMap.remove(cache, "user:999") + * console.log(notRemoved) // false (key didn't exist) + * + * // Verify removal + * const hasUser3 = yield* TxHashMap.has(cache, "user:3") + * console.log(hasUser3) // false + * + * const size = yield* TxHashMap.size(cache) + * console.log(size) // 2 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const remove: { + (key: K1): (self: TxHashMap) => Effect.Effect + (self: TxHashMap, key: K1): Effect.Effect +} = dual( + 2, + (self: TxHashMap, key: K1): Effect.Effect => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const existed = HashMap.has(currentMap, key) + if (existed) { + yield* TxRef.set(self.ref, HashMap.remove(currentMap, key)) + } + return existed + }).pipe(Effect.tx) +) + +/** + * Removes all entries from the TxHashMap. + * + * **Details** + * + * This function mutates the original TxHashMap by clearing all key-value pairs. + * It does not return a new TxHashMap reference. + * + * **Example** (Clearing all entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const sessionMap = yield* TxHashMap.make( + * ["session1", { userId: "alice", expires: "2024-01-01T12:00:00Z" }], + * ["session2", { userId: "bob", expires: "2024-01-01T13:00:00Z" }], + * ["session3", { userId: "charlie", expires: "2024-01-01T14:00:00Z" }] + * ) + * + * // Check initial state + * const initialSize = yield* TxHashMap.size(sessionMap) + * console.log(initialSize) // 3 + * + * // Clear all sessions (e.g., during maintenance) + * yield* TxHashMap.clear(sessionMap) + * + * // Verify cleared + * const finalSize = yield* TxHashMap.size(sessionMap) + * console.log(finalSize) // 0 + * + * const isEmpty = yield* TxHashMap.isEmpty(sessionMap) + * console.log(isEmpty) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const clear = (self: TxHashMap): Effect.Effect => TxRef.set(self.ref, HashMap.empty()) + +/** + * Returns the number of entries in the TxHashMap. + * + * **Example** (Counting entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const metrics = yield* TxHashMap.make( + * ["requests", 1000], + * ["errors", 5], + * ["users", 50] + * ) + * + * const count = yield* TxHashMap.size(metrics) + * console.log(count) // 3 + * + * // Add more metrics + * yield* TxHashMap.set(metrics, "response_time", 250) + * const newCount = yield* TxHashMap.size(metrics) + * console.log(newCount) // 4 + * + * // Remove a metric + * yield* TxHashMap.remove(metrics, "errors") + * const finalCount = yield* TxHashMap.size(metrics) + * console.log(finalCount) // 3 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const size = (self: TxHashMap): Effect.Effect => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.size(map) + }) + +/** + * Checks whether the TxHashMap is empty. + * + * **Example** (Checking for an empty map) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Start with empty map + * const cache = yield* TxHashMap.empty() + * const empty = yield* TxHashMap.isEmpty(cache) + * console.log(empty) // true + * + * // Add an item + * yield* TxHashMap.set(cache, "key1", "value1") + * const stillEmpty = yield* TxHashMap.isEmpty(cache) + * console.log(stillEmpty) // false + * + * // Clear and check again + * yield* TxHashMap.clear(cache) + * const emptyAgain = yield* TxHashMap.isEmpty(cache) + * console.log(emptyAgain) // true + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const isEmpty = (self: TxHashMap): Effect.Effect => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.isEmpty(map) + }) + +/** + * Checks whether the TxHashMap is non-empty. + * + * **Example** (Checking for a non-empty map) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const inventory = yield* TxHashMap.make(["laptop", 5]) + * + * const hasItems = yield* TxHashMap.isNonEmpty(inventory) + * console.log(hasItems) // true + * + * // Clear inventory + * yield* TxHashMap.clear(inventory) + * const stillHasItems = yield* TxHashMap.isNonEmpty(inventory) + * console.log(stillHasItems) // false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isNonEmpty = (self: TxHashMap): Effect.Effect => + Effect.map(isEmpty(self), (empty) => !empty) + +/** + * Updates the value for the specified key if it exists, returning the previous value in `Some`; returns `None` and leaves the map unchanged when the key is absent. + * + * **Details** + * + * This function mutates the original TxHashMap by updating the value at the + * specified key. It does not return a new TxHashMap reference. + * + * **Example** (Updating existing values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const counters = yield* TxHashMap.make( + * ["downloads", 100], + * ["views", 250] + * ) + * + * // Increment existing counter + * const oldDownloads = yield* TxHashMap.modify( + * counters, + * "downloads", + * (count) => count + 1 + * ) + * console.log(oldDownloads) // Option.some(100) + * + * const newDownloads = yield* TxHashMap.get(counters, "downloads") + * console.log(newDownloads) // Option.some(101) + * + * // Try to modify non-existent key + * const nonExistent = yield* TxHashMap.modify( + * counters, + * "clicks", + * (count) => count + 1 + * ) + * console.log(nonExistent) // Option.none() + * + * // Update views counter with direct method call + * yield* TxHashMap.modify(counters, "views", (views) => views * 2) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const modify: { + ( + key: K, + f: (value: V) => V + ): (self: TxHashMap) => Effect.Effect> + (self: TxHashMap, key: K, f: (value: V) => V): Effect.Effect> +} = dual( + 3, + ( + self: TxHashMap, + key: K, + f: (value: V) => V + ): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const currentValue = HashMap.get(currentMap, key) + if (Option.isSome(currentValue)) { + const newValue = f(currentValue.value) + yield* TxRef.set(self.ref, HashMap.set(currentMap, key, newValue)) + return currentValue + } + return Option.none() + }).pipe(Effect.tx) +) + +/** + * Updates the value for the specified key using an Option-based update function. + * + * **Details** + * + * This function mutates the original TxHashMap by updating, adding, or removing + * the key-value pair based on the function result. It does not return a new + * TxHashMap reference. + * + * **Example** (Updating values with Option) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const storage = yield* TxHashMap.make([ + * "file1.txt", + * "content1" + * ], ["access_count", 0]) + * + * // Increment existing counter + * yield* TxHashMap.modifyAt(storage, "access_count", (current) => + * current._tag === "Some" && typeof current.value === "number" + * ? { ...current, value: current.value + 1 } + * : current + * ) + * const count1 = yield* TxHashMap.get(storage, "access_count") + * console.log(count1) // Option.some(1) + * + * // Increment existing counter again + * yield* TxHashMap.modifyAt(storage, "access_count", (current) => + * current._tag === "Some" && typeof current.value === "number" + * ? { ...current, value: current.value + 1 } + * : current + * ) + * const count2 = yield* TxHashMap.get(storage, "access_count") + * console.log(count2) // Option.some(2) + * + * // Update an existing string entry + * yield* TxHashMap.modifyAt(storage, "file1.txt", (current) => + * current._tag === "Some" && typeof current.value === "string" + * ? { ...current, value: `${current.value}.bak` } + * : current + * ) + * const backup = yield* TxHashMap.get(storage, "file1.txt") + * console.log(backup) // Option.some("content1.bak") + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const modifyAt: { + ( + key: K, + f: (value: Option.Option) => Option.Option + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + key: K, + f: (value: Option.Option) => Option.Option + ): Effect.Effect +} = dual( + 3, + ( + self: TxHashMap, + key: K, + f: (value: Option.Option) => Option.Option + ): Effect.Effect => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const currentValue = HashMap.get(currentMap, key) + const newValue = f(currentValue) + + if (Option.isSome(newValue)) { + yield* TxRef.set(self.ref, HashMap.set(currentMap, key, newValue.value)) + } else if (Option.isSome(currentValue)) { + yield* TxRef.set(self.ref, HashMap.remove(currentMap, key)) + } + }).pipe(Effect.tx) +) + +/** + * Returns an array of all keys in the TxHashMap. + * + * **Example** (Reading keys) + * + * ```ts + * import { Effect, Option, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const userRoles = yield* TxHashMap.make( + * ["alice", "admin"], + * ["bob", "user"], + * ["charlie", "moderator"] + * ) + * + * const usernames = yield* TxHashMap.keys(userRoles) + * console.log(usernames.sort()) // ["alice", "bob", "charlie"] + * + * // Useful for iteration + * for (const username of usernames) { + * const role = yield* TxHashMap.get(userRoles, username) + * if (role._tag === "Some") { + * console.log(`${username}: ${role.value}`) + * } + * } + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const keys = (self: TxHashMap): Effect.Effect> => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return Array.from(HashMap.keys(map)) + }) + +/** + * Returns an array of all values in the TxHashMap. + * + * **Example** (Reading values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const scores = yield* TxHashMap.make( + * ["alice", 95], + * ["bob", 87], + * ["charlie", 92] + * ) + * + * const allScores = yield* TxHashMap.values(scores) + * console.log(allScores.sort((a, b) => a - b)) // [87, 92, 95] + * + * // Calculate average + * const average = allScores.reduce((sum, score) => sum + score, 0) / + * allScores.length + * console.log(average.toFixed(2)) // "91.33" + * + * // Find maximum + * const maxScore = Math.max(...allScores) + * console.log(maxScore) // 95 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const values = (self: TxHashMap): Effect.Effect> => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.toValues(map) + }) + +/** + * Returns an array of all key-value pairs in the TxHashMap. + * + * **Example** (Reading entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const config = yield* TxHashMap.make( + * ["host", "localhost"], + * ["port", "3000"], + * ["ssl", "false"] + * ) + * + * const allEntries = yield* TxHashMap.entries(config) + * const sortedEntries = allEntries.toSorted(([left], [right]) => left.localeCompare(right)) + * console.log(sortedEntries) + * // [["host", "localhost"], ["port", "3000"], ["ssl", "false"]] + * + * // Process configuration entries + * for (const [key, value] of sortedEntries) { + * console.log(`${key}=${value}`) + * } + * // host=localhost + * // port=3000 + * // ssl=false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const entries = ( + self: TxHashMap +): Effect.Effect> => + Effect.gen(function*() { + const map = yield* TxRef.get(self.ref) + return HashMap.toEntries(map) + }) + +/** + * Returns an immutable snapshot of the current TxHashMap state. + * + * **Example** (Taking immutable snapshots) + * + * ```ts + * import { Effect, HashMap, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const liveData = yield* TxHashMap.make( + * ["temperature", 22.5], + * ["humidity", 45.2], + * ["pressure", 1013.25] + * ) + * + * // Take snapshot for reporting + * const snapshot = yield* TxHashMap.snapshot(liveData) + * + * // Continue modifying live data + * yield* TxHashMap.set(liveData, "temperature", 23.1) + * yield* TxHashMap.set(liveData, "wind_speed", 5.3) + * + * // Snapshot remains unchanged + * console.log(HashMap.size(snapshot)) // 3 + * console.log(HashMap.get(snapshot, "temperature")) // Option.some(22.5) + * + * // Can use regular HashMap operations on snapshot + * const tempReading = HashMap.get(snapshot, "temperature") + * const humidityReading = HashMap.get(snapshot, "humidity") + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const snapshot = ( + self: TxHashMap +): Effect.Effect> => TxRef.get(self.ref) + +/** + * Merges another HashMap into this TxHashMap. If both maps contain the same key, + * the value from the other map will be used. + * + * **Details** + * + * This function mutates the original TxHashMap by merging the provided HashMap + * into it. It does not return a new TxHashMap reference. + * + * **Example** (Merging HashMaps) + * + * ```ts + * import { Effect, HashMap, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create initial user preferences + * const userPrefs = yield* TxHashMap.make( + * ["theme", "light"], + * ["language", "en"], + * ["notifications", "enabled"] + * ) + * + * // New preferences to merge in + * const newSettings = HashMap.make( + * ["theme", "dark"], // will override existing + * ["timezone", "UTC"], // new setting + * ["sound", "enabled"] // new setting + * ) + * + * // Merge the new settings + * yield* TxHashMap.union(userPrefs, newSettings) + * + * // Check the merged result + * const theme = yield* TxHashMap.get(userPrefs, "theme") + * console.log(theme) // Option.some("dark") - overridden + * + * const language = yield* TxHashMap.get(userPrefs, "language") + * console.log(language) // Option.some("en") - preserved + * + * const timezone = yield* TxHashMap.get(userPrefs, "timezone") + * console.log(timezone) // Option.some("UTC") - newly added + * + * const size = yield* TxHashMap.size(userPrefs) + * console.log(size) // 5 total settings + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const union: { + ( + other: HashMap.HashMap + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + other: HashMap.HashMap + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + other: HashMap.HashMap + ): Effect.Effect => TxRef.update(self.ref, (map) => HashMap.union(map, other)) +) + +/** + * Removes multiple keys from the TxHashMap. + * + * **Details** + * + * This function mutates the original TxHashMap by removing all specified keys. + * It does not return a new TxHashMap reference. + * + * **Example** (Removing multiple keys) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a cache with temporary data + * const cache = yield* TxHashMap.make( + * ["session_1", { user: "alice", expires: "2024-01-01" }], + * ["session_2", { user: "bob", expires: "2024-01-01" }], + * ["session_3", { user: "charlie", expires: "2024-12-31" }], + * ["temp_data_1", { value: "temporary" }], + * ["temp_data_2", { value: "also_temporary" }] + * ) + * + * console.log(yield* TxHashMap.size(cache)) // 5 + * + * // Remove expired sessions and temporary data + * const keysToRemove = ["session_1", "session_2", "temp_data_1", "temp_data_2"] + * yield* TxHashMap.removeMany(cache, keysToRemove) + * + * console.log(yield* TxHashMap.size(cache)) // 1 + * + * // Verify only the valid session remains + * const remainingSession = yield* TxHashMap.get(cache, "session_3") + * console.log(remainingSession) // Option.some({ user: "charlie", expires: "2024-12-31" }) + * + * // Can also remove from Set, Array, or any iterable + * const moreKeysToRemove = new Set(["session_3"]) + * yield* TxHashMap.removeMany(cache, moreKeysToRemove) + * console.log(yield* TxHashMap.isEmpty(cache)) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const removeMany: { + (keys: Iterable): (self: TxHashMap) => Effect.Effect + (self: TxHashMap, keys: Iterable): Effect.Effect +} = dual( + 2, + (self: TxHashMap, keys: Iterable): Effect.Effect => + TxRef.update(self.ref, (map) => HashMap.removeMany(map, keys)) +) + +/** + * Sets multiple key-value pairs in the TxHashMap. + * + * **Details** + * + * This function mutates the original TxHashMap by setting all provided key-value + * pairs. It does not return a new TxHashMap reference. + * + * **Example** (Setting multiple entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an empty product catalog + * const catalog = yield* TxHashMap.empty< + * string, + * { price: number; stock: number } + * >() + * + * // Bulk load initial products + * const initialProducts: Array< + * readonly [string, { price: number; stock: number }] + * > = [ + * ["laptop", { price: 999, stock: 5 }], + * ["mouse", { price: 29, stock: 50 }], + * ["keyboard", { price: 79, stock: 20 }], + * ["monitor", { price: 299, stock: 8 }] + * ] + * + * yield* TxHashMap.setMany(catalog, initialProducts) + * + * console.log(yield* TxHashMap.size(catalog)) // 4 + * + * // Update prices with a new batch + * const priceUpdates: Array< + * readonly [string, { price: number; stock: number }] + * > = [ + * ["laptop", { price: 899, stock: 5 }], // sale price + * ["mouse", { price: 25, stock: 50 }], // sale price + * ["webcam", { price: 89, stock: 12 }] // new product + * ] + * + * yield* TxHashMap.setMany(catalog, priceUpdates) + * + * console.log(yield* TxHashMap.size(catalog)) // 5 (4 original + 1 new) + * + * // Verify the updates + * const laptop = yield* TxHashMap.get(catalog, "laptop") + * console.log(laptop) // Option.some({ price: 899, stock: 5 }) + * + * // Can also use Map, Set of tuples, or any iterable of entries + * const jsMap = new Map([["tablet", { price: 399, stock: 3 }]]) + * yield* TxHashMap.setMany(catalog, jsMap) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const setMany: { + ( + entries: Iterable + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + entries: Iterable + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + entries: Iterable + ): Effect.Effect => TxRef.update(self.ref, (map) => HashMap.setMany(map, entries)) +) + +/** + * Returns `true` if the specified value is a `TxHashMap`, `false` otherwise. + * + * **Example** (Checking TxHashMap values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const txMap = yield* TxHashMap.make(["key", "value"]) + * + * console.log(TxHashMap.isTxHashMap(txMap)) // true + * console.log(TxHashMap.isTxHashMap({})) // false + * console.log(TxHashMap.isTxHashMap(null)) // false + * console.log(TxHashMap.isTxHashMap("not a map")) // false + * + * // Useful for type guards in runtime checks + * const validateInput = (value: unknown) => { + * if (TxHashMap.isTxHashMap(value)) { + * // TypeScript now knows this is a TxHashMap + * return Effect.succeed("Valid TxHashMap") + * } + * return Effect.fail("Invalid input") + * } + * }) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxHashMap = (value: unknown): value is TxHashMap => { + return hasProperty(value, TypeId) +} + +/** + * Looks up the value for the specified key using a caller-supplied hash. + * + * **Gotchas** + * + * The supplied hash must be the hash for the same key, such as a precomputed + * `Hash.hash(key)` value. If the hash does not match the key, an existing entry + * may not be found. + * + * **Example** (Looking up values with precomputed hashes) + * + * ```ts + * import { Effect, Hash, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a cache with user sessions + * const cache = yield* TxHashMap.make( + * ["session_abc123", { userId: "user1", lastActive: 1_700_000_000_000 }], + * ["session_def456", { userId: "user2", lastActive: 1_700_000_060_000 }] + * ) + * + * // When you have precomputed hash (e.g., from another lookup) + * const sessionId = "session_abc123" + * const precomputedHash = Hash.string(sessionId) + * + * // Use hash-optimized lookup for performance in hot paths + * const session = yield* TxHashMap.getHash(cache, sessionId, precomputedHash) + * console.log(session) // Option.some({ userId: "user1", lastActive: ... }) + * + * // This avoids recomputing the hash when you already have it + * const invalidSession = yield* TxHashMap.getHash( + * cache, + * "invalid", + * Hash.string("invalid") + * ) + * console.log(invalidSession) // Option.none() + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const getHash: { + ( + key: K1, + hash: number + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + key: K1, + hash: number + ): Effect.Effect> +} = dual( + 3, + ( + self: TxHashMap, + key: K1, + hash: number + ): Effect.Effect> => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.getHash(map, key, hash))) +) + +/** + * Checks whether the specified key has an entry using a caller-supplied hash. + * + * **Gotchas** + * + * The supplied hash must be the hash for the same key, such as a precomputed + * `Hash.hash(key)` value. If the hash does not match the key, an existing entry + * may not be found. + * + * **Example** (Checking keys with precomputed hashes) + * + * ```ts + * import { Effect, Hash, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an access control map + * const permissions = yield* TxHashMap.make( + * ["admin", { read: true, write: true, delete: true }], + * ["user", { read: true, write: false, delete: false }] + * ) + * + * // When checking permissions frequently with same roles + * const role = "admin" + * const roleHash = Hash.string(role) + * + * // Use hash-optimized existence check + * const hasAdminRole = yield* TxHashMap.hasHash(permissions, role, roleHash) + * console.log(hasAdminRole) // true + * + * // Check non-existent role + * const hasGuestRole = yield* TxHashMap.hasHash( + * permissions, + * "guest", + * Hash.string("guest") + * ) + * console.log(hasGuestRole) // false + * + * // Useful in hot paths where hash is computed once and reused + * const roles = ["admin", "user", "moderator"] + * const roleHashes = roles.map((role) => [role, Hash.string(role)] as const) + * + * for (const [role, hash] of roleHashes) { + * const exists = yield* TxHashMap.hasHash(permissions, role, hash) + * console.log(`Role ${role}: ${exists}`) + * } + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const hasHash: { + ( + key: K1, + hash: number + ): (self: TxHashMap) => Effect.Effect + (self: TxHashMap, key: K1, hash: number): Effect.Effect +} = dual( + 3, + ( + self: TxHashMap, + key: K1, + hash: number + ): Effect.Effect => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.hasHash(map, key, hash))) +) + +/** + * Transforms all values in the TxHashMap using the provided function, preserving keys. + * + * **Details** + * + * This function returns a new TxHashMap reference with the transformed values. + * The original TxHashMap is not modified. + * + * **Example** (Mapping values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a user profile map + * const profiles = yield* TxHashMap.make( + * ["alice", { name: "Alice", age: 30, active: true }], + * ["bob", { name: "Bob", age: 25, active: false }], + * ["charlie", { name: "Charlie", age: 35, active: true }] + * ) + * + * // Transform to extract just names with greeting + * const greetings = yield* TxHashMap.map( + * profiles, + * (profile, userId) => `Hello, ${profile.name}! (User: ${userId})` + * ) + * + * // Check the transformed values + * const aliceGreeting = yield* TxHashMap.get(greetings, "alice") + * console.log(aliceGreeting) // Option.some("Hello, Alice! (User: alice)") + * + * // Data-last usage with pipe + * const ages = yield* profiles.pipe( + * TxHashMap.map((profile) => profile.age) + * ) + * + * const aliceAge = yield* TxHashMap.get(ages, "alice") + * console.log(aliceAge) // Option.some(30) + * + * // Original map is unchanged + * const originalAlice = yield* TxHashMap.get(profiles, "alice") + * console.log(originalAlice) // Option.some({ name: "Alice", age: 30, active: true }) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const map: { + ( + f: (value: V, key: K) => A + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + f: (value: V, key: K) => A + ): Effect.Effect> +} = dual( + 2, + ( + self: TxHashMap, + f: (value: V, key: K) => A + ): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const mappedMap = HashMap.map(currentMap, f) + return yield* fromHashMap(mappedMap) + }).pipe(Effect.tx) +) + +/** + * Filters the TxHashMap to keep only entries that satisfy the provided predicate. + * + * **Details** + * + * This function returns a new TxHashMap reference containing only the entries + * that match the condition. The original TxHashMap is not modified. + * + * **Example** (Filtering entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a product inventory + * const inventory = yield* TxHashMap.make( + * ["laptop", { price: 999, stock: 5, category: "electronics" }], + * ["mouse", { price: 29, stock: 50, category: "electronics" }], + * ["book", { price: 15, stock: 100, category: "books" }], + * ["phone", { price: 699, stock: 0, category: "electronics" }] + * ) + * + * // Filter to get only electronics in stock + * const electronicsInStock = yield* TxHashMap.filter( + * inventory, + * (product) => product.category === "electronics" && product.stock > 0 + * ) + * + * const size = yield* TxHashMap.size(electronicsInStock) + * console.log(size) // 2 (laptop and mouse) + * + * // Data-last usage with pipe + * const expensiveItems = yield* inventory.pipe( + * TxHashMap.filter((product) => product.price > 500) + * ) + * + * const expensiveSize = yield* TxHashMap.size(expensiveItems) + * console.log(expensiveSize) // 2 (laptop and phone) + * + * // Type guard usage + * const highValueItems = yield* TxHashMap.filter( + * inventory, + * (product): product is typeof product & { price: number } => + * product.price > 50 + * ) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const filter: { + ( + predicate: (value: V, key: K) => value is B + ): (self: TxHashMap) => Effect.Effect> + ( + predicate: (value: V, key: K) => boolean + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + predicate: (value: V, key: K) => value is B + ): Effect.Effect> + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect> +} = dual( + 2, + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const filteredMap = HashMap.filter(currentMap, predicate) + return yield* fromHashMap(filteredMap) + }).pipe(Effect.tx) +) + +/** + * Reduces the TxHashMap entries to a single value by applying a reducer function. + * Iterates over all key-value pairs and accumulates them into a final result. + * + * **Example** (Reducing entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a sales data map + * const sales = yield* TxHashMap.make( + * ["Q1", 15000], + * ["Q2", 18000], + * ["Q3", 22000], + * ["Q4", 25000] + * ) + * + * // Calculate total sales + * const totalSales = yield* TxHashMap.reduce( + * sales, + * 0, + * (total, amount, quarter) => { + * console.log(`Adding ${quarter}: ${amount}`) + * return total + amount + * } + * ) + * console.log(`Total sales: ${totalSales}`) // 80000 + * + * // Data-last usage with pipe + * const quarterlyReport = yield* sales.pipe( + * TxHashMap.reduce( + * { quarters: 0, total: 0, max: 0 }, + * (report, amount, quarter) => ({ + * quarters: report.quarters + 1, + * total: report.total + amount, + * max: Math.max(report.max, amount) + * }) + * ) + * ) + * console.log(quarterlyReport) // { quarters: 4, total: 80000, max: 25000 } + * + * // Build a summary string + * const summary = yield* TxHashMap.reduce( + * sales, + * "", + * (acc, amount, quarter) => acc + `${quarter}: $${amount.toLocaleString()}\n` + * ) + * console.log(summary) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const reduce: { + ( + zero: A, + f: (accumulator: A, value: V, key: K) => A + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + zero: A, + f: (accumulator: A, value: V, key: K) => A + ): Effect.Effect +} = dual( + 3, + ( + self: TxHashMap, + zero: A, + f: (accumulator: A, value: V, key: K) => A + ): Effect.Effect => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.reduce(map, zero, f))) +) + +/** + * Combines filtering and mapping in a single operation. Applies a filter to each + * entry, keeping only successful results and transforming them. + * + * **Details** + * + * This function returns a new TxHashMap reference containing only the transformed + * entries that succeeded. The original TxHashMap is not modified. + * + * **Example** (Filtering and mapping entries) + * + * ```ts + * import { Effect, Option, Result, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a mixed data map + * const userData = yield* TxHashMap.make( + * ["alice", { age: "30", role: "admin", active: true }], + * ["bob", { age: "invalid", role: "user", active: true }], + * ["charlie", { age: "25", role: "admin", active: false }], + * ["diana", { age: "28", role: "user", active: true }] + * ) + * + * // Extract valid ages for active admin users only + * const activeAdminAges = yield* TxHashMap.filterMap( + * userData, + * (user, username) => { + * if (!user.active || user.role !== "admin") return Result.failVoid + * const age = parseInt(user.age) + * if (isNaN(age)) return Result.failVoid + * return Result.succeed({ + * username, + * age, + * seniority: age > 27 ? "senior" : "junior" + * }) + * } + * ) + * + * const aliceData = yield* TxHashMap.get(activeAdminAges, "alice") + * console.log(aliceData) // Option.some({ username: "alice", age: 30, seniority: "senior" }) + * + * const charlieData = yield* TxHashMap.get(activeAdminAges, "charlie") + * console.log(charlieData) // Option.none() (not active) + * + * // Data-last usage with pipe + * const validAges = yield* userData.pipe( + * TxHashMap.filterMap((user) => { + * const age = parseInt(user.age) + * return isNaN(age) ? Result.failVoid : Result.succeed(age) + * }) + * ) + * + * const size = yield* TxHashMap.size(validAges) + * console.log(size) // 3 (alice, charlie, diana have valid ages) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const filterMap: { + ( + f: (input: V, key: K) => Result + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + f: (input: V, key: K) => Result + ): Effect.Effect> +} = dual( + 2, + ( + self: TxHashMap, + f: (input: V, key: K) => Result + ): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const filteredMap = HashMap.filterMap(currentMap, f) + return yield* fromHashMap(filteredMap) + }).pipe(Effect.tx) +) + +/** + * Checks whether any entry in the TxHashMap matches the given predicate. + * + * **Example** (Checking entries with a predicate) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a user status map + * const currentTime = 1_700_000_000_000 + * const userStatuses = yield* TxHashMap.make( + * ["alice", { status: "online", lastSeen: currentTime }], + * ["bob", { status: "offline", lastSeen: currentTime - 3_600_000 }], + * ["charlie", { status: "online", lastSeen: currentTime }] + * ) + * + * // Check if any users are online + * const hasOnlineUsers = yield* TxHashMap.hasBy( + * userStatuses, + * (user) => user.status === "online" + * ) + * console.log(hasOnlineUsers) // true + * + * // Check if any users have specific username pattern + * const hasAdminUser = yield* TxHashMap.hasBy( + * userStatuses, + * (user, username) => username.startsWith("admin") + * ) + * console.log(hasAdminUser) // false + * + * // Data-last usage with pipe + * const hasRecentActivity = yield* userStatuses.pipe( + * TxHashMap.hasBy((user) => currentTime - user.lastSeen < 1_800_000) // 30 minutes + * ) + * console.log(hasRecentActivity) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const hasBy: { + ( + predicate: (value: V, key: K) => boolean + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.hasBy(map, predicate))) +) + +/** + * Finds the first entry in the TxHashMap that matches the given predicate. + * Returns the key-value pair as a tuple wrapped in an Option. + * + * **Example** (Finding the first matching entry) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a task priority map + * const tasks = yield* TxHashMap.make( + * ["task1", { priority: 1, assignee: "alice", completed: false }], + * ["task2", { priority: 3, assignee: "bob", completed: true }], + * ["task3", { priority: 2, assignee: "alice", completed: false }] + * ) + * + * // Find first high-priority incomplete task + * const highPriorityTask = yield* TxHashMap.findFirst( + * tasks, + * (task) => task.priority >= 2 && !task.completed + * ) + * + * if (highPriorityTask._tag === "Some") { + * const [taskId, task] = highPriorityTask.value + * console.log(`Found task: ${taskId}, priority: ${task.priority}`) + * // "Found task: task3, priority: 2" + * } + * + * // Find first task assigned to specific user + * const aliceTask = yield* tasks.pipe( + * TxHashMap.findFirst((task) => task.assignee === "alice") + * ) + * + * if (aliceTask._tag === "Some") { + * console.log(`Alice's task: ${aliceTask.value[0]}`) + * } + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const findFirst: { + ( + predicate: (value: V, key: K) => boolean + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect> +} = dual( + 2, + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect> => + TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.findFirst(map, predicate))) +) + +/** + * Checks whether at least one entry in the TxHashMap satisfies the given predicate. + * + * **Example** (Checking whether some entries match) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a product inventory + * const inventory = yield* TxHashMap.make( + * ["laptop", { price: 999, stock: 5 }], + * ["mouse", { price: 29, stock: 50 }], + * ["keyboard", { price: 79, stock: 0 }] + * ) + * + * // Check if any products are expensive + * const hasExpensiveProducts = yield* TxHashMap.some( + * inventory, + * (product) => product.price > 500 + * ) + * console.log(hasExpensiveProducts) // true + * + * // Check if any products are out of stock + * const hasOutOfStock = yield* TxHashMap.some( + * inventory, + * (product) => product.stock === 0 + * ) + * console.log(hasOutOfStock) // true + * + * // Data-last usage with pipe + * const hasAffordableItems = yield* inventory.pipe( + * TxHashMap.some((product) => product.price < 50) + * ) + * console.log(hasAffordableItems) // true (mouse is $29) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const some: { + ( + predicate: (value: V, key: K) => boolean + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.some(map, predicate))) +) + +/** + * Checks whether all entries in the TxHashMap satisfy the given predicate. + * + * **Example** (Checking whether every entry matches) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a user permissions map + * const permissions = yield* TxHashMap.make( + * ["alice", { canRead: true, canWrite: true, canDelete: false }], + * ["bob", { canRead: true, canWrite: false, canDelete: false }], + * ["charlie", { canRead: true, canWrite: true, canDelete: true }] + * ) + * + * // Check if all users can read + * const allCanRead = yield* TxHashMap.every( + * permissions, + * (perms) => perms.canRead + * ) + * console.log(allCanRead) // true + * + * // Check if all users can write + * const allCanWrite = yield* TxHashMap.every( + * permissions, + * (perms) => perms.canWrite + * ) + * console.log(allCanWrite) // false + * + * // Data-last usage with pipe + * const allHaveBasicAccess = yield* permissions.pipe( + * TxHashMap.every((perms, username) => perms.canRead && username.length > 2) + * ) + * console.log(allHaveBasicAccess) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const every: { + ( + predicate: (value: V, key: K) => boolean + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + predicate: (value: V, key: K) => boolean + ): Effect.Effect => TxRef.get(self.ref).pipe(Effect.map((map) => HashMap.every(map, predicate))) +) + +/** + * Executes a side-effect function for each entry in the TxHashMap. + * The function receives the value and key as parameters and can perform effects. + * + * **Example** (Running effects for each entry) + * + * ```ts + * import { Console, Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a log processing map + * const logs = yield* TxHashMap.make( + * ["error.log", { size: 1024, level: "error" }], + * ["access.log", { size: 2048, level: "info" }], + * ["debug.log", { size: 512, level: "debug" }] + * ) + * + * // Process each log file with side effects + * yield* TxHashMap.forEach(logs, (logInfo, filename) => + * Effect.gen(function*() { + * yield* Console.log( + * `Processing ${filename}: ${logInfo.size} bytes, level: ${logInfo.level}` + * ) + * if (logInfo.level === "error") { + * yield* Console.log(`⚠️ Error log detected: ${filename}`) + * } + * })) + * + * // Data-last usage with pipe + * yield* logs.pipe( + * TxHashMap.forEach((logInfo) => + * logInfo.size > 1000 + * ? Console.log(`Large log file: ${logInfo.size} bytes`) + * : Effect.void + * ) + * ) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const forEach: { + ( + f: (value: V, key: K) => Effect.Effect + ): (self: TxHashMap) => Effect.Effect + ( + self: TxHashMap, + f: (value: V, key: K) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: TxHashMap, + f: (value: V, key: K) => Effect.Effect + ): Effect.Effect => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const entries = HashMap.toEntries(currentMap) + yield* Effect.forEach(entries, ([key, value]) => f(value, key)) + }) +) + +/** + * Maps each entry effectfully to a `TxHashMap` and flattens the produced maps. + * + * **Details** + * + * This function returns a new TxHashMap reference with the flattened results. + * The original TxHashMap is not modified. + * + * **Example** (Flat mapping entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a department-employee map + * const departments = yield* TxHashMap.make( + * ["engineering", ["alice", "bob"]], + * ["marketing", ["charlie", "diana"]] + * ) + * + * // Expand each department into individual employee entries with metadata + * const employeeDetails = yield* TxHashMap.flatMap( + * departments, + * (employees, department) => + * Effect.gen(function*() { + * const employeeMap = yield* TxHashMap.empty< + * string, + * { department: string; role: string } + * >() + * for (let i = 0; i < employees.length; i++) { + * const employee = employees[i] + * const role = i === 0 ? "lead" : "member" + * yield* TxHashMap.set(employeeMap, employee, { department, role }) + * } + * return employeeMap + * }) + * ) + * + * // Check the flattened result + * const alice = yield* TxHashMap.get(employeeDetails, "alice") + * console.log(alice) // Option.some({ department: "engineering", role: "lead" }) + * + * const charlie = yield* TxHashMap.get(employeeDetails, "charlie") + * console.log(charlie) // Option.some({ department: "marketing", role: "lead" }) + * + * const size = yield* TxHashMap.size(employeeDetails) + * console.log(size) // 4 (all employees) + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const flatMap: { + ( + f: (value: V, key: K) => Effect.Effect> + ): (self: TxHashMap) => Effect.Effect> + ( + self: TxHashMap, + f: (value: V, key: K) => Effect.Effect> + ): Effect.Effect> +} = dual( + 2, + ( + self: TxHashMap, + f: (value: V, key: K) => Effect.Effect> + ): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const result = yield* empty() + + const mapEntries = HashMap.toEntries(currentMap) + for (const [key, value] of mapEntries) { + const newMap = yield* f(value, key) + const newEntries = yield* entries(newMap) + yield* setMany(result, newEntries) + } + + return result + }).pipe(Effect.tx) +) + +/** + * Removes all None values from a TxHashMap containing Option values. + * + * **Details** + * + * This function returns a new TxHashMap reference with only the Some values + * unwrapped. The original TxHashMap is not modified. + * + * **Example** (Compacting optional values) + * + * ```ts + * import { Effect, Option, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a map with optional user data + * const userData = yield* TxHashMap.make< + * string, + * Option.Option<{ age: number; email?: string }> + * >( + * ["alice", Option.some({ age: 30, email: "alice@example.com" })], + * ["bob", Option.none()], // incomplete data + * ["charlie", Option.some({ age: 25 })], + * ["diana", Option.none()], // missing data + * ["eve", Option.some({ age: 28, email: "eve@example.com" })] + * ) + * + * // Remove all None values and unwrap Some values + * const validUsers = yield* TxHashMap.compact(userData) + * + * const size = yield* TxHashMap.size(validUsers) + * console.log(size) // 3 (alice, charlie, eve) + * + * const alice = yield* TxHashMap.get(validUsers, "alice") + * console.log(alice) // Option.some({ age: 30, email: "alice@example.com" }) + * + * const bob = yield* TxHashMap.get(validUsers, "bob") + * console.log(bob) // Option.none() (removed from map) + * + * // Useful for cleaning up optional data processing results + * const userAges = yield* TxHashMap.map(validUsers, (user) => user.age) + * const ageEntries = yield* TxHashMap.entries(userAges) + * const sortedAgeEntries = ageEntries.toSorted(([left], [right]) => left.localeCompare(right)) + * console.log(sortedAgeEntries) // [["alice", 30], ["charlie", 25], ["eve", 28]] + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const compact = ( + self: TxHashMap> +): Effect.Effect> => + Effect.gen(function*() { + const currentMap = yield* TxRef.get(self.ref) + const compactedMap = HashMap.compact(currentMap) + return yield* fromHashMap(compactedMap) + }).pipe(Effect.tx) + +/** + * Returns an array of all key-value pairs in the TxHashMap. + * This is an alias for the `entries` function, providing API consistency with HashMap. + * + * **Example** (Converting to entries) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const settings = yield* TxHashMap.make( + * ["theme", "dark"], + * ["language", "en-US"], + * ["timezone", "UTC"] + * ) + * + * // Get all entries as an array + * const allEntries = yield* TxHashMap.toEntries(settings) + * const sortedEntries = allEntries.toSorted(([left], [right]) => left.localeCompare(right)) + * console.log(sortedEntries) + * // [["language", "en-US"], ["theme", "dark"], ["timezone", "UTC"]] + * + * // Process entries + * for (const [setting, value] of sortedEntries) { + * console.log(`${setting}: ${value}`) + * } + * + * // Convert to object for JSON serialization + * const settingsObj = Object.fromEntries(sortedEntries) + * console.log(JSON.stringify(settingsObj)) + * // {"language":"en-US","theme":"dark","timezone":"UTC"} + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const toEntries = ( + self: TxHashMap +): Effect.Effect> => entries(self) + +/** + * Returns an array of all values in the TxHashMap. + * This is an alias for the `values` function, providing API consistency with HashMap. + * + * **Example** (Converting to values) + * + * ```ts + * import { Effect, TxHashMap } from "effect" + * + * const program = Effect.gen(function*() { + * const inventory = yield* TxHashMap.make( + * ["laptop", { price: 999, stock: 5 }], + * ["mouse", { price: 29, stock: 50 }], + * ["keyboard", { price: 79, stock: 20 }] + * ) + * + * // Get all product information + * const products = yield* TxHashMap.toValues(inventory) + * console.log(products.length) // 3 + * + * // Calculate total inventory value + * const totalValue = products.reduce( + * (sum, product) => sum + (product.price * product.stock), + * 0 + * ) + * console.log(`Total inventory value: $${totalValue}`) // Total inventory value: $8025 + * + * // Find products with low stock + * const lowStockProducts = products.filter((product) => product.stock < 10) + * console.log(`${lowStockProducts.length} product with low stock`) // 1 product with low stock + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const toValues = (self: TxHashMap): Effect.Effect> => values(self) + +/** + * Helper function to create a TxHashMap from an existing HashMap + */ +const fromHashMap = (hashMap: HashMap.HashMap): Effect.Effect> => + Effect.gen(function*() { + const ref = yield* TxRef.make(hashMap) + return Object.assign(Object.create(TxHashMapProto), { ref }) + }) diff --git a/repos/effect/packages/effect/src/TxHashSet.ts b/repos/effect/packages/effect/src/TxHashSet.ts new file mode 100644 index 0000000..3e6e0e0 --- /dev/null +++ b/repos/effect/packages/effect/src/TxHashSet.ts @@ -0,0 +1,908 @@ +/** + * Transactional hash sets for storing unique values inside Effect + * transactions. + * + * A `TxHashSet` keeps an immutable `HashSet` inside a `TxRef`, so membership + * checks and updates can commit atomically with other transactional operations. + * Use it when several pieces of shared transactional state must change + * together, such as adding a value only after checking related state. The + * module includes the usual set operations, including adding, removing, + * membership checks, set algebra, mapping, filtering, reducing, and conversion + * back to `HashSet`. + * + * @since 2.0.0 + */ + +import * as Effect from "./Effect.ts" +import { format } from "./Formatter.ts" +import { dual } from "./Function.ts" +import * as HashSet from "./HashSet.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty, type Predicate, type Refinement } from "./Predicate.ts" +import * as TxRef from "./TxRef.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = "~effect/transactions/TxHashSet" + +const TxHashSetProto = { + [TypeId]: TypeId, + [NodeInspectSymbol](this: TxHashSet) { + return toJson(this) + }, + toString(this: TxHashSet) { + return `TxHashSet(${format(toJson((this).ref))})` + }, + toJSON(this: TxHashSet) { + return { + _id: "TxHashSet", + ref: toJson((this).ref) + } + }, + pipe(this: TxHashSet) { + return pipeArguments(this, arguments) + } +} + +/** + * A TxHashSet is a transactional hash set data structure that provides atomic operations on unique values within Effect transactions. It uses an immutable HashSet internally with TxRef for transactional semantics, ensuring all operations are performed atomically. + * + * **Details** + * + * Mutation operations such as `add`, `remove`, and `clear` update the original TxHashSet and return `Effect` or `Effect`. Transform operations such as `union`, `intersection`, `difference`, `map`, and `filter` create new TxHashSet instances and leave the original TxHashSet unchanged. + * + * **Example** (Using transactional hash sets) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional hash set + * const txSet = yield* TxHashSet.make("apple", "banana", "cherry") + * + * // Single operations are automatically transactional + * yield* TxHashSet.add(txSet, "grape") + * const hasApple = yield* TxHashSet.has(txSet, "apple") + * console.log(hasApple) // true + * + * // Multi-step atomic operations + * yield* Effect.tx( + * Effect.gen(function*() { + * const hasCherry = yield* TxHashSet.has(txSet, "cherry") + * if (hasCherry) { + * yield* TxHashSet.remove(txSet, "cherry") + * yield* TxHashSet.add(txSet, "orange") + * } + * }) + * ) + * + * const size = yield* TxHashSet.size(txSet) + * console.log(size) // 4 + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxHashSet extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly ref: TxRef.TxRef> +} + +/** + * The TxHashSet namespace contains type-level utilities and helper types + * for working with TxHashSet instances. + * + * **Example** (Extracting value types inside transactions) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional color set + * const colors = yield* TxHashSet.make("red", "green", "blue") + * + * // Extract the value type for reuse + * type Color = TxHashSet.TxHashSet.Value // string + * + * // Use extracted type in functions + * const addColor = (color: Color) => TxHashSet.add(colors, color) + * + * yield* addColor("yellow") + * }) + * ``` + * + * @since 4.0.0 + */ +export declare namespace TxHashSet { + /** + * Extracts the value type from a `TxHashSet` type. + * + * **Example** (Extracting a TxHashSet value type) + * + * ```ts + * import type { TxHashSet } from "effect" + * + * type FruitSet = TxHashSet.TxHashSet<"apple" | "banana" | "cherry"> + * + * // Extract the value type + * type Fruit = TxHashSet.TxHashSet.Value // "apple" | "banana" | "cherry" + * + * const processFruit = (fruit: Fruit) => { + * return `Processing ${fruit}` + * } + * + * console.log(processFruit("apple")) // Processing apple + * ``` + * + * @category utility types + * @since 4.0.0 + */ + export type Value = T extends TxHashSet ? V : never +} + +const makeTxHashSet = (ref: TxRef.TxRef>): TxHashSet => { + const self = Object.create(TxHashSetProto) + self.ref = ref + return self +} + +/** + * Creates an empty TxHashSet. + * + * **Example** (Creating an empty transactional hash set) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.empty() + * + * console.log(yield* TxHashSet.size(txSet)) // 0 + * console.log(yield* TxHashSet.isEmpty(txSet)) // true + * + * // Add some values + * yield* TxHashSet.add(txSet, "hello") + * yield* TxHashSet.add(txSet, "world") + * console.log(yield* TxHashSet.size(txSet)) // 2 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (): Effect.Effect> => + Effect.gen(function*() { + const ref = yield* TxRef.make(HashSet.empty()) + return makeTxHashSet(ref) + }) + +/** + * Creates a TxHashSet from a variable number of values. + * + * **Example** (Creating transactional hash sets from values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const fruits = yield* TxHashSet.make("apple", "banana", "cherry") + * console.log(yield* TxHashSet.size(fruits)) // 3 + * + * const numbers = yield* TxHashSet.make(1, 2, 3, 2, 1) // Duplicates ignored + * console.log(yield* TxHashSet.size(numbers)) // 3 + * + * const mixed = yield* TxHashSet.make("hello", 42, true) + * console.log(yield* TxHashSet.size(mixed)) // 3 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = >( + ...values: Values +): Effect.Effect> => + Effect.gen(function*() { + const hashSet = HashSet.make(...values) + const ref = yield* TxRef.make(hashSet) + return makeTxHashSet(ref) + }) + +/** + * Creates a TxHashSet from an iterable collection of values. + * + * **Example** (Creating a transactional hash set from an iterable) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const fromArray = yield* TxHashSet.fromIterable(["a", "b", "c", "b", "a"]) + * console.log(yield* TxHashSet.size(fromArray)) // 3 + * + * const fromSet = yield* TxHashSet.fromIterable(new Set([1, 2, 3])) + * console.log(yield* TxHashSet.size(fromSet)) // 3 + * + * const fromString = yield* TxHashSet.fromIterable("hello") + * const values = yield* TxHashSet.toHashSet(fromString) + * console.log(Array.from(values).sort()) // ["e", "h", "l", "o"] + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable = (values: Iterable): Effect.Effect> => + Effect.gen(function*() { + const hashSet = HashSet.fromIterable(values) + const ref = yield* TxRef.make(hashSet) + return makeTxHashSet(ref) + }) + +/** + * Creates a TxHashSet from an existing HashSet. + * + * **Example** (Creating a transactional hash set from a HashSet) + * + * ```ts + * import { Effect, HashSet, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const hashSet = HashSet.make("x", "y", "z") + * const txSet = yield* TxHashSet.fromHashSet(hashSet) + * + * console.log(yield* TxHashSet.size(txSet)) // 3 + * console.log(yield* TxHashSet.has(txSet, "y")) // true + * + * // Original hashSet is unchanged when txSet is modified + * yield* TxHashSet.add(txSet, "w") + * console.log(HashSet.size(hashSet)) // 3 (original unchanged) + * console.log(yield* TxHashSet.size(txSet)) // 4 + * }) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromHashSet = (hashSet: HashSet.HashSet): Effect.Effect> => + Effect.gen(function*() { + const ref = yield* TxRef.make(hashSet) + return makeTxHashSet(ref) + }) + +/** + * Checks whether a value is a TxHashSet. + * + * **Example** (Checking for a TxHashSet) + * + * ```ts + * import { Effect, HashSet, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make(1, 2, 3) + * const hashSet = HashSet.make(1, 2, 3) + * const array = [1, 2, 3] + * + * console.log(TxHashSet.isTxHashSet(txSet)) // true + * console.log(TxHashSet.isTxHashSet(hashSet)) // false + * console.log(TxHashSet.isTxHashSet(array)) // false + * console.log(TxHashSet.isTxHashSet(null)) // false + * }) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxHashSet = (u: unknown): u is TxHashSet => hasProperty(u, TypeId) + +/** + * Adds a value to the TxHashSet. If the value already exists, the operation has no effect. + * + * **Details** + * + * This function mutates the original TxHashSet by adding the specified value. It does not return a new TxHashSet reference. + * + * **Example** (Adding values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make("a", "b") + * + * yield* TxHashSet.add(txSet, "c") + * console.log(yield* TxHashSet.size(txSet)) // 3 + * console.log(yield* TxHashSet.has(txSet, "c")) // true + * + * // Adding existing value has no effect + * yield* TxHashSet.add(txSet, "a") + * console.log(yield* TxHashSet.size(txSet)) // 3 (unchanged) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const add: { + (value: V): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, value: V): Effect.Effect +} = dual< + (value: V) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, value: V) => Effect.Effect +>(2, (self: TxHashSet, value: V) => TxRef.update(self.ref, (set) => HashSet.add(set, value))) + +/** + * Removes a value from the TxHashSet. + * + * **Details** + * + * This function mutates the original TxHashSet by removing the specified value. It does not return a new TxHashSet reference. + * + * **Example** (Removing values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make("a", "b", "c") + * + * const removed = yield* TxHashSet.remove(txSet, "b") + * console.log(removed) // true (value existed and was removed) + * console.log(yield* TxHashSet.size(txSet)) // 2 + * console.log(yield* TxHashSet.has(txSet, "b")) // false + * + * // Removing non-existent value returns false + * const notRemoved = yield* TxHashSet.remove(txSet, "d") + * console.log(notRemoved) // false + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const remove: { + (value: V): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, value: V): Effect.Effect +} = dual< + (value: V) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, value: V) => Effect.Effect +>(2, (self: TxHashSet, value: V) => + Effect.gen(function*() { + const currentSet = yield* TxRef.get(self.ref) + const existed = HashSet.has(currentSet, value) + if (existed) { + yield* TxRef.set(self.ref, HashSet.remove(currentSet, value)) + } + return existed + }).pipe(Effect.tx)) + +/** + * Checks whether the TxHashSet contains the specified value. + * + * **Example** (Checking membership) + * + * ```ts + * import { Effect, Equal, Hash, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make("apple", "banana", "cherry") + * + * console.log(yield* TxHashSet.has(txSet, "apple")) // true + * console.log(yield* TxHashSet.has(txSet, "grape")) // false + * + * // Works with any type that implements Equal + * class Person implements Equal.Equal { + * constructor(readonly name: string) {} + * + * [Equal.symbol](other: unknown) { + * return other instanceof Person && this.name === other.name + * } + * + * [Hash.symbol](): number { + * return Hash.string(this.name) + * } + * } + * + * const people = yield* TxHashSet.make(new Person("Alice"), new Person("Bob")) + * console.log(yield* TxHashSet.has(people, new Person("Alice"))) // true + * }) + * ``` + * + * @category elements + * @since 2.0.0 + */ +export const has: { + (value: V): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, value: V): Effect.Effect +} = dual< + (value: V) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, value: V) => Effect.Effect +>(2, (self: TxHashSet, value: V) => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.has(set, value) + })) + +/** + * Returns the number of values in the TxHashSet. + * + * **Example** (Getting the set size) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const empty = yield* TxHashSet.empty() + * console.log(yield* TxHashSet.size(empty)) // 0 + * + * const small = yield* TxHashSet.make("a", "b") + * console.log(yield* TxHashSet.size(small)) // 2 + * + * const fromIterable = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"]) + * console.log(yield* TxHashSet.size(fromIterable)) // 3 (duplicates ignored) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: TxHashSet): Effect.Effect => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.size(set) + }) + +/** + * Checks whether the TxHashSet is empty. + * + * **Example** (Checking whether a set is empty) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const empty = yield* TxHashSet.empty() + * console.log(yield* TxHashSet.isEmpty(empty)) // true + * + * const nonEmpty = yield* TxHashSet.make("a") + * console.log(yield* TxHashSet.isEmpty(nonEmpty)) // false + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isEmpty = (self: TxHashSet): Effect.Effect => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.isEmpty(set) + }) + +/** + * Removes all values from the TxHashSet. + * + * **Details** + * + * This function mutates the original TxHashSet by clearing all values. It does not return a new TxHashSet reference. + * + * **Example** (Clearing all values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make("a", "b", "c") + * console.log(yield* TxHashSet.size(txSet)) // 3 + * + * yield* TxHashSet.clear(txSet) + * console.log(yield* TxHashSet.size(txSet)) // 0 + * console.log(yield* TxHashSet.isEmpty(txSet)) // true + * }) + * ``` + * + * @category mutations + * @since 4.0.0 + */ +export const clear = (self: TxHashSet): Effect.Effect => TxRef.set(self.ref, HashSet.empty()) + +/** + * Creates the union of two TxHashSets, returning a new TxHashSet. + * + * **Example** (Combining sets with union) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set1 = yield* TxHashSet.make("a", "b") + * const set2 = yield* TxHashSet.make("b", "c") + * const combined = yield* TxHashSet.union(set1, set2) + * + * const values = yield* TxHashSet.toHashSet(combined) + * console.log(Array.from(values).sort()) // ["a", "b", "c"] + * console.log(yield* TxHashSet.size(combined)) // 3 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const union: { + (that: TxHashSet): (self: TxHashSet) => Effect.Effect> + (self: TxHashSet, that: TxHashSet): Effect.Effect> +} = dual< + ( + that: TxHashSet + ) => (self: TxHashSet) => Effect.Effect>, + ( + self: TxHashSet, + that: TxHashSet + ) => Effect.Effect> +>(2, (self: TxHashSet, that: TxHashSet) => + Effect.gen(function*() { + const set1 = yield* TxRef.get(self.ref) + const set2 = yield* TxRef.get(that.ref) + const combined = HashSet.union(set1, set2) + return yield* fromHashSet(combined) + }).pipe(Effect.tx)) + +/** + * Creates the intersection of two TxHashSets, returning a new TxHashSet. + * + * **Example** (Finding common values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set1 = yield* TxHashSet.make("a", "b", "c") + * const set2 = yield* TxHashSet.make("b", "c", "d") + * const common = yield* TxHashSet.intersection(set1, set2) + * + * const values = yield* TxHashSet.toHashSet(common) + * console.log(Array.from(values).sort()) // ["b", "c"] + * console.log(yield* TxHashSet.size(common)) // 2 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const intersection: { + (that: TxHashSet): (self: TxHashSet) => Effect.Effect> + (self: TxHashSet, that: TxHashSet): Effect.Effect> +} = dual< + ( + that: TxHashSet + ) => (self: TxHashSet) => Effect.Effect>, + ( + self: TxHashSet, + that: TxHashSet + ) => Effect.Effect> +>(2, (self: TxHashSet, that: TxHashSet) => + Effect.gen(function*() { + const set1 = yield* TxRef.get(self.ref) + const set2 = yield* TxRef.get(that.ref) + const common = HashSet.intersection(set1, set2) + return yield* fromHashSet(common) + }).pipe(Effect.tx)) + +/** + * Creates the difference of two TxHashSets (elements in the first set that are not in the second), returning a new TxHashSet. + * + * **Example** (Finding values absent from another set) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const set1 = yield* TxHashSet.make("a", "b", "c") + * const set2 = yield* TxHashSet.make("b", "d") + * const diff = yield* TxHashSet.difference(set1, set2) + * + * const values = yield* TxHashSet.toHashSet(diff) + * console.log(Array.from(values).sort()) // ["a", "c"] + * console.log(yield* TxHashSet.size(diff)) // 2 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const difference: { + (that: TxHashSet): (self: TxHashSet) => Effect.Effect> + (self: TxHashSet, that: TxHashSet): Effect.Effect> +} = dual< + ( + that: TxHashSet + ) => (self: TxHashSet) => Effect.Effect>, + ( + self: TxHashSet, + that: TxHashSet + ) => Effect.Effect> +>(2, (self: TxHashSet, that: TxHashSet) => + Effect.gen(function*() { + const set1 = yield* TxRef.get(self.ref) + const set2 = yield* TxRef.get(that.ref) + const diff = HashSet.difference(set1, set2) + return yield* fromHashSet(diff) + }).pipe(Effect.tx)) + +/** + * Checks whether a TxHashSet is a subset of another TxHashSet. + * + * **Example** (Checking subset relationships) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const small = yield* TxHashSet.make("a", "b") + * const large = yield* TxHashSet.make("a", "b", "c", "d") + * const other = yield* TxHashSet.make("x", "y") + * + * console.log(yield* TxHashSet.isSubset(small, large)) // true + * console.log(yield* TxHashSet.isSubset(large, small)) // false + * console.log(yield* TxHashSet.isSubset(small, other)) // false + * console.log(yield* TxHashSet.isSubset(small, small)) // true + * }) + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const isSubset: { + (that: TxHashSet): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, that: TxHashSet): Effect.Effect +} = dual< + (that: TxHashSet) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, that: TxHashSet) => Effect.Effect +>(2, (self: TxHashSet, that: TxHashSet) => + Effect.gen(function*() { + const set1 = yield* TxRef.get(self.ref) + const set2 = yield* TxRef.get(that.ref) + return HashSet.isSubset(set1, set2) + }).pipe(Effect.tx)) + +/** + * Checks whether at least one value in the TxHashSet satisfies the predicate. + * + * **Example** (Testing whether some values match) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5) + * + * console.log(yield* TxHashSet.some(numbers, (n) => n > 3)) // true + * console.log(yield* TxHashSet.some(numbers, (n) => n > 10)) // false + * + * const empty = yield* TxHashSet.empty() + * console.log(yield* TxHashSet.some(empty, (n) => n > 0)) // false + * }) + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const some: { + (predicate: Predicate): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, predicate: Predicate): Effect.Effect +} = dual< + (predicate: Predicate) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, predicate: Predicate) => Effect.Effect +>(2, (self: TxHashSet, predicate: Predicate) => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.some(set, predicate) + })) + +/** + * Checks whether all values in the TxHashSet satisfy the predicate. + * + * **Example** (Testing whether every value matches) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const numbers = yield* TxHashSet.make(2, 4, 6, 8) + * + * console.log(yield* TxHashSet.every(numbers, (n) => n % 2 === 0)) // true + * console.log(yield* TxHashSet.every(numbers, (n) => n > 5)) // false + * + * const empty = yield* TxHashSet.empty() + * console.log(yield* TxHashSet.every(empty, (n) => n > 0)) // true (vacuously true) + * }) + * ``` + * + * @category elements + * @since 4.0.0 + */ +export const every: { + (predicate: Predicate): (self: TxHashSet) => Effect.Effect + (self: TxHashSet, predicate: Predicate): Effect.Effect +} = dual< + (predicate: Predicate) => (self: TxHashSet) => Effect.Effect, + (self: TxHashSet, predicate: Predicate) => Effect.Effect +>(2, (self: TxHashSet, predicate: Predicate) => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.every(set, predicate) + })) + +/** + * Maps each value in the TxHashSet using the provided function, returning a new TxHashSet. + * + * **Example** (Mapping values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const numbers = yield* TxHashSet.make(1, 2, 3) + * const doubled = yield* TxHashSet.map(numbers, (n) => n * 2) + * + * const values = yield* TxHashSet.toHashSet(doubled) + * console.log(Array.from(values).sort()) // [2, 4, 6] + * console.log(yield* TxHashSet.size(doubled)) // 3 + * + * // Mapping can reduce size if function produces duplicates + * const strings = yield* TxHashSet.make("apple", "banana", "cherry") + * const lengths = yield* TxHashSet.map(strings, (s) => s.length) + * const lengthValues = yield* TxHashSet.toHashSet(lengths) + * console.log(Array.from(lengthValues).sort()) // [5, 6] (apple=5, banana=6, cherry=6) + * }) + * ``` + * + * @category mapping + * @since 4.0.0 + */ +export const map: { + (f: (value: V) => U): (self: TxHashSet) => Effect.Effect> + (self: TxHashSet, f: (value: V) => U): Effect.Effect> +} = dual< + (f: (value: V) => U) => (self: TxHashSet) => Effect.Effect>, + (self: TxHashSet, f: (value: V) => U) => Effect.Effect> +>(2, (self: TxHashSet, f: (value: V) => U) => + Effect.gen(function*() { + const currentSet = yield* TxRef.get(self.ref) + const mappedSet = HashSet.map(currentSet, f) + return yield* fromHashSet(mappedSet) + }).pipe(Effect.tx)) + +/** + * Filters the TxHashSet keeping only values that satisfy the predicate, returning a new TxHashSet. + * + * **Example** (Filtering values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5, 6) + * const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0) + * + * const values = yield* TxHashSet.toHashSet(evens) + * console.log(Array.from(values).sort()) // [2, 4, 6] + * console.log(yield* TxHashSet.size(evens)) // 3 + * }) + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const filter: { + ( + refinement: Refinement, U> + ): (self: TxHashSet) => Effect.Effect> + ( + predicate: Predicate> + ): (self: TxHashSet) => Effect.Effect> + ( + self: TxHashSet, + refinement: Refinement + ): Effect.Effect> + (self: TxHashSet, predicate: Predicate): Effect.Effect> +} = dual< + { + ( + refinement: Refinement, U> + ): (self: TxHashSet) => Effect.Effect> + ( + predicate: Predicate> + ): (self: TxHashSet) => Effect.Effect> + }, + { + ( + self: TxHashSet, + refinement: Refinement + ): Effect.Effect> + (self: TxHashSet, predicate: Predicate): Effect.Effect> + } +>(2, (self: TxHashSet, predicate: Predicate) => + Effect.gen(function*() { + const currentSet = yield* TxRef.get(self.ref) + const filteredSet = HashSet.filter(currentSet, predicate) + return yield* fromHashSet(filteredSet) + }).pipe(Effect.tx)) + +/** + * Reduces the TxHashSet to a single value by iterating through the values and applying an accumulator function. + * + * **Example** (Reducing values) + * + * ```ts + * import { Effect, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5) + * const sum = yield* TxHashSet.reduce(numbers, 0, (acc, n) => acc + n) + * + * console.log(sum) // 15 + * + * const strings = yield* TxHashSet.make("a", "b", "c") + * const concatenated = yield* TxHashSet.reduce(strings, "", (acc, s) => acc + s) + * console.log(concatenated) // Order may vary: "abc", "bac", etc. + * }) + * ``` + * + * @category folding + * @since 2.0.0 + */ +export const reduce: { + ( + zero: U, + f: (accumulator: U, value: V) => U + ): (self: TxHashSet) => Effect.Effect + ( + self: TxHashSet, + zero: U, + f: (accumulator: U, value: V) => U + ): Effect.Effect +} = dual< + ( + zero: U, + f: (accumulator: U, value: V) => U + ) => (self: TxHashSet) => Effect.Effect, + ( + self: TxHashSet, + zero: U, + f: (accumulator: U, value: V) => U + ) => Effect.Effect +>(3, (self: TxHashSet, zero: U, f: (accumulator: U, value: V) => U) => + Effect.gen(function*() { + const set = yield* TxRef.get(self.ref) + return HashSet.reduce(set, zero, f) + })) + +/** + * Converts the TxHashSet to an immutable HashSet snapshot. + * + * **Example** (Taking a HashSet snapshot) + * + * ```ts + * import { Effect, HashSet, TxHashSet } from "effect" + * + * const program = Effect.gen(function*() { + * const txSet = yield* TxHashSet.make("x", "y", "z") + * const hashSet = yield* TxHashSet.toHashSet(txSet) + * + * console.log(HashSet.size(hashSet)) // 3 + * console.log(HashSet.has(hashSet, "y")) // true + * + * // hashSet is a snapshot - modifications to txSet don't affect it + * yield* TxHashSet.add(txSet, "w") + * console.log(HashSet.size(hashSet)) // 3 (unchanged) + * console.log(yield* TxHashSet.size(txSet)) // 4 + * }) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const toHashSet = (self: TxHashSet): Effect.Effect> => TxRef.get(self.ref) diff --git a/repos/effect/packages/effect/src/TxPriorityQueue.ts b/repos/effect/packages/effect/src/TxPriorityQueue.ts new file mode 100644 index 0000000..6f5b410 --- /dev/null +++ b/repos/effect/packages/effect/src/TxPriorityQueue.ts @@ -0,0 +1,567 @@ +/** + * Transactional priority queues whose state is stored in a `TxRef`. Elements + * are kept in the order defined by the `Order` supplied at construction time, + * and dequeue operations return the first element according to that ordering. + * + * Use `TxPriorityQueue` when multiple fibers coordinate through a shared queue + * and queue operations need to compose with other transactional state changes. + * The retrying `peek` and `take` operations wait transactionally when the queue + * is empty, so they can be combined with other transactional reads and writes in + * one atomic workflow. + * + * @since 4.0.0 + */ + +import type { Chunk } from "./Chunk.ts" +import * as C from "./Chunk.ts" +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Option } from "./Option.ts" +import * as O from "./Option.ts" +import type { Order } from "./Order.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty, type Predicate } from "./Predicate.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxPriorityQueue" + +/** + * A transactional priority queue backed by a sorted `Chunk`. + * + * **Details** + * + * Elements are stored in ascending order according to the `Order` provided at + * construction time. `take` returns the smallest element, `peek` observes it + * without removing. + * + * **Example** (Dequeuing values by priority) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * yield* TxPriorityQueue.offer(pq, 3) + * yield* TxPriorityQueue.offer(pq, 1) + * yield* TxPriorityQueue.offer(pq, 2) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxPriorityQueue extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly ref: TxRef.TxRef> + readonly ord: Order +} + +const TxPriorityQueueProto: Omit, typeof TypeId | "ref" | "ord"> = { + [NodeInspectSymbol](this: TxPriorityQueue) { + return toJson(this) + }, + toJSON(this: TxPriorityQueue) { + return { + _id: "TxPriorityQueue" + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeTxPriorityQueue = (ref: TxRef.TxRef>, ord: Order): TxPriorityQueue => { + const self = Object.create(TxPriorityQueueProto) + self[TypeId] = TypeId + self.ref = ref + self.ord = ord + return self +} + +const insertSorted = (chunk: Chunk, value: A, ord: Order): Chunk => { + const arr = C.toArray(chunk) as Array + let lo = 0 + let hi = arr.length + while (lo < hi) { + const mid = (lo + hi) >>> 1 + if (ord(arr[mid], value) <= 0) { + lo = mid + 1 + } else { + hi = mid + } + } + const out = Array(arr.length + 1) as Array + for (let i = 0; i < lo; i++) out[i] = arr[i] + out[lo] = value + for (let i = lo; i < arr.length; i++) out[i + 1] = arr[i] + return C.fromIterable(out) +} + +/** + * Creates an empty `TxPriorityQueue` with the given ordering. + * + * **Example** (Creating an empty priority queue) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * const empty = yield* TxPriorityQueue.isEmpty(pq) + * console.log(empty) // true + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const empty = (order: Order): Effect.Effect> => + Effect.map(TxRef.make>(C.empty()), (ref) => makeTxPriorityQueue(ref, order)) + +/** + * Creates a `TxPriorityQueue` from an iterable of elements. + * + * **Example** (Creating a priority queue from an iterable) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const fromIterable: { + (order: Order): (iterable: Iterable) => Effect.Effect> + (order: Order, iterable: Iterable): Effect.Effect> +} = dual( + 2, + (order: Order, iterable: Iterable): Effect.Effect> => { + const arr = Array.from(iterable).sort((a, b) => order(a, b)) + return Effect.map( + TxRef.make>(C.fromIterable(arr)), + (ref) => makeTxPriorityQueue(ref, order) + ) + } +) + +/** + * Creates a `TxPriorityQueue` from variadic elements. + * + * **Example** (Creating a priority queue from variadic values) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.make(Order.Number)(3, 1, 2) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (order: Order) => (...elements: Array): Effect.Effect> => + fromIterable(order, elements) + +/** + * Returns the number of elements in the queue. + * + * **Example** (Getting the queue size) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3]) + * const s = yield* TxPriorityQueue.size(pq) + * console.log(s) // 3 + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: TxPriorityQueue): Effect.Effect => Effect.map(TxRef.get(self.ref), C.size) + +/** + * Returns `true` if the queue is empty. + * + * **Example** (Checking whether a queue is empty) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * const empty = yield* TxPriorityQueue.isEmpty(pq) + * console.log(empty) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isEmpty = (self: TxPriorityQueue): Effect.Effect => Effect.map(size(self), (n) => n === 0) + +/** + * Returns `true` if the queue has at least one element. + * + * **Example** (Checking whether a queue has elements) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1]) + * const nonEmpty = yield* TxPriorityQueue.isNonEmpty(pq) + * console.log(nonEmpty) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isNonEmpty = (self: TxPriorityQueue): Effect.Effect => Effect.map(size(self), (n) => n > 0) + +/** + * Observes the smallest element without removing it. + * + * **When to use** + * + * Use to inspect the next prioritized value and retry transactionally while + * the queue is empty. + * + * **Example** (Peeking at the next value) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) + * const top = yield* TxPriorityQueue.peek(pq) + * console.log(top) // 1 + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const peek = (self: TxPriorityQueue): Effect.Effect => + Effect.gen(function*() { + const chunk = yield* TxRef.get(self.ref) + const head = C.head(chunk) + if (O.isNone(head)) { + return yield* Effect.txRetry + } + return head.value + }).pipe(Effect.tx) + +/** + * Observes the smallest element without removing it, returning `None` when the + * queue is empty. + * + * **When to use** + * + * Use to inspect the next prioritized value without retrying on an empty queue. + * + * **Example** (Peeking without retrying) + * + * ```ts + * import { Effect, Option, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * const result = yield* TxPriorityQueue.peekOption(pq) + * console.log(Option.isNone(result)) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const peekOption = (self: TxPriorityQueue): Effect.Effect> => + Effect.map(TxRef.get(self.ref), C.head) + +/** + * Inserts an element into the queue in sorted position. + * + * **Example** (Offering a value) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * yield* TxPriorityQueue.offer(pq, 2) + * yield* TxPriorityQueue.offer(pq, 1) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const offer: { + (value: A): (self: TxPriorityQueue) => Effect.Effect + (self: TxPriorityQueue, value: A): Effect.Effect +} = dual( + 2, + (self: TxPriorityQueue, value: A): Effect.Effect => + TxRef.update(self.ref, (chunk) => insertSorted(chunk, value, self.ord)) +) + +/** + * Inserts all elements from an iterable into the queue. + * + * **Example** (Offering multiple values) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * yield* TxPriorityQueue.offerAll(pq, [3, 1, 2]) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const offerAll: { + (values: Iterable): (self: TxPriorityQueue) => Effect.Effect + (self: TxPriorityQueue, values: Iterable): Effect.Effect +} = dual( + 2, + (self: TxPriorityQueue, values: Iterable): Effect.Effect => + TxRef.update(self.ref, (chunk) => { + const arr = [...C.toArray(chunk), ...values].sort((a, b) => self.ord(a, b)) + return C.fromIterable(arr) + }) +) + +/** + * Takes the smallest element from the queue. Retries if the queue is empty. + * + * **Example** (Taking the next value) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) + * const first = yield* TxPriorityQueue.take(pq) + * console.log(first) // 1 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const take = (self: TxPriorityQueue): Effect.Effect => + Effect.gen(function*() { + const chunk = yield* TxRef.get(self.ref) + const head = C.head(chunk) + if (O.isNone(head)) { + return yield* Effect.txRetry + } + yield* TxRef.set(self.ref, C.drop(chunk, 1)) + return head.value + }).pipe(Effect.tx) + +/** + * Takes all elements from the queue, returning them in priority order. + * + * **Example** (Taking all values in priority order) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) + * const all = yield* TxPriorityQueue.takeAll(pq) + * console.log(all) // [1, 2, 3] + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const takeAll = (self: TxPriorityQueue): Effect.Effect> => + Effect.map( + TxRef.modify(self.ref, (chunk) => [chunk, C.empty()]), + C.toArray + ) + +/** + * Tries to take the smallest element. Returns `None` if the queue is empty. + * + * **Example** (Taking without retrying) + * + * ```ts + * import { Effect, Option, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * const result = yield* TxPriorityQueue.takeOption(pq) + * console.log(Option.isNone(result)) // true + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const takeOption = (self: TxPriorityQueue): Effect.Effect> => + TxRef.modify(self.ref, (chunk) => { + const head = C.head(chunk) + if (O.isNone(head)) { + return [O.none(), chunk] + } + return [O.some(head.value), C.drop(chunk, 1)] + }) + +/** + * Takes up to `n` elements from the queue in priority order. + * + * **Example** (Taking up to a limit) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [5, 3, 1, 4, 2]) + * const top2 = yield* TxPriorityQueue.takeUpTo(pq, 2) + * console.log(top2) // [1, 2] + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const takeUpTo: { + (n: number): (self: TxPriorityQueue) => Effect.Effect> + (self: TxPriorityQueue, n: number): Effect.Effect> +} = dual( + 2, + (self: TxPriorityQueue, n: number): Effect.Effect> => + Effect.map( + TxRef.modify(self.ref, (chunk) => { + const taken = C.take(chunk, n) + const rest = C.drop(chunk, n) + return [taken, rest] + }), + C.toArray + ) +) + +/** + * Removes elements matching the predicate. + * + * **Example** (Removing matching values) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3, 4, 5]) + * yield* TxPriorityQueue.removeIf(pq, (n) => n % 2 === 0) + * const all = yield* TxPriorityQueue.takeAll(pq) + * console.log(all) // [1, 3, 5] + * }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const removeIf: { + (predicate: Predicate): (self: TxPriorityQueue) => Effect.Effect + (self: TxPriorityQueue, predicate: Predicate): Effect.Effect +} = dual( + 2, + (self: TxPriorityQueue, predicate: Predicate): Effect.Effect => + TxRef.update(self.ref, (chunk) => C.filter(chunk, (a) => !predicate(a))) +) + +/** + * Keeps only elements matching the predicate. + * + * **Example** (Retaining matching values) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3, 4, 5]) + * yield* TxPriorityQueue.retainIf(pq, (n) => n % 2 === 0) + * const all = yield* TxPriorityQueue.takeAll(pq) + * console.log(all) // [2, 4] + * }) + * ``` + * + * @category filtering + * @since 2.0.0 + */ +export const retainIf: { + (predicate: Predicate): (self: TxPriorityQueue) => Effect.Effect + (self: TxPriorityQueue, predicate: Predicate): Effect.Effect +} = dual( + 2, + (self: TxPriorityQueue, predicate: Predicate): Effect.Effect => + TxRef.update(self.ref, (chunk) => C.filter(chunk, predicate)) +) + +/** + * Returns all elements in priority order without removing them. + * + * **Example** (Reading values in priority order) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) + * const all = yield* TxPriorityQueue.toArray(pq) + * console.log(all) // [1, 2, 3] + * }) + * ``` + * + * @category converting + * @since 2.0.0 + */ +export const toArray = (self: TxPriorityQueue): Effect.Effect> => + Effect.map(TxRef.get(self.ref), C.toArray) + +/** + * Determines if the provided value is a `TxPriorityQueue`. + * + * **Example** (Checking for a TxPriorityQueue) + * + * ```ts + * import { Effect, Order, TxPriorityQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const pq = yield* TxPriorityQueue.empty(Order.Number) + * console.log(TxPriorityQueue.isTxPriorityQueue(pq)) // true + * console.log(TxPriorityQueue.isTxPriorityQueue("nope")) // false + * }) + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxPriorityQueue = (u: unknown): u is TxPriorityQueue => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/TxPubSub.ts b/repos/effect/packages/effect/src/TxPubSub.ts new file mode 100644 index 0000000..e1ecbc9 --- /dev/null +++ b/repos/effect/packages/effect/src/TxPubSub.ts @@ -0,0 +1,684 @@ +/** + * Broadcasts values to subscribers inside Effect transactions. + * + * A `TxPubSub` is a transactional publish/subscribe hub. Each subscriber + * owns a `TxQueue`, and each published value is offered to the subscriber + * queues that are registered at the time of publication. This module includes + * bounded, dropping, sliding, and unbounded hubs, publishing helpers, scoped + * subscriptions, shutdown operations, and a guard. + * + * @since 4.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Scope from "./Scope.ts" +import * as TxQueue from "./TxQueue.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxPubSub" + +/** + * A TxPubSub represents a transactional publish/subscribe hub that broadcasts messages + * to all current subscribers using Software Transactional Memory (STM) semantics. + * + * **Example** (Subscribing to a transactional pub/sub) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, "hello") + * const msg = yield* TxQueue.take(sub) + * console.log(msg) // "hello" + * }) + * ) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxPubSub extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + /** @internal */ + readonly subscribersRef: TxRef.TxRef>> + /** @internal */ + readonly shutdownRef: TxRef.TxRef + readonly strategy: "bounded" | "unbounded" | "dropping" | "sliding" + readonly capacity: number +} + +const TxPubSubProto: Omit, typeof TypeId | "subscribersRef" | "shutdownRef" | "strategy" | "capacity"> = { + [NodeInspectSymbol](this: TxPubSub) { + return toJson(this) + }, + toJSON(this: TxPubSub) { + return { + _id: "TxPubSub", + strategy: this.strategy, + capacity: this.capacity + } + }, + toString(this: TxPubSub) { + return `TxPubSub(${this.strategy}, ${this.capacity})` + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeTxPubSub = ( + subscribersRef: TxRef.TxRef>>, + shutdownRef: TxRef.TxRef, + strategy: "bounded" | "unbounded" | "dropping" | "sliding", + cap: number +): TxPubSub => { + const self = Object.create(TxPubSubProto) + self[TypeId] = TypeId + self.subscribersRef = subscribersRef + self.shutdownRef = shutdownRef + self.strategy = strategy + self.capacity = cap + return self +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Creates a bounded TxPubSub with the specified capacity. When a subscriber's + * queue is full, the publisher will retry the transaction until space is available. + * + * **Example** (Creating a bounded pub/sub) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.bounded(16) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, 42) + * const value = yield* TxQueue.take(sub) + * console.log(value) // 42 + * }) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const bounded = (capacity: number): Effect.Effect> => + Effect.gen(function*() { + const subscribersRef = yield* TxRef.make>>([]) + const shutdownRef = yield* TxRef.make(false) + return makeTxPubSub(subscribersRef, shutdownRef, "bounded", capacity) + }).pipe(Effect.tx) + +/** + * Creates a dropping TxPubSub with the specified capacity. When a subscriber's + * queue is full, the message is dropped for that subscriber. + * + * **Example** (Creating a dropping pub/sub) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.dropping(2) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, 1) + * yield* TxPubSub.publish(hub, 2) + * yield* TxPubSub.publish(hub, 3) // dropped + * const v1 = yield* TxQueue.take(sub) + * const v2 = yield* TxQueue.take(sub) + * console.log(v1, v2) // 1 2 + * }) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const dropping = (capacity: number): Effect.Effect> => + Effect.gen(function*() { + const subscribersRef = yield* TxRef.make>>([]) + const shutdownRef = yield* TxRef.make(false) + return makeTxPubSub(subscribersRef, shutdownRef, "dropping", capacity) + }).pipe(Effect.tx) + +/** + * Creates a sliding TxPubSub with the specified capacity. When a subscriber's + * queue is full, the oldest message in that subscriber's queue is dropped. + * + * **Example** (Creating a sliding pub/sub) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.sliding(2) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, 1) + * yield* TxPubSub.publish(hub, 2) + * yield* TxPubSub.publish(hub, 3) // evicts 1 + * const v1 = yield* TxQueue.take(sub) + * console.log(v1) // 2 + * }) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sliding = (capacity: number): Effect.Effect> => + Effect.gen(function*() { + const subscribersRef = yield* TxRef.make>>([]) + const shutdownRef = yield* TxRef.make(false) + return makeTxPubSub(subscribersRef, shutdownRef, "sliding", capacity) + }).pipe(Effect.tx) + +/** + * Creates an unbounded TxPubSub with unlimited capacity. Messages are always accepted. + * + * **Example** (Creating an unbounded pub/sub) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, "msg") + * const msg = yield* TxQueue.take(sub) + * console.log(msg) // "msg" + * }) + * ) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unbounded = (): Effect.Effect> => + Effect.gen(function*() { + const subscribersRef = yield* TxRef.make>>([]) + const shutdownRef = yield* TxRef.make(false) + return makeTxPubSub(subscribersRef, shutdownRef, "unbounded", Number.POSITIVE_INFINITY) + }).pipe(Effect.tx) + +// ============================================================================= +// Getters +// ============================================================================= + +/** + * Returns the capacity of the TxPubSub. + * + * **Example** (Reading pub/sub capacity) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.bounded(16) + * console.log(TxPubSub.capacity(hub)) // 16 + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const capacity = (self: TxPubSub): number => self.capacity + +/** + * Returns the current number of messages across all subscriber queues (the max). + * + * **Example** (Reading subscriber queue size) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, 1) + * yield* TxPubSub.publish(hub, 2) + * const s = yield* TxPubSub.size(hub) + * console.log(s) // 2 + * }) + * ) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const size = (self: TxPubSub): Effect.Effect => + Effect.gen(function*() { + const subscribers = yield* TxRef.get(self.subscribersRef) + let maxSize = 0 + for (const queue of subscribers) { + const s = yield* TxQueue.size(queue) + if (s > maxSize) maxSize = s + } + return maxSize + }).pipe(Effect.tx) + +/** + * Checks whether the TxPubSub has no pending messages (all subscriber queues are empty). + * + * **Example** (Checking whether a pub/sub is empty) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * const empty = yield* TxPubSub.isEmpty(hub) + * console.log(empty) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isEmpty = (self: TxPubSub): Effect.Effect => Effect.map(size(self), (s) => s === 0) + +/** + * Checks whether any subscriber queue is at capacity. + * + * **Example** (Checking whether a pub/sub is full) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.bounded(2) + * const full = yield* TxPubSub.isFull(hub) + * console.log(full) // false + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isFull = (self: TxPubSub): Effect.Effect => + Effect.gen(function*() { + if (self.capacity === Number.POSITIVE_INFINITY) return false + const subscribers = yield* TxRef.get(self.subscribersRef) + for (const queue of subscribers) { + if (yield* TxQueue.isFull(queue)) return true + } + return false + }).pipe(Effect.tx) + +/** + * Checks whether the TxPubSub has been shut down. + * + * **Example** (Checking whether a pub/sub is shut down) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * console.log(yield* TxPubSub.isShutdown(hub)) // false + * yield* TxPubSub.shutdown(hub) + * console.log(yield* TxPubSub.isShutdown(hub)) // true + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const isShutdown = (self: TxPubSub): Effect.Effect => TxRef.get(self.shutdownRef) + +// ============================================================================= +// Mutations +// ============================================================================= + +/** + * Publishes a message to all current subscribers. + * + * **Details** + * + * Returns `true` if the message was delivered to all subscribers, or `false` if the hub is shut down or the message was dropped for any subscriber. For the bounded strategy, the transaction retries if any subscriber queue is full. For the sliding strategy, full subscriber queues drop their oldest messages. For the dropping strategy, full subscriber queues drop the new message and the operation returns `false`. + * + * **Example** (Publishing a message to subscribers) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * // No subscribers - publish is a no-op + * const r1 = yield* TxPubSub.publish(hub, "no one listening") + * console.log(r1) // true + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publish(hub, "hello") + * const msg = yield* TxQueue.take(sub) + * console.log(msg) // "hello" + * }) + * ) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const publish: { + (value: A): (self: TxPubSub) => Effect.Effect + (self: TxPubSub, value: A): Effect.Effect +} = dual( + 2, + (self: TxPubSub, value: A): Effect.Effect => + Effect.gen(function*() { + if (yield* TxRef.get(self.shutdownRef)) return false + + const subscribers = yield* TxRef.get(self.subscribersRef) + let allAccepted = true + + for (const queue of subscribers) { + const accepted = yield* TxQueue.offer(queue, value) + if (!accepted) allAccepted = false + } + + return allAccepted + }).pipe(Effect.tx) +) + +/** + * Publishes all messages from an iterable to all current subscribers. + * + * **Details** + * + * Returns `true` if all messages were delivered to all subscribers. + * + * **Example** (Publishing multiple messages to subscribers) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxPubSub.subscribe(hub) + * yield* TxPubSub.publishAll(hub, [1, 2, 3]) + * const v1 = yield* TxQueue.take(sub) + * const v2 = yield* TxQueue.take(sub) + * const v3 = yield* TxQueue.take(sub) + * console.log(v1, v2, v3) // 1 2 3 + * }) + * ) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const publishAll: { + (values: Iterable): (self: TxPubSub) => Effect.Effect + (self: TxPubSub, values: Iterable): Effect.Effect +} = dual( + 2, + (self: TxPubSub, values: Iterable): Effect.Effect => + Effect.gen(function*() { + if (yield* TxRef.get(self.shutdownRef)) return false + + let allAccepted = true + for (const value of values) { + const accepted = yield* publish(self, value) + if (!accepted) allAccepted = false + } + return allAccepted + }).pipe(Effect.tx) +) + +/** + * Subscribes to the TxPubSub, returning a scoped `TxQueue` for messages published after subscription. + * + * **Details** + * + * The returned queue uses the hub's capacity strategy: bounded subscriptions backpressure publishers when full, dropping subscriptions may miss new messages when full, and sliding subscriptions may evict older queued messages. The subscription is automatically removed when the scope is closed. + * + * **Example** (Subscribing multiple queues) + * + * ```ts + * import { Effect, TxPubSub, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub1 = yield* TxPubSub.subscribe(hub) + * const sub2 = yield* TxPubSub.subscribe(hub) + * + * yield* TxPubSub.publish(hub, "broadcast") + * + * const msg1 = yield* TxQueue.take(sub1) + * const msg2 = yield* TxQueue.take(sub2) + * console.log(msg1, msg2) // "broadcast" "broadcast" + * }) + * ) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const subscribe = (self: TxPubSub): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.tx(acquireSubscriber(self)), + (queue) => Effect.tx(releaseSubscriber(self, queue)) + ) + +/** + * Creates a subscriber queue and registers it with the pub/sub. + * + * **When to use** + * + * Use to create and register a subscriber queue inside a larger transaction + * when registration must be atomic with other Tx operations. + * + * **Details** + * + * This is the transactional acquire step of `subscribe`, exposed so that callers can compose it with other Tx operations in a single transaction, such as `TxSubscriptionRef.changes`. + * + * @see {@link subscribe} for the scoped acquire and release wrapper when no custom transaction composition is needed + * @see {@link releaseSubscriber} to remove and shut down a queue returned by `acquireSubscriber` + * + * @category mutations + * @since 4.0.0 + */ +export const acquireSubscriber = ( + self: TxPubSub +): Effect.Effect, never, Effect.Transaction> => + Effect.gen(function*() { + const queue = yield* makeSubscriberQueue(self.strategy, self.capacity) + yield* TxRef.update(self.subscribersRef, (subs) => [...subs, queue]) + return queue + }) + +/** + * Removes a subscriber queue from the pub/sub and shuts it down. + * + * **When to use** + * + * Use to release a manually acquired subscriber queue inside a larger + * transaction, removing it from the pub/sub and shutting it down together with + * related transactional cleanup. + * + * **Details** + * + * This is the transactional release step of `subscribe`, exposed so that callers can compose it with other Tx operations in a single transaction. + * + * **Gotchas** + * + * The supplied queue is shut down after being removed, so callers should pass a + * queue acquired for this pub/sub. + * + * @see {@link acquireSubscriber} for the matching transactional acquire step + * @see {@link subscribe} for the scoped acquire and release wrapper + * + * @category mutations + * @since 4.0.0 + */ +export const releaseSubscriber: { + (queue: TxQueue.TxQueue): (self: TxPubSub) => Effect.Effect + (self: TxPubSub, queue: TxQueue.TxQueue): Effect.Effect +} = dual( + 2, + ( + self: TxPubSub, + queue: TxQueue.TxQueue + ): Effect.Effect => + Effect.gen(function*() { + yield* TxRef.update(self.subscribersRef, (subs) => subs.filter((q) => q !== queue)) + yield* TxQueue.shutdown(queue) + }) +) + +const makeSubscriberQueue = ( + strategy: "bounded" | "unbounded" | "dropping" | "sliding", + cap: number +): Effect.Effect> => { + switch (strategy) { + case "bounded": + return TxQueue.bounded(cap) + case "dropping": + return TxQueue.dropping(cap) + case "sliding": + return TxQueue.sliding(cap) + case "unbounded": + return TxQueue.unbounded() + } +} + +/** + * Shuts down the TxPubSub and all subscriber queues registered at the time of shutdown. + * + * **Details** + * + * After shutdown, `publish` and `publishAll` return `false`, and `awaitShutdown` completes. The operation is idempotent. + * + * **Gotchas** + * + * Subscribers acquired after shutdown are not automatically shut down by this call. + * + * **Example** (Shutting down a pub/sub) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * yield* TxPubSub.shutdown(hub) + * + * const shut = yield* TxPubSub.isShutdown(hub) + * console.log(shut) // true + * + * const accepted = yield* TxPubSub.publish(hub, 1) + * console.log(accepted) // false + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const shutdown = (self: TxPubSub): Effect.Effect => + Effect.gen(function*() { + const alreadyShutdown = yield* TxRef.get(self.shutdownRef) + if (alreadyShutdown) return + + yield* TxRef.set(self.shutdownRef, true) + const subscribers = yield* TxRef.get(self.subscribersRef) + for (const queue of subscribers) { + yield* TxQueue.shutdown(queue) + } + }).pipe(Effect.tx) + +/** + * Waits for the TxPubSub to be shut down. + * + * **Example** (Waiting for shutdown) + * + * ```ts + * import { Effect, TxPubSub } from "effect" + * + * const program = Effect.gen(function*() { + * const hub = yield* TxPubSub.unbounded() + * + * const fiber = yield* Effect.forkChild(TxPubSub.awaitShutdown(hub)) + * yield* TxPubSub.shutdown(hub) + * yield* fiber.await + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const awaitShutdown = (self: TxPubSub): Effect.Effect => + Effect.gen(function*() { + const shut = yield* TxRef.get(self.shutdownRef) + if (shut) return + return yield* Effect.txRetry + }).pipe(Effect.tx) + +// ============================================================================= +// Guards +// ============================================================================= + +/** + * Checks whether the given value is a TxPubSub. + * + * **Example** (Checking for a TxPubSub) + * + * ```ts + * import { TxPubSub } from "effect" + * + * declare const someValue: unknown + * + * if (TxPubSub.isTxPubSub(someValue)) { + * console.log("This is a TxPubSub") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxPubSub = (u: unknown): u is TxPubSub => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/TxQueue.ts b/repos/effect/packages/effect/src/TxQueue.ts new file mode 100644 index 0000000..5e9f7cd --- /dev/null +++ b/repos/effect/packages/effect/src/TxQueue.ts @@ -0,0 +1,1488 @@ +/** + * Transactional queues whose state changes participate in Effect transactions. + * + * A `TxQueue` stores values of type `A`, exposes write-only `TxEnqueue` + * and read-only `TxDequeue` handles, and can complete, fail, or shut down with + * causes observed by consumers. Queue operations can retry transactionally when + * they cannot proceed, such as taking from an empty open queue or offering to a + * full bounded queue. This makes the queue useful for coordinating producers + * and consumers alongside other transactional state changes. + * + * @since 4.0.0 + */ +import type * as Arr from "./Array.ts" +import * as Cause from "./Cause.ts" +import * as Chunk from "./Chunk.ts" +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import { hasProperty } from "./Predicate.ts" +import { type ExcludeDone, isDoneCause } from "./Pull.ts" +import * as TxChunk from "./TxChunk.ts" +import * as TxRef from "./TxRef.ts" +import type * as Types from "./Types.ts" + +/** + * Represents the state of a transactional queue with sophisticated lifecycle management. + * + * **Details** + * + * The queue progresses through three states: + * - **Open**: Accepting offers and serving takes normally + * - **Closing**: No new offers accepted, serving remaining items until empty + * - **Done**: Terminal state with completion cause, no further operations possible + * + * **Example** (Inspecting queue lifecycle states) + * + * ```ts + * import type { TxQueue } from "effect" + * + * // State progression example + * declare const state: TxQueue.State + * + * if (state._tag === "Open") { + * console.log("Queue is accepting new items") + * } else if (state._tag === "Closing") { + * console.log("Queue is draining, cause:", state.cause) + * } else { + * console.log("Queue is done, cause:", state.cause) + * } + * ``` + * + * @category models + * @since 4.0.0 + */ +export type State<_A, E> = + | { + readonly _tag: "Open" + } + | { + readonly _tag: "Closing" + readonly cause: Cause.Cause + } + | { + readonly _tag: "Done" + readonly cause: Cause.Cause + } + +const EnqueueTypeId = "~effect/transactions/TxQueue/Enqueue" +const DequeueTypeId = "~effect/transactions/TxQueue/Dequeue" +const TypeId = "~effect/transactions/TxQueue" + +/** + * Namespace containing type definitions for TxEnqueue variance annotations. + * + * @since 4.0.0 + */ +export declare namespace TxEnqueue { + /** + * Variance annotation interface for TxEnqueue contravariance. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + readonly _A: Types.Contravariant + readonly _E: Types.Contravariant + } +} + +/** + * Namespace containing type definitions for TxDequeue variance annotations. + * + * @since 4.0.0 + */ +export declare namespace TxDequeue { + /** + * Variance annotation interface for TxDequeue covariance. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * Namespace containing type definitions for TxQueue variance annotations. + * + * @since 4.0.0 + */ +export declare namespace TxQueue { + /** + * Variance annotation interface for TxQueue invariance. + * + * @category models + * @since 4.0.0 + */ + export interface Variance { + readonly _A: Types.Invariant + readonly _E: Types.Invariant + } +} + +/** + * Represents the shared state of a transactional queue that can be inspected. + * This interface contains the core properties needed for queue state inspection + * operations like size, capacity, and completion status. + * + * @category models + * @since 4.0.0 + */ +export interface TxQueueState extends Inspectable { + readonly strategy: "bounded" | "unbounded" | "dropping" | "sliding" + readonly capacity: number + readonly items: TxChunk.TxChunk + readonly stateRef: TxRef.TxRef> +} + +/** + * A TxEnqueue represents the write-only interface of a transactional queue, providing + * operations for adding elements (enqueue operations) and inspecting queue state. + * + * **Example** (Offering values through enqueue handles) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * import type { Cause } from "effect" + * + * const program = Effect.gen(function*() { + * // Queue without error channel + * const queue = yield* TxQueue.bounded(10) + * const accepted = yield* TxQueue.offer(queue, 42) + * + * // Queue with error channel for completion signaling + * const faultTolerantQueue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(faultTolerantQueue, [1, 2, 3]) + * yield* TxQueue.fail(faultTolerantQueue, "processing complete") + * + * // Works with Done for clean completion + * const completableQueue = yield* TxQueue.bounded< + * string, + * Cause.Done + * >(5) + * yield* TxQueue.offer(completableQueue, "task") + * yield* TxQueue.end(completableQueue) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxEnqueue extends TxQueueState { + readonly [EnqueueTypeId]: TxEnqueue.Variance +} + +/** + * A TxDequeue represents the read-only interface of a transactional queue, providing + * operations for consuming elements (dequeue operations) and inspecting queue state. + * + * **Example** (Taking values through dequeue handles) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Queue without error channel + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offer(queue, 42) + * const item = yield* TxQueue.take(queue) + * console.log(item) // 42 + * + * // Queue with error channel - errors propagate through E-channel + * const faultTolerantQueue = yield* TxQueue.bounded(10) + * yield* TxQueue.fail(faultTolerantQueue, "processing failed") + * + * // All dequeue operations now fail with the error directly + * const takeResult = yield* Effect.flip(TxQueue.take(faultTolerantQueue)) // "processing failed" + * const peekResult = yield* Effect.flip(TxQueue.peek(faultTolerantQueue)) // "processing failed" + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxDequeue extends TxQueueState { + readonly [DequeueTypeId]: TxDequeue.Variance +} + +/** + * A TxQueue represents a transactional queue data structure that provides both + * enqueue and dequeue operations with Software Transactional Memory (STM) semantics. + * + * **Example** (Combining enqueue and dequeue operations) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a bounded transactional queue (E defaults to never) + * const queue = yield* TxQueue.bounded(10) + * + * // Single operations - automatically transactional + * const accepted = yield* TxQueue.offer(queue, 42) + * const item = yield* TxQueue.take(queue) // Effect + * console.log(item) // 42 + * + * // Queue with error channel + * const faultTolerantQueue = yield* TxQueue.bounded(10) + * + * // Operations can handle queue-level failures + * yield* TxQueue.fail(faultTolerantQueue, "queue failed") + * const result = yield* Effect.flip(TxQueue.take(faultTolerantQueue)) + * console.log(result) // "queue failed" + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxQueue extends TxEnqueue, TxDequeue { + readonly [TypeId]: TxQueue.Variance +} + +/** + * Checks whether the given value is a TxEnqueue. + * + * **Example** (Checking enqueue handles) + * + * ```ts + * import { TxQueue } from "effect" + * + * declare const someValue: unknown + * + * if (TxQueue.isTxEnqueue(someValue)) { + * // someValue is now typed as TxEnqueue + * console.log("This is a TxEnqueue") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxEnqueue = (u: unknown): u is TxEnqueue => hasProperty(u, EnqueueTypeId) + +/** + * Checks whether the given value is a TxDequeue. + * + * **Example** (Checking dequeue handles) + * + * ```ts + * import { TxQueue } from "effect" + * + * declare const someValue: unknown + * + * if (TxQueue.isTxDequeue(someValue)) { + * // someValue is now typed as TxDequeue + * console.log("This is a TxDequeue") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxDequeue = (u: unknown): u is TxDequeue => hasProperty(u, DequeueTypeId) + +/** + * Checks whether the given value is a TxQueue. + * + * **Example** (Checking queue handles) + * + * ```ts + * import { TxQueue } from "effect" + * + * declare const someValue: unknown + * + * if (TxQueue.isTxQueue(someValue)) { + * // someValue is now typed as TxQueue + * console.log("This is a TxQueue") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxQueue = (u: unknown): u is TxQueue => hasProperty(u, TypeId) + +// ============================================================================= +// Proto +// ============================================================================= + +const TxQueueProto = { + [EnqueueTypeId]: { _A: (_: never) => _, _E: (_: never) => _ }, + [DequeueTypeId]: { _A: (_: never) => _, _E: (_: never) => _ }, + [TypeId]: { _A: (_: never) => _, _E: (_: never) => _ }, + [NodeInspectSymbol](this: TxQueue) { + return toJson(this) + }, + toString(this: TxQueue) { + return `TxQueue(${this.strategy}, ${this.capacity})` + }, + toJSON(this: TxQueue) { + return { + _id: "TxQueue", + strategy: this.strategy, + capacity: this.capacity + } + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Creates a new bounded `TxQueue` with the specified capacity. + * + * **Details** + * + * This function returns a new TxQueue reference with the specified capacity. No existing TxQueue instances are modified. + * + * **Example** (Creating bounded queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a bounded queue (E defaults to never) + * const queue = yield* TxQueue.bounded(10) + * + * // Create a bounded queue with error channel + * const faultTolerantQueue = yield* TxQueue.bounded(10) + * + * // Offer items - will succeed until capacity is reached + * yield* TxQueue.offer(queue, 1) + * yield* TxQueue.offer(queue, 2) + * + * const item = yield* TxQueue.take(queue) + * console.log(item) // 1 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const bounded = ( + capacity: number +): Effect.Effect> => + Effect.gen(function*() { + const items = yield* TxChunk.empty() + const stateRef = yield* TxRef.make>({ _tag: "Open" }) + + const txQueue = Object.create(TxQueueProto) + txQueue.strategy = "bounded" + txQueue.capacity = capacity + txQueue.items = items + txQueue.stateRef = stateRef + return txQueue + }).pipe(Effect.tx) + +/** + * Creates a new unbounded `TxQueue` with unlimited capacity. + * + * **Details** + * + * This function returns a new TxQueue reference with unlimited capacity. No existing TxQueue instances are modified. + * + * **Example** (Creating unbounded queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create an unbounded queue (E defaults to never) + * const queue = yield* TxQueue.unbounded() + * + * // Create an unbounded queue with error channel + * const faultTolerantQueue = yield* TxQueue.unbounded() + * + * // Can offer unlimited items + * yield* TxQueue.offer(queue, "hello") + * yield* TxQueue.offer(queue, "world") + * + * const size = yield* TxQueue.size(queue) + * console.log(size) // 2 + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const unbounded = (): Effect.Effect> => + Effect.gen(function*() { + const items = yield* TxChunk.empty() + const stateRef = yield* TxRef.make>({ _tag: "Open" }) + + const txQueue = Object.create(TxQueueProto) + txQueue.strategy = "unbounded" + txQueue.capacity = Number.POSITIVE_INFINITY + txQueue.items = items + txQueue.stateRef = stateRef + return txQueue + }).pipe(Effect.tx) + +/** + * Creates a new dropping `TxQueue` with the specified capacity that drops new items when full. + * + * **Details** + * + * This function returns a new TxQueue reference with dropping strategy. No existing TxQueue instances are modified. + * + * **Example** (Creating dropping queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a dropping queue with capacity 2 + * const queue = yield* TxQueue.dropping(2) + * + * // Fill to capacity + * yield* TxQueue.offer(queue, 1) + * yield* TxQueue.offer(queue, 2) + * + * // This will be dropped (returns false) + * const accepted = yield* TxQueue.offer(queue, 3) + * console.log(accepted) // false + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const dropping = ( + capacity: number +): Effect.Effect> => + Effect.gen(function*() { + const items = yield* TxChunk.empty() + const stateRef = yield* TxRef.make>({ _tag: "Open" }) + + const txQueue = Object.create(TxQueueProto) + txQueue.strategy = "dropping" + txQueue.capacity = capacity + txQueue.items = items + txQueue.stateRef = stateRef + return txQueue + }).pipe(Effect.tx) + +/** + * Creates a new sliding `TxQueue` with the specified capacity that evicts old items when full. + * + * **Details** + * + * This function returns a new TxQueue reference with sliding strategy. No existing TxQueue instances are modified. + * + * **Example** (Creating sliding queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a sliding queue with capacity 2 + * const queue = yield* TxQueue.sliding(2) + * + * // Fill to capacity + * yield* TxQueue.offer(queue, 1) + * yield* TxQueue.offer(queue, 2) + * + * // This will evict item 1 and add 3 + * yield* TxQueue.offer(queue, 3) + * + * const item = yield* TxQueue.take(queue) + * console.log(item) // 2 (item 1 was evicted) + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const sliding = ( + capacity: number +): Effect.Effect> => + Effect.gen(function*() { + const items = yield* TxChunk.empty() + const stateRef = yield* TxRef.make>({ _tag: "Open" }) + + const txQueue = Object.create(TxQueueProto) + txQueue.strategy = "sliding" + txQueue.capacity = capacity + txQueue.items = items + txQueue.stateRef = stateRef + return txQueue + }).pipe(Effect.tx) + +// ============================================================================= +// Core Queue Operations +// ============================================================================= + +/** + * Offers an item to the queue and returns whether it was accepted. + * + * **Details** + * + * Open unbounded queues always accept; open bounded queues retry while full; dropping queues return `false` when full; sliding queues evict the oldest item when full. Closing or done queues return `false`. This function mutates the original TxQueue by adding the item according to the queue's strategy. It does not return a new TxQueue reference. + * + * **Example** (Offering a value) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Offer an item - returns true if accepted + * const accepted = yield* TxQueue.offer(queue, 42) + * console.log(accepted) // true + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const offer: { + (value: A): (self: TxEnqueue) => Effect.Effect + (self: TxEnqueue, value: A): Effect.Effect +} = dual( + 2, + (self: TxEnqueue, value: A): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + if (state._tag === "Done" || state._tag === "Closing") { + return false + } + + const currentSize = yield* size(self) + + // Unbounded - always accept + if (self.strategy === "unbounded") { + yield* TxChunk.append(self.items, value) + return true + } + + // For bounded queues, check capacity + if (currentSize < self.capacity) { + yield* TxChunk.append(self.items, value) + return true + } + + // Queue is at capacity, strategy-specific behavior + if (self.strategy === "dropping") { + return false // Drop the new item + } + + if (self.strategy === "sliding") { + yield* TxChunk.drop(self.items, 1) // Remove oldest item + yield* TxChunk.append(self.items, value) // Add new item + return true + } + + // bounded strategy - block until space is available + return yield* Effect.txRetry + }).pipe(Effect.tx) +) + +/** + * Offers multiple items to the queue, returning the items that were not + * accepted. + * + * **Details** + * + * Each item follows `offer` semantics: bounded queues retry while full, dropping queues reject new items when full, sliding queues evict old items to accept new items, and closing or done queues reject all items. This function mutates the original TxQueue by adding items according to the queue's strategy. It does not return a new TxQueue reference. + * + * **Example** (Offering multiple values) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Offer multiple items - returns rejected items as array + * const rejected = yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) + * console.log(rejected) // [] if all accepted + * console.log(rejected.length) // 0 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const offerAll: { + (values: Iterable): (self: TxEnqueue) => Effect.Effect> + (self: TxEnqueue, values: Iterable): Effect.Effect> +} = dual( + 2, + (self: TxEnqueue, values: Iterable): Effect.Effect> => + Effect.gen(function*() { + const rejected: Array = [] + + for (const value of values) { + const accepted = yield* offer(self, value) + if (!accepted) { + rejected.push(value) + } + } + + return rejected + }).pipe(Effect.tx) +) + +/** + * Takes the next item from the queue, retrying the transaction while the queue + * is empty. + * + * **Details** + * + * If the queue is done, the effect fails with the queue's completion cause. This function mutates the original TxQueue by removing the first item. It does not return a new TxQueue reference. + * + * **Example** (Taking a value) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offer(queue, 42) + * + * // Take an item - blocks if empty + * const item = yield* TxQueue.take(queue) + * console.log(item) // 42 + * + * // When queue fails, take fails with the same error + * yield* TxQueue.fail(queue, "queue error") + * const result = yield* Effect.flip(TxQueue.take(queue)) + * console.log(result) // "queue error" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const take = (self: TxDequeue): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + // Check if queue is done - forward the cause directly + if (state._tag === "Done") { + return yield* Effect.failCause(state.cause) + } + + // If no items available, retry transaction + if (yield* isEmpty(self)) { + return yield* Effect.txRetry + } + + // Take item from queue + const chunk = yield* TxChunk.get(self.items) + const head = Chunk.head(chunk) + if (Option.isNone(head)) { + return yield* Effect.txRetry + } + + yield* TxChunk.drop(self.items, 1) + + // Check if we need to transition Closing → Done + if (state._tag === "Closing" && (yield* isEmpty(self))) { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + } + + return head.value + }).pipe(Effect.tx) + +/** + * Tries to take an item from the queue without blocking. + * + * **Example** (Polling without blocking) + * + * ```ts + * import { Effect, Option, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Poll returns Option.none if empty + * const maybe = yield* TxQueue.poll(queue) + * console.log(Option.isNone(maybe)) // true + * + * yield* TxQueue.offer(queue, 42) + * const item = yield* TxQueue.poll(queue) + * console.log(Option.getOrNull(item)) // 42 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const poll = (self: TxDequeue): Effect.Effect> => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + if (state._tag === "Done") { + return Option.none() + } + + const chunk = yield* TxChunk.get(self.items) + const head = Chunk.head(chunk) + if (Option.isNone(head)) { + return Option.none() + } + + yield* TxChunk.drop(self.items, 1) + return Option.some(head.value) + }).pipe(Effect.tx) + +/** + * Takes all items from the queue. Blocks if the queue is empty. + * + * **Details** + * + * If the queue is already in a failed state, the error is propagated through the E-channel. This follows the same patterns as `take` and waits when there are no elements. It returns a non-empty array because it blocks until at least one item is available. This function mutates the original TxQueue by removing all items. It does not return a new TxQueue reference. + * + * **Example** (Taking all queued values) + * + * ```ts + * import { Array, Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * // Take all items atomically - returns NonEmptyArray + * const items = yield* TxQueue.takeAll(queue) + * console.log(items) // [1, 2, 3, 4, 5] + * console.log(Array.isArrayNonEmpty(items)) // true + * }) + * + * // Error propagation example + * const errorExample = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(5) + * yield* TxQueue.offerAll(queue, [1, 2]) + * yield* TxQueue.fail(queue, "processing error") + * + * // takeAll() propagates the queue error through E-channel + * const result = yield* Effect.flip(TxQueue.takeAll(queue)) + * console.log(result) // "processing error" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const takeAll = (self: TxDequeue): Effect.Effect, E> => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + // Handle done queue + if (state._tag === "Done") { + return yield* Effect.failCause(state.cause) + } + + // Wait if empty - same pattern as take() + if (yield* isEmpty(self)) { + return yield* Effect.txRetry + } + + const chunk = yield* TxChunk.get(self.items) + + // Take all items (guaranteed non-empty due to isEmpty check above) + const items = Chunk.toArray(chunk) as Arr.NonEmptyArray + yield* TxChunk.set(self.items, Chunk.empty()) + + // Check if we need to transition Closing → Done + if (state._tag === "Closing") { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + } + + return items + }).pipe(Effect.tx) + +/** + * Takes up to `n` items from the queue in a single transaction. + * + * **Details** + * + * For an open queue, waits until `min(n, capacity)` items are available, then removes that many items. If `n` is less than or equal to zero, returns an empty array without modifying the queue. If the queue is closing, drains the currently available items and transitions to `Done`. If the queue is already done, the effect fails with the queue's completion cause. This function mutates the original TxQueue by removing the taken items. It does not return a new TxQueue reference. + * + * **Example** (Taking a fixed number of values) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(5) + * yield* TxQueue.offerAll(queue, [1, 2, 3, 4]) + * + * const items = yield* TxQueue.takeN(queue, 4) + * console.log(items) // [1, 2, 3, 4] + * + * // This requests more than capacity (5), so takes all available (up to 5) + * yield* TxQueue.offerAll(queue, [5, 6, 7, 8, 9]) + * const all = yield* TxQueue.takeN(queue, 10) + * console.log(all) // [5, 6, 7, 8, 9] + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const takeN: { + (n: number): (self: TxDequeue) => Effect.Effect, E> + (self: TxDequeue, n: number): Effect.Effect, E> +} = dual( + 2, + (self: TxDequeue, n: number): Effect.Effect, E> => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + // Check if queue is done - forward the cause directly + if (state._tag === "Done") { + return yield* Effect.failCause(state.cause) + } + + const currentSize = yield* size(self) + + // Determine how many items we can/should take + const requestedCount = n + const maxPossible = Math.min(requestedCount, self.capacity) + + // If we can't get the requested amount due to capacity constraints, + // take what the capacity allows. Otherwise, wait for the full amount. + const shouldWaitForFull = requestedCount <= self.capacity + const minimumRequired = shouldWaitForFull ? requestedCount : maxPossible + + // If we don't have enough items available + if (currentSize < minimumRequired) { + // If queue is closing, transition to done and return what we have + if (state._tag === "Closing") { + if (yield* isEmpty(self)) { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + return [] + } + // Take all remaining items when closing + const chunk = yield* TxChunk.get(self.items) + const taken = Chunk.toArray(chunk) + yield* TxChunk.set(self.items, Chunk.empty()) + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + return taken + } + + // Queue is still open but not enough items - retry transaction + return yield* Effect.txRetry + } + + // Take the determined number of items + const toTake = minimumRequired + const chunk = yield* TxChunk.get(self.items) + const taken = Chunk.take(chunk, toTake) + yield* TxChunk.drop(self.items, toTake) + + // Check if we need to transition Closing → Done + if (state._tag === "Closing" && (yield* isEmpty(self))) { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + } + + return Chunk.toArray(taken) + }).pipe(Effect.tx) +) + +/** + * Takes between `min` and `max` currently available items, waiting for `min` on + * an open queue. + * + * **Details** + * + * If the queue is closing, drains the currently available items even when fewer than `min` are available and transitions to `Done`. Invalid ranges (`min <= 0`, `max <= 0`, or `min > max`) return an empty array. If the queue is already done, the effect fails with the queue's completion cause. + * + * **Example** (Taking batches within bounds) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7, 8]) + * + * // Take between 2 and 5 items + * const batch1 = yield* TxQueue.takeBetween(queue, 2, 5) + * console.log(batch1) // [1, 2, 3, 4, 5] - took 5 (up to max) + * + * // Take between 1 and 10 items (but only 3 remain) + * const batch2 = yield* TxQueue.takeBetween(queue, 1, 10) + * console.log(batch2) // [6, 7, 8] - took 3 (all remaining) + * + * // Would wait for at least 1 item to be available + * // const batch3 = yield* TxQueue.takeBetween(queue, 1, 3) + * }) + * ``` + * + * @category taking + * @since 2.0.0 + */ +export const takeBetween: { + (min: number, max: number): (self: TxDequeue) => Effect.Effect, E> + (self: TxDequeue, min: number, max: number): Effect.Effect, E> +} = dual( + 3, + (self: TxDequeue, min: number, max: number): Effect.Effect, E> => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + // Check if queue is done - forward the cause directly + if (state._tag === "Done") { + return yield* Effect.failCause(state.cause) + } + + // Validate parameters + if (min <= 0 || max <= 0 || min > max) { + return [] + } + + const currentSize = yield* size(self) + + // If we have less than minimum required items + if (currentSize < min) { + // If queue is closing, transition to done and return what we have + if (state._tag === "Closing") { + if (yield* isEmpty(self)) { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + return [] + } + // Take all remaining items when closing (if >= min or all available) + const chunk = yield* TxChunk.get(self.items) + const taken = Chunk.toArray(chunk) + yield* TxChunk.set(self.items, Chunk.empty()) + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + return taken + } + + // Queue is still open but not enough items - retry transaction + return yield* Effect.txRetry + } + + // We have at least the minimum, take up to the maximum + const toTake = Math.min(currentSize, max) + const chunk = yield* TxChunk.get(self.items) + const taken = Chunk.take(chunk, toTake) + yield* TxChunk.drop(self.items, toTake) + + // Check if we need to transition Closing → Done + if (state._tag === "Closing" && (yield* isEmpty(self))) { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + } + + return Chunk.toArray(taken) + }).pipe(Effect.tx) +) + +/** + * Waits transactionally for the next item and returns it without removing it. + * + * **Details** + * + * If the queue is open but empty, the transaction retries until an item is available or the queue completes. If the queue is done, the queue's completion cause is propagated through the error channel. + * + * **Example** (Peeking without removing values) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offer(queue, 42) + * + * // Peek at the next item without removing it + * const item = yield* TxQueue.peek(queue) + * console.log(item) // 42 + * + * // Item is still in the queue + * const size = yield* TxQueue.size(queue) + * console.log(size) // 1 + * }) + * + * // Error handling example + * const errorExample = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(5) + * yield* TxQueue.fail(queue, "queue failed") + * + * // peek() propagates the queue error through E-channel + * const result = yield* Effect.flip(TxQueue.peek(queue)) + * console.log(result) // "queue failed" + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const peek = (self: TxDequeue): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + if (state._tag === "Done") { + return yield* Effect.failCause(state.cause) + } + + const chunk = yield* TxChunk.get(self.items) + const head = Chunk.head(chunk) + if (Option.isNone(head)) { + return yield* Effect.txRetry + } + + return head.value + }).pipe(Effect.tx) + +/** + * Gets the current size of the queue. + * + * **Example** (Reading queue size) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(queue, [1, 2, 3]) + * + * const size = yield* TxQueue.size(queue) + * console.log(size) // 3 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const size = (self: TxQueueState): Effect.Effect => TxChunk.size(self.items) + +/** + * Checks whether the queue is empty. + * + * **Example** (Checking whether a queue is empty) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * const empty = yield* TxQueue.isEmpty(queue) + * console.log(empty) // true + * + * yield* TxQueue.offer(queue, 42) + * const stillEmpty = yield* TxQueue.isEmpty(queue) + * console.log(stillEmpty) // false + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const isEmpty = (self: TxQueueState): Effect.Effect => TxChunk.isEmpty(self.items) + +/** + * Checks whether the queue is at capacity. + * + * **Example** (Checking whether a queue is full) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(2) + * + * const full = yield* TxQueue.isFull(queue) + * console.log(full) // false + * + * yield* TxQueue.offerAll(queue, [1, 2]) + * const nowFull = yield* TxQueue.isFull(queue) + * console.log(nowFull) // true + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const isFull = (self: TxQueueState): Effect.Effect => + self.capacity === Number.POSITIVE_INFINITY + ? Effect.succeed(false) + : Effect.map(size(self), (currentSize) => currentSize >= self.capacity) + +/** + * Interrupts the queue gracefully with the current fiber's interruption cause. + * + * **Details** + * + * If the queue still contains items, it enters the closing state so buffered items can be drained before consumers observe the interruption. If it is empty, it transitions directly to done. Returns `false` if the queue was already closing or done. + * + * **Example** (Interrupting queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offer(queue, 42) + * + * // Interrupt gracefully - allows remaining items to be consumed + * const result = yield* TxQueue.interrupt(queue) + * console.log(result) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const interrupt = (self: TxEnqueue): Effect.Effect => + Effect.withFiber((fiber) => failCause(self, Cause.interrupt(fiber.id))) + +/** + * Fails the queue with the specified error, discarding any buffered items. + * + * **Details** + * + * The queue transitions directly to done with `Cause.fail(error)`. Returns `false` if the queue was already closing or done. + * + * **Example** (Failing queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Fail the queue with an error + * const result = yield* TxQueue.fail(queue, "connection lost") + * console.log(result) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const fail: { + (error: E): (self: TxEnqueue) => Effect.Effect + (self: TxEnqueue, error: E): Effect.Effect +} = dual( + 2, + (self: TxEnqueue, error: E): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + if (state._tag !== "Open") { + return false // Already closing/done + } + + // Fail transitions directly to Done, clearing items + yield* TxChunk.set(self.items, Chunk.empty()) + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: Cause.fail(error) }) + + return true + }).pipe(Effect.tx) +) + +/** + * Completes the queue with the specified cause. + * + * **Details** + * + * If the queue is empty, it transitions directly to done. If it still contains items, it enters the closing state so buffered items can be drained before the cause is observed. Returns `false` if the queue was already closing or done. + * + * **Example** (Failing queues with causes) + * + * ```ts + * import { Cause, Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Complete with specific cause + * const cause = Cause.interrupt() + * const result = yield* TxQueue.failCause(queue, cause) + * console.log(result) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const failCause: { + (cause: Cause.Cause): (self: TxEnqueue) => Effect.Effect + (self: TxEnqueue, cause: Cause.Cause): Effect.Effect +} = dual( + 2, + (self: TxEnqueue, cause: Cause.Cause): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + if (state._tag !== "Open") { + return false // Already closing/done + } + + if (yield* isEmpty(self)) { + // Can transition directly to Done + yield* TxRef.set(self.stateRef, { _tag: "Done", cause }) + } else { + // Need to go through Closing state + yield* TxRef.set(self.stateRef, { _tag: "Closing", cause }) + } + + return true + }).pipe(Effect.tx) +) + +/** + * Ends a queue by signaling completion with a `Cause.Done` error. + * + * **Details** + * + * This is a convenience wrapper around `failCause` for queues whose error channel can contain `Cause.Done`. If buffered items remain, the queue enters the closing state and those items may still be consumed before later `take` or `peek` operations fail with `Cause.Done`. + * + * **Example** (Ending queues) + * + * ```ts + * import { Cause, Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // Signal the end of the queue + * const result = yield* TxQueue.end(queue) + * console.log(result) // true + * + * // All operations will now fail with Done + * const takeResult = yield* Effect.flip(TxQueue.take(queue)) + * console.log(Cause.isDone(takeResult)) // true + * + * const peekResult = yield* Effect.flip(TxQueue.peek(queue)) + * console.log(Cause.isDone(peekResult)) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const end = (self: TxEnqueue): Effect.Effect => + failCause(self, Cause.fail(Cause.Done())) + +/** + * Removes and returns all currently buffered elements without changing the + * queue state. + * + * **Details** + * + * If the queue is already done with a `Cause.Done` error, returns an empty array. If the queue is done for any other cause, including interruption or failure, that cause is propagated. + * + * **Example** (Clearing queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * const sizeBefore = yield* TxQueue.size(queue) + * console.log(sizeBefore) // 5 + * + * const cleared = yield* TxQueue.clear(queue) + * console.log(cleared) // [1, 2, 3, 4, 5] + * + * const sizeAfter = yield* TxQueue.size(queue) + * console.log(sizeAfter) // 0 + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const clear = (self: TxEnqueue): Effect.Effect, ExcludeDone> => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + if (state._tag === "Done") { + // Return empty array only for halt causes (like Cause.Done) + if (isDoneCause(state.cause)) { + return [] + } + return yield* Effect.failCause(state.cause) + } + const chunk = yield* TxChunk.get(self.items) + yield* TxChunk.set(self.items, Chunk.empty()) + return Chunk.toArray(chunk) + }).pipe(Effect.tx) + +/** + * Shuts down the queue immediately by clearing all items and interrupting it (legacy compatibility). + * + * **Details** + * + * This operation clears all items from the queue using `clear`, then interrupts the queue using `interrupt`. This function mutates the original TxQueue by clearing its contents and marking it as shutdown. It does not return a new TxQueue reference. + * + * **Example** (Shutting down queues) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) + * + * const sizeBefore = yield* TxQueue.size(queue) + * console.log(sizeBefore) // 5 + * + * yield* TxQueue.shutdown(queue) + * + * const sizeAfter = yield* TxQueue.size(queue) + * console.log(sizeAfter) // 0 (cleared) + * + * const isShutdown = yield* TxQueue.isShutdown(queue) + * console.log(isShutdown) // true (interrupted) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const shutdown = (self: TxEnqueue): Effect.Effect => + Effect.gen(function*() { + yield* Effect.ignore(clear(self)) + return yield* interrupt(self) + }).pipe(Effect.tx) + +/** + * Checks whether the queue is in the open state. + * + * **Example** (Checking open state) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * const open = yield* TxQueue.isOpen(queue) + * console.log(open) // true + * + * yield* TxQueue.interrupt(queue) + * const stillOpen = yield* TxQueue.isOpen(queue) + * console.log(stillOpen) // false + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isOpen = (self: TxQueueState): Effect.Effect => + Effect.map(TxRef.get(self.stateRef), (state) => state._tag === "Open") + +/** + * Checks whether the queue is in the closing state. + * + * **Example** (Checking closing state) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * yield* TxQueue.offer(queue, 42) + * + * const closing = yield* TxQueue.isClosing(queue) + * console.log(closing) // false + * + * yield* TxQueue.interrupt(queue) + * const nowClosing = yield* TxQueue.isClosing(queue) + * console.log(nowClosing) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isClosing = (self: TxQueueState): Effect.Effect => + Effect.map(TxRef.get(self.stateRef), (state) => state._tag === "Closing") + +/** + * Checks whether the queue is done (completed or failed). + * + * **Example** (Checking done state) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * const done = yield* TxQueue.isDone(queue) + * console.log(done) // false + * + * yield* TxQueue.interrupt(queue) + * const nowDone = yield* TxQueue.isDone(queue) + * console.log(nowDone) // true + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const isDone = (self: TxQueueState): Effect.Effect => + Effect.map(TxRef.get(self.stateRef), (state) => state._tag === "Done") + +/** + * Checks whether the queue is shutdown (legacy compatibility). + * + * **Example** (Checking shutdown state) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * const isShutdown = yield* TxQueue.isShutdown(queue) + * console.log(isShutdown) // false + * + * yield* TxQueue.shutdown(queue) + * const nowShutdown = yield* TxQueue.isShutdown(queue) + * console.log(nowShutdown) // true + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const isShutdown = (self: TxQueueState): Effect.Effect => isDone(self) + +/** + * Waits for the queue to complete (either successfully or with failure). + * + * **Example** (Awaiting queue completion) + * + * ```ts + * import { Effect, TxQueue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* TxQueue.bounded(10) + * + * // In another fiber, end the queue + * yield* Effect.forkChild(Effect.delay(TxQueue.interrupt(queue), "100 millis")) + * + * // Wait for completion - succeeds when queue ends + * yield* TxQueue.awaitCompletion(queue) + * console.log("Queue completed successfully") + * }) + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const awaitCompletion = (self: TxQueueState): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + + if (state._tag === "Done") { + return void 0 + } + + // Not done yet, retry transaction + return yield* Effect.txRetry + }).pipe(Effect.tx) diff --git a/repos/effect/packages/effect/src/TxReentrantLock.ts b/repos/effect/packages/effect/src/TxReentrantLock.ts new file mode 100644 index 0000000..f4dfc2e --- /dev/null +++ b/repos/effect/packages/effect/src/TxReentrantLock.ts @@ -0,0 +1,655 @@ +/** + * Coordinates shared access inside transactions with read and write locks. + * + * A `TxReentrantLock` lets many fibers hold read locks at the same time, or one + * fiber hold a write lock for exclusive access. Lock ownership is tracked by + * fiber, so a fiber that already holds the lock can acquire it again and later + * release each acquisition. Attempts that cannot proceed retry transactionally + * until the lock becomes available. This module includes manual, scoped, and + * wrapper-style operations for read and write locking. + * + * @since 4.0.0 + */ +import * as Effect from "./Effect.ts" +import * as HashMap from "./HashMap.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Scope from "./Scope.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxReentrantLock" + +/** + * @category models + * @since 4.0.0 + */ +interface LockState { + readonly readers: HashMap.HashMap + readonly writer: Option.Option +} + +const emptyState: LockState = { + readers: HashMap.empty(), + writer: Option.none() +} + +/** + * A TxReentrantLock provides a transactional read/write lock with reentrant semantics. + * Multiple readers can hold the lock concurrently, or a single writer can hold exclusive + * access. A fiber holding the write lock may acquire additional read/write locks (reentrancy). + * + * **Example** (Using read and write locks) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * + * // Multiple readers can proceed concurrently + * yield* TxReentrantLock.withReadLock(lock, Effect.succeed("reading")) + * + * // Writer gets exclusive access + * yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("writing")) + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxReentrantLock extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + /** @internal */ + readonly stateRef: TxRef.TxRef +} + +const TxReentrantLockProto: Omit = { + [NodeInspectSymbol](this: TxReentrantLock) { + return toJson(this) + }, + toJSON(this: TxReentrantLock) { + return { _id: "TxReentrantLock" } + }, + toString() { + return "TxReentrantLock" + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Creates a new TxReentrantLock. + * + * **Example** (Creating a reentrant lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const isLocked = yield* TxReentrantLock.locked(lock) + * console.log(isLocked) // false + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (): Effect.Effect => + Effect.gen(function*() { + const stateRef = yield* TxRef.make(emptyState) + const self = Object.create(TxReentrantLockProto) + self[TypeId] = TypeId + self.stateRef = stateRef + return self + }).pipe(Effect.tx) + +// ============================================================================= +// Mutations +// ============================================================================= + +/** + * Acquires a read lock. Blocks if another fiber holds the write lock. + * If the current fiber already holds the write lock, the read lock is granted (reentrancy). + * Returns the current number of read locks held by this fiber. + * + * **Example** (Acquiring a read lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const count = yield* TxReentrantLock.acquireRead(lock) + * console.log(count) // 1 + * yield* TxReentrantLock.releaseRead(lock) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const acquireRead = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + const fiberId = fiber.id + + // If another fiber holds the write lock, retry + if (Option.isSome(state.writer) && state.writer.value[0] !== fiberId) { + return yield* Effect.txRetry + } + + // Grant read lock + const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0) + const newCount = currentCount + 1 + yield* TxRef.set(self.stateRef, { + ...state, + readers: HashMap.set(state.readers, fiberId, newCount) + }) + return newCount + }).pipe(Effect.tx) + ) + +/** + * Acquires the write lock for the current fiber. + * + * **When to use** + * + * Use to enter an exclusive section manually when `withWriteLock` is not the + * right shape. + * + * **Details** + * + * Blocks if any other fiber holds a read or write lock. If the current fiber + * already holds the write lock, the count is incremented. If the current fiber + * holds a read lock, the write lock is granted as an upgrade. + * + * Returns the current number of write locks held by this fiber. + * + * **Example** (Acquiring a write lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const count = yield* TxReentrantLock.acquireWrite(lock) + * console.log(count) // 1 + * yield* TxReentrantLock.releaseWrite(lock) + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const acquireWrite = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + const fiberId = fiber.id + + // If another fiber holds the write lock, retry + if (Option.isSome(state.writer) && state.writer.value[0] !== fiberId) { + return yield* Effect.txRetry + } + + // If other fibers hold read locks, retry + for (const [readerId] of state.readers) { + if (readerId !== fiberId && Option.getOrElse(HashMap.get(state.readers, readerId), () => 0) > 0) { + return yield* Effect.txRetry + } + } + + // Grant write lock + if (Option.isSome(state.writer)) { + // Reentrant: increment write count + const newCount = state.writer.value[1] + 1 + yield* TxRef.set(self.stateRef, { + ...state, + writer: Option.some([fiberId, newCount] as const) + }) + return newCount + } + + // First write lock acquisition + yield* TxRef.set(self.stateRef, { + ...state, + writer: Option.some([fiberId, 1] as const) + }) + return 1 + }).pipe(Effect.tx) + ) + +/** + * Releases one read lock held by the current fiber. + * + * **When to use** + * + * Use to leave a manually acquired read lock. + * + * **Details** + * + * Returns the remaining number of read locks held by this fiber. + * + * **Example** (Releasing a read lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * yield* TxReentrantLock.acquireRead(lock) + * const remaining = yield* TxReentrantLock.releaseRead(lock) + * console.log(remaining) // 0 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const releaseRead = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + const fiberId = fiber.id + const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0) + + if (currentCount <= 0) return 0 + + const newCount = currentCount - 1 + const newReaders = newCount === 0 + ? HashMap.remove(state.readers, fiberId) + : HashMap.set(state.readers, fiberId, newCount) + + yield* TxRef.set(self.stateRef, { ...state, readers: newReaders }) + return newCount + }).pipe(Effect.tx) + ) + +/** + * Releases one write lock held by the current fiber. + * + * **When to use** + * + * Use to leave a manually acquired write lock. + * + * **Details** + * + * Returns the remaining number of write locks held by this fiber. + * + * **Example** (Releasing a write lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * yield* TxReentrantLock.acquireWrite(lock) + * const remaining = yield* TxReentrantLock.releaseWrite(lock) + * console.log(remaining) // 0 + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const releaseWrite = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + const fiberId = fiber.id + + if (Option.isNone(state.writer) || state.writer.value[0] !== fiberId) return 0 + + const newCount = state.writer.value[1] - 1 + const newWriter = newCount <= 0 + ? Option.none() + : Option.some([fiberId, newCount] as const) + + yield* TxRef.set(self.stateRef, { ...state, writer: newWriter }) + return newCount + }).pipe(Effect.tx) + ) + +/** + * Acquires a read lock for the duration of the scope. + * The lock is automatically released when the scope closes. + * + * **Example** (Holding a scoped read lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * yield* TxReentrantLock.readLock(lock) + * // read lock is held for the duration of the scope + * }) + * ) + * // read lock is released + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const readLock = (self: TxReentrantLock): Effect.Effect => + Effect.acquireRelease( + acquireRead(self), + () => releaseRead(self) + ) + +/** + * Acquires a write lock for the duration of the scope. + * The lock is automatically released when the scope closes. + * + * **Example** (Holding a scoped write lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * yield* TxReentrantLock.writeLock(lock) + * // write lock is held for the duration of the scope + * }) + * ) + * // write lock is released + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const writeLock = (self: TxReentrantLock): Effect.Effect => + Effect.acquireRelease( + acquireWrite(self), + () => releaseWrite(self) + ) + +/** + * Runs the provided effect while holding a read lock. The lock is automatically + * released after the effect completes, fails, or is interrupted. + * + * **Example** (Running an effect with a read lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const result = yield* TxReentrantLock.withReadLock( + * lock, + * Effect.succeed("read data") + * ) + * console.log(result) // "read data" + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const withReadLock: { + (effect: Effect.Effect): (self: TxReentrantLock) => Effect.Effect + (self: TxReentrantLock, effect: Effect.Effect): Effect.Effect +} = ((...args: Array) => { + if (args.length === 1) { + const [effect] = args + return (self: TxReentrantLock) => + Effect.acquireUseRelease( + acquireRead(self), + () => effect, + () => releaseRead(self) + ) + } + const [self, effect] = args + return Effect.acquireUseRelease( + acquireRead(self), + () => effect, + () => releaseRead(self) + ) +}) as any + +/** + * Runs the provided effect while holding a write lock. The lock is automatically + * released after the effect completes, fails, or is interrupted. + * + * **Example** (Running an effect with a write lock) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const result = yield* TxReentrantLock.withWriteLock( + * lock, + * Effect.succeed("wrote data") + * ) + * console.log(result) // "wrote data" + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const withWriteLock: { + (effect: Effect.Effect): (self: TxReentrantLock) => Effect.Effect + (self: TxReentrantLock, effect: Effect.Effect): Effect.Effect +} = ((...args: Array) => { + if (args.length === 1) { + const [effect] = args + return (self: TxReentrantLock) => + Effect.acquireUseRelease( + acquireWrite(self), + () => effect, + () => releaseWrite(self) + ) + } + const [self, effect] = args + return Effect.acquireUseRelease( + acquireWrite(self), + () => effect, + () => releaseWrite(self) + ) +}) as any + +/** + * Runs an effect while holding a write lock. + * + * **When to use** + * + * Use when you need to run an effect with exclusive write access through a + * `TxReentrantLock` and prefer the concise lock helper. + * + * **Example** (Running an effect with exclusive access) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const result = yield* TxReentrantLock.withLock( + * lock, + * Effect.succeed("exclusive operation") + * ) + * console.log(result) // "exclusive operation" + * }) + * ``` + * + * @category mutations + * @since 2.0.0 + */ +export const withLock: { + (effect: Effect.Effect): (self: TxReentrantLock) => Effect.Effect + (self: TxReentrantLock, effect: Effect.Effect): Effect.Effect +} = withWriteLock + +// ============================================================================= +// Getters +// ============================================================================= + +/** + * Returns the total number of read locks held across all fibers. + * + * **Example** (Counting read locks) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * yield* TxReentrantLock.acquireRead(lock) + * const count = yield* TxReentrantLock.readLocks(lock) + * console.log(count) // 1 + * yield* TxReentrantLock.releaseRead(lock) + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const readLocks = (self: TxReentrantLock): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + let total = 0 + for (const [, count] of state.readers) { + total += count + } + return total + }) + +/** + * Returns the number of write locks held (0 or the reentrant count). + * + * **Example** (Counting write locks) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const count = yield* TxReentrantLock.writeLocks(lock) + * console.log(count) // 0 + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const writeLocks = (self: TxReentrantLock): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + return Option.isSome(state.writer) ? state.writer.value[1] : 0 + }) + +/** + * Checks whether the lock is held by any fiber (read or write). + * + * **Example** (Checking whether a lock is held) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const isLocked = yield* TxReentrantLock.locked(lock) + * console.log(isLocked) // false + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const locked = (self: TxReentrantLock): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + return HashMap.size(state.readers) > 0 || Option.isSome(state.writer) + }) + +/** + * Checks whether any fiber holds a read lock. + * + * **Example** (Checking whether a read lock is held) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const isReadLocked = yield* TxReentrantLock.readLocked(lock) + * console.log(isReadLocked) // false + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const readLocked = (self: TxReentrantLock): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + return HashMap.size(state.readers) > 0 + }) + +/** + * Checks whether any fiber holds a write lock. + * + * **Example** (Checking whether a write lock is held) + * + * ```ts + * import { Effect, TxReentrantLock } from "effect" + * + * const program = Effect.gen(function*() { + * const lock = yield* TxReentrantLock.make() + * const isWriteLocked = yield* TxReentrantLock.writeLocked(lock) + * console.log(isWriteLocked) // false + * }) + * ``` + * + * @category getters + * @since 2.0.0 + */ +export const writeLocked = (self: TxReentrantLock): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + return Option.isSome(state.writer) + }) + +// ============================================================================= +// Guards +// ============================================================================= + +/** + * Checks whether the given value is a TxReentrantLock. + * + * **Example** (Checking for TxReentrantLock values) + * + * ```ts + * import { TxReentrantLock } from "effect" + * + * declare const someValue: unknown + * + * if (TxReentrantLock.isTxReentrantLock(someValue)) { + * console.log("This is a TxReentrantLock") + * } + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isTxReentrantLock = (u: unknown): u is TxReentrantLock => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/TxRef.ts b/repos/effect/packages/effect/src/TxRef.ts new file mode 100644 index 0000000..94e7891 --- /dev/null +++ b/repos/effect/packages/effect/src/TxRef.ts @@ -0,0 +1,282 @@ +/** + * Transactional references for coordinating mutable state with Effect + * transactions. A `TxRef` stores a current value, but reads and writes inside + * `Effect.tx` are recorded in a transaction journal and committed together only + * when the outermost transaction succeeds. + * + * This is the basic building block behind the other transactional collections + * in Effect. The module provides effectful and unsafe constructors plus the + * core operations for reading, setting, updating, and modifying a transactional + * value while returning a separate result. + * + * @since 4.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import { pipeArguments } from "./Pipeable.ts" +import type { Pipeable } from "./Pipeable.ts" +import type { NoInfer } from "./Types.ts" + +const TypeId = "~effect/transactions/TxRef" + +/** + * TxRef is a transactional value, it can be read and modified within the body of a transaction. + * + * **When to use** + * + * Use to store mutable state that must be read and modified inside Effect + * transactions. + * + * **Details** + * + * Accessed values are tracked by the transaction in order to detect conflicts and in order to + * track changes, a transaction will retry whenever a conflict is detected or whenever the + * transaction explicitely calls to `Effect.txRetry` and any of the accessed TxRef values + * change. + * + * **Example** (Using a transactional reference) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional reference + * const ref: TxRef.TxRef = yield* TxRef.make(0) + * + * // Use within a transaction + * yield* Effect.tx(Effect.gen(function*() { + * const current = yield* TxRef.get(ref) + * yield* TxRef.set(ref, current + 1) + * })) + * + * const final = yield* TxRef.get(ref) + * console.log(final) // 1 + * }) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface TxRef extends Pipeable { + readonly [TypeId]: typeof TypeId + + version: number + pending: Map void> + value: A +} + +/** + * Creates a new `TxRef` with the specified initial value. + * + * **When to use** + * + * Use to create a `TxRef` inside an `Effect` workflow. + * + * **Example** (Creating transactional references) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * // Create a transactional reference with initial value + * const counter = yield* TxRef.make(0) + * const name = yield* TxRef.make("Alice") + * + * // Use in transactions + * yield* Effect.tx(Effect.gen(function*() { + * yield* TxRef.set(counter, 42) + * yield* TxRef.set(name, "Bob") + * })) + * + * console.log(yield* TxRef.get(counter)) // 42 + * console.log(yield* TxRef.get(name)) // "Bob" + * }) + * ``` + * + * @category constructors + * @since 2.0.0 + */ +export const make = (initial: A) => Effect.sync(() => makeUnsafe(initial)) + +/** + * Creates a new `TxRef` synchronously with the specified initial value. + * + * **When to use** + * + * Use to construct a `TxRef` synchronously when it must be created outside an + * `Effect` workflow. + * + * **Example** (Creating transactional references unsafely) + * + * ```ts + * import { TxRef } from "effect" + * + * // Create a TxRef synchronously (unsafe - use make instead in Effect contexts) + * const counter = TxRef.makeUnsafe(0) + * const config = TxRef.makeUnsafe({ timeout: 5000, retries: 3 }) + * + * // These are now ready to use in transactions + * console.log(counter.value) // 0 + * console.log(config.value) // { timeout: 5000, retries: 3 } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const makeUnsafe = (initial: A): TxRef => ({ + [TypeId]: TypeId, + pending: new Map(), + pipe() { + return pipeArguments(this, arguments) + }, + version: 0, + value: initial +}) + +/** + * Modifies the value of the `TxRef` using the provided function. + * + * **When to use** + * + * Use to update a `TxRef` and return a computed result from the same + * transaction step. + * + * **Example** (Modifying transactional references) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* TxRef.make(0) + * + * // Modify and return both old and new value + * const result = yield* TxRef.modify(counter, (current) => [current * 2, current + 1]) + * + * console.log(result) // 0 (the return value: current * 2) + * console.log(yield* TxRef.get(counter)) // 1 (the new value: current + 1) + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const modify: { + (f: (current: NoInfer) => [returnValue: R, newValue: A]): (self: TxRef) => Effect.Effect + (self: TxRef, f: (current: A) => [returnValue: R, newValue: A]): Effect.Effect +} = dual(2, ( + self: TxRef, + f: (current: A) => [returnValue: R, newValue: A] +): Effect.Effect => + Effect.Transaction.pipe( + Effect.flatMap((state) => + Effect.sync(() => { + if (!state.journal.has(self)) { + state.journal.set(self, { version: self.version, value: self.value }) + } + const current = state.journal.get(self)! + const [returnValue, next] = f(current.value) + current.value = next + return returnValue + }) + ), + Effect.tx + )) + +/** + * Updates the value of the `TxRef` using the provided function. + * + * **When to use** + * + * Use to transform a `TxRef` when no result value is needed. + * + * **Example** (Updating transactional references) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* TxRef.make(10) + * + * // Update the value using a function + * yield* Effect.tx( + * TxRef.update(counter, (current) => current * 2) + * ) + * + * console.log(yield* TxRef.get(counter)) // 20 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const update: { + (f: (current: NoInfer) => A): (self: TxRef) => Effect.Effect + (self: TxRef, f: (current: A) => A): Effect.Effect +} = dual(2, ( + self: TxRef, + f: (current: A) => A +): Effect.Effect => modify(self, (current) => [void 0, f(current)])) + +/** + * Reads the current value of the `TxRef`. + * + * **When to use** + * + * Use to read the current value of a `TxRef`. + * + * **Example** (Reading transactional references) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* TxRef.make(42) + * + * // Read the value within a transaction + * const value = yield* Effect.tx( + * TxRef.get(counter) + * ) + * + * console.log(value) // 42 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const get = (self: TxRef): Effect.Effect => modify(self, (current) => [current, current]) + +/** + * Sets the value of the `TxRef`. + * + * **When to use** + * + * Use to replace the value of a `TxRef`. + * + * **Example** (Setting transactional references) + * + * ```ts + * import { Effect, TxRef } from "effect" + * + * const program = Effect.gen(function*() { + * const counter = yield* TxRef.make(0) + * + * // Set a new value within a transaction + * yield* Effect.tx( + * TxRef.set(counter, 100) + * ) + * + * console.log(yield* TxRef.get(counter)) // 100 + * }) + * ``` + * + * @category combinators + * @since 2.0.0 + */ +export const set: { + (value: A): (self: TxRef) => Effect.Effect + (self: TxRef, value: A): Effect.Effect +} = dual(2, ( + self: TxRef, + value: A +): Effect.Effect => update(self, () => value)) diff --git a/repos/effect/packages/effect/src/TxSemaphore.ts b/repos/effect/packages/effect/src/TxSemaphore.ts new file mode 100644 index 0000000..7886e2d --- /dev/null +++ b/repos/effect/packages/effect/src/TxSemaphore.ts @@ -0,0 +1,707 @@ +/** + * Coordinates access to limited resources inside transactions. + * + * A `TxSemaphore` has a fixed capacity and stores its available permit count in + * a `TxRef`. Acquiring or releasing permits can therefore commit atomically + * with other transactional state changes. This module includes operations for + * creating semaphores, checking capacity and availability, acquiring or + * releasing permits, and running effects while permits are held. + * + * @since 4.0.0 + */ + +import * as Effect from "./Effect.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Scope from "./Scope.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxSemaphore" + +/** + * A transactional semaphore that manages permits using Software Transactional + * Memory (STM) semantics, providing atomic permit acquisition and release + * operations within Effect transactions for concurrency control over limited + * resources. + * + * **When to use** + * + * Use to coordinate permit accounting atomically with other transactional + * state changes. + * + * **Example** (Managing permits transactionally) + * + * ```ts + * import { Effect, TxSemaphore } from "effect" + * + * // Create a semaphore with 3 permits for managing concurrent database connections + * const program = Effect.gen(function*() { + * const dbSemaphore = yield* TxSemaphore.make(3) + * + * // Acquire a permit before accessing the database + * yield* TxSemaphore.acquire(dbSemaphore) + * console.log("Database connection acquired") + * + * // Perform database operations... + * + * // Release the permit when done + * yield* TxSemaphore.release(dbSemaphore) + * console.log("Database connection released") + * }) + * ``` + * + * @see {@link make} for creating a transactional semaphore + * @see {@link withPermit} for automatically acquiring and releasing one permit + * @see {@link acquire} for manually acquiring one permit transactionally + * + * @category models + * @since 4.0.0 + */ +export interface TxSemaphore extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + readonly permitsRef: TxRef.TxRef + readonly capacity: number +} + +const TxSemaphoreProto: Omit = { + [NodeInspectSymbol](this: TxSemaphore) { + return toJson(this) + }, + toJSON(this: TxSemaphore) { + return { + _id: "TxSemaphore", + capacity: this.capacity + } + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const makeTxSemaphore = (permitsRef: TxRef.TxRef, capacity: number): TxSemaphore => { + const self = Object.create(TxSemaphoreProto) + self[TypeId] = TypeId + self.permitsRef = permitsRef + self.capacity = capacity + return self +} + +/** + * Creates a new TxSemaphore with the specified number of permits. + * + * **When to use** + * + * Use to create a transactional semaphore with a fixed permit capacity. + * + * **Example** (Creating a semaphore) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * // Create a semaphore for managing concurrent access to a resource pool + * const program = Effect.gen(function*() { + * // Create a semaphore with 3 permits for a connection pool + * const connectionSemaphore = yield* TxSemaphore.make(3) + * + * // Check initial state + * const available = yield* TxSemaphore.available(connectionSemaphore) + * const capacity = yield* TxSemaphore.capacity(connectionSemaphore) + * + * yield* Console.log( + * `Created semaphore with ${capacity} permits, ${available} available` + * ) + * // Output: "Created semaphore with 3 permits, 3 available" + * }) + * ``` + * + * @see {@link available} for reading the current available permit count + * @see {@link capacity} for reading the fixed total permit count + * + * @category constructors + * @since 2.0.0 + */ +export const make = (permits: number): Effect.Effect => + Effect.gen(function*() { + if (permits < 0) { + return yield* Effect.die(new Error("Permits must be non-negative")) + } + + const permitsRef = yield* TxRef.make(permits) + return makeTxSemaphore(permitsRef, permits) + }).pipe(Effect.tx) + +/** + * Gets the current number of available permits in the semaphore. + * + * **When to use** + * + * Use to inspect how many permits are currently available. + * + * **Example** (Checking available permits) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(5) + * + * // Check available permits before acquiring + * const before = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`Available permits: ${before}`) // 5 + * + * // Acquire some permits + * yield* TxSemaphore.acquire(semaphore) + * yield* TxSemaphore.acquire(semaphore) + * + * // Check available permits after acquiring + * const after = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`Available permits: ${after}`) // 3 + * }) + * ``` + * + * @see {@link capacity} for reading the fixed total permit count + * + * @category combinators + * @since 2.0.0 + */ +export const available = (self: TxSemaphore): Effect.Effect => TxRef.get(self.permitsRef) + +/** + * Gets the maximum capacity (total permits) of the semaphore. + * + * **When to use** + * + * Use to inspect the fixed total number of permits managed by the semaphore. + * + * **Example** (Checking semaphore capacity) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(10) + * + * const capacity = yield* TxSemaphore.capacity(semaphore) + * yield* Console.log(`Semaphore capacity: ${capacity}`) // 10 + * + * // Capacity remains constant regardless of current permits + * yield* TxSemaphore.acquire(semaphore) + * const stillSame = yield* TxSemaphore.capacity(semaphore) + * yield* Console.log(`Capacity after acquire: ${stillSame}`) // 10 + * }) + * ``` + * + * @see {@link available} for reading the current available permit count + * + * @category combinators + * @since 4.0.0 + */ +export const capacity = (self: TxSemaphore): Effect.Effect => Effect.succeed(self.capacity) + +/** + * Acquires a single permit from the semaphore. If no permits are available, + * the effect will block until one becomes available. + * + * **When to use** + * + * Use to manually acquire one permit transactionally, waiting until one is + * available. + * + * **Example** (Acquiring a permit) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(2) + * + * yield* Console.log("Acquiring first permit...") + * yield* TxSemaphore.acquire(semaphore) + * yield* Console.log("First permit acquired") + * + * yield* Console.log("Acquiring second permit...") + * yield* TxSemaphore.acquire(semaphore) + * yield* Console.log("Second permit acquired") + * + * const available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`Available permits: ${available}`) // 0 + * }) + * ``` + * + * @see {@link tryAcquire} for a non-blocking single-permit attempt + * @see {@link release} for returning one permit + * @see {@link withPermit} for automatic acquire and release around an effect + * + * @category combinators + * @since 2.0.0 + */ +export const acquire = (self: TxSemaphore): Effect.Effect => + Effect.gen(function*() { + const permits = yield* TxRef.get(self.permitsRef) + if (permits <= 0) { + return yield* Effect.txRetry + } + yield* TxRef.set(self.permitsRef, permits - 1) + }).pipe(Effect.tx) + +/** + * Acquires the specified number of permits from the semaphore. + * + * **When to use** + * + * Use to manually acquire multiple permits transactionally, waiting until all + * requested permits are available. + * + * **Details** + * + * If fewer than `n` permits are available, the transaction retries until enough + * permits are released. + * + * **Gotchas** + * + * Passing a non-positive `n` dies with a defect. Passing a value greater than + * the semaphore capacity can wait forever because the capacity is fixed. + * + * **Example** (Acquiring multiple permits) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(5) + * + * yield* Console.log("Acquiring 3 permits...") + * yield* TxSemaphore.acquireN(semaphore, 3) + * yield* Console.log("3 permits acquired") + * + * const available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`Available permits: ${available}`) // 2 + * }) + * ``` + * + * @see {@link tryAcquireN} for a non-blocking multi-permit attempt + * @see {@link releaseN} for returning multiple permits + * @see {@link withPermits} for automatic acquire and release around an effect + * + * @category combinators + * @since 2.0.0 + */ +export const acquireN = (self: TxSemaphore, n: number): Effect.Effect => { + if (n <= 0) { + return Effect.die(new Error("Number of permits must be positive")) + } + return Effect.gen(function*() { + const permits = yield* TxRef.get(self.permitsRef) + if (permits < n) { + return yield* Effect.txRetry + } + yield* TxRef.set(self.permitsRef, permits - n) + }).pipe(Effect.tx) +} + +/** + * Tries to acquire a single permit from the semaphore without blocking, + * returning `true` if successful or `false` if no permits are available. + * + * **When to use** + * + * Use to attempt a single-permit acquisition without retrying when no permit is + * available. + * + * **Example** (Trying to acquire a permit) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(1) + * + * // First try should succeed + * const first = yield* TxSemaphore.tryAcquire(semaphore) + * yield* Console.log(`First try: ${first}`) // true + * + * // Second try should fail (no permits left) + * const second = yield* TxSemaphore.tryAcquire(semaphore) + * yield* Console.log(`Second try: ${second}`) // false + * }) + * ``` + * + * @see {@link acquire} for waiting until one permit is available + * @see {@link tryAcquireN} for attempting to acquire multiple permits without blocking + * + * @category combinators + * @since 4.0.0 + */ +export const tryAcquire = (self: TxSemaphore): Effect.Effect => + TxRef.modify(self.permitsRef, (permits: number) => { + if (permits > 0) { + return [true, permits - 1] + } + return [false, permits] + }) + +/** + * Tries to acquire the specified number of permits from the semaphore without + * blocking, returning `true` if successful or `false` if not enough permits are + * available. + * + * **When to use** + * + * Use to attempt a multi-permit acquisition without retrying when not enough + * permits are available. + * + * **Example** (Trying to acquire multiple permits) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(3) + * + * // Try to acquire 2 permits (should succeed) + * const first = yield* TxSemaphore.tryAcquireN(semaphore, 2) + * yield* Console.log(`First try (2 permits): ${first}`) // true + * + * // Try to acquire 2 more permits (should fail, only 1 left) + * const second = yield* TxSemaphore.tryAcquireN(semaphore, 2) + * yield* Console.log(`Second try (2 permits): ${second}`) // false + * }) + * ``` + * + * @see {@link acquireN} for waiting until all requested permits are available + * @see {@link tryAcquire} for attempting to acquire one permit without blocking + * + * @category combinators + * @since 4.0.0 + */ +export const tryAcquireN = (self: TxSemaphore, n: number): Effect.Effect => { + if (n <= 0) { + return Effect.die(new Error("Number of permits must be positive")) + } + return TxRef.modify(self.permitsRef, (permits: number) => { + if (permits >= n) { + return [true, permits - n] + } + return [false, permits] + }) +} + +/** + * Releases one permit back to the semaphore, making it available for + * acquisition. + * + * **When to use** + * + * Use to manually return one permit after a transactional acquire. + * + * **Details** + * + * If the semaphore is already at capacity, this operation leaves the permit + * count unchanged. + * + * **Example** (Releasing a permit) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(2) + * + * // Acquire a permit + * yield* TxSemaphore.acquire(semaphore) + * let available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`After acquire: ${available}`) // 1 + * + * // Release the permit + * yield* TxSemaphore.release(semaphore) + * available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`After release: ${available}`) // 2 + * }) + * ``` + * + * @see {@link acquire} for manually acquiring one permit + * @see {@link releaseN} for returning multiple permits + * + * @category combinators + * @since 2.0.0 + */ +export const release = (self: TxSemaphore): Effect.Effect => + TxRef.update(self.permitsRef, (permits: number) => permits >= self.capacity ? permits : permits + 1) + +/** + * Releases the specified number of permits back to the semaphore. + * + * **When to use** + * + * Use to manually return multiple permits after a transactional acquire. + * + * **Details** + * + * The available permit count is capped at the semaphore capacity. + * + * **Gotchas** + * + * Passing a non-positive `n` dies with a defect. + * + * **Example** (Releasing multiple permits) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(5) + * + * // Acquire 3 permits + * yield* TxSemaphore.acquireN(semaphore, 3) + * let available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`After acquire: ${available}`) // 2 + * + * // Release 2 permits + * yield* TxSemaphore.releaseN(semaphore, 2) + * available = yield* TxSemaphore.available(semaphore) + * yield* Console.log(`After release: ${available}`) // 4 + * }) + * ``` + * + * @see {@link acquireN} for manually acquiring multiple permits + * @see {@link release} for returning one permit + * + * @category combinators + * @since 2.0.0 + */ +export const releaseN = (self: TxSemaphore, n: number): Effect.Effect => { + if (n <= 0) { + return Effect.die(new Error("Number of permits must be positive")) + } + return TxRef.update(self.permitsRef, (permits: number) => { + const newPermits = permits + n + return newPermits > self.capacity ? self.capacity : newPermits + }) +} + +/** + * Executes an effect with a single permit from the semaphore. The permit is + * automatically acquired before execution and released afterwards, even if the + * effect fails or is interrupted. + * + * **When to use** + * + * Use to run an effect while automatically acquiring and releasing one + * transactional permit. + * + * **Details** + * + * The permit acquisition and release operations use atomic semantics to ensure + * proper resource management with Effect's scoped operations. + * + * **Example** (Running an effect with a permit) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(2) + * + * // Execute database operation with automatic permit management + * const result = yield* TxSemaphore.withPermit( + * semaphore, + * Effect.gen(function*() { + * yield* Console.log("Permit acquired, accessing database...") + * yield* Effect.sleep("100 millis") // Simulate database work + * yield* Console.log("Database operation complete") + * return "query result" + * }) + * ) + * + * yield* Console.log(`Result: ${result}`) + * // Permit is automatically released here + * }) + * ``` + * + * @see {@link withPermits} for automatically acquiring and releasing multiple permits + * @see {@link withPermitScoped} for acquiring one permit for the current scope + * @see {@link acquire} for manual single-permit acquisition + * + * @category combinators + * @since 2.0.0 + */ +export const withPermit: { + (self: TxSemaphore): (effect: Effect.Effect) => Effect.Effect + (self: TxSemaphore, effect: Effect.Effect): Effect.Effect +} = ((...args: Array) => { + if (args.length === 1) { + const [self] = args + return (effect: Effect.Effect) => + Effect.acquireUseRelease( + acquire(self), + () => effect, + () => release(self) + ) + } + const [self, effect] = args + return Effect.acquireUseRelease( + acquire(self), + () => effect, + () => release(self) + ) +}) as any + +/** + * Runs an effect while holding the specified number of permits from the + * semaphore. + * + * **When to use** + * + * Use to run an effect while automatically acquiring and releasing multiple + * transactional permits. + * + * **Details** + * + * The permits are acquired before the effect starts and released after it + * completes, fails, or is interrupted. + * + * **Gotchas** + * + * Passing a non-positive `n` dies with a defect. Passing a value greater than + * the semaphore capacity can wait forever. + * + * **Example** (Running an effect with multiple permits) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(5) + * + * // Execute batch operation with 3 permits + * const results = yield* TxSemaphore.withPermits( + * semaphore, + * 3, + * Effect.gen(function*() { + * yield* Console.log("3 permits acquired, processing batch...") + * yield* Effect.sleep("200 millis") // Simulate batch processing + * return ["result1", "result2", "result3"] + * }) + * ) + * + * yield* Console.log(`Batch results: ${results.join(", ")}`) + * // All 3 permits are automatically released here + * }) + * ``` + * + * @see {@link withPermit} for automatically acquiring and releasing one permit + * @see {@link acquireN} for manual multi-permit acquisition + * + * @category combinators + * @since 2.0.0 + */ +export const withPermits: { + (self: TxSemaphore, n: number): (effect: Effect.Effect) => Effect.Effect + (self: TxSemaphore, n: number, effect: Effect.Effect): Effect.Effect +} = ((...args: Array) => { + if (args.length === 2) { + const [self, n] = args + return (effect: Effect.Effect) => + Effect.acquireUseRelease( + acquireN(self, n), + () => effect, + () => releaseN(self, n) + ) + } + const [self, n, effect] = args + return Effect.acquireUseRelease( + acquireN(self, n), + () => effect, + () => releaseN(self, n) + ) +}) as any + +/** + * Acquires a single permit from the semaphore in a scoped manner. The permit + * will be automatically released when the scope is closed, even if effects + * within the scope fail or are interrupted. + * + * **When to use** + * + * Use to acquire one transactional permit for the lifetime of the current + * scope. + * + * **Details** + * + * The permit acquisition and release operations use atomic semantics to ensure + * proper resource management with Effect's scoped operations. + * + * **Example** (Acquiring a scoped permit) + * + * ```ts + * import { Console, Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(3) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * // Acquire permit for the duration of this scope + * yield* TxSemaphore.withPermitScoped(semaphore) + * yield* Console.log("Permit acquired for scope") + * + * // Do work within the scope + * yield* Effect.sleep("500 millis") + * yield* Console.log("Work completed") + * + * // Permit will be automatically released when scope closes + * }) + * ) + * + * yield* Console.log("Scope closed, permit released") + * }) + * ``` + * + * @see {@link withPermit} for acquiring one permit around a single effect + * @see {@link acquire} for manual single-permit acquisition + * + * @category combinators + * @since 2.0.0 + */ +export const withPermitScoped = (self: TxSemaphore): Effect.Effect => + Effect.acquireRelease( + acquire(self), + () => release(self) + ) + +/** + * Determines if the provided value is a TxSemaphore. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `TxSemaphore`. + * + * **Example** (Checking semaphore values) + * + * ```ts + * import { Effect, TxSemaphore } from "effect" + * + * const program = Effect.gen(function*() { + * const semaphore = yield* TxSemaphore.make(5) + * const notSemaphore = { some: "object" } + * + * console.log(TxSemaphore.isTxSemaphore(semaphore)) // true + * console.log(TxSemaphore.isTxSemaphore(notSemaphore)) // false + * + * // Useful for runtime type checking in generic functions + * if (TxSemaphore.isTxSemaphore(semaphore)) { + * const available = yield* TxSemaphore.available(semaphore) + * console.log(`Available permits: ${available}`) + * } + * }) + * ``` + * + * @see {@link make} for creating a `TxSemaphore` + * + * @category guards + * @since 4.0.0 + */ +export const isTxSemaphore = (u: unknown): u is TxSemaphore => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/TxSubscriptionRef.ts b/repos/effect/packages/effect/src/TxSubscriptionRef.ts new file mode 100644 index 0000000..aa4a894 --- /dev/null +++ b/repos/effect/packages/effect/src/TxSubscriptionRef.ts @@ -0,0 +1,518 @@ +/** + * Stores transactional state and publishes committed changes. + * + * A `TxSubscriptionRef` combines a `TxRef` for the current value with a + * transactional pub/sub channel for updates. Subscribers first receive the + * current value and then every later value that is published by committed + * updates. This module includes constructors, reads, writes, update and modify + * helpers, transactional-queue subscriptions, stream subscriptions, and a guard. + * + * @since 4.0.0 + */ +import * as Effect from "./Effect.ts" +import { dual } from "./Function.ts" +import type { Inspectable } from "./Inspectable.ts" +import { NodeInspectSymbol, toJson } from "./Inspectable.ts" +import type { Pipeable } from "./Pipeable.ts" +import { pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import type * as Scope from "./Scope.ts" +import * as Stream from "./Stream.ts" +import * as TxPubSub from "./TxPubSub.ts" +import * as TxQueue from "./TxQueue.ts" +import * as TxRef from "./TxRef.ts" + +const TypeId = "~effect/transactions/TxSubscriptionRef" + +/** + * A TxSubscriptionRef is a transactional reference that allows subscribing to all + * committed changes. Subscribers receive the current value followed by every subsequent + * update via a transactional dequeue. + * + * **When to use** + * + * Use to store transactional state whose committed changes must be observable by + * subscribers. + * + * **Example** (Subscribing to transactional changes) + * + * ```ts + * import { Effect, TxQueue, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(0) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxSubscriptionRef.changes(ref) + * const initial = yield* TxQueue.take(sub) + * console.log(initial) // 0 + * + * yield* TxSubscriptionRef.set(ref, 1) + * const next = yield* TxQueue.take(sub) + * console.log(next) // 1 + * }) + * ) + * }) + * ``` + * + * @see {@link make} for creating a transactional subscription reference + * @see {@link changes} for subscribing through a transactional queue + * @see {@link changesStream} for subscribing through a `Stream` + * + * @category models + * @since 4.0.0 + */ +export interface TxSubscriptionRef extends Inspectable, Pipeable { + readonly [TypeId]: typeof TypeId + /** @internal */ + readonly ref: TxRef.TxRef + /** @internal */ + readonly pubsub: TxPubSub.TxPubSub +} + +const TxSubscriptionRefProto: Omit, typeof TypeId | "ref" | "pubsub"> = { + [NodeInspectSymbol](this: TxSubscriptionRef) { + return toJson(this) + }, + toJSON(this: TxSubscriptionRef) { + return { _id: "TxSubscriptionRef" } + }, + toString() { + return "TxSubscriptionRef" + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Creates a new TxSubscriptionRef with the specified initial value. + * + * **When to use** + * + * Use to create a `TxSubscriptionRef` that publishes every committed update to + * subscribers. + * + * **Example** (Creating a transactional subscription reference) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(42) + * const value = yield* TxSubscriptionRef.get(ref) + * console.log(value) // 42 + * }) + * ``` + * + * @see {@link changes} for subscribing to the created reference + * + * @category constructors + * @since 3.10.0 + */ +export const make = (value: A): Effect.Effect> => + Effect.gen(function*() { + const ref = yield* TxRef.make(value) + const pubsub = yield* TxPubSub.unbounded() + const self = Object.create(TxSubscriptionRefProto) + self[TypeId] = TypeId + self.ref = ref + self.pubsub = pubsub + return self + }).pipe(Effect.tx) + +// ============================================================================= +// Getters +// ============================================================================= + +/** + * Reads the current value of the TxSubscriptionRef. + * + * **When to use** + * + * Use to read the current `TxSubscriptionRef` value without subscribing to + * future changes. + * + * **Example** (Reading the current value) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make("hello") + * const value = yield* TxSubscriptionRef.get(ref) + * console.log(value) // "hello" + * }) + * ``` + * + * @see {@link changes} for reading the current value and subsequent updates + * + * @category getters + * @since 3.10.0 + */ +export const get = (self: TxSubscriptionRef): Effect.Effect => TxRef.get(self.ref) + +// ============================================================================= +// Mutations +// ============================================================================= + +/** + * Modifies the value of the TxSubscriptionRef using a function that returns both a + * result and the new value. The new value is published to all subscribers atomically. + * + * **When to use** + * + * Use to compute a separate return value and next `TxSubscriptionRef` state in + * one transactional update. + * + * **Example** (Modifying and returning a value) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(10) + * const result = yield* TxSubscriptionRef.modify(ref, (n) => [`was ${n}`, n + 1]) + * console.log(result) // "was 10" + * console.log(yield* TxSubscriptionRef.get(ref)) // 11 + * }) + * ``` + * + * @see {@link update} for deriving the next value without a separate return value + * @see {@link set} for replacing the value directly + * + * @category mutations + * @since 3.10.0 + */ +export const modify: { + ( + f: (current: A) => [returnValue: B, newValue: A] + ): (self: TxSubscriptionRef) => Effect.Effect + ( + self: TxSubscriptionRef, + f: (current: A) => [returnValue: B, newValue: A] + ): Effect.Effect +} = dual( + 2, + ( + self: TxSubscriptionRef, + f: (current: A) => [returnValue: B, newValue: A] + ): Effect.Effect => + Effect.gen(function*() { + const current = yield* TxRef.get(self.ref) + const [returnValue, newValue] = f(current) + yield* TxRef.set(self.ref, newValue) + yield* TxPubSub.publish(self.pubsub, newValue) + return returnValue + }).pipe(Effect.tx) +) + +/** + * Sets the value of the TxSubscriptionRef and publishes the new value to all subscribers. + * + * **When to use** + * + * Use to replace the current `TxSubscriptionRef` value with a known value and + * publish it. + * + * **Example** (Setting a new value) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(0) + * yield* TxSubscriptionRef.set(ref, 42) + * console.log(yield* TxSubscriptionRef.get(ref)) // 42 + * }) + * ``` + * + * @see {@link update} for deriving the new value from the current value + * @see {@link getAndSet} for setting while returning the previous value + * + * @category mutations + * @since 3.10.0 + */ +export const set: { + (value: A): (self: TxSubscriptionRef) => Effect.Effect + (self: TxSubscriptionRef, value: A): Effect.Effect +} = dual( + 2, + (self: TxSubscriptionRef, value: A): Effect.Effect => modify(self, () => [void 0, value]) +) + +/** + * Updates the value of the TxSubscriptionRef using a function and publishes the new + * value to all subscribers. + * + * **When to use** + * + * Use to derive the next `TxSubscriptionRef` value from the current value and + * publish it. + * + * **Example** (Updating a value) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(5) + * yield* TxSubscriptionRef.update(ref, (n) => n * 2) + * console.log(yield* TxSubscriptionRef.get(ref)) // 10 + * }) + * ``` + * + * @see {@link set} for replacing the value directly + * @see {@link updateAndGet} for returning the new value after the update + * + * @category mutations + * @since 3.10.0 + */ +export const update: { + (f: (current: A) => A): (self: TxSubscriptionRef) => Effect.Effect + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect +} = dual( + 2, + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect => + modify(self, (current) => [void 0, f(current)]) +) + +/** + * Gets the current value and sets a new value atomically. Publishes the new value + * to all subscribers. + * + * **When to use** + * + * Use to replace a `TxSubscriptionRef` value while returning the previous value + * and publishing the update to subscribers. + * + * **Example** (Getting and setting atomically) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make("a") + * const old = yield* TxSubscriptionRef.getAndSet(ref, "b") + * console.log(old) // "a" + * console.log(yield* TxSubscriptionRef.get(ref)) // "b" + * }) + * ``` + * + * @see {@link set} for setting without returning the previous value + * @see {@link getAndUpdate} for deriving the new value from the previous value + * + * @category mutations + * @since 3.10.0 + */ +export const getAndSet: { + (value: A): (self: TxSubscriptionRef) => Effect.Effect + (self: TxSubscriptionRef, value: A): Effect.Effect +} = dual( + 2, + (self: TxSubscriptionRef, value: A): Effect.Effect => modify(self, (current) => [current, value]) +) + +/** + * Gets the current value and updates it using a function atomically. Publishes + * the new value to all subscribers. + * + * **When to use** + * + * Use to derive and publish a new `TxSubscriptionRef` value while returning the + * previous value. + * + * **Example** (Getting and updating atomically) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(1) + * const old = yield* TxSubscriptionRef.getAndUpdate(ref, (n) => n + 10) + * console.log(old) // 1 + * console.log(yield* TxSubscriptionRef.get(ref)) // 11 + * }) + * ``` + * + * @see {@link update} for updating without returning the previous value + * @see {@link updateAndGet} for returning the new value instead + * + * @category mutations + * @since 3.10.0 + */ +export const getAndUpdate: { + (f: (current: A) => A): (self: TxSubscriptionRef) => Effect.Effect + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect +} = dual( + 2, + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect => + modify(self, (current) => [current, f(current)]) +) + +/** + * Updates the value using a function and returns the new value. Publishes the + * new value to all subscribers. + * + * **When to use** + * + * Use to derive and publish a new `TxSubscriptionRef` value while returning + * that new value. + * + * **Example** (Updating and reading atomically) + * + * ```ts + * import { Effect, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(3) + * const result = yield* TxSubscriptionRef.updateAndGet(ref, (n) => n * 3) + * console.log(result) // 9 + * }) + * ``` + * + * @see {@link update} for updating without returning the new value + * @see {@link getAndUpdate} for returning the previous value instead + * + * @category mutations + * @since 3.10.0 + */ +export const updateAndGet: { + (f: (current: A) => A): (self: TxSubscriptionRef) => Effect.Effect + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect +} = dual( + 2, + (self: TxSubscriptionRef, f: (current: A) => A): Effect.Effect => + modify(self, (current) => { + const newValue = f(current) + return [newValue, newValue] + }) +) + +// ============================================================================= +// Subscriptions +// ============================================================================= + +/** + * Subscribes to all changes of the TxSubscriptionRef. Returns a scoped TxDequeue + * that first yields the current value, then every subsequent update. + * + * **When to use** + * + * Use to subscribe to `TxSubscriptionRef` committed changes through a scoped + * transactional queue. + * + * **Example** (Subscribing to changes) + * + * ```ts + * import { Effect, TxQueue, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(0) + * + * yield* Effect.scoped( + * Effect.gen(function*() { + * const sub = yield* TxSubscriptionRef.changes(ref) + * const initial = yield* TxQueue.take(sub) + * console.log(initial) // 0 + * + * yield* TxSubscriptionRef.set(ref, 1) + * const next = yield* TxQueue.take(sub) + * console.log(next) // 1 + * }) + * ) + * }) + * ``` + * + * @see {@link changesStream} for subscribing through a `Stream` + * + * @category subscriptions + * @since 3.10.0 + */ +export const changes = ( + self: TxSubscriptionRef +): Effect.Effect, never, Scope.Scope> => + Effect.acquireRelease( + Effect.tx( + Effect.gen(function*() { + const sub = yield* TxPubSub.acquireSubscriber(self.pubsub) + const current = yield* TxRef.get(self.ref) + yield* TxQueue.offer(sub, current) + return sub + }) + ), + (queue) => Effect.tx(TxPubSub.releaseSubscriber(self.pubsub, queue)) + ) + +/** + * Returns a Stream of all changes to the TxSubscriptionRef, starting with the + * current value followed by every subsequent update. + * + * **When to use** + * + * Use to consume `TxSubscriptionRef` committed changes as a `Stream`. + * + * **Example** (Streaming changes) + * + * ```ts + * import { Effect, Stream, TxSubscriptionRef } from "effect" + * + * const program = Effect.gen(function*() { + * const ref = yield* TxSubscriptionRef.make(0) + * yield* TxSubscriptionRef.set(ref, 1) + * yield* TxSubscriptionRef.set(ref, 2) + * + * const values = yield* Stream.runCollect( + * TxSubscriptionRef.changesStream(ref).pipe(Stream.take(1)) + * ) + * console.log(values) // [2] + * }) + * ``` + * + * @see {@link changes} for subscribing through a transactional queue + * + * @category subscriptions + * @since 3.10.0 + */ +export const changesStream = (self: TxSubscriptionRef): Stream.Stream => + Stream.unwrap( + Effect.map( + changes(self), + (sub) => Stream.fromEffectRepeat(Effect.tx(TxQueue.take(sub))) + ) + ) + +// ============================================================================= +// Guards +// ============================================================================= + +/** + * Checks whether the given value is a TxSubscriptionRef. + * + * **When to use** + * + * Use to narrow an unknown value before treating it as a `TxSubscriptionRef`. + * + * **Example** (Checking transactional subscription references) + * + * ```ts + * import { TxSubscriptionRef } from "effect" + * + * declare const someValue: unknown + * + * if (TxSubscriptionRef.isTxSubscriptionRef(someValue)) { + * console.log("This is a TxSubscriptionRef") + * } + * ``` + * + * @see {@link make} for creating a `TxSubscriptionRef` + * + * @category guards + * @since 4.0.0 + */ +export const isTxSubscriptionRef = (u: unknown): u is TxSubscriptionRef => hasProperty(u, TypeId) diff --git a/repos/effect/packages/effect/src/Types.ts b/repos/effect/packages/effect/src/Types.ts new file mode 100644 index 0000000..94abd3a --- /dev/null +++ b/repos/effect/packages/effect/src/Types.ts @@ -0,0 +1,1121 @@ +/** + * Provides compile-time utility types for TypeScript. + * + * Everything in this module is type-level only; it does not define runtime + * values. The types are used throughout Effect to work with tuple lengths, + * object shapes, tagged unions, reason-tagged errors, mutability, exactness, + * required keys, concurrency settings, and variance markers. + * + * @since 4.0.0 + */ + +/** + * @category tuples + * @since 2.0.0 + */ +type TupleOf_> = `${N}` extends `-${number}` ? never + : R["length"] extends N ? R + : TupleOf_ + +/** + * Constructs a tuple type with exactly `N` elements of type `T`. + * + * **When to use** + * + * Use when you need a fixed-length array type, especially instead of manually + * writing `[T, T, T, ...]` for longer tuples. + * + * **Details** + * + * - If `N` is a literal number, produces a tuple of that exact length. + * - If `N` is the general `number` type (non-literal), degrades to `Array`. + * - Negative numbers produce `never`. + * + * **Example** (Checking fixed-length tuples) + * + * ```ts + * import type { Types } from "effect" + * + * // Exactly 3 numbers + * const triple: Types.TupleOf<3, number> = [1, 2, 3] + * + * // @ts-expect-error - too few elements + * const tooFew: Types.TupleOf<3, number> = [1, 2] + * + * // @ts-expect-error - too many elements + * const tooMany: Types.TupleOf<3, number> = [1, 2, 3, 4] + * ``` + * + * @see {@link TupleOfAtLeast} + * + * @category tuples + * @since 3.3.0 + */ +export type TupleOf = N extends N ? number extends N ? Array : TupleOf_ : never + +/** + * Constructs a tuple type with at least `N` elements of type `T`. + * + * **When to use** + * + * Use when you need a minimum-length array type that still allows additional + * elements. This is useful for variadic function signatures that require a + * minimum arity. + * + * **Details** + * + * Produces a tuple with `N` fixed positions followed by `...Array`. + * + * **Example** (Checking minimum-length tuples) + * + * ```ts + * import type { Types } from "effect" + * + * // At least 2 strings + * const ok1: Types.TupleOfAtLeast<2, string> = ["a", "b"] + * const ok2: Types.TupleOfAtLeast<2, string> = ["a", "b", "c", "d"] + * + * // @ts-expect-error - too few elements + * const bad: Types.TupleOfAtLeast<2, string> = ["a"] + * ``` + * + * @see {@link TupleOf} + * + * @category tuples + * @since 3.3.0 + */ +export type TupleOfAtLeast = [...TupleOf, ...Array] + +/** + * Extracts the `_tag` string literal types from a union. + * + * **When to use** + * + * Use to get all discriminant values from a tagged union type. + * + * **Details** + * + * Members without a `_tag` field are ignored and produce `never`. + * + * **Example** (Extracting tags) + * + * ```ts + * import type { Types } from "effect" + * + * type MyError = + * | { readonly _tag: "NotFound"; readonly id: string } + * | { readonly _tag: "Timeout"; readonly ms: number } + * | string + * + * type Result = Types.Tags + * // "NotFound" | "Timeout" + * ``` + * + * @see {@link ExtractTag} + * @see {@link ExcludeTag} + * + * @category types + * @since 2.0.0 + */ +export type Tags = E extends { readonly _tag: string } ? E["_tag"] : never + +/** + * Excludes members of a tagged union by their `_tag` value. + * + * **When to use** + * + * Use to remove tagged-union members whose `_tag` matches a specific value in + * type-level code. + * + * **Details** + * + * Non-tagged members of the union are preserved. + * + * **Example** (Removing a variant) + * + * ```ts + * import type { Types } from "effect" + * + * type MyError = + * | { readonly _tag: "NotFound"; readonly id: string } + * | { readonly _tag: "Timeout"; readonly ms: number } + * | string + * + * type WithoutTimeout = Types.ExcludeTag + * // { readonly _tag: "NotFound"; readonly id: string } | string + * ``` + * + * @see {@link ExtractTag} + * @see {@link Tags} + * + * @category types + * @since 2.0.0 + */ +export type ExcludeTag = Exclude + +/** + * Extracts a specific member of a tagged union by its `_tag` value. + * + * **When to use** + * + * Use to select tagged-union members whose `_tag` matches a specific value in + * type-level code. + * + * **Details** + * + * Returns `never` if no member matches the tag. + * + * **Example** (Extracting a variant) + * + * ```ts + * import type { Types } from "effect" + * + * type MyError = + * | { readonly _tag: "NotFound"; readonly id: string } + * | { readonly _tag: "Timeout"; readonly ms: number } + * + * type TimeoutError = Types.ExtractTag + * // { readonly _tag: "Timeout"; readonly ms: number } + * ``` + * + * @see {@link ExcludeTag} + * @see {@link Tags} + * + * @category types + * @since 2.0.0 + */ +export type ExtractTag = E extends { readonly _tag: infer T } ? K extends T ? E : never : never + +/** + * Transforms a union type into an intersection type. + * + * **When to use** + * + * Use to combine all members of a union into a single type with all their + * properties. This is useful in advanced generic code where you need to merge + * union variants. + * + * **Details** + * + * - Uses distributive conditional types and contra-variant inference. + * - If the union members are incompatible (e.g. `string | number`), the + * result is `never`. + * + * **Example** (Converting a union to an intersection) + * + * ```ts + * import type { Types } from "effect" + * + * type Union = { a: string } | { b: number } + * type Result = Types.UnionToIntersection + * // { a: string } & { b: number } + * ``` + * + * @see {@link IsUnion} + * + * @category types + * @since 2.0.0 + */ +export type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R + : never + +/** + * Flattens an intersection type into a single object type for readability. + * + * **When to use** + * + * Use to clean up IDE tooltips that show `A & B & C` instead of a merged + * object. + * + * **Details** + * + * Does not change the type semantically, only its display. + * + * **Example** (Simplifying an intersection) + * + * ```ts + * import type { Types } from "effect" + * + * // Without Simplify: IDE shows { a: number } & { b: string } + * // With Simplify: IDE shows { a: number; b: string } + * type Clean = Types.Simplify<{ a: number } & { b: string }> + * ``` + * + * @see {@link MergeLeft} + * @see {@link MergeRight} + * + * @category types + * @since 2.0.0 + */ +export type Simplify = { + [K in keyof A]: A[K] +} extends infer B ? B : never + +/** + * Determines if two types are exactly equal at the type level. + * + * **When to use** + * + * Use to assert type equality in conditional types or type-level tests. + * + * **Details** + * + * - Uses the `() => T extends X ? 1 : 2` trick for exact equality, + * distinguishing between `any`, `unknown`, `never`, and other types. + * - Resolves to `true` if `X` and `Y` are identical, `false` otherwise. + * + * **Example** (Checking type equality) + * + * ```ts + * import type { Types } from "effect" + * + * type Yes = Types.Equals<{ a: number }, { a: number }> // true + * type No = Types.Equals<{ a: number }, { a: string }> // false + * type AnyCheck = Types.Equals // false + * ``` + * + * @see {@link EqualsWith} + * + * @category models + * @since 2.0.0 + */ +export type Equals = (() => T extends X ? 1 : 2) extends < + T +>() => T extends Y ? 1 : 2 ? true + : false + +/** + * Determines if two types are equal, returning custom types for each case. + * + * **When to use** + * + * Use when you need a type-level if/else based on type equality. + * + * **Details** + * + * Returns `Y` when `A` and `B` are equal, `N` otherwise. + * + * **Example** (Choosing a conditional type based on equality) + * + * ```ts + * import type { Types } from "effect" + * + * type R1 = Types.EqualsWith // "same" + * type R2 = Types.EqualsWith // "diff" + * ``` + * + * @see {@link Equals} + * + * @category models + * @since 3.15.0 + */ +export type EqualsWith = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? Y : N + +/** + * Checks whether an object type contains any of the specified keys. + * + * **When to use** + * + * Use to branch type-level logic when at least one key from a candidate key set + * exists on an object type. + * + * **Details** + * + * Returns `true` if at least one key from `Key` exists in `A`, `false` + * otherwise. + * + * **Example** (Checking key presence) + * + * ```ts + * import type { Types } from "effect" + * + * type Yes = Types.Has<{ a: number; b: string }, "a" | "c"> // true + * type No = Types.Has<{ a: number }, "b" | "c"> // false + * ``` + * + * @category models + * @since 2.0.0 + */ +export type Has = (Key extends infer K ? K extends keyof A ? true : never : never) extends never + ? false + : true + +/** + * Left-biased merge of two object types where keys from `Source` take + * precedence over `Target` on conflict. + * + * **When to use** + * + * Use when you want left-biased merging where the first argument wins. + * + * **Details** + * + * Implemented as `MergeRight`. + * + * **Example** (Merging with left bias) + * + * ```ts + * import type { Types } from "effect" + * + * type Result = Types.MergeLeft< + * { a: number; b: number }, + * { a: string; c: boolean } + * > + * // { a: number; b: number; c: boolean } + * ``` + * + * @see {@link MergeRight} + * @see {@link Simplify} + * + * @category models + * @since 2.0.0 + */ +export type MergeLeft = MergeRight + +/** + * Right-biased merge of two object types where keys from `Source` take + * precedence over `Target` on conflict. + * + * **When to use** + * + * Use when you want right-biased merging where the second argument wins. + * + * **Details** + * + * The result is automatically simplified via {@link Simplify}. + * + * **Example** (Right-biased merge) + * + * ```ts + * import type { Types } from "effect" + * + * type Result = Types.MergeRight< + * { a: number; b: number }, + * { a: string; c: boolean } + * > + * // { a: string; b: number; c: boolean } + * ``` + * + * @see {@link MergeLeft} + * @see {@link Simplify} + * + * @category models + * @since 2.0.0 + */ +export type MergeRight = Simplify< + & Source + & { + [Key in keyof Target as Key extends keyof Source ? never : Key]: Target[Key] + } +> + +/** + * Describes the concurrency level for Effect operations that run multiple + * effects. + * + * **When to use** + * + * Use to type options that control how many effects may run at the same time. + * + * **Details** + * + * - `number` — run at most N effects concurrently. + * - `"unbounded"` — run all effects concurrently with no limit. + * - `"inherit"` — inherit the concurrency from the surrounding context. + * + * **Example** (Setting concurrency values) + * + * ```ts + * import type { Types } from "effect" + * + * const sequential: Types.Concurrency = 1 + * const limited: Types.Concurrency = 5 + * const unbounded: Types.Concurrency = "unbounded" + * const inherit: Types.Concurrency = "inherit" + * ``` + * + * @category models + * @since 2.0.0 + */ +export type Concurrency = number | "unbounded" | "inherit" + +/** + * Removes `readonly` from all properties of `T`. Supports arrays, tuples, + * and records. + * + * **When to use** + * + * Use when you need a mutable version of a readonly type. + * + * **Details** + * + * Only affects the top level; nested properties remain readonly. + * + * **Example** (Converting shallowly to mutable types) + * + * ```ts + * import type { Types } from "effect" + * + * type Obj = Types.Mutable<{ + * readonly a: string + * readonly b: ReadonlyArray + * }> + * // { a: string; b: ReadonlyArray } + * // ^ mutable ^ still readonly inside + * + * type Arr = Types.Mutable> + * // string[] + * + * type Tup = Types.Mutable + * // [string, number] + * ``` + * + * @see {@link DeepMutable} + * + * @category types + * @since 2.0.0 + */ +export type Mutable = { + -readonly [P in keyof T]: T[P] +} + +/** + * Recursively removes `readonly` from all properties, including nested + * objects, arrays, `Map`, and `Set`. + * + * **When to use** + * + * Use when you need a fully mutable version of a deeply readonly type. + * + * **Details** + * + * Recursion stops at primitives (`string`, `number`, `boolean`, `bigint`, + * `symbol`) and functions. + * + * **Example** (Converting deeply to mutable types) + * + * ```ts + * import type { Types } from "effect" + * + * type Deep = Types.DeepMutable<{ + * readonly a: string + * readonly b: ReadonlyArray<{ readonly c: number }> + * }> + * // { a: string; b: Array<{ c: number }> } + * ``` + * + * @see {@link Mutable} + * + * @category types + * @since 3.1.0 + */ +export type DeepMutable = T extends ReadonlyMap ? Map, DeepMutable> + : T extends ReadonlySet ? Set> + : T extends string | number | boolean | bigint | symbol | Function ? T + : { -readonly [K in keyof T]: DeepMutable } + +/** + * Prevents TypeScript from inferring a type parameter from a specific + * position. + * + * **When to use** + * + * Use when a function parameter must match an inferred type without becoming + * an inference source. + * + * **Details** + * + * The parameter using `NoInfer` must still match the inferred type. + * + * **Example** (Controlling inference) + * + * ```ts + * import type { Types } from "effect" + * + * declare function withDefault(value: T, fallback: Types.NoInfer): T + * + * // T is inferred as "a" | "b" from the first argument only + * const result = withDefault<"a" | "b">("a", "b") + * ``` + * + * @category models + * @since 2.0.0 + */ +export type NoInfer = [A][A extends any ? 0 : never] + +/** + * Function-type alias encoding invariant variance for a phantom type + * parameter. + * + * **When to use** + * + * Use as a phantom field type to make a type parameter invariant, neither + * covariant nor contravariant. + * + * **Details** + * + * A value of type `Invariant` cannot be assigned to `Invariant` unless + * `A` and `B` are the same type. + * + * **Example** (Defining an invariant phantom type) + * + * ```ts + * import type { Types } from "effect" + * + * interface Container { + * readonly _phantom: Types.Invariant + * readonly value: T + * } + * ``` + * + * @see {@link Invariant.Type} + * @see {@link Covariant} + * @see {@link Contravariant} + * + * @category models + * @since 2.0.0 + */ +export type Invariant = (_: A) => A + +/** + * Namespace for {@link Invariant}-related utilities. + * + * **When to use** + * + * Use when referring to type-level helpers nested under `Invariant`. + * + * @since 3.9.0 + */ +export declare namespace Invariant { + /** + * Extracts the type parameter `A` from an `Invariant`. + * + * **When to use** + * + * Use to recover the carried type from an invariant phantom marker. + * + * **Example** (Extracting the inner type) + * + * ```ts + * import type { Types } from "effect" + * + * type Inner = Types.Invariant.Type> + * // number + * ``` + * + * @see {@link Invariant} + * + * @category models + * @since 3.9.0 + */ + export type Type = A extends Invariant ? U : never +} + +/** + * Function-type alias encoding covariant variance for a phantom type + * parameter. + * + * **When to use** + * + * Use as a phantom field type to make a type parameter covariant in output + * position. + * + * **Details** + * + * `Covariant` is assignable to `Covariant` when `A extends B`, following + * the subtype direction. + * + * **Example** (Defining a covariant phantom type) + * + * ```ts + * import type { Types } from "effect" + * + * interface Producer { + * readonly _phantom: Types.Covariant + * readonly get: () => T + * } + * ``` + * + * @see {@link Covariant.Type} + * @see {@link Contravariant} + * @see {@link Invariant} + * + * @category models + * @since 2.0.0 + */ +export type Covariant = (_: never) => A + +/** + * Namespace for {@link Covariant}-related utilities. + * + * **When to use** + * + * Use when referring to type-level helpers nested under `Covariant`. + * + * @since 3.9.0 + */ +export declare namespace Covariant { + /** + * Extracts the type parameter `A` from a `Covariant`. + * + * **When to use** + * + * Use to recover the carried type from a covariant phantom marker. + * + * **Example** (Extracting the inner type) + * + * ```ts + * import type { Types } from "effect" + * + * type Inner = Types.Covariant.Type> + * // string + * ``` + * + * @see {@link Covariant} + * + * @category models + * @since 3.9.0 + */ + export type Type = A extends Covariant ? U : never +} + +/** + * Function-type alias encoding contravariant variance for a phantom type + * parameter. + * + * **When to use** + * + * Use as a phantom field type to make a type parameter contravariant in input + * position. + * + * **Details** + * + * `Contravariant` is assignable to `Contravariant` when `B extends A`, + * following the supertype direction. + * + * **Example** (Defining a contravariant phantom type) + * + * ```ts + * import type { Types } from "effect" + * + * interface Consumer { + * readonly _phantom: Types.Contravariant + * readonly accept: (value: T) => void + * } + * ``` + * + * @see {@link Contravariant.Type} + * @see {@link Covariant} + * @see {@link Invariant} + * + * @category models + * @since 2.0.0 + */ +export type Contravariant = (_: A) => void + +/** + * Namespace for {@link Contravariant}-related utilities. + * + * **When to use** + * + * Use when referring to type-level helpers nested under `Contravariant`. + * + * @since 3.9.0 + */ +export declare namespace Contravariant { + /** + * Extracts the type parameter `A` from a `Contravariant`. + * + * **When to use** + * + * Use to recover the carried type from a contravariant phantom marker. + * + * **Example** (Extracting the inner type) + * + * ```ts + * import type { Types } from "effect" + * + * type Inner = Types.Contravariant.Type> + * // string + * ``` + * + * @see {@link Contravariant} + * + * @category models + * @since 3.9.0 + */ + export type Type = A extends Contravariant ? U : never +} + +/** + * Conditional type that returns `void` if `S` is an empty object type, + * otherwise returns `S`. + * + * **When to use** + * + * Use to erase an empty object type from an API result or parameter position. + * + * @category types + * @since 3.19.20 + */ +export type VoidIfEmpty = keyof S extends never ? void : S + +/** + * Excludes function types from a union, keeping only non-function members. + * + * **When to use** + * + * Use to filter out callable types from a union. + * + * **Details** + * + * Returns `never` if the entire union consists of function types. + * + * **Example** (Filtering out functions) + * + * ```ts + * import type { Types } from "effect" + * + * type Result = Types.NotFunction void) | number> + * // string | number + * ``` + * + * @category types + * @since 2.0.0 + */ +export type NotFunction = T extends Function ? never : T + +/** + * Constrains a type to prevent excess properties not present in `T`. + * + * **When to use** + * + * Use to catch accidental extra properties in generic functions at compile time. + * + * **Details** + * + * Extra keys from `U` that are not in `T` are mapped to `never`. + * + * **Example** (Preventing extra properties) + * + * ```ts + * import type { Types } from "effect" + * + * type Expected = { a: number; b: string } + * type Input = { a: number; b: string; c: boolean } + * + * type Result = Types.NoExcessProperties + * // { a: number; b: string; readonly c: never } + * ``` + * + * @category types + * @since 3.9.0 + */ +export type NoExcessProperties = T & Readonly, never>> + +/** + * Branded marker interface representing an unassigned type parameter. + * + * **When to use** + * + * Use when Effect's type-level machinery needs to represent a type parameter + * that has not been assigned yet. + * + * **Details** + * + * Used internally by the Effect type system to indicate that a type parameter + * has not been assigned a concrete type. + * + * @see {@link unhandled} + * + * @category types + * @since 4.0.0 + */ +export interface unassigned { + readonly _: unique symbol +} + +/** + * Branded marker interface representing an unhandled error type. + * + * **When to use** + * + * Use when Effect's type-level machinery needs to represent an error type that + * has not been handled yet. + * + * **Details** + * + * Used internally by the Effect type system to indicate that an error type + * has not been handled. + * + * @see {@link unassigned} + * + * @category types + * @since 4.0.0 + */ +export interface unhandled { + readonly _: unique symbol +} + +/** + * Checks whether a type `T` is a union type. + * + * **When to use** + * + * Use to branch type-level logic depending on whether a type is a union. + * + * **Details** + * + * - Compares `[T]` against `[UnionToIntersection]`. If they differ, `T` + * must be a union. + * - Returns `true` if `T` is a union of two or more members. + * - Returns `false` for single types, `never`, or `any`. + * + * **Example** (Detecting union types) + * + * ```ts + * import type { Types } from "effect" + * + * type Yes = Types.IsUnion<"a" | "b"> // true + * type No = Types.IsUnion // false + * ``` + * + * @see {@link UnionToIntersection} + * + * @category types + * @since 4.0.0 + */ +export type IsUnion = [T] extends [UnionToIntersection] ? false : true + +/** + * Extracts the `reason` type from an error that has a `reason` field. + * + * **When to use** + * + * Use when an error type stores nested sub-errors in a `reason` field and you + * need that field's full union type as a standalone type. + * + * **Details** + * + * Returns `never` if `E` has no `reason` field. + * + * **Example** (Extracting reason types) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Reasons = Types.ReasonOf + * // RateLimitError | QuotaError + * ``` + * + * @see {@link ReasonTags} + * @see {@link ExtractReason} + * @see {@link ExcludeReason} + * + * @category types + * @since 4.0.0 + */ +export type ReasonOf = E extends { readonly reason: infer R } ? R : never + +/** + * Extracts the `_tag` values from the `reason` type of an error. + * + * **When to use** + * + * Use to get the discriminant values available inside a nested `reason` + * error union. + * + * **Details** + * + * This is shorthand for `Tags>`. It returns `never` if `E` has no + * `reason` field or the reason has no `_tag`. + * + * **Example** (Getting reason tags) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Result = Types.ReasonTags + * // "RateLimitError" | "QuotaError" + * ``` + * + * @see {@link ReasonOf} + * @see {@link ExtractReason} + * + * @category types + * @since 4.0.0 + */ +export type ReasonTags = E extends { readonly reason: { readonly _tag: string } } ? E["reason"]["_tag"] + : never + +/** + * Extracts a specific reason variant by its `_tag` from an error's `reason` + * field. + * + * **When to use** + * + * Use when you need the nested reason variant type itself, selected by `_tag`, + * rather than the enclosing error type. + * + * **Details** + * + * Returns `never` if `E` has no matching reason variant. + * + * **Example** (Extracting a reason variant) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Result = Types.ExtractReason + * // { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * ``` + * + * @see {@link ExcludeReason} + * @see {@link ReasonOf} + * @see {@link ReasonTags} + * + * @category types + * @since 4.0.0 + */ +export type ExtractReason = E extends { readonly reason: infer R } + ? R extends { readonly _tag: infer T } ? K extends T ? R : never + : never + : never + +/** + * Narrows a specific reason variant by its `_tag` from an error's `reason` + * field. + * + * **When to use** + * + * Use to preserve the original error shape while narrowing its nested reason + * field to the matching variant. + * + * **Details** + * + * Returns `never` if `E` has no matching reason variant. + * + * **Example** (Narrowing a reason variant) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Result = Types.NarrowReason + * // ApiError & { readonly reason: { readonly _tag: "RateLimitError"; readonly retryAfter: number } } + * ``` + * + * @see {@link ExcludeReason} + * @see {@link ReasonOf} + * @see {@link ReasonTags} + * + * @category types + * @since 4.0.0 + */ +export type NarrowReason = E extends { readonly reason: infer R } + ? R extends { readonly _tag: infer T } ? K extends T ? E & { readonly reason: R } : never + : never + : never + +/** + * Narrows an error's `reason` field to exclude a specific reason variant by + * its `_tag`. + * + * **When to use** + * + * Use to narrow the error to only the remaining reason variants after + * excluding the matched one. + * + * **Details** + * + * Returns `never` if `E` has no `reason` field or no remaining variants. + * + * **Example** (Omitting a reason variant) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Result = Types.OmitReason + * // ApiError & { readonly reason: { readonly _tag: "QuotaError"; readonly limit: number } } + * ``` + * + * @see {@link NarrowReason} + * @see {@link ExcludeReason} + * @see {@link ReasonOf} + * @see {@link ReasonTags} + * + * @category types + * @since 4.0.0 + */ +export type OmitReason = E extends { readonly reason: infer R } + ? R extends { readonly _tag: infer T } ? K extends T ? never : E & { readonly reason: R } + : never + : never + +/** + * Excludes a specific reason variant by its `_tag` from an error's `reason` + * field. + * + * **When to use** + * + * Use when you need the remaining nested reason union type after removing + * variants handled by `_tag`, rather than the enclosing error type. + * + * **Details** + * + * Returns `never` if `E` has no `reason` field. + * + * **Example** (Excluding a reason variant) + * + * ```ts + * import type { Types } from "effect" + * + * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * type QuotaError = { readonly _tag: "QuotaError"; readonly limit: number } + * type ApiError = { readonly _tag: "ApiError"; readonly reason: RateLimitError | QuotaError } + * + * type Result = Types.ExcludeReason + * // { readonly _tag: "QuotaError"; readonly limit: number } + * ``` + * + * @see {@link ExtractReason} + * @see {@link ReasonOf} + * @see {@link ReasonTags} + * + * @category types + * @since 4.0.0 + */ +export type ExcludeReason = E extends { readonly reason: infer R } + ? Exclude + : never + +/** + * Extracts the required keys from a type. + * + * **When to use** + * + * Use to derive the keys whose properties must be present on an object type. + * + * @category types + * @since 4.0.0 + */ +export type RequiredKeys = { [K in keyof T]-?: {} extends Pick ? never : K }[keyof T] diff --git a/repos/effect/packages/effect/src/UndefinedOr.ts b/repos/effect/packages/effect/src/UndefinedOr.ts new file mode 100644 index 0000000..eb9be88 --- /dev/null +++ b/repos/effect/packages/effect/src/UndefinedOr.ts @@ -0,0 +1,239 @@ +/** + * Works with values that may be `undefined`. + * + * Use this module for plain TypeScript values of type `A | undefined` when + * `undefined` is the only absence marker. It is a small alternative to wrapping + * values in `Option` when your data already uses `undefined` to mean "no + * value". The module includes helpers for mapping defined values, matching both + * cases, throwing when a value is missing, adapting throwing functions, and + * building reducers or combiners. + * + * @since 4.0.0 + */ +import * as Combiner from "./Combiner.ts" +import type { LazyArg } from "./Function.ts" +import { dual } from "./Function.ts" +import * as Reducer from "./Reducer.ts" + +/** + * Maps a defined value with `f`, or returns `undefined` unchanged. + * + * **When to use** + * + * Use to apply a pure transformation to an `A | undefined` value while + * preserving `undefined` as absence. + * + * @see {@link match} when you need to handle the `undefined` case explicitly + * + * @category mapping + * @since 4.0.0 + */ +export const map: { + (f: (a: A) => B): (self: A | undefined) => B | undefined + (self: A | undefined, f: (a: A) => B): B | undefined +} = dual(2, (self, f) => (self === undefined ? undefined : f(self))) + +/** + * Pattern matches on an `A | undefined` value, running `onDefined` when the + * value is present or evaluating `onUndefined` when the value is `undefined`. + * + * **When to use** + * + * Use when you need to turn an `A | undefined` into a non-optional result by + * handling both the defined and undefined branches in one expression. + * + * @see {@link map} for transforming defined values while preserving `undefined` + * @see {@link getOrThrowWith} for throwing when the value is `undefined` instead of returning a fallback branch + * + * @category pattern matching + * @since 4.0.0 + */ +export const match: { + (options: { + readonly onUndefined: LazyArg + readonly onDefined: (a: A) => C + }): (self: A | undefined) => B | C + (self: A | undefined, options: { + readonly onUndefined: LazyArg + readonly onDefined: (a: A) => C + }): B | C +} = dual( + 2, + (self: A | undefined, { onDefined, onUndefined }: { + readonly onUndefined: LazyArg + readonly onDefined: (a: A) => C + }): B | C => self === undefined ? onUndefined() : onDefined(self) +) + +/** + * Returns the defined value, or throws the value produced by `onUndefined` + * when the input is `undefined`. + * + * **When to use** + * + * Use when you need fail-fast unwrapping of an `A | undefined` value and want + * to provide the thrown error for the undefined case. + * + * **Details** + * + * Defined values are returned unchanged. When the input is `undefined`, + * `onUndefined` is called and its result is thrown. + * + * @see {@link getOrThrow} for the default-error sibling + * @see {@link match} for handling defined and undefined cases without throwing + * + * @category getters + * @since 4.0.0 + */ +export const getOrThrowWith: { + (onUndefined: () => unknown): (self: A | undefined) => A + (self: A | undefined, onUndefined: () => unknown): A +} = dual(2, (self: A | undefined, onUndefined: () => unknown): A => { + if (self !== undefined) { + return self + } + throw onUndefined() +}) + +/** + * Returns the defined value, or throws a default `Error` when the input is + * `undefined`. + * + * **When to use** + * + * Use when you need to unwrap a value that should already be defined and a + * generic missing-value `Error` is acceptable. + * + * **Details** + * + * Defined inputs are returned unchanged. `undefined` throws + * `new Error("getOrThrow called on a undefined")`. + * + * @see {@link getOrThrowWith} for the sibling that lets callers choose the thrown value + * @see {@link match} for handling defined and undefined cases without throwing + * + * @category getters + * @since 4.0.0 + */ +export const getOrThrow: (self: A | undefined) => A = getOrThrowWith(() => + new Error("getOrThrow called on a undefined") +) + +/** + * Converts a throwing function into one that returns successful results + * unchanged and returns `undefined` when the function throws. + * + * **When to use** + * + * Use to adapt exception-throwing functions when `undefined` is the absence + * value you want to return for failures. + * + * **Gotchas** + * + * Thrown values are discarded. If the wrapped function can successfully return + * `undefined`, that success is indistinguishable from a thrown failure. + * + * @category converting + * @since 4.0.0 + */ +export const liftThrowable = , B>( + f: (...a: A) => B +): (...a: A) => B | undefined => +(...a) => { + try { + return f(...a) + } catch { + return undefined + } +} + +/** + * Creates a `Reducer` for `UndefinedOr` that prioritizes the first non-`undefined` + * value and combines values when both operands are present. + * + * **When to use** + * + * Use when you need to reduce values that may be `undefined`, keeping the + * first defined value as a fallback and combining only when both operands are + * defined. + * + * **Details** + * + * Combining `undefined` with `undefined` returns `undefined`. Combining a + * defined value with `undefined` keeps the defined value, so the first defined + * value wins when only one side is present. When both values are defined, they + * are combined with `combiner.combine`. The reducer's initial value is + * `undefined`. + * + * @category constructors + * @since 4.0.0 + */ +export function makeReducer(combiner: Combiner.Combiner): Reducer.Reducer { + return Reducer.make((self, that) => { + if (self === undefined) return that + if (that === undefined) return self + return combiner.combine(self, that) + }, undefined as A | undefined) +} + +/** + * Creates a `Combiner` for `A | undefined` that combines values only when both + * operands are defined. + * + * **When to use** + * + * Use to lift a `Combiner` so any `undefined` operand makes the combined result + * `undefined`. + * + * **Details** + * + * - `undefined` combined with any value returns `undefined` + * - Any value combined with `undefined` returns `undefined` + * - `a` combined with `b` returns `combiner.combine(a, b)` + * + * @see {@link makeReducerFailFast} if you have a `Reducer` and want to lift it + * to `UndefinedOr` values. + * + * @category constructors + * @since 4.0.0 + */ +export function makeCombinerFailFast(combiner: Combiner.Combiner): Combiner.Combiner { + return Combiner.make((self, that) => { + if (self === undefined || that === undefined) return undefined + return combiner.combine(self, that) + }) +} + +/** + * Creates a `Reducer` for `A | undefined` by wrapping an existing reducer with + * fail-fast semantics. + * + * **When to use** + * + * Use to wrap an existing `Reducer` so any `undefined` value aborts the entire + * reduction result. + * + * **Details** + * + * - Initial value is the wrapped reducer's `initialValue` + * - Combining two defined values delegates to the wrapped reducer + * - If the accumulator or next value is `undefined`, the reduction returns `undefined` + * + * @see {@link makeCombinerFailFast} if you only have a `Combiner` and want to + * lift it to `UndefinedOr` values. + * + * @category constructors + * @since 4.0.0 + */ +export function makeReducerFailFast(reducer: Reducer.Reducer): Reducer.Reducer { + const combine = makeCombinerFailFast(reducer).combine + const initialValue = reducer.initialValue as A | undefined + return Reducer.make(combine, initialValue, (collection) => { + let out = initialValue + for (const value of collection) { + out = combine(out, value) + if (out === undefined) return out + } + return out + }) +} diff --git a/repos/effect/packages/effect/src/Unify.ts b/repos/effect/packages/effect/src/Unify.ts new file mode 100644 index 0000000..080b1be --- /dev/null +++ b/repos/effect/packages/effect/src/Unify.ts @@ -0,0 +1,314 @@ +/** + * Defines Effect's type-level unification protocol. + * + * Unification collapses unions of protocol-enabled values into their public data + * types. It is mostly for maintainers of Effect data types and advanced library + * authors; application code usually benefits from it through APIs such as + * `Effect`, `Option`, `Result`, `Stream`, `Layer`, and `Match`. This module + * exports the protocol symbols, the `Unify` type that performs normalization, + * and `unify`, an identity function that changes only the inferred type. + * + * @since 2.0.0 + */ + +import { identity } from "./Function.ts" + +/** + * Defines the unique symbol used to identify unification behavior in Effect types. + * + * **When to use** + * + * Use to define the widened type produced by the `Unify` protocol for a custom + * protocol-enabled data type. + * + * **Details** + * + * This symbol is a type-level protocol key. It describes how a protocol-enabled + * type widens during unification and has no runtime behavior. + * + * @see {@link typeSymbol} for storing the source type information used during unification + * @see {@link ignoreSymbol} for excluding protocol entries from unification + * + * @category symbols + * @since 2.0.0 + */ +export declare const unifySymbol: unique symbol + +/** + * The type of the unifySymbol. + * + * **When to use** + * + * Use to reference the unification behavior property key in type-level + * protocol definitions. + * + * **Details** + * + * This type represents the unique symbol used for identifying unification + * behavior in Effect types. It's typically used in type-level operations + * to enable automatic type unification. + * + * @category symbols + * @since 2.0.0 + */ +export type unifySymbol = typeof unifySymbol + +/** + * Defines the unique symbol used to identify the type information for unification. + * + * **When to use** + * + * Use when you need a type-level protocol key that exposes the source type + * read by `Unify` from a protocol-enabled data type. + * + * **Details** + * + * This symbol is a type-level protocol key. It stores the source type that + * unification reads when widening protocol-enabled values. + * + * @see {@link unifySymbol} for defining how protocol entries widen + * + * @category symbols + * @since 2.0.0 + */ +export declare const typeSymbol: unique symbol + +/** + * The type of the typeSymbol. + * + * **When to use** + * + * Use to reference the type information property key in type-level protocol + * definitions. + * + * **Details** + * + * This type represents the unique symbol used for storing type information + * in types that support unification. It's used in type-level operations + * to access and manipulate type information. + * + * @category symbols + * @since 2.0.0 + */ +export type typeSymbol = typeof typeSymbol + +/** + * Defines the unique symbol used to specify types that should be ignored during unification. + * + * **When to use** + * + * Use to hide helper protocol entries from `Unify` when they should not + * contribute to the widened type. + * + * **Details** + * + * This symbol is a type-level protocol key. It lists protocol entries that + * unification should ignore when computing the widened type. + * + * @see {@link unifySymbol} for defining the protocol entries being filtered + * + * @category symbols + * @since 2.0.0 + */ +export declare const ignoreSymbol: unique symbol + +/** + * The type of the ignoreSymbol. + * + * **When to use** + * + * Use to reference the ignored-property key in type-level protocol + * definitions. + * + * **Details** + * + * This type represents the unique symbol used for marking types that should + * be ignored during unification operations. It's used in type-level operations + * to exclude specific types from the unification process. + * + * @category symbols + * @since 2.0.0 + */ +export type ignoreSymbol = typeof ignoreSymbol + +type MaybeReturn = F extends () => infer R ? R : NonNullable + +type Keys = X extends [infer A, infer Ignore] ? Exclude + : never + +type Values = X extends [infer A, infer Ignore] + ? Keys<[A, Ignore]> extends infer K ? K extends keyof A ? MaybeReturn : never : never + : never + +type Ignore = X extends { [ignoreSymbol]?: infer Obj } ? keyof NonNullable + : never + +type ExtractTypes< + X +> = X extends { + [typeSymbol]?: infer _Type + [unifySymbol]?: infer _Unify +} ? [NonNullable<_Unify>, Ignore] + : never + +type FilterIn = A extends any ? typeSymbol extends keyof A ? A : never : never + +type FilterInUnmatched = A extends any + ? typeSymbol extends keyof A + ? A extends { [unifySymbol]?: infer U } ? [Extract, K>] extends [never] ? A : never + : A + : never + : never + +type FilterOut = A extends any ? typeSymbol extends keyof A ? never : A : never + +/** + * Unifies types that implement the unification protocol. + * + * **When to use** + * + * Use to normalize unions of types that expose Effect's unification protocol. + * + * **Details** + * + * This type performs automatic type unification for types that contain + * the unification symbols (`unifySymbol`, `typeSymbol`, `ignoreSymbol`). + * It's primarily used internally by the Effect type system to handle + * complex type unions and provide better type inference. + * + * **Example** (Unifying protocol types) + * + * ```ts + * import type { Unify } from "effect" + * + * // Example of types that can be unified + * type UnifiableA = { + * value: string + * [Unify.typeSymbol]?: string + * [Unify.unifySymbol]?: { String: () => string } + * } + * + * type UnifiableB = { + * value: number + * [Unify.typeSymbol]?: number + * [Unify.unifySymbol]?: { Number: () => number } + * } + * + * // Unify automatically handles the union + * type Unified = Unify.Unify + * // Results in a properly unified type + * ``` + * + * @see {@link unify} for applying this normalization to a value or function + * + * @category models + * @since 2.0.0 + */ +export type Unify = Values< + ExtractTypes< + ( + & FilterIn + & { [typeSymbol]: A } + ) + > +> extends infer Z ? + | Z + | FilterInUnmatched< + A, + Keys< + ExtractTypes< + ( + & FilterIn + & { [typeSymbol]: A } + ) + > + > + > + | FilterOut + : never + +/** + * Applies `Unify` to a value or function return type at compile time. + * + * **When to use** + * + * Use to keep a value or function unchanged at runtime while normalizing its + * inferred type with Effect's unification protocol. + * + * **Details** + * + * This is an identity function at runtime. For functions, the returned function + * has the same runtime behavior while its return type is normalized with the + * Effect unification protocol. + * + * **Example** (Unifying values and function results) + * + * ```ts + * import { Unify } from "effect" + * + * // Unify a simple value + * const unifiedValue = Unify.unify("hello") + * // Type: string + * + * // Unify a function result + * const createUnifiableValue = () => ({ + * value: "test", + * [Unify.typeSymbol]: "string" as const, + * [Unify.unifySymbol]: { String: () => "test" as const } + * }) + * + * const unifiedFunction = Unify.unify(createUnifiableValue) + * // The result will be properly unified + * + * // Unify with curried functions + * const curriedFunction = (a: string) => (b: number) => ({ result: a + b }) + * const unifiedCurried = Unify.unify(curriedFunction) + * // Type: (a: string) => (b: number) => Unify<{ result: string }> + * ``` + * + * @see {@link Unify} for the type-level normalization applied by this helper + * + * @category utility types + * @since 2.0.0 + */ +export const unify: { + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + Args4 extends Array, + Args5 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => (...args: Args5) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => (...args: Args5) => Unify + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + Args4 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => (...args: Args4) => Unify + < + Args extends Array, + Args2 extends Array, + Args3 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => (...args: Args3) => T + ): (...args: Args) => (...args: Args2) => (...args: Args3) => Unify + < + Args extends Array, + Args2 extends Array, + T + >( + x: (...args: Args) => (...args: Args2) => T + ): (...args: Args) => (...args: Args2) => Unify + < + Args extends Array, + T + >(x: (...args: Args) => T): (...args: Args) => Unify + (x: T): Unify +} = identity as any diff --git a/repos/effect/packages/effect/src/Utils.ts b/repos/effect/packages/effect/src/Utils.ts new file mode 100644 index 0000000..0886e5f --- /dev/null +++ b/repos/effect/packages/effect/src/Utils.ts @@ -0,0 +1,221 @@ +/** + * Internal and advanced utilities used by Effect's generator-based syntax and + * higher-kinded type support. This is not a general-purpose utility module for + * application code. + * + * `SingleShotGen` makes an Effect-style value work with `yield*` inside + * generator helpers. `Variance` and `Gen` provide the type-level signatures + * used by modules such as `Effect`, `Option`, and `Result` to type their + * `gen` APIs. + * + * @since 2.0.0 + */ +import type { Kind, TypeLambda } from "./HKT.ts" +import type * as Types from "./Types.ts" + +/** + * Yields its wrapped value exactly once through an `IterableIterator`. + * + * **When to use** + * + * Use to implement `[Symbol.iterator]()` on Effect-like types so they can be + * `yield*`-ed inside generator functions, such as `Effect.gen` and + * `Option.gen`. + * + * **Details** + * + * The first call to `next()` returns `{ value: self, done: false }`. Every + * subsequent call returns `{ value: a, done: true }` where `a` is the argument + * passed to `next()`. `[Symbol.iterator]()` returns a **new** `SingleShotGen` + * wrapping the same value, so the outer type can be iterated multiple times. + * + * **Example** (Yielding a wrapped value in a generator) + * + * ```ts + * import { Utils } from "effect" + * + * const gen = new Utils.SingleShotGen("hello") + * + * // First call yields the wrapped value + * console.log(gen.next(0)) + * // { value: "hello", done: false } + * + * // Second call signals completion with the provided value + * console.log(gen.next(42)) + * // { value: 42, done: true } + * ``` + * + * @see {@link Gen} for the type-level signature that relies on `SingleShotGen` + * @category constructors + * @since 2.0.0 + */ +export class SingleShotGen implements IterableIterator { + private called = false + readonly self: T + + constructor(self: T) { + this.self = self + } + + /** + * Yields the stored value once, then completes with the value sent back in. + * + * **When to use** + * + * Use to advance a `SingleShotGen` through its single yield and completion + * step. + * + * @since 2.0.0 + */ + next(a: A): IteratorResult { + return this.called ? + ({ + value: a, + done: true + }) : + (this.called = true, + ({ + value: this.self, + done: false + })) + } + + /** + * Creates a fresh single-shot iterator over the stored value. + * + * **When to use** + * + * Use to iterate the wrapped value again without reusing the consumed + * iterator state. + * + * @since 2.0.0 + */ + [Symbol.iterator](): IterableIterator { + return new SingleShotGen(this.self) + } +} + +/** + * Type-level marker encoding the variance of a `TypeLambda`'s type + * parameters. + * + * **When to use** + * + * Use to define variance constraints for a higher-kinded type so that + * {@link Gen} can correctly infer `R`, `O`, and `E` from yielded values. + * + * **Details** + * + * `F` is invariant and must match exactly. `R` is contravariant in the input + * or environment position. `O` and `E` are covariant in the output and error + * positions. This is a pure type-level construct with no runtime + * representation. + * + * **Example** (Declaring variance for a TypeLambda) + * + * ```ts + * import type { Option, Utils } from "effect" + * + * declare const variance: Utils.Variance< + * Option.OptionTypeLambda, + * never, + * never, + * never + * > + * ``` + * + * @see {@link Gen} for the type-level signature that uses `Variance` + * @category models + * @since 2.0.0 + */ +export interface Variance { + readonly _F: Types.Invariant + readonly _R: Types.Contravariant + readonly _O: Types.Covariant + readonly _E: Types.Covariant +} + +/** + * Type-level signature for generator-based monadic composition over any + * `TypeLambda`. + * + * **When to use** + * + * Use to type the `gen` function of a module that supports generator syntax, + * such as `Option.gen`, `Result.gen`, and `Effect.gen`. + * + * **Details** + * + * This is a pure type alias with no runtime behavior. It infers `R`, `O`, and + * `E` from the yielded values via {@link Variance} or `Kind` constraints. The + * generator's return type `A` becomes the output's `A` parameter. + * + * **Example** (Typing a gen function for Option) + * + * ```ts + * import type { Option, Utils } from "effect" + * + * declare const gen: Utils.Gen + * ``` + * + * @see {@link Variance} for encoding the variance used for inference + * @see {@link SingleShotGen} for the iterator protocol that makes yielding work + * @category models + * @since 2.0.0 + */ +export type Gen = < + Self, + K extends Variance | Kind, + A +>( + ...args: + | [ + self: Self, + body: (this: Self) => Generator + ] + | [ + body: () => Generator + ] +) => Kind< + F, + [K] extends [Variance] ? R + : [K] extends [Kind] ? R + : never, + [K] extends [Variance] ? O + : [K] extends [Kind] ? O + : never, + [K] extends [Variance] ? E + : [K] extends [Kind] ? E + : never, + A +> + +// the probe is wrapped in a single function call (rather than module-level +// statements) so the whole selection is pure-annotated by the build and +// tree-shakable when `internalCall` is unused. +const pickInternalCall = (): (body: () => A) => A => { + const InternalTypeId = "~effect/Utils/internal" + + const standard = { + [InternalTypeId]: (body: () => A) => { + return body() + } + } + + const forced = { + [InternalTypeId]: (body: () => A) => { + try { + return body() + } finally { + // + } + } + } + + const isNotOptimizedAway = standard[InternalTypeId](() => new Error().stack)?.includes(InternalTypeId) === true + + return isNotOptimizedAway ? standard[InternalTypeId] : forced[InternalTypeId] +} + +/** @internal */ +export const internalCall = pickInternalCall() diff --git a/repos/effect/packages/effect/src/index.ts b/repos/effect/packages/effect/src/index.ts new file mode 100644 index 0000000..8b2a9e3 --- /dev/null +++ b/repos/effect/packages/effect/src/index.ts @@ -0,0 +1,722 @@ +/** + * @since 2.0.0 + */ + +export { + /** + * @since 2.0.0 + */ + absurd, + /** + * @since 2.0.0 + */ + cast, + /** + * @since 2.0.0 + */ + flow, + /** + * @since 2.0.0 + */ + hole, + /** + * @since 2.0.0 + */ + identity, + /** + * @since 2.0.0 + */ + pipe +} from "./Function.ts" + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 2.0.0 + */ +export * as Array from "./Array.ts" + +/** + * @since 2.0.0 + */ +export * as BigDecimal from "./BigDecimal.ts" + +/** + * @since 2.0.0 + */ +export * as BigInt from "./BigInt.ts" + +/** + * @since 2.0.0 + */ +export * as Boolean from "./Boolean.ts" + +/** + * @since 2.0.0 + */ +export * as Brand from "./Brand.ts" + +/** + * @since 4.0.0 + */ +export * as Cache from "./Cache.ts" + +/** + * @since 2.0.0 + */ +export * as Cause from "./Cause.ts" + +/** + * @since 2.0.0 + */ +export * as Channel from "./Channel.ts" + +/** + * @since 4.0.0 + */ +export * as ChannelSchema from "./ChannelSchema.ts" + +/** + * @since 2.0.0 + */ +export * as Chunk from "./Chunk.ts" + +/** + * @since 2.0.0 + */ +export * as Clock from "./Clock.ts" + +/** + * @since 4.0.0 + */ +export * as Combiner from "./Combiner.ts" + +/** + * @since 4.0.0 + */ +export * as Config from "./Config.ts" + +/** + * @since 4.0.0 + */ +export * as ConfigProvider from "./ConfigProvider.ts" + +/** + * @since 2.0.0 + */ +export * as Console from "./Console.ts" + +/** + * @since 4.0.0 + */ +export * as Context from "./Context.ts" + +/** + * @since 2.0.0 + */ +export * as Cron from "./Cron.ts" + +/** + * @since 4.0.0 + */ +export * as Crypto from "./Crypto.ts" + +/** + * @since 2.0.0 + */ +export * as Data from "./Data.ts" + +/** + * @since 3.6.0 + */ +export * as DateTime from "./DateTime.ts" + +/** + * @since 2.0.0 + */ +export * as Deferred from "./Deferred.ts" + +/** + * @since 4.0.0 + */ +export * as Differ from "./Differ.ts" + +/** + * @since 2.0.0 + */ +export * as Duration from "./Duration.ts" + +/** + * @since 2.0.0 + */ +export * as Effect from "./Effect.ts" + +/** + * @since 4.0.0 + */ +export * as Effectable from "./Effectable.ts" + +/** + * @since 4.0.0 + */ +export * as Encoding from "./Encoding.ts" + +/** + * @since 2.0.0 + */ +export * as Equal from "./Equal.ts" + +/** + * @since 2.0.0 + */ +export * as Equivalence from "./Equivalence.ts" + +/** + * @since 4.0.0 + */ +export * as ErrorReporter from "./ErrorReporter.ts" + +/** + * @since 3.16.0 + */ +export * as ExecutionPlan from "./ExecutionPlan.ts" + +/** + * @since 2.0.0 + */ +export * as Exit from "./Exit.ts" + +/** + * @since 2.0.0 + */ +export * as Fiber from "./Fiber.ts" + +/** + * @since 2.0.0 + */ +export * as FiberHandle from "./FiberHandle.ts" + +/** + * @since 2.0.0 + */ +export * as FiberMap from "./FiberMap.ts" + +/** + * @since 2.0.0 + */ +export * as FiberSet from "./FiberSet.ts" + +/** + * @since 4.0.0 + */ +export * as FileSystem from "./FileSystem.ts" + +/** + * @since 4.0.0 + */ +export * as Filter from "./Filter.ts" + +/** + * @since 4.0.0 + */ +export * as Formatter from "./Formatter.ts" + +/** + * @since 2.0.0 + */ +export * as Function from "./Function.ts" + +/** + * @since 4.0.0 + */ +export * as Graph from "./Graph.ts" + +/** + * @since 2.0.0 + */ +export * as Hash from "./Hash.ts" + +/** + * @since 2.0.0 + */ +export * as HashMap from "./HashMap.ts" + +/** + * @since 4.0.0 + */ +export * as HashRing from "./HashRing.ts" + +/** + * @since 2.0.0 + */ +export * as HashSet from "./HashSet.ts" + +/** + * @since 2.0.0 + */ +export * as HKT from "./HKT.ts" + +/** + * @since 2.0.0 + */ +export * as Inspectable from "./Inspectable.ts" + +/** + * @since 2.0.0 + */ +export * as Iterable from "./Iterable.ts" + +/** + * @since 4.0.0 + */ +export * as JsonPatch from "./JsonPatch.ts" + +/** + * @since 4.0.0 + */ +export * as JsonPointer from "./JsonPointer.ts" + +/** + * @since 4.0.0 + */ +export * as JsonSchema from "./JsonSchema.ts" + +/** + * @since 4.0.0 + */ +export * as Latch from "./Latch.ts" + +/** + * @since 2.0.0 + */ +export * as Layer from "./Layer.ts" + +/** + * @since 3.14.0 + */ +export * as LayerMap from "./LayerMap.ts" + +/** + * @since 4.0.0 + */ +export * as LayerRef from "./LayerRef.ts" + +/** + * @since 2.0.0 + */ +export * as Logger from "./Logger.ts" + +/** + * @since 2.0.0 + */ +export * as LogLevel from "./LogLevel.ts" + +/** + * @since 2.0.0 + */ +export * as ManagedRuntime from "./ManagedRuntime.ts" + +/** + * @since 4.0.0 + */ +export * as Match from "./Match.ts" + +/** + * @since 2.0.0 + */ +export * as Metric from "./Metric.ts" + +/** + * @since 2.0.0 + */ +export * as MutableHashMap from "./MutableHashMap.ts" + +/** + * @since 2.0.0 + */ +export * as MutableHashSet from "./MutableHashSet.ts" + +/** + * @since 4.0.0 + */ +export * as MutableList from "./MutableList.ts" + +/** + * @since 2.0.0 + */ +export * as MutableRef from "./MutableRef.ts" + +/** + * @since 4.0.0 + */ +export * as Newtype from "./Newtype.ts" + +/** + * @since 2.0.0 + */ +export * as NonEmptyIterable from "./NonEmptyIterable.ts" + +/** + * @since 2.0.0 + */ +export * as Number from "./Number.ts" + +/** + * @since 4.0.0 + */ +export * as Optic from "./Optic.ts" + +/** + * @since 2.0.0 + */ +export * as Option from "./Option.ts" + +/** + * @since 2.0.0 + */ +export * as Order from "./Order.ts" + +/** + * @since 2.0.0 + */ +export * as Ordering from "./Ordering.ts" + +/** + * @since 4.0.0 + */ +export * as PartitionedSemaphore from "./PartitionedSemaphore.ts" + +/** + * @since 4.0.0 + */ +export * as Path from "./Path.ts" + +/** + * @since 2.0.0 + */ +export * as Pipeable from "./Pipeable.ts" + +/** + * @since 4.0.0 + */ +export * as PlatformError from "./PlatformError.ts" + +/** + * @since 2.0.0 + */ +export * as Pool from "./Pool.ts" + +/** + * @since 2.0.0 + */ +export * as Predicate from "./Predicate.ts" + +/** + * @since 2.0.0 + */ +export * as PrimaryKey from "./PrimaryKey.ts" + +/** + * @since 2.0.0 + */ +export * as PubSub from "./PubSub.ts" + +/** + * @since 4.0.0 + */ +export * as Pull from "./Pull.ts" + +/** + * @since 3.8.0 + */ +export * as Queue from "./Queue.ts" + +/** + * @since 4.0.0 + */ +export * as Random from "./Random.ts" + +/** + * @since 3.5.0 + */ +export * as RcMap from "./RcMap.ts" + +/** + * @since 3.5.0 + */ +export * as RcRef from "./RcRef.ts" + +/** + * @since 2.0.0 + */ +export * as Record from "./Record.ts" + +/** + * @since 4.0.0 + */ +export * as Redactable from "./Redactable.ts" + +/** + * @since 3.3.0 + */ +export * as Redacted from "./Redacted.ts" + +/** + * @since 4.0.0 + */ +export * as Reducer from "./Reducer.ts" + +/** + * @since 2.0.0 + */ +export * as Ref from "./Ref.ts" + +/** + * @since 4.0.0 + */ +export * as References from "./References.ts" + +/** + * @since 2.0.0 + */ +export * as RegExp from "./RegExp.ts" + +/** + * @since 2.0.0 + */ +export * as Request from "./Request.ts" + +/** + * @since 2.0.0 + */ +export * as RequestResolver from "./RequestResolver.ts" + +/** + * @since 2.0.0 + */ +export * as Resource from "./Resource.ts" + +/** + * @since 4.0.0 + */ +export * as Result from "./Result.ts" + +/** + * @since 4.0.0 + */ +export * as Runtime from "./Runtime.ts" + +/** + * @since 2.0.0 + */ +export * as Schedule from "./Schedule.ts" + +/** + * @since 2.0.0 + */ +export * as Scheduler from "./Scheduler.ts" + +/** + * @since 4.0.0 + */ +export * as Schema from "./Schema.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaAST from "./SchemaAST.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaError from "./SchemaError.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaGetter from "./SchemaGetter.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaIssue from "./SchemaIssue.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaParser from "./SchemaParser.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaRepresentation from "./SchemaRepresentation.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaTransformation from "./SchemaTransformation.ts" + +/** + * @since 4.0.0 + */ +export * as SchemaUtils from "./SchemaUtils.ts" + +/** + * @since 2.0.0 + */ +export * as Scope from "./Scope.ts" + +/** + * @since 4.0.0 + */ +export * as ScopedCache from "./ScopedCache.ts" + +/** + * @since 2.0.0 + */ +export * as ScopedRef from "./ScopedRef.ts" + +/** + * @since 4.0.0 + */ +export * as Semaphore from "./Semaphore.ts" + +/** + * @since 2.0.0 + */ +export * as Sink from "./Sink.ts" + +/** + * @since 4.0.0 + */ +export * as Stdio from "./Stdio.ts" + +/** + * @since 2.0.0 + */ +export * as Stream from "./Stream.ts" + +/** + * @since 2.0.0 + */ +export * as String from "./String.ts" + +/** + * @since 2.0.0 + */ +export * as Struct from "./Struct.ts" + +/** + * @since 2.0.0 + */ +export * as SubscriptionRef from "./SubscriptionRef.ts" + +/** + * @since 2.0.0 + */ +export * as Symbol from "./Symbol.ts" + +/** + * @since 2.0.0 + */ +export * as SynchronizedRef from "./SynchronizedRef.ts" + +/** + * @since 2.0.0 + */ +export * as Take from "./Take.ts" + +/** + * @since 4.0.0 + */ +export * as Terminal from "./Terminal.ts" + +/** + * @since 2.0.0 + */ +export * as Tracer from "./Tracer.ts" + +/** + * @since 2.0.0 + */ +export * as Trie from "./Trie.ts" + +/** + * @since 2.0.0 + */ +export * as Tuple from "./Tuple.ts" + +/** + * @since 4.0.0 + */ +export * as TxChunk from "./TxChunk.ts" + +/** + * @since 4.0.0 + */ +export * as TxDeferred from "./TxDeferred.ts" + +/** + * @since 2.0.0 + */ +export * as TxHashMap from "./TxHashMap.ts" + +/** + * @since 2.0.0 + */ +export * as TxHashSet from "./TxHashSet.ts" + +/** + * @since 4.0.0 + */ +export * as TxPriorityQueue from "./TxPriorityQueue.ts" + +/** + * @since 4.0.0 + */ +export * as TxPubSub from "./TxPubSub.ts" + +/** + * @since 4.0.0 + */ +export * as TxQueue from "./TxQueue.ts" + +/** + * @since 4.0.0 + */ +export * as TxReentrantLock from "./TxReentrantLock.ts" + +/** + * @since 4.0.0 + */ +export * as TxRef from "./TxRef.ts" + +/** + * @since 4.0.0 + */ +export * as TxSemaphore from "./TxSemaphore.ts" + +/** + * @since 4.0.0 + */ +export * as TxSubscriptionRef from "./TxSubscriptionRef.ts" + +/** + * @since 4.0.0 + */ +export * as Types from "./Types.ts" + +/** + * @since 4.0.0 + */ +export * as UndefinedOr from "./UndefinedOr.ts" + +/** + * @since 2.0.0 + */ +export * as Unify from "./Unify.ts" + +/** + * @since 2.0.0 + */ +export * as Utils from "./Utils.ts" diff --git a/repos/effect/packages/effect/src/internal/array.ts b/repos/effect/packages/effect/src/internal/array.ts new file mode 100644 index 0000000..9f5100e --- /dev/null +++ b/repos/effect/packages/effect/src/internal/array.ts @@ -0,0 +1,8 @@ +/** + * @since 2.0.0 + */ + +import type { NonEmptyArray } from "../Array.ts" + +/** @internal */ +export const isArrayNonEmpty = (self: ReadonlyArray): self is NonEmptyArray => self.length > 0 diff --git a/repos/effect/packages/effect/src/internal/core.ts b/repos/effect/packages/effect/src/internal/core.ts new file mode 100644 index 0000000..ebcfbbc --- /dev/null +++ b/repos/effect/packages/effect/src/internal/core.ts @@ -0,0 +1,666 @@ +import type * as Cause from "../Cause.ts" +import type * as Context from "../Context.ts" +import type * as Effect from "../Effect.ts" +import * as Equal from "../Equal.ts" +import type * as Exit from "../Exit.ts" +import { format } from "../Formatter.ts" +import { dual, identity } from "../Function.ts" +import * as Hash from "../Hash.ts" +import { NodeInspectSymbol } from "../Inspectable.ts" +import { pipeArguments } from "../Pipeable.ts" +import { hasProperty } from "../Predicate.ts" +import type { StackFrame } from "../References.ts" +import type * as Types from "../Types.ts" +import { SingleShotGen } from "../Utils.ts" +import type { FiberImpl } from "./effect.ts" + +/** @internal */ +export const EffectTypeId = `~effect/Effect` as const + +/** @internal */ +export const ExitTypeId = `~effect/Exit` as const + +const effectVariance = { + _A: identity, + _E: identity, + _R: identity +} + +/** @internal */ +export const identifier = `${EffectTypeId}/identifier` as const +/** @internal */ +export type identifier = typeof identifier + +/** @internal */ +export const args = `${EffectTypeId}/args` as const +/** @internal */ +export type args = typeof args + +/** @internal */ +export const evaluate = `${EffectTypeId}/evaluate` as const +/** @internal */ +export type evaluate = typeof evaluate + +/** @internal */ +export const contA = `${EffectTypeId}/successCont` as const +/** @internal */ +export type contA = typeof contA + +/** @internal */ +export const contE = `${EffectTypeId}/failureCont` as const +/** @internal */ +export type contE = typeof contE + +/** @internal */ +export const contAll = `${EffectTypeId}/ensureCont` as const +/** @internal */ +export type contAll = typeof contAll + +/** @internal */ +export const Yield = Symbol.for("effect/Effect/Yield") +/** @internal */ +export type Yield = typeof Yield + +/** @internal */ +export const PipeInspectableProto = { + pipe() { + return pipeArguments(this, arguments) + }, + toJSON(this: any) { + return { ...this } + }, + toString() { + return format(this.toJSON(), { ignoreToString: true, space: 2 }) + }, + [NodeInspectSymbol]() { + return this.toJSON() + } +} + +/** @internal */ +export const StructuralProto = { + [Hash.symbol](this: any): number { + return Hash.structureKeys(this, Object.keys(this)) + }, + [Equal.symbol](this: any, that: any): boolean { + const selfKeys = Object.keys(this) + const thatKeys = Object.keys(that) + if (selfKeys.length !== thatKeys.length) return false + for (let i = 0; i < selfKeys.length; i++) { + if (selfKeys[i] !== thatKeys[i] || !Equal.equals(this[selfKeys[i]], that[selfKeys[i]])) { + return false + } + } + return true + } +} + +/** @internal */ +export const EffectProto = { + [EffectTypeId]: effectVariance, + ...PipeInspectableProto, + [Symbol.iterator]() { + return new SingleShotGen(this) as any + }, + toJSON(this: Primitive) { + return { + _id: "Effect", + op: this[identifier], + ...(args in this ? { args: this[args] } : undefined) + } + } +} + +/** @internal */ +export const isEffect = (u: unknown): u is Effect.Effect => hasProperty(u, EffectTypeId) + +/** @internal */ +export const isExit = (u: unknown): u is Exit.Exit => hasProperty(u, ExitTypeId) + +// ---------------------------------------------------------------------------- +// Cause +// ---------------------------------------------------------------------------- + +/** @internal */ +export const CauseTypeId = "~effect/Cause" + +/** @internal */ +export const CauseReasonTypeId = "~effect/Cause/Reason" + +/** @internal */ +export const isCause = (self: unknown): self is Cause.Cause => hasProperty(self, CauseTypeId) + +/** @internal */ +export const isCauseReason = (self: unknown): self is Cause.Reason => hasProperty(self, CauseReasonTypeId) + +/** @internal */ +export class CauseImpl implements Cause.Cause { + readonly [CauseTypeId]: typeof CauseTypeId + readonly reasons: ReadonlyArray< + Cause.Fail | Cause.Die | Cause.Interrupt + > + constructor( + failures: ReadonlyArray< + Cause.Fail | Cause.Die | Cause.Interrupt + > + ) { + this[CauseTypeId] = CauseTypeId + this.reasons = failures + } + pipe() { + return pipeArguments(this, arguments) + } + toJSON(): unknown { + return { + _id: "Cause", + failures: this.reasons.map((f) => f.toJSON()) + } + } + toString() { + return `Cause(${format(this.reasons)})` + } + [NodeInspectSymbol]() { + return this.toJSON() + } + [Equal.symbol](that: any): boolean { + return ( + isCause(that) && + this.reasons.length === that.reasons.length && + this.reasons.every((e, i) => Equal.equals(e, that.reasons[i])) + ) + } + [Hash.symbol](): number { + return Hash.array(this.reasons) + } +} + +const annotationsMap = new WeakMap>() + +/** @internal */ +export abstract class ReasonBase implements Cause.Cause.ReasonProto { + readonly [CauseReasonTypeId]: typeof CauseReasonTypeId + readonly annotations: ReadonlyMap + readonly _tag: Tag + + constructor( + _tag: Tag, + annotations: ReadonlyMap, + originalError: unknown + ) { + this[CauseReasonTypeId] = CauseReasonTypeId + this._tag = _tag + if ( + annotations !== constEmptyAnnotations && typeof originalError === "object" && originalError !== null && + annotations.size > 0 + ) { + const prevAnnotations = annotationsMap.get(originalError) + if (prevAnnotations) { + annotations = new Map([ + ...prevAnnotations, + ...annotations + ]) + } + annotationsMap.set(originalError, annotations) + } + this.annotations = annotations + } + + annotate( + annotations: Context.Context, + options?: { readonly overwrite?: boolean | undefined } + ): this { + if (annotations.mapUnsafe.size === 0) return this + const newAnnotations = new Map(this.annotations) + annotations.mapUnsafe.forEach((value, key) => { + if (options?.overwrite !== true && newAnnotations.has(key)) return + newAnnotations.set(key, value) + }) + const self = Object.assign(Object.create(Object.getPrototypeOf(this)), this) + self.annotations = newAnnotations + return self + } + + pipe() { + return pipeArguments(this, arguments) + } + + abstract toJSON(): unknown + abstract [Equal.symbol](that: any): boolean + abstract [Hash.symbol](): number + + toString() { + return format(this) + } + + [NodeInspectSymbol]() { + return this.toString() + } +} + +/** @internal */ +export const constEmptyAnnotations = new Map() + +/** @internal */ +export class Fail extends ReasonBase<"Fail"> implements Cause.Fail { + readonly error: E + constructor( + error: E, + annotations = constEmptyAnnotations + ) { + super("Fail", annotations, error) + this.error = error + } + override toString() { + return `Fail(${format(this.error)})` + } + toJSON(): unknown { + return { + _tag: "Fail", + error: this.error + } + } + [Equal.symbol](that: any): boolean { + return ( + isFailReason(that) && + Equal.equals(this.error, that.error) && + Equal.equals(this.annotations, that.annotations) + ) + } + [Hash.symbol](): number { + return Hash.combine(Hash.string(this._tag))( + Hash.combine(Hash.hash(this.error))(Hash.hash(this.annotations)) + ) + } +} + +/** @internal */ +export const causeFromReasons = ( + reasons: ReadonlyArray> +): Cause.Cause => new CauseImpl(reasons) + +/** @internal */ +export const causeEmpty: Cause.Cause = new CauseImpl([]) + +/** @internal */ +export const causeFail = (error: E): Cause.Cause => new CauseImpl([new Fail(error)]) + +/** @internal */ +export class Die extends ReasonBase<"Die"> implements Cause.Die { + readonly defect: unknown + constructor( + defect: unknown, + annotations = constEmptyAnnotations + ) { + super("Die", annotations, defect) + this.defect = defect + } + override toString() { + return `Die(${format(this.defect)})` + } + toJSON(): unknown { + return { + _tag: "Die", + defect: this.defect + } + } + [Equal.symbol](that: any): boolean { + return ( + isDieReason(that) && + Equal.equals(this.defect, that.defect) && + Equal.equals(this.annotations, that.annotations) + ) + } + [Hash.symbol](): number { + return Hash.combine(Hash.string(this._tag))( + Hash.combine(Hash.hash(this.defect))(Hash.hash(this.annotations)) + ) + } +} + +/** @internal */ +export const causeDie = (defect: unknown): Cause.Cause => new CauseImpl([new Die(defect)]) + +/** @internal */ +export const causeAnnotate: { + ( + annotations: Context.Context, + options?: { + readonly overwrite?: boolean | undefined + } + ): (self: Cause.Cause) => Cause.Cause + ( + self: Cause.Cause, + annotations: Context.Context, + options?: { + readonly overwrite?: boolean | undefined + } + ): Cause.Cause +} = dual( + (args) => isCause(args[0]), + ( + self: Cause.Cause, + annotations: Context.Context, + options?: { + readonly overwrite?: boolean | undefined + } + ): Cause.Cause => { + if (annotations.mapUnsafe.size === 0) return self + return new CauseImpl(self.reasons.map((f) => f.annotate(annotations, options))) + } +) + +/** @internal */ +export const isFailReason = ( + self: Cause.Reason +): self is Cause.Fail => self._tag === "Fail" + +/** @internal */ +export const isDieReason = (self: Cause.Reason): self is Cause.Die => self._tag === "Die" + +/** @internal */ +export const isInterruptReason = (self: Cause.Reason): self is Cause.Interrupt => self._tag === "Interrupt" + +/** @internal */ +export interface Primitive { + readonly [identifier]: string + readonly [contA]: + | ((value: unknown, fiber: FiberImpl, exit?: Exit.Exit) => Primitive | Yield) + | undefined + readonly [contE]: + | ((cause: Cause.Cause, fiber: FiberImpl, exit?: Exit.Exit) => Primitive | Yield) + | undefined + readonly [contAll]: + | (( + fiber: FiberImpl + ) => + | ((value: unknown, fiber: FiberImpl) => Primitive | Yield) + | undefined) + | undefined + [evaluate](fiber: FiberImpl): Primitive | Yield +} + +function defaultEvaluate(_fiber: FiberImpl): Primitive | Yield { + return exitDie(`Effect.evaluate: Not implemented`) as any +} + +/** @internal */ +export const makePrimitiveProto = (options: { + readonly op: Op + readonly [evaluate]?: ( + fiber: FiberImpl + ) => Primitive | Effect.Effect | Yield + readonly [contA]?: ( + this: Primitive, + value: any, + fiber: FiberImpl + ) => Primitive | Effect.Effect | Yield + readonly [contE]?: ( + this: Primitive, + cause: Cause.Cause, + fiber: FiberImpl + ) => Primitive | Effect.Effect | Yield + readonly [contAll]?: ( + this: Primitive, + fiber: FiberImpl + ) => void | ((value: any, fiber: FiberImpl) => void) +}): Primitive => + ({ + ...EffectProto, + [identifier]: options.op, + [evaluate]: options[evaluate] ?? defaultEvaluate, + [contA]: options[contA], + [contE]: options[contE], + [contAll]: options[contAll] + }) as any + +/** @internal */ +export const makePrimitive = < + Fn extends (...args: Array) => any, + Single extends boolean = true +>(options: { + readonly op: string + readonly single?: Single + readonly [evaluate]?: ( + this: Primitive & { + readonly [args]: Single extends true ? Parameters[0] : Parameters + }, + fiber: FiberImpl + ) => Primitive | Effect.Effect | Yield + readonly [contA]?: ( + this: Primitive & { + readonly [args]: Single extends true ? Parameters[0] : Parameters + }, + value: any, + fiber: FiberImpl, + exit?: Exit.Exit + ) => Primitive | Effect.Effect | Yield + readonly [contE]?: ( + this: Primitive & { + readonly [args]: Single extends true ? Parameters[0] : Parameters + }, + cause: Cause.Cause, + fiber: FiberImpl, + exit?: Exit.Exit + ) => Primitive | Effect.Effect | Yield + readonly [contAll]?: ( + this: Primitive & { + readonly [args]: Single extends true ? Parameters[0] : Parameters + }, + fiber: FiberImpl + ) => void | ((value: any, fiber: FiberImpl) => void) +}): Fn => { + const Proto = makePrimitiveProto(options as any) + return function() { + const self = Object.create(Proto) + self[args] = options.single === false ? arguments : arguments[0] + return self + } as Fn +} + +/** @internal */ +export const makeExit = < + Fn extends (...args: Array) => any, + Prop extends string +>(options: { + readonly op: "Success" | "Failure" + readonly prop: Prop + readonly [evaluate]: ( + this: Exit.Exit & { [args]: Parameters[0] }, + fiber: FiberImpl + ) => Primitive | Yield +}): Fn => { + const Proto = { + ...makePrimitiveProto(options), + [ExitTypeId]: ExitTypeId, + _tag: options.op, + get [options.prop](): any { + return (this as any)[args] + }, + toString(this: any) { + return `${options.op}(${format(this[args])})` + }, + toJSON(this: any) { + return { + _id: "Exit", + _tag: options.op, + [options.prop]: this[args] + } + }, + [Equal.symbol](this: any, that: any): boolean { + return ( + isExit(that) && + that._tag === this._tag && + Equal.equals(this[args], (that as any)[args]) + ) + }, + [Hash.symbol](this: any): number { + return Hash.combine(Hash.string(options.op), Hash.hash(this[args])) + } + } + return function(value: unknown) { + const self = Object.create(Proto) + self[args] = value + return self + } as Fn +} + +/** @internal */ +export const exitSucceed: (a: A) => Exit.Exit = makeExit({ + op: "Success", + prop: "value", + [evaluate](fiber) { + const cont = fiber.getCont(contA) + return cont ? cont[contA](this[args], fiber, this) : fiber.yieldWith(this) + } +}) + +/** @internal */ +export const StackTraceKey = { + key: "effect/Cause/StackTrace" satisfies typeof Cause.StackTrace.key +} as Context.Service + +/** @internal */ +export const InterruptorStackTrace = { + key: "effect/Cause/InterruptorStackTrace" satisfies typeof Cause.InterruptorStackTrace.key +} as Context.Service + +/** @internal */ +export const exitFailCause: (cause: Cause.Cause) => Exit.Exit = makeExit({ + op: "Failure", + prop: "cause", + [evaluate](fiber) { + let cause = this[args] + let annotated = false + if (fiber.currentStackFrame) { + cause = causeAnnotate(cause, { mapUnsafe: new Map([[StackTraceKey.key, fiber.currentStackFrame]]) } as any) + annotated = true + } + let cont = fiber.getCont(contE) + while (fiber.interruptible && fiber._interruptedCause && cont) { + cont = fiber.getCont(contE) + } + return cont + ? cont[contE](cause, fiber, annotated ? undefined : this) + : fiber.yieldWith(annotated ? this : exitFailCause(cause)) + } +}) + +/** @internal */ +export const exitFail = (e: E): Exit.Exit => exitFailCause(causeFail(e)) + +/** @internal */ +export const exitDie = (defect: unknown): Exit.Exit => exitFailCause(causeDie(defect)) + +/** @internal */ +export const withFiber: ( + evaluate: (fiber: FiberImpl) => Effect.Effect +) => Effect.Effect = makePrimitive({ + op: "WithFiber", + [evaluate](fiber) { + return this[args](fiber) + } +}) + +/** @internal */ +export const YieldableError: new( + message?: string, + options?: ErrorOptions +) => Cause.YieldableError = (function() { + class YieldableError extends globalThis.Error {} + const proto = makePrimitiveProto({ + op: "YieldableError", + [evaluate]() { + return exitFail(this) + } + }) + delete (proto as any).toString + Object.assign( + YieldableError.prototype, + proto + ) + return YieldableError as any +})() + +/** @internal */ +export const Error: new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> +) => Cause.YieldableError & Readonly = (function() { + const plainArgsSymbol = Symbol.for("effect/Data/Error/plainArgs") + return class Base extends YieldableError { + constructor(args: any) { + super(args?.message, args?.cause ? { cause: args.cause } : undefined) + if (args) { + Object.assign(this, args) + // @effect-diagnostics-next-line floatingEffect:off + Object.defineProperty(this, plainArgsSymbol, { + value: args, + enumerable: false + }) + } + } + override toJSON() { + return { ...(this as any)[plainArgsSymbol], ...this } + } + } as any +})() + +/** @internal */ +export const TaggedError = ( + tag: Tag +): new = {}>( + args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +) => Cause.YieldableError & { readonly _tag: Tag } & Readonly => { + class Base extends Error<{}> { + readonly _tag = tag + } + ;(Base.prototype as any).name = tag + return Base as any +} + +/** @internal */ +export const NoSuchElementErrorTypeId = "~effect/Cause/NoSuchElementError" + +/** @internal */ +export const isNoSuchElementError = ( + u: unknown +): u is Cause.NoSuchElementError => hasProperty(u, NoSuchElementErrorTypeId) + +/** @internal */ +export class NoSuchElementError extends TaggedError("NoSuchElementError") { + readonly [NoSuchElementErrorTypeId] = NoSuchElementErrorTypeId + constructor(message?: string) { + super({ message } as any) + } +} + +/** @internal */ +export const DoneTypeId = "~effect/Cause/Done" + +/** @internal */ +export const isDone = ( + u: unknown +): u is Cause.Done => hasProperty(u, DoneTypeId) + +const DoneVoid: Cause.Done = { + [DoneTypeId]: DoneTypeId, + _tag: "Done", + value: undefined +} + +/** @internal */ +export const Done = (value?: A): Cause.Done => { + if (value === undefined) return DoneVoid as Cause.Done + return { + [DoneTypeId]: DoneTypeId, + _tag: "Done", + value + } +} + +const doneVoid = exitFail(DoneVoid) + +/** @internal */ +export const done = (value?: A): Effect.Effect> => { + if (value === undefined) return doneVoid as any + return exitFail(Done(value)) +} diff --git a/repos/effect/packages/effect/src/internal/dateTime.ts b/repos/effect/packages/effect/src/internal/dateTime.ts new file mode 100644 index 0000000..63a9c2c --- /dev/null +++ b/repos/effect/packages/effect/src/internal/dateTime.ts @@ -0,0 +1,1264 @@ +import { IllegalArgumentError } from "../Cause.ts" +import * as Clock from "../Clock.ts" +import type * as DateTime from "../DateTime.ts" +import * as Duration from "../Duration.ts" +import type * as Effect from "../Effect.ts" +import * as Equal from "../Equal.ts" +import * as Equ from "../Equivalence.ts" +import type { LazyArg } from "../Function.ts" +import { dual } from "../Function.ts" +import * as Hash from "../Hash.ts" +import * as Inspectable from "../Inspectable.ts" +import * as Option from "../Option.ts" +import * as order from "../Order.ts" +import { pipeArguments } from "../Pipeable.ts" +import * as Predicate from "../Predicate.ts" +import type { Mutable } from "../Types.ts" +import * as effect from "./effect.ts" + +/** @internal */ +export const TypeId = "~effect/time/DateTime" + +/** @internal */ +export const TimeZoneTypeId = "~effect/time/DateTime/TimeZone" + +const Proto = { + [TypeId]: TypeId, + pipe() { + return pipeArguments(this, arguments) + }, + [Inspectable.NodeInspectSymbol](this: DateTime.DateTime) { + return this.toString() + }, + toJSON(this: DateTime.DateTime) { + return toDateUtc(this).toJSON() + } +} + +const ProtoUtc = { + ...Proto, + _tag: "Utc", + [Hash.symbol](this: DateTime.Utc) { + return Hash.number(this.epochMilliseconds) + }, + [Equal.symbol](this: DateTime.Utc, that: unknown) { + return isDateTime(that) && that._tag === "Utc" && this.epochMilliseconds === that.epochMilliseconds + }, + toString(this: DateTime.Utc) { + return `DateTime.Utc(${toDateUtc(this).toJSON()})` + } +} + +const ProtoZoned = { + ...Proto, + _tag: "Zoned", + [Hash.symbol](this: DateTime.Zoned) { + return Hash.combine(Hash.number(this.epochMilliseconds))(Hash.hash(this.zone)) + }, + [Equal.symbol](this: DateTime.Zoned, that: unknown) { + return isDateTime(that) && that._tag === "Zoned" && this.epochMilliseconds === that.epochMilliseconds && + Equal.equals(this.zone, that.zone) + }, + toString(this: DateTime.Zoned) { + return `DateTime.Zoned(${formatIsoZoned(this)})` + } +} + +const ProtoTimeZone = { + [TimeZoneTypeId]: TimeZoneTypeId, + [Inspectable.NodeInspectSymbol](this: DateTime.TimeZone) { + return this.toString() + } +} + +const ProtoTimeZoneNamed = { + ...ProtoTimeZone, + _tag: "Named", + [Hash.symbol](this: DateTime.TimeZone.Named) { + return Hash.string(`Named:${this.id}`) + }, + [Equal.symbol](this: DateTime.TimeZone.Named, that: unknown) { + return isTimeZone(that) && that._tag === "Named" && this.id === that.id + }, + toString(this: DateTime.TimeZone.Named) { + return `TimeZone.Named(${this.id})` + }, + toJSON(this: DateTime.TimeZone.Named) { + return { + _id: "TimeZone", + _tag: "Named", + id: this.id + } + } +} + +const ProtoTimeZoneOffset = { + ...ProtoTimeZone, + _tag: "Offset", + [Hash.symbol](this: DateTime.TimeZone.Offset) { + return Hash.string(`Offset:${this.offset}`) + }, + [Equal.symbol](this: DateTime.TimeZone.Offset, that: unknown) { + return isTimeZone(that) && that._tag === "Offset" && this.offset === that.offset + }, + toString(this: DateTime.TimeZone.Offset) { + return `TimeZone.Offset(${offsetToString(this.offset)})` + }, + toJSON(this: DateTime.TimeZone.Offset) { + return { + _id: "TimeZone", + _tag: "Offset", + offset: this.offset + } + } +} + +/** @internal */ +export const makeZonedProto = ( + epochMillis: number, + zone: DateTime.TimeZone, + partsUtc?: DateTime.DateTime.PartsWithWeekday +): DateTime.Zoned => { + const self = Object.create(ProtoZoned) + self.epochMilliseconds = epochMillis + self.zone = zone + Object.defineProperty(self, "partsUtc", { + value: partsUtc, + enumerable: false, + writable: true + }) + Object.defineProperty(self, "adjustedEpochMillis", { + value: undefined, + enumerable: false, + writable: true + }) + Object.defineProperty(self, "partsAdjusted", { + value: undefined, + enumerable: false, + writable: true + }) + return self +} + +// ============================================================================= +// guards +// ============================================================================= + +/** @internal */ +export const isDateTime = (u: unknown): u is DateTime.DateTime => Predicate.hasProperty(u, TypeId) + +const isDateTimeArgs = (args: IArguments) => isDateTime(args[0]) + +/** @internal */ +export const isTimeZone = (u: unknown): u is DateTime.TimeZone => Predicate.hasProperty(u, TimeZoneTypeId) + +/** @internal */ +export const isTimeZoneOffset = (u: unknown): u is DateTime.TimeZone.Offset => isTimeZone(u) && u._tag === "Offset" + +/** @internal */ +export const isTimeZoneNamed = (u: unknown): u is DateTime.TimeZone.Named => isTimeZone(u) && u._tag === "Named" + +/** @internal */ +export const isUtc = (self: DateTime.DateTime): self is DateTime.Utc => self._tag === "Utc" + +/** @internal */ +export const isZoned = (self: DateTime.DateTime): self is DateTime.Zoned => self._tag === "Zoned" + +// ============================================================================= +// instances +// ============================================================================= + +/** @internal */ +export const Equivalence: Equ.Equivalence = Equ.make((a, b) => + a.epochMilliseconds === b.epochMilliseconds +) + +/** @internal */ +export const Order: order.Order = order.make((self, that) => + self.epochMilliseconds < that.epochMilliseconds ? -1 : self.epochMilliseconds > that.epochMilliseconds ? 1 : 0 +) + +/** @internal */ +export const clamp: { + ( + options: { readonly minimum: Min; readonly maximum: Max } + ): (self: A) => A | Min | Max + ( + self: A, + options: { readonly minimum: Min; readonly maximum: Max } + ): A | Min | Max +} = order.clamp(Order) + +// ============================================================================= +// constructors +// ============================================================================= + +const makeUtc = (epochMillis: number): DateTime.Utc => { + const self = Object.create(ProtoUtc) + self.epochMilliseconds = epochMillis + Object.defineProperty(self, "partsUtc", { + value: undefined, + enumerable: false, + writable: true + }) + return self +} + +/** @internal */ +export const fromDateUnsafe = (date: Date): DateTime.Utc => { + const epochMillis = date.getTime() + if (Number.isNaN(epochMillis)) { + throw new IllegalArgumentError("Invalid date") + } + return makeUtc(epochMillis) +} + +/** @internal */ +export const makeUnsafe = (input: A): DateTime.DateTime.PreserveZone => { + if (isDateTime(input)) { + return input as DateTime.DateTime.PreserveZone + } else if (input instanceof Date) { + return fromDateUnsafe(input) as DateTime.DateTime.PreserveZone + } else if (typeof input === "object") { + if ("epochMilliseconds" in input) { + return makeUtc(input.epochMilliseconds) as DateTime.DateTime.PreserveZone + } + const date = new Date(0) + setPartsDate(date, input) + return fromDateUnsafe(date) as DateTime.DateTime.PreserveZone + } else if (typeof input === "string" && !hasZone(input)) { + return fromDateUnsafe(new Date(input + "Z")) as DateTime.DateTime.PreserveZone + } + return fromDateUnsafe(new Date(input)) as DateTime.DateTime.PreserveZone +} + +/** + * Detects whether a date string already contains timezone info. + * Without a zone, `new Date("2024-01-01T12:00:00")` is parsed as local time, + * so `makeUnsafe` appends "Z" to force UTC interpretation. + * This check prevents appending "Z" to strings that already have a zone + * (e.g. "2024-01-01T12:00:00Z", "...+05:30", "...GMT"), which would produce invalid dates. + */ +const hasZone = (input: string): boolean => /Z|GMT|[+-]\d{2}$|[+-]\d{2}:?\d{2}$|\]$/.test(input) + +const minEpochMillis = -8640000000000000 + (12 * 60 * 60 * 1000) +const maxEpochMillis = 8640000000000000 - (14 * 60 * 60 * 1000) + +/** @internal */ +export const makeZonedUnsafe = (input: DateTime.DateTime.Input, options?: { + readonly timeZone?: number | string | DateTime.TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => { + let timeZoneOption = options?.timeZone + if (timeZoneOption === undefined && isDateTime(input) && isZoned(input)) { + return input + } + const self = makeUnsafe(input) + if (self.epochMilliseconds < minEpochMillis || self.epochMilliseconds > maxEpochMillis) { + throw new RangeError(`Epoch millis out of range: ${self.epochMilliseconds}`) + } + if (timeZoneOption === undefined && typeof input === "object" && "timeZoneId" in input) { + timeZoneOption = input.timeZoneId + } + let zone: DateTime.TimeZone + if (timeZoneOption === undefined) { + const offset = new Date(self.epochMilliseconds).getTimezoneOffset() * -60 * 1000 + zone = zoneMakeOffset(offset) + } else if (isTimeZone(timeZoneOption)) { + zone = timeZoneOption + } else if (typeof timeZoneOption === "number") { + zone = zoneMakeOffset(timeZoneOption) + } else { + const parsedZone = zoneFromString(timeZoneOption) + if (Option.isNone(parsedZone)) { + throw new IllegalArgumentError(`Invalid time zone: ${timeZoneOption}`) + } + zone = parsedZone.value + } + if (options?.adjustForTimeZone !== true) { + return makeZonedProto(self.epochMilliseconds, zone, self.partsUtc) + } + return makeZonedFromAdjusted(self.epochMilliseconds, zone, options?.disambiguation ?? "compatible") +} + +/** @internal */ +export const makeZoned: ( + input: DateTime.DateTime.Input, + options?: { + readonly timeZone?: number | string | DateTime.TimeZone | undefined + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + } +) => Option.Option = Option.liftThrowable(makeZonedUnsafe) + +/** @internal */ +export const make: (input: A) => Option.Option> = + Option.liftThrowable(makeUnsafe) + +const zonedStringRegExp = /^(.{17,35})\[(.+)\]$/ + +/** @internal */ +export const makeZonedFromString = (input: string): Option.Option => { + const match = zonedStringRegExp.exec(input) + if (match === null) { + const offset = parseOffset(input) + return offset !== null ? makeZoned(input, { timeZone: offset }) : Option.none() + } + const [, isoString, timeZone] = match + return makeZoned(isoString, { timeZone }) +} + +/** @internal */ +export const now: Effect.Effect = effect.map(Clock.currentTimeMillis, makeUtc) + +/** @internal */ +export const nowAsDate: Effect.Effect = effect.map(Clock.currentTimeMillis, (millis) => new Date(millis)) + +/** @internal */ +export const nowUnsafe: LazyArg = () => makeUtc(Date.now()) + +// ============================================================================= +// time zones +// ============================================================================= + +/** @internal */ +export const toUtc = (self: DateTime.DateTime): DateTime.Utc => makeUtc(self.epochMilliseconds) + +/** @internal */ +export const setZone: { + (zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, zone: DateTime.TimeZone, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => + options?.adjustForTimeZone === true + ? makeZonedFromAdjusted(self.epochMilliseconds, zone, options?.disambiguation ?? "compatible") + : makeZonedProto(self.epochMilliseconds, zone, self.partsUtc)) + +/** @internal */ +export const setZoneOffset: { + (offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, offset: number, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => setZone(self, zoneMakeOffset(offset), options)) + +const validZoneCache = new Map() + +const formatOptions: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "longOffset", + fractionalSecondDigits: 3, + hourCycle: "h23" +} + +const zoneMakeIntl = (format: Intl.DateTimeFormat): DateTime.TimeZone.Named => { + const zoneId = format.resolvedOptions().timeZone + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + const zone = Object.create(ProtoTimeZoneNamed) + zone.id = zoneId + zone.format = format + validZoneCache.set(zoneId, zone) + return zone +} + +/** @internal */ +export const zoneMakeNamedUnsafe = (zoneId: string): DateTime.TimeZone.Named => { + if (validZoneCache.has(zoneId)) { + return validZoneCache.get(zoneId)! + } + try { + return zoneMakeIntl( + new Intl.DateTimeFormat("en-US", { + ...formatOptions, + timeZone: zoneId + }) + ) + } catch { + throw new IllegalArgumentError(`Invalid time zone: ${zoneId}`) + } +} + +/** @internal */ +export const zoneMakeOffset = (offset: number): DateTime.TimeZone.Offset => { + const zone = Object.create(ProtoTimeZoneOffset) + zone.offset = offset + return zone +} + +/** @internal */ +export const zoneMakeNamed: (zoneId: string) => Option.Option = Option.liftThrowable( + zoneMakeNamedUnsafe +) + +/** @internal */ +export const zoneMakeNamedEffect = (zoneId: string): Effect.Effect => + effect.try({ + try: () => zoneMakeNamedUnsafe(zoneId), + catch: (e) => e as IllegalArgumentError + }) + +/** @internal */ +export const zoneMakeLocal = (): DateTime.TimeZone.Named => + zoneMakeIntl(new Intl.DateTimeFormat("en-US", formatOptions)) + +const offsetZoneRegExp = /^(?:GMT|[+-])/ + +/** @internal */ +export const zoneFromString = (zone: string): Option.Option => { + if (offsetZoneRegExp.test(zone)) { + const offset = parseOffset(zone) + return offset === null ? Option.none() : Option.some(zoneMakeOffset(offset)) + } + return zoneMakeNamed(zone) +} + +/** @internal */ +export const zoneToString = (self: DateTime.TimeZone): string => { + if (self._tag === "Offset") { + return offsetToString(self.offset) + } + return self.id +} + +/** @internal */ +export const setZoneNamed: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => Option.Option + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): Option.Option +} = dual( + isDateTimeArgs, + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): Option.Option => Option.map(zoneMakeNamed(zoneId), (zone) => setZone(self, zone, options)) +) + +/** @internal */ +export const setZoneNamedUnsafe: { + (zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: DateTime.DateTime) => DateTime.Zoned + (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined + }): DateTime.Zoned +} = dual(isDateTimeArgs, (self: DateTime.DateTime, zoneId: string, options?: { + readonly adjustForTimeZone?: boolean | undefined + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.Zoned => setZone(self, zoneMakeNamedUnsafe(zoneId), options)) + +// ============================================================================= +// comparisons +// ============================================================================= + +/** @internal */ +export const distance: { + (other: DateTime.DateTime): (self: DateTime.DateTime) => Duration.Duration + (self: DateTime.DateTime, other: DateTime.DateTime): Duration.Duration +} = dual( + 2, + (self: DateTime.DateTime, other: DateTime.DateTime): Duration.Duration => + Duration.millis(toEpochMillis(other) - toEpochMillis(self)) +) + +/** @internal */ +export const min: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = order.min(Order) + +/** @internal */ +export const max: { + (that: That): (self: Self) => Self | That + (self: Self, that: That): Self | That +} = order.max(Order) + +/** @internal */ +export const isGreaterThan: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.isGreaterThan(Order) + +/** @internal */ +export const isGreaterThanOrEqualTo: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** @internal */ +export const isLessThan: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.isLessThan(Order) + +/** @internal */ +export const isLessThanOrEqualTo: { + (that: DateTime.DateTime): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, that: DateTime.DateTime): boolean +} = order.isLessThanOrEqualTo(Order) + +/** @internal */ +export const between: { + (options: { minimum: DateTime.DateTime; maximum: DateTime.DateTime }): (self: DateTime.DateTime) => boolean + (self: DateTime.DateTime, options: { minimum: DateTime.DateTime; maximum: DateTime.DateTime }): boolean +} = order.isBetween(Order) + +/** @internal */ +export const isFuture = (self: DateTime.DateTime): Effect.Effect => effect.map(now, isLessThan(self)) + +/** @internal */ +export const isFutureUnsafe = (self: DateTime.DateTime): boolean => isLessThan(nowUnsafe(), self) + +/** @internal */ +export const isPast = (self: DateTime.DateTime): Effect.Effect => effect.map(now, isGreaterThan(self)) + +/** @internal */ +export const isPastUnsafe = (self: DateTime.DateTime): boolean => isGreaterThan(nowUnsafe(), self) + +// ============================================================================= +// conversions +// ============================================================================= + +/** @internal */ +export const toDateUtc = (self: DateTime.DateTime): Date => new Date(self.epochMilliseconds) + +/** @internal */ +export const toDate = (self: DateTime.DateTime): Date => { + if (self._tag === "Utc") { + return new Date(self.epochMilliseconds) + } else if (self.zone._tag === "Offset") { + return new Date(self.epochMilliseconds + self.zone.offset) + } else if (self.adjustedEpochMilliseconds !== undefined) { + return new Date(self.adjustedEpochMilliseconds) + } + const parts = self.zone.format.formatToParts(self.epochMilliseconds).filter((_) => _.type !== "literal") + const date = new Date(0) + date.setUTCFullYear( + Number(parts[2].value), + Number(parts[0].value) - 1, + Number(parts[1].value) + ) + date.setUTCHours( + Number(parts[3].value), + Number(parts[4].value), + Number(parts[5].value), + Number(parts[6].value) + ) + self.adjustedEpochMilliseconds = date.getTime() + return date +} + +/** @internal */ +export const zonedOffset = (self: DateTime.Zoned): number => { + const date = toDate(self) + return date.getTime() - toEpochMillis(self) +} + +const offsetToString = (offset: number): string => { + const abs = Math.abs(offset) + let hours = Math.floor(abs / (60 * 60 * 1000)) + let minutes = Math.round((abs % (60 * 60 * 1000)) / (60 * 1000)) + if (minutes === 60) { + hours += 1 + minutes = 0 + } + return `${offset < 0 ? "-" : "+"}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}` +} + +/** @internal */ +export const zonedOffsetIso = (self: DateTime.Zoned): string => offsetToString(zonedOffset(self)) + +/** @internal */ +export const toEpochMillis = (self: DateTime.DateTime): number => self.epochMilliseconds + +/** @internal */ +export const removeTime = (self: DateTime.DateTime): DateTime.Utc => + withDate(self, (date) => { + date.setUTCHours(0, 0, 0, 0) + return makeUtc(date.getTime()) + }) + +// ============================================================================= +// parts +// ============================================================================= + +const dateToParts = (date: Date): DateTime.DateTime.PartsWithWeekday => ({ + millisecond: date.getUTCMilliseconds(), + second: date.getUTCSeconds(), + minute: date.getUTCMinutes(), + hour: date.getUTCHours(), + day: date.getUTCDate(), + weekDay: date.getUTCDay(), + month: date.getUTCMonth() + 1, + year: date.getUTCFullYear() +}) + +/** @internal */ +export const toParts = (self: DateTime.DateTime): DateTime.DateTime.PartsWithWeekday => { + if (self._tag === "Utc") { + return toPartsUtc(self) + } else if (self.partsAdjusted !== undefined) { + return self.partsAdjusted + } + self.partsAdjusted = withDate(self, dateToParts) + return self.partsAdjusted +} + +/** @internal */ +export const toPartsUtc = (self: DateTime.DateTime): DateTime.DateTime.PartsWithWeekday => { + if (self.partsUtc !== undefined) { + return self.partsUtc + } + self.partsUtc = withDateUtc(self, dateToParts) + return self.partsUtc +} + +/** @internal */ +export const getPartUtc: { + (part: keyof DateTime.DateTime.PartsWithWeekday): (self: DateTime.DateTime) => number + (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number => toPartsUtc(self)[part]) + +/** @internal */ +export const getPart: { + (part: keyof DateTime.DateTime.PartsWithWeekday): (self: DateTime.DateTime) => number + (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number +} = dual(2, (self: DateTime.DateTime, part: keyof DateTime.DateTime.PartsWithWeekday): number => toParts(self)[part]) + +const setPartsDate = (date: Date, parts: Partial): void => { + if (parts.year !== undefined) { + date.setUTCFullYear(parts.year) + } + if (parts.month !== undefined) { + date.setUTCMonth(parts.month - 1) + } + if (parts.day !== undefined) { + date.setUTCDate(parts.day) + } + if (parts.weekDay !== undefined) { + const diff = parts.weekDay - date.getUTCDay() + date.setUTCDate(date.getUTCDate() + diff) + } + if (parts.hour !== undefined) { + date.setUTCHours(parts.hour) + } + if (parts.minute !== undefined) { + date.setUTCMinutes(parts.minute) + } + if (parts.second !== undefined) { + date.setUTCSeconds(parts.second) + } + if (parts.millisecond !== undefined) { + date.setUTCMilliseconds(parts.millisecond) + } +} + +/** @internal */ +export const setParts: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutate(self, (date) => setPartsDate(date, parts)) +) + +/** @internal */ +export const setPartsUtc: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutateUtc(self, (date) => setPartsDate(date, parts)) +) + +// ============================================================================= +// mapping +// ============================================================================= + +const constDayMillis = 24 * 60 * 60 * 1000 + +const makeZonedFromAdjusted = ( + adjustedMillis: number, + zone: DateTime.TimeZone, + disambiguation: DateTime.Disambiguation +): DateTime.Zoned => { + if (zone._tag === "Offset") { + return makeZonedProto(adjustedMillis - zone.offset, zone) + } + const beforeOffset = calculateNamedOffset( + adjustedMillis - constDayMillis, + adjustedMillis, + zone + ) + const afterOffset = calculateNamedOffset( + adjustedMillis + constDayMillis, + adjustedMillis, + zone + ) + // If there is no transition, we can return early + if (beforeOffset === afterOffset) { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + const isForwards = beforeOffset < afterOffset + const transitionMillis = beforeOffset - afterOffset + // If the transition is forwards, we only need to check if we should move the + // local wall clock time forward if it is inside the gap + if (isForwards) { + const currentAfterOffset = calculateNamedOffset( + adjustedMillis - afterOffset, + adjustedMillis, + zone + ) + if (currentAfterOffset === afterOffset) { + return makeZonedProto(adjustedMillis - afterOffset, zone) + } + const before = makeZonedProto(adjustedMillis - beforeOffset, zone) + const beforeAdjustedMillis = toDate(before).getTime() + // If the wall clock time has changed, we are inside the gap + if (adjustedMillis !== beforeAdjustedMillis) { + switch (disambiguation) { + case "reject": { + const formatted = new Date(adjustedMillis).toISOString() + throw new RangeError(`Gap time: ${formatted} does not exist in time zone ${zone.id}`) + } + case "earlier": + return makeZonedProto(adjustedMillis - afterOffset, zone) + + case "compatible": + case "later": + return before + } + } + // The wall clock time is in the earlier offset, so we use that + return before + } + + const currentBeforeOffset = calculateNamedOffset( + adjustedMillis - beforeOffset, + adjustedMillis, + zone + ) + // The wall clock time is in the earlier offset, so we use that + if (currentBeforeOffset === beforeOffset) { + if (disambiguation === "earlier" || disambiguation === "compatible") { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + const laterOffset = calculateNamedOffset( + adjustedMillis - beforeOffset + transitionMillis, + adjustedMillis + transitionMillis, + zone + ) + if (laterOffset === beforeOffset) { + return makeZonedProto(adjustedMillis - beforeOffset, zone) + } + // If the offset changed in this period, then we are inside the period where + // the wall clock time occurs twice, once in the earlier offset and once in + // the later offset. + if (disambiguation === "reject") { + const formatted = new Date(adjustedMillis).toISOString() + throw new RangeError(`Ambiguous time: ${formatted} occurs twice in time zone ${zone.id}`) + } + // If the disambiguation is "later", we return the later offset below + } + return makeZonedProto(adjustedMillis - afterOffset, zone) +} + +const offsetRegExp = /([+-])(\d{2}):(\d{2})$/ +const parseOffset = (offset: string): number | null => { + const match = offsetRegExp.exec(offset) + if (match === null) { + return null + } + const [, sign, hours, minutes] = match + return (sign === "+" ? 1 : -1) * (Number(hours) * 60 + Number(minutes)) * 60 * 1000 +} + +const calculateNamedOffset = ( + utcMillis: number, + adjustedMillis: number, + zone: DateTime.TimeZone.Named +): number => { + const offset = zone.format.formatToParts(utcMillis).find((_) => _.type === "timeZoneName")?.value ?? "" + if (offset === "GMT") { + return 0 + } + const result = parseOffset(offset) + if (result === null) { + // fallback to using the adjusted date + return zonedOffset(makeZonedProto(adjustedMillis, zone)) + } + return result +} + +/** @internal */ +export const mutate: { + (f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined + }): (self: A) => A + (self: A, f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, f: (date: Date) => void, options?: { + readonly disambiguation?: DateTime.Disambiguation | undefined +}): DateTime.DateTime => { + if (self._tag === "Utc") { + const date = toDateUtc(self) + f(date) + return makeUtc(date.getTime()) + } + const adjustedDate = toDate(self) + const newAdjustedDate = new Date(adjustedDate.getTime()) + f(newAdjustedDate) + return makeZonedFromAdjusted(newAdjustedDate.getTime(), self.zone, options?.disambiguation ?? "compatible") +}) + +/** @internal */ +export const mutateUtc: { + (f: (date: Date) => void): (self: A) => A + (self: A, f: (date: Date) => void): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => void): DateTime.DateTime => + mapEpochMillis(self, (millis) => { + const date = new Date(millis) + f(date) + return date.getTime() + })) + +/** @internal */ +export const mapEpochMillis: { + (f: (millis: number) => number): (self: A) => A + (self: A, f: (millis: number) => number): A +} = dual(2, (self: DateTime.DateTime, f: (millis: number) => number): DateTime.DateTime => { + const millis = f(toEpochMillis(self)) + return self._tag === "Utc" ? makeUtc(millis) : makeZonedProto(millis, self.zone) +}) + +/** @internal */ +export const withDate: { + (f: (date: Date) => A): (self: DateTime.DateTime) => A + (self: DateTime.DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => A): A => f(toDate(self))) + +/** @internal */ +export const withDateUtc: { + (f: (date: Date) => A): (self: DateTime.DateTime) => A + (self: DateTime.DateTime, f: (date: Date) => A): A +} = dual(2, (self: DateTime.DateTime, f: (date: Date) => A): A => f(toDateUtc(self))) + +/** @internal */ +export const match: { + (options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B + }): (self: DateTime.DateTime) => A | B + (self: DateTime.DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B + }): A | B +} = dual(2, (self: DateTime.DateTime, options: { + readonly onUtc: (_: DateTime.Utc) => A + readonly onZoned: (_: DateTime.Zoned) => B +}): A | B => self._tag === "Utc" ? options.onUtc(self) : options.onZoned(self)) + +// ============================================================================= +// math +// ============================================================================= + +/** @internal */ +export const addDuration: { + (duration: Duration.Input): (self: A) => A + (self: A, duration: Duration.Input): A +} = dual( + 2, + (self: DateTime.DateTime, duration: Duration.Input): DateTime.DateTime => + mapEpochMillis(self, (millis) => millis + Duration.toMillis(Duration.fromInputUnsafe(duration))) +) + +/** @internal */ +export const subtractDuration: { + (duration: Duration.Input): (self: A) => A + (self: A, duration: Duration.Input): A +} = dual( + 2, + (self: DateTime.DateTime, duration: Duration.Input): DateTime.DateTime => + mapEpochMillis(self, (millis) => millis - Duration.toMillis(Duration.fromInputUnsafe(duration))) +) + +const addMillis = (date: Date, amount: number): void => { + date.setTime(date.getTime() + amount) +} + +/** @internal */ +export const add: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual( + 2, + (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => + mutate(self, (date) => { + if (parts.milliseconds) { + addMillis(date, parts.milliseconds) + } + if (parts.seconds) { + addMillis(date, parts.seconds * 1000) + } + if (parts.minutes) { + addMillis(date, parts.minutes * 60 * 1000) + } + if (parts.hours) { + addMillis(date, parts.hours * 60 * 60 * 1000) + } + if (parts.days) { + date.setUTCDate(date.getUTCDate() + parts.days) + } + if (parts.weeks) { + date.setUTCDate(date.getUTCDate() + parts.weeks * 7) + } + if (parts.months) { + const day = date.getUTCDate() + date.setUTCMonth(date.getUTCMonth() + parts.months + 1, 0) + if (day < date.getUTCDate()) { + date.setUTCDate(day) + } + } + if (parts.years) { + const day = date.getUTCDate() + const month = date.getUTCMonth() + date.setUTCFullYear( + date.getUTCFullYear() + parts.years, + month + 1, + 0 + ) + if (day < date.getUTCDate()) { + date.setUTCDate(day) + } + } + }) +) + +/** @internal */ +export const subtract: { + ( + parts: Partial + ): (self: A) => A + ( + self: A, + parts: Partial + ): A +} = dual(2, (self: DateTime.DateTime, parts: Partial): DateTime.DateTime => { + const newParts = {} as Partial> + for (const key in parts) { + newParts[key as keyof DateTime.DateTime.PartsForMath] = -1 * parts[key as keyof DateTime.DateTime.PartsForMath]! + } + return add(self, newParts) +}) + +const startOfDate = (date: Date, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) => { + switch (part) { + case "second": { + date.setUTCMilliseconds(0) + break + } + case "minute": { + date.setUTCSeconds(0, 0) + break + } + case "hour": { + date.setUTCMinutes(0, 0, 0) + break + } + case "day": { + date.setUTCHours(0, 0, 0, 0) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff) + date.setUTCHours(0, 0, 0, 0) + break + } + case "month": { + date.setUTCDate(1) + date.setUTCHours(0, 0, 0, 0) + break + } + case "year": { + date.setUTCMonth(0, 1) + date.setUTCHours(0, 0, 0, 0) + break + } + } +} + +/** @internal */ +export const startOf: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => mutate(self, (date) => startOfDate(date, part, options))) + +const endOfDate = (date: Date, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}) => { + switch (part) { + case "second": { + date.setUTCMilliseconds(999) + break + } + case "minute": { + date.setUTCSeconds(59, 999) + break + } + case "hour": { + date.setUTCMinutes(59, 59, 999) + break + } + case "day": { + date.setUTCHours(23, 59, 59, 999) + break + } + case "week": { + const weekStartsOn = options?.weekStartsOn ?? 0 + const day = date.getUTCDay() + const diff = (day - weekStartsOn + 7) % 7 + date.setUTCDate(date.getUTCDate() - diff + 6) + date.setUTCHours(23, 59, 59, 999) + break + } + case "month": { + date.setUTCMonth(date.getUTCMonth() + 1, 0) + date.setUTCHours(23, 59, 59, 999) + break + } + case "year": { + date.setUTCMonth(11, 31) + date.setUTCHours(23, 59, 59, 999) + break + } + } +} + +/** @internal */ +export const endOf: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => mutate(self, (date) => endOfDate(date, part, options))) + +/** @internal */ +export const nearest: { + (part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): (self: A) => A + (self: A, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined + }): A +} = dual(isDateTimeArgs, (self: DateTime.DateTime, part: DateTime.DateTime.UnitSingular, options?: { + readonly weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined +}): DateTime.DateTime => + mutate(self, (date) => { + if (part === "millisecond") return + const millis = date.getTime() + const start = new Date(millis) + startOfDate(start, part, options) + const startMillis = start.getTime() + const end = new Date(millis) + endOfDate(end, part, options) + const endMillis = end.getTime() + 1 + const diffStart = millis - startMillis + const diffEnd = endMillis - millis + if (diffStart < diffEnd) { + date.setTime(startMillis) + } else { + date.setTime(endMillis) + } + })) + +// ============================================================================= +// formatting +// ============================================================================= + +const intlTimeZone = (self: DateTime.TimeZone): string => { + if (self._tag === "Named") { + return self.id + } + return offsetToString(self.offset) +} + +/** @internal */ +export const format: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => { + try { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: self._tag === "Utc" ? "UTC" : intlTimeZone(self.zone), + ...options + }).format(self.epochMilliseconds) + } catch { + return new Intl.DateTimeFormat(options?.locale, { + timeZone: "UTC", + ...options + }).format(toDate(self)) + } +}) + +/** @internal */ +export const formatLocal: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => new Intl.DateTimeFormat(options?.locale, options).format(self.epochMilliseconds)) + +/** @internal */ +export const formatUtc: { + ( + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): (self: DateTime.DateTime) => string + ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined + ): string +} = dual(isDateTimeArgs, ( + self: DateTime.DateTime, + options?: + | Intl.DateTimeFormatOptions & { + readonly locale?: Intl.LocalesArgument + } + | undefined +): string => + new Intl.DateTimeFormat(options?.locale, { + ...options, + timeZone: "UTC" + }).format(self.epochMilliseconds)) + +/** @internal */ +export const formatIntl: { + (format: Intl.DateTimeFormat): (self: DateTime.DateTime) => string + (self: DateTime.DateTime, format: Intl.DateTimeFormat): string +} = dual(2, (self: DateTime.DateTime, format: Intl.DateTimeFormat): string => format.format(self.epochMilliseconds)) + +/** @internal */ +export const formatIso = (self: DateTime.DateTime): string => toDateUtc(self).toISOString() + +/** @internal */ +export const formatIsoDate = (self: DateTime.DateTime): string => toDate(self).toISOString().slice(0, 10) + +/** @internal */ +export const formatIsoDateUtc = (self: DateTime.DateTime): string => toDateUtc(self).toISOString().slice(0, 10) + +/** @internal */ +export const formatIsoOffset = (self: DateTime.DateTime): string => { + const date = toDate(self) + return self._tag === "Utc" ? date.toISOString() : `${date.toISOString().slice(0, -1)}${zonedOffsetIso(self)}` +} + +/** @internal */ +export const formatIsoZoned = (self: DateTime.Zoned): string => + self.zone._tag === "Offset" ? formatIsoOffset(self) : `${formatIsoOffset(self)}[${self.zone.id}]` diff --git a/repos/effect/packages/effect/src/internal/doNotation.ts b/repos/effect/packages/effect/src/internal/doNotation.ts new file mode 100644 index 0000000..7c6e3c9 --- /dev/null +++ b/repos/effect/packages/effect/src/internal/doNotation.ts @@ -0,0 +1,117 @@ +import { dual } from "../Function.ts" +import type { Kind, TypeLambda } from "../HKT.ts" +import type { NoInfer } from "../Types.ts" + +interface Map { + ( + f: (a: A) => B + ): (self: Kind) => Kind + ( + self: Kind, + f: (a: A) => B + ): Kind +} + +interface FlatMap { + ( + f: (a: A) => Kind + ): ( + self: Kind + ) => Kind + ( + self: Kind, + f: (a: A) => Kind + ): Kind +} + +/** @internal */ +export const let_ = ( + map: Map +): { + ( + name: Exclude, + f: (a: NoInfer) => B + ): ( + self: Kind + ) => Kind + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => B + ): Kind +} => + dual( + 3, + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => B + ): Kind => + map(self, (a) => ({ ...a, [name]: f(a) }) as any) + ) + +/** @internal */ +export const bindTo = ( + map: Map +): { + ( + name: N + ): ( + self: Kind + ) => Kind> + ( + self: Kind, + name: N + ): Kind> +} => + dual( + 2, + ( + self: Kind, + name: N + ): Kind> => map(self, (a) => ({ [name]: a }) as Record) + ) + +/** @internal */ +export const bind = ( + map: Map, + flatMap: FlatMap +): { + ( + name: Exclude, + f: (a: NoInfer) => Kind + ): ( + self: Kind + ) => Kind< + F, + R1 & R2, + O1 | O2, + E1 | E2, + { [K in keyof A | N]: K extends keyof A ? A[K] : B } + > + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => Kind + ): Kind< + F, + R1 & R2, + O1 | O2, + E1 | E2, + { [K in keyof A | N]: K extends keyof A ? A[K] : B } + > +} => + dual( + 3, + ( + self: Kind, + name: Exclude, + f: (a: NoInfer) => Kind + ): Kind< + F, + R1 & R2, + O1 | O2, + E1 | E2, + { [K in keyof A | N]: K extends keyof A ? A[K] : B } + > => flatMap(self, (a) => map(f(a), (b) => ({ ...a, [name]: b }) as any)) + ) diff --git a/repos/effect/packages/effect/src/internal/effect.ts b/repos/effect/packages/effect/src/internal/effect.ts new file mode 100644 index 0000000..97ae1ee --- /dev/null +++ b/repos/effect/packages/effect/src/internal/effect.ts @@ -0,0 +1,6519 @@ +import * as Arr from "../Array.ts" +import type * as Cause from "../Cause.ts" +import type * as Clock from "../Clock.ts" +import type * as Console from "../Console.ts" +import * as Context from "../Context.ts" +import * as Duration from "../Duration.ts" +import type * as Effect from "../Effect.ts" +import * as Equal from "../Equal.ts" +import type * as Exit from "../Exit.ts" +import type * as Fiber from "../Fiber.ts" +import * as Filter from "../Filter.ts" +import { formatJson } from "../Formatter.ts" +import type { LazyArg } from "../Function.ts" +import { constant, constFalse, constTrue, constUndefined, constVoid, dual, identity } from "../Function.ts" +import * as Hash from "../Hash.ts" +import { toJson, toStringUnknown } from "../Inspectable.ts" +import * as Iterable from "../Iterable.ts" +import type * as _Latch from "../Latch.ts" +import type * as Logger from "../Logger.ts" +import type * as LogLevel from "../LogLevel.ts" +import type * as Metric from "../Metric.ts" +import * as Option from "../Option.ts" +import * as Order from "../Order.ts" +import { pipeArguments } from "../Pipeable.ts" +import type * as Predicate from "../Predicate.ts" +import { hasProperty, isIterable, isString, isTagged } from "../Predicate.ts" +import { currentFiberTypeId, redact } from "../Redactable.ts" +import type { StackFrame } from "../References.ts" +import * as Result from "../Result.ts" +import * as Scheduler from "../Scheduler.ts" +import type * as Scope from "../Scope.ts" +import * as Tracer from "../Tracer.ts" +import type { + Concurrency, + EqualsWith, + ExcludeReason, + ExcludeTag, + ExtractReason, + ExtractTag, + NarrowReason, + NoInfer, + OmitReason, + ReasonOf, + ReasonTags, + Simplify, + Tags, + unassigned +} from "../Types.ts" +import { internalCall } from "../Utils.ts" +import type { Primitive } from "./core.ts" +import { + args, + causeAnnotate, + causeEmpty, + causeFromReasons, + CauseImpl, + constEmptyAnnotations, + contA, + contAll, + contE, + evaluate, + exitDie, + exitFail, + exitFailCause, + exitSucceed, + ExitTypeId, + Fail, + InterruptorStackTrace, + isCause, + isDieReason, + isEffect, + isFailReason, + isInterruptReason, + isNoSuchElementError, + makePrimitive, + makePrimitiveProto, + NoSuchElementError, + ReasonBase, + StackTraceKey as CauseStackTrace, + TaggedError, + withFiber, + Yield +} from "./core.ts" +import * as doNotation from "./doNotation.ts" +import * as InternalMetric from "./metric.ts" +import { + CurrentConcurrency, + CurrentErrorReporters, + CurrentLogAnnotations, + CurrentLogLevel, + CurrentLogSpans, + CurrentStackFrame, + MinimumLogLevel, + TracerEnabled, + TracerSpanAnnotations, + TracerSpanLinks, + TracerTimingEnabled +} from "./references.ts" +import { getStackTraceLimit, setStackTraceLimit } from "./stackTraceLimit.ts" +import { addSpanStackTrace, makeStackCleaner } from "./tracer.ts" +import { version } from "./version.ts" + +// ---------------------------------------------------------------------------- +// Cause +// ---------------------------------------------------------------------------- + +/** @internal */ +export class Interrupt extends ReasonBase<"Interrupt"> implements Cause.Interrupt { + readonly fiberId: number | undefined + constructor( + fiberId: number | undefined, + annotations = constEmptyAnnotations + ) { + super("Interrupt", annotations, "Interrupted") + this.fiberId = fiberId + } + override toString() { + return `Interrupt(${this.fiberId})` + } + toJSON(): unknown { + return { + _tag: "Interrupt", + fiberId: this.fiberId + } + } + [Equal.symbol](that: any): boolean { + return ( + isInterruptReason(that) && + this.fiberId === that.fiberId && + this.annotations === that.annotations + ) + } + [Hash.symbol](): number { + return Hash.combine(Hash.string(`${this._tag}:${this.fiberId}`))( + Hash.random(this.annotations) + ) + } +} + +/** @internal */ +export const makeInterruptReason = (fiberId?: number | undefined): Cause.Interrupt => new Interrupt(fiberId) + +/** @internal */ +export const causeInterrupt = ( + fiberId?: number | undefined +): Cause.Cause => new CauseImpl([new Interrupt(fiberId)]) + +/** @internal */ +export const hasFails = (self: Cause.Cause): boolean => self.reasons.some(isFailReason) + +/** @internal */ +export const findFail = (self: Cause.Cause): Result.Result, Cause.Cause> => { + const reason = self.reasons.find(isFailReason) + return reason ? Result.succeed(reason) : Result.fail(self as Cause.Cause) +} + +/** @internal */ +export const findError = (self: Cause.Cause): Result.Result> => { + for (let i = 0; i < self.reasons.length; i++) { + const reason = self.reasons[i] + if (reason._tag === "Fail") { + return Result.succeed(reason.error) + } + } + return Result.fail(self as Cause.Cause) +} + +/** @internal */ +export const findErrorOption = Filter.toOption(findError) + +/** @internal */ +export const hasDies = (self: Cause.Cause): boolean => self.reasons.some(isDieReason) + +/** @internal */ +export const findDie = (self: Cause.Cause): Result.Result> => { + const reason = self.reasons.find(isDieReason) + return reason ? Result.succeed(reason) : Result.fail(self) +} + +/** @internal */ +export const findDefect = (self: Cause.Cause): Result.Result> => { + const reason = self.reasons.find(isDieReason) + return reason ? Result.succeed(reason.defect) : Result.fail(self) +} + +/** @internal */ +export const hasInterrupts = (self: Cause.Cause): boolean => self.reasons.some(isInterruptReason) + +/** @internal */ +export const findInterrupt = (self: Cause.Cause): Result.Result> => { + const reason = self.reasons.find(isInterruptReason) + return reason ? Result.succeed(reason) : Result.fail(self) +} + +/** @internal */ +export const causeFilterInterruptors = ( + self: Cause.Cause +): Result.Result, Cause.Cause> => { + let interruptors: Set | undefined + for (let i = 0; i < self.reasons.length; i++) { + const f = self.reasons[i] + if (f._tag !== "Interrupt") continue + interruptors ??= new Set() + if (f.fiberId !== undefined) { + interruptors.add(f.fiberId) + } + } + return interruptors ? Result.succeed(interruptors) : Result.fail(self) +} + +/** @internal */ +export const causeInterruptors = (self: Cause.Cause): ReadonlySet => { + const result = causeFilterInterruptors(self) + return Result.isFailure(result) ? emptySet : result.success +} +const emptySet = new Set() + +/** @internal */ +export const hasInterruptsOnly = (self: Cause.Cause): boolean => + self.reasons.length > 0 && self.reasons.every(isInterruptReason) + +/** @internal */ +export const reasonAnnotations = ( + self: Cause.Reason +): Context.Context => Context.makeUnsafe(self.annotations) + +/** @internal */ +export const causeAnnotations = ( + self: Cause.Cause +): Context.Context => { + const map = new Map() + for (const f of self.reasons) { + if (f.annotations.size > 0) { + for (const [key, value] of f.annotations) { + map.set(key, value) + } + } + } + return Context.makeUnsafe(map) +} + +/** @internal */ +export const causeCombine: { + (that: Cause.Cause): (self: Cause.Cause) => Cause.Cause + (self: Cause.Cause, that: Cause.Cause): Cause.Cause +} = dual( + 2, + (self: Cause.Cause, that: Cause.Cause): Cause.Cause => { + if (self.reasons.length === 0) { + return that as Cause.Cause + } else if (that.reasons.length === 0) { + return self as Cause.Cause + } + const newCause = new CauseImpl( + Arr.union(self.reasons, that.reasons) + ) + return Equal.equals(self, newCause) ? self : newCause + } +) + +/** @internal */ +export const causeMap: { + (f: (error: NoInfer) => E2): (self: Cause.Cause) => Cause.Cause + (self: Cause.Cause, f: (error: NoInfer) => E2): Cause.Cause +} = dual( + 2, + (self: Cause.Cause, f: (error: NoInfer) => E2): Cause.Cause => { + let hasFail = false + const failures = self.reasons.map((failure) => { + if (isFailReason(failure)) { + hasFail = true + return new Fail(f(failure.error)) + } + return failure + }) + return hasFail ? causeFromReasons(failures) : self as any + } +) + +/** @internal */ +export const causePartition = ( + self: Cause.Cause +): { + readonly Fail: ReadonlyArray> + readonly Die: ReadonlyArray + readonly Interrupt: ReadonlyArray +} => { + const obj = { + Fail: [] as Array>, + Die: [] as Array, + Interrupt: [] as Array + } + for (let i = 0; i < self.reasons.length; i++) { + obj[self.reasons[i]._tag].push(self.reasons[i] as any) + } + return obj +} + +/** @internal */ +export const causeSquash = (self: Cause.Cause): unknown => { + const partitioned = causePartition(self) + if (partitioned.Fail.length > 0) { + return partitioned.Fail[0].error + } else if (partitioned.Die.length > 0) { + return partitioned.Die[0].defect + } else if (partitioned.Interrupt.length > 0) { + return new globalThis.Error("All fibers interrupted without error") + } + return new globalThis.Error("Empty cause") +} + +/** @internal */ +export const causePrettyErrors = (self: Cause.Cause, options?: { + readonly includeCauseInStack?: boolean | undefined +}): Array => { + const errors: Array = [] + const interrupts: Array = [] + if (self.reasons.length === 0) return errors + + const prevStackLimit = getStackTraceLimit() + setStackTraceLimit(1) + + for (const failure of self.reasons) { + if (failure._tag === "Interrupt") { + interrupts.push(failure) + continue + } + errors.push( + causePrettyError( + failure._tag === "Die" ? failure.defect : failure.error as any, + failure.annotations, + options + ) + ) + } + if (errors.length === 0) { + const cause = new Error("The fiber was interrupted by:") + cause.name = "InterruptCause" + cause.stack = interruptCauseStack(cause, interrupts) + const error = new globalThis.Error("All fibers interrupted without error", { cause }) + error.name = "InterruptError" + error.stack = `${error.name}: ${error.message}` + errors.push(causePrettyError(error, interrupts[0].annotations, options)) + } + + setStackTraceLimit(prevStackLimit) + return errors +} + +/** @internal */ +export const causePrettyError = ( + original: Record | Error, + annotations?: ReadonlyMap, + options?: { + readonly includeCauseInStack?: boolean | undefined + } +): Error => { + const kind = typeof original + let error: Error + if (original && kind === "object") { + error = new globalThis.Error(causePrettyMessage(original), { + cause: original.cause ? causePrettyError(original.cause as any) : undefined + }) + if (typeof original.name === "string") { + error.name = original.name + } + if (typeof original.stack === "string") { + error.stack = cleanErrorStack(original.stack, error, annotations) + } else { + const stack = `${error.name}: ${error.message}` + error.stack = annotations ? addStackAnnotations(stack, annotations) : stack + } + if (options?.includeCauseInStack) { + error.stack = renderPrettyError(error)! + } + for (const key of Object.keys(original)) { + if (!(key in error)) { + ;(error as any)[key] = (original as any)[key] + } + } + } else { + error = new globalThis.Error( + !original ? `Unknown error: ${original}` : kind === "string" ? original as any : formatJson(original) + ) + } + return error +} + +const causePrettyMessage = (u: Record | Error): string => { + if (typeof u.message === "string") { + return u.message + } else if ( + typeof u.toString === "function" + && u.toString !== Object.prototype.toString + && u.toString !== Array.prototype.toString + ) { + try { + return u.toString() + } catch { + // something's off, rollback to json + } + } + return formatJson(u) +} + +const locationRegExp = /\((.*)\)/g + +const cleanErrorStack = ( + stack: string, + error: Error, + annotations: ReadonlyMap | undefined +): string => { + const message = `${error.name}: ${error.message}` + const lines = (stack.startsWith(message) ? stack.slice(message.length) : stack).split("\n") + const out: Array = [message] + for (let i = 1; i < lines.length; i++) { + if (/(?:Generator\.next|~effect\/Effect)/.test(lines[i])) { + break + } + out.push(lines[i]) + } + return annotations ? addStackAnnotations(out.join("\n"), annotations) : out.join("\n") +} + +const addStackAnnotations = (stack: string, annotations: ReadonlyMap) => { + const frame = annotations?.get(CauseStackTrace.key) as StackFrame | undefined + if (frame) { + stack = `${stack}\n${currentStackTrace(frame)}` + } + return stack +} + +const interruptCauseStack = (error: Error, interrupts: Array): string => { + const out: Array = [`${error.name}: ${error.message}`] + for (const current of interrupts) { + const fiberId = current.fiberId !== undefined ? `#${current.fiberId}` : "unknown" + const frame = current.annotations.get(InterruptorStackTrace.key) as StackFrame | undefined + out.push(` at fiber (${fiberId})`) + if (frame) out.push(currentStackTrace(frame)) + } + return out.join("\n") +} + +const currentStackTrace = (frame: StackFrame): string => { + const out: Array = [] + let current: StackFrame | undefined = frame + let i = 0 + while (current && i < 10) { + const stack = current.stack() + if (stack) { + const locationMatchAll = stack.matchAll(locationRegExp) + let match = false + for (const [, location] of locationMatchAll) { + match = true + out.push(` at ${current.name} (${location})`) + } + if (!match) { + out.push(` at ${current.name} (${stack.replace(/^at /, "")})`) + } + } else { + out.push(` at ${current.name}`) + } + current = current.parent + i++ + } + return out.join("\n") +} + +/** @internal */ +export const causePretty = (cause: Cause.Cause): string => + causePrettyErrors(cause).map(renderPrettyError).join("\n") + +const renderPrettyError = (e: Error): string | undefined => + e.cause ? `${e.stack} {\n${renderErrorCause(e.cause as Error, " ")}\n}` : e.stack + +const renderErrorCause = (cause: Error, 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 Error, `${prefix} `)}\n${prefix}}` + } + return stack +} + +// ---------------------------------------------------------------------------- +// Fiber +// ---------------------------------------------------------------------------- + +/** @internal */ +export const FiberTypeId = `~effect/Fiber/${version}` as const + +const fiberVariance = { + _A: identity, + _E: identity +} + +const fiberIdStore = { id: 0 } + +/** @internal */ +export const getCurrentFiber = (): Fiber.Fiber | undefined => (globalThis as any)[currentFiberTypeId] + +/** @internal */ +export class FiberImpl implements Fiber.Fiber { + constructor( + context: Context.Context, + interruptible: boolean = true + ) { + this[FiberTypeId] = fiberVariance as any + this.setContext(context) + this.id = ++fiberIdStore.id + this.currentOpCount = 0 + this.interruptible = interruptible + this._stack = [] + this._observers = [] + this._exit = undefined + this._children = undefined + this._interruptedCause = undefined + this._yielded = undefined + this._running = false + this._deferredInterrupt = false + this.runtimeMetrics?.recordFiberStart(this.context) + } + + readonly [FiberTypeId]: Fiber.Fiber.Variance + + readonly id: number + interruptible: boolean + currentOpCount: number + readonly _stack: Array + readonly _observers: Array<(exit: Exit.Exit) => void> + _exit: Exit.Exit | undefined + _currentExit: Exit.Exit | undefined + _children: Set> | undefined + _interruptedCause: Cause.Cause | undefined + _yielded: Exit.Exit | (() => void) | undefined + _running: boolean + _deferredInterrupt: boolean + + // set in setContext + context!: Context.Context + currentScheduler!: Scheduler.Scheduler + currentTracerContext: Tracer.Tracer["context"] + currentSpan: Tracer.AnySpan | undefined + currentLogLevel!: LogLevel.LogLevel + minimumLogLevel!: LogLevel.LogLevel + currentStackFrame: StackFrame | undefined + runtimeMetrics: Metric.FiberRuntimeMetricsService | undefined + maxOpsBeforeYield!: number + currentPreventYield!: boolean + + _dispatcher: Scheduler.SchedulerDispatcher | undefined = undefined + get currentDispatcher(): Scheduler.SchedulerDispatcher { + return this._dispatcher ??= this.currentScheduler.makeDispatcher() + } + + getRef(ref: Context.Reference): X { + return Context.getReferenceUnsafe(this.context, ref) + } + addObserver(cb: (exit: Exit.Exit) => void): () => void { + if (this._exit) { + cb(this._exit) + return constVoid + } + this._observers.push(cb) + return () => { + const index = this._observers.indexOf(cb) + if (index >= 0) { + this._observers.splice(index, 1) + } + } + } + interruptUnsafe(fiberId?: number | undefined, annotations?: Context.Context | undefined): void { + if (this._exit) { + return + } + let cause = causeInterrupt(fiberId) + if (this.currentStackFrame) { + cause = causeAnnotate(cause, Context.make(CauseStackTrace, this.currentStackFrame)) + } + if (annotations) { + cause = causeAnnotate(cause, annotations) + } + this._interruptedCause = this._interruptedCause + ? causeCombine(this._interruptedCause, cause) + : cause + if (this.interruptible) { + if (this._running) { + this._deferredInterrupt = true + } else { + this.evaluate(failCause(this._interruptedCause) as any) + } + } + } + pollUnsafe(): Exit.Exit | undefined { + return this._exit + } + evaluate(effect: Primitive): void { + if (this._exit) { + return + } else if (this._yielded !== undefined) { + const yielded = this._yielded as () => void + this._yielded = undefined + yielded() + } + const exit = this.runLoop(effect) + if (exit === Yield) { + return + } + // the interruptChildren middleware is added in Effect.forkChild, so it can be + // tree-shaken if not used + const interruptChildren = fiberMiddleware.interruptChildren && + fiberMiddleware.interruptChildren(this) + if (interruptChildren !== undefined) { + return this.evaluate(flatMap(interruptChildren, () => exit) as any) + } + + this._exit = exit + this.runtimeMetrics?.recordFiberEnd(this.context, this._exit) + for (let i = 0; i < this._observers.length; i++) { + this._observers[i](exit) + } + this._observers.length = 0 + } + runLoop(effect: Primitive): Exit.Exit | Yield { + const prevFiber = (globalThis as any)[currentFiberTypeId] + ;(globalThis as any)[currentFiberTypeId] = this + const prevRunning = this._running + this._running = true + let yielding = false + let current: Primitive | Yield = effect + this.currentOpCount = 0 + try { + while (true) { + if (this._deferredInterrupt) { + this._deferredInterrupt = false + current = failCause(this._interruptedCause!) as any + } + this.currentOpCount++ + if ( + !yielding && + !this.currentPreventYield && + this.currentScheduler.shouldYield(this as any) + ) { + yielding = true + const prev = current + current = flatMap(yieldNow, () => prev as any) as any + } + current = this.currentTracerContext + ? this.currentTracerContext(current as any, this) + : (current as any)[evaluate](this) + if (current === Yield) { + const yielded = this._yielded! + if (ExitTypeId in yielded) { + this._deferredInterrupt = false + this._yielded = undefined + return yielded + } else if (this._deferredInterrupt) { + this._yielded = undefined + yielded() + continue + } + return Yield + } + } + } catch (error) { + if (!hasProperty(current, evaluate)) { + return exitDie(`Fiber.runLoop: Not a valid effect: ${String(current)}`) + } + return this.runLoop(exitDie(error) as any) + } finally { + this._running = prevRunning + ;(globalThis as any)[currentFiberTypeId] = prevFiber + } + } + getCont(symbol: S): + | (Primitive & Record Primitive>) + | undefined + { + if (this._deferredInterrupt) { + this._deferredInterrupt = false + return deferredInterruptCont + } + while (true) { + const op = this._stack.pop() + if (!op) return undefined + const cont = op[contAll] && op[contAll](this) + if (cont) { + ;(cont as any)[symbol] = cont + return cont as any + } + if (op[symbol]) return op as any + } + } + yieldWith(value: Exit.Exit | (() => void)): Yield { + this._yielded = value + return Yield + } + children(): Set> { + return (this._children ??= new Set()) + } + pipe() { + return pipeArguments(this, arguments) + } + setContext(context: Context.Context): void { + this.context = context + const scheduler = this.getRef(Scheduler.Scheduler) + if (scheduler !== this.currentScheduler) { + this.currentScheduler = scheduler + this._dispatcher = undefined + } + this.currentSpan = context.mapUnsafe.get(Tracer.ParentSpanKey) + this.currentLogLevel = this.getRef(CurrentLogLevel) + this.minimumLogLevel = this.getRef(MinimumLogLevel) + this.currentStackFrame = context.mapUnsafe.get(CurrentStackFrame.key) + this.maxOpsBeforeYield = this.getRef(Scheduler.MaxOpsBeforeYield) + this.currentPreventYield = this.getRef(Scheduler.PreventSchedulerYield) + this.runtimeMetrics = context.mapUnsafe.get(InternalMetric.FiberRuntimeMetricsKey) + const currentTracer = context.mapUnsafe.get(Tracer.TracerKey) + this.currentTracerContext = currentTracer ? currentTracer["context"] : undefined + } + get currentSpanLocal(): Tracer.Span | undefined { + return this.currentSpan?._tag === "Span" ? this.currentSpan : undefined + } +} + +const deferredInterruptCont: any = { + [contA](_value: unknown, fiber: FiberImpl) { + return failCause(fiber._interruptedCause!) + }, + [contE](_cause: unknown, fiber: FiberImpl) { + return failCause(fiber._interruptedCause!) + } +} + +const fiberMiddleware = { + interruptChildren: undefined as + | ((fiber: FiberImpl) => Effect.Effect | undefined) + | undefined +} + +const fiberStackAnnotations = (fiber: Fiber.Fiber) => { + if (!fiber.currentStackFrame) return undefined + const annotations = new Map() + annotations.set(CauseStackTrace.key, fiber.currentStackFrame) + return Context.makeUnsafe(annotations) +} + +const fiberInterruptChildren = (fiber: FiberImpl) => { + if (fiber._children === undefined || fiber._children.size === 0) { + return undefined + } + return fiberInterruptAll(fiber._children) +} + +/** @internal */ +export const fiberAwait = ( + self: Fiber.Fiber +): Effect.Effect> => { + const impl = self as FiberImpl + if (impl._exit) return succeed(impl._exit) + return callback((resume) => { + if (impl._exit) return resume(succeed(impl._exit)) + return sync(self.addObserver((exit) => resume(succeed(exit)))) + }) +} + +/** @internal */ +export const fiberAwaitAll = >( + self: Iterable +): Effect.Effect< + Array< + Exit.Exit< + Fiber extends Fiber.Fiber ? _A : never, + Fiber extends Fiber.Fiber ? _E : never + > + > +> => + callback((resume) => { + const iter = self[Symbol.iterator]() as Iterator + const exits: Array> = [] + let cancel: (() => void) | undefined = undefined + function loop() { + let result = iter.next() + while (!result.done) { + if (result.value._exit) { + exits.push(result.value._exit) + result = iter.next() + continue + } + cancel = result.value.addObserver((exit) => { + exits.push(exit) + loop() + }) + return + } + resume(succeed(exits)) + } + loop() + return sync(() => cancel?.()) + }) + +/** @internal */ +export const fiberJoin = (self: Fiber.Fiber): Effect.Effect => { + const impl = self as FiberImpl + if (impl._exit) return impl._exit + return callback((resume) => { + if (impl._exit) return resume(impl._exit) + return sync(self.addObserver(resume)) + }) +} + +/** @internal */ +export const fiberJoinAll = >>(self: A): Effect.Effect< + Arr.ReadonlyArray.With> ? _A : never>, + A extends Fiber.Fiber ? _E : never +> => + callback((resume) => { + const fibers = Array.from(self) + if (fibers.length === 0) return resume(succeed(Arr.empty() as any)) + const out = new Array(fibers.length) as Arr.NonEmptyArray + const cancels = Arr.empty<() => void>() + let done = 0 + let failed = false + for (let i = 0; i < fibers.length; i++) { + if (failed) break + cancels.push(fibers[i].addObserver((exit) => { + done++ + if (exit._tag === "Failure") { + failed = true + cancels.forEach((cancel) => cancel()) + return resume(exit as any) + } + out[i] = exit.value + if (done === fibers.length) { + resume(succeed(out)) + } + })) + } + }) + +/** @internal */ +export const fiberInterrupt = ( + self: Fiber.Fiber +): Effect.Effect => withFiber((fiber) => fiberInterruptAs(self, fiber.id)) + +/** @internal */ +export const fiberInterruptAs: { + ( + fiberId: number | undefined, + annotations?: Context.Context | undefined + ): (self: Fiber.Fiber) => Effect.Effect + ( + self: Fiber.Fiber, + fiberId: number | undefined, + annotations?: Context.Context | undefined + ): Effect.Effect +} = dual( + (args) => hasProperty(args[0], FiberTypeId), + ( + self: Fiber.Fiber, + fiberId: number | undefined, + annotations?: Context.Context | undefined + ): Effect.Effect => + withFiber((parent) => { + let ann = fiberStackAnnotations(parent) + ann = ann && annotations ? Context.merge(ann, annotations) : ann ?? annotations + self.interruptUnsafe(fiberId, ann) + return asVoid(fiberAwait(self)) + }) +) + +/** @internal */ +export const fiberInterruptAll = >>( + fibers: A +): Effect.Effect => + withFiber((parent) => { + const annotations = fiberStackAnnotations(parent) + let fiberArr = Arr.empty>() + for (const fiber of fibers) { + fiber.interruptUnsafe(parent.id, annotations) + fiberArr.push(fiber) + } + return asVoid(fiberAwaitAll(fiberArr)) + }) + +/** @internal */ +export const fiberInterruptAllAs: { + (fiberId: number): >>(fibers: A) => Effect.Effect + >>(fibers: A, fiberId: number): Effect.Effect +} = dual(2, >>( + fibers: A, + fiberId: number +): Effect.Effect => + withFiber((parent) => { + const annotations = fiberStackAnnotations(parent) + const fiberArr = Arr.empty>() + for (const fiber of fibers) { + fiber.interruptUnsafe(fiberId, annotations) + fiberArr.push(fiber) + } + return asVoid(fiberAwaitAll(fiberArr)) + })) + +/** @internal */ +export const succeed: (value: A) => Effect.Effect = exitSucceed + +/** @internal */ +export const failCause: (cause: Cause.Cause) => Effect.Effect = exitFailCause + +/** @internal */ +export const fail: (error: E) => Effect.Effect = exitFail + +/** @internal */ +export const sync: (thunk: LazyArg) => Effect.Effect = makePrimitive({ + op: "Sync", + [evaluate](fiber): Primitive | Yield { + const value = this[args]() + const cont = fiber.getCont(contA) + return cont ? cont[contA](value, fiber) : fiber.yieldWith(exitSucceed(value)) + } +}) + +/** @internal */ +export const suspend: ( + evaluate: LazyArg> +) => Effect.Effect = makePrimitive({ + op: "Suspend", + [evaluate](_fiber) { + return this[args]() + } +}) + +/** @internal */ +export const fromOption: | LazyArg, E = Cause.NoSuchElementError>( + arg: Arg, + ...rest: [Arg] extends [Option.Option] ? [onNone?: LazyArg] : [] +) => [Arg] extends [Option.Option] ? Effect.Effect + : [Arg] extends [LazyArg] ? (option: Option.Option) => Effect.Effect + : never = dual( + (args) => args.length >= 2 || Option.isOption(args[0]), + (option: Option.Option, onNone?: LazyArg): Effect.Effect => + Option.isNone(option) + ? fail(onNone ? onNone() : new NoSuchElementError("Effect.fromOption: Option.none")) + : succeed(option.value) + ) + +/** @internal */ +export const fromResult: (result: Result.Result) => Effect.Effect = Result.match({ + onFailure: fail, + onSuccess: succeed +}) + +/** @internal */ +export const fromNullishOr = (value: A): Effect.Effect, Cause.NoSuchElementError> => + value == null ? fail(new NoSuchElementError()) : succeed(value) + +/** @internal */ +export const yieldNowWith: (priority?: number) => Effect.Effect = makePrimitive({ + op: "Yield", + [evaluate](fiber) { + let resumed = false + fiber.currentDispatcher.scheduleTask(() => { + if (resumed) return + fiber.evaluate(exitVoid as any) + }, this[args] ?? 0) + return fiber.yieldWith(() => { + resumed = true + }) + } +}) + +/** @internal */ +export const yieldNow: Effect.Effect = yieldNowWith(0) + +/** @internal */ +export const succeedSome = (a: A): Effect.Effect> => succeed(Option.some(a)) + +/** @internal */ +export const succeedNone: Effect.Effect> = succeed( + Option.none() +) + +/** @internal */ +export const transposeOption = ( + self: Option.Option> +): Effect.Effect, E, R> => Option.isNone(self) ? succeedNone : map(self.value, Option.some) + +/** @internal */ +export const failCauseSync = ( + evaluate: LazyArg> +): Effect.Effect => suspend(() => failCause(internalCall(evaluate))) + +/** @internal */ +export const die = (defect: unknown): Effect.Effect => exitDie(defect) + +/** @internal */ +export const failSync = (error: LazyArg): Effect.Effect => suspend(() => fail(internalCall(error))) + +/** @internal */ +const void_: Effect.Effect = succeed(void 0) +/** @internal */ +export { void_ as void } + +/** @internal */ +const try_ = ( + options: { + readonly try: LazyArg + readonly catch: (error: unknown) => E + } | LazyArg +): Effect.Effect => { + const evaluate = typeof options === "function" ? options : options.try + const catcher = typeof options === "function" + ? ((cause: unknown) => new UnknownError(cause, "An error occurred in Effect.try")) + : options.catch + return suspend(() => { + try { + return succeed(internalCall(evaluate)) + } catch (err) { + return fail(internalCall(() => catcher(err)) as E) + } + }) +} +/** @internal */ +export { try_ as try } + +/** @internal */ +export const promise = ( + evaluate: (signal: AbortSignal) => PromiseLike +): Effect.Effect => + callbackOptions(function(resume, signal) { + internalCall(() => evaluate(signal!)).then( + (a) => resume(succeed(a)), + (e) => resume(die(e)) + ) + }, evaluate.length !== 0) + +/** @internal */ +export const tryPromise = ( + options: { + readonly try: (signal: AbortSignal) => PromiseLike + readonly catch: (error: unknown) => E + } | ((signal: AbortSignal) => PromiseLike) +): Effect.Effect => { + const f = typeof options === "function" ? options : options.try + const catcher = typeof options === "function" + ? ((cause: unknown) => new UnknownError(cause, "An error occurred in Effect.tryPromise")) + : options.catch + return callbackOptions(function(resume, signal) { + const failWithCatch = (cause: unknown) => { + try { + resume(fail(internalCall(() => catcher(cause)) as E)) + } catch (err) { + resume(die(err)) + } + } + try { + internalCall(() => f(signal!)).then( + (a) => resume(succeed(a)), + failWithCatch + ) + } catch (err) { + failWithCatch(err) + } + }, f.length !== 0) +} + +/** @internal */ +export const withFiberId = ( + f: (fiberId: number) => Effect.Effect +): Effect.Effect => withFiber((fiber) => f(fiber.id)) + +/** @internal */ +export const fiber = withFiber(succeed) + +/** @internal */ +export const fiberId = withFiberId(succeed) + +const callbackOptions: ( + register: ( + this: Scheduler.Scheduler, + resume: (effect: Effect.Effect) => void, + signal?: AbortSignal + ) => void | Effect.Effect, + withSignal: boolean +) => Effect.Effect = makePrimitive({ + op: "Async", + single: false, + [evaluate](fiber) { + const register = internalCall(() => this[args][0].bind(fiber.currentScheduler)) + let resumed = false + let yielded: boolean | Primitive = false + const controller = this[args][1] ? new AbortController() : undefined + const onCancel = register((effect) => { + if (resumed) return + resumed = true + if (yielded) { + fiber.evaluate(effect as any) + } else { + yielded = effect as any + } + }, controller?.signal) + if (yielded !== false) return yielded + yielded = true + fiber._yielded = () => { + resumed = true + } + if (controller === undefined && onCancel === undefined) { + return Yield + } + fiber._stack.push( + asyncFinalizer(() => { + resumed = true + controller?.abort() + return onCancel ?? exitVoid + }) + ) + return Yield + } +}) + +const asyncFinalizer: ( + onInterrupt: () => Effect.Effect +) => Primitive = makePrimitive({ + op: "AsyncFinalizer", + [contAll](fiber) { + if (fiber.interruptible) { + fiber.interruptible = false + fiber._stack.push(setInterruptibleTrue) + } + }, + [contE](cause, _fiber) { + return hasInterrupts(cause) + ? flatMap(this[args](), () => failCause(cause)) + : failCause(cause) + } +}) + +/** @internal */ +export const callback = ( + register: ( + this: Scheduler.Scheduler, + resume: (effect: Effect.Effect) => void, + signal: AbortSignal + ) => void | Effect.Effect +): Effect.Effect => callbackOptions(register as any, register.length >= 2) + +/** @internal */ +export const never: Effect.Effect = callback(constVoid) + +/** @internal */ +export const gen = < + Self, + Eff extends Effect.Effect, + AEff +>( + ...args: + | [options: { readonly self: Self }, body: (this: Self) => Generator] + | [body: () => Generator] +): Effect.Effect< + AEff, + [Eff] extends [never] ? never + : [Eff] extends [Effect.Effect] ? E + : never, + [Eff] extends [never] ? never + : [Eff] extends [Effect.Effect] ? R + : never +> => + suspend(() => + fromIteratorUnsafe( + args.length === 1 ? args[0]() : (args[1].call(args[0].self) as any) + ) + ) + +/** @internal */ +export const fnUntraced: Effect.fn.Untraced = ( + body: Function, + ...pipeables: Array +) => { + const fn = pipeables.length === 0 + ? function(this: any) { + return suspend(() => fromIteratorUnsafe(body.apply(this, arguments))) + } + : function(this: any) { + let effect = suspend(() => fromIteratorUnsafe(body.apply(this, arguments))) + for (let i = 0; i < pipeables.length; i++) { + effect = pipeables[i](effect, ...arguments) + } + return effect + } + return defineFunctionLength(body.length, fn) +} + +const defineFunctionLength = (length: number, fn: F): F => + Object.defineProperty(fn, "length", { + value: length, + configurable: true + }) + +const fnStackCleaner = makeStackCleaner(2) + +/** @internal */ +export const fn: typeof Effect.fn = function() { + const nameFirst = typeof arguments[0] === "string" + const name = nameFirst ? arguments[0] : "Effect.fn" + const spanOptions = nameFirst ? arguments[1] : undefined + + const prevLimit = getStackTraceLimit() + setStackTraceLimit(2) + const defError = new globalThis.Error() + setStackTraceLimit(prevLimit) + + if (nameFirst) { + return (body: Function | { readonly self: any }, ...pipeables: Array) => + makeFn(name, body, defError, pipeables, nameFirst, spanOptions) + } + + return makeFn( + name, + arguments[0], + defError, + Array.prototype.slice.call(arguments, 1), + nameFirst, + spanOptions + ) +} as any + +const makeFn = ( + name: string, + bodyOrOptions: Function | { readonly self: any }, + defError: Error, + pipeables: Array, + addSpan: boolean, + spanOptions: Tracer.SpanOptionsNoTrace | undefined +) => { + const body = typeof bodyOrOptions === "function" + ? bodyOrOptions + : (pipeables.pop()!).bind(bodyOrOptions.self) + + return defineFunctionLength(body.length, function(this: any, ...args: Array) { + let result = suspend(() => { + const iter = body.apply(this, arguments) + return isEffect(iter) ? iter : fromIteratorUnsafe(iter) + }) + for (let i = 0; i < pipeables.length; i++) { + result = pipeables[i](result, ...args) + } + if (!isEffect(result)) { + return result + } + const prevLimit = getStackTraceLimit() + setStackTraceLimit(2) + const callError = new globalThis.Error() + setStackTraceLimit(prevLimit) + return updateService( + addSpan ? + useSpan(name, spanOptions!, (span) => provideParentSpan(result, span)) : + result, + CurrentStackFrame, + (prev) => ({ + name, + stack: fnStackCleaner(() => callError.stack), + parent: { + name: `${name} (definition)`, + stack: fnStackCleaner(() => defError.stack), + parent: prev + } + }) + ) + }) +} + +/** @internal */ +export const fnUntracedEager: Effect.fn.Untraced = ( + body: Function, + ...pipeables: Array +) => + defineFunctionLength( + body.length, + pipeables.length === 0 + ? function(this: any) { + return fromIteratorEagerUnsafe(() => body.apply(this, arguments)) + } + : function(this: any) { + let effect = fromIteratorEagerUnsafe(() => body.apply(this, arguments)) + for (const pipeable of pipeables) { + effect = pipeable(effect) + } + return effect + } + ) + +const fromIteratorEagerUnsafe = ( + evaluate: () => Iterator> +): Effect.Effect => { + try { + const iterator = evaluate() + let value: any = undefined + + // Try to resolve synchronously in a loop + while (true) { + const state = iterator.next(value) + + if (state.done) { + return succeed(state.value) + } + + const primitive = state.value as any + + if (primitive && primitive._tag === "Success") { + value = primitive.value + continue + } else if (primitive && primitive._tag === "Failure") { + return state.value + } else { + let isFirstExecution = true + + return suspend(() => { + if (isFirstExecution) { + isFirstExecution = false + return flatMap(state.value, (value) => fromIteratorUnsafe(iterator, value)) + } else { + return suspend(() => fromIteratorUnsafe(evaluate())) + } + }) + } + } + } catch (error) { + return die(error) + } +} + +const fromIteratorUnsafe: ( + iterator: Iterator>, + initial?: undefined +) => Effect.Effect = makePrimitive({ + op: "Iterator", + single: false, + [contA](value, fiber) { + const iter = this[args][0] + while (true) { + const state = iter.next(value) + if (state.done) return succeed(state.value) + if (!effectIsExit(state.value)) { + fiber._stack.push(this) + return state.value + } else if (state.value._tag === "Failure") { + return state.value + } + value = state.value.value + } + }, + [evaluate](this: any, fiber: FiberImpl) { + return this[contA](this[args][1], fiber) + } +}) + +// ---------------------------------------------------------------------------- +// mapping & sequencing +// ---------------------------------------------------------------------------- + +/** @internal */ +export const as: { + ( + value: B + ): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, value: B): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + value: B + ): Effect.Effect => { + const b = succeed(value) + return flatMap(self, (_) => b) + } +) + +/** @internal */ +export const asSome = ( + self: Effect.Effect +): Effect.Effect, E, R> => map(self, Option.some) + +/** @internal */ +export const flip = ( + self: Effect.Effect +): Effect.Effect => + matchEffect(self, { + onFailure: succeed, + onSuccess: fail + }) + +/** @internal */ +export const andThen: { + ( + f: (a: A) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + f: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect + ( + self: Effect.Effect, + f: Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: ((a: A) => Effect.Effect) | Effect.Effect + ): Effect.Effect => + flatMap(self, (a) => isEffect(f) ? f : internalCall(() => (f as (a: A) => Effect.Effect)(a))) +) + +/** @internal */ +export const tap: { + ( + f: (a: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + f: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: NoInfer) => Effect.Effect + ): Effect.Effect + ( + self: Effect.Effect, + f: Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: ((a: A) => Effect.Effect) | Effect.Effect + ): Effect.Effect => + flatMap(self, (a) => as(isEffect(f) ? f : internalCall(() => (f as (a: A) => Effect.Effect)(a)), a)) +) + +/** @internal */ +export const asVoid = ( + self: Effect.Effect +): Effect.Effect => flatMap(self, (_) => exitVoid) + +/** @internal */ +export const sandbox = ( + self: Effect.Effect +): Effect.Effect, R> => catchCause(self, fail) + +/** @internal */ +export const raceAll = >( + all: Iterable, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } +): Effect.Effect< + Effect.Success, + Effect.Error, + Effect.Services +> => + withFiber((parent) => + callback((resume) => { + const effects = Arr.fromIterable(all) + const len = effects.length + let doneCount = 0 + let done = false + const fibers = new Set>() + const failures: Array> = [] + const onExit = (exit: Exit.Exit, fiber: Fiber.Fiber, i: number) => { + doneCount++ + if (exit._tag === "Failure") { + failures.push(...exit.cause.reasons) + if (doneCount >= len) { + resume(failCause(causeFromReasons(failures))) + } + return + } + const isWinner = !done + done = true + resume( + fibers.size === 0 + ? exit + : flatMap(uninterruptible(fiberInterruptAll(fibers)), () => exit) + ) + if (isWinner && options?.onWinner) { + options.onWinner({ fiber, index: i, parentFiber: parent }) + } + } + + for (let i = 0; i < len; i++) { + const fiber = forkUnsafe(parent, effects[i], true, true, false) + fibers.add(fiber) + fiber.addObserver((exit) => { + fibers.delete(fiber) + onExit(exit, fiber, i) + }) + if (done) break + } + + return fiberInterruptAll(fibers) + }) + ) + +/** @internal */ +export const raceAllFirst = >( + all: Iterable, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } +): Effect.Effect< + Effect.Success, + Effect.Error, + Effect.Services +> => + withFiber((parent) => + callback((resume) => { + let done = false + const fibers = new Set>() + const onExit = (exit: Exit.Exit) => { + done = true + resume( + fibers.size === 0 + ? exit + : flatMap(uninterruptible(fiberInterruptAll(fibers)), () => exit) + ) + } + + let i = 0 + for (const effect of all) { + if (done) break + const index = i++ + const fiber = forkUnsafe(parent, effect, true, true, false) + fibers.add(fiber) + fiber.addObserver((exit) => { + fibers.delete(fiber) + const isWinner = !done + onExit(exit) + if (isWinner && options?.onWinner) { + options.onWinner({ fiber, index, parentFiber: parent }) + } + }) + } + + return fiberInterruptAll(fibers) + }) + ) + +/** @internal */ +export const race: { + ( + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): Effect.Effect +} = dual( + (args) => isEffect(args[1]), + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): Effect.Effect => raceAll([self, that], options) +) + +/** @internal */ +export const raceFirst: { + ( + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): Effect.Effect +} = dual( + (args) => isEffect(args[1]), + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { + readonly onWinner?: (options: { + readonly fiber: Fiber.Fiber + readonly index: number + readonly parentFiber: Fiber.Fiber + }) => void + } + ): Effect.Effect => raceAllFirst([self, that], options) +) + +/** @internal */ +export const flatMap: { + ( + f: (a: A) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect => { + const onSuccess = Object.create(OnSuccessProto) + onSuccess[args] = self + onSuccess[contA] = f.length !== 1 ? (a: A) => f(a) : f + return onSuccess + } +) +const OnSuccessProto = makePrimitiveProto({ + op: "OnSuccess", + [evaluate](this: any, fiber: FiberImpl): Primitive { + fiber._stack.push(this) + return this[args] + } +}) + +/** @internal */ +export const matchCauseEffectEager: { + (options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + }): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect => { + if (effectIsExit(self)) { + return self._tag === "Success" + ? options.onSuccess(self.value) + : options.onFailure(self.cause) + } + return matchCauseEffect(self, options) + } +) + +/** @internal */ +export const effectIsExit = (effect: Effect.Effect): effect is Exit.Exit => ExitTypeId in effect + +/** @internal */ +export const flatMapEager: { + ( + f: (a: A) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (a: A) => Effect.Effect + ): Effect.Effect => { + if (effectIsExit(self)) { + return self._tag === "Success" ? f(self.value) : self as Exit.Exit + } + return flatMap(self, f) + } +) + +// ---------------------------------------------------------------------------- +// mapping & sequencing +// ---------------------------------------------------------------------------- + +/** @internal */ +export const flatten = ( + self: Effect.Effect, E2, R2> +): Effect.Effect => flatMap(self, identity) + +/** @internal */ +export const map: { + ( + f: (a: A) => B + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => B + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (a: A) => B + ): Effect.Effect => flatMap(self, (a) => succeed(internalCall(() => f(a)))) +) + +/** @internal */ +export const mapEager: { + ( + f: (a: A) => B + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (a: A) => B + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (a: A) => B + ): Effect.Effect => effectIsExit(self) ? exitMap(self, f) : map(self, f) +) + +/** @internal */ +export const mapErrorEager: { + ( + f: (e: E) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: E) => E2 + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: E) => E2 + ): Effect.Effect => effectIsExit(self) ? exitMapError(self, f) : mapError(self, f) +) + +/** @internal */ +export const mapBothEager: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect.Effect => effectIsExit(self) ? exitMapBoth(self, options) : mapBoth(self, options) +) + +/** @internal */ +export const catchEager: { + ( + f: (e: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: NoInfer) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: NoInfer) => Effect.Effect + ): Effect.Effect => { + if (effectIsExit(self)) { + if (self._tag === "Success") return self as Exit.Exit + const error = findError(self.cause) + if (Result.isFailure(error)) return self as Exit.Exit + return f(error.success) + } + return catch_(self, f) + } +) + +// ---------------------------------------------------------------------------- +// Exit +// ---------------------------------------------------------------------------- + +/** @internal */ +export const exitInterrupt = (fiberId?: number | undefined): Exit.Exit => exitFailCause(causeInterrupt(fiberId)) + +/** @internal */ +export const exitIsSuccess = ( + self: Exit.Exit +): self is Exit.Success => self._tag === "Success" + +/** @internal */ +export const exitFilterSuccess = ( + self: Exit.Exit +): Result.Result, Exit.Failure> => + self._tag === "Success" ? Result.succeed(self as any) : Result.fail(self as any) + +/** @internal */ +export const exitFilterValue = ( + self: Exit.Exit +): Result.Result> => + self._tag === "Success" ? Result.succeed(self.value) : Result.fail(self as any) + +/** @internal */ +export const exitIsFailure = ( + self: Exit.Exit +): self is Exit.Failure => self._tag === "Failure" + +/** @internal */ +export const exitFilterFailure = ( + self: Exit.Exit +): Result.Result, Exit.Success> => + self._tag === "Failure" ? Result.succeed(self as any) : Result.fail(self as any) + +/** @internal */ +export const exitFilterCause = ( + self: Exit.Exit +): Result.Result, Exit.Success> => + self._tag === "Failure" ? Result.succeed(self.cause) : Result.fail(self as any) + +/** @internal */ +export const exitFindError = Filter.composePassthrough( + exitFilterCause, + findError +) + +/** @internal */ +export const exitFindDefect = Filter.composePassthrough( + exitFilterCause, + findDefect +) + +/** @internal */ +export const exitHasInterrupts = ( + self: Exit.Exit +): self is Exit.Failure => self._tag === "Failure" && hasInterrupts(self.cause) + +/** @internal */ +export const exitHasDies = ( + self: Exit.Exit +): self is Exit.Failure => self._tag === "Failure" && hasDies(self.cause) + +/** @internal */ +export const exitHasFails = ( + self: Exit.Exit +): self is Exit.Failure => self._tag === "Failure" && hasFails(self.cause) + +/** @internal */ +export const exitVoid: Exit.Exit = exitSucceed(void 0) + +/** @internal */ +export const exitMap: { + (f: (a: A) => B): (self: Exit.Exit) => Exit.Exit + (self: Exit.Exit, f: (a: A) => B): Exit.Exit +} = dual( + 2, + (self: Exit.Exit, f: (a: A) => B): Exit.Exit => + self._tag === "Success" ? exitSucceed(f(self.value)) : (self as any) +) + +/** @internal */ +export const exitMapError: { + (f: (a: NoInfer) => E2): (self: Exit.Exit) => Exit.Exit + (self: Exit.Exit, f: (a: NoInfer) => E2): Exit.Exit +} = dual( + 2, + (self: Exit.Exit, f: (a: NoInfer) => E2): Exit.Exit => { + if (self._tag === "Success") return self as Exit.Exit + const error = findError(self.cause) + if (Result.isFailure(error)) return self as Exit.Exit + return exitFail(f(error.success)) + } +) + +/** @internal */ +export const exitMapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Exit.Exit) => Exit.Exit + ( + self: Exit.Exit, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Exit.Exit +} = dual( + 2, + ( + self: Exit.Exit, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Exit.Exit => { + if (self._tag === "Success") return exitSucceed(options.onSuccess(self.value)) + const error = findError(self.cause) + if (Result.isFailure(error)) return self as Exit.Exit + return exitFail(options.onFailure(error.success)) + } +) + +/** @internal */ +export const exitAs: { + (b: B): (self: Exit.Exit) => Exit.Exit + (self: Exit.Exit, b: B): Exit.Exit +} = dual( + 2, + (self: Exit.Exit, b: B): Exit.Exit => exitIsSuccess(self) ? exitSucceed(b) : (self as any) +) + +/** @internal */ +export const exitZipRight: { + ( + that: Exit.Exit + ): (self: Exit.Exit) => Exit.Exit + ( + self: Exit.Exit, + that: Exit.Exit + ): Exit.Exit +} = dual( + 2, + ( + self: Exit.Exit, + that: Exit.Exit + ): Exit.Exit => (exitIsSuccess(self) ? that : (self as any)) +) + +/** @internal */ +export const exitMatch: { + (options: { + readonly onSuccess: (a: NoInfer) => X1 + readonly onFailure: (cause: Cause.Cause>) => X2 + }): (self: Exit.Exit) => X1 | X2 + ( + self: Exit.Exit, + options: { + readonly onSuccess: (a: A) => X1 + readonly onFailure: (cause: Cause.Cause) => X2 + } + ): X1 | X2 +} = dual( + 2, + ( + self: Exit.Exit, + options: { + readonly onSuccess: (a: A) => X1 + readonly onFailure: (cause: Cause.Cause) => X2 + } + ): X1 | X2 => + exitIsSuccess(self) + ? options.onSuccess(self.value) + : options.onFailure(self.cause) +) + +/** @internal */ +export const exitAsVoid: (self: Exit.Exit) => Exit.Exit = exitAs(void 0) + +/** @internal */ +export const exitAsVoidAll = >>( + exits: I +): Exit.Exit< + void, + I extends Iterable> ? _E : never +> => { + const failures: Array> = [] + for (const exit of exits) { + if (exit._tag === "Failure") { + failures.push(...exit.cause.reasons) + } + } + return failures.length === 0 ? exitVoid : exitFailCause(causeFromReasons(failures)) +} + +/** @internal */ +export const exitGetSuccess = (self: Exit.Exit): Option.Option => + exitIsSuccess(self) ? Option.some(self.value) : Option.none() + +/** @internal */ +export const exitGetCause = (self: Exit.Exit): Option.Option> => + exitIsFailure(self) ? Option.some(self.cause) : Option.none() + +/** @internal */ +export const exitFindErrorOption = (self: Exit.Exit): Option.Option => { + const error = exitFindError(self) + return Result.isFailure(error) ? Option.none() : Option.some(error.success) +} + +// ---------------------------------------------------------------------------- +// environment +// ---------------------------------------------------------------------------- + +/** @internal */ +export const service = (service: Context.Key): Effect.Effect => service + +/** @internal */ +export const serviceOption = ( + service: Context.Key +): Effect.Effect> => withFiber((fiber) => succeed(Context.getOption(fiber.context, service))) + +/** @internal */ +export const serviceOptional = ( + service: Context.Key +): Effect.Effect => + withFiber((fiber) => + fiber.context.mapUnsafe.has(service.key) + ? succeed(Context.getUnsafe(fiber.context, service)) + : fail(new NoSuchElementError()) + ) + +/** @internal */ +export const updateContext: { + ( + f: (context: Context.Context) => Context.Context> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (context: Context.Context) => Context.Context> + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (context: Context.Context) => Context.Context> + ): Effect.Effect => + withFiber((fiber) => { + const prevContext = fiber.context as Context.Context + const nextContext = f(prevContext) + if (prevContext === nextContext) return self as any + fiber.setContext(nextContext) + return onExitPrimitive(self, () => { + fiber.setContext(prevContext) + return undefined + }) + }) +) + +/** @internal */ +export const updateService: { + ( + service: Context.Key, + f: (value: A) => A + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + service: Context.Key, + f: (value: A) => A + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + service: Context.Key, + f: (value: A) => A + ): Effect.Effect => + updateContext(self, (s) => { + const prev = Context.getUnsafe(s, service) + const next = f(prev) + if (prev === next) return s + return Context.add(s, service, next) + }) +) + +/** @internal */ +export const context = (): Effect.Effect> => getContext as any +const getContext = withFiber((fiber) => succeed(fiber.context)) + +/** @internal */ +export const contextWith = ( + f: (context: Context.Context) => Effect.Effect +): Effect.Effect => withFiber((fiber) => f(fiber.context as Context.Context)) + +/** @internal */ +export const setContext: { + ( + context: Context.Context + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + context: Context.Context + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + context: Context.Context + ): Effect.Effect => updateContext(self, constant(context)) +) + +/** @internal */ +export const provideContext: { + ( + context: Context.Context + ): ( + self: Effect.Effect + ) => Effect.Effect> + ( + self: Effect.Effect, + context: Context.Context + ): Effect.Effect> +} = dual( + 2, + ( + self: Effect.Effect, + context: Context.Context + ): Effect.Effect> => { + if (effectIsExit(self)) return self as any + return updateContext(self, Context.merge(context)) as any + } +) + +/** @internal */ +export const provideService: { + ( + service: Context.Key + ): { + (implementation: S): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, implementation: S): Effect.Effect> + } + ( + key: Context.Key, + implementation: S + ): ( + self: Effect.Effect + ) => Effect.Effect> + ( + self: Effect.Effect, + service: Context.Key, + implementation: S + ): Effect.Effect> +} = function(this: any) { + if (arguments.length === 1) { + return dual(2, (self, impl) => provideServiceImpl(self, arguments[0], impl)) as any + } + return dual(3, (self, service, impl) => provideServiceImpl(self, service, impl)) + .apply(this, arguments as any) as any +} + +const provideServiceImpl = ( + self: Effect.Effect, + service: Context.Key, + implementation: S +): Effect.Effect> => + updateContext(self, (s) => { + const prev = s.mapUnsafe.get(service.key) + if (prev === implementation) return s + return Context.add(s, service, implementation) + }) as any + +/** @internal */ +export const provideServiceEffect: { + ( + service: Context.Key, + acquire: Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect | R2> + ( + self: Effect.Effect, + service: Context.Key, + acquire: Effect.Effect + ): Effect.Effect | R2> +} = dual( + 3, + ( + self: Effect.Effect, + service: Context.Key, + acquire: Effect.Effect + ): Effect.Effect | R2> => + flatMap(acquire, (implementation) => provideService(self, service, implementation)) +) + +/** @internal */ +export const withConcurrency: { + ( + concurrency: "unbounded" | number + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + concurrency: "unbounded" | number + ): Effect.Effect +} = provideService(CurrentConcurrency) + +// ---------------------------------------------------------------------------- +// zipping +// ---------------------------------------------------------------------------- + +/** @internal */ +export const zip: { + ( + that: Effect.Effect, + options?: { readonly concurrent?: boolean | undefined } | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect<[A, A2], E2 | E, R2 | R> + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { readonly concurrent?: boolean | undefined } + ): Effect.Effect<[A, A2], E | E2, R | R2> +} = dual( + (args) => isEffect(args[1]), + ( + self: Effect.Effect, + that: Effect.Effect, + options?: { readonly concurrent?: boolean | undefined } + ): Effect.Effect<[A, A2], E | E2, R | R2> => zipWith(self, that, (a, a2) => [a, a2], options) +) + +/** @internal */ +export const zipWith: { + ( + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): Effect.Effect +} = dual( + (args) => isEffect(args[1]), + ( + self: Effect.Effect, + that: Effect.Effect, + f: (a: A, b: A2) => B, + options?: { readonly concurrent?: boolean | undefined } + ): Effect.Effect => + options?.concurrent + // Use `all` exclusively for concurrent cases, as it introduces additional overhead due to the management of concurrency + ? map(all([self, that], { concurrency: 2 }), ([a, a2]) => internalCall(() => f(a, a2))) + : flatMap(self, (a) => map(that, (a2) => internalCall(() => f(a, a2)))) +) + +// ---------------------------------------------------------------------------- +// filtering & conditionals +// ---------------------------------------------------------------------------- + +/* @internal */ +export const filterOrFail: { + ( + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + refinement: Predicate.Refinement, B> + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 + ): Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, B> + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate> + ): Effect.Effect +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + orFailWith?: (a: any) => E2 +): Effect.Effect => + filterOrElse( + self, + predicate as any, + orFailWith ? (a: any) => fail(orFailWith(a)) : () => fail(new NoSuchElementError() as E2) + )) + +/** @internal */ +export const when: { + ( + condition: Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect, E | E2, R | R2> + ( + self: Effect.Effect, + condition: Effect.Effect + ): Effect.Effect, E | E2, R | R2> +} = dual( + 2, + ( + self: Effect.Effect, + condition: Effect.Effect + ): Effect.Effect, E | E2, R | R2> => flatMap(condition, (pass) => pass ? asSome(self) : succeedNone) +) + +// ---------------------------------------------------------------------------- +// repetition +// ---------------------------------------------------------------------------- + +/** @internal */ +export const replicate: { + ( + n: number + ): (self: Effect.Effect) => Array> + ( + self: Effect.Effect, + n: number + ): Array> +} = dual( + 2, + ( + self: Effect.Effect, + n: number + ): Array> => Array.from({ length: n }, () => self) +) + +/** @internal */ +export const replicateEffect: { + ( + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } + ): (self: Effect.Effect) => Effect.Effect, E, R> + ( + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + n: number, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } + ): Effect.Effect, E, R> + ( + self: Effect.Effect, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Effect.Effect +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + n: number, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Effect.Effect => all(replicate(self, n), options) +) + +/** @internal */ +export const forever: { + < + Arg extends Effect.Effect | { + readonly disableYield?: boolean | undefined + } | undefined = { + readonly disableYield?: boolean | undefined + } + >( + effectOrOptions: Arg, + options?: { + readonly disableYield?: boolean | undefined + } | undefined + ): [Arg] extends [Effect.Effect] ? Effect.Effect + : (self: Effect.Effect) => Effect.Effect +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + options?: { + readonly disableYield?: boolean | undefined + } +): Effect.Effect => + whileLoop({ + while: constTrue, + body: constant(options?.disableYield ? self : flatMap(self, (_) => yieldNow)), + step: constVoid + }) as any) + +// ---------------------------------------------------------------------------- +// error handling +// ---------------------------------------------------------------------------- + +/** @internal */ +export const catchCause: { + ( + f: (cause: NoInfer>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (cause: NoInfer>) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (cause: NoInfer>) => Effect.Effect + ): Effect.Effect => { + const onFailure = Object.create(OnFailureProto) + onFailure[args] = self + onFailure[contE] = f.length !== 1 ? (cause: Cause.Cause) => f(cause) : f + return onFailure + } +) +const OnFailureProto = makePrimitiveProto({ + op: "OnFailure", + [evaluate](this: any, fiber: FiberImpl): Primitive { + fiber._stack.push(this as any) + return this[args] + } +}) + +/** @internal */ +export const catchCauseIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect => + catchCause(self, (cause): Effect.Effect => { + if (!predicate(cause)) { + return failCause(cause) as any + } + return internalCall(() => f(cause)) + }) +) + +/** @internal */ +export const catchCauseFilter: { + >( + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect | E2, R | R2> + >( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect | E2, R | R2> +} = dual( + 3, + >( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect | E2, R | R2> => + catchCause(self, (cause): Effect.Effect | E2, R2> => { + const eb = filter(cause) + return Result.isFailure(eb) ? failCause(eb.failure) : internalCall(() => f(eb.success, cause)) + }) +) + +/** @internal */ +export const catch_: { + ( + f: (e: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: NoInfer) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (a: NoInfer) => Effect.Effect + ): Effect.Effect => catchCauseFilter(self, findError as any, (e: any) => f(e)) as any +) + +/** @internal */ +export const catchNoSuchElement = ( + self: Effect.Effect +): Effect.Effect, Exclude, R> => + matchEffect(self, { + onFailure: (error) => + isNoSuchElementError(error) + ? succeedNone + : fail(error as Exclude), + onSuccess: succeedSome + }) + +/** @internal */ +export const catchDefect: { + ( + f: (defect: unknown) => Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect + ): Effect.Effect => catchCauseFilter(self, findDefect as any, f as any) as any +) + +/** @internal */ +export const tapCause: { + ( + f: (cause: NoInfer>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (cause: NoInfer>) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (cause: NoInfer>) => Effect.Effect + ): Effect.Effect => + catchCause(self, (cause) => andThen(internalCall(() => f(cause)), failCause(cause))) +) + +/** @internal */ +export const tapCauseIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect => + catchCauseIf( + self, + predicate, + (cause) => andThen(internalCall(() => f(cause)), failCause(cause)) + ) +) + +/** @internal */ +export const tapCauseFilter: { + >( + filter: Filter.Filter, EB, X>, + f: (a: EB, cause: Cause.Cause) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + >( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (a: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + >( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (a: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect => + catchCause(self, (cause) => { + const result = filter(cause) + if (Result.isFailure(result)) { + return failCause(cause) + } + return andThen(internalCall(() => f(result.success, cause)), failCause(cause)) + }) +) + +/** @internal */ +export const tapError: { + ( + f: (e: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: NoInfer) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: NoInfer) => Effect.Effect + ): Effect.Effect => tapCauseFilter(self, findError as any, (e: any) => f(e)) as any +) + +/** @internal */ +export const tapErrorTag: { + | Arr.NonEmptyReadonlyArray>, E, A1, E1, R1>( + k: K, + f: ( + e: ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1 + >( + self: Effect.Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1 + >( + self: Effect.Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect.Effect + ): Effect.Effect => { + const predicate = Array.isArray(k) + ? ((e: E): e is ExtractTag ? K[number] : K> => + hasProperty(e, "_tag") && k.includes(e._tag)) + : isTagged(k as string) + return tapError( + self, + (error) => + predicate(error) + ? f(error as ExtractTag ? K[number] : K>) + : void_ + ) + } +) + +/** @internal */ +export const tapDefect: { + ( + f: (defect: unknown) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (defect: unknown) => Effect.Effect + ): Effect.Effect => tapCauseFilter(self, findDefect as any, (_: any) => f(_)) as any +) + +/** @internal */ +export const catchIf: { + ( + refinement: Predicate.Refinement, EB>, + f: (e: EB) => Effect.Effect, + orElse?: ((e: Exclude) => Effect.Effect) | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect< + A | A2 | Exclude, + E2 | E3 | (A3 extends unassigned ? Exclude : never), + R | R2 | R3 + > + ( + predicate: Predicate.Predicate>, + f: (e: NoInfer) => Effect.Effect, + orElse?: ((e: NoInfer) => Effect.Effect) | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + f: (e: EB) => Effect.Effect, + orElse?: ((e: Exclude) => Effect.Effect) | undefined + ): Effect.Effect< + A | A2 | Exclude, + E2 | E3 | (A3 extends unassigned ? Exclude : never), + R | R2 | R3 + > + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + f: (e: E) => Effect.Effect, + orElse?: ((e: E) => Effect.Effect) | undefined + ): Effect.Effect, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + predicate: Predicate.Predicate, + f: (e: E) => Effect.Effect, + orElse?: ((e: E) => Effect.Effect) | undefined + ): Effect.Effect => + catchCause(self, (cause): Effect.Effect => { + const error = findError(cause) + if (Result.isFailure(error)) return failCause(error.failure) + if (!predicate(error.success)) { + return orElse ? internalCall(() => orElse(error.success as any)) : failCause(cause as any as Cause.Cause) + } + return internalCall(() => f(error.success as any)) + }) +) + +/** @internal */ +export const catchFilter: { + ( + filter: Filter.Filter, EB, X>, + f: (e: EB) => Effect.Effect, + orElse?: ((e: X) => Effect.Effect) | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> + ( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (e: EB) => Effect.Effect, + orElse?: ((e: X) => Effect.Effect) | undefined + ): Effect.Effect, E2 | E3 | (A3 extends unassigned ? X : never), R | R2 | R3> +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (e: EB) => Effect.Effect, + orElse?: ((e: X) => Effect.Effect) | undefined + ): Effect.Effect => + catchCause(self, (cause): Effect.Effect => { + const error = findError(cause) + if (Result.isFailure(error)) return failCause(error.failure) + const result = filter(error.success) + if (Result.isFailure(result)) { + return orElse ? internalCall(() => orElse(result.failure as any)) : failCause(cause as any as Cause.Cause) + } + return internalCall(() => f(result.success)) + }) +) + +/** @internal */ +export const catchTag: { + < + const K extends Tags | Arr.NonEmptyReadonlyArray>, + E, + A1, + E1, + R1, + A2 = unassigned, + E2 = never, + R2 = never + >( + k: K, + f: ( + e: ExtractTag, K extends Arr.NonEmptyReadonlyArray ? K[number] : K> + ) => Effect.Effect, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Effect.Effect) + | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect.Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect.Effect, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Effect.Effect) + | undefined + ): Effect.Effect< + A | A1 | Exclude, + | E1 + | E2 + | (A2 extends unassigned ? ExcludeTag ? K[number] : K> : never), + R | R1 | R2 + > +} = dual( + (args) => isEffect(args[0]), + < + A, + E, + R, + const K extends Tags | Arr.NonEmptyReadonlyArray>, + R1, + E1, + A1, + A2 = never, + E2 = ExcludeTag ? K[number] : K>, + R2 = never + >( + self: Effect.Effect, + k: K, + f: (e: ExtractTag ? K[number] : K>) => Effect.Effect, + orElse?: + | ((e: ExcludeTag ? K[number] : K>) => Effect.Effect) + | undefined + ): Effect.Effect => { + const pred = Array.isArray(k) + ? ((e: E): e is any => hasProperty(e, "_tag") && k.includes(e._tag)) + : isTagged(k as string) + return catchIf(self, pred, f, orElse as any) as any + } +) + +/** @internal */ +export const catchTags: { + < + E, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Effect.Effect + } : + {}), + A2 = unassigned, + E2 = never, + R2 = never + >( + cases: Cases, + orElse?: ((e: Exclude) => Effect.Effect) | undefined + ): (self: Effect.Effect) => Effect.Effect< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? R : never + }[keyof Cases] + > + < + R, + E, + A, + Cases extends (E extends { _tag: string } ? { + [K in E["_tag"]]+?: (error: Extract) => Effect.Effect + } : + {}), + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect.Effect, + cases: Cases, + orElse?: ((e: Exclude) => Effect.Effect) | undefined + ): Effect.Effect< + | A + | Exclude + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? A : never + }[keyof Cases], + | E2 + | (A2 extends unassigned ? Exclude : never) + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? E : never + }[keyof Cases], + | R + | R2 + | { + [K in keyof Cases]: Cases[K] extends ((...args: Array) => Effect.Effect) ? R : never + }[keyof Cases] + > +} = dual((args) => isEffect(args[0]), (self: Effect.Effect, cases: Record, orElse: any) => { + let keys: Array + return catchFilter( + self, + (e) => { + keys ??= Object.keys(cases) + return hasProperty(e, "_tag") && isString(e["_tag"]) && keys.includes(e["_tag"]) + ? Result.succeed(e) + : Result.fail(e) + }, + (e: any) => internalCall(() => cases[e["_tag"] as string](e)), + orElse + ) as any +}) + +/** @internal */ +export const catchReason: { + < + K extends Tags, + E, + RK extends ReasonTags, K>>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + errorTag: K, + reasonTag: RK, + f: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Effect.Effect, + orElse?: + | (( + reasons: ExcludeReason, K>, RK>, + error: OmitReason, K>, RK> + ) => Effect.Effect) + | undefined + ): ( + self: Effect.Effect + ) => Effect.Effect< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > + < + A, + E, + R, + K extends Tags, + RK extends ReasonTags>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + self: Effect.Effect, + errorTag: K, + reasonTag: RK, + f: ( + reason: ExtractReason, RK>, + error: NarrowReason, RK> + ) => Effect.Effect, + orElse?: + | (( + reasons: ExcludeReason, RK>, + error: OmitReason, RK> + ) => Effect.Effect) + | undefined + ): Effect.Effect< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > +} = dual( + (args) => isEffect(args[0]), + < + A, + E, + R, + K extends Tags, + RK extends ReasonTags>, + A2, + E2, + R2, + A3 = unassigned, + E3 = never, + R3 = never + >( + self: Effect.Effect, + errorTag: K, + reasonTag: RK, + f: (reason: ExtractReason, RK>, error: ExtractTag) => Effect.Effect, + orElse?: + | (( + reasons: ExcludeReason, RK>, + error: OmitReason, RK> + ) => Effect.Effect) + | undefined + ): Effect.Effect< + A | A2 | Exclude, + ExcludeTag | E2 | E3 | (A3 extends unassigned ? ExtractTag : never), + R | R2 | R3 + > => + catchIf( + self, + ((e: any) => isTagged(e, errorTag) && hasProperty(e, "reason")) as any, + (e: any): Effect.Effect => { + const reason = e.reason as any + if (isTagged(reason, reasonTag)) return f(reason as any, e) + return orElse ? internalCall(() => orElse(reason, e)) : fail(e) + } + ) as any +) + +/** @internal */ +export const catchReasons: { + < + K extends Tags, + E, + Cases extends { + [RK in ReasonTags, K>>]+?: ( + reason: ExtractReason, K>, RK>, + error: NarrowReason, K>, RK> + ) => Effect.Effect + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Effect.Effect) + | undefined + ): (self: Effect.Effect) => Effect.Effect< + | A + | Exclude + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? A : never }[ + keyof Cases + ], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? E : never }[ + keyof Cases + ], + | R + | R2 + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? R : never }[ + keyof Cases + ] + > + < + A, + E, + R, + K extends Tags, + Cases extends { + [RK in ReasonTags>]+?: ( + reason: ExtractReason, RK>, + error: NarrowReason, RK> + ) => Effect.Effect + }, + A2 = unassigned, + E2 = never, + R2 = never + >( + self: Effect.Effect, + errorTag: K, + cases: Cases, + orElse?: + | (( + reason: ExcludeReason, K>, Extract>, + error: OmitReason, K>, Extract> + ) => Effect.Effect) + | undefined + ): Effect.Effect< + | A + | Exclude + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? A : never }[ + keyof Cases + ], + | ExcludeTag + | E2 + | (A2 extends unassigned ? ExtractTag : never) + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? E : never }[ + keyof Cases + ], + | R + | R2 + | { [RK in keyof Cases]: Cases[RK] extends (...args: Array) => Effect.Effect ? R : never }[ + keyof Cases + ] + > +} = dual((args) => isEffect(args[0]), (self, errorTag, cases, orElse) => { + let keys: Array + return catchIf( + self, + ((e: any) => + isTagged(e, errorTag) && + hasProperty(e, "reason") && + hasProperty(e.reason, "_tag") && + isString(e.reason._tag)) as any, + (e: any) => { + const reason = e.reason + keys ??= Object.keys(cases) + if (keys.includes(reason._tag)) { + return internalCall(() => (cases as any)[reason._tag](reason, e)) + } + return orElse ? internalCall(() => orElse(reason, e)) : fail(e) + } + ) +}) + +/** @internal */ +export const unwrapReason: { + < + K extends Effect.TagsWithReason, + E + >( + errorTag: K + ): (self: Effect.Effect) => Effect.Effect | ReasonOf>, R> + < + A, + E, + R, + K extends Effect.TagsWithReason + >( + self: Effect.Effect, + errorTag: K + ): Effect.Effect | ReasonOf>, R> +} = dual( + 2, + < + A, + E, + R, + K extends Effect.TagsWithReason + >( + self: Effect.Effect, + errorTag: K + ): Effect.Effect | ReasonOf>, R> => + catchFilter( + self, + (e: any) => { + if (isTagged(e, errorTag) && hasProperty(e, "reason")) { + return Result.succeed(e.reason) + } + return Result.fail(e) + }, + fail as any + ) as any +) + +/** @internal */ +export const mapError: { + ( + f: (e: E) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (e: E) => E2 + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (e: E) => E2 + ): Effect.Effect => catch_(self, (error) => failSync(() => f(error))) +) + +/* @internal */ +export const mapBoth: { + ( + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } + ): Effect.Effect +} = dual(2, ( + self: Effect.Effect, + options: { readonly onFailure: (e: E) => E2; readonly onSuccess: (a: A) => A2 } +): Effect.Effect => + matchEffect(self, { + onFailure: (e) => failSync(() => options.onFailure(e)), + onSuccess: (a) => sync(() => options.onSuccess(a)) + })) + +/** @internal */ +export const orDie = ( + self: Effect.Effect +): Effect.Effect => catch_(self, die) + +/** @internal */ +export const orElseSucceed: { + ( + f: LazyArg + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: LazyArg + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: LazyArg + ): Effect.Effect => catch_(self, (_) => sync(f)) +) + +/** @internal */ +export const firstSuccessOf = >( + effects: Iterable +): Effect.Effect, Effect.Error, Effect.Services> => + suspend(() => { + const iterator = effects[Symbol.iterator]() + let state = iterator.next() + if (state.done) { + return die(new Error("Received an empty collection of effects")) + } + function loop(current: IteratorYieldResult): Eff { + const next = iterator.next() + if (next.done) return current.value + return catch_(current.value, (_) => loop(next)) as any + } + return loop(state) + }) + +/** @internal */ +export const eventually = (self: Effect.Effect): Effect.Effect => + catch_(self, (_) => flatMap(yieldNow, () => eventually(self))) + +/** @internal */ +export const ignore: < + Arg extends Effect.Effect | { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined = { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } +>( + effectOrOptions: Arg, + options?: { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined +) => [Arg] extends [Effect.Effect] ? Effect.Effect + : (self: Effect.Effect) => Effect.Effect = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + options?: { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined + ): Effect.Effect => { + if (!options?.log) { + return matchEffect(self, { onFailure: (_) => void_, onSuccess: (_) => void_ }) + } + const logEffect = logWithLevel(options.log === true ? undefined : options.log) + return matchCauseEffect(self, { + onFailure(cause) { + const failure = findFail(cause) + return Result.isFailure(failure) + ? failCause(failure.failure) + : options.message === undefined + ? logEffect(cause) + : logEffect(options.message, cause) + }, + onSuccess: (_) => void_ + }) + } + ) + +/** @internal */ +export const ignoreCause: < + Arg extends Effect.Effect | { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined = { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } +>( + effectOrOptions: Arg, + options?: { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined +) => [Arg] extends [Effect.Effect] ? Effect.Effect + : (self: Effect.Effect) => Effect.Effect = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + options?: { + readonly log?: boolean | LogLevel.Severity | undefined + readonly message?: string | undefined + } | undefined + ): Effect.Effect => { + if (!options?.log) { + return matchCauseEffect(self, { onFailure: (_) => void_, onSuccess: (_) => void_ }) + } + const logEffect = logWithLevel(options.log === true ? undefined : options.log) + return matchCauseEffect(self, { + onFailure: (cause) => options.message === undefined ? logEffect(cause) : logEffect(options.message, cause), + onSuccess: (_) => void_ + }) + } + ) + +/** @internal */ +export const option = ( + self: Effect.Effect +): Effect.Effect, never, R> => match(self, { onFailure: Option.none, onSuccess: Option.some }) + +/** @internal */ +export const result = ( + self: Effect.Effect +): Effect.Effect, never, R> => + matchEager(self, { onFailure: Result.fail, onSuccess: Result.succeed }) + +// ---------------------------------------------------------------------------- +// pattern matching +// ---------------------------------------------------------------------------- + +/** @internal */ +export const matchCauseEffect: { + (options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + }): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect => { + const primitive = Object.create(OnSuccessAndFailureProto) + primitive[args] = self + primitive[contA] = options.onSuccess.length !== 1 ? (a: A) => options.onSuccess(a) : options.onSuccess + primitive[contE] = options.onFailure.length !== 1 + ? (cause: Cause.Cause) => options.onFailure(cause) + : options.onFailure + return primitive + } +) +const OnSuccessAndFailureProto = makePrimitiveProto({ + op: "OnSuccessAndFailure", + [evaluate](this: any, fiber: FiberImpl): Primitive { + fiber._stack.push(this) + return this[args] + } +}) + +/** @internal */ +export const matchCause: { + (options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + }): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (a: A) => A3 + } + ): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => sync(() => options.onFailure(cause)), + onSuccess: (value) => sync(() => options.onSuccess(value)) + }) +) + +/** @internal */ +export const matchEffect: { + (options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + }): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (e: E) => Effect.Effect + readonly onSuccess: (a: A) => Effect.Effect + } + ): Effect.Effect => + matchCauseEffect(self, { + onFailure: (cause) => { + const fail = cause.reasons.find(isFailReason) + return fail + ? internalCall(() => options.onFailure(fail.error)) + : failCause(cause as Cause.Cause) + }, + onSuccess: options.onSuccess + }) +) + +/** @internal */ +export const match: { + (options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect => + matchEffect(self, { + onFailure: (error) => sync(() => options.onFailure(error)), + onSuccess: (value) => sync(() => options.onSuccess(value)) + }) +) + +/** @internal */ +export const matchEager: { + (options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (error: E) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect => { + if (effectIsExit(self)) { + if (self._tag === "Success") return exitSucceed(options.onSuccess(self.value)) + const error = findError(self.cause) + if (Result.isFailure(error)) return self as Exit.Exit + return exitSucceed(options.onFailure(error.success)) + } + return match(self, options) + } +) + +/** @internal */ +export const matchCauseEager: { + (options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (value: A) => A3 + }): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly onFailure: (cause: Cause.Cause) => A2 + readonly onSuccess: (value: A) => A3 + } + ): Effect.Effect => { + if (effectIsExit(self)) { + if (self._tag === "Success") return exitSucceed(options.onSuccess(self.value)) + return exitSucceed(options.onFailure(self.cause)) + } + return matchCause(self, options) + } +) + +/** @internal */ +export const exit = (self: Effect.Effect): Effect.Effect, never, R> => + effectIsExit(self) ? exitSucceed(self) : exitPrimitive(self) + +const exitPrimitive: (self: Effect.Effect) => Effect.Effect, never, R> = + makePrimitive({ + op: "Exit", + [evaluate](fiber): Primitive { + fiber._stack.push(this) + return this[args] as any + }, + [contA](value, _, exit) { + return succeed(exit ?? exitSucceed(value)) + }, + [contE](cause, _, exit) { + return succeed(exit ?? exitFailCause(cause)) + } + }) + +// ---------------------------------------------------------------------------- +// Condition checking +// ---------------------------------------------------------------------------- + +/** @internal */ +export const isFailure: (self: Effect.Effect) => Effect.Effect = matchEager({ + onFailure: () => true, + onSuccess: () => false +}) + +/** @internal */ +export const isSuccess: (self: Effect.Effect) => Effect.Effect = matchEager({ + onFailure: () => false, + onSuccess: () => true +}) + +// ---------------------------------------------------------------------------- +// delays & timeouts +// ---------------------------------------------------------------------------- + +/** @internal */ +export const delay: { + ( + duration: Duration.Input + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect => andThen(sleep(duration), self) +) + +/** @internal */ +export const timeoutOrElse: { + (options: { + readonly duration: Duration.Input + readonly orElse: LazyArg> + }): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + options: { + readonly duration: Duration.Input + readonly orElse: LazyArg> + } + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + options: { + readonly duration: Duration.Input + readonly orElse: LazyArg> + } + ): Effect.Effect => + raceFirst( + self, + flatMap(sleep(options.duration), options.orElse) + ) +) + +/** @internal */ +export const timeout: { + ( + duration: Duration.Input + ): ( + self: Effect.Effect + ) => Effect.Effect + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect => + timeoutOrElse(self, { + duration, + orElse: () => fail(new TimeoutError()) + }) +) + +/** @internal */ +export const timeoutOption: { + ( + duration: Duration.Input + ): ( + self: Effect.Effect + ) => Effect.Effect, E, R> + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect, E, R> +} = dual( + 2, + ( + self: Effect.Effect, + duration: Duration.Input + ): Effect.Effect, E, R> => + raceFirst( + asSome(self), + as(sleep(duration), Option.none()) + ) +) + +/** @internal */ +export const timed = ( + self: Effect.Effect +): Effect.Effect<[duration: Duration.Duration, result: A], E, R> => + clockWith((clock) => { + const start = clock.currentTimeNanosUnsafe() + return map(self, (a) => [Duration.nanos(clock.currentTimeNanosUnsafe() - start), a]) + }) + +// ---------------------------------------------------------------------------- +// resources & finalization +// ---------------------------------------------------------------------------- + +/** @internal */ +export const ScopeTypeId = "~effect/Scope" + +/** @internal */ +export const ScopeCloseableTypeId = "~effect/Scope/Closeable" + +/** @internal */ +export const scopeTag: Context.Service = Context.Service("effect/Scope") + +/** @internal */ +export const scopeClose = (self: Scope.Scope, exit_: Exit.Exit) => + suspend(() => scopeCloseUnsafe(self, exit_) ?? void_) + +/** @internal */ +export const scopeCloseUnsafe = (self: Scope.Scope, exit_: Exit.Exit) => { + if (self.state._tag === "Closed") return + const closed: Scope.State.Closed = { _tag: "Closed", exit: exit_ } + if (self.state._tag === "Empty") { + self.state = closed + return + } + const { finalizers } = self.state + self.state = closed + if (finalizers.size === 0) { + return + } else if (finalizers.size === 1) { + return finalizers.values().next().value!(exit_) + } + return scopeCloseFinalizers(self, finalizers, exit_) +} + +const scopeCloseFinalizers = fnUntraced(function*( + self: Scope.Scope, + finalizers: Scope.State.Open["finalizers"], + exit_: Exit.Exit +) { + let exits: Array> = [] + const fibers: Array> = [] + const arr = Array.from(finalizers.values()) + const parent = getCurrentFiber()! + for (let i = arr.length - 1; i >= 0; i--) { + const finalizer = arr[i] + if (self.strategy === "sequential") { + exits.push(yield* exit(finalizer(exit_))) + } else { + fibers.push(forkUnsafe(parent, finalizer(exit_), true, true, "inherit")) + } + } + if (fibers.length > 0) { + exits = yield* fiberAwaitAll(fibers) + } + return yield* exitAsVoidAll(exits) +}) + +/** @internal */ +export const scopeFork = (scope: Scope.Scope, finalizerStrategy?: "sequential" | "parallel") => + sync(() => scopeForkUnsafe(scope, finalizerStrategy)) + +/** @internal */ +export const scopeForkUnsafe = (scope: Scope.Scope, finalizerStrategy?: "sequential" | "parallel") => { + const newScope = scopeMakeUnsafe(finalizerStrategy) + if (scope.state._tag === "Closed") { + newScope.state = scope.state + return newScope + } + const key = {} + scopeAddFinalizerUnsafe(scope, key, (exit) => scopeClose(newScope, exit)) + scopeAddFinalizerUnsafe(newScope, key, (_) => sync(() => scopeRemoveFinalizerUnsafe(scope, key))) + return newScope +} + +/** @internal */ +export const scopeAddFinalizerExit = ( + scope: Scope.Scope, + finalizer: (exit: Exit.Exit) => Effect.Effect +): Effect.Effect => { + return suspend(() => { + if (scope.state._tag === "Closed") { + return finalizer(scope.state.exit) + } + scopeAddFinalizerUnsafe(scope, {}, finalizer) + return void_ + }) +} + +/** @internal */ +export const scopeAddFinalizer = ( + scope: Scope.Scope, + finalizer: Effect.Effect +): Effect.Effect => scopeAddFinalizerExit(scope, constant(finalizer)) + +/** @internal */ +export const scopeAddFinalizerUnsafe = ( + scope: Scope.Scope, + key: {}, + finalizer: (exit: Exit.Exit) => Effect.Effect +): void => { + if (scope.state._tag === "Empty") { + scope.state = { _tag: "Open", finalizers: new Map([[key, finalizer]]) } + } else if (scope.state._tag === "Open") { + scope.state.finalizers.set(key, finalizer) + } +} + +/** @internal */ +export const scopeRemoveFinalizerUnsafe = ( + scope: Scope.Scope, + key: {} +): void => { + if (scope.state._tag === "Open") { + scope.state.finalizers.delete(key) + } +} + +/** @internal */ +export const scopeMakeUnsafe = (finalizerStrategy: "sequential" | "parallel" = "sequential"): Scope.Closeable => ({ + [ScopeCloseableTypeId]: ScopeCloseableTypeId, + [ScopeTypeId]: ScopeTypeId, + strategy: finalizerStrategy, + state: constScopeEmpty +}) + +const constScopeEmpty = { _tag: "Empty" } as const + +/** @internal */ +export const scopeMake = (finalizerStrategy?: "sequential" | "parallel"): Effect.Effect => + sync(() => scopeMakeUnsafe(finalizerStrategy)) + +/** @internal */ +export const scope: Effect.Effect = scopeTag + +/** @internal */ +export const provideScope: { + (value: Scope.Scope): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, value: Scope.Scope): Effect.Effect> +} = provideService(scopeTag) + +/** @internal */ +export const scoped = (self: Effect.Effect): Effect.Effect> => + withFiber((fiber) => { + const prev = fiber.context + const scope = scopeMakeUnsafe() + fiber.setContext(Context.add(fiber.context, scopeTag, scope)) + return onExitPrimitive(self, (exit) => { + fiber.setContext(prev) + return scopeCloseUnsafe(scope, exit) + }) + }) as any + +/** @internal */ +export const scopeUse: { + ( + scope: Scope.Closeable + ): (self: Effect.Effect) => Effect.Effect> + (self: Effect.Effect, scope: Scope.Closeable): Effect.Effect> +} = dual( + 2, + (self: Effect.Effect, scope: Scope.Closeable): Effect.Effect> => + onExit(provideScope(self, scope), (exit) => suspend(() => scopeCloseUnsafe(scope, exit) ?? void_)) +) + +/** @internal */ +export const scopedWith = ( + f: (scope: Scope.Scope) => Effect.Effect +): Effect.Effect => + suspend(() => { + const scope = scopeMakeUnsafe() + return onExit(f(scope), (exit) => suspend(() => scopeCloseUnsafe(scope, exit) ?? void_)) + }) + +/** @internal */ +export const acquireRelease = ( + acquire: Effect.Effect, + release: (a: A, exit: Exit.Exit) => Effect.Effect, + options?: { readonly interruptible?: boolean } +): Effect.Effect => + contextWith((context: Context.Context) => + uninterruptibleMask((restore) => + flatMap( + scope, + (scope) => + tap( + options?.interruptible ? restore(acquire) : acquire, + (a) => scopeAddFinalizerExit(scope, (exit) => provideContext(release(a, exit), context)) + ) + ) + ) + ) + +/** @internal */ +export const addFinalizer = ( + finalizer: (exit: Exit.Exit) => Effect.Effect +): Effect.Effect => + flatMap( + scope, + (scope) => + contextWith((context: Context.Context) => + scopeAddFinalizerExit(scope, (exit) => provideContext(finalizer(exit), context)) + ) + ) + +/** @internal */ +export const onExitPrimitive: ( + self: Effect.Effect, + f: (exit: Exit.Exit) => Effect.Effect | undefined, + interruptible?: boolean +) => Effect.Effect = makePrimitive({ + op: "OnExit", + single: false, + [evaluate](fiber: FiberImpl) { + fiber._stack.push(this) + return this[args][0] + }, + [contAll](fiber) { + if (fiber.interruptible && this[args][2] !== true) { + fiber._stack.push(setInterruptibleTrue) + fiber.interruptible = false + } + }, + [contA](value, _, exit) { + exit ??= exitSucceed(value) + const eff = this[args][1](exit) + return eff ? flatMap(eff, (_) => exit) : exit + }, + [contE](cause, _, exit) { + exit ??= exitFailCause(cause) + const eff = this[args][1](exit) + return eff ? flatMap(eff, (_) => exit) : exit + } +}) + +/** @internal */ +export const onExit: { + ( + f: (exit: Exit.Exit) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (exit: Exit.Exit) => Effect.Effect + ): Effect.Effect +} = dual(2, onExitPrimitive) + +/** @internal */ +export const ensuring: { + ( + finalizer: Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + finalizer: Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + finalizer: Effect.Effect + ): Effect.Effect => onExit(self, (_) => finalizer) +) + +/** @internal */ +export const onExitIf: { + ( + predicate: Predicate.Predicate, NoInfer>>, + f: (exit: Exit.Exit, NoInfer>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate, NoInfer>>, + f: (exit: Exit.Exit, NoInfer>) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + predicate: Predicate.Predicate, NoInfer>>, + f: (exit: Exit.Exit, NoInfer>) => Effect.Effect + ): Effect.Effect => + onExit(self, (exit) => { + if (!predicate(exit)) { + return void_ + } + return f(exit) + }) +) + +/** @internal */ +export const onExitFilter: { + ( + filter: Filter.Filter, NoInfer>, B, X>, + f: (b: B, exit: Exit.Exit, NoInfer>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + filter: Filter.Filter, NoInfer>, B, X>, + f: (b: B, exit: Exit.Exit, NoInfer>) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + filter: Filter.Filter, NoInfer>, B, X>, + f: (b: B, exit: Exit.Exit, NoInfer>) => Effect.Effect + ): Effect.Effect => + onExit(self, (exit) => { + const b = filter(exit) + return Result.isFailure(b) ? void_ : f(b.success, exit) + }) +) + +/** @internal */ +export const onError: { + ( + f: (cause: Cause.Cause>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + f: (cause: Cause.Cause>) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + f: (cause: Cause.Cause>) => Effect.Effect + ): Effect.Effect => onExitFilter(self, exitFilterCause as any, f as any) as any +) + +/** @internal */ +export const onErrorIf: { + ( + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + f: (cause: Cause.Cause) => Effect.Effect + ): Effect.Effect => + onExitIf( + self, + (exit): exit is Exit.Failure => { + if (exit._tag !== "Failure") { + return false + } + return predicate(exit.cause) + }, + (exit) => f((exit as Exit.Failure).cause) + ) as any +) + +/** @internal */ +export const onErrorFilter: { + ( + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect +} = dual( + 3, + ( + self: Effect.Effect, + filter: Filter.Filter, EB, X>, + f: (failure: EB, cause: Cause.Cause) => Effect.Effect + ): Effect.Effect => + onExit(self, (exit) => { + if (exit._tag !== "Failure") { + return void_ + } + const result = filter(exit.cause) + return Result.isFailure(result) ? void_ : f(result.success, exit.cause) + }) +) + +/** @internal */ +export const onInterrupt: { + ( + finalizer: (interruptors: ReadonlySet) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + finalizer: (interruptors: ReadonlySet) => Effect.Effect + ): Effect.Effect +} = dual( + 2, + ( + self: Effect.Effect, + finalizer: (interruptors: ReadonlySet) => Effect.Effect + ): Effect.Effect => onErrorFilter(causeFilterInterruptors as any, finalizer)(self) as any +) + +/** @internal */ +export const acquireUseRelease = ( + acquire: Effect.Effect, + use: (a: Resource) => Effect.Effect, + release: (a: Resource, exit: Exit.Exit) => Effect.Effect +): Effect.Effect => + uninterruptibleMask((restore) => + flatMap(acquire, (a) => + onExitPrimitive( + restore(use(a)), + (exit) => release(a, exit), + true + )) + ) + +/** @internal */ +export const acquireDisposable = ( + acquire: Effect.Effect +): Effect.Effect => + acquireRelease(acquire, (resource) => + hasProperty(resource, Symbol.asyncDispose) + ? promise(() => resource[Symbol.asyncDispose]()) + : sync(() => resource[Symbol.dispose]())) + +// ---------------------------------------------------------------------------- +// Caching +// ---------------------------------------------------------------------------- + +/** @internal */ +export const cachedInvalidateWithTTL: { + (timeToLive: Duration.Input): ( + self: Effect.Effect + ) => Effect.Effect<[Effect.Effect, Effect.Effect]> + ( + self: Effect.Effect, + timeToLive: Duration.Input + ): Effect.Effect<[Effect.Effect, Effect.Effect]> +} = dual(2, ( + self: Effect.Effect, + ttl: Duration.Input +): Effect.Effect<[Effect.Effect, Effect.Effect]> => + sync(() => { + const ttlMillis = Duration.toMillis(Duration.fromInputUnsafe(ttl)) + const isFinite = Number.isFinite(ttlMillis) + const latch = makeLatchUnsafe(false) + let expiresAt = 0 + let running = false + let exit: Exit.Exit | undefined + const wait = flatMap(latch.await, () => exit!) + return [ + withFiber((fiber) => { + const clock = fiber.getRef(ClockRef) + const now = isFinite ? clock.currentTimeMillisUnsafe() : 0 + if (running || now < expiresAt) return exit ?? wait + running = true + latch.closeUnsafe() + exit = undefined + return onExit(self, (exit_) => + sync(() => { + running = false + expiresAt = clock.currentTimeMillisUnsafe() + ttlMillis + exit = exit_ + latch.openUnsafe() + })) + }), + sync(() => { + expiresAt = 0 + latch.closeUnsafe() + exit = undefined + }) + ] + })) + +/** @internal */ +export const cachedWithTTL: { + ( + timeToLive: Duration.Input + ): (self: Effect.Effect) => Effect.Effect> + ( + self: Effect.Effect, + timeToLive: Duration.Input + ): Effect.Effect> +} = dual( + 2, + ( + self: Effect.Effect, + timeToLive: Duration.Input + ): Effect.Effect> => map(cachedInvalidateWithTTL(self, timeToLive), (tuple) => tuple[0]) +) + +/** @internal */ +export const cached = (self: Effect.Effect): Effect.Effect> => + cachedWithTTL(self, Duration.infinity) + +// ---------------------------------------------------------------------------- +// interruption +// ---------------------------------------------------------------------------- + +/** @internal */ +export const interrupt: Effect.Effect = withFiber((fiber) => failCause(causeInterrupt(fiber.id))) + +/** @internal */ +export const uninterruptible = ( + self: Effect.Effect +): Effect.Effect => + withFiber((fiber) => { + if (!fiber.interruptible) return self + fiber.interruptible = false + fiber._stack.push(setInterruptibleTrue) + return self + }) + +const setInterruptible: (interruptible: boolean) => Primitive = makePrimitive({ + op: "SetInterruptible", + [contAll](fiber) { + fiber.interruptible = this[args] + if (fiber._interruptedCause && fiber.interruptible) { + return () => failCause(fiber._interruptedCause!) + } + } +}) +const setInterruptibleTrue = setInterruptible(true) +const setInterruptibleFalse = setInterruptible(false) + +/** @internal */ +export const interruptible = ( + self: Effect.Effect +): Effect.Effect => + withFiber((fiber) => { + if (fiber.interruptible) return self + fiber.interruptible = true + fiber._stack.push(setInterruptibleFalse) + if (fiber._interruptedCause) return failCause(fiber._interruptedCause) + return self + }) + +/** @internal */ +export const uninterruptibleMask = ( + f: ( + restore: ( + effect: Effect.Effect + ) => Effect.Effect + ) => Effect.Effect +): Effect.Effect => + withFiber((fiber) => { + if (!fiber.interruptible) return f(identity) + fiber.interruptible = false + fiber._stack.push(setInterruptibleTrue) + return f(interruptible) + }) + +/** @internal */ +export const interruptibleMask = ( + f: ( + restore: ( + effect: Effect.Effect + ) => Effect.Effect + ) => Effect.Effect +): Effect.Effect => + withFiber((fiber) => { + if (fiber.interruptible) return f(identity) + fiber.interruptible = true + fiber._stack.push(setInterruptibleFalse) + return f(uninterruptible) + }) + +/** @internal */ +export const abortSignal: Effect.Effect = map( + acquireRelease( + sync(() => new AbortController()), + (controller) => sync(() => controller.abort()) + ), + (_) => _.signal +) + +// ======================================================================== +// collecting & elements +// ======================================================================== + +/** @internal */ +export const all = < + const Arg extends + | Iterable> + | Record>, + O extends { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + readonly mode?: "default" | "result" | undefined + } +>( + arg: Arg, + options?: O +): Effect.All.Return => { + if (isIterable(arg)) { + return options?.mode === "result" + ? (forEach as any)(arg, result, options) + : (forEach as any)(arg, identity, options) + } else if (options?.discard) { + return options.mode === "result" + ? (forEach as any)(Object.values(arg), result, options) + : (forEach as any)(Object.values(arg), identity, options) + } + return suspend(() => { + const out: Record = {} + return as( + forEach( + Object.entries(arg), + ([key, effect]) => + map(options?.mode === "result" ? result(effect) : effect, (value) => { + out[key] = value + }), + { + discard: true, + concurrency: options?.concurrency + } + ), + out + ) + }) as any +} + +/** @internal */ +export const partition: { + ( + f: (a: A, i: number) => Effect.Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): (elements: Iterable) => Effect.Effect<[excluded: Array, satisfying: Array], never, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect<[excluded: Array, satisfying: Array], never, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect<[excluded: Array, satisfying: Array], never, R> => + map( + forEach(elements, (a, i) => result(f(a, i)), options), + (results) => Arr.partition(results, identity) + ) +) + +/** @internal */ +export const validate: { + ( + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } | undefined + ): (elements: Iterable) => Effect.Effect, Arr.NonEmptyArray, R> + ( + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): (elements: Iterable) => Effect.Effect, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: false | undefined + } | undefined + ): Effect.Effect, Arr.NonEmptyArray, R> + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options: { + readonly concurrency?: Concurrency | undefined + readonly discard: true + } + ): Effect.Effect, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + } | undefined + ): Effect.Effect | void, Arr.NonEmptyArray, R> => + flatMap( + partition(elements, f, { concurrency: options?.concurrency }), + ([excluded, satisfying]) => { + if (Arr.isArrayNonEmpty(excluded)) { + return fail(excluded) + } + return options?.discard ? void_ : succeed(satisfying) + } + ) +) + +/** @internal */ +export const findFirst: { + ( + predicate: (a: NoInfer, i: number) => Effect.Effect + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + predicate: (a: NoInfer, i: number) => Effect.Effect + ): Effect.Effect, E, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + predicate: (a: A, i: number) => Effect.Effect + ): Effect.Effect, E, R> => + suspend(() => { + const iterator = elements[Symbol.iterator]() + const next = iterator.next() + if (!next.done) { + return findFirstLoop(iterator, 0, predicate, next.value) + } + return succeed(Option.none()) + }) +) + +const findFirstLoop = ( + iterator: Iterator, + index: number, + predicate: (a: A, i: number) => Effect.Effect, + value: A +): Effect.Effect, E, R> => + flatMap(predicate(value, index), (keep) => { + if (keep) { + return succeed(Option.some(value)) + } + const next = iterator.next() + if (!next.done) { + return findFirstLoop(iterator, index + 1, predicate, next.value) + } + return succeed(Option.none()) + }) + +/** @internal */ +export const findFirstFilter: { + ( + filter: (input: NoInfer, i: number) => Effect.Effect, E, R> + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + filter: (input: NoInfer, i: number) => Effect.Effect, E, R> + ): Effect.Effect, E, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + filter: (input: A, i: number) => Effect.Effect, E, R> + ): Effect.Effect, E, R> => + suspend(() => { + const iterator = elements[Symbol.iterator]() + const next = iterator.next() + if (!next.done) { + return findFirstFilterLoop(iterator, 0, filter, next.value) + } + return succeed(Option.none()) + }) +) + +const findFirstFilterLoop = ( + iterator: Iterator, + index: number, + filter: (input: A, i: number) => Effect.Effect, E, R>, + value: A +): Effect.Effect, E, R> => + flatMap(filter(value, index), (result) => { + if (Result.isSuccess(result)) { + return succeed(Option.some(result.success)) + } + const next = iterator.next() + if (!next.done) { + return findFirstFilterLoop(iterator, index + 1, filter, next.value) + } + return succeed(Option.none()) + }) + +/** @internal */ +export const whileLoop: (options: { + readonly while: LazyArg + readonly body: LazyArg> + readonly step: (a: A) => void +}) => Effect.Effect = makePrimitive({ + op: "While", + [contA](value, fiber) { + this[args].step(value) + if (this[args].while()) { + fiber._stack.push(this) + return this[args].body() + } + return exitVoid + }, + [evaluate](fiber) { + if (this[args].while()) { + fiber._stack.push(this) + return this[args].body() + } + return exitVoid + } +}) + +/** @internal */ +export const forEach: { + , const Discard extends boolean = false>( + f: (a: Arr.ReadonlyArray.Infer, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: Discard | undefined + } | undefined + ): ( + self: S + ) => Effect.Effect : void, E, R> + , const Discard extends boolean = false>( + self: S, + f: (a: Arr.ReadonlyArray.Infer, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: Discard | undefined + } | undefined + ): Effect.Effect : void, E, R> +} = dual((args) => typeof args[1] === "function", ( + iterable: Iterable, + f: (a: A, index: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly discard?: boolean | undefined + } +): Effect.Effect => + withFiber((parent) => { + const concurrencyOption = options?.concurrency === "inherit" + ? parent.getRef(CurrentConcurrency) + : (options?.concurrency ?? 1) + const concurrency = concurrencyOption === "unbounded" + ? Number.POSITIVE_INFINITY + : Math.max(1, concurrencyOption) + + if (concurrency === 1) { + return forEachSequential(iterable, f, options) + } + + const items = Arr.fromIterable(iterable) + let length = items.length + if (length === 0) { + return options?.discard ? void_ : succeed([]) + } + + const out: Array | undefined = options?.discard + ? undefined + : new Array(length) + const eff = forEachConcurrent({ f, out }, items, { concurrency }) + return eff ? as(eff, out as any) : succeed(out as any) + })) + +const forEachSequential = ( + iterable: Iterable, + f: (a: A, index: number) => Effect.Effect, + options?: { + readonly discard?: boolean | undefined + } +) => + suspend(() => { + const out: Array | undefined = options?.discard ? undefined : [] + const iterator = iterable[Symbol.iterator]() + let state = iterator.next() + let index = 0 + return as( + whileLoop({ + while: () => !state.done, + body: () => f(state.value!, index++), + step: (b) => { + if (out) out.push(b) + state = iterator.next() + } + }), + out + ) + }) + +type IterateEagerOptions = { + readonly concurrency?: number | undefined + readonly start?: number | undefined + readonly end?: number | undefined + readonly orderedStep?: boolean | undefined +} + +const iterateEagerImpl = (options: { + readonly onItem: (state: S, item: A, index: number) => Effect.Effect + readonly step: (state: NoInfer, item: A, exit: Exit.Exit, index: number) => Exit.Exit | void +}): ( + initialState: S, + items: ReadonlyArray, + options?: IterateEagerOptions +) => Effect.Effect | undefined => { + const onItem = options.onItem + const step = options.step + + return ( + state: S, + items: ReadonlyArray, + opts: IterateEagerOptions | undefined + ): Effect.Effect | undefined => { + let index = opts?.start ?? 0 + const end = opts?.end ?? items.length + const concurrency = opts?.concurrency ?? 1 + const orderedStep = opts?.orderedStep === true && concurrency > 1 + let done = false + let parentFiber: Fiber.Fiber | undefined + let fibers: Set> | undefined + let resume: ((effect: Effect.Effect) => void) | undefined + let interrupted = false + let terminal: Exit.Exit | void + let effect: Effect.Effect | undefined + let nextIndex = index + const exits: Array | undefined> | undefined = orderedStep ? new Array(end) : undefined + + const runStep = (item: A, exit: Exit.Exit, currentIndex: number): Exit.Exit | void => { + if (!orderedStep) return step(state, item, exit, currentIndex) + if (terminal) return terminal + exits![currentIndex] = exit + while (nextIndex < end) { + const nextExit = exits![nextIndex] + if (nextExit === undefined) return + exits![nextIndex] = undefined + const index = nextIndex++ + const result = step(state, items[index], nextExit, index) + if (result) return result + } + } + + const go = (): Effect.Effect | undefined => { + let paused = false + for (; !terminal && index < end; index++) { + const item = items[index] + const eff = effect ?? onItem(state, item, index) + + // fast case (already an exit) + if (effectIsExit(eff)) { + terminal = runStep(item, eff, index) + if (terminal) break + + // Use flatMap for concurrency of 1 + } else if (concurrency === 1) { + return flatMap(exit(eff), (exit) => { + terminal = runStep(item, exit, index) + index++ + return terminal ?? go() ?? void_ + }) + + // We have an effect, so enter "async" mode + } else if (!parentFiber) { + return callback((cb) => { + parentFiber = getCurrentFiber()! + effect = eff + resume = cb + const result = go() + if (result) return cb(result) + return suspend(() => { + terminal = exitVoid + interrupted = true + return fibers ? fiberInterruptAll(fibers) : void_ + }) + }) + + // Fork the effect with concurrency > 1 + } else { + // Clear the temporary effect from capturing the parentFiber + effect = undefined + + const fiber = forkUnsafe(parentFiber, eff, true, true, "inherit") + if (fiber._exit) { + terminal = runStep(item, fiber._exit, index) + if (terminal) break + continue + } + + // Add the fiber to the Set + if (fibers) fibers.add(fiber) + else fibers = new Set([fiber]) + + const currentIndex = index + fiber.addObserver((exit) => { + fibers!.delete(fiber) + if (terminal) { + if (!interrupted && exit._tag === "Failure") { + for (const reason of exit.cause.reasons) { + if (reason._tag === "Interrupt") continue + else if (terminal._tag === "Failure") { + ;(terminal.cause.reasons as Array).push(reason) + } else { + terminal = exitFailCause(causeFromReasons([reason])) + } + } + } + } else { + const result = runStep(item, exit, currentIndex) + if (result) { + terminal = result._tag === "Failure" + ? exitFailCause(causeFromReasons(result.cause.reasons.slice())) + : result + go() + } + } + + if (paused) { + const eff = go() + if (eff) resume!(eff) + } else if (done && fibers!.size === 0) { + resume!(terminal ?? void_) + } + }) + + // Check if we have reached the concurrency limit + if (fibers.size < concurrency) continue + paused = true + index++ + return + } + } + + done = true + + if (terminal) { + if (fibers && fibers.size > 0) { + const annotations = fiberStackAnnotations(parentFiber!) + fibers.forEach((f) => f.interruptUnsafe(parentFiber!.id, annotations)) + return + } + if (resume || terminal._tag === "Failure") { + return terminal + } + } else if (resume) { + if (!fibers) { + return exitVoid + } else if (fibers.size === 0) { + resume(void_) + } + } + } + + return go() + } +} + +/** @internal */ +export const iterateEager = (): (options: { + readonly onItem: (state: S, item: A, index: number) => Effect.Effect + readonly step: (state: NoInfer, item: A, exit: Exit.Exit, index: number) => Exit.Exit | void +}) => ( + initialState: S, + items: ReadonlyArray, + options?: IterateEagerOptions +) => Effect.Effect | undefined => iterateEagerImpl + +const forEachConcurrent = iterateEagerImpl({ + onItem( + state: { + readonly f: (a: any, i: number) => Effect.Effect + readonly out: Array | undefined + }, + item, + index + ) { + return state.f(item, index) + }, + step(state, _, exit, index) { + if (exit._tag === "Failure") return exit + else if (state.out) { + state.out[index] = exit.value + } + } +}) + +/* @internal */ +export const filterOrElse: { + ( + refinement: Predicate.Refinement, B>, + orElse: (a: EqualsWith, Exclude, B>>) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + refinement: Predicate.Refinement, + orElse: (a: EqualsWith>) => Effect.Effect + ): Effect.Effect + ( + self: Effect.Effect, + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + predicate: Predicate.Predicate, + orElse: (a: A) => Effect.Effect +): Effect.Effect => + flatMap( + self, + (a) => predicate(a) ? succeed(a) : orElse(a) + )) + +/** @internal */ +export const filterMapOrElse: { + ( + filter: Filter.Filter, B, X>, + orElse: (x: X) => Effect.Effect + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + filter: Filter.Filter, B, X>, + orElse: (x: X) => Effect.Effect + ): Effect.Effect +} = dual(3, ( + self: Effect.Effect, + filter: Filter.Filter, B, X>, + orElse: (x: X) => Effect.Effect +): Effect.Effect => + flatMap( + self, + (a) => { + const result = filter(a) + return (Result.isFailure(result) + ? orElse(result.failure) + : succeed(result.success)) as Effect.Effect + } + )) + +/* @internal */ +export const filterMapOrFail: { + ( + filter: Filter.Filter, B, X>, + orFailWith: (x: X) => E2 + ): (self: Effect.Effect) => Effect.Effect + ( + filter: Filter.Filter, B, X> + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + filter: Filter.Filter, B, X>, + orFailWith: (x: X) => E2 + ): Effect.Effect + ( + self: Effect.Effect, + filter: Filter.Filter, B, X> + ): Effect.Effect +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + filter: Filter.Filter, B, X>, + orFailWith?: (x: X) => E2 +): Effect.Effect => + filterMapOrElse( + self, + filter, + orFailWith ? (x: X) => fail(orFailWith(x)) : () => fail(new NoSuchElementError() as E2) + )) + +/** @internal */ +export const filter: { + ( + refinement: Predicate.Refinement, B> + ): (elements: Iterable) => Effect.Effect> + ( + predicate: Predicate.Predicate> + ): (elements: Iterable) => Effect.Effect> + ( + predicate: (a: NoInfer, i: number) => Effect.Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): (iterable: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + refinement: Predicate.Refinement + ): Effect.Effect> + ( + elements: Iterable, + predicate: Predicate.Predicate + ): Effect.Effect> + ( + iterable: Iterable, + predicate: (a: NoInfer, i: number) => Effect.Effect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect, E, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + predicate: + | Predicate.Predicate + | ((a: A, i: number) => Effect.Effect), + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect, E, R> => + suspend(() => { + const out: Array = [] + return as( + forEach( + elements, + (a, i) => { + const result = (predicate as Function)(a, i) + if (typeof result === "boolean") { + if (result) out.push(a) + return void_ as any + } + return map(result, (keep) => { + if (keep) { + out.push(a) + } + }) + }, + { + discard: true, + concurrency: options?.concurrency + } + ), + out + ) + }) +) + +/** @internal */ +export const filterMap: { + ( + filter: Filter.Filter, B, X> + ): (elements: Iterable) => Effect.Effect> + ( + elements: Iterable, + filter: Filter.Filter, B, X> + ): Effect.Effect> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + filter: Filter.Filter + ): Effect.Effect> => + suspend(() => { + const out: Array = [] + for (const a of elements) { + const result = filter(a) + if (Result.isSuccess(result)) { + out.push(result.success) + } + } + return succeed(out) + }) +) + +/** @internal */ +export const filterMapEffect: { + ( + filter: Filter.FilterEffect, B, X, E, R>, + options?: { readonly concurrency?: Concurrency | undefined } + ): (elements: Iterable) => Effect.Effect, E, R> + ( + elements: Iterable, + filter: Filter.FilterEffect, B, X, E, R>, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect, E, R> +} = dual( + (args) => isIterable(args[0]) && !isEffect(args[0]), + ( + elements: Iterable, + filter: Filter.FilterEffect, + options?: { readonly concurrency?: Concurrency | undefined } + ): Effect.Effect, E, R> => + suspend(() => { + const out: Array = [] + return as( + forEach( + elements, + (a) => + map(filter(a), (result) => { + if (Result.isSuccess(result)) { + out.push(result.success) + } + }), + { + discard: true, + concurrency: options?.concurrency + } + ), + out + ) + }) +) + +// ---------------------------------------------------------------------------- +// do notation +// ---------------------------------------------------------------------------- + +/** @internal */ +export const Do: Effect.Effect<{}> = succeed({}) + +/** @internal */ +export const bindTo: { + ( + name: N + ): ( + self: Effect.Effect + ) => Effect.Effect, E, R> + ( + self: Effect.Effect, + name: N + ): Effect.Effect, E, R> +} = doNotation.bindTo(map) + +/** @internal */ +export const bind: { + , B, E2, R2>( + name: N, + f: (a: NoInfer) => Effect.Effect + ): ( + self: Effect.Effect + ) => Effect.Effect & Record>, E | E2, R | R2> + , E, R, B, E2, R2, N extends string>( + self: Effect.Effect, + name: N, + f: (a: NoInfer) => Effect.Effect + ): Effect.Effect & Record>, E | E2, R | R2> +} = doNotation.bind(map, flatMap) + +/** @internal */ +const let_: { + , B>( + name: N, + f: (a: NoInfer) => B + ): ( + self: Effect.Effect + ) => Effect.Effect & Record>, E, R> + , E, R, B, N extends string>( + self: Effect.Effect, + name: N, + f: (a: NoInfer) => B + ): Effect.Effect & Record>, E, R> +} = doNotation.let_(map) + +/** @internal */ +export { let_ as let } + +// ---------------------------------------------------------------------------- +// fibers & forking +// ---------------------------------------------------------------------------- + +/** @internal */ +export const forkChild: { + < + Arg extends Effect.Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + >( + effectOrOptions: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined + ): [Arg] extends [Effect.Effect] ? Effect.Effect, never, _R> + : (self: Effect.Effect) => Effect.Effect, never, R> +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + options?: { + readonly startImmediately?: boolean + readonly uninterruptible?: boolean | "inherit" + } +): Effect.Effect, never, R> => + withFiber((fiber) => { + interruptChildrenPatch() + return succeed(forkUnsafe( + fiber, + self, + options?.startImmediately, + false, + options?.uninterruptible ?? false + )) + })) + +/** @internal */ +export const forkUnsafe = ( + parent: Fiber.Fiber, + effect: Effect.Effect, + immediate = false, + daemon = false, + uninterruptible: boolean | "inherit" = false +): FiberImpl => { + const interruptible = uninterruptible === "inherit" ? parent.interruptible : !uninterruptible + const child = new FiberImpl(parent.context, interruptible) + if (immediate) { + child.evaluate(effect as any) + } else { + parent.currentDispatcher.scheduleTask(() => child.evaluate(effect as any), 0) + } + if (!daemon && !child._exit) { + parent.children().add(child) + child.addObserver(() => parent._children!.delete(child)) + } + return child +} + +/** @internal */ +export const forkDetach: { + < + Arg extends Effect.Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + >( + effectOrOptions: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined + ): [Arg] extends [Effect.Effect] ? Effect.Effect, never, _R> + : (self: Effect.Effect) => Effect.Effect, never, R> +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + options?: { + readonly startImmediately?: boolean + readonly uninterruptible?: boolean | "inherit" | undefined + } +): Effect.Effect, never, R> => + withFiber((fiber) => succeed(forkUnsafe(fiber, self, options?.startImmediately, true, options?.uninterruptible)))) + +/** @internal */ +export const awaitAllChildren = ( + self: Effect.Effect +): Effect.Effect => + withFiber((fiber) => { + const initialChildren = fiber._children && Arr.fromIterable(fiber._children) + return onExit( + self, + (_) => { + let children = fiber._children + if (children === undefined || children.size === 0) { + return void_ + } else if (initialChildren) { + children = Iterable.filter( + children, + (child: FiberImpl) => !initialChildren.includes(child) + ) as Set> + } + return asVoid(fiberAwaitAll(children)) + } + ) + }) + +/** @internal */ +export const forkIn: { + ( + scope: Scope.Scope, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + ): ( + self: Effect.Effect + ) => Effect.Effect, never, R> + ( + self: Effect.Effect, + scope: Scope.Scope, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + ): Effect.Effect, never, R> +} = dual( + (args) => isEffect(args[0]), + ( + self: Effect.Effect, + scope: Scope.Scope, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + ): Effect.Effect, never, R> => + withFiber((parent) => { + const fiber = forkUnsafe(parent, self, options?.startImmediately, true, options?.uninterruptible) + if (!(fiber as FiberImpl)._exit) { + if (scope.state._tag !== "Closed") { + const key = {} + const finalizer = () => withFiberId((interruptor) => interruptor === fiber.id ? void_ : fiberInterrupt(fiber)) + scopeAddFinalizerUnsafe(scope, key, finalizer) + fiber.addObserver(() => scopeRemoveFinalizerUnsafe(scope, key)) + } else { + fiber.interruptUnsafe(parent.id, fiberStackAnnotations(parent)) + } + } + return succeed(fiber) + }) +) + +/** @internal */ +export const forkScoped: { + < + Arg extends Effect.Effect | { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined = { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } + >( + effectOrOptions: Arg, + options?: { + readonly startImmediately?: boolean | undefined + readonly uninterruptible?: boolean | "inherit" | undefined + } | undefined + ): [Arg] extends [Effect.Effect] ? + Effect.Effect, never, _R | Scope.Scope> + : (self: Effect.Effect) => Effect.Effect, never, R | Scope.Scope> +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + options?: { + readonly startImmediately?: boolean + readonly uninterruptible?: boolean | "inherit" + } +): Effect.Effect, never, R | Scope.Scope> => flatMap(scope, (scope) => forkIn(self, scope, options))) + +// ---------------------------------------------------------------------------- +// execution +// ---------------------------------------------------------------------------- + +/** @internal */ +export const runForkWith = (context: Context.Context) => +( + effect: Effect.Effect, + options?: Effect.RunOptions | undefined +): Fiber.Fiber => { + const fiber = new FiberImpl( + options?.scheduler ? Context.add(context, Scheduler.Scheduler, options.scheduler) : context, + options?.uninterruptible !== true + ) + fiber.evaluate(effect as any) + if (fiber._exit) return fiber + + if (options?.signal) { + if (options.signal.aborted) { + fiber.interruptUnsafe() + } else { + const abort = () => fiber.interruptUnsafe() + options.signal.addEventListener("abort", abort, { once: true }) + fiber.addObserver(() => options.signal!.removeEventListener("abort", abort)) + } + } + if (options?.onFiberStart) { + options.onFiberStart(fiber) + } + return fiber +} + +/** @internal */ +export const fiberRunIn: { + (scope: Scope.Scope): (self: Fiber.Fiber) => Fiber.Fiber + ( + self: Fiber.Fiber, + scope: Scope.Scope + ): Fiber.Fiber +} = dual(2, ( + self: FiberImpl, + scope: Scope.Scope +): Fiber.Fiber => { + if (self._exit) { + return self + } else if (scope.state._tag === "Closed") { + self.interruptUnsafe(self.id) + return self + } + const key = {} + scopeAddFinalizerUnsafe(scope, key, () => fiberInterrupt(self)) + self.addObserver(() => scopeRemoveFinalizerUnsafe(scope, key)) + return self +}) + +/** @internal */ +export const runFork: ( + effect: Effect.Effect, + options?: Effect.RunOptions | undefined +) => Fiber.Fiber = runForkWith(Context.empty()) + +/** @internal */ +export const runCallbackWith = (context: Context.Context) => { + const runFork = runForkWith(context) + return ( + effect: Effect.Effect, + options?: + | Effect.RunOptions & { + readonly onExit: (exit: Exit.Exit) => void + } + | undefined + ): (interruptor?: number | undefined) => void => { + const fiber = runFork(effect, options) + if (options?.onExit) { + fiber.addObserver(options.onExit) + } + return (interruptor) => { + return fiber.interruptUnsafe(interruptor) + } + } +} + +/** @internal */ +export const runCallback = runCallbackWith(Context.empty()) + +/** @internal */ +export const runPromiseExitWith = (context: Context.Context) => { + const runFork = runForkWith(context) + return ( + effect: Effect.Effect, + options?: Effect.RunOptions | undefined + ): Promise> => { + const fiber = runFork(effect, options) + return new Promise((resolve) => { + fiber.addObserver((exit) => resolve(exit)) + }) + } +} + +/** @internal */ +export const runPromiseExit = runPromiseExitWith(Context.empty()) + +/** @internal */ +export const runPromiseWith = (context: Context.Context) => { + const runPromiseExit = runPromiseExitWith(context) + return ( + effect: Effect.Effect, + options?: + | Effect.RunOptions + | undefined + ): Promise => + runPromiseExit(effect, options).then((exit) => { + if (exit._tag === "Failure") { + throw causeSquash(exit.cause) + } + return exit.value + }) +} + +/** @internal */ +export const runPromise: ( + effect: Effect.Effect, + options?: + | Effect.RunOptions + | undefined +) => Promise = runPromiseWith(Context.empty()) + +/** @internal */ +export const runSyncExitWith = (context: Context.Context) => { + const runFork = runForkWith(context) + return (effect: Effect.Effect): Exit.Exit => { + if (effectIsExit(effect)) return effect + const scheduler = new Scheduler.MixedScheduler("sync") + const fiber = runFork(effect, { scheduler }) + fiber.currentDispatcher?.flush() + return (fiber as FiberImpl)._exit ?? exitDie(new AsyncFiberError(fiber)) + } +} + +/** @internal */ +export const runSyncExit: (effect: Effect.Effect) => Exit.Exit = runSyncExitWith( + Context.empty() +) + +/** @internal */ +export const runSyncWith = (context: Context.Context) => { + const runSyncExit = runSyncExitWith(context) + return (effect: Effect.Effect): A => { + const exit = runSyncExit(effect) + if (exit._tag === "Failure") throw causeSquash(exit.cause) + return exit.value + } +} + +/** @internal */ +export const runSync: (effect: Effect.Effect) => A = runSyncWith(Context.empty()) + +const succeedTrue = succeed(true) +const succeedFalse = succeed(false) + +class Latch implements _Latch.Latch { + waiters: Array<(_: Effect.Effect) => void> = [] + scheduled = false + private _isOpen: boolean + + constructor(isOpen: boolean) { + this._isOpen = isOpen + } + + private scheduleUnsafe(fiber: Fiber.Fiber) { + if (this.scheduled || this.waiters.length === 0) { + return succeedTrue + } + this.scheduled = true + fiber.currentDispatcher.scheduleTask(this.flushWaiters, 0) + return succeedTrue + } + private flushWaiters = () => { + this.scheduled = false + const waiters = this.waiters + this.waiters = [] + for (let i = 0; i < waiters.length; i++) { + waiters[i](exitVoid) + } + } + + open = withFiber((fiber) => { + if (this._isOpen) return succeedFalse + this._isOpen = true + return this.scheduleUnsafe(fiber) + }) + release = withFiber((fiber) => this._isOpen ? succeedFalse : this.scheduleUnsafe(fiber)) + openUnsafe() { + if (this._isOpen) return false + this._isOpen = true + this.flushWaiters() + return true + } + await = callback((resume) => { + if (this._isOpen) { + return resume(void_) + } + this.waiters.push(resume) + return sync(() => { + const index = this.waiters.indexOf(resume) + if (index !== -1) { + this.waiters.splice(index, 1) + } + }) + }) + closeUnsafe() { + if (!this._isOpen) return false + this._isOpen = false + return true + } + close = sync(() => this.closeUnsafe()) + whenOpen = (self: Effect.Effect): Effect.Effect => flatMap(this.await, () => self) + isOpen() { + return this._isOpen + } +} + +/** @internal */ +export const makeLatchUnsafe = (open?: boolean | undefined): _Latch.Latch => new Latch(open ?? false) + +/** @internal */ +export const makeLatch = (open?: boolean | undefined) => sync(() => makeLatchUnsafe(open)) + +// ---------------------------------------------------------------------------- +// Tracer +// ---------------------------------------------------------------------------- + +/** @internal */ +export const tracer: Effect.Effect = withFiber((fiber) => succeed(fiber.getRef(Tracer.Tracer))) + +/** @internal */ +export const withTracer: { + (tracer: Tracer.Tracer): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, tracer: Tracer.Tracer): Effect.Effect +} = dual( + 2, + (effect: Effect.Effect, tracer: Tracer.Tracer): Effect.Effect => + provideService(effect, Tracer.Tracer, tracer) +) + +/** @internal */ +export const withTracerEnabled: { + (enabled: boolean): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, enabled: boolean): Effect.Effect +} = provideService(TracerEnabled) + +/** @internal */ +export const withTracerTiming: { + (enabled: boolean): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, enabled: boolean): Effect.Effect +} = provideService(TracerTimingEnabled) + +const bigint0 = BigInt(0) + +const NoopSpanProto: Omit = { + _tag: "Span", + spanId: "noop", + traceId: "noop", + sampled: false, + status: { + _tag: "Ended", + startTime: bigint0, + endTime: bigint0, + exit: exitVoid + }, + attributes: new Map(), + links: [], + kind: "internal", + attribute() {}, + event() {}, + end() {}, + addLinks() {} +} + +/** @internal */ +export const noopSpan = (options: { + readonly name: string + readonly parent: Option.Option + readonly annotations: Context.Context +}): Tracer.Span => Object.assign(Object.create(NoopSpanProto), options) + +const filterDisablePropagation = (span: Tracer.AnySpan | undefined): Option.Option => { + if (!span) return Option.none() + return Context.get(span.annotations, Tracer.DisablePropagation) + ? span._tag === "Span" ? filterDisablePropagation(Option.getOrUndefined(span.parent)) : Option.none() + : Option.some(span) +} + +/** @internal */ +export const makeSpanUnsafe = ( + fiber: Fiber.Fiber, + name: string, + options: Tracer.SpanOptionsNoTrace | undefined +) => { + const disablePropagation = !fiber.getRef(TracerEnabled) || + (options?.annotations && Context.get(options.annotations, Tracer.DisablePropagation)) + const parent = options?.parent !== undefined + ? Option.some(options.parent) + : options?.root + ? Option.none() + : filterDisablePropagation(fiber.currentSpan) + + let span: Tracer.Span + + if (disablePropagation) { + span = noopSpan({ + name, + parent, + annotations: Context.add( + options?.annotations ?? Context.empty(), + Tracer.DisablePropagation, + true + ) + }) + } else { + const tracer = fiber.getRef(Tracer.Tracer) + const clock = fiber.getRef(ClockRef) + const timingEnabled = fiber.getRef(TracerTimingEnabled) + const annotationsFromEnv = fiber.getRef(TracerSpanAnnotations) + const linksFromEnv = fiber.getRef(TracerSpanLinks) + const level = options?.level ?? fiber.getRef(Tracer.CurrentTraceLevel) + + const links = options?.links !== undefined ? + [...linksFromEnv, ...options.links] : + linksFromEnv.slice() + + span = tracer.span({ + name, + parent, + annotations: options?.annotations ?? Context.empty(), + links, + startTime: timingEnabled ? clock.currentTimeNanosUnsafe() : BigInt(0), + kind: options?.kind ?? "internal", + root: options?.root ?? Option.isNone(parent), + sampled: options?.sampled ?? + (Option.isSome(parent) && parent.value.sampled === false + ? false + : !isLogLevelGreaterThan(fiber.getRef(Tracer.MinimumTraceLevel), level)) + }) + + for (const [key, value] of Object.entries(annotationsFromEnv)) { + span.attribute(key, value) + } + if (options?.attributes !== undefined) { + for (const [key, value] of Object.entries(options.attributes)) { + span.attribute(key, value) + } + } + } + + return span +} + +/** @internal */ +export const makeSpan = ( + name: string, + options?: Tracer.SpanOptions +): Effect.Effect => withFiber((fiber) => succeed(makeSpanUnsafe(fiber, name, options))) + +/** @internal */ +export const makeSpanScoped = ( + name: string, + options?: Tracer.SpanOptionsNoTrace | undefined +): Effect.Effect => + uninterruptible( + withFiber((fiber) => { + const scope = Context.getUnsafe(fiber.context, scopeTag) + const span = makeSpanUnsafe(fiber, name, options ?? {}) + const clock = fiber.getRef(ClockRef) + const timingEnabled = fiber.getRef(TracerTimingEnabled) + return as( + scopeAddFinalizerExit(scope, (exit) => endSpan(span, exit, clock, timingEnabled)), + span + ) + }) + ) + +/** @internal */ +export const withSpanScoped: { + ( + name: string, + options?: Tracer.SpanOptions + ): (self: Effect.Effect) => Effect.Effect | Scope.Scope> + ( + self: Effect.Effect, + name: string, + options?: Tracer.SpanOptions + ): Effect.Effect | Scope.Scope> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const options = addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) + if (dataFirst) { + const self = arguments[0] + return flatMap( + makeSpanScoped(name, options), + (span) => withParentSpan(self, span, options) + ) + } + return (self: Effect.Effect) => + flatMap( + makeSpanScoped(name, options), + (span) => withParentSpan(self, span, options) + ) +} as any + +const provideSpanStackFrame = (name: string, stack: (() => string | undefined) | undefined) => { + stack = typeof stack === "function" ? stack : constUndefined + return updateService(CurrentStackFrame, (parent) => ({ + name, + stack, + parent + })) +} + +/** @internal */ +export const spanAnnotations: Effect.Effect>> = TracerSpanAnnotations + +/** @internal */ +export const spanLinks: Effect.Effect> = TracerSpanLinks + +/** @internal */ +export const linkSpans: { + ( + span: Tracer.AnySpan | ReadonlyArray, + attributes?: Record + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + span: Tracer.AnySpan | ReadonlyArray, + attributes?: Record + ): Effect.Effect +} = dual((args) => isEffect(args[0]), ( + self: Effect.Effect, + span: Tracer.AnySpan | ReadonlyArray, + attributes: Record = {} +): Effect.Effect => { + const spans: Array = Array.isArray(span) ? span : [span] + const links = spans.map((span): Tracer.SpanLink => ({ span, attributes })) + return updateService(self, TracerSpanLinks, (current) => [...current, ...links]) +}) + +/** @internal */ +export const endSpan = ( + span: Tracer.Span, + exit: Exit.Exit, + clock: Clock.Clock, + timingEnabled: boolean +) => + sync(() => { + if (span.status._tag === "Ended") return + span.end(timingEnabled ? clock.currentTimeNanosUnsafe() : bigint0, exit) + }) + +/** @internal */ +export const useSpan: { + (name: string, evaluate: (span: Tracer.Span) => Effect.Effect): Effect.Effect + ( + name: string, + options: Tracer.SpanOptionsNoTrace, + evaluate: (span: Tracer.Span) => Effect.Effect + ): Effect.Effect +} = ( + name: string, + ...args: [evaluate: (span: Tracer.Span) => Effect.Effect] | [ + options: any, + evaluate: (span: Tracer.Span) => Effect.Effect + ] +): Effect.Effect => { + const options = args.length === 1 ? undefined : args[0] + const evaluate: (span: Tracer.Span) => Effect.Effect = args[args.length - 1] + return withFiber((fiber) => { + const span = makeSpanUnsafe(fiber, name, options) + const clock = fiber.getRef(ClockRef) + return onExit(internalCall(() => evaluate(span)), (exit) => + sync(() => { + if (span.status._tag === "Ended") return + span.end(clock.currentTimeNanosUnsafe(), exit) + })) + }) +} + +const provideParentSpan = provideService(Tracer.ParentSpan) + +/** @internal */ +export const withParentSpan: { + ( + value: Tracer.AnySpan, + options?: Tracer.TraceOptions + ): (self: Effect.Effect) => Effect.Effect> + ( + self: Effect.Effect, + value: Tracer.AnySpan, + options?: Tracer.TraceOptions + ): Effect.Effect> +} = function() { + const dataFirst = isEffect(arguments[0]) + const span: Tracer.AnySpan = dataFirst ? arguments[1] : arguments[0] + let options = dataFirst ? arguments[2] : arguments[1] + let provideStackFrame: (self: Effect.Effect) => Effect.Effect = identity + if (span._tag === "Span") { + options = addSpanStackTrace(options) + provideStackFrame = provideSpanStackFrame(span.name, options?.captureStackTrace) + } + if (dataFirst) { + return provideParentSpan(provideStackFrame(arguments[0]), span) + } + return (self: Effect.Effect) => provideParentSpan(provideStackFrame(self), span) +} as any + +/** @internal */ +export const withSpan: { + >( + name: string, + options?: Tracer.SpanOptionsNoTrace | ((...args: NoInfer) => Tracer.SpanOptionsNoTrace) | undefined, + traceOptions?: Tracer.TraceOptions | undefined + ): (self: Effect.Effect, ...args: Args) => Effect.Effect> + ( + self: Effect.Effect, + name: string, + options?: Tracer.SpanOptions | undefined + ): Effect.Effect> +} = function() { + const dataFirst = typeof arguments[0] !== "string" + const name = dataFirst ? arguments[1] : arguments[0] + const traceOptions = addSpanStackTrace(arguments[2]) + if (dataFirst) { + const self = arguments[0] + return useSpan(name, arguments[2], (span) => withParentSpan(self, span, traceOptions)) + } + const fnArg = typeof arguments[1] === "function" ? arguments[1] : undefined + const options = fnArg ? undefined : arguments[1] + return (self: Effect.Effect, ...args: any) => + useSpan( + name, + fnArg ? fnArg(...args) : options, + (span) => withParentSpan(self, span, traceOptions) + ) +} as any + +/** @internal */ +export const annotateSpans: { + (key: string, value: unknown): (effect: Effect.Effect) => Effect.Effect + (values: Record): (effect: Effect.Effect) => Effect.Effect + (effect: Effect.Effect, key: string, value: unknown): Effect.Effect + (effect: Effect.Effect, values: Record): Effect.Effect +} = dual( + (args) => isEffect(args[0]), + ( + effect: Effect.Effect, + ...args: [Record] | [key: string, value: unknown] + ): Effect.Effect => + updateService(effect, TracerSpanAnnotations, (annotations) => { + const newAnnotations = { ...annotations } + if (args.length === 1) { + Object.assign(newAnnotations, args[0]) + } else { + newAnnotations[args[0]] = args[1] + } + return newAnnotations + }) +) + +/** @internal */ +export const annotateCurrentSpan: { + (key: string, value: unknown): Effect.Effect + (values: Record): Effect.Effect +} = (...args: [Record] | [key: string, value: unknown]) => + withFiber((fiber) => { + const span = fiber.currentSpanLocal + if (span) { + if (args.length === 1) { + for (const [key, value] of Object.entries(args[0])) { + span.attribute(key, value) + } + } else { + span.attribute(args[0], args[1]) + } + } + return void_ + }) + +/** @internal */ +export const currentSpan: Effect.Effect = withFiber((fiber) => { + const span = fiber.currentSpanLocal + return span ? succeed(span) : fail(new NoSuchElementError()) +}) + +/** @internal */ +export const currentParentSpan: Effect.Effect = serviceOptional( + Tracer.ParentSpan +) + +// ---------------------------------------------------------------------------- +// Clock +// ---------------------------------------------------------------------------- + +/** @internal */ +export const ClockRef = Context.Reference("effect/Clock", { + defaultValue: (): Clock.Clock => new ClockImpl() +}) + +const MAX_TIMER_MILLIS = 2 ** 31 - 1 + +class ClockImpl implements Clock.Clock { + currentTimeMillisUnsafe(): number { + return Date.now() + } + readonly currentTimeMillis: Effect.Effect = sync(() => this.currentTimeMillisUnsafe()) + currentTimeNanosUnsafe(): bigint { + return processOrPerformanceNow() + } + readonly currentTimeNanos: Effect.Effect = sync(() => this.currentTimeNanosUnsafe()) + sleep(duration: Duration.Duration): Effect.Effect { + return this.sleepMillis(Duration.toMillis(duration)) + } + private sleepMillis(millis: number): Effect.Effect { + if (millis <= 0) return yieldNow + else if (!Number.isFinite(millis)) return never + return callback((resume) => { + const continuation = millis > MAX_TIMER_MILLIS + ? this.sleepMillis(millis - MAX_TIMER_MILLIS) + : void_ + const handle = setTimeout(() => resume(continuation), Math.min(millis, MAX_TIMER_MILLIS)) + return sync(() => clearTimeout(handle)) + }) + } +} + +const performanceNowNanos = (function() { + const bigint1e6 = BigInt(1_000_000) + if (typeof performance === "undefined" || typeof performance.now === "undefined") { + return () => BigInt(Date.now()) * bigint1e6 + } + let origin: bigint + return () => { + origin ??= (BigInt(Date.now()) * bigint1e6) - BigInt(Math.round(performance.now() * 1_000_000)) + return origin + BigInt(Math.round(performance.now() * 1_000_000)) + } +})() +const processOrPerformanceNow = (function() { + const processHrtime = + typeof process === "object" && "hrtime" in process && typeof process.hrtime.bigint === "function" ? + process.hrtime : + undefined + if (!processHrtime) { + return performanceNowNanos + } + const origin = (BigInt(Date.now()) * BigInt(1e6)) - processHrtime.bigint() + return () => origin + processHrtime.bigint() +})() + +/** @internal */ +export const clockWith = (f: (clock: Clock.Clock) => Effect.Effect): Effect.Effect => + withFiber((fiber) => f(fiber.getRef(ClockRef))) + +/** @internal */ +export const sleep = (duration: Duration.Input): Effect.Effect => + clockWith((clock) => clock.sleep(Duration.fromInputUnsafe(duration))) + +/** @internal */ +export const currentTimeMillis: Effect.Effect = clockWith((clock) => clock.currentTimeMillis) + +/** @internal */ +export const currentTimeNanos: Effect.Effect = clockWith((clock) => clock.currentTimeNanos) + +// ---------------------------------------------------------------------------- +// Errors +// ---------------------------------------------------------------------------- + +/** @internal */ +export const TimeoutErrorTypeId = "~effect/Cause/TimeoutError" + +/** @internal */ +export const isTimeoutError = (u: unknown): u is Cause.TimeoutError => hasProperty(u, TimeoutErrorTypeId) + +/** @internal */ +export class TimeoutError extends TaggedError("TimeoutError") { + readonly [TimeoutErrorTypeId] = TimeoutErrorTypeId + constructor(message?: string) { + super({ message } as any) + } +} + +/** @internal */ +export const IllegalArgumentErrorTypeId = "~effect/Cause/IllegalArgumentError" + +/** @internal */ +export const isIllegalArgumentError = ( + u: unknown +): u is Cause.IllegalArgumentError => hasProperty(u, IllegalArgumentErrorTypeId) + +/** @internal */ +export class IllegalArgumentError extends TaggedError("IllegalArgumentError") { + readonly [IllegalArgumentErrorTypeId] = IllegalArgumentErrorTypeId + constructor(message?: string) { + super({ message } as any) + } +} + +/** @internal */ +export const ExceededCapacityErrorTypeId = "~effect/Cause/ExceededCapacityError" + +/** @internal */ +export const isExceededCapacityError = ( + u: unknown +): u is Cause.ExceededCapacityError => hasProperty(u, ExceededCapacityErrorTypeId) + +/** @internal */ +export class ExceededCapacityError extends TaggedError("ExceededCapacityError") { + readonly [ExceededCapacityErrorTypeId] = ExceededCapacityErrorTypeId + constructor(message?: string) { + super({ message } as any) + } +} + +/** @internal */ +export const AsyncFiberErrorTypeId = "~effect/Cause/AsyncFiberError" + +/** @internal */ +export const isAsyncFiberError = ( + u: unknown +): u is Cause.AsyncFiberError => hasProperty(u, AsyncFiberErrorTypeId) + +/** @internal */ +export class AsyncFiberError extends TaggedError("AsyncFiberError")<{ + fiber: Fiber.Fiber + message: string +}> { + readonly [AsyncFiberErrorTypeId] = AsyncFiberErrorTypeId + constructor(fiber: Fiber.Fiber) { + super({ + message: "An asynchronous Effect was executed with Effect.runSync", + fiber + }) + } +} + +/** @internal */ +export const UnknownErrorTypeId = "~effect/Cause/UnknownError" + +/** @internal */ +export const isUnknownError = ( + u: unknown +): u is Cause.UnknownError => hasProperty(u, UnknownErrorTypeId) + +/** @internal */ +export class UnknownError extends TaggedError("UnknownError")<{ + cause: unknown + message?: string | undefined +}> { + readonly [UnknownErrorTypeId] = UnknownErrorTypeId + constructor(cause: unknown, message?: string) { + super({ message, cause } as any) + } +} + +// ---------------------------------------------------------------------------- +// Console +// ---------------------------------------------------------------------------- + +/** @internal */ +export const ConsoleRef = Context.Reference( + "effect/Console/CurrentConsole", + { defaultValue: (): Console.Console => globalThis.console } +) + +// ---------------------------------------------------------------------------- +// LogLevel +// ---------------------------------------------------------------------------- + +/** @internal */ +export const logLevelToOrder = (level: LogLevel.LogLevel) => { + switch (level) { + case "All": + return Number.MIN_SAFE_INTEGER + case "Fatal": + return 50_000 + case "Error": + return 40_000 + case "Warn": + return 30_000 + case "Info": + return 20_000 + case "Debug": + return 10_000 + case "Trace": + return 0 + case "None": + return Number.MAX_SAFE_INTEGER + } +} + +/** @internal */ +export const LogLevelOrder = Order.mapInput(Order.Number, logLevelToOrder) + +/** @internal */ +export const isLogLevelGreaterThan = Order.isGreaterThan(LogLevelOrder) + +// ---------------------------------------------------------------------------- +// Logger +// ---------------------------------------------------------------------------- + +/** @internal */ +export const CurrentLoggers = Context.Reference< + ReadonlySet> +>("effect/Loggers/CurrentLoggers", { + defaultValue: () => new Set([defaultLogger, tracerLogger]) +}) + +/** @internal */ +export const LogToStderr = Context.Reference("effect/Logger/LogToStderr", { + defaultValue: constFalse +}) + +/** @internal */ +export const annotateLogsScoped: { + (key: string, value: unknown): Effect.Effect + (values: Record): Effect.Effect +} = function() { + const entries = typeof arguments[0] === "string" ? + [[arguments[0], arguments[1]]] : + Object.entries(arguments[0]) + return uninterruptible(withFiber((fiber) => { + const prev = fiber.getRef(CurrentLogAnnotations) + const next = { ...prev } + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i] + next[key] = value + } + fiber.setContext(Context.add(fiber.context, CurrentLogAnnotations, next)) + return scopeAddFinalizerExit(Context.getUnsafe(fiber.context, scopeTag), (_) => { + const current = fiber.getRef(CurrentLogAnnotations) + const next = { ...current } + for (let i = 0; i < entries.length; i++) { + const [key, value] = entries[i] + if (current[key] !== value) continue + if (key in prev) { + next[key] = prev[key] + } else { + delete next[key] + } + } + fiber.setContext(Context.add(fiber.context, CurrentLogAnnotations, next)) + return void_ + }) + })) +} + +/** @internal */ +export const LoggerTypeId = "~effect/Logger" + +const LoggerProto = { + [LoggerTypeId]: { + _Message: identity, + _Output: identity + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const loggerMake = ( + log: (options: Logger.Options) => Output +): Logger.Logger => { + const self = Object.create(LoggerProto) + self.log = log + return self +} + +/** + * Sanitize a given string by replacing spaces, equal signs, and double quotes + * with underscores. + * + * @internal + */ +export const formatLabel = (key: string) => key.replace(/[\s="]/g, "_") + +/** + * Formats a log span into a `

, f: Fn] +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminator + +/** + * Matches values where a specified field starts with a given prefix. + * + * **When to use** + * + * Use to match string discriminator values by prefix instead of exact value. + * + * **Details** + * + * Instead of checking for exact matches, this helper matches values that share + * a common prefix. For example, if the discriminant field contains hierarchical + * names like `"A"`, `"A.A"`, and `"B"`, a single `"A"` rule can match both + * `"A"` and `"A.A"`. + * + * **Example** (Matching discriminator prefixes) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type<{ type: "A" } | { type: "B" } | { type: "A.A" } | {}>(), + * Match.discriminatorStartsWith("type")("A", (_) => 1 as const), + * Match.discriminatorStartsWith("type")("B", (_) => 2 as const), + * Match.orElse((_) => 3 as const) + * ) + * + * console.log(match({ type: "A" })) // 1 + * console.log(match({ type: "B" })) // 2 + * console.log(match({ type: "A.A" })) // 1 + * ``` + * + * @see {@link discriminator} for matching exact discriminator values + * + * @category Defining patterns + * @since 4.0.0 + */ +export const discriminatorStartsWith: ( + field: D +) => >) => Ret>( + pattern: P, + f: Fn +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminatorStartsWith + +/** + * Matches values based on a field that serves as a discriminator, mapping each + * possible value to a corresponding handler. + * + * **When to use** + * + * Use to define several discriminator handlers at once without finalizing the + * matcher. + * + * **Details** + * + * This function simplifies working with discriminated unions by letting you + * define a set of handlers for each possible value of a given field. Instead of + * chaining multiple calls to {@link discriminator}, this function allows + * defining all possible cases at once using an object where the keys are the + * possible values of the field, and the values are the corresponding handler + * functions. + * + * **Example** (Mapping discriminator handlers) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type< + * { type: "A"; a: string } | { type: "B"; b: number } | { + * type: "C" + * c: boolean + * } + * >(), + * Match.discriminators("type")({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }), + * Match.exhaustive + * ) + * ``` + * + * @see {@link discriminator} for adding one discriminator case to a matcher pipeline + * @see {@link discriminatorsExhaustive} for handling every discriminator value and finalizing the matcher + * + * @category Defining patterns + * @since 4.0.0 + */ +export const discriminators: ( + field: D +) => < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags & string]?: ((_: Extract>) => Ret) | undefined } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | ReturnType, + Pr, + Ret +> = internal.discriminators + +/** + * Matches values by a discriminator field and requires every possible case to + * be handled. + * + * **When to use** + * + * Use to define an exhaustive discriminator handler map that finalizes the + * matcher. + * + * **Details** + * + * This is the exhaustive variant of {@link discriminators}. Each possible + * discriminator value must have a corresponding handler, so the matcher is + * finalized directly and does not require `Match.exhaustive` at the end of the + * pipeline. + * + * **Example** (Handling all discriminator cases) + * + * ```ts + * import { Match, pipe } from "effect" + * + * const match = pipe( + * Match.type< + * { type: "A"; a: string } | { type: "B"; b: number } | { + * type: "C" + * c: boolean + * } + * >(), + * Match.discriminatorsExhaustive("type")({ + * A: (a) => a.a, + * B: (b) => b.b, + * C: (c) => c.c + * }) + * ) + * ``` + * + * @see {@link discriminators} for defining discriminator handlers without finalizing the matcher + * + * @category Defining patterns + * @since 4.0.0 + */ +export const discriminatorsExhaustive: ( + field: D +) => < + R, + Ret, + P extends + & { readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret } + & { readonly [Tag in Exclude>]: never } +>( + fields: P +) => ( + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.discriminatorsExhaustive + +/** + * Matches discriminated union members by their `_tag` field. + * + * **When to use** + * + * Use to handle one or more `_tag` cases with the same matcher branch. + * + * **Details** + * + * This helper follows the Effect convention that discriminated unions use + * `"_tag"` as their discriminator field. Use {@link discriminator} for a + * different discriminator field. + * + * **Example** (Matching a discriminated union by tag) + * + * ```ts + * import { Match } from "effect" + * + * type Event = + * | { readonly _tag: "fetch" } + * | { readonly _tag: "success"; readonly data: string } + * | { readonly _tag: "error"; readonly error: Error } + * | { readonly _tag: "cancel" } + * + * const match = Match.type().pipe( + * // Match either "fetch" or "success" + * Match.tag("fetch", "success", () => `Ok!`), + * // Match "error" and extract the error message + * Match.tag("error", (event) => `Error: ${event.error.message}`), + * // Match "cancel" + * Match.tag("cancel", () => "Cancelled"), + * Match.exhaustive + * ) + * + * console.log(match({ _tag: "success", data: "Hello" })) + * // Output: "Ok!" + * + * console.log(match({ _tag: "error", error: new Error("Oops!") })) + * // Output: "Error: Oops!" + * ``` + * + * @category Defining patterns + * @since 4.0.0 + */ +export const tag: < + R, + P extends Types.Tags<"_tag", R> & string, + Ret, + Fn extends (_: Extract>) => Ret +>( + ...pattern: [first: P, ...values: Array